From 85a4a76481df5fc0b47f03b656582e675ce81fb1 Mon Sep 17 00:00:00 2001 From: Tanner Date: Fri, 5 Apr 2024 11:30:04 -0600 Subject: [PATCH 01/15] Finish UI for toggling "snap to canvas edge" behavior A menu is now provided (`View > Snap to canvas edge`), and custom snap distance is changeable via `Tools > Options > Interface`. A lot of code is required to provide a UI for this feature, since snap settings need to be persistent between sessions, but still easily toggleable in a top-level menu. Next up: actually implementing the snap feature (sigh). --- Forms/MainWindow.frm | 12 ++++++++++++ Forms/Tools_Options.frm | 42 ++++++++++++++++++++++++++++++++++------- Modules/Actions.bas | 19 +++++++++++-------- Modules/Interface.bas | 24 +++++++++++++++++++++++ Modules/Menus.bas | 2 ++ Modules/MoveTool.bas | 39 +++++++++++++++++++++++++++++++++++--- Modules/UserPrefs.bas | 5 ++++- PhotoDemon.vbp | 2 +- 8 files changed, 125 insertions(+), 20 deletions(-) diff --git a/Forms/MainWindow.frm b/Forms/MainWindow.frm index 5926dba6f..0dc73d9f1 100644 --- a/Forms/MainWindow.frm +++ b/Forms/MainWindow.frm @@ -1729,6 +1729,14 @@ Begin VB.Form FormMain Caption = "Show status bar" Index = 7 End + Begin VB.Menu MnuView + Caption = "-" + Index = 8 + End + Begin VB.Menu MnuView + Caption = "Snap to canvas edges" + Index = 9 + End End Begin VB.Menu MnuWindowTop Caption = "Window" @@ -3828,6 +3836,10 @@ Private Sub MnuView_Click(Index As Integer) Actions.LaunchAction_ByName "view_rulers" Case 7 Actions.LaunchAction_ByName "view_statusbar" + Case 8 + '(separator) + Case 9 + Actions.LaunchAction_ByName "snap_canvasedge" End Select End Sub diff --git a/Forms/Tools_Options.frm b/Forms/Tools_Options.frm index 3279ab46c..0aca45f75 100644 --- a/Forms/Tools_Options.frm +++ b/Forms/Tools_Options.frm @@ -53,10 +53,23 @@ Begin VB.Form FormOptions Width = 8295 _ExtentX = 14631 _ExtentY = 11853 + Begin PhotoDemon.pdSpinner spnSnapDistance + Height = 375 + Left = 120 + TabIndex = 44 + Top = 4560 + Width = 1935 + _ExtentX = 3413 + _ExtentY = 661 + DefaultValue = 8 + Min = 1 + Max = 255 + Value = 8 + End Begin PhotoDemon.pdPictureBox picGrid Height = 735 Left = 150 - Top = 4530 + Top = 5610 Width = 735 _ExtentX = 1296 _ExtentY = 1296 @@ -133,7 +146,7 @@ Begin VB.Form FormOptions Height = 810 Left = 1080 TabIndex = 2 - Top = 4500 + Top = 5580 Width = 3015 _ExtentX = 5318 _ExtentY = 1429 @@ -144,7 +157,7 @@ Begin VB.Form FormOptions Height = 795 Left = 4140 TabIndex = 4 - Top = 4500 + Top = 5580 Width = 3015 _ExtentX = 5318 _ExtentY = 1402 @@ -155,7 +168,7 @@ Begin VB.Form FormOptions Height = 690 Left = 7260 TabIndex = 5 - Top = 4560 + Top = 5640 Width = 465 _ExtentX = 820 _ExtentY = 1217 @@ -165,7 +178,7 @@ Begin VB.Form FormOptions Height = 690 Left = 7770 TabIndex = 6 - Top = 4560 + Top = 5640 Width = 465 _ExtentX = 820 _ExtentY = 1217 @@ -175,7 +188,7 @@ Begin VB.Form FormOptions Height = 285 Index = 2 Left = 0 - Top = 4080 + Top = 5160 Width = 8205 _ExtentX = 14473 _ExtentY = 503 @@ -205,6 +218,18 @@ Begin VB.Form FormOptions ForeColor = 4210752 Layout = 2 End + Begin PhotoDemon.pdLabel lblTitle + Height = 285 + Index = 23 + Left = 0 + Top = 4080 + Width = 8100 + _ExtentX = 14288 + _ExtentY = 503 + Caption = "snap distance (in pixels)" + FontSize = 12 + ForeColor = 4210752 + End End Begin PhotoDemon.pdContainer picContainer Height = 6720 @@ -1180,10 +1205,12 @@ Private Sub cmdBarMini_OKClick() g_RecentMacros.MRU_NotifyNewMaxLimit End If + UserPrefs.SetPref_Long "Interface", "snap-distance", spnSnapDistance.Value + Tools_Move.SetSnapDistance spnSnapDistance.Value + UserPrefs.SetPref_Long "Transparency", "Alpha Check Mode", CLng(cboAlphaCheck.ListIndex) UserPrefs.SetPref_Long "Transparency", "Alpha Check One", CLng(csAlphaOne.Color) UserPrefs.SetPref_Long "Transparency", "Alpha Check Two", CLng(csAlphaTwo.Color) - UserPrefs.SetPref_Long "Transparency", "Alpha Check Size", cboAlphaCheckSize.ListIndex Drawing.CreateAlphaCheckerboardDIB g_CheckerboardPattern @@ -1408,6 +1435,7 @@ Private Sub LoadAllPreferences() csCanvasColor.Color = UserPrefs.GetCanvasColor() tudRecentFiles.Value = UserPrefs.GetPref_Long("Interface", "Recent Files Limit", 10) btsMRUStyle.ListIndex = UserPrefs.GetPref_Long("Interface", "MRU Caption Length", 0) + spnSnapDistance.Value = UserPrefs.GetPref_Long("Interface", "snap-distance", 8&) m_userInitiatedAlphaSelection = False cboAlphaCheck.ListIndex = UserPrefs.GetPref_Long("Transparency", "Alpha Check Mode", 0) csAlphaOne.Color = UserPrefs.GetPref_Long("Transparency", "Alpha Check One", RGB(255, 255, 255)) diff --git a/Modules/Actions.bas b/Modules/Actions.bas index 94b676b52..a4cfb1492 100644 --- a/Modules/Actions.bas +++ b/Modules/Actions.bas @@ -1299,6 +1299,7 @@ Private Function Launch_ByName_MenuView(ByRef srcMenuName As String, Optional By If (Not PDImages.IsImageActive()) Then Exit Function Dim cmdFound As Boolean: cmdFound = True + Dim newState As Boolean Select Case srcMenuName @@ -1344,16 +1345,17 @@ Private Function Launch_ByName_MenuView(ByRef srcMenuName As String, Optional By If FormMain.MainCanvas(0).IsZoomEnabled Then FormMain.MainCanvas(0).SetZoomDropDownIndex 21 Case "view_rulers" - Dim newRulerState As Boolean - newRulerState = Not FormMain.MainCanvas(0).GetRulerVisibility() - FormMain.MnuView(6).Checked = newRulerState - FormMain.MainCanvas(0).SetRulerVisibility newRulerState + newState = Not FormMain.MainCanvas(0).GetRulerVisibility() + FormMain.MnuView(6).Checked = newState + FormMain.MainCanvas(0).SetRulerVisibility newState Case "view_statusbar" - Dim newStatusBarState As Boolean - newStatusBarState = Not FormMain.MainCanvas(0).GetStatusBarVisibility() - FormMain.MnuView(7).Checked = newStatusBarState - FormMain.MainCanvas(0).SetStatusBarVisibility newStatusBarState + newState = Not FormMain.MainCanvas(0).GetStatusBarVisibility() + FormMain.MnuView(7).Checked = newState + FormMain.MainCanvas(0).SetStatusBarVisibility newState + + Case "snap_canvasedge" + Interface.ToggleSnapOptions pdst_CanvasEdge Case Else cmdFound = False @@ -2000,6 +2002,7 @@ Public Sub BuildActionDatabase() AddAction "zoom_1_16", vbNullString AddAction "view_rulers", vbNullString AddAction "view_statusbar", vbNullString + AddAction "snap_canvasedge", vbNullString 'AddAction "window_toolbox" AddAction "window_displaytoolbox", vbNullString AddAction "window_displaytoolcategories", vbNullString diff --git a/Modules/Interface.bas b/Modules/Interface.bas index 11c304e27..20448afdc 100644 --- a/Modules/Interface.bas +++ b/Modules/Interface.bas @@ -1210,6 +1210,30 @@ Public Sub ToggleImageTabstripVisibility(ByVal newSetting As Long, Optional ByVa End Sub +'Toggle one of the "snap to..." settings in the View menu. +' To forcibly set to a specific state (instead of toggling), set the forceInsteadOfToggle param to TRUE. +Public Sub ToggleSnapOptions(ByVal snapTarget As PD_SnapTargets, Optional ByVal forceInsteadOfToggle As Boolean = False, Optional ByVal newState As Boolean = True) + + 'Convert the snap target into a menu index + Const IDX_BASE As Long = 9 + Dim idxTarget As Long + + 'While calculating which on-screen menu to update, we also need to relay changes to two places: + ' 1) the tools_move module (which handles actual snap calculations) + ' 2) the user preferences file (to ensure everything is synchronized between sessions) + Select Case snapTarget + Case pdst_CanvasEdge + idxTarget = IDX_BASE + 0 + If (Not forceInsteadOfToggle) Then newState = Not Tools_Move.GetSnapCanvasEdge() + Tools_Move.SetSnapCanvasEdge newState + UserPrefs.SetPref_Boolean "Interface", "snap-canvas-edge", newState + End Select + + 'Update the target menu state + FormMain.MnuView(idxTarget).Checked = newState + +End Sub + Public Function FixDPI(ByVal pxMeasurement As Long) As Long 'The first time this function is called, m_DPIRatio will be 0. Calculate it. diff --git a/Modules/Menus.bas b/Modules/Menus.bas index 4fb895e1f..0e03bd910 100644 --- a/Modules/Menus.bas +++ b/Modules/Menus.bas @@ -590,6 +590,8 @@ Public Sub InitializeMenus() AddMenuItem "-", "-", 8, 5 AddMenuItem "Show rulers", "view_rulers", 8, 6 AddMenuItem "Show status bar", "view_statusbar", 8, 7 + AddMenuItem "-", "-", 8, 8 + AddMenuItem "Snap to canvas edge", "snap_canvasedge", 8, 9 'Window Menu AddMenuItem "Window", "window_top", 9 diff --git a/Modules/MoveTool.bas b/Modules/MoveTool.bas index c662ec47b..de8cc2275 100644 --- a/Modules/MoveTool.bas +++ b/Modules/MoveTool.bas @@ -3,13 +3,14 @@ Attribute VB_Name = "Tools_Move" 'PhotoDemon Move/Size Tool Manager 'Copyright 2014-2024 by Tanner Helland 'Created: 24/May/14 -'Last updated: 22/December/22 -'Last update: add some trivial key-handling bits for the Hand tool (which is a different tool, but it has -' so few features that it's easier to just condense things here) +'Last updated: 05/April/24 +'Last update: start wiring up Snap capabilities ' 'This module interfaces between the layer move/size UI and actual layer backend. Look in the relevant ' tool panel form for more details on how the UI relays relevant tool data here. ' +'As of 2024, This module also handles move-related duties like snapping to various features. +' 'Unless otherwise noted, all source code in this file is shared under a simplified BSD license. ' Full license details are available in the LICENSE.md file, or at https://photodemon.org/license/ ' @@ -17,10 +18,19 @@ Attribute VB_Name = "Tools_Move" Option Explicit +Public Enum PD_SnapTargets + pdst_CanvasEdge +End Enum + +#If False Then + Private Const pdst_CanvasEdge = 0 +#End If + 'The move/size tool exposes a number of UI-only options (like drawing borders around active layers). ' To improve viewport performance, we cache those settings locally, and the viewport queries us instead ' of directly querying the associated UI elements. Private m_DrawLayerBorders As Boolean, m_DrawCornerNodes As Boolean, m_DrawRotateNodes As Boolean +Private m_SnapToCanvasEdge As Boolean, m_SnapDistance As Long 'Same goes for various selection-related move settings (for moving selected pixels). These are simple ' flags whose value is relayed from the Move/Size options panel. @@ -315,6 +325,19 @@ Public Function GetDrawLayerRotateNodes() As Boolean GetDrawLayerRotateNodes = m_DrawRotateNodes End Function +Public Function GetSnapCanvasEdge() As Boolean + GetSnapCanvasEdge = m_SnapToCanvasEdge +End Function + +Public Function GetSnapDistance() As Long + + GetSnapDistance = m_SnapDistance + + 'Failsafe only; should never trigger + If (GetSnapDistance < 1) Then GetSnapDistance = 8 + +End Function + Public Sub SetDrawLayerBorders(ByVal newState As Boolean) m_DrawLayerBorders = newState End Sub @@ -327,6 +350,16 @@ Public Sub SetDrawLayerRotateNodes(ByVal newState As Boolean) m_DrawRotateNodes = newState End Sub +Public Sub SetSnapCanvasEdge(ByVal newState As Boolean) + m_SnapToCanvasEdge = newState +End Sub + +Public Sub SetSnapDistance(ByVal newDistance As Long) + m_SnapDistance = newDistance + If (m_SnapDistance < 1) Then m_SnapDistance = 1 + If (m_SnapDistance > 255) Then m_SnapDistance = 255 'GIMP uses a 255 max value; that seems reasonable +End Sub + 'Relay functions for move selected pixels behavior Public Function GetMoveSelectedPixels_DefaultCut() As Boolean GetMoveSelectedPixels_DefaultCut = m_SelectionDefaultCut diff --git a/Modules/UserPrefs.bas b/Modules/UserPrefs.bas index 0c0d9755f..57f5bfd58 100644 --- a/Modules/UserPrefs.bas +++ b/Modules/UserPrefs.bas @@ -5,7 +5,7 @@ Attribute VB_Name = "UserPrefs" 'Created: 03/November/12 'Last updated: 21/February/22 'Last update: revert nightly builds to default to "nightly build" update track (I've gotten much better -' at disciplined nightly build development, and they are far more stable than the used to be). +' at disciplined nightly build development, and they are far more stable than they used to be). ' 'This is the modern incarnation of PD's old "INI file" module. It is responsible for managing all ' persistent user settings. @@ -609,6 +609,9 @@ Public Sub LoadUserSettings() Tools.SetToolSetting_HighResMouse UserPrefs.GetPref_Boolean("Tools", "HighResMouseInput", True) m_CanvasColor = Colors.GetRGBLongFromHex(UserPrefs.GetPref_String("Interface", "CanvasColor", "#a0a0a0")) + Interface.ToggleSnapOptions pdst_CanvasEdge, True, UserPrefs.GetPref_Boolean("Interface", "snap-canvas-edge", True) + Tools_Move.SetSnapDistance UserPrefs.GetPref_Long("Interface", "snap-distance", 8&) + 'Users can supply a (secret!) "UIFont" setting in the "Interface" segment if they ' want to override PD's default font object. m_UIFontName = UserPrefs.GetPref_String("Interface", "UIFont", vbNullString, False) diff --git a/PhotoDemon.vbp b/PhotoDemon.vbp index 877cd1506..22e80acc4 100644 --- a/PhotoDemon.vbp +++ b/PhotoDemon.vbp @@ -526,7 +526,7 @@ Description="PhotoDemon Photo Editor" CompatibleMode="0" MajorVer=9 MinorVer=1 -RevisionVer=347 +RevisionVer=348 AutoIncrementVer=1 ServerSupportFiles=0 VersionComments="Copyright 2000-2024 Tanner Helland - photodemon.org" From c3a3cfc5fb6296025cfd9a0dd81631461a58e3f8 Mon Sep 17 00:00:00 2001 From: Tanner Date: Mon, 8 Apr 2024 11:20:37 -0600 Subject: [PATCH 02/15] Implement snapping to canvas edges... ...when moving a layer. It works well! This behavior can be toggled via `View > Snap to canvas edges`. Minimum snap distance can be set via `Tools > Options > Interface`. Still TODO: 1) Snapping when resizing (not moving) a layer 2) Anything involving selection tools Relates to #498. --- Modules/Tools.bas | 271 ++++++++++++++++++++++++++++++++++------------ PhotoDemon.vbp | 2 +- 2 files changed, 204 insertions(+), 69 deletions(-) diff --git a/Modules/Tools.bas b/Modules/Tools.bas index 44a437a6a..03233334e 100644 --- a/Modules/Tools.bas +++ b/Modules/Tools.bas @@ -72,6 +72,15 @@ Private m_MiddleMouseState As Boolean ' original one.) Private m_MoveSelectedPixels As Boolean +'When snapping coordinates, we need to compare all possible snap targets and choose the best independent +' x and y snap coordinate (assuming they fall beneath the snap threshold for the current zoom level). +' Two distances are provided: one each for left/right (or top/bottom). +Private Type SnapComparison + cValue As Double + cDistance1 As Double + cDistance2 As Double +End Type + 'Get/Set the "alternate" state for a paint tool (typically triggered by pressing ALT) Public Function GetToolAltState() As Boolean GetToolAltState = m_PaintToolAltState @@ -240,6 +249,118 @@ Public Sub PanImageCanvas(ByVal initX As Long, ByVal initY As Long, ByVal curX A End Sub +'Snap the passed rect to any relevant snap targets (based on the user's current snap settings). +' Because this function snaps only by moving the target rect, it is guaranteed that only the +' top and left values will be changed by the function (width/height will *not*). +' +'TODO: this function obviously snaps just the left/right coords, at present - but some ops +' will eventually need "stretching" the rect to fit - do we handle that here, or use a separate function? +' IDK yet. +Private Sub SnapRectByMoving(ByRef srcRectF As RectF, ByRef dstRectF As RectF) + + 'By default, return the same rect. (This is important if the user has disabled snapping.) + dstRectF = srcRectF + + 'Skip any further processing if the user hasn't enabled snapping + If (Not Tools_Move.GetSnapCanvasEdge()) Then Exit Sub + + 'Start by constructing a list of potential snap targets, based on current user settings. + Dim xSnaps() As SnapComparison, ySnaps() As SnapComparison, numXSnaps As Long, numYSnaps As Long + + 'Start with some arbitrarily sized list (these will be enlarged as necessary) + ReDim xSnaps(0 To 15) As SnapComparison + ReDim ySnaps(0 To 15) As SnapComparison + + 'Canvas edges first + If Tools_Move.GetSnapCanvasEdge() Then + + 'Ensure at least two coords available in the target arrays + If (UBound(xSnaps) < numXSnaps + 1) Then ReDim Preserve xSnaps(0 To numXSnaps * 2 - 1) As SnapComparison + If (UBound(ySnaps) < numYSnaps + 1) Then ReDim Preserve ySnaps(0 To numYSnaps * 2 - 1) As SnapComparison + + 'Add canvas boundaries to the snap list + xSnaps(numXSnaps).cValue = 0# + xSnaps(numXSnaps + 1).cValue = PDImages.GetActiveImage.Width + numXSnaps = numXSnaps + 2 + + ySnaps(numYSnaps).cValue = 0# + ySnaps(numYSnaps + 1).cValue = PDImages.GetActiveImage.Height + numYSnaps = numYSnaps + 2 + + End If + + 'We now have a list of snap comparison targets. We don't care what these targets represent - + ' we just want to find the "best" one from each list. + + 'Convert the source snap rectangle into a right/bottom rect (instead of a default width/height one) + Dim compareRectF As RectF_RB + compareRectF.Left = srcRectF.Left + compareRectF.Top = srcRectF.Top + compareRectF.Right = srcRectF.Left + srcRectF.Width - 1 + compareRectF.Bottom = srcRectF.Top + srcRectF.Height - 1 + + 'With the best point found in each list, see if they fall beneath the target snap distance. + Dim i As Long, idxSmallestX As Long, minDistX As Double + + 'Set the minimum distance to an arbitrarily huge number + minDistX = DOUBLE_MAX + For i = 0 To numXSnaps - 1 + With xSnaps(i) + .cDistance1 = PDMath.DistanceOneDimension(compareRectF.Left, .cValue) + If (.cDistance1 < minDistX) Then + minDistX = .cDistance1 + idxSmallestX = i + End If + .cDistance2 = PDMath.DistanceOneDimension(compareRectF.Right, .cValue) + If (.cDistance2 < minDistX) Then + minDistX = .cDistance2 + idxSmallestX = i + End If + End With + Next i + + 'Repeat all the above steps for y-coordinates + Dim idxSmallestY As Long, minDistY As Double + minDistY = DOUBLE_MAX + + For i = 0 To numYSnaps - 1 + With ySnaps(i) + .cDistance1 = PDMath.DistanceOneDimension(compareRectF.Top, .cValue) + If (.cDistance1 < minDistY) Then + minDistY = .cDistance1 + idxSmallestY = i + End If + .cDistance2 = PDMath.DistanceOneDimension(compareRectF.Bottom, .cValue) + If (.cDistance2 < minDistY) Then + minDistY = .cDistance2 + idxSmallestY = i + End If + End With + Next i + + 'Determine the minimum snap distance required for this zoom value. + Dim snapThreshold As Double + snapThreshold = Tools_Move.GetSnapDistance() * (1# / Zoom.GetZoomRatioFromIndex(PDImages.GetActiveImage.ImgViewport.GetZoomIndex)) + + 'If the minimum value falls beneath the minimum snap distance, snap away! + If (minDistX < snapThreshold) Then + If (xSnaps(idxSmallestX).cDistance1 < xSnaps(idxSmallestX).cDistance2) Then + dstRectF.Left = xSnaps(idxSmallestX).cValue + Else + dstRectF.Left = xSnaps(idxSmallestX).cValue - dstRectF.Width + End If + End If + + If (minDistY < snapThreshold) Then + If (ySnaps(idxSmallestY).cDistance1 < ySnaps(idxSmallestY).cDistance2) Then + dstRectF.Top = ySnaps(idxSmallestY).cValue + Else + dstRectF.Top = ySnaps(idxSmallestY).cValue - dstRectF.Height + End If + End If + +End Sub + 'This function can be used to move and/or non-destructively resize an image layer. ' 'If this action occurs during a Mouse_Up event, the finalizeTransform parameter should be set to TRUE. This instructs the function @@ -272,6 +393,10 @@ Public Sub TransformCurrentLayer(ByVal curImageX As Double, ByVal curImageY As D ' and disallow anything that results in invalid coordinates or sizes. Dim newLeft As Double, newTop As Double, newRight As Double, newBottom As Double + 'In 2024, "snap" support was added to move operations. This requires to pre-process all coordinates against + ' potential snap targets *before* handing them off to the target layer object. + Dim srcRectF As RectF, snappedRectF As RectF + 'The way we assign new offsets and/or sizes to the layer depends on the POI (point of interest) the user is interacting with. ' Layers currently support nine points of interest: each of their 4 corners, 4 rotational points (lying on the center of ' each edge), and anywhere in the layer interior (for moving the layer). @@ -285,7 +410,7 @@ Public Sub TransformCurrentLayer(ByVal curImageX As Double, ByVal curImageY As D 'Aspect ratio is locked on SHIFT keypress, or with the fixed toggle on the move/size toolpanel Dim lockAspectRatio As Boolean lockAspectRatio = isShiftDown - If (g_CurrentTool = NAV_MOVE) Then lockAspectRatio = lockAspectRatio Or toolpanel_MoveSize.chkAspectRatio.Value + If (g_CurrentTool = NAV_MOVE) Then lockAspectRatio = lockAspectRatio Or toolpanel_MoveSize.chkAspectRatio.value 'Check the POI we were given, and update the layer accordingly. With srcLayer @@ -298,8 +423,8 @@ Public Sub TransformCurrentLayer(ByVal curImageX As Double, ByVal curImageY As D srcCanvas.SetRedrawSuspension False Exit Sub - '0: the mouse is dragging the top-left corner of the layer. The comments here are uniform for all POIs, so for brevity's sake, - ' I'll keep the others short. + '0: the mouse is dragging the top-left corner of the layer. The comments here are uniform for all POIs, + ' so for brevity's sake, I'll keep the others short. Case poi_CornerNW 'The opposite corner coordinate (bottom-left) stays in exactly the same place @@ -428,8 +553,18 @@ Public Sub TransformCurrentLayer(ByVal curImageX As Double, ByVal curImageY As D '5: interior of the layer (e.g. move the layer instead of resize it) Case poi_Interior + + 'Pass the new coordinates to the layer engine, then retrieve the new layer rect + ' the transform produces .SetLayerOffsetX m_InitLayerCoords_Pure(0).x + hOffsetImage .SetLayerOffsetY m_InitLayerCoords_Pure(0).y + vOffsetImage + .GetLayerBoundaryRect srcRectF + + 'Hand the layer rect off to the snap calculator, then take whatever it returns and + ' forward just the left/top coordinates *back* to the target layer. + SnapRectByMoving srcRectF, snappedRectF + .SetLayerOffsetX snappedRectF.Left + .SetLayerOffsetY snappedRectF.Top End Select @@ -759,14 +894,14 @@ Public Sub SyncToolOptionsUIToCurrentLayer() With toolpanel_TextBasic .txtTextTool.Text = PDImages.GetActiveImage.GetActiveLayer.GetTextLayerProperty(ptp_Text) .cboTextFontFace.ListIndex = .cboTextFontFace.ListIndexByString(PDImages.GetActiveImage.GetActiveLayer.GetTextLayerProperty(ptp_FontFace), vbTextCompare) - .sldTextFontSize.Value = PDImages.GetActiveImage.GetActiveLayer.GetTextLayerProperty(ptp_FontSize) + .sldTextFontSize.value = PDImages.GetActiveImage.GetActiveLayer.GetTextLayerProperty(ptp_FontSize) .csTextFontColor.Color = PDImages.GetActiveImage.GetActiveLayer.GetTextLayerProperty(ptp_FontColor) .cboTextRenderingHint.ListIndex = PDImages.GetActiveImage.GetActiveLayer.GetTextLayerProperty(ptp_TextAntialiasing) - .sltTextClarity.Value = PDImages.GetActiveImage.GetActiveLayer.GetTextLayerProperty(ptp_TextContrast) - .btnFontStyles(0).Value = CBool(PDImages.GetActiveImage.GetActiveLayer.GetTextLayerProperty(ptp_FontBold)) - .btnFontStyles(1).Value = CBool(PDImages.GetActiveImage.GetActiveLayer.GetTextLayerProperty(ptp_FontItalic)) - .btnFontStyles(2).Value = CBool(PDImages.GetActiveImage.GetActiveLayer.GetTextLayerProperty(ptp_FontUnderline)) - .btnFontStyles(3).Value = CBool(PDImages.GetActiveImage.GetActiveLayer.GetTextLayerProperty(ptp_FontStrikeout)) + .sltTextClarity.value = PDImages.GetActiveImage.GetActiveLayer.GetTextLayerProperty(ptp_TextContrast) + .btnFontStyles(0).value = CBool(PDImages.GetActiveImage.GetActiveLayer.GetTextLayerProperty(ptp_FontBold)) + .btnFontStyles(1).value = CBool(PDImages.GetActiveImage.GetActiveLayer.GetTextLayerProperty(ptp_FontItalic)) + .btnFontStyles(2).value = CBool(PDImages.GetActiveImage.GetActiveLayer.GetTextLayerProperty(ptp_FontUnderline)) + .btnFontStyles(3).value = CBool(PDImages.GetActiveImage.GetActiveLayer.GetTextLayerProperty(ptp_FontStrikeout)) .btsHAlignment.ListIndex = PDImages.GetActiveImage.GetActiveLayer.GetTextLayerProperty(ptp_HorizontalAlignment) .btsVAlignment.ListIndex = PDImages.GetActiveImage.GetActiveLayer.GetTextLayerProperty(ptp_VerticalAlignment) End With @@ -828,8 +963,8 @@ Public Sub SyncCurrentLayerToToolOptionsUI() Case NAV_MOVE 'The Layer Move tool has four text up/downs: two for layer position (x, y) and two for layer size (w, y) - PDImages.GetActiveImage.GetActiveLayer.SetLayerOffsetX toolpanel_MoveSize.tudLayerMove(0).Value - PDImages.GetActiveImage.GetActiveLayer.SetLayerOffsetY toolpanel_MoveSize.tudLayerMove(1).Value + PDImages.GetActiveImage.GetActiveLayer.SetLayerOffsetX toolpanel_MoveSize.tudLayerMove(0).value + PDImages.GetActiveImage.GetActiveLayer.SetLayerOffsetY toolpanel_MoveSize.tudLayerMove(1).value 'Setting layer width and height isn't activated at present, on purpose 'PDImages.GetActiveImage.getActiveLayer.setLayerWidth toolpanel_MoveSize.tudLayerMove(2).Value @@ -839,23 +974,23 @@ Public Sub SyncCurrentLayerToToolOptionsUI() PDImages.GetActiveImage.GetActiveLayer.SetLayerResizeQuality toolpanel_MoveSize.cboLayerResizeQuality.ListIndex 'Layer angle and shear are newly available as of 7.0 - PDImages.GetActiveImage.GetActiveLayer.SetLayerAngle toolpanel_MoveSize.sltLayerAngle.Value - PDImages.GetActiveImage.GetActiveLayer.SetLayerShearX toolpanel_MoveSize.sltLayerShearX.Value - PDImages.GetActiveImage.GetActiveLayer.SetLayerShearY toolpanel_MoveSize.sltLayerShearY.Value + PDImages.GetActiveImage.GetActiveLayer.SetLayerAngle toolpanel_MoveSize.sltLayerAngle.value + PDImages.GetActiveImage.GetActiveLayer.SetLayerShearX toolpanel_MoveSize.sltLayerShearX.value + PDImages.GetActiveImage.GetActiveLayer.SetLayerShearY toolpanel_MoveSize.sltLayerShearY.value Case TEXT_BASIC With PDImages.GetActiveImage.GetActiveLayer .SetTextLayerProperty ptp_Text, toolpanel_TextBasic.txtTextTool.Text .SetTextLayerProperty ptp_FontFace, toolpanel_TextBasic.cboTextFontFace.List(toolpanel_TextBasic.cboTextFontFace.ListIndex) - .SetTextLayerProperty ptp_FontSize, toolpanel_TextBasic.sldTextFontSize.Value + .SetTextLayerProperty ptp_FontSize, toolpanel_TextBasic.sldTextFontSize.value .SetTextLayerProperty ptp_FontColor, toolpanel_TextBasic.csTextFontColor.Color .SetTextLayerProperty ptp_TextAntialiasing, toolpanel_TextBasic.cboTextRenderingHint.ListIndex - .SetTextLayerProperty ptp_TextContrast, toolpanel_TextBasic.sltTextClarity.Value - .SetTextLayerProperty ptp_FontBold, toolpanel_TextBasic.btnFontStyles(0).Value - .SetTextLayerProperty ptp_FontItalic, toolpanel_TextBasic.btnFontStyles(1).Value - .SetTextLayerProperty ptp_FontUnderline, toolpanel_TextBasic.btnFontStyles(2).Value - .SetTextLayerProperty ptp_FontStrikeout, toolpanel_TextBasic.btnFontStyles(3).Value + .SetTextLayerProperty ptp_TextContrast, toolpanel_TextBasic.sltTextClarity.value + .SetTextLayerProperty ptp_FontBold, toolpanel_TextBasic.btnFontStyles(0).value + .SetTextLayerProperty ptp_FontItalic, toolpanel_TextBasic.btnFontStyles(1).value + .SetTextLayerProperty ptp_FontUnderline, toolpanel_TextBasic.btnFontStyles(2).value + .SetTextLayerProperty ptp_FontStrikeout, toolpanel_TextBasic.btnFontStyles(3).value .SetTextLayerProperty ptp_HorizontalAlignment, toolpanel_TextBasic.btsHAlignment.ListIndex .SetTextLayerProperty ptp_VerticalAlignment, toolpanel_TextBasic.btsVAlignment.ListIndex End With @@ -870,39 +1005,39 @@ Public Sub SyncCurrentLayerToToolOptionsUI() With PDImages.GetActiveImage.GetActiveLayer .SetTextLayerProperty ptp_Text, toolpanel_TextAdvanced.txtTextTool.Text .SetTextLayerProperty ptp_FontFace, toolpanel_TextAdvanced.cboTextFontFace.List(toolpanel_TextAdvanced.cboTextFontFace.ListIndex) - .SetTextLayerProperty ptp_FontSize, toolpanel_TextAdvanced.sldTextFontSize.Value + .SetTextLayerProperty ptp_FontSize, toolpanel_TextAdvanced.sldTextFontSize.value .SetTextLayerProperty ptp_StretchToFit, toolpanel_TextAdvanced.btsStretch.ListIndex .SetTextLayerProperty ptp_TextAntialiasing, toolpanel_TextAdvanced.cboTextRenderingHint.ListIndex .SetTextLayerProperty ptp_TextHinting, (toolpanel_TextAdvanced.btsHinting.ListIndex = 1) - .SetTextLayerProperty ptp_FontBold, toolpanel_TextAdvanced.btnFontStyles(0).Value - .SetTextLayerProperty ptp_FontItalic, toolpanel_TextAdvanced.btnFontStyles(1).Value - .SetTextLayerProperty ptp_FontUnderline, toolpanel_TextAdvanced.btnFontStyles(2).Value - .SetTextLayerProperty ptp_FontStrikeout, toolpanel_TextAdvanced.btnFontStyles(3).Value + .SetTextLayerProperty ptp_FontBold, toolpanel_TextAdvanced.btnFontStyles(0).value + .SetTextLayerProperty ptp_FontItalic, toolpanel_TextAdvanced.btnFontStyles(1).value + .SetTextLayerProperty ptp_FontUnderline, toolpanel_TextAdvanced.btnFontStyles(2).value + .SetTextLayerProperty ptp_FontStrikeout, toolpanel_TextAdvanced.btnFontStyles(3).value .SetTextLayerProperty ptp_HorizontalAlignment, toolpanel_TextAdvanced.btsHAlignment.ListIndex .SetTextLayerProperty ptp_VerticalAlignment, toolpanel_TextAdvanced.btsVAlignment.ListIndex .SetTextLayerProperty ptp_WordWrap, toolpanel_TextAdvanced.cboWordWrap.ListIndex - .SetTextLayerProperty ptp_FillActive, toolpanel_TextAdvanced.chkFillText.Value + .SetTextLayerProperty ptp_FillActive, toolpanel_TextAdvanced.chkFillText.value .SetTextLayerProperty ptp_FillBrush, toolpanel_TextAdvanced.bsText.Brush - .SetTextLayerProperty ptp_OutlineActive, toolpanel_TextAdvanced.chkOutlineText.Value + .SetTextLayerProperty ptp_OutlineActive, toolpanel_TextAdvanced.chkOutlineText.value .SetTextLayerProperty ptp_OutlinePen, toolpanel_TextAdvanced.psText.Pen - .SetTextLayerProperty ptp_BackgroundActive, toolpanel_TextAdvanced.chkBackground.Value + .SetTextLayerProperty ptp_BackgroundActive, toolpanel_TextAdvanced.chkBackground.value .SetTextLayerProperty ptp_BackgroundBrush, toolpanel_TextAdvanced.bsTextBackground.Brush - .SetTextLayerProperty ptp_BackBorderActive, toolpanel_TextAdvanced.chkBackgroundBorder.Value + .SetTextLayerProperty ptp_BackBorderActive, toolpanel_TextAdvanced.chkBackgroundBorder.value .SetTextLayerProperty ptp_BackBorderPen, toolpanel_TextAdvanced.psTextBackground.Pen - .SetTextLayerProperty ptp_LineSpacing, toolpanel_TextAdvanced.sldLineSpacing.Value - .SetTextLayerProperty ptp_MarginLeft, toolpanel_TextAdvanced.tudMargin(0).Value - .SetTextLayerProperty ptp_MarginRight, toolpanel_TextAdvanced.tudMargin(1).Value - .SetTextLayerProperty ptp_MarginTop, toolpanel_TextAdvanced.tudMargin(2).Value - .SetTextLayerProperty ptp_MarginBottom, toolpanel_TextAdvanced.tudMargin(3).Value - .SetTextLayerProperty ptp_CharInflation, toolpanel_TextAdvanced.sltCharInflation.Value - .SetTextLayerProperty ptp_CharJitterX, toolpanel_TextAdvanced.tudJitter(0).Value - .SetTextLayerProperty ptp_CharJitterY, toolpanel_TextAdvanced.tudJitter(1).Value + .SetTextLayerProperty ptp_LineSpacing, toolpanel_TextAdvanced.sldLineSpacing.value + .SetTextLayerProperty ptp_MarginLeft, toolpanel_TextAdvanced.tudMargin(0).value + .SetTextLayerProperty ptp_MarginRight, toolpanel_TextAdvanced.tudMargin(1).value + .SetTextLayerProperty ptp_MarginTop, toolpanel_TextAdvanced.tudMargin(2).value + .SetTextLayerProperty ptp_MarginBottom, toolpanel_TextAdvanced.tudMargin(3).value + .SetTextLayerProperty ptp_CharInflation, toolpanel_TextAdvanced.sltCharInflation.value + .SetTextLayerProperty ptp_CharJitterX, toolpanel_TextAdvanced.tudJitter(0).value + .SetTextLayerProperty ptp_CharJitterY, toolpanel_TextAdvanced.tudJitter(1).value .SetTextLayerProperty ptp_CharMirror, toolpanel_TextAdvanced.cboCharMirror.ListIndex - .SetTextLayerProperty ptp_CharOrientation, toolpanel_TextAdvanced.sltCharOrientation.Value + .SetTextLayerProperty ptp_CharOrientation, toolpanel_TextAdvanced.sltCharOrientation.value .SetTextLayerProperty ptp_CharRemap, toolpanel_TextAdvanced.cboCharCase.ListIndex - .SetTextLayerProperty ptp_CharSpacing, toolpanel_TextAdvanced.sltCharSpacing.Value + .SetTextLayerProperty ptp_CharSpacing, toolpanel_TextAdvanced.sltCharSpacing.value .SetTextLayerProperty ptp_AlignLastLine, toolpanel_TextAdvanced.btsHAlignJustify.ListIndex - .SetTextLayerProperty ptp_OutlineAboveFill, toolpanel_TextAdvanced.chkFillFirst.Value + .SetTextLayerProperty ptp_OutlineAboveFill, toolpanel_TextAdvanced.chkFillFirst.value End With 'Advanced text layers are rendered using a PhotoDemon-specific renderer. @@ -983,11 +1118,11 @@ Public Sub QuickToolAction_HardnessDown() Select Case g_CurrentTool Case PAINT_SOFTBRUSH - curValue = toolpanel_Paintbrush.sltBrushSetting(2).Value + curValue = toolpanel_Paintbrush.sltBrushSetting(2).value Case PAINT_ERASER - curValue = toolpanel_Eraser.sltBrushSetting(2).Value + curValue = toolpanel_Eraser.sltBrushSetting(2).value Case PAINT_CLONE - curValue = toolpanel_Clone.sltBrushSetting(2).Value + curValue = toolpanel_Clone.sltBrushSetting(2).value Case Else Exit Sub End Select @@ -1001,11 +1136,11 @@ Public Sub QuickToolAction_HardnessDown() 'Assign the new value Select Case g_CurrentTool Case PAINT_SOFTBRUSH - toolpanel_Paintbrush.sltBrushSetting(2).Value = curValue + toolpanel_Paintbrush.sltBrushSetting(2).value = curValue Case PAINT_ERASER - toolpanel_Eraser.sltBrushSetting(2).Value = curValue + toolpanel_Eraser.sltBrushSetting(2).value = curValue Case PAINT_CLONE - toolpanel_Clone.sltBrushSetting(2).Value = curValue + toolpanel_Clone.sltBrushSetting(2).value = curValue End Select End Sub @@ -1017,11 +1152,11 @@ Public Sub QuickToolAction_HardnessUp() Select Case g_CurrentTool Case PAINT_SOFTBRUSH - curValue = toolpanel_Paintbrush.sltBrushSetting(2).Value + curValue = toolpanel_Paintbrush.sltBrushSetting(2).value Case PAINT_ERASER - curValue = toolpanel_Eraser.sltBrushSetting(2).Value + curValue = toolpanel_Eraser.sltBrushSetting(2).value Case PAINT_CLONE - curValue = toolpanel_Clone.sltBrushSetting(2).Value + curValue = toolpanel_Clone.sltBrushSetting(2).value Case Else Exit Sub End Select @@ -1035,11 +1170,11 @@ Public Sub QuickToolAction_HardnessUp() 'Assign the new value Select Case g_CurrentTool Case PAINT_SOFTBRUSH - toolpanel_Paintbrush.sltBrushSetting(2).Value = curValue + toolpanel_Paintbrush.sltBrushSetting(2).value = curValue Case PAINT_ERASER - toolpanel_Eraser.sltBrushSetting(2).Value = curValue + toolpanel_Eraser.sltBrushSetting(2).value = curValue Case PAINT_CLONE - toolpanel_Clone.sltBrushSetting(2).Value = curValue + toolpanel_Clone.sltBrushSetting(2).value = curValue End Select End Sub @@ -1053,13 +1188,13 @@ Public Sub QuickToolAction_SizeDown() Select Case g_CurrentTool Case PAINT_PENCIL - curValue = toolpanel_Pencil.sltBrushSetting(0).Value + curValue = toolpanel_Pencil.sltBrushSetting(0).value Case PAINT_SOFTBRUSH - curValue = toolpanel_Paintbrush.sltBrushSetting(0).Value + curValue = toolpanel_Paintbrush.sltBrushSetting(0).value Case PAINT_ERASER - curValue = toolpanel_Eraser.sltBrushSetting(0).Value + curValue = toolpanel_Eraser.sltBrushSetting(0).value Case PAINT_CLONE - curValue = toolpanel_Clone.sltBrushSetting(0).Value + curValue = toolpanel_Clone.sltBrushSetting(0).value Case Else Exit Sub End Select @@ -1083,13 +1218,13 @@ Public Sub QuickToolAction_SizeDown() 'Assign the new value Select Case g_CurrentTool Case PAINT_PENCIL - toolpanel_Pencil.sltBrushSetting(0).Value = curValue + toolpanel_Pencil.sltBrushSetting(0).value = curValue Case PAINT_SOFTBRUSH - toolpanel_Paintbrush.sltBrushSetting(0).Value = curValue + toolpanel_Paintbrush.sltBrushSetting(0).value = curValue Case PAINT_ERASER - toolpanel_Eraser.sltBrushSetting(0).Value = curValue + toolpanel_Eraser.sltBrushSetting(0).value = curValue Case PAINT_CLONE - toolpanel_Clone.sltBrushSetting(0).Value = curValue + toolpanel_Clone.sltBrushSetting(0).value = curValue End Select End Sub @@ -1101,13 +1236,13 @@ Public Sub QuickToolAction_SizeUp() Select Case g_CurrentTool Case PAINT_PENCIL - curValue = toolpanel_Pencil.sltBrushSetting(0).Value + curValue = toolpanel_Pencil.sltBrushSetting(0).value Case PAINT_SOFTBRUSH - curValue = toolpanel_Paintbrush.sltBrushSetting(0).Value + curValue = toolpanel_Paintbrush.sltBrushSetting(0).value Case PAINT_ERASER - curValue = toolpanel_Eraser.sltBrushSetting(0).Value + curValue = toolpanel_Eraser.sltBrushSetting(0).value Case PAINT_CLONE - curValue = toolpanel_Clone.sltBrushSetting(0).Value + curValue = toolpanel_Clone.sltBrushSetting(0).value Case Else Exit Sub End Select @@ -1142,13 +1277,13 @@ Public Sub QuickToolAction_SizeUp() 'Assign the new value Select Case g_CurrentTool Case PAINT_PENCIL - toolpanel_Pencil.sltBrushSetting(0).Value = curValue + toolpanel_Pencil.sltBrushSetting(0).value = curValue Case PAINT_SOFTBRUSH - toolpanel_Paintbrush.sltBrushSetting(0).Value = curValue + toolpanel_Paintbrush.sltBrushSetting(0).value = curValue Case PAINT_ERASER - toolpanel_Eraser.sltBrushSetting(0).Value = curValue + toolpanel_Eraser.sltBrushSetting(0).value = curValue Case PAINT_CLONE - toolpanel_Clone.sltBrushSetting(0).Value = curValue + toolpanel_Clone.sltBrushSetting(0).value = curValue End Select End Sub diff --git a/PhotoDemon.vbp b/PhotoDemon.vbp index 22e80acc4..9491561ef 100644 --- a/PhotoDemon.vbp +++ b/PhotoDemon.vbp @@ -526,7 +526,7 @@ Description="PhotoDemon Photo Editor" CompatibleMode="0" MajorVer=9 MinorVer=1 -RevisionVer=348 +RevisionVer=349 AutoIncrementVer=1 ServerSupportFiles=0 VersionComments="Copyright 2000-2024 Tanner Helland - photodemon.org" From 7190cbed5c12442a362087d75962cdd601afa931 Mon Sep 17 00:00:00 2001 From: Tanner Date: Wed, 10 Apr 2024 19:09:38 -0600 Subject: [PATCH 03/15] Implement snap when resizing layers Snap-to-canvas-edge now works on all four layer corners when resizing via the move tool. Importantly, snapping also works when the layer has non-destructive rotation or skew applied! Relates to #498 --- Modules/Tools.bas | 317 ++++++++++++++++++++++++++++++---------------- PhotoDemon.vbp | 2 +- 2 files changed, 212 insertions(+), 107 deletions(-) diff --git a/Modules/Tools.bas b/Modules/Tools.bas index 03233334e..12442f64f 100644 --- a/Modules/Tools.bas +++ b/Modules/Tools.bas @@ -3,8 +3,8 @@ Attribute VB_Name = "Tools" 'Helper functions for various PhotoDemon tools 'Copyright 2014-2024 by Tanner Helland 'Created: 06/February/14 -'Last updated: 21/January/22 -'Last update: new support for moving only selected pixels via the Move/Size tool +'Last updated: 10/April/24 +'Last update: add snap support when moving or resizing a layer ' 'To keep the pdCanvas user control codebase lean, many of its MouseMove events redirect here, to specialized ' functions that take mouse actions on the canvas and translate them into tool actions. @@ -249,13 +249,67 @@ Public Sub PanImageCanvas(ByVal initX As Long, ByVal initY As Long, ByVal curX A End Sub +'Snap the passed point to any relevant snap targets (based on the user's current snap settings). +Private Sub SnapPointByMoving(ByRef srcPointF As PointFloat, ByRef dstPointF As PointFloat) + + 'If no snap targets exist (because the user has disabled snapping), ensure the destination point + ' mirrors the source point + dstPointF = srcPointF + + 'Skip any further processing if the user hasn't enabled snapping + If (Not Tools_Move.GetSnapCanvasEdge()) Then Exit Sub + + 'Start by constructing a list of potential snap targets, based on current user settings. + Dim xSnaps() As SnapComparison, ySnaps() As SnapComparison, numXSnaps As Long, numYSnaps As Long + numXSnaps = GetSnapTargets_X(xSnaps) + numYSnaps = GetSnapTargets_Y(ySnaps) + + 'Ensure some snap targets exist + If (numXSnaps = 0) Or (numYSnaps = 0) Then Exit Sub + + 'We now have a list of snap comparison targets. We don't care what these targets represent - + ' we just want to find the "best" one from each list. + Dim i As Long, idxSmallestX As Long, minDistX As Double + + 'Set the minimum distance to an arbitrarily huge number, then find minimum x-distances + minDistX = DOUBLE_MAX + For i = 0 To numXSnaps - 1 + With xSnaps(i) + .cDistance1 = PDMath.DistanceOneDimension(srcPointF.x, .cValue) + If (.cDistance1 < minDistX) Then + minDistX = .cDistance1 + idxSmallestX = i + End If + End With + Next i + + 'Repeat all the above steps for y-coordinates + Dim idxSmallestY As Long, minDistY As Double + minDistY = DOUBLE_MAX + + For i = 0 To numYSnaps - 1 + With ySnaps(i) + .cDistance1 = PDMath.DistanceOneDimension(srcPointF.y, .cValue) + If (.cDistance1 < minDistY) Then + minDistY = .cDistance1 + idxSmallestY = i + End If + End With + Next i + + 'Determine the minimum snap distance required for this zoom value. + Dim snapThreshold As Double + snapThreshold = Tools_Move.GetSnapDistance() * (1# / Zoom.GetZoomRatioFromIndex(PDImages.GetActiveImage.ImgViewport.GetZoomIndex)) + + 'If the minimum value falls beneath the minimum snap distance, snap away! + If (minDistX < snapThreshold) Then dstPointF.x = xSnaps(idxSmallestX).cValue + If (minDistY < snapThreshold) Then dstPointF.y = ySnaps(idxSmallestY).cValue + +End Sub + 'Snap the passed rect to any relevant snap targets (based on the user's current snap settings). ' Because this function snaps only by moving the target rect, it is guaranteed that only the ' top and left values will be changed by the function (width/height will *not*). -' -'TODO: this function obviously snaps just the left/right coords, at present - but some ops -' will eventually need "stretching" the rect to fit - do we handle that here, or use a separate function? -' IDK yet. Private Sub SnapRectByMoving(ByRef srcRectF As RectF, ByRef dstRectF As RectF) 'By default, return the same rect. (This is important if the user has disabled snapping.) @@ -266,28 +320,11 @@ Private Sub SnapRectByMoving(ByRef srcRectF As RectF, ByRef dstRectF As RectF) 'Start by constructing a list of potential snap targets, based on current user settings. Dim xSnaps() As SnapComparison, ySnaps() As SnapComparison, numXSnaps As Long, numYSnaps As Long + numXSnaps = GetSnapTargets_X(xSnaps) + numYSnaps = GetSnapTargets_Y(ySnaps) - 'Start with some arbitrarily sized list (these will be enlarged as necessary) - ReDim xSnaps(0 To 15) As SnapComparison - ReDim ySnaps(0 To 15) As SnapComparison - - 'Canvas edges first - If Tools_Move.GetSnapCanvasEdge() Then - - 'Ensure at least two coords available in the target arrays - If (UBound(xSnaps) < numXSnaps + 1) Then ReDim Preserve xSnaps(0 To numXSnaps * 2 - 1) As SnapComparison - If (UBound(ySnaps) < numYSnaps + 1) Then ReDim Preserve ySnaps(0 To numYSnaps * 2 - 1) As SnapComparison - - 'Add canvas boundaries to the snap list - xSnaps(numXSnaps).cValue = 0# - xSnaps(numXSnaps + 1).cValue = PDImages.GetActiveImage.Width - numXSnaps = numXSnaps + 2 - - ySnaps(numYSnaps).cValue = 0# - ySnaps(numYSnaps + 1).cValue = PDImages.GetActiveImage.Height - numYSnaps = numYSnaps + 2 - - End If + 'Ensure some snap targets exist + If (numXSnaps = 0) Or (numYSnaps = 0) Then Exit Sub 'We now have a list of snap comparison targets. We don't care what these targets represent - ' we just want to find the "best" one from each list. @@ -299,10 +336,9 @@ Private Sub SnapRectByMoving(ByRef srcRectF As RectF, ByRef dstRectF As RectF) compareRectF.Right = srcRectF.Left + srcRectF.Width - 1 compareRectF.Bottom = srcRectF.Top + srcRectF.Height - 1 - 'With the best point found in each list, see if they fall beneath the target snap distance. Dim i As Long, idxSmallestX As Long, minDistX As Double - 'Set the minimum distance to an arbitrarily huge number + 'Set the minimum distance to an arbitrarily huge number, then find the smallest x-distance minDistX = DOUBLE_MAX For i = 0 To numXSnaps - 1 With xSnaps(i) @@ -361,6 +397,52 @@ Private Sub SnapRectByMoving(ByRef srcRectF As RectF, ByRef dstRectF As RectF) End Sub +'Get a list of current x-snap targets (determined by user settings). +' RETURNS: number of entries in the list, or 0 if snapping is disabled by the user. +Private Function GetSnapTargets_X(ByRef dstSnaps() As SnapComparison) As Long + + 'Start with some arbitrarily sized list (these will be enlarged as necessary) + ReDim dstSnaps(0 To 15) As SnapComparison + GetSnapTargets_X = 0 + + 'Canvas edges first + If Tools_Move.GetSnapCanvasEdge() Then + + 'Ensure at space is available in the target array + If (UBound(dstSnaps) < GetSnapTargets_X + 1) Then ReDim Preserve dstSnaps(0 To GetSnapTargets_X * 2 - 1) As SnapComparison + + 'Add canvas boundaries to the snap list + dstSnaps(GetSnapTargets_X).cValue = 0# + dstSnaps(GetSnapTargets_X + 1).cValue = PDImages.GetActiveImage.Width + GetSnapTargets_X = GetSnapTargets_X + 2 + + End If + +End Function + +'Get a list of current y-snap targets (determined by user settings). +' RETURNS: number of entries in the list, or 0 if snapping is disabled by the user. +Private Function GetSnapTargets_Y(ByRef dstSnaps() As SnapComparison) As Long + + 'Start with some arbitrarily sized list (these will be enlarged as necessary) + ReDim dstSnaps(0 To 15) As SnapComparison + GetSnapTargets_Y = 0 + + 'Canvas edges first + If Tools_Move.GetSnapCanvasEdge() Then + + 'Ensure at space is available in the target array + If (UBound(dstSnaps) < GetSnapTargets_Y + 1) Then ReDim Preserve dstSnaps(0 To GetSnapTargets_Y * 2 - 1) As SnapComparison + + 'Add canvas boundaries to the snap list + dstSnaps(GetSnapTargets_Y).cValue = 0# + dstSnaps(GetSnapTargets_Y + 1).cValue = PDImages.GetActiveImage.Height + GetSnapTargets_Y = GetSnapTargets_Y + 2 + + End If + +End Function + 'This function can be used to move and/or non-destructively resize an image layer. ' 'If this action occurs during a Mouse_Up event, the finalizeTransform parameter should be set to TRUE. This instructs the function @@ -374,11 +456,32 @@ Public Sub TransformCurrentLayer(ByVal curImageX As Double, ByVal curImageY As D 'Also, mark the tool engine as busy to prevent re-entrance issues Tools.SetToolBusyState True + 'For operations only involving a single point of transformation (e.g. resizing a layer by corner-dragging), + ' we can apply snapping *now*, to the mouse coordinate itself. + ' + 'For operations that transform multiple points (like moving an entire layer), we need to snap points *besides* + ' the mouse pointer (e.g. the layer edges, which are not located at the mouse position), so we'll need to wait + ' to snap until the transform has been applied to the underlying layer. + Dim srcPtF As PointFloat, snappedPtF As PointFloat + If Tools_Move.GetSnapCanvasEdge() Then + + Select Case m_CurPOI + Case poi_CornerNW, poi_CornerNE, poi_CornerSW, poi_CornerSE + srcPtF.x = curImageX + srcPtF.y = curImageY + SnapPointByMoving srcPtF, snappedPtF + curImageX = snappedPtF.x + curImageY = snappedPtF.y + + End Select + + End If + 'Convert the current x/y pair to the layer coordinate space. This takes into account any active affine transforms ' on the image (e.g. rotation), which may place the point in a totally different position relative to the underlying layer. Dim curLayerX As Single, curLayerY As Single Drawing.ConvertImageCoordsToLayerCoords srcImage, srcLayer, curImageX, curImageY, curLayerX, curLayerY - + 'As a convenience for later calculations, calculate offsets between the initial transform coordinates (set at MouseDown) ' and the current ones. Repeat this for both the image and layer coordinate spaces, as we need different ones for different ' transform types. @@ -391,11 +494,7 @@ Public Sub TransformCurrentLayer(ByVal curImageX As Double, ByVal curImageY As D 'To prevent the user from flipping or mirroring the image, we must do some bound checking on their changes, ' and disallow anything that results in invalid coordinates or sizes. - Dim newLeft As Double, newTop As Double, newRight As Double, newBottom As Double - - 'In 2024, "snap" support was added to move operations. This requires to pre-process all coordinates against - ' potential snap targets *before* handing them off to the target layer object. - Dim srcRectF As RectF, snappedRectF As RectF + Dim newLeft As Single, newTop As Single, newRight As Single, newBottom As Single 'The way we assign new offsets and/or sizes to the layer depends on the POI (point of interest) the user is interacting with. ' Layers currently support nine points of interest: each of their 4 corners, 4 rotational points (lying on the center of @@ -410,7 +509,7 @@ Public Sub TransformCurrentLayer(ByVal curImageX As Double, ByVal curImageY As D 'Aspect ratio is locked on SHIFT keypress, or with the fixed toggle on the move/size toolpanel Dim lockAspectRatio As Boolean lockAspectRatio = isShiftDown - If (g_CurrentTool = NAV_MOVE) Then lockAspectRatio = lockAspectRatio Or toolpanel_MoveSize.chkAspectRatio.value + If (g_CurrentTool = NAV_MOVE) Then lockAspectRatio = lockAspectRatio Or toolpanel_MoveSize.chkAspectRatio.Value 'Check the POI we were given, and update the layer accordingly. With srcLayer @@ -475,6 +574,7 @@ Public Sub TransformCurrentLayer(ByVal curImageX As Double, ByVal curImageY As D newLeft = m_InitLayerCoords_Pure(0).x newTop = m_InitLayerCoords_Pure(0).y + 'Finish calculating things like required minimum layer size and aspect ratio preservation If ((curLayerX - newLeft) > 1#) Then newRight = curLayerX Else newRight = newLeft + 1# If lockAspectRatio Then newBottom = newTop + (newRight - newLeft) / m_LayerAspectRatio Else newBottom = curLayerY If ((newBottom - newTop) < 1#) Then newBottom = newTop + 1# @@ -534,10 +634,11 @@ Public Sub TransformCurrentLayer(ByVal curImageX As Double, ByVal curImageY As D Dim newAngle As Double newAngle = PDMath.AngleBetweenTwoIntersectingLines(ptIntersect, pt1, pt2, True) - 'Because the angle function finds the absolute inner angle, it will never be greater than 180 degrees. This also means - ' that +90 and -90 (from a UI standpoint) return the same 90 result. A simple workaround is to force the sign to - ' match the difference between the relevant coordinate of the intersecting lines. (The relevant coordinate varies - ' based on the orientation of the default, non-rotated line defined by ptIntersect and pt1.) + 'Because the angle function finds the absolute inner angle, it will never be greater than 180 degrees. + ' This also means that +90 and -90 (from a UI standpoint) return the same 90 result. A simple workaround + ' is to force the sign to match the difference between the relevant coordinate of the intersecting lines. + ' (The relevant coordinate varies based on the orientation of the default, non-rotated line defined by + ' ptIntersect and pt1.) If (m_CurPOI = poi_EdgeE) Then If (pt2.y < pt1.y) Then newAngle = -newAngle ElseIf (m_CurPOI = poi_EdgeS) Then @@ -558,10 +659,14 @@ Public Sub TransformCurrentLayer(ByVal curImageX As Double, ByVal curImageY As D ' the transform produces .SetLayerOffsetX m_InitLayerCoords_Pure(0).x + hOffsetImage .SetLayerOffsetY m_InitLayerCoords_Pure(0).y + vOffsetImage + + Dim srcRectF As RectF .GetLayerBoundaryRect srcRectF 'Hand the layer rect off to the snap calculator, then take whatever it returns and - ' forward just the left/top coordinates *back* to the target layer. + ' forward just the left/top coordinates *back* to the target layer (because snapping + ' won't modify the rect's size - only its position). + Dim snappedRectF As RectF SnapRectByMoving srcRectF, snappedRectF .SetLayerOffsetX snappedRectF.Left .SetLayerOffsetY snappedRectF.Top @@ -894,14 +999,14 @@ Public Sub SyncToolOptionsUIToCurrentLayer() With toolpanel_TextBasic .txtTextTool.Text = PDImages.GetActiveImage.GetActiveLayer.GetTextLayerProperty(ptp_Text) .cboTextFontFace.ListIndex = .cboTextFontFace.ListIndexByString(PDImages.GetActiveImage.GetActiveLayer.GetTextLayerProperty(ptp_FontFace), vbTextCompare) - .sldTextFontSize.value = PDImages.GetActiveImage.GetActiveLayer.GetTextLayerProperty(ptp_FontSize) + .sldTextFontSize.Value = PDImages.GetActiveImage.GetActiveLayer.GetTextLayerProperty(ptp_FontSize) .csTextFontColor.Color = PDImages.GetActiveImage.GetActiveLayer.GetTextLayerProperty(ptp_FontColor) .cboTextRenderingHint.ListIndex = PDImages.GetActiveImage.GetActiveLayer.GetTextLayerProperty(ptp_TextAntialiasing) - .sltTextClarity.value = PDImages.GetActiveImage.GetActiveLayer.GetTextLayerProperty(ptp_TextContrast) - .btnFontStyles(0).value = CBool(PDImages.GetActiveImage.GetActiveLayer.GetTextLayerProperty(ptp_FontBold)) - .btnFontStyles(1).value = CBool(PDImages.GetActiveImage.GetActiveLayer.GetTextLayerProperty(ptp_FontItalic)) - .btnFontStyles(2).value = CBool(PDImages.GetActiveImage.GetActiveLayer.GetTextLayerProperty(ptp_FontUnderline)) - .btnFontStyles(3).value = CBool(PDImages.GetActiveImage.GetActiveLayer.GetTextLayerProperty(ptp_FontStrikeout)) + .sltTextClarity.Value = PDImages.GetActiveImage.GetActiveLayer.GetTextLayerProperty(ptp_TextContrast) + .btnFontStyles(0).Value = CBool(PDImages.GetActiveImage.GetActiveLayer.GetTextLayerProperty(ptp_FontBold)) + .btnFontStyles(1).Value = CBool(PDImages.GetActiveImage.GetActiveLayer.GetTextLayerProperty(ptp_FontItalic)) + .btnFontStyles(2).Value = CBool(PDImages.GetActiveImage.GetActiveLayer.GetTextLayerProperty(ptp_FontUnderline)) + .btnFontStyles(3).Value = CBool(PDImages.GetActiveImage.GetActiveLayer.GetTextLayerProperty(ptp_FontStrikeout)) .btsHAlignment.ListIndex = PDImages.GetActiveImage.GetActiveLayer.GetTextLayerProperty(ptp_HorizontalAlignment) .btsVAlignment.ListIndex = PDImages.GetActiveImage.GetActiveLayer.GetTextLayerProperty(ptp_VerticalAlignment) End With @@ -963,8 +1068,8 @@ Public Sub SyncCurrentLayerToToolOptionsUI() Case NAV_MOVE 'The Layer Move tool has four text up/downs: two for layer position (x, y) and two for layer size (w, y) - PDImages.GetActiveImage.GetActiveLayer.SetLayerOffsetX toolpanel_MoveSize.tudLayerMove(0).value - PDImages.GetActiveImage.GetActiveLayer.SetLayerOffsetY toolpanel_MoveSize.tudLayerMove(1).value + PDImages.GetActiveImage.GetActiveLayer.SetLayerOffsetX toolpanel_MoveSize.tudLayerMove(0).Value + PDImages.GetActiveImage.GetActiveLayer.SetLayerOffsetY toolpanel_MoveSize.tudLayerMove(1).Value 'Setting layer width and height isn't activated at present, on purpose 'PDImages.GetActiveImage.getActiveLayer.setLayerWidth toolpanel_MoveSize.tudLayerMove(2).Value @@ -974,23 +1079,23 @@ Public Sub SyncCurrentLayerToToolOptionsUI() PDImages.GetActiveImage.GetActiveLayer.SetLayerResizeQuality toolpanel_MoveSize.cboLayerResizeQuality.ListIndex 'Layer angle and shear are newly available as of 7.0 - PDImages.GetActiveImage.GetActiveLayer.SetLayerAngle toolpanel_MoveSize.sltLayerAngle.value - PDImages.GetActiveImage.GetActiveLayer.SetLayerShearX toolpanel_MoveSize.sltLayerShearX.value - PDImages.GetActiveImage.GetActiveLayer.SetLayerShearY toolpanel_MoveSize.sltLayerShearY.value + PDImages.GetActiveImage.GetActiveLayer.SetLayerAngle toolpanel_MoveSize.sltLayerAngle.Value + PDImages.GetActiveImage.GetActiveLayer.SetLayerShearX toolpanel_MoveSize.sltLayerShearX.Value + PDImages.GetActiveImage.GetActiveLayer.SetLayerShearY toolpanel_MoveSize.sltLayerShearY.Value Case TEXT_BASIC With PDImages.GetActiveImage.GetActiveLayer .SetTextLayerProperty ptp_Text, toolpanel_TextBasic.txtTextTool.Text .SetTextLayerProperty ptp_FontFace, toolpanel_TextBasic.cboTextFontFace.List(toolpanel_TextBasic.cboTextFontFace.ListIndex) - .SetTextLayerProperty ptp_FontSize, toolpanel_TextBasic.sldTextFontSize.value + .SetTextLayerProperty ptp_FontSize, toolpanel_TextBasic.sldTextFontSize.Value .SetTextLayerProperty ptp_FontColor, toolpanel_TextBasic.csTextFontColor.Color .SetTextLayerProperty ptp_TextAntialiasing, toolpanel_TextBasic.cboTextRenderingHint.ListIndex - .SetTextLayerProperty ptp_TextContrast, toolpanel_TextBasic.sltTextClarity.value - .SetTextLayerProperty ptp_FontBold, toolpanel_TextBasic.btnFontStyles(0).value - .SetTextLayerProperty ptp_FontItalic, toolpanel_TextBasic.btnFontStyles(1).value - .SetTextLayerProperty ptp_FontUnderline, toolpanel_TextBasic.btnFontStyles(2).value - .SetTextLayerProperty ptp_FontStrikeout, toolpanel_TextBasic.btnFontStyles(3).value + .SetTextLayerProperty ptp_TextContrast, toolpanel_TextBasic.sltTextClarity.Value + .SetTextLayerProperty ptp_FontBold, toolpanel_TextBasic.btnFontStyles(0).Value + .SetTextLayerProperty ptp_FontItalic, toolpanel_TextBasic.btnFontStyles(1).Value + .SetTextLayerProperty ptp_FontUnderline, toolpanel_TextBasic.btnFontStyles(2).Value + .SetTextLayerProperty ptp_FontStrikeout, toolpanel_TextBasic.btnFontStyles(3).Value .SetTextLayerProperty ptp_HorizontalAlignment, toolpanel_TextBasic.btsHAlignment.ListIndex .SetTextLayerProperty ptp_VerticalAlignment, toolpanel_TextBasic.btsVAlignment.ListIndex End With @@ -1005,39 +1110,39 @@ Public Sub SyncCurrentLayerToToolOptionsUI() With PDImages.GetActiveImage.GetActiveLayer .SetTextLayerProperty ptp_Text, toolpanel_TextAdvanced.txtTextTool.Text .SetTextLayerProperty ptp_FontFace, toolpanel_TextAdvanced.cboTextFontFace.List(toolpanel_TextAdvanced.cboTextFontFace.ListIndex) - .SetTextLayerProperty ptp_FontSize, toolpanel_TextAdvanced.sldTextFontSize.value + .SetTextLayerProperty ptp_FontSize, toolpanel_TextAdvanced.sldTextFontSize.Value .SetTextLayerProperty ptp_StretchToFit, toolpanel_TextAdvanced.btsStretch.ListIndex .SetTextLayerProperty ptp_TextAntialiasing, toolpanel_TextAdvanced.cboTextRenderingHint.ListIndex .SetTextLayerProperty ptp_TextHinting, (toolpanel_TextAdvanced.btsHinting.ListIndex = 1) - .SetTextLayerProperty ptp_FontBold, toolpanel_TextAdvanced.btnFontStyles(0).value - .SetTextLayerProperty ptp_FontItalic, toolpanel_TextAdvanced.btnFontStyles(1).value - .SetTextLayerProperty ptp_FontUnderline, toolpanel_TextAdvanced.btnFontStyles(2).value - .SetTextLayerProperty ptp_FontStrikeout, toolpanel_TextAdvanced.btnFontStyles(3).value + .SetTextLayerProperty ptp_FontBold, toolpanel_TextAdvanced.btnFontStyles(0).Value + .SetTextLayerProperty ptp_FontItalic, toolpanel_TextAdvanced.btnFontStyles(1).Value + .SetTextLayerProperty ptp_FontUnderline, toolpanel_TextAdvanced.btnFontStyles(2).Value + .SetTextLayerProperty ptp_FontStrikeout, toolpanel_TextAdvanced.btnFontStyles(3).Value .SetTextLayerProperty ptp_HorizontalAlignment, toolpanel_TextAdvanced.btsHAlignment.ListIndex .SetTextLayerProperty ptp_VerticalAlignment, toolpanel_TextAdvanced.btsVAlignment.ListIndex .SetTextLayerProperty ptp_WordWrap, toolpanel_TextAdvanced.cboWordWrap.ListIndex - .SetTextLayerProperty ptp_FillActive, toolpanel_TextAdvanced.chkFillText.value + .SetTextLayerProperty ptp_FillActive, toolpanel_TextAdvanced.chkFillText.Value .SetTextLayerProperty ptp_FillBrush, toolpanel_TextAdvanced.bsText.Brush - .SetTextLayerProperty ptp_OutlineActive, toolpanel_TextAdvanced.chkOutlineText.value + .SetTextLayerProperty ptp_OutlineActive, toolpanel_TextAdvanced.chkOutlineText.Value .SetTextLayerProperty ptp_OutlinePen, toolpanel_TextAdvanced.psText.Pen - .SetTextLayerProperty ptp_BackgroundActive, toolpanel_TextAdvanced.chkBackground.value + .SetTextLayerProperty ptp_BackgroundActive, toolpanel_TextAdvanced.chkBackground.Value .SetTextLayerProperty ptp_BackgroundBrush, toolpanel_TextAdvanced.bsTextBackground.Brush - .SetTextLayerProperty ptp_BackBorderActive, toolpanel_TextAdvanced.chkBackgroundBorder.value + .SetTextLayerProperty ptp_BackBorderActive, toolpanel_TextAdvanced.chkBackgroundBorder.Value .SetTextLayerProperty ptp_BackBorderPen, toolpanel_TextAdvanced.psTextBackground.Pen - .SetTextLayerProperty ptp_LineSpacing, toolpanel_TextAdvanced.sldLineSpacing.value - .SetTextLayerProperty ptp_MarginLeft, toolpanel_TextAdvanced.tudMargin(0).value - .SetTextLayerProperty ptp_MarginRight, toolpanel_TextAdvanced.tudMargin(1).value - .SetTextLayerProperty ptp_MarginTop, toolpanel_TextAdvanced.tudMargin(2).value - .SetTextLayerProperty ptp_MarginBottom, toolpanel_TextAdvanced.tudMargin(3).value - .SetTextLayerProperty ptp_CharInflation, toolpanel_TextAdvanced.sltCharInflation.value - .SetTextLayerProperty ptp_CharJitterX, toolpanel_TextAdvanced.tudJitter(0).value - .SetTextLayerProperty ptp_CharJitterY, toolpanel_TextAdvanced.tudJitter(1).value + .SetTextLayerProperty ptp_LineSpacing, toolpanel_TextAdvanced.sldLineSpacing.Value + .SetTextLayerProperty ptp_MarginLeft, toolpanel_TextAdvanced.tudMargin(0).Value + .SetTextLayerProperty ptp_MarginRight, toolpanel_TextAdvanced.tudMargin(1).Value + .SetTextLayerProperty ptp_MarginTop, toolpanel_TextAdvanced.tudMargin(2).Value + .SetTextLayerProperty ptp_MarginBottom, toolpanel_TextAdvanced.tudMargin(3).Value + .SetTextLayerProperty ptp_CharInflation, toolpanel_TextAdvanced.sltCharInflation.Value + .SetTextLayerProperty ptp_CharJitterX, toolpanel_TextAdvanced.tudJitter(0).Value + .SetTextLayerProperty ptp_CharJitterY, toolpanel_TextAdvanced.tudJitter(1).Value .SetTextLayerProperty ptp_CharMirror, toolpanel_TextAdvanced.cboCharMirror.ListIndex - .SetTextLayerProperty ptp_CharOrientation, toolpanel_TextAdvanced.sltCharOrientation.value + .SetTextLayerProperty ptp_CharOrientation, toolpanel_TextAdvanced.sltCharOrientation.Value .SetTextLayerProperty ptp_CharRemap, toolpanel_TextAdvanced.cboCharCase.ListIndex - .SetTextLayerProperty ptp_CharSpacing, toolpanel_TextAdvanced.sltCharSpacing.value + .SetTextLayerProperty ptp_CharSpacing, toolpanel_TextAdvanced.sltCharSpacing.Value .SetTextLayerProperty ptp_AlignLastLine, toolpanel_TextAdvanced.btsHAlignJustify.ListIndex - .SetTextLayerProperty ptp_OutlineAboveFill, toolpanel_TextAdvanced.chkFillFirst.value + .SetTextLayerProperty ptp_OutlineAboveFill, toolpanel_TextAdvanced.chkFillFirst.Value End With 'Advanced text layers are rendered using a PhotoDemon-specific renderer. @@ -1118,11 +1223,11 @@ Public Sub QuickToolAction_HardnessDown() Select Case g_CurrentTool Case PAINT_SOFTBRUSH - curValue = toolpanel_Paintbrush.sltBrushSetting(2).value + curValue = toolpanel_Paintbrush.sltBrushSetting(2).Value Case PAINT_ERASER - curValue = toolpanel_Eraser.sltBrushSetting(2).value + curValue = toolpanel_Eraser.sltBrushSetting(2).Value Case PAINT_CLONE - curValue = toolpanel_Clone.sltBrushSetting(2).value + curValue = toolpanel_Clone.sltBrushSetting(2).Value Case Else Exit Sub End Select @@ -1136,11 +1241,11 @@ Public Sub QuickToolAction_HardnessDown() 'Assign the new value Select Case g_CurrentTool Case PAINT_SOFTBRUSH - toolpanel_Paintbrush.sltBrushSetting(2).value = curValue + toolpanel_Paintbrush.sltBrushSetting(2).Value = curValue Case PAINT_ERASER - toolpanel_Eraser.sltBrushSetting(2).value = curValue + toolpanel_Eraser.sltBrushSetting(2).Value = curValue Case PAINT_CLONE - toolpanel_Clone.sltBrushSetting(2).value = curValue + toolpanel_Clone.sltBrushSetting(2).Value = curValue End Select End Sub @@ -1152,11 +1257,11 @@ Public Sub QuickToolAction_HardnessUp() Select Case g_CurrentTool Case PAINT_SOFTBRUSH - curValue = toolpanel_Paintbrush.sltBrushSetting(2).value + curValue = toolpanel_Paintbrush.sltBrushSetting(2).Value Case PAINT_ERASER - curValue = toolpanel_Eraser.sltBrushSetting(2).value + curValue = toolpanel_Eraser.sltBrushSetting(2).Value Case PAINT_CLONE - curValue = toolpanel_Clone.sltBrushSetting(2).value + curValue = toolpanel_Clone.sltBrushSetting(2).Value Case Else Exit Sub End Select @@ -1170,11 +1275,11 @@ Public Sub QuickToolAction_HardnessUp() 'Assign the new value Select Case g_CurrentTool Case PAINT_SOFTBRUSH - toolpanel_Paintbrush.sltBrushSetting(2).value = curValue + toolpanel_Paintbrush.sltBrushSetting(2).Value = curValue Case PAINT_ERASER - toolpanel_Eraser.sltBrushSetting(2).value = curValue + toolpanel_Eraser.sltBrushSetting(2).Value = curValue Case PAINT_CLONE - toolpanel_Clone.sltBrushSetting(2).value = curValue + toolpanel_Clone.sltBrushSetting(2).Value = curValue End Select End Sub @@ -1188,13 +1293,13 @@ Public Sub QuickToolAction_SizeDown() Select Case g_CurrentTool Case PAINT_PENCIL - curValue = toolpanel_Pencil.sltBrushSetting(0).value + curValue = toolpanel_Pencil.sltBrushSetting(0).Value Case PAINT_SOFTBRUSH - curValue = toolpanel_Paintbrush.sltBrushSetting(0).value + curValue = toolpanel_Paintbrush.sltBrushSetting(0).Value Case PAINT_ERASER - curValue = toolpanel_Eraser.sltBrushSetting(0).value + curValue = toolpanel_Eraser.sltBrushSetting(0).Value Case PAINT_CLONE - curValue = toolpanel_Clone.sltBrushSetting(0).value + curValue = toolpanel_Clone.sltBrushSetting(0).Value Case Else Exit Sub End Select @@ -1218,13 +1323,13 @@ Public Sub QuickToolAction_SizeDown() 'Assign the new value Select Case g_CurrentTool Case PAINT_PENCIL - toolpanel_Pencil.sltBrushSetting(0).value = curValue + toolpanel_Pencil.sltBrushSetting(0).Value = curValue Case PAINT_SOFTBRUSH - toolpanel_Paintbrush.sltBrushSetting(0).value = curValue + toolpanel_Paintbrush.sltBrushSetting(0).Value = curValue Case PAINT_ERASER - toolpanel_Eraser.sltBrushSetting(0).value = curValue + toolpanel_Eraser.sltBrushSetting(0).Value = curValue Case PAINT_CLONE - toolpanel_Clone.sltBrushSetting(0).value = curValue + toolpanel_Clone.sltBrushSetting(0).Value = curValue End Select End Sub @@ -1236,13 +1341,13 @@ Public Sub QuickToolAction_SizeUp() Select Case g_CurrentTool Case PAINT_PENCIL - curValue = toolpanel_Pencil.sltBrushSetting(0).value + curValue = toolpanel_Pencil.sltBrushSetting(0).Value Case PAINT_SOFTBRUSH - curValue = toolpanel_Paintbrush.sltBrushSetting(0).value + curValue = toolpanel_Paintbrush.sltBrushSetting(0).Value Case PAINT_ERASER - curValue = toolpanel_Eraser.sltBrushSetting(0).value + curValue = toolpanel_Eraser.sltBrushSetting(0).Value Case PAINT_CLONE - curValue = toolpanel_Clone.sltBrushSetting(0).value + curValue = toolpanel_Clone.sltBrushSetting(0).Value Case Else Exit Sub End Select @@ -1277,13 +1382,13 @@ Public Sub QuickToolAction_SizeUp() 'Assign the new value Select Case g_CurrentTool Case PAINT_PENCIL - toolpanel_Pencil.sltBrushSetting(0).value = curValue + toolpanel_Pencil.sltBrushSetting(0).Value = curValue Case PAINT_SOFTBRUSH - toolpanel_Paintbrush.sltBrushSetting(0).value = curValue + toolpanel_Paintbrush.sltBrushSetting(0).Value = curValue Case PAINT_ERASER - toolpanel_Eraser.sltBrushSetting(0).value = curValue + toolpanel_Eraser.sltBrushSetting(0).Value = curValue Case PAINT_CLONE - toolpanel_Clone.sltBrushSetting(0).value = curValue + toolpanel_Clone.sltBrushSetting(0).Value = curValue End Select End Sub diff --git a/PhotoDemon.vbp b/PhotoDemon.vbp index 9491561ef..7e38f8724 100644 --- a/PhotoDemon.vbp +++ b/PhotoDemon.vbp @@ -526,7 +526,7 @@ Description="PhotoDemon Photo Editor" CompatibleMode="0" MajorVer=9 MinorVer=1 -RevisionVer=349 +RevisionVer=350 AutoIncrementVer=1 ServerSupportFiles=0 VersionComments="Copyright 2000-2024 Tanner Helland - photodemon.org" From 6ea41700a991be71ea00924d90844b85fffb8ad9 Mon Sep 17 00:00:00 2001 From: Tanner Date: Thu, 11 Apr 2024 11:15:56 -0600 Subject: [PATCH 04/15] New top-level "View > Snap" menu with hotkey support Relates to #498 The `View` menu now provides a top-level `Snap` setting that lets the user toggle snapping completely on or completely off (without losing individual snap-to settings). This matches Photoshop's behavior and is mapped to the same hotkey, `Ctrl+Shift+;`. --- Forms/MainWindow.frm | 23 +++++++++++++++++++--- Forms/Tools_Options.frm | 2 +- Modules/Actions.bas | 4 ++++ Modules/Hotkeys.bas | 1 + Modules/Interface.bas | 30 ++++++++++++++-------------- Modules/Menus.bas | 18 ++++++++++++----- Modules/MoveTool.bas | 43 +++++++++++++++++++++++++++++++---------- Modules/Tools.bas | 14 +++++++------- Modules/UserPrefs.bas | 3 ++- PhotoDemon.vbp | 2 +- 10 files changed, 98 insertions(+), 42 deletions(-) diff --git a/Forms/MainWindow.frm b/Forms/MainWindow.frm index 0dc73d9f1..906b00edd 100644 --- a/Forms/MainWindow.frm +++ b/Forms/MainWindow.frm @@ -1734,9 +1734,17 @@ Begin VB.Form FormMain Index = 8 End Begin VB.Menu MnuView - Caption = "Snap to canvas edges" + Caption = "Snap" Index = 9 End + Begin VB.Menu MnuView + Caption = "Snap to" + Index = 10 + Begin VB.Menu MnuSnap + Caption = "Canvas edges" + Index = 0 + End + End End Begin VB.Menu MnuWindowTop Caption = "Window" @@ -3741,6 +3749,13 @@ Private Sub MnuSharpen_Click(Index As Integer) End Select End Sub +Private Sub MnuSnap_Click(Index As Integer) + Select Case Index + Case 0 + Actions.LaunchAction_ByName "snap_canvasedge" + End Select +End Sub + Private Sub MnuSpecificZoom_Click(Index As Integer) Select Case Index Case 0 @@ -3829,7 +3844,7 @@ Private Sub MnuView_Click(Index As Integer) Case 3 Actions.LaunchAction_ByName "view_zoomout" Case 4 - 'zoom-to-value top-level menu + 'zoom-to-value top-level Case 5 '(separator) Case 6 @@ -3839,7 +3854,9 @@ Private Sub MnuView_Click(Index As Integer) Case 8 '(separator) Case 9 - Actions.LaunchAction_ByName "snap_canvasedge" + Actions.LaunchAction_ByName "snap_global" + Case 10 + 'snap-to top-level End Select End Sub diff --git a/Forms/Tools_Options.frm b/Forms/Tools_Options.frm index 0aca45f75..4e35d8b0e 100644 --- a/Forms/Tools_Options.frm +++ b/Forms/Tools_Options.frm @@ -1206,7 +1206,7 @@ Private Sub cmdBarMini_OKClick() End If UserPrefs.SetPref_Long "Interface", "snap-distance", spnSnapDistance.Value - Tools_Move.SetSnapDistance spnSnapDistance.Value + Tools_Move.SetSnap_Distance spnSnapDistance.Value UserPrefs.SetPref_Long "Transparency", "Alpha Check Mode", CLng(cboAlphaCheck.ListIndex) UserPrefs.SetPref_Long "Transparency", "Alpha Check One", CLng(csAlphaOne.Color) diff --git a/Modules/Actions.bas b/Modules/Actions.bas index a4cfb1492..e69170945 100644 --- a/Modules/Actions.bas +++ b/Modules/Actions.bas @@ -1353,6 +1353,9 @@ Private Function Launch_ByName_MenuView(ByRef srcMenuName As String, Optional By newState = Not FormMain.MainCanvas(0).GetStatusBarVisibility() FormMain.MnuView(7).Checked = newState FormMain.MainCanvas(0).SetStatusBarVisibility newState + + Case "snap_global" + Interface.ToggleSnapOptions pdst_Global Case "snap_canvasedge" Interface.ToggleSnapOptions pdst_CanvasEdge @@ -2002,6 +2005,7 @@ Public Sub BuildActionDatabase() AddAction "zoom_1_16", vbNullString AddAction "view_rulers", vbNullString AddAction "view_statusbar", vbNullString + AddAction "snap_global", vbNullString AddAction "snap_canvasedge", vbNullString 'AddAction "window_toolbox" AddAction "window_displaytoolbox", vbNullString diff --git a/Modules/Hotkeys.bas b/Modules/Hotkeys.bas index 92dd3bf0c..f07d7da07 100644 --- a/Modules/Hotkeys.bas +++ b/Modules/Hotkeys.bas @@ -310,6 +310,7 @@ Public Sub InitializeDefaultHotkeys() Hotkeys.AddHotkey vbKey3, vbShiftMask, "zoom_1_4" Hotkeys.AddHotkey vbKey4, vbShiftMask, "zoom_1_8" Hotkeys.AddHotkey vbKey5, vbShiftMask, "zoom_1_16" + Hotkeys.AddHotkey VK_OEM_1, vbCtrlMask Or vbShiftMask, "snap_global" 'Window menu Hotkeys.AddHotkey vbKeyPageDown, , "window_next" diff --git a/Modules/Interface.bas b/Modules/Interface.bas index 20448afdc..925749394 100644 --- a/Modules/Interface.bas +++ b/Modules/Interface.bas @@ -391,18 +391,18 @@ Public Sub SyncUI_CurrentLayerSettings() nonDestructiveResizeActive = (PDImages.GetActiveImage.GetActiveLayer.GetLayerCanvasXModifier <> 1#) Or (PDImages.GetActiveImage.GetActiveLayer.GetLayerCanvasYModifier <> 1#) 'If non-destructive resizing is active, the "reset layer size" menu (and corresponding Move Tool button) must be enabled. - Menus.SetMenuEnabled "layer_resetsize", nonDestructiveResizeActive + If (Menus.IsMenuEnabled("layer_resetsize") <> nonDestructiveResizeActive) Then Menus.SetMenuEnabled "layer_resetsize", nonDestructiveResizeActive If (g_CurrentTool = NAV_MOVE) Then toolpanel_MoveSize.cmdLayerAffinePermanent.Enabled = PDImages.GetActiveImage.GetActiveLayer.AffineTransformsActive(True) End If 'Layer visibility - Menus.SetMenuChecked "layer_show", PDImages.GetActiveImage.GetActiveLayer.GetLayerVisibility() + If (Menus.IsMenuChecked("layer_show") <> PDImages.GetActiveImage.GetActiveLayer.GetLayerVisibility()) Then Menus.SetMenuChecked "layer_show", PDImages.GetActiveImage.GetActiveLayer.GetLayerVisibility() 'Layer rasterization depends on the current layer type - Menus.SetMenuEnabled "layer_rasterizecurrent", PDImages.GetActiveImage.GetActiveLayer.IsLayerVector - Menus.SetMenuEnabled "layer_rasterizeall", (PDImages.GetActiveImage.GetNumOfVectorLayers > 0) + If (Menus.IsMenuEnabled("layer_rasterizecurrent") <> PDImages.GetActiveImage.GetActiveLayer.IsLayerVector) Then Menus.SetMenuEnabled "layer_rasterizecurrent", PDImages.GetActiveImage.GetActiveLayer.IsLayerVector + If (Menus.IsMenuEnabled("layer_rasterizeall") <> (PDImages.GetActiveImage.GetNumOfVectorLayers > 0)) Then Menus.SetMenuEnabled "layer_rasterizeall", (PDImages.GetActiveImage.GetNumOfVectorLayers > 0) End Sub @@ -732,6 +732,8 @@ Public Sub SetUIGroupState(ByVal metaItem As PD_UI_Group, ByVal newState As Bool 'View (top-menu level) Case PDUI_View Menus.SetMenuEnabled "view_top", newState + Menus.SetMenuChecked "snap_global", Tools_Move.GetSnap_Global() + Menus.SetMenuChecked "snap_canvasedge", Tools_Move.GetSnap_CanvasEdge() 'ImageOps is all Image-related menu items; it enables/disables the Image, Layer, Select, Color, and Print menus. ' (This flag is very useful for items that require at least one open image to operate.) @@ -1214,24 +1216,24 @@ End Sub ' To forcibly set to a specific state (instead of toggling), set the forceInsteadOfToggle param to TRUE. Public Sub ToggleSnapOptions(ByVal snapTarget As PD_SnapTargets, Optional ByVal forceInsteadOfToggle As Boolean = False, Optional ByVal newState As Boolean = True) - 'Convert the snap target into a menu index - Const IDX_BASE As Long = 9 - Dim idxTarget As Long - 'While calculating which on-screen menu to update, we also need to relay changes to two places: ' 1) the tools_move module (which handles actual snap calculations) ' 2) the user preferences file (to ensure everything is synchronized between sessions) Select Case snapTarget + Case pdst_Global + If (Not forceInsteadOfToggle) Then newState = Not Tools_Move.GetSnap_Global() + Tools_Move.SetSnap_Global newState + UserPrefs.SetPref_Boolean "Interface", "snap-global", newState + Menus.SetMenuChecked "snap_global", newState + Case pdst_CanvasEdge - idxTarget = IDX_BASE + 0 - If (Not forceInsteadOfToggle) Then newState = Not Tools_Move.GetSnapCanvasEdge() - Tools_Move.SetSnapCanvasEdge newState + If (Not forceInsteadOfToggle) Then newState = Not Tools_Move.GetSnap_CanvasEdge() + Tools_Move.SetSnap_CanvasEdge newState UserPrefs.SetPref_Boolean "Interface", "snap-canvas-edge", newState + Menus.SetMenuChecked "snap_canvasedge", newState + End Select - 'Update the target menu state - FormMain.MnuView(idxTarget).Checked = newState - End Sub Public Function FixDPI(ByVal pxMeasurement As Long) As Long diff --git a/Modules/Menus.bas b/Modules/Menus.bas index 0e03bd910..b50a39d68 100644 --- a/Modules/Menus.bas +++ b/Modules/Menus.bas @@ -591,7 +591,9 @@ Public Sub InitializeMenus() AddMenuItem "Show rulers", "view_rulers", 8, 6 AddMenuItem "Show status bar", "view_statusbar", 8, 7 AddMenuItem "-", "-", 8, 8 - AddMenuItem "Snap to canvas edge", "snap_canvasedge", 8, 9 + AddMenuItem "Snap", "snap_global", 8, 9 + AddMenuItem "Snap to", "snap_top", 8, 10, allowInSearches:=False + AddMenuItem "Canvas edge", "snap_canvasedge", 8, 10, 0 'Window Menu AddMenuItem "Window", "window_top", 9 @@ -1657,8 +1659,11 @@ End Function 'Helper check for resolving menu enablement by menu name. Note that PD *does not* enforce unique menu names; in fact, they are ' specifically allowed by design. As such, this function only returns the *first* matching entry, with the assumption that ' same-named menus are enabled and disabled as a group. -Public Function SetMenuChecked(ByRef mnuName As String, Optional ByVal isChecked As Boolean = True) As Boolean - +Public Sub SetMenuChecked(ByRef mnuName As String, Optional ByVal isChecked As Boolean = True) + + 'Avoid redundant calls + If (Menus.IsMenuChecked(mnuName) = isChecked) Then Exit Sub + 'Resolve the menu name into an index into our menu collection Dim mnuIndex As Long If GetIndexFromName(mnuName, mnuIndex) Then @@ -1691,10 +1696,13 @@ Public Function SetMenuChecked(ByRef mnuName As String, Optional ByVal isChecked End If -End Function +End Sub Public Sub SetMenuEnabled(ByRef mnuName As String, Optional ByVal isEnabled As Boolean = True) - + + 'Avoid redundant calls + If (Menus.IsMenuEnabled(mnuName) = isEnabled) Then Exit Sub + 'Resolve the menu name into an index into our menu collection Dim mnuIndex As Long If GetIndexFromName(mnuName, mnuIndex) Then diff --git a/Modules/MoveTool.bas b/Modules/MoveTool.bas index de8cc2275..c1b8866ac 100644 --- a/Modules/MoveTool.bas +++ b/Modules/MoveTool.bas @@ -19,18 +19,19 @@ Attribute VB_Name = "Tools_Move" Option Explicit Public Enum PD_SnapTargets - pdst_CanvasEdge + pdst_Global = 0 + pdst_CanvasEdge = 1 End Enum #If False Then - Private Const pdst_CanvasEdge = 0 + Private Const pdst_Global = 0, pdst_CanvasEdge = 1 #End If 'The move/size tool exposes a number of UI-only options (like drawing borders around active layers). ' To improve viewport performance, we cache those settings locally, and the viewport queries us instead ' of directly querying the associated UI elements. Private m_DrawLayerBorders As Boolean, m_DrawCornerNodes As Boolean, m_DrawRotateNodes As Boolean -Private m_SnapToCanvasEdge As Boolean, m_SnapDistance As Long +Private m_SnapGlobal As Boolean, m_SnapToCanvasEdge As Boolean, m_SnapDistance As Long 'Same goes for various selection-related move settings (for moving selected pixels). These are simple ' flags whose value is relayed from the Move/Size options panel. @@ -325,19 +326,37 @@ Public Function GetDrawLayerRotateNodes() As Boolean GetDrawLayerRotateNodes = m_DrawRotateNodes End Function -Public Function GetSnapCanvasEdge() As Boolean - GetSnapCanvasEdge = m_SnapToCanvasEdge +'Returns TRUE if *any* snap-to-edge behaviors are enabled. Useful for skipping all snap checks. +Public Function GetSnap_Any() As Boolean + GetSnap_Any = m_SnapGlobal + If m_SnapGlobal Then + GetSnap_Any = m_SnapToCanvasEdge + 'TODO: OR against other snap options when added + End If +End Function + +Public Function GetSnap_CanvasEdge() As Boolean + GetSnap_CanvasEdge = m_SnapToCanvasEdge End Function -Public Function GetSnapDistance() As Long +Public Function GetSnap_Distance() As Long - GetSnapDistance = m_SnapDistance + GetSnap_Distance = m_SnapDistance 'Failsafe only; should never trigger - If (GetSnapDistance < 1) Then GetSnapDistance = 8 + If (GetSnap_Distance < 1) Then GetSnap_Distance = 8 End Function +'Returns TRUE if the top-level "View > Snap" menu is checked. Note that the user can enable/disable +' individual snap targets regardless of this setting, but if this setting is FALSE, we must ignore all +' other snap options. (This is how Photoshop behaves; the top-level Snap setting is mapped to a +' keyboard accelerator so the user can quickly enable/disable snap behavior without losing current +' per-target snap settings.) +Public Function GetSnap_Global() As Boolean + GetSnap_Global = m_SnapGlobal +End Function + Public Sub SetDrawLayerBorders(ByVal newState As Boolean) m_DrawLayerBorders = newState End Sub @@ -350,16 +369,20 @@ Public Sub SetDrawLayerRotateNodes(ByVal newState As Boolean) m_DrawRotateNodes = newState End Sub -Public Sub SetSnapCanvasEdge(ByVal newState As Boolean) +Public Sub SetSnap_CanvasEdge(ByVal newState As Boolean) m_SnapToCanvasEdge = newState End Sub -Public Sub SetSnapDistance(ByVal newDistance As Long) +Public Sub SetSnap_Distance(ByVal newDistance As Long) m_SnapDistance = newDistance If (m_SnapDistance < 1) Then m_SnapDistance = 1 If (m_SnapDistance > 255) Then m_SnapDistance = 255 'GIMP uses a 255 max value; that seems reasonable End Sub +Public Sub SetSnap_Global(ByVal newState As Boolean) + m_SnapGlobal = newState +End Sub + 'Relay functions for move selected pixels behavior Public Function GetMoveSelectedPixels_DefaultCut() As Boolean GetMoveSelectedPixels_DefaultCut = m_SelectionDefaultCut diff --git a/Modules/Tools.bas b/Modules/Tools.bas index 12442f64f..1097b11d2 100644 --- a/Modules/Tools.bas +++ b/Modules/Tools.bas @@ -257,7 +257,7 @@ Private Sub SnapPointByMoving(ByRef srcPointF As PointFloat, ByRef dstPointF As dstPointF = srcPointF 'Skip any further processing if the user hasn't enabled snapping - If (Not Tools_Move.GetSnapCanvasEdge()) Then Exit Sub + If (Not Tools_Move.GetSnap_Any()) Then Exit Sub 'Start by constructing a list of potential snap targets, based on current user settings. Dim xSnaps() As SnapComparison, ySnaps() As SnapComparison, numXSnaps As Long, numYSnaps As Long @@ -299,7 +299,7 @@ Private Sub SnapPointByMoving(ByRef srcPointF As PointFloat, ByRef dstPointF As 'Determine the minimum snap distance required for this zoom value. Dim snapThreshold As Double - snapThreshold = Tools_Move.GetSnapDistance() * (1# / Zoom.GetZoomRatioFromIndex(PDImages.GetActiveImage.ImgViewport.GetZoomIndex)) + snapThreshold = Tools_Move.GetSnap_Distance() * (1# / Zoom.GetZoomRatioFromIndex(PDImages.GetActiveImage.ImgViewport.GetZoomIndex)) 'If the minimum value falls beneath the minimum snap distance, snap away! If (minDistX < snapThreshold) Then dstPointF.x = xSnaps(idxSmallestX).cValue @@ -316,7 +316,7 @@ Private Sub SnapRectByMoving(ByRef srcRectF As RectF, ByRef dstRectF As RectF) dstRectF = srcRectF 'Skip any further processing if the user hasn't enabled snapping - If (Not Tools_Move.GetSnapCanvasEdge()) Then Exit Sub + If (Not Tools_Move.GetSnap_Any()) Then Exit Sub 'Start by constructing a list of potential snap targets, based on current user settings. Dim xSnaps() As SnapComparison, ySnaps() As SnapComparison, numXSnaps As Long, numYSnaps As Long @@ -376,7 +376,7 @@ Private Sub SnapRectByMoving(ByRef srcRectF As RectF, ByRef dstRectF As RectF) 'Determine the minimum snap distance required for this zoom value. Dim snapThreshold As Double - snapThreshold = Tools_Move.GetSnapDistance() * (1# / Zoom.GetZoomRatioFromIndex(PDImages.GetActiveImage.ImgViewport.GetZoomIndex)) + snapThreshold = Tools_Move.GetSnap_Distance() * (1# / Zoom.GetZoomRatioFromIndex(PDImages.GetActiveImage.ImgViewport.GetZoomIndex)) 'If the minimum value falls beneath the minimum snap distance, snap away! If (minDistX < snapThreshold) Then @@ -406,7 +406,7 @@ Private Function GetSnapTargets_X(ByRef dstSnaps() As SnapComparison) As Long GetSnapTargets_X = 0 'Canvas edges first - If Tools_Move.GetSnapCanvasEdge() Then + If Tools_Move.GetSnap_CanvasEdge() Then 'Ensure at space is available in the target array If (UBound(dstSnaps) < GetSnapTargets_X + 1) Then ReDim Preserve dstSnaps(0 To GetSnapTargets_X * 2 - 1) As SnapComparison @@ -429,7 +429,7 @@ Private Function GetSnapTargets_Y(ByRef dstSnaps() As SnapComparison) As Long GetSnapTargets_Y = 0 'Canvas edges first - If Tools_Move.GetSnapCanvasEdge() Then + If Tools_Move.GetSnap_CanvasEdge() Then 'Ensure at space is available in the target array If (UBound(dstSnaps) < GetSnapTargets_Y + 1) Then ReDim Preserve dstSnaps(0 To GetSnapTargets_Y * 2 - 1) As SnapComparison @@ -463,7 +463,7 @@ Public Sub TransformCurrentLayer(ByVal curImageX As Double, ByVal curImageY As D ' the mouse pointer (e.g. the layer edges, which are not located at the mouse position), so we'll need to wait ' to snap until the transform has been applied to the underlying layer. Dim srcPtF As PointFloat, snappedPtF As PointFloat - If Tools_Move.GetSnapCanvasEdge() Then + If Tools_Move.GetSnap_Any() Then Select Case m_CurPOI Case poi_CornerNW, poi_CornerNE, poi_CornerSW, poi_CornerSE diff --git a/Modules/UserPrefs.bas b/Modules/UserPrefs.bas index 57f5bfd58..aa1b574e1 100644 --- a/Modules/UserPrefs.bas +++ b/Modules/UserPrefs.bas @@ -609,8 +609,9 @@ Public Sub LoadUserSettings() Tools.SetToolSetting_HighResMouse UserPrefs.GetPref_Boolean("Tools", "HighResMouseInput", True) m_CanvasColor = Colors.GetRGBLongFromHex(UserPrefs.GetPref_String("Interface", "CanvasColor", "#a0a0a0")) + Interface.ToggleSnapOptions pdst_Global, True, UserPrefs.GetPref_Boolean("Interface", "snap-global", True) Interface.ToggleSnapOptions pdst_CanvasEdge, True, UserPrefs.GetPref_Boolean("Interface", "snap-canvas-edge", True) - Tools_Move.SetSnapDistance UserPrefs.GetPref_Long("Interface", "snap-distance", 8&) + Tools_Move.SetSnap_Distance UserPrefs.GetPref_Long("Interface", "snap-distance", 8&) 'Users can supply a (secret!) "UIFont" setting in the "Interface" segment if they ' want to override PD's default font object. diff --git a/PhotoDemon.vbp b/PhotoDemon.vbp index 7e38f8724..f01a0989c 100644 --- a/PhotoDemon.vbp +++ b/PhotoDemon.vbp @@ -526,7 +526,7 @@ Description="PhotoDemon Photo Editor" CompatibleMode="0" MajorVer=9 MinorVer=1 -RevisionVer=350 +RevisionVer=352 AutoIncrementVer=1 ServerSupportFiles=0 VersionComments="Copyright 2000-2024 Tanner Helland - photodemon.org" From 4830c3765e5f1129ab842e24ebc641eb62ca8df0 Mon Sep 17 00:00:00 2001 From: Tanner Date: Tue, 16 Apr 2024 07:33:24 -0600 Subject: [PATCH 05/15] Snap: rework to support rotated/skewed layers... ...when moving. Layers with affine transforms are now snapped correctly! --- Modules/Tools.bas | 91 +++++++++++++++++++++++++++++++++++++++++++---- PhotoDemon.vbp | 2 +- 2 files changed, 85 insertions(+), 8 deletions(-) diff --git a/Modules/Tools.bas b/Modules/Tools.bas index 1097b11d2..27d468ed6 100644 --- a/Modules/Tools.bas +++ b/Modules/Tools.bas @@ -307,6 +307,75 @@ Private Sub SnapPointByMoving(ByRef srcPointF As PointFloat, ByRef dstPointF As End Sub +'Given a list of points, compare each to all snap points and find the *best* match among them. Based on that, +' return the x/y offset required to move the best-match point onto the snap target. +Private Sub SnapPointListByMoving(ByRef srcPoints() As PointFloat, ByVal numOfPoints As Long, ByRef dstOffsetX As Long, ByRef dstOffsetY As Long) + + dstOffsetX = 0 + dstOffsetY = 0 + + 'Failsafe only; caller (in PD) will never set this to <= 0 + If (numOfPoints <= 0) Then Exit Sub + + 'Skip any further processing if the user hasn't enabled snapping + If (Not Tools_Move.GetSnap_Any()) Then Exit Sub + + 'Start by constructing a list of potential snap targets, based on current user settings. + Dim xSnaps() As SnapComparison, ySnaps() As SnapComparison, numXSnaps As Long, numYSnaps As Long + numXSnaps = GetSnapTargets_X(xSnaps) + numYSnaps = GetSnapTargets_Y(ySnaps) + + 'Ensure some snap targets exist + If (numXSnaps = 0) Or (numYSnaps = 0) Then Exit Sub + + 'We now have a list of snap comparison targets. We don't care what these targets represent - + ' we just want to find the "best" one from each list. + Dim idxSmallestX As Long, idxSmallestPointX As Long, minDistX As Double + + 'Set the minimum distance to an arbitrarily huge number, then find minimum x-distances + minDistX = DOUBLE_MAX + + Dim i As Long, j As Long + For j = 0 To numOfPoints - 1 + For i = 0 To numXSnaps - 1 + With xSnaps(i) + .cDistance1 = PDMath.DistanceOneDimension(srcPoints(j).x, .cValue) + If (.cDistance1 < minDistX) Then + minDistX = .cDistance1 + idxSmallestX = i + idxSmallestPointX = j + End If + End With + Next i + Next j + + 'Repeat all the above steps for y-coordinates + Dim idxSmallestY As Long, idxSmallestPointY As Long, minDistY As Double + minDistY = DOUBLE_MAX + + For j = 0 To numOfPoints - 1 + For i = 0 To numYSnaps - 1 + With ySnaps(i) + .cDistance1 = PDMath.DistanceOneDimension(srcPoints(j).y, .cValue) + If (.cDistance1 < minDistY) Then + minDistY = .cDistance1 + idxSmallestY = i + idxSmallestPointY = j + End If + End With + Next i + Next j + + 'Determine the minimum snap distance required for this zoom value. + Dim snapThreshold As Double + snapThreshold = Tools_Move.GetSnap_Distance() * (1# / Zoom.GetZoomRatioFromIndex(PDImages.GetActiveImage.ImgViewport.GetZoomIndex)) + + 'If the minimum value falls beneath the minimum snap distance, snap away! + If (minDistX < snapThreshold) Then dstOffsetX = (xSnaps(idxSmallestX).cValue - srcPoints(idxSmallestPointX).x) + If (minDistY < snapThreshold) Then dstOffsetY = (ySnaps(idxSmallestY).cValue - srcPoints(idxSmallestPointY).y) + +End Sub + 'Snap the passed rect to any relevant snap targets (based on the user's current snap settings). ' Because this function snaps only by moving the target rect, it is guaranteed that only the ' top and left values will be changed by the function (width/height will *not*). @@ -418,6 +487,8 @@ Private Function GetSnapTargets_X(ByRef dstSnaps() As SnapComparison) As Long End If + 'TODO: more snap targets in the future... + End Function 'Get a list of current y-snap targets (determined by user settings). @@ -441,6 +512,8 @@ Private Function GetSnapTargets_Y(ByRef dstSnaps() As SnapComparison) As Long End If + 'TODO: more snap targets in the future... + End Function 'This function can be used to move and/or non-destructively resize an image layer. @@ -663,13 +736,17 @@ Public Sub TransformCurrentLayer(ByVal curImageX As Double, ByVal curImageY As D Dim srcRectF As RectF .GetLayerBoundaryRect srcRectF - 'Hand the layer rect off to the snap calculator, then take whatever it returns and - ' forward just the left/top coordinates *back* to the target layer (because snapping - ' won't modify the rect's size - only its position). - Dim snappedRectF As RectF - SnapRectByMoving srcRectF, snappedRectF - .SetLayerOffsetX snappedRectF.Left - .SetLayerOffsetY snappedRectF.Top + Dim listOfCorners() As PointFloat + ReDim listOfCorners(0 To 3) As PointFloat + .GetLayerCornerCoordinates listOfCorners + + Dim snapOffsetX As Long, snapOffsetY As Long + SnapPointListByMoving listOfCorners, 4, snapOffsetX, snapOffsetY + + 'Hand the layer corners off to the snap calculator, then take whatever it returns and + ' forward the original left/top position + snapped offsets to the source layer + .SetLayerOffsetX .GetLayerOffsetX + snapOffsetX + .SetLayerOffsetY .GetLayerOffsetY + snapOffsetY End Select diff --git a/PhotoDemon.vbp b/PhotoDemon.vbp index f01a0989c..0a2b3d3db 100644 --- a/PhotoDemon.vbp +++ b/PhotoDemon.vbp @@ -526,7 +526,7 @@ Description="PhotoDemon Photo Editor" CompatibleMode="0" MajorVer=9 MinorVer=1 -RevisionVer=352 +RevisionVer=354 AutoIncrementVer=1 ServerSupportFiles=0 VersionComments="Copyright 2000-2024 Tanner Helland - photodemon.org" From 15e779dd2b633eb241e5931b527297aecb8aba71 Mon Sep 17 00:00:00 2001 From: Tanner Date: Tue, 16 Apr 2024 09:22:14 -0600 Subject: [PATCH 06/15] Localizations for new `View > Snap` feature --- App/PhotoDemon/Languages/French.xml | 34 ++++++++++++++---- App/PhotoDemon/Languages/German.xml | 34 ++++++++++++++---- App/PhotoDemon/Languages/Indonesian.xml | 34 ++++++++++++++---- App/PhotoDemon/Languages/Italian.xml | 34 ++++++++++++++---- App/PhotoDemon/Languages/Macedonian.xml | 34 ++++++++++++++---- App/PhotoDemon/Languages/Master/MASTER.xml | 34 ++++++++++++++---- App/PhotoDemon/Languages/Master/Phrases.db | Bin 176653 -> 176780 bytes App/PhotoDemon/Languages/Polish.xml | 34 ++++++++++++++---- .../Languages/Simplified_Chinese.xml | 34 ++++++++++++++---- App/PhotoDemon/Languages/Spanish_(Mexico).xml | 34 ++++++++++++++---- App/PhotoDemon/Languages/Spanish_(Spain).xml | 34 ++++++++++++++---- .../Languages/Traditional_Chinese.xml | 34 ++++++++++++++---- App/PhotoDemon/Languages/Turkish.xml | 34 ++++++++++++++---- App/PhotoDemon/Languages/Vlaams.xml | 34 ++++++++++++++---- PhotoDemon.vbp | 6 ++-- 15 files changed, 354 insertions(+), 94 deletions(-) diff --git a/App/PhotoDemon/Languages/French.xml b/App/PhotoDemon/Languages/French.xml index 2b904eeb4..e698b0f49 100644 --- a/App/PhotoDemon/Languages/French.xml +++ b/App/PhotoDemon/Languages/French.xml @@ -6,7 +6,7 @@ fr-FR Français -6.7.617 +6.7.618 Complete Jean Jacques Piedfort (orig. Frank Donckers) @@ -5544,6 +5544,21 @@ La valeur finale doit être entre %3 and %4. Montrer la barre des statuts + +Snap + + + + +Snap to + + + + +Canvas edges + + + Window Fenêtre @@ -5747,7 +5762,7 @@ La mise à jour est traitée automatiquement en arrière-plan. Vous recevrez une Langue changée avec succès. - + Monochrome Conversion @@ -13377,6 +13392,11 @@ Pour continuer, veuillez télécharger une copie récente de PhotoDemon sur phot couleur d'arrière plan de la zone de travail + +snap distance (in pixels) + + + when images arrive from an external source (like Windows Explorer) quand des images viennent d'une source extérieure (tel que l'Explorateur de Windows). @@ -14048,7 +14068,7 @@ Si vous choisissez de désactiver les mises à jour, n'oubliez pas de visiter ph Ce nouvel emplacement de dossier temporaire ne prendra effet qu'après le redémarrage du programme. - + Reset all library options @@ -14258,10 +14278,10 @@ Si vous choisissez de désactiver les mises à jour, n'oubliez pas de visiter ph -2695 +2699 - - - + + + \ No newline at end of file diff --git a/App/PhotoDemon/Languages/German.xml b/App/PhotoDemon/Languages/German.xml index 7fe4e6655..ccb91b27e 100644 --- a/App/PhotoDemon/Languages/German.xml +++ b/App/PhotoDemon/Languages/German.xml @@ -6,7 +6,7 @@ de-DE Deutsch (German) -9.2.312 +9.2.313 Up-to-date rk (ehem. Frank Donckers, Helmut Kuerbiss) @@ -5545,6 +5545,21 @@ Der finale Wert muss zwischen %3 und %4 liegen. Statusleiste zeigen + +Snap + + + + +Snap to + + + + +Canvas edges + + + Window Fenster @@ -5748,7 +5763,7 @@ Das Update wird automatisch im Hintergrund verarbeitet. Sie werden eine neue Ben Sprache erfolgreich geändert. - + Monochrome Conversion @@ -13370,6 +13385,11 @@ Um fortzusetzen, laden Sie bitte eine neue Version von PhotoDemon von photodemon Leinwand-Hintergrundfarbe + +snap distance (in pixels) + + + when images arrive from an external source (like Windows Explorer) Wenn Bilder aus einer externen Quelle (wie Windows-Explorer) ankommen @@ -14032,7 +14052,7 @@ Wenn Sie sich dennoch dafür entscheiden, Updates zu deaktivieren, vergessen Sie Dieser neue Ort des temporären Ordners wird erst wirksam, wenn Sie das Programm neu starten. - + Reset all library options @@ -14242,10 +14262,10 @@ Wenn Sie sich dennoch dafür entscheiden, Updates zu deaktivieren, vergessen Sie -2695 +2699 - - - + + + \ No newline at end of file diff --git a/App/PhotoDemon/Languages/Indonesian.xml b/App/PhotoDemon/Languages/Indonesian.xml index fd5df69be..5a2ec5470 100644 --- a/App/PhotoDemon/Languages/Indonesian.xml +++ b/App/PhotoDemon/Languages/Indonesian.xml @@ -6,7 +6,7 @@ id-ID Bahasa Indonesia (ID) -8.9.1717 +8.9.1718 Terselesaikan Ari Sohandri Putra @@ -5540,6 +5540,21 @@ The final value must be between %3 and %4. + +Snap + + + + +Snap to + + + + +Canvas edges + + + Window jendela @@ -5743,7 +5758,7 @@ Pembaruan sedang diproses secara otomatis di latar belakang. Anda akan menerima Bahasa berhasil diubah. - + Monochrome Conversion @@ -13366,6 +13381,11 @@ Untuk melanjutkan, unduh salinan PhotoDemon yang diperbarui dari photodemon.org. + +snap distance (in pixels) + + + when images arrive from an external source (like Windows Explorer) @@ -14028,7 +14048,7 @@ Jika Anda masih memilih untuk menonaktifkan pembaruan, pastikan untuk mengunjung Lokasi folder sementara yang baru ini tidak akan berlaku sampai Anda memulai ulang program. - + Reset all library options @@ -14238,10 +14258,10 @@ Jika Anda masih memilih untuk menonaktifkan pembaruan, pastikan untuk mengunjung -2695 +2699 - - - + + + \ No newline at end of file diff --git a/App/PhotoDemon/Languages/Italian.xml b/App/PhotoDemon/Languages/Italian.xml index 57a13b83c..a1308409f 100644 --- a/App/PhotoDemon/Languages/Italian.xml +++ b/App/PhotoDemon/Languages/Italian.xml @@ -6,7 +6,7 @@ it-IT Italiano -8.9.1634 +8.9.1635 Completa GioRock, ManfroMarce @@ -5544,6 +5544,21 @@ Il valore finale deve essere compreso tra %3 e %4. Mostra barra di stato + +Snap + + + + +Snap to + + + + +Canvas edges + + + Window Finestra @@ -5747,7 +5762,7 @@ L'aggiornamento viene elaborato automaticamente in background. Riceverai una nu Lingua modificata con successo. - + Monochrome Conversion @@ -13371,6 +13386,11 @@ Per continuare, si prega di scaricare una nuova copia di PhotoDemon da photodemo colore di sfondo della tela + +snap distance (in pixels) + + + when images arrive from an external source (like Windows Explorer) quando le immagini arrivano da una fonte esterna (come Esplora risorse) @@ -14033,7 +14053,7 @@ Se si sceglie comunque di disabilitare gli aggiornamenti, non dimenticate di vis La nuova posizione della cartella temporanea non avrà effetto finché non si riavvia il programma. - + Reset all library options @@ -14243,10 +14263,10 @@ Se si sceglie comunque di disabilitare gli aggiornamenti, non dimenticate di vis -2695 +2699 - - - + + + \ No newline at end of file diff --git a/App/PhotoDemon/Languages/Macedonian.xml b/App/PhotoDemon/Languages/Macedonian.xml index 3543e73a3..d0bad6483 100644 --- a/App/PhotoDemon/Languages/Macedonian.xml +++ b/App/PhotoDemon/Languages/Macedonian.xml @@ -6,7 +6,7 @@ mk-MK Македонски -8.9.1725 +8.9.1726 80% complete Бобан Ѓерасимоски @@ -5535,6 +5535,21 @@ The final value must be between %3 and %4. + +Snap + + + + +Snap to + + + + +Canvas edges + + + Window прозорец @@ -5738,7 +5753,7 @@ The update is automatically processing in the background. You will receive a ne Јазик успешно променет. - + Monochrome Conversion @@ -13362,6 +13377,11 @@ To continue, please download a fresh copy of PhotoDemon from photodemon.org. + +snap distance (in pixels) + + + when images arrive from an external source (like Windows Explorer) @@ -14020,7 +14040,7 @@ If сеуште изберете да го исклучите ажурирања Оваа нова локација привремена папка нема да се случи се додека не рестартирам програмата. - + Reset all library options @@ -14230,10 +14250,10 @@ If сеуште изберете да го исклучите ажурирања -2695 +2699 - - - + + + \ No newline at end of file diff --git a/App/PhotoDemon/Languages/Master/MASTER.xml b/App/PhotoDemon/Languages/Master/MASTER.xml index f4cfec408..ce4408ef9 100644 --- a/App/PhotoDemon/Languages/Master/MASTER.xml +++ b/App/PhotoDemon/Languages/Master/MASTER.xml @@ -6,7 +6,7 @@ en-US English (US) - MASTER COPY - 9.1.347 + 9.1.355 Automatically generated from PhotoDemon's source code Tanner Helland @@ -5496,6 +5496,21 @@ The final value must be between %3 and %4. + +Snap + + + + +Snap to + + + + +Canvas edges + + + Window @@ -5695,7 +5710,7 @@ The update is automatically processing in the background. You will receive a ne - + Monochrome Conversion @@ -13309,6 +13324,11 @@ To continue, please download a fresh copy of PhotoDemon from photodemon.org. + +snap distance (in pixels) + + + when images arrive from an external source (like Windows Explorer) @@ -13965,7 +13985,7 @@ If you still choose to disable updates, don't forget to visit photodemon.org fro - + Reset all library options @@ -14175,10 +14195,10 @@ If you still choose to disable updates, don't forget to visit photodemon.org fro - 2695 + 2699 - - - + + + \ No newline at end of file diff --git a/App/PhotoDemon/Languages/Master/Phrases.db b/App/PhotoDemon/Languages/Master/Phrases.db index 787b085d6f3ad9832aef569c0f7246ff8d743f79..4443f8972260acf3352e9f7833d67b2e37f0377b 100644 GIT binary patch delta 123 zcmeBu!qxMXi@zWxAfqU;IMv08k%57sdn14KCpH#_;Jn0w$%?N;lz_BCNj@KgZ(?R% zcxGNoez{&+Q7$77gL7hDSz@t5YRcq?&qbQMK5g&%#OU89CCN|>(vp%{T#}fVoT{La YnWs>YS&^DkJbmIkCXwwoDws?y0hoF$>;M1& delta 41 xcmeBq%GLXXi@zWxAfqU;IMv08k%57seItMMr{-Iqw%_{1=-;+IqLRtP5&&Mb5f=ae diff --git a/App/PhotoDemon/Languages/Polish.xml b/App/PhotoDemon/Languages/Polish.xml index 081e9ca1c..e8d4beda4 100644 --- a/App/PhotoDemon/Languages/Polish.xml +++ b/App/PhotoDemon/Languages/Polish.xml @@ -6,7 +6,7 @@ pl-PL Polski -9.0.24 +9.0.25 100% complete Ryszard @@ -5542,6 +5542,21 @@ Ostateczna wartość musi zawierać się w przedziale od %3 do %4. Pokaż pasek stanu + +Snap + + + + +Snap to + + + + +Canvas edges + + + Window Okno @@ -5742,7 +5757,7 @@ The update is automatically processing in the background. You will receive a ne Język został pomyślnie zmieniony. - + Monochrome Conversion @@ -13370,6 +13385,11 @@ To continue, please download a fresh copy of PhotoDemon from photodemon.org.Kolor tła płótna + +snap distance (in pixels) + + + when images arrive from an external source (like Windows Explorer) Gdy obrazy przychodzą z zewnętrznego źródła (np. Windows Explorer) @@ -14032,7 +14052,7 @@ Jeśli nadal decydujesz się na wyłączenie aktualizacji, nie zapomnij odwiedzi Nowa lokalizacja folderu tymczasowego zostanie zastosowana dopiero po ponownym uruchomieniu programu. - + Reset all library options @@ -14242,10 +14262,10 @@ Jeśli nadal decydujesz się na wyłączenie aktualizacji, nie zapomnij odwiedzi -2695 +2699 - - - + + + \ No newline at end of file diff --git a/App/PhotoDemon/Languages/Simplified_Chinese.xml b/App/PhotoDemon/Languages/Simplified_Chinese.xml index 7620aeabc..b2f628a1a 100644 --- a/App/PhotoDemon/Languages/Simplified_Chinese.xml +++ b/App/PhotoDemon/Languages/Simplified_Chinese.xml @@ -6,7 +6,7 @@ zh-CN 简体中文(夜间更新版,9.2 build 311) -9.2 build 311.2 +9.2 build 311.3 完成 Charltsing(QQ 564955427) revised on March 15, 2024, ChenLin(QQ:289778005), Lsbdx at 52pojie.cn, shishi @@ -5540,6 +5540,21 @@ The final value must be between %3 and %4. 显示状态栏 + +Snap + + + + +Snap to + + + + +Canvas edges + + + Window 窗口 @@ -5742,7 +5757,7 @@ The update is automatically processing in the background. You will receive a ne 语言切换成功。 - + Monochrome Conversion @@ -13366,6 +13381,11 @@ To continue, please download a fresh copy of PhotoDemon from photodemon.org.画布背景色 + +snap distance (in pixels) + + + when images arrive from an external source (like Windows Explorer) 当图像来自外部来源(如Windows资源管理器)时。 @@ -14028,7 +14048,7 @@ If you still choose to disable updates, don't forget to visit photodemon.org fro 新的临时文件夹目录将在重新启动程序后生效。 - + Reset all library options @@ -14238,11 +14258,11 @@ If you still choose to disable updates, don't forget to visit photodemon.org fro -2695 +2699 - - - + + + diff --git a/App/PhotoDemon/Languages/Spanish_(Mexico).xml b/App/PhotoDemon/Languages/Spanish_(Mexico).xml index aa692dec5..662ec65f8 100644 --- a/App/PhotoDemon/Languages/Spanish_(Mexico).xml +++ b/App/PhotoDemon/Languages/Spanish_(Mexico).xml @@ -6,7 +6,7 @@ es-MX español (México) -9.0.28 +9.0.29 completo Plinio C Garcia, with help from DeepL.com @@ -5545,6 +5545,21 @@ El valor final debe estar entre %3 y %4. Mostrar barra de estado + +Snap + + + + +Snap to + + + + +Canvas edges + + + Window Ventana @@ -5748,7 +5763,7 @@ La actualización se está procesando automáticamente en segundo plano. Recibi Idioma cambiado correctamente. - + Monochrome Conversion @@ -13372,6 +13387,11 @@ Para continuar, descargue una copia nueva de PhotoDemon desde photodemon.org.color de fondo del lienzo + +snap distance (in pixels) + + + when images arrive from an external source (like Windows Explorer) cuando las imágenes llegan de una fuente externa @@ -14034,7 +14054,7 @@ If usted todavía elige desactivar las actualizaciones, no se olvide de visitar Esta nueva ubicación de la carpeta temporal no tendrá efecto hasta que reinicie el programa. - + Reset all library options @@ -14244,10 +14264,10 @@ If usted todavía elige desactivar las actualizaciones, no se olvide de visitar -2695 +2699 - - - + + + \ No newline at end of file diff --git a/App/PhotoDemon/Languages/Spanish_(Spain).xml b/App/PhotoDemon/Languages/Spanish_(Spain).xml index 999b0edf3..6af71918b 100644 --- a/App/PhotoDemon/Languages/Spanish_(Spain).xml +++ b/App/PhotoDemon/Languages/Spanish_(Spain).xml @@ -6,7 +6,7 @@ es-ES español (España) -6.7.339 +6.7.340 completo Tecnorama @@ -5538,6 +5538,21 @@ The final value must be between %3 and %4. + +Snap + + + + +Snap to + + + + +Canvas edges + + + Window Ventanas @@ -5741,7 +5756,7 @@ La actualización se está procesando automáticamente en segundo plano. Recibir Idioma cambiado correctamente. - + Monochrome Conversion @@ -13365,6 +13380,11 @@ Para continuar, descargue una copia actualizada de PhotoDemon desde photodemon.o + +snap distance (in pixels) + + + when images arrive from an external source (like Windows Explorer) @@ -14027,7 +14047,7 @@ Si, con todo y con ello, elige desactivar las actualizaciones, no olvide visitar Esta nueva ubicación de la carpeta temporal no surtirá efecto hasta que reinicie el programa. - + Reset all library options @@ -14237,10 +14257,10 @@ Si, con todo y con ello, elige desactivar las actualizaciones, no olvide visitar -2695 +2699 - - - + + + \ No newline at end of file diff --git a/App/PhotoDemon/Languages/Traditional_Chinese.xml b/App/PhotoDemon/Languages/Traditional_Chinese.xml index a85e9f414..112774489 100644 --- a/App/PhotoDemon/Languages/Traditional_Chinese.xml +++ b/App/PhotoDemon/Languages/Traditional_Chinese.xml @@ -6,7 +6,7 @@ zh-TW 繁體中文 -7.0.299 +7.0.300 incomplete Chiahong Hong @@ -5503,6 +5503,21 @@ The final value must be between %3 and %4. 顯示狀態列 + +Snap + + + + +Snap to + + + + +Canvas edges + + + Window 視窗 @@ -5706,7 +5721,7 @@ The update is automatically processing in the background. You will receive a ne 語言切換成功。 - + Monochrome Conversion @@ -13320,6 +13335,11 @@ To continue, please download a fresh copy of PhotoDemon from photodemon.org. + +snap distance (in pixels) + + + when images arrive from an external source (like Windows Explorer) @@ -13978,7 +13998,7 @@ If you still choose to disable updates, don't forget to visit photodemon.org fro - + Reset all library options @@ -14188,10 +14208,10 @@ If you still choose to disable updates, don't forget to visit photodemon.org fro -2695 +2699 - - - + + + \ No newline at end of file diff --git a/App/PhotoDemon/Languages/Turkish.xml b/App/PhotoDemon/Languages/Turkish.xml index 5bd0ebf01..42a562bb6 100644 --- a/App/PhotoDemon/Languages/Turkish.xml +++ b/App/PhotoDemon/Languages/Turkish.xml @@ -6,7 +6,7 @@ tr-TR Türkçe (Turkish) -1.0.28 +1.0.29 20% complete Anıl Yılmaz @@ -5521,6 +5521,21 @@ The final value must be between %3 and %4. + +Snap + + + + +Snap to + + + + +Canvas edges + + + Window Pencere @@ -5720,7 +5735,7 @@ The update is automatically processing in the background. You will receive a ne - + Monochrome Conversion @@ -13334,6 +13349,11 @@ To continue, please download a fresh copy of PhotoDemon from photodemon.org. + +snap distance (in pixels) + + + when images arrive from an external source (like Windows Explorer) @@ -13990,7 +14010,7 @@ If you still choose to disable updates, don't forget to visit photodemon.org fro - + Reset all library options @@ -14200,10 +14220,10 @@ If you still choose to disable updates, don't forget to visit photodemon.org fro -2695 +2699 - - - + + + \ No newline at end of file diff --git a/App/PhotoDemon/Languages/Vlaams.xml b/App/PhotoDemon/Languages/Vlaams.xml index b5c0bf7f6..447b54cd5 100644 --- a/App/PhotoDemon/Languages/Vlaams.xml +++ b/App/PhotoDemon/Languages/Vlaams.xml @@ -6,7 +6,7 @@ nl-BE Vlaams (Nederlands) -8.9.1725 +8.9.1726 80% complete Frank Donckers @@ -5545,6 +5545,21 @@ De uiteindelijke waarde moet tussen %3 en %4 liggen. Statusbalk tonen + +Snap + + + + +Snap to + + + + +Canvas edges + + + Window Venster @@ -5748,7 +5763,7 @@ De update wordt automatisch verwerkt op de achtergrond. U krijgt een nieuwe mede Taal succesvol gewijzigd. - + Monochrome Conversion @@ -13372,6 +13387,11 @@ Download een nieuwe kopij van PhotoDemon vanop photodemon.org. om verder te gaan + +snap distance (in pixels) + + + when images arrive from an external source (like Windows Explorer) wanneer afbeeldingen van een externe bron komen (zoals Windows Verkenner) @@ -14034,7 +14054,7 @@ If u nog steeds kiezen om updates uit te schakelen, vergeet dan niet om photodem Deze nieuwe tijdelijke maplocatie zal geen effect hebben voordat je het programma herstart hebt - + Reset all library options @@ -14244,10 +14264,10 @@ If u nog steeds kiezen om updates uit te schakelen, vergeet dan niet om photodem -2695 +2699 - - - + + + \ No newline at end of file diff --git a/PhotoDemon.vbp b/PhotoDemon.vbp index 0a2b3d3db..4aa00ce83 100644 --- a/PhotoDemon.vbp +++ b/PhotoDemon.vbp @@ -1,6 +1,6 @@ Type=Exe -Reference=*\G{50A7E9B0-70EF-11D1-B75A-00A0C90564FE}#1.0#0#\\?\C:\Windows\SysWOW64\shell32.dll#Microsoft Shell Controls And Automation -Reference=*\G{00020430-0000-0000-C000-000000000046}#2.0#0#\\?\C:\Windows\SysWOW64\stdole2.tlb#OLE Automation +Reference=*\G{50A7E9B0-70EF-11D1-B75A-00A0C90564FE}#1.0#0#..\..\Windows\SysWOW64\shell32.dll#Microsoft Shell Controls And Automation +Reference=*\G{00020430-0000-0000-C000-000000000046}#2.0#0#..\..\Windows\SysWOW64\stdole2.tlb#OLE Automation Module=PDMain; Modules\Main.bas Module=Public_Constants; Modules\PublicConstants.bas Module=Public_EnumsAndTypes; Modules\PublicEnumsAndTypes.bas @@ -526,7 +526,7 @@ Description="PhotoDemon Photo Editor" CompatibleMode="0" MajorVer=9 MinorVer=1 -RevisionVer=354 +RevisionVer=355 AutoIncrementVer=1 ServerSupportFiles=0 VersionComments="Copyright 2000-2024 Tanner Helland - photodemon.org" From 4031c7657ca90ba7c16b2353a8dff72a6ca376d3 Mon Sep 17 00:00:00 2001 From: Tanner Date: Tue, 16 Apr 2024 14:07:14 -0600 Subject: [PATCH 07/15] Snap engine: refactor to prep for selection tool support --- Forms/Tools_Options.frm | 2 +- Modules/Actions.bas | 4 +- Modules/Interface.bas | 28 +-- Modules/MoveTool.bas | 55 ------ Modules/Snap.bas | 379 ++++++++++++++++++++++++++++++++++++++++ Modules/Tools.bas | 310 ++------------------------------ Modules/UserPrefs.bas | 6 +- PhotoDemon.vbp | 7 +- 8 files changed, 409 insertions(+), 382 deletions(-) create mode 100644 Modules/Snap.bas diff --git a/Forms/Tools_Options.frm b/Forms/Tools_Options.frm index 4e35d8b0e..6e300a76d 100644 --- a/Forms/Tools_Options.frm +++ b/Forms/Tools_Options.frm @@ -1206,7 +1206,7 @@ Private Sub cmdBarMini_OKClick() End If UserPrefs.SetPref_Long "Interface", "snap-distance", spnSnapDistance.Value - Tools_Move.SetSnap_Distance spnSnapDistance.Value + Snap.SetSnap_Distance spnSnapDistance.Value UserPrefs.SetPref_Long "Transparency", "Alpha Check Mode", CLng(cboAlphaCheck.ListIndex) UserPrefs.SetPref_Long "Transparency", "Alpha Check One", CLng(csAlphaOne.Color) diff --git a/Modules/Actions.bas b/Modules/Actions.bas index e69170945..5aa581f46 100644 --- a/Modules/Actions.bas +++ b/Modules/Actions.bas @@ -1355,10 +1355,10 @@ Private Function Launch_ByName_MenuView(ByRef srcMenuName As String, Optional By FormMain.MainCanvas(0).SetStatusBarVisibility newState Case "snap_global" - Interface.ToggleSnapOptions pdst_Global + Snap.ToggleSnapOptions pdst_Global Case "snap_canvasedge" - Interface.ToggleSnapOptions pdst_CanvasEdge + Snap.ToggleSnapOptions pdst_CanvasEdge Case Else cmdFound = False diff --git a/Modules/Interface.bas b/Modules/Interface.bas index 925749394..7fb0955ae 100644 --- a/Modules/Interface.bas +++ b/Modules/Interface.bas @@ -732,8 +732,8 @@ Public Sub SetUIGroupState(ByVal metaItem As PD_UI_Group, ByVal newState As Bool 'View (top-menu level) Case PDUI_View Menus.SetMenuEnabled "view_top", newState - Menus.SetMenuChecked "snap_global", Tools_Move.GetSnap_Global() - Menus.SetMenuChecked "snap_canvasedge", Tools_Move.GetSnap_CanvasEdge() + Menus.SetMenuChecked "snap_global", Snap.GetSnap_Global() + Menus.SetMenuChecked "snap_canvasedge", Snap.GetSnap_CanvasEdge() 'ImageOps is all Image-related menu items; it enables/disables the Image, Layer, Select, Color, and Print menus. ' (This flag is very useful for items that require at least one open image to operate.) @@ -1212,30 +1212,6 @@ Public Sub ToggleImageTabstripVisibility(ByVal newSetting As Long, Optional ByVa End Sub -'Toggle one of the "snap to..." settings in the View menu. -' To forcibly set to a specific state (instead of toggling), set the forceInsteadOfToggle param to TRUE. -Public Sub ToggleSnapOptions(ByVal snapTarget As PD_SnapTargets, Optional ByVal forceInsteadOfToggle As Boolean = False, Optional ByVal newState As Boolean = True) - - 'While calculating which on-screen menu to update, we also need to relay changes to two places: - ' 1) the tools_move module (which handles actual snap calculations) - ' 2) the user preferences file (to ensure everything is synchronized between sessions) - Select Case snapTarget - Case pdst_Global - If (Not forceInsteadOfToggle) Then newState = Not Tools_Move.GetSnap_Global() - Tools_Move.SetSnap_Global newState - UserPrefs.SetPref_Boolean "Interface", "snap-global", newState - Menus.SetMenuChecked "snap_global", newState - - Case pdst_CanvasEdge - If (Not forceInsteadOfToggle) Then newState = Not Tools_Move.GetSnap_CanvasEdge() - Tools_Move.SetSnap_CanvasEdge newState - UserPrefs.SetPref_Boolean "Interface", "snap-canvas-edge", newState - Menus.SetMenuChecked "snap_canvasedge", newState - - End Select - -End Sub - Public Function FixDPI(ByVal pxMeasurement As Long) As Long 'The first time this function is called, m_DPIRatio will be 0. Calculate it. diff --git a/Modules/MoveTool.bas b/Modules/MoveTool.bas index c1b8866ac..46973199b 100644 --- a/Modules/MoveTool.bas +++ b/Modules/MoveTool.bas @@ -18,20 +18,10 @@ Attribute VB_Name = "Tools_Move" Option Explicit -Public Enum PD_SnapTargets - pdst_Global = 0 - pdst_CanvasEdge = 1 -End Enum - -#If False Then - Private Const pdst_Global = 0, pdst_CanvasEdge = 1 -#End If - 'The move/size tool exposes a number of UI-only options (like drawing borders around active layers). ' To improve viewport performance, we cache those settings locally, and the viewport queries us instead ' of directly querying the associated UI elements. Private m_DrawLayerBorders As Boolean, m_DrawCornerNodes As Boolean, m_DrawRotateNodes As Boolean -Private m_SnapGlobal As Boolean, m_SnapToCanvasEdge As Boolean, m_SnapDistance As Long 'Same goes for various selection-related move settings (for moving selected pixels). These are simple ' flags whose value is relayed from the Move/Size options panel. @@ -326,37 +316,6 @@ Public Function GetDrawLayerRotateNodes() As Boolean GetDrawLayerRotateNodes = m_DrawRotateNodes End Function -'Returns TRUE if *any* snap-to-edge behaviors are enabled. Useful for skipping all snap checks. -Public Function GetSnap_Any() As Boolean - GetSnap_Any = m_SnapGlobal - If m_SnapGlobal Then - GetSnap_Any = m_SnapToCanvasEdge - 'TODO: OR against other snap options when added - End If -End Function - -Public Function GetSnap_CanvasEdge() As Boolean - GetSnap_CanvasEdge = m_SnapToCanvasEdge -End Function - -Public Function GetSnap_Distance() As Long - - GetSnap_Distance = m_SnapDistance - - 'Failsafe only; should never trigger - If (GetSnap_Distance < 1) Then GetSnap_Distance = 8 - -End Function - -'Returns TRUE if the top-level "View > Snap" menu is checked. Note that the user can enable/disable -' individual snap targets regardless of this setting, but if this setting is FALSE, we must ignore all -' other snap options. (This is how Photoshop behaves; the top-level Snap setting is mapped to a -' keyboard accelerator so the user can quickly enable/disable snap behavior without losing current -' per-target snap settings.) -Public Function GetSnap_Global() As Boolean - GetSnap_Global = m_SnapGlobal -End Function - Public Sub SetDrawLayerBorders(ByVal newState As Boolean) m_DrawLayerBorders = newState End Sub @@ -369,20 +328,6 @@ Public Sub SetDrawLayerRotateNodes(ByVal newState As Boolean) m_DrawRotateNodes = newState End Sub -Public Sub SetSnap_CanvasEdge(ByVal newState As Boolean) - m_SnapToCanvasEdge = newState -End Sub - -Public Sub SetSnap_Distance(ByVal newDistance As Long) - m_SnapDistance = newDistance - If (m_SnapDistance < 1) Then m_SnapDistance = 1 - If (m_SnapDistance > 255) Then m_SnapDistance = 255 'GIMP uses a 255 max value; that seems reasonable -End Sub - -Public Sub SetSnap_Global(ByVal newState As Boolean) - m_SnapGlobal = newState -End Sub - 'Relay functions for move selected pixels behavior Public Function GetMoveSelectedPixels_DefaultCut() As Boolean GetMoveSelectedPixels_DefaultCut = m_SelectionDefaultCut diff --git a/Modules/Snap.bas b/Modules/Snap.bas new file mode 100644 index 000000000..102441277 --- /dev/null +++ b/Modules/Snap.bas @@ -0,0 +1,379 @@ +Attribute VB_Name = "Snap" +'*************************************************************************** +'Snap-to-target Handler +'Copyright 2024-2024 by Tanner Helland +'Created: 16/April/24 +'Last updated: 16/April/24 +'Last update: migrate all snap setting and behavior management to one central place +' +'In 2024, snap-to-target support was added to various PhotoDemon tools. Thank you to all the users +' who suggested this feature! +' +'Unless otherwise noted, all source code in this file is shared under a simplified BSD license. +' Full license details are available in the LICENSE.md file, or at https://photodemon.org/license/ +' +'*************************************************************************** + +Option Explicit + +Public Enum PD_SnapTargets + pdst_Global = 0 + pdst_CanvasEdge = 1 +End Enum + +#If False Then + Private Const pdst_Global = 0, pdst_CanvasEdge = 1 +#End If + +'When snapping coordinates, we need to compare all possible snap targets and choose the best independent +' x and y snap coordinate (assuming they fall beneath the snap threshold for the current zoom level). +' Two distances are provided: one each for left/right (or top/bottom). +Private Type SnapComparison + cValue As Double + cDistance1 As Double + cDistance2 As Double +End Type + +'To improve performance, snap-to settings are cached locally (instead of traveling out to +' the user preference engine on every call). +Private m_SnapGlobal As Boolean, m_SnapToCanvasEdge As Boolean, m_SnapDistance As Long + +'Returns TRUE if *any* snap-to-edge behaviors are enabled. Useful for skipping all snap checks. +Public Function GetSnap_Any() As Boolean + GetSnap_Any = m_SnapGlobal + If m_SnapGlobal Then + GetSnap_Any = m_SnapToCanvasEdge + 'TODO: OR against other snap options when added + End If +End Function + +Public Function GetSnap_CanvasEdge() As Boolean + GetSnap_CanvasEdge = m_SnapToCanvasEdge +End Function + +Public Function GetSnap_Distance() As Long + + GetSnap_Distance = m_SnapDistance + + 'Failsafe only; should never trigger + If (GetSnap_Distance < 1) Then GetSnap_Distance = 8 + +End Function + +'Returns TRUE if the top-level "View > Snap" menu is checked. Note that the user can enable/disable +' individual snap targets regardless of this setting, but if this setting is FALSE, we must ignore all +' other snap options. (This is how Photoshop behaves; the top-level Snap setting is mapped to a +' keyboard accelerator so the user can quickly enable/disable snap behavior without losing current +' per-target snap settings.) +Public Function GetSnap_Global() As Boolean + GetSnap_Global = m_SnapGlobal +End Function + +Public Sub SetSnap_CanvasEdge(ByVal newState As Boolean) + m_SnapToCanvasEdge = newState +End Sub + +Public Sub SetSnap_Distance(ByVal newDistance As Long) + m_SnapDistance = newDistance + If (m_SnapDistance < 1) Then m_SnapDistance = 1 + If (m_SnapDistance > 255) Then m_SnapDistance = 255 'GIMP uses a 255 max value; that seems reasonable +End Sub + +Public Sub SetSnap_Global(ByVal newState As Boolean) + m_SnapGlobal = newState +End Sub + +'Toggle one of the "snap to..." settings in the View menu. +' To forcibly set to a specific state (instead of toggling), set the forceInsteadOfToggle param to TRUE. +Public Sub ToggleSnapOptions(ByVal snapTarget As PD_SnapTargets, Optional ByVal forceInsteadOfToggle As Boolean = False, Optional ByVal newState As Boolean = True) + + 'While calculating which on-screen menu to update, we also need to relay changes to two places: + ' 1) the tools_move module (which handles actual snap calculations) + ' 2) the user preferences file (to ensure everything is synchronized between sessions) + Select Case snapTarget + Case pdst_Global + If (Not forceInsteadOfToggle) Then newState = Not Snap.GetSnap_Global() + Snap.SetSnap_Global newState + UserPrefs.SetPref_Boolean "Interface", "snap-global", newState + Menus.SetMenuChecked "snap_global", newState + + Case pdst_CanvasEdge + If (Not forceInsteadOfToggle) Then newState = Not Snap.GetSnap_CanvasEdge() + Snap.SetSnap_CanvasEdge newState + UserPrefs.SetPref_Boolean "Interface", "snap-canvas-edge", newState + Menus.SetMenuChecked "snap_canvasedge", newState + + End Select + +End Sub + +'Snap the passed point to any relevant snap targets (based on the user's current snap settings). +Public Sub SnapPointByMoving(ByRef srcPointF As PointFloat, ByRef dstPointF As PointFloat) + + 'If no snap targets exist (because the user has disabled snapping), ensure the destination point + ' mirrors the source point + dstPointF = srcPointF + + 'Skip any further processing if the user hasn't enabled snapping + If (Not Snap.GetSnap_Any()) Then Exit Sub + + 'Start by constructing a list of potential snap targets, based on current user settings. + Dim xSnaps() As SnapComparison, ySnaps() As SnapComparison, numXSnaps As Long, numYSnaps As Long + numXSnaps = GetSnapTargets_X(xSnaps) + numYSnaps = GetSnapTargets_Y(ySnaps) + + 'Ensure some snap targets exist + If (numXSnaps = 0) Or (numYSnaps = 0) Then Exit Sub + + 'We now have a list of snap comparison targets. We don't care what these targets represent - + ' we just want to find the "best" one from each list. + Dim i As Long, idxSmallestX As Long, minDistX As Double + + 'Set the minimum distance to an arbitrarily huge number, then find minimum x-distances + minDistX = DOUBLE_MAX + For i = 0 To numXSnaps - 1 + With xSnaps(i) + .cDistance1 = PDMath.DistanceOneDimension(srcPointF.x, .cValue) + If (.cDistance1 < minDistX) Then + minDistX = .cDistance1 + idxSmallestX = i + End If + End With + Next i + + 'Repeat all the above steps for y-coordinates + Dim idxSmallestY As Long, minDistY As Double + minDistY = DOUBLE_MAX + + For i = 0 To numYSnaps - 1 + With ySnaps(i) + .cDistance1 = PDMath.DistanceOneDimension(srcPointF.y, .cValue) + If (.cDistance1 < minDistY) Then + minDistY = .cDistance1 + idxSmallestY = i + End If + End With + Next i + + 'Determine the minimum snap distance required for this zoom value. + Dim snapThreshold As Double + snapThreshold = GetSnapDistanceScaledForZoom() + + 'If the minimum value falls beneath the minimum snap distance, snap away! + If (minDistX < snapThreshold) Then dstPointF.x = xSnaps(idxSmallestX).cValue + If (minDistY < snapThreshold) Then dstPointF.y = ySnaps(idxSmallestY).cValue + +End Sub + +'Given a list of points, compare each to all snap points and find the *best* match among them. Based on that, +' return the x/y offset required to move the best-match point onto the snap target. +Public Sub SnapPointListByMoving(ByRef srcPoints() As PointFloat, ByVal numOfPoints As Long, ByRef dstOffsetX As Long, ByRef dstOffsetY As Long) + + dstOffsetX = 0 + dstOffsetY = 0 + + 'Failsafe only; caller (in PD) will never set this to <= 0 + If (numOfPoints <= 0) Then Exit Sub + + 'Skip any further processing if the user hasn't enabled snapping + If (Not Snap.GetSnap_Any()) Then Exit Sub + + 'Start by constructing a list of potential snap targets, based on current user settings. + Dim xSnaps() As SnapComparison, ySnaps() As SnapComparison, numXSnaps As Long, numYSnaps As Long + numXSnaps = GetSnapTargets_X(xSnaps) + numYSnaps = GetSnapTargets_Y(ySnaps) + + 'Ensure some snap targets exist + If (numXSnaps = 0) Or (numYSnaps = 0) Then Exit Sub + + 'We now have a list of snap comparison targets. We don't care what these targets represent - + ' we just want to find the "best" one from each list. + Dim idxSmallestX As Long, idxSmallestPointX As Long, minDistX As Double + + 'Set the minimum distance to an arbitrarily huge number, then find minimum x-distances + minDistX = DOUBLE_MAX + + Dim i As Long, j As Long + For j = 0 To numOfPoints - 1 + For i = 0 To numXSnaps - 1 + With xSnaps(i) + .cDistance1 = PDMath.DistanceOneDimension(srcPoints(j).x, .cValue) + If (.cDistance1 < minDistX) Then + minDistX = .cDistance1 + idxSmallestX = i + idxSmallestPointX = j + End If + End With + Next i + Next j + + 'Repeat all the above steps for y-coordinates + Dim idxSmallestY As Long, idxSmallestPointY As Long, minDistY As Double + minDistY = DOUBLE_MAX + + For j = 0 To numOfPoints - 1 + For i = 0 To numYSnaps - 1 + With ySnaps(i) + .cDistance1 = PDMath.DistanceOneDimension(srcPoints(j).y, .cValue) + If (.cDistance1 < minDistY) Then + minDistY = .cDistance1 + idxSmallestY = i + idxSmallestPointY = j + End If + End With + Next i + Next j + + 'Determine the minimum snap distance required for this zoom value. + Dim snapThreshold As Double + snapThreshold = GetSnapDistanceScaledForZoom() + + 'If the minimum value falls beneath the minimum snap distance, snap away! + If (minDistX < snapThreshold) Then dstOffsetX = (xSnaps(idxSmallestX).cValue - srcPoints(idxSmallestPointX).x) + If (minDistY < snapThreshold) Then dstOffsetY = (ySnaps(idxSmallestY).cValue - srcPoints(idxSmallestPointY).y) + +End Sub + +'Snap the passed rect to any relevant snap targets (based on the user's current snap settings). +' Because this function snaps only by moving the target rect, it is guaranteed that only the +' top and left values will be changed by the function (width/height will *not*). +Public Sub SnapRectByMoving(ByRef srcRectF As RectF, ByRef dstRectF As RectF) + + 'By default, return the same rect. (This is important if the user has disabled snapping.) + dstRectF = srcRectF + + 'Skip any further processing if the user hasn't enabled snapping + If (Not Snap.GetSnap_Any()) Then Exit Sub + + 'Start by constructing a list of potential snap targets, based on current user settings. + Dim xSnaps() As SnapComparison, ySnaps() As SnapComparison, numXSnaps As Long, numYSnaps As Long + numXSnaps = GetSnapTargets_X(xSnaps) + numYSnaps = GetSnapTargets_Y(ySnaps) + + 'Ensure some snap targets exist + If (numXSnaps = 0) Or (numYSnaps = 0) Then Exit Sub + + 'We now have a list of snap comparison targets. We don't care what these targets represent - + ' we just want to find the "best" one from each list. + + 'Convert the source snap rectangle into a right/bottom rect (instead of a default width/height one) + Dim compareRectF As RectF_RB + compareRectF.Left = srcRectF.Left + compareRectF.Top = srcRectF.Top + compareRectF.Right = srcRectF.Left + srcRectF.Width - 1 + compareRectF.Bottom = srcRectF.Top + srcRectF.Height - 1 + + Dim i As Long, idxSmallestX As Long, minDistX As Double + + 'Set the minimum distance to an arbitrarily huge number, then find the smallest x-distance + minDistX = DOUBLE_MAX + For i = 0 To numXSnaps - 1 + With xSnaps(i) + .cDistance1 = PDMath.DistanceOneDimension(compareRectF.Left, .cValue) + If (.cDistance1 < minDistX) Then + minDistX = .cDistance1 + idxSmallestX = i + End If + .cDistance2 = PDMath.DistanceOneDimension(compareRectF.Right, .cValue) + If (.cDistance2 < minDistX) Then + minDistX = .cDistance2 + idxSmallestX = i + End If + End With + Next i + + 'Repeat all the above steps for y-coordinates + Dim idxSmallestY As Long, minDistY As Double + minDistY = DOUBLE_MAX + + For i = 0 To numYSnaps - 1 + With ySnaps(i) + .cDistance1 = PDMath.DistanceOneDimension(compareRectF.Top, .cValue) + If (.cDistance1 < minDistY) Then + minDistY = .cDistance1 + idxSmallestY = i + End If + .cDistance2 = PDMath.DistanceOneDimension(compareRectF.Bottom, .cValue) + If (.cDistance2 < minDistY) Then + minDistY = .cDistance2 + idxSmallestY = i + End If + End With + Next i + + 'Determine the minimum snap distance required for this zoom value. + Dim snapThreshold As Double + snapThreshold = GetSnapDistanceScaledForZoom() + + 'If the minimum value falls beneath the minimum snap distance, snap away! + If (minDistX < snapThreshold) Then + If (xSnaps(idxSmallestX).cDistance1 < xSnaps(idxSmallestX).cDistance2) Then + dstRectF.Left = xSnaps(idxSmallestX).cValue + Else + dstRectF.Left = xSnaps(idxSmallestX).cValue - dstRectF.Width + End If + End If + + If (minDistY < snapThreshold) Then + If (ySnaps(idxSmallestY).cDistance1 < ySnaps(idxSmallestY).cDistance2) Then + dstRectF.Top = ySnaps(idxSmallestY).cValue + Else + dstRectF.Top = ySnaps(idxSmallestY).cValue - dstRectF.Height + End If + End If + +End Sub + +Private Function GetSnapDistanceScaledForZoom() As Double + GetSnapDistanceScaledForZoom = Snap.GetSnap_Distance() * (1# / Zoom.GetZoomRatioFromIndex(PDImages.GetActiveImage.ImgViewport.GetZoomIndex)) +End Function + +'Get a list of current x-snap targets (determined by user settings). +' RETURNS: number of entries in the list, or 0 if snapping is disabled by the user. +Private Function GetSnapTargets_X(ByRef dstSnaps() As SnapComparison) As Long + + 'Start with some arbitrarily sized list (these will be enlarged as necessary) + ReDim dstSnaps(0 To 15) As SnapComparison + GetSnapTargets_X = 0 + + 'Canvas edges first + If Snap.GetSnap_CanvasEdge() Then + + 'Ensure at space is available in the target array + If (UBound(dstSnaps) < GetSnapTargets_X + 1) Then ReDim Preserve dstSnaps(0 To GetSnapTargets_X * 2 - 1) As SnapComparison + + 'Add canvas boundaries to the snap list + dstSnaps(GetSnapTargets_X).cValue = 0# + dstSnaps(GetSnapTargets_X + 1).cValue = PDImages.GetActiveImage.Width + GetSnapTargets_X = GetSnapTargets_X + 2 + + End If + + 'TODO: more snap targets in the future... + +End Function + +'Get a list of current y-snap targets (determined by user settings). +' RETURNS: number of entries in the list, or 0 if snapping is disabled by the user. +Private Function GetSnapTargets_Y(ByRef dstSnaps() As SnapComparison) As Long + + 'Start with some arbitrarily sized list (these will be enlarged as necessary) + ReDim dstSnaps(0 To 15) As SnapComparison + GetSnapTargets_Y = 0 + + 'Canvas edges first + If Snap.GetSnap_CanvasEdge() Then + + 'Ensure at space is available in the target array + If (UBound(dstSnaps) < GetSnapTargets_Y + 1) Then ReDim Preserve dstSnaps(0 To GetSnapTargets_Y * 2 - 1) As SnapComparison + + 'Add canvas boundaries to the snap list + dstSnaps(GetSnapTargets_Y).cValue = 0# + dstSnaps(GetSnapTargets_Y + 1).cValue = PDImages.GetActiveImage.Height + GetSnapTargets_Y = GetSnapTargets_Y + 2 + + End If + + 'TODO: more snap targets in the future... + +End Function diff --git a/Modules/Tools.bas b/Modules/Tools.bas index 27d468ed6..e845da02a 100644 --- a/Modules/Tools.bas +++ b/Modules/Tools.bas @@ -72,15 +72,6 @@ Private m_MiddleMouseState As Boolean ' original one.) Private m_MoveSelectedPixels As Boolean -'When snapping coordinates, we need to compare all possible snap targets and choose the best independent -' x and y snap coordinate (assuming they fall beneath the snap threshold for the current zoom level). -' Two distances are provided: one each for left/right (or top/bottom). -Private Type SnapComparison - cValue As Double - cDistance1 As Double - cDistance2 As Double -End Type - 'Get/Set the "alternate" state for a paint tool (typically triggered by pressing ALT) Public Function GetToolAltState() As Boolean GetToolAltState = m_PaintToolAltState @@ -249,273 +240,6 @@ Public Sub PanImageCanvas(ByVal initX As Long, ByVal initY As Long, ByVal curX A End Sub -'Snap the passed point to any relevant snap targets (based on the user's current snap settings). -Private Sub SnapPointByMoving(ByRef srcPointF As PointFloat, ByRef dstPointF As PointFloat) - - 'If no snap targets exist (because the user has disabled snapping), ensure the destination point - ' mirrors the source point - dstPointF = srcPointF - - 'Skip any further processing if the user hasn't enabled snapping - If (Not Tools_Move.GetSnap_Any()) Then Exit Sub - - 'Start by constructing a list of potential snap targets, based on current user settings. - Dim xSnaps() As SnapComparison, ySnaps() As SnapComparison, numXSnaps As Long, numYSnaps As Long - numXSnaps = GetSnapTargets_X(xSnaps) - numYSnaps = GetSnapTargets_Y(ySnaps) - - 'Ensure some snap targets exist - If (numXSnaps = 0) Or (numYSnaps = 0) Then Exit Sub - - 'We now have a list of snap comparison targets. We don't care what these targets represent - - ' we just want to find the "best" one from each list. - Dim i As Long, idxSmallestX As Long, minDistX As Double - - 'Set the minimum distance to an arbitrarily huge number, then find minimum x-distances - minDistX = DOUBLE_MAX - For i = 0 To numXSnaps - 1 - With xSnaps(i) - .cDistance1 = PDMath.DistanceOneDimension(srcPointF.x, .cValue) - If (.cDistance1 < minDistX) Then - minDistX = .cDistance1 - idxSmallestX = i - End If - End With - Next i - - 'Repeat all the above steps for y-coordinates - Dim idxSmallestY As Long, minDistY As Double - minDistY = DOUBLE_MAX - - For i = 0 To numYSnaps - 1 - With ySnaps(i) - .cDistance1 = PDMath.DistanceOneDimension(srcPointF.y, .cValue) - If (.cDistance1 < minDistY) Then - minDistY = .cDistance1 - idxSmallestY = i - End If - End With - Next i - - 'Determine the minimum snap distance required for this zoom value. - Dim snapThreshold As Double - snapThreshold = Tools_Move.GetSnap_Distance() * (1# / Zoom.GetZoomRatioFromIndex(PDImages.GetActiveImage.ImgViewport.GetZoomIndex)) - - 'If the minimum value falls beneath the minimum snap distance, snap away! - If (minDistX < snapThreshold) Then dstPointF.x = xSnaps(idxSmallestX).cValue - If (minDistY < snapThreshold) Then dstPointF.y = ySnaps(idxSmallestY).cValue - -End Sub - -'Given a list of points, compare each to all snap points and find the *best* match among them. Based on that, -' return the x/y offset required to move the best-match point onto the snap target. -Private Sub SnapPointListByMoving(ByRef srcPoints() As PointFloat, ByVal numOfPoints As Long, ByRef dstOffsetX As Long, ByRef dstOffsetY As Long) - - dstOffsetX = 0 - dstOffsetY = 0 - - 'Failsafe only; caller (in PD) will never set this to <= 0 - If (numOfPoints <= 0) Then Exit Sub - - 'Skip any further processing if the user hasn't enabled snapping - If (Not Tools_Move.GetSnap_Any()) Then Exit Sub - - 'Start by constructing a list of potential snap targets, based on current user settings. - Dim xSnaps() As SnapComparison, ySnaps() As SnapComparison, numXSnaps As Long, numYSnaps As Long - numXSnaps = GetSnapTargets_X(xSnaps) - numYSnaps = GetSnapTargets_Y(ySnaps) - - 'Ensure some snap targets exist - If (numXSnaps = 0) Or (numYSnaps = 0) Then Exit Sub - - 'We now have a list of snap comparison targets. We don't care what these targets represent - - ' we just want to find the "best" one from each list. - Dim idxSmallestX As Long, idxSmallestPointX As Long, minDistX As Double - - 'Set the minimum distance to an arbitrarily huge number, then find minimum x-distances - minDistX = DOUBLE_MAX - - Dim i As Long, j As Long - For j = 0 To numOfPoints - 1 - For i = 0 To numXSnaps - 1 - With xSnaps(i) - .cDistance1 = PDMath.DistanceOneDimension(srcPoints(j).x, .cValue) - If (.cDistance1 < minDistX) Then - minDistX = .cDistance1 - idxSmallestX = i - idxSmallestPointX = j - End If - End With - Next i - Next j - - 'Repeat all the above steps for y-coordinates - Dim idxSmallestY As Long, idxSmallestPointY As Long, minDistY As Double - minDistY = DOUBLE_MAX - - For j = 0 To numOfPoints - 1 - For i = 0 To numYSnaps - 1 - With ySnaps(i) - .cDistance1 = PDMath.DistanceOneDimension(srcPoints(j).y, .cValue) - If (.cDistance1 < minDistY) Then - minDistY = .cDistance1 - idxSmallestY = i - idxSmallestPointY = j - End If - End With - Next i - Next j - - 'Determine the minimum snap distance required for this zoom value. - Dim snapThreshold As Double - snapThreshold = Tools_Move.GetSnap_Distance() * (1# / Zoom.GetZoomRatioFromIndex(PDImages.GetActiveImage.ImgViewport.GetZoomIndex)) - - 'If the minimum value falls beneath the minimum snap distance, snap away! - If (minDistX < snapThreshold) Then dstOffsetX = (xSnaps(idxSmallestX).cValue - srcPoints(idxSmallestPointX).x) - If (minDistY < snapThreshold) Then dstOffsetY = (ySnaps(idxSmallestY).cValue - srcPoints(idxSmallestPointY).y) - -End Sub - -'Snap the passed rect to any relevant snap targets (based on the user's current snap settings). -' Because this function snaps only by moving the target rect, it is guaranteed that only the -' top and left values will be changed by the function (width/height will *not*). -Private Sub SnapRectByMoving(ByRef srcRectF As RectF, ByRef dstRectF As RectF) - - 'By default, return the same rect. (This is important if the user has disabled snapping.) - dstRectF = srcRectF - - 'Skip any further processing if the user hasn't enabled snapping - If (Not Tools_Move.GetSnap_Any()) Then Exit Sub - - 'Start by constructing a list of potential snap targets, based on current user settings. - Dim xSnaps() As SnapComparison, ySnaps() As SnapComparison, numXSnaps As Long, numYSnaps As Long - numXSnaps = GetSnapTargets_X(xSnaps) - numYSnaps = GetSnapTargets_Y(ySnaps) - - 'Ensure some snap targets exist - If (numXSnaps = 0) Or (numYSnaps = 0) Then Exit Sub - - 'We now have a list of snap comparison targets. We don't care what these targets represent - - ' we just want to find the "best" one from each list. - - 'Convert the source snap rectangle into a right/bottom rect (instead of a default width/height one) - Dim compareRectF As RectF_RB - compareRectF.Left = srcRectF.Left - compareRectF.Top = srcRectF.Top - compareRectF.Right = srcRectF.Left + srcRectF.Width - 1 - compareRectF.Bottom = srcRectF.Top + srcRectF.Height - 1 - - Dim i As Long, idxSmallestX As Long, minDistX As Double - - 'Set the minimum distance to an arbitrarily huge number, then find the smallest x-distance - minDistX = DOUBLE_MAX - For i = 0 To numXSnaps - 1 - With xSnaps(i) - .cDistance1 = PDMath.DistanceOneDimension(compareRectF.Left, .cValue) - If (.cDistance1 < minDistX) Then - minDistX = .cDistance1 - idxSmallestX = i - End If - .cDistance2 = PDMath.DistanceOneDimension(compareRectF.Right, .cValue) - If (.cDistance2 < minDistX) Then - minDistX = .cDistance2 - idxSmallestX = i - End If - End With - Next i - - 'Repeat all the above steps for y-coordinates - Dim idxSmallestY As Long, minDistY As Double - minDistY = DOUBLE_MAX - - For i = 0 To numYSnaps - 1 - With ySnaps(i) - .cDistance1 = PDMath.DistanceOneDimension(compareRectF.Top, .cValue) - If (.cDistance1 < minDistY) Then - minDistY = .cDistance1 - idxSmallestY = i - End If - .cDistance2 = PDMath.DistanceOneDimension(compareRectF.Bottom, .cValue) - If (.cDistance2 < minDistY) Then - minDistY = .cDistance2 - idxSmallestY = i - End If - End With - Next i - - 'Determine the minimum snap distance required for this zoom value. - Dim snapThreshold As Double - snapThreshold = Tools_Move.GetSnap_Distance() * (1# / Zoom.GetZoomRatioFromIndex(PDImages.GetActiveImage.ImgViewport.GetZoomIndex)) - - 'If the minimum value falls beneath the minimum snap distance, snap away! - If (minDistX < snapThreshold) Then - If (xSnaps(idxSmallestX).cDistance1 < xSnaps(idxSmallestX).cDistance2) Then - dstRectF.Left = xSnaps(idxSmallestX).cValue - Else - dstRectF.Left = xSnaps(idxSmallestX).cValue - dstRectF.Width - End If - End If - - If (minDistY < snapThreshold) Then - If (ySnaps(idxSmallestY).cDistance1 < ySnaps(idxSmallestY).cDistance2) Then - dstRectF.Top = ySnaps(idxSmallestY).cValue - Else - dstRectF.Top = ySnaps(idxSmallestY).cValue - dstRectF.Height - End If - End If - -End Sub - -'Get a list of current x-snap targets (determined by user settings). -' RETURNS: number of entries in the list, or 0 if snapping is disabled by the user. -Private Function GetSnapTargets_X(ByRef dstSnaps() As SnapComparison) As Long - - 'Start with some arbitrarily sized list (these will be enlarged as necessary) - ReDim dstSnaps(0 To 15) As SnapComparison - GetSnapTargets_X = 0 - - 'Canvas edges first - If Tools_Move.GetSnap_CanvasEdge() Then - - 'Ensure at space is available in the target array - If (UBound(dstSnaps) < GetSnapTargets_X + 1) Then ReDim Preserve dstSnaps(0 To GetSnapTargets_X * 2 - 1) As SnapComparison - - 'Add canvas boundaries to the snap list - dstSnaps(GetSnapTargets_X).cValue = 0# - dstSnaps(GetSnapTargets_X + 1).cValue = PDImages.GetActiveImage.Width - GetSnapTargets_X = GetSnapTargets_X + 2 - - End If - - 'TODO: more snap targets in the future... - -End Function - -'Get a list of current y-snap targets (determined by user settings). -' RETURNS: number of entries in the list, or 0 if snapping is disabled by the user. -Private Function GetSnapTargets_Y(ByRef dstSnaps() As SnapComparison) As Long - - 'Start with some arbitrarily sized list (these will be enlarged as necessary) - ReDim dstSnaps(0 To 15) As SnapComparison - GetSnapTargets_Y = 0 - - 'Canvas edges first - If Tools_Move.GetSnap_CanvasEdge() Then - - 'Ensure at space is available in the target array - If (UBound(dstSnaps) < GetSnapTargets_Y + 1) Then ReDim Preserve dstSnaps(0 To GetSnapTargets_Y * 2 - 1) As SnapComparison - - 'Add canvas boundaries to the snap list - dstSnaps(GetSnapTargets_Y).cValue = 0# - dstSnaps(GetSnapTargets_Y + 1).cValue = PDImages.GetActiveImage.Height - GetSnapTargets_Y = GetSnapTargets_Y + 2 - - End If - - 'TODO: more snap targets in the future... - -End Function - 'This function can be used to move and/or non-destructively resize an image layer. ' 'If this action occurs during a Mouse_Up event, the finalizeTransform parameter should be set to TRUE. This instructs the function @@ -536,13 +260,13 @@ Public Sub TransformCurrentLayer(ByVal curImageX As Double, ByVal curImageY As D ' the mouse pointer (e.g. the layer edges, which are not located at the mouse position), so we'll need to wait ' to snap until the transform has been applied to the underlying layer. Dim srcPtF As PointFloat, snappedPtF As PointFloat - If Tools_Move.GetSnap_Any() Then + If Snap.GetSnap_Any() Then Select Case m_CurPOI Case poi_CornerNW, poi_CornerNE, poi_CornerSW, poi_CornerSE srcPtF.x = curImageX srcPtF.y = curImageY - SnapPointByMoving srcPtF, snappedPtF + Snap.SnapPointByMoving srcPtF, snappedPtF curImageX = snappedPtF.x curImageY = snappedPtF.y @@ -733,21 +457,23 @@ Public Sub TransformCurrentLayer(ByVal curImageX As Double, ByVal curImageY As D .SetLayerOffsetX m_InitLayerCoords_Pure(0).x + hOffsetImage .SetLayerOffsetY m_InitLayerCoords_Pure(0).y + vOffsetImage - Dim srcRectF As RectF - .GetLayerBoundaryRect srcRectF - - Dim listOfCorners() As PointFloat - ReDim listOfCorners(0 To 3) As PointFloat - .GetLayerCornerCoordinates listOfCorners - - Dim snapOffsetX As Long, snapOffsetY As Long - SnapPointListByMoving listOfCorners, 4, snapOffsetX, snapOffsetY + 'Apply snapping (contingent on user settings). + If Snap.GetSnap_Any() Then + + Dim listOfCorners() As PointFloat + ReDim listOfCorners(0 To 3) As PointFloat + .GetLayerCornerCoordinates listOfCorners + + Dim snapOffsetX As Long, snapOffsetY As Long + Snap.SnapPointListByMoving listOfCorners, 4, snapOffsetX, snapOffsetY + + 'Hand the layer corners off to the snap calculator, then take whatever it returns and + ' forward the original left/top position + snapped offsets to the source layer + .SetLayerOffsetX .GetLayerOffsetX + snapOffsetX + .SetLayerOffsetY .GetLayerOffsetY + snapOffsetY + + End If - 'Hand the layer corners off to the snap calculator, then take whatever it returns and - ' forward the original left/top position + snapped offsets to the source layer - .SetLayerOffsetX .GetLayerOffsetX + snapOffsetX - .SetLayerOffsetY .GetLayerOffsetY + snapOffsetY - End Select 'If this layer is moved and/or resized while rotation is active, we need to adjust the layer's rotational center diff --git a/Modules/UserPrefs.bas b/Modules/UserPrefs.bas index aa1b574e1..8f6127545 100644 --- a/Modules/UserPrefs.bas +++ b/Modules/UserPrefs.bas @@ -609,9 +609,9 @@ Public Sub LoadUserSettings() Tools.SetToolSetting_HighResMouse UserPrefs.GetPref_Boolean("Tools", "HighResMouseInput", True) m_CanvasColor = Colors.GetRGBLongFromHex(UserPrefs.GetPref_String("Interface", "CanvasColor", "#a0a0a0")) - Interface.ToggleSnapOptions pdst_Global, True, UserPrefs.GetPref_Boolean("Interface", "snap-global", True) - Interface.ToggleSnapOptions pdst_CanvasEdge, True, UserPrefs.GetPref_Boolean("Interface", "snap-canvas-edge", True) - Tools_Move.SetSnap_Distance UserPrefs.GetPref_Long("Interface", "snap-distance", 8&) + Snap.ToggleSnapOptions pdst_Global, True, UserPrefs.GetPref_Boolean("Interface", "snap-global", True) + Snap.ToggleSnapOptions pdst_CanvasEdge, True, UserPrefs.GetPref_Boolean("Interface", "snap-canvas-edge", True) + Snap.SetSnap_Distance UserPrefs.GetPref_Long("Interface", "snap-distance", 8&) 'Users can supply a (secret!) "UIFont" setting in the "Interface" segment if they ' want to override PD's default font object. diff --git a/PhotoDemon.vbp b/PhotoDemon.vbp index 4aa00ce83..690ba5bb6 100644 --- a/PhotoDemon.vbp +++ b/PhotoDemon.vbp @@ -1,6 +1,6 @@ Type=Exe -Reference=*\G{50A7E9B0-70EF-11D1-B75A-00A0C90564FE}#1.0#0#..\..\Windows\SysWOW64\shell32.dll#Microsoft Shell Controls And Automation -Reference=*\G{00020430-0000-0000-C000-000000000046}#2.0#0#..\..\Windows\SysWOW64\stdole2.tlb#OLE Automation +Reference=*\G{50A7E9B0-70EF-11D1-B75A-00A0C90564FE}#1.0#0#\\?\C:\Windows\SysWOW64\shell32.dll#Microsoft Shell Controls And Automation +Reference=*\G{00020430-0000-0000-C000-000000000046}#2.0#0#\\?\C:\Windows\SysWOW64\stdole2.tlb#OLE Automation Module=PDMain; Modules\Main.bas Module=Public_Constants; Modules\PublicConstants.bas Module=Public_EnumsAndTypes; Modules\PublicEnumsAndTypes.bas @@ -85,6 +85,7 @@ Module=SelectionFiles; Modules\SelectionFiles.bas Module=SelectionFilters; Modules\SelectionFilters.bas Module=Selections; Modules\Selections.bas Module=SelectionUI; Modules\SelectionUI.bas +Module=Snap; Modules\Snap.bas Module=Strings; Modules\Strings.bas Module=TextSupport; Modules\TextSupport.bas Module=Toolboxes; Modules\Toolboxes.bas @@ -526,7 +527,7 @@ Description="PhotoDemon Photo Editor" CompatibleMode="0" MajorVer=9 MinorVer=1 -RevisionVer=355 +RevisionVer=358 AutoIncrementVer=1 ServerSupportFiles=0 VersionComments="Copyright 2000-2024 Tanner Helland - photodemon.org" From bbaf2bdf03b041568ca15ba54342c031498a34b1 Mon Sep 17 00:00:00 2001 From: Tanner Date: Wed, 17 Apr 2024 14:16:13 -0600 Subject: [PATCH 08/15] PSD export: continue improving compatibility Details in code comments; basically, this improves a rare edge-case where PSDs with many small 24-bit layers display unreliable transparency-around-each-layer behavior in Photoshop. --- Classes/pdPSD.cls | 12 +++++++----- Classes/pdPSDLayer.cls | 31 ++++++++++++++++++++++++------- Classes/pdStream.cls | 6 ++++-- PhotoDemon.vbp | 2 +- 4 files changed, 36 insertions(+), 15 deletions(-) diff --git a/Classes/pdPSD.cls b/Classes/pdPSD.cls index 9d3ebe2f4..d72c2d65c 100644 --- a/Classes/pdPSD.cls +++ b/Classes/pdPSD.cls @@ -550,6 +550,8 @@ Private Function ExportStep4_WriteMergedImage(ByRef cStream As pdStream, ByRef s ' the start of the file, and embed a "3" instead of "4" for the number of channels in the image. If (Not alphaMatters) Then + If PSD_DEBUG_VERBOSE Then PDDebug.LogAction "dropping alpha channel to conserve space" + Dim curStreamPosition As Long curStreamPosition = cStream.GetPosition() @@ -762,8 +764,8 @@ Private Function ExportStep2_WriteImageResources(ByRef cStream As pdStream, ByRe 'Size of the version number is calculated as follows: ' 4 bytes - version number (always 1) ' 1 byte - real merged data included in file (e.g. "max compatibility" used at export time) - ' (variable bytes) - name of PSD writer as a "Unicode string" (4 byte len + 2 bytes * num chars) - ' (variable bytes) - name of PSD reader as a "Unicode string" (4 byte len + 2 bytes * num chars) + ' (variable bytes) - name of PSD writer as a "Unicode string" (4 byte len + 2 bytes * num chars + 2-byte trailing null) + ' (variable bytes) - name of PSD reader as a "Unicode string" (4 byte len + 2 bytes * num chars + 2-byte trailing null) ' 4 bytes - file version (always 1) blockSize = 4 + 1 + 4 'Fixed-size entries blockSize = blockSize + 4 + 4 'Size descriptors for two Unicode strings @@ -771,15 +773,15 @@ Private Function ExportStep2_WriteImageResources(ByRef cStream As pdStream, ByRe Dim writerName As String, readerName As String writerName = "PhotoDemon" readerName = writerName & " " & Updates.GetPhotoDemonVersion() - blockSize = blockSize + LenB(writerName) + LenB(readerName) 'Add string sizes to calculation + blockSize = blockSize + LenB(writerName) + 2 + LenB(readerName) + 2 'Add string sizes to calculation, including trailing nulls WriteImageResourceHeader cStream, &H421, blockSize cStream.WriteLong_BE 1 If useMaxCompatibility Then cStream.WriteByte 1 Else cStream.WriteByte 0 cStream.WriteLong_BE Len(writerName) - cStream.WriteString_UnicodeBE writerName + cStream.WriteString_UnicodeBE writerName, True cStream.WriteLong_BE Len(readerName) - cStream.WriteString_UnicodeBE readerName + cStream.WriteString_UnicodeBE readerName, True cStream.WriteLong_BE 1 'See above note about image resource block padding diff --git a/Classes/pdPSDLayer.cls b/Classes/pdPSDLayer.cls index c2be71159..8e7e460a3 100644 --- a/Classes/pdPSDLayer.cls +++ b/Classes/pdPSDLayer.cls @@ -15,9 +15,8 @@ Attribute VB_Exposed = False 'PhotoDemon PSD (PhotoShop Image) Layer Container and Parser 'Copyright 2019-2024 by Tanner Helland 'Created: 15/January/19 -'Last updated: 27/June/23 -'Last update: add additional failsafe checks for vector mask intersection with attached layer -' (vector masks can overlap layers in unpredictable ways, with clean intersections not always guaranteed) +'Last updated: 17/April/24 +'Last update: improve compatibility by always writing RGBA layer data ' 'This class contains layer-specific data pulled from a PSD file. It is populated by a parent ' pdPSD instance. It has no purpose outside of a PSD parsing context; for layer handling inside @@ -3290,6 +3289,23 @@ Friend Sub WriteLayerData(ByRef cStream As pdStream, ByRef srcLayer As pdLayer, 'Unpremultiply the temporary DIB before continuing, if alpha values are relevant If m_LayerHasAlpha Then tmpLayerDIB.SetAlphaPremultiplication False + 'UPDATE April 2024 + ' + 'PhotoDemon has traditionally written out 24-bit RGB data if a layer doesn't use meaningful alpha, + ' but this produces unexpected results on some versions of Photoshop. Unfortunately, I haven't been + ' able to track down a consistent pattern in why this happens - all other software can open PD's PSDs + ' without trouble when this occurs, but PS shows some kind of file offset problem, with weird layer + ' settings and unreliable handling of layer metadata. + ' + 'GIMP avoids this problem by *always* writing an alpha channel, regardless of relevancy, + ' and indeed this solves the same problem when exporting PSD files from PD. + ' + 'Until I can figure out why only some layered images struggle with 24-bit data, I've switched to + ' *always* writing RGBA channels for each layer. (Note that we deliberately set this forcible alpha + ' value here, *after* unpremultiplying alpha, because only opaque layers will mark m_LayerHasAlpha + ' as FALSE and their alpha channels consist of only 255, so unpremultiplying is irrelevant.) + m_LayerHasAlpha = True + 'First is layer rect. Note that Adobe uses the non-standard top/left/bottom/right order. ' (Note also that layer groups are always written as 0-size rects.) If isGroupMarker Then @@ -3513,12 +3529,13 @@ Friend Sub WriteLayerData(ByRef cStream As pdStream, ByRef srcLayer As pdLayer, 'Write a 4-byte hard-coded ASCII identifier of the optional block cStream.WriteString_ASCII "luni" - 'Write the length of the *entire* data section (4-byte string length + string itself) - cStream.WriteLong_BE LenB(lName) + 4 + 'Write the length of the *entire* data section, in bytes + ' (4-byte string length + string itself + 2-byte null terminator) + cStream.WriteLong_BE 4 + LenB(lName) + 2 - 'Write the string itself + 'Write the string itself (which is prefixed by the length IN CODE POINTS, ignoring the terminating null) cStream.WriteLong_BE Len(lName) - cStream.WriteString_UnicodeBE lName, False + cStream.WriteString_UnicodeBE lName, True 'Optional blocks are supposed to be even-padded, but we don't have to worry about this with ' Unicode strings (2 bytes per char, remember!) diff --git a/Classes/pdStream.cls b/Classes/pdStream.cls index 060037fcf..e4d083974 100644 --- a/Classes/pdStream.cls +++ b/Classes/pdStream.cls @@ -1284,14 +1284,16 @@ End Function ' using big-endian storage. Friend Function WriteString_UnicodeBE(ByRef srcString As String, Optional ByVal addTrailingNull As Boolean = False) As Boolean + WriteString_UnicodeBE = True + 'There's no good way to do this, but since performance isn't a huge deal in PD's current use-cases, ' just write each char one at a time. Dim i As Long For i = 1 To Len(srcString) - WriteString_UnicodeBE = Me.WriteInt_BE(AscW(Mid$(srcString, i, 1))) + WriteString_UnicodeBE = WriteString_UnicodeBE And Me.WriteInt_BE(AscW(Mid$(srcString, i, 1))) Next i - If addTrailingNull Then WriteString_UnicodeBE = Me.WriteInt(0) + If addTrailingNull Then WriteString_UnicodeBE = WriteString_UnicodeBE And Me.WriteInt(0) End Function diff --git a/PhotoDemon.vbp b/PhotoDemon.vbp index 690ba5bb6..5b2487ff6 100644 --- a/PhotoDemon.vbp +++ b/PhotoDemon.vbp @@ -527,7 +527,7 @@ Description="PhotoDemon Photo Editor" CompatibleMode="0" MajorVer=9 MinorVer=1 -RevisionVer=358 +RevisionVer=359 AutoIncrementVer=1 ServerSupportFiles=0 VersionComments="Copyright 2000-2024 Tanner Helland - photodemon.org" From 89f0cf7b40ede62a61c0e236bd26179f00bbe5fc Mon Sep 17 00:00:00 2001 From: Tanner Date: Wed, 17 Apr 2024 16:11:55 -0600 Subject: [PATCH 09/15] Selection tools: cursor now "snaps" under some conditions Rectangle, ellipse, and polygon selections now snap when moving individual points, specifically. Snap when moving an entire selection is still TODO! (Snap is not currently planned for lasso or magic wand selections. For the lasso tool, I hope to add a magnetic lasso someday...) --- Classes/pdSelection.cls | 33 ++++++++++++++++++++++++++++++++- Modules/SelectionUI.bas | 2 +- Modules/Tools.bas | 2 +- PhotoDemon.vbp | 2 +- 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/Classes/pdSelection.cls b/Classes/pdSelection.cls index 3cab4e978..82d41002e 100644 --- a/Classes/pdSelection.cls +++ b/Classes/pdSelection.cls @@ -1271,6 +1271,37 @@ Friend Sub SetAdditionalCoordinates(ByVal x As Double, ByVal y As Double) x = Int(x + 0.5) y = Int(y + 0.5) + 'For operations only involving a single point of transformation (e.g. resizing a selection by node-dragging), + ' we can apply snapping *now*, to the mouse coordinate itself. + ' + 'For operations that transform multiple points (like moving an entire selection), we need to snap points + ' *besides* the mouse pointer (e.g. the selection edges, or other polygon points which are not located at + ' the mouse position), so we must wait to snap until the transform has been applied to the underlying + ' selection object. + Dim srcPtF As PointFloat, snappedPtF As PointFloat + If Snap.GetSnap_Any() Then + + Dim okToSnap As Boolean + If m_TransformModeActive Then + okToSnap = okToSnap Or (m_SelectionShape = ss_Rectangle) + okToSnap = okToSnap Or (m_SelectionShape = ss_Circle) + okToSnap = okToSnap Or ((m_SelectionShape = ss_Polygon) And ((m_CurrentPOI <> poi_Interior) And (m_CurrentPOI <> poi_Undefined))) + Else + okToSnap = okToSnap Or (m_SelectionShape = ss_Rectangle) + okToSnap = okToSnap Or (m_SelectionShape = ss_Circle) + End If + + + If okToSnap Then + srcPtF.x = x + srcPtF.y = y + Snap.SnapPointByMoving srcPtF, snappedPtF + x = snappedPtF.x + y = snappedPtF.y + End If + + End If + 'Check for an active transformation mode. (A transformation is something like resizing or moving an existing selection, ' versus drawing a new one from scratch.) If m_TransformModeActive Then @@ -1426,7 +1457,7 @@ Friend Sub SetAdditionalCoordinates(ByVal x As Double, ByVal y As Double) m_IsTransformable = True 'Wand selections are not technically transformable, but we allow the user to click-drag to - ' move the initiaion point + ' move the initiation point. Case ss_Wand If (m_CornersUnlocked.Left <> x) Or (m_CornersUnlocked.Top <> y) Then diff --git a/Modules/SelectionUI.bas b/Modules/SelectionUI.bas index 247fa60e2..08574ce7c 100644 --- a/Modules/SelectionUI.bas +++ b/Modules/SelectionUI.bas @@ -912,7 +912,7 @@ Public Sub NotifySelectionMouseMove(ByRef srcCanvas As pdCanvas, ByVal lmbState 'Pass new points to the active selection PDImages.GetActiveImage.MainSelection.SetAdditionalCoordinates imgX, imgY SelectionUI.SyncTextToCurrentSelection PDImages.GetActiveImageID() - + End If 'Force a redraw of the viewport diff --git a/Modules/Tools.bas b/Modules/Tools.bas index e845da02a..73d752565 100644 --- a/Modules/Tools.bas +++ b/Modules/Tools.bas @@ -273,7 +273,7 @@ Public Sub TransformCurrentLayer(ByVal curImageX As Double, ByVal curImageY As D End Select End If - + 'Convert the current x/y pair to the layer coordinate space. This takes into account any active affine transforms ' on the image (e.g. rotation), which may place the point in a totally different position relative to the underlying layer. Dim curLayerX As Single, curLayerY As Single diff --git a/PhotoDemon.vbp b/PhotoDemon.vbp index 5b2487ff6..b25d5563b 100644 --- a/PhotoDemon.vbp +++ b/PhotoDemon.vbp @@ -527,7 +527,7 @@ Description="PhotoDemon Photo Editor" CompatibleMode="0" MajorVer=9 MinorVer=1 -RevisionVer=359 +RevisionVer=361 AutoIncrementVer=1 ServerSupportFiles=0 VersionComments="Copyright 2000-2024 Tanner Helland - photodemon.org" From 5e66d2fca23f597caa2b79a2cb13ac804471b420 Mon Sep 17 00:00:00 2001 From: Tanner Date: Wed, 17 Apr 2024 16:29:37 -0600 Subject: [PATCH 10/15] Selections: snap now works when moving... ...rectangular or elliptical selections. Polygons are TODO next! --- Classes/pdSelection.cls | 13 ++++++++++--- Modules/PDMath.bas | 24 ++++++++++++++++++++++++ PhotoDemon.vbp | 2 +- 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/Classes/pdSelection.cls b/Classes/pdSelection.cls index 82d41002e..22e9c8c45 100644 --- a/Classes/pdSelection.cls +++ b/Classes/pdSelection.cls @@ -1283,15 +1283,14 @@ Friend Sub SetAdditionalCoordinates(ByVal x As Double, ByVal y As Double) Dim okToSnap As Boolean If m_TransformModeActive Then - okToSnap = okToSnap Or (m_SelectionShape = ss_Rectangle) - okToSnap = okToSnap Or (m_SelectionShape = ss_Circle) + okToSnap = okToSnap Or ((m_SelectionShape = ss_Rectangle) And ((m_CurrentPOI <> poi_Interior) And (m_CurrentPOI <> poi_Undefined))) + okToSnap = okToSnap Or ((m_SelectionShape = ss_Circle) And ((m_CurrentPOI <> poi_Interior) And (m_CurrentPOI <> poi_Undefined))) okToSnap = okToSnap Or ((m_SelectionShape = ss_Polygon) And ((m_CurrentPOI <> poi_Interior) And (m_CurrentPOI <> poi_Undefined))) Else okToSnap = okToSnap Or (m_SelectionShape = ss_Rectangle) okToSnap = okToSnap Or (m_SelectionShape = ss_Circle) End If - If okToSnap Then srcPtF.x = x srcPtF.y = y @@ -1376,6 +1375,14 @@ Friend Sub SetAdditionalCoordinates(ByVal x As Double, ByVal y As Double) .Bottom = .Top + m_CornersLocked.Height End With + 'We now need to apply "snap" settings, if any + If Snap.GetSnap_Any() Then + Dim srcRectF_Orig As RectF, dstRectF_Snapped As RectF + PDMath.GetRectF_FromRectFRB m_CornersUnlocked, srcRectF_Orig + Snap.SnapRectByMoving srcRectF_Orig, dstRectF_Snapped + PDMath.GetRectFRB_FromRectF dstRectF_Snapped, m_CornersUnlocked + End If + End Select 'If a transform mode is active, re-mark the selection as being transformable diff --git a/Modules/PDMath.bas b/Modules/PDMath.bas index 4a4c6cdb7..e20f78523 100644 --- a/Modules/PDMath.bas +++ b/Modules/PDMath.bas @@ -1451,6 +1451,30 @@ Public Sub GetNearestIntRectF(ByRef srcRectF As RectF) If (PDMath.Frac(srcRectF.Height + yOffset) >= 0.5) Then srcRectF.Height = Int(srcRectF.Height + 1#) Else srcRectF.Height = Int(srcRectF.Height) End Sub +'Note that GDI rects (and possibly others) have strict requirements about the way right/bottom coords are defined, +' so these convenience functions may need additional tweaking by the caller if forwarding the rect to an external library. +Public Sub GetRectFRB_FromRectF(ByRef srcRectF As RectF, ByRef dstRectF_RB As RectF_RB) + + With dstRectF_RB + .Left = srcRectF.Left + .Top = srcRectF.Top + .Right = srcRectF.Left + srcRectF.Width + .Bottom = srcRectF.Top + srcRectF.Height + End With + +End Sub + +Public Sub GetRectF_FromRectFRB(ByRef srcRectF_RB As RectF_RB, ByRef dstRectF As RectF) + + With dstRectF + .Left = srcRectF_RB.Left + .Top = srcRectF_RB.Top + .Width = srcRectF_RB.Right - srcRectF_RB.Left + .Height = srcRectF_RB.Bottom - srcRectF_RB.Top + End With + +End Sub + Public Function ClampL(ByVal srcL As Long, ByVal minL As Long, ByVal maxL As Long) As Long If (srcL < minL) Then ClampL = minL diff --git a/PhotoDemon.vbp b/PhotoDemon.vbp index b25d5563b..932c7493e 100644 --- a/PhotoDemon.vbp +++ b/PhotoDemon.vbp @@ -527,7 +527,7 @@ Description="PhotoDemon Photo Editor" CompatibleMode="0" MajorVer=9 MinorVer=1 -RevisionVer=361 +RevisionVer=362 AutoIncrementVer=1 ServerSupportFiles=0 VersionComments="Copyright 2000-2024 Tanner Helland - photodemon.org" From bf219540490b9f334b4a96acabee0450598ae109 Mon Sep 17 00:00:00 2001 From: Tanner Date: Thu, 18 Apr 2024 11:21:36 -0600 Subject: [PATCH 11/15] Polygon selections now snap-to-target when moving --- Classes/pdSelection.cls | 40 ++++++++++++++++++++++++++++++++++------ PhotoDemon.vbp | 2 +- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/Classes/pdSelection.cls b/Classes/pdSelection.cls index 22e9c8c45..3ef521be2 100644 --- a/Classes/pdSelection.cls +++ b/Classes/pdSelection.cls @@ -1407,12 +1407,40 @@ Friend Sub SetAdditionalCoordinates(ByVal x As Double, ByVal y As Double) 'Failsafe check for rapid clicks If (m_NumOfPolygonPoints - 1 <= UBound(m_PolygonPointsBackup)) Then - 'Rebuild the main polygon array by copying all points from the backup array, and applying the current - ' x/y transformation distance to them. - For i = 0 To m_NumOfPolygonPoints - 1 - m_PolygonPoints(i).x = m_PolygonPointsBackup(i).x + (x - m_MoveXDist) - m_PolygonPoints(i).y = m_PolygonPointsBackup(i).y + (y - m_MoveYDist) - Next i + 'Apply snap, as relevant + Dim xOffset As Long, yOffset As Long + xOffset = (x - m_MoveXDist) + yOffset = (y - m_MoveYDist) + + If Snap.GetSnap_Any() Then + + 'Make a local copy of all points, as they would appear if moved to the new position + Dim snapPoints() As PointFloat + ReDim snapPoints(0 To UBound(m_PolygonPoints)) As PointFloat + For i = 0 To m_NumOfPolygonPoints - 1 + snapPoints(i).x = m_PolygonPointsBackup(i).x + xOffset + snapPoints(i).y = m_PolygonPointsBackup(i).y + yOffset + Next i + + 'Snap this list of points to its best-fit location + xOffset = 0 + yOffset = 0 + Snap.SnapPointListByMoving snapPoints, m_NumOfPolygonPoints, xOffset, yOffset + + 'Relay the snapped points back to the main polygon point collection + For i = 0 To m_NumOfPolygonPoints - 1 + m_PolygonPoints(i).x = snapPoints(i).x + xOffset + m_PolygonPoints(i).y = snapPoints(i).y + yOffset + Next i + + 'Rebuild the main polygon array by copying all points from the backup array, + ' and applying the current x/y transformation distance to them. + Else + For i = 0 To m_NumOfPolygonPoints - 1 + m_PolygonPoints(i).x = m_PolygonPointsBackup(i).x + xOffset + m_PolygonPoints(i).y = m_PolygonPointsBackup(i).y + yOffset + Next i + End If End If diff --git a/PhotoDemon.vbp b/PhotoDemon.vbp index 932c7493e..03ede4cfb 100644 --- a/PhotoDemon.vbp +++ b/PhotoDemon.vbp @@ -527,7 +527,7 @@ Description="PhotoDemon Photo Editor" CompatibleMode="0" MajorVer=9 MinorVer=1 -RevisionVer=362 +RevisionVer=363 AutoIncrementVer=1 ServerSupportFiles=0 VersionComments="Copyright 2000-2024 Tanner Helland - photodemon.org" From 17f111fdbd0fd88b087eca1e37c4836b3c69c822 Mon Sep 17 00:00:00 2001 From: Tanner Date: Thu, 18 Apr 2024 16:39:41 -0600 Subject: [PATCH 12/15] Add UI for `Snap > Centerlines` and `Snap > Layers` These will allow auto-snapping to centers of objects (and the document) and boundaries of other layers --- Forms/MainWindow.frm | 12 ++++++++++++ Modules/Actions.bas | 11 +++++++++++ Modules/Interface.bas | 2 ++ Modules/Menus.bas | 4 +++- Modules/Snap.bas | 38 ++++++++++++++++++++++++++++++++++---- Modules/UserPrefs.bas | 2 ++ PhotoDemon.vbp | 2 +- 7 files changed, 65 insertions(+), 6 deletions(-) diff --git a/Forms/MainWindow.frm b/Forms/MainWindow.frm index 906b00edd..cea085057 100644 --- a/Forms/MainWindow.frm +++ b/Forms/MainWindow.frm @@ -1744,6 +1744,14 @@ Begin VB.Form FormMain Caption = "Canvas edges" Index = 0 End + Begin VB.Menu MnuSnap + Caption = "Centerlines" + Index = 1 + End + Begin VB.Menu MnuSnap + Caption = "Layers" + Index = 2 + End End End Begin VB.Menu MnuWindowTop @@ -3753,6 +3761,10 @@ Private Sub MnuSnap_Click(Index As Integer) Select Case Index Case 0 Actions.LaunchAction_ByName "snap_canvasedge" + Case 1 + Actions.LaunchAction_ByName "snap_centerline" + Case 2 + Actions.LaunchAction_ByName "snap_layer" End Select End Sub diff --git a/Modules/Actions.bas b/Modules/Actions.bas index 5aa581f46..e037c73ca 100644 --- a/Modules/Actions.bas +++ b/Modules/Actions.bas @@ -1360,6 +1360,12 @@ Private Function Launch_ByName_MenuView(ByRef srcMenuName As String, Optional By Case "snap_canvasedge" Snap.ToggleSnapOptions pdst_CanvasEdge + Case "snap_centerline" + Snap.ToggleSnapOptions pdst_Centerline + + Case "snap_layer" + Snap.ToggleSnapOptions pdst_Layer + Case Else cmdFound = False @@ -1973,6 +1979,7 @@ Public Sub BuildActionDatabase() AddAction "effects_animation_speed", "Animation playback speed" AddAction "effects_customfilter", "Custom filter", True, True AddAction "effects_8bf", "Photoshop (8bf) plugin", True, True + 'AddAction "tools_language" AddAction "tools_languageeditor", vbNullString AddAction "tools_theme", vbNullString @@ -1990,6 +1997,7 @@ Public Sub BuildActionDatabase() 'AddAction "tools_themepackage" 'AddAction "tools_standalonepackage" 'AddAction "effects_developertest" + AddAction "view_fit", vbNullString AddAction "view_zoomin", vbNullString AddAction "view_zoomout", vbNullString @@ -2007,6 +2015,9 @@ Public Sub BuildActionDatabase() AddAction "view_statusbar", vbNullString AddAction "snap_global", vbNullString AddAction "snap_canvasedge", vbNullString + AddAction "snap_centerline", vbNullString + AddAction "snap_layer", vbNullString + 'AddAction "window_toolbox" AddAction "window_displaytoolbox", vbNullString AddAction "window_displaytoolcategories", vbNullString diff --git a/Modules/Interface.bas b/Modules/Interface.bas index 7fb0955ae..09b35f0fc 100644 --- a/Modules/Interface.bas +++ b/Modules/Interface.bas @@ -734,6 +734,8 @@ Public Sub SetUIGroupState(ByVal metaItem As PD_UI_Group, ByVal newState As Bool Menus.SetMenuEnabled "view_top", newState Menus.SetMenuChecked "snap_global", Snap.GetSnap_Global() Menus.SetMenuChecked "snap_canvasedge", Snap.GetSnap_CanvasEdge() + Menus.SetMenuChecked "snap_centerline", Snap.GetSnap_Centerline() + Menus.SetMenuChecked "snap_layer", Snap.GetSnap_Layer() 'ImageOps is all Image-related menu items; it enables/disables the Image, Layer, Select, Color, and Print menus. ' (This flag is very useful for items that require at least one open image to operate.) diff --git a/Modules/Menus.bas b/Modules/Menus.bas index b50a39d68..75749f683 100644 --- a/Modules/Menus.bas +++ b/Modules/Menus.bas @@ -593,7 +593,9 @@ Public Sub InitializeMenus() AddMenuItem "-", "-", 8, 8 AddMenuItem "Snap", "snap_global", 8, 9 AddMenuItem "Snap to", "snap_top", 8, 10, allowInSearches:=False - AddMenuItem "Canvas edge", "snap_canvasedge", 8, 10, 0 + AddMenuItem "Canvas edges", "snap_canvasedge", 8, 10, 0 + AddMenuItem "Centerlines", "snap_centerline", 8, 10, 1 + AddMenuItem "Layers", "snap_layer", 8, 10, 2 'Window Menu AddMenuItem "Window", "window_top", 9 diff --git a/Modules/Snap.bas b/Modules/Snap.bas index 102441277..eeecf33b9 100644 --- a/Modules/Snap.bas +++ b/Modules/Snap.bas @@ -19,10 +19,12 @@ Option Explicit Public Enum PD_SnapTargets pdst_Global = 0 pdst_CanvasEdge = 1 + pdst_Centerline = 2 + pdst_Layer = 3 End Enum #If False Then - Private Const pdst_Global = 0, pdst_CanvasEdge = 1 + Private Const pdst_Global = 0, pdst_CanvasEdge = 1, pdst_Centerline = 2, pdst_Layer = 3 #End If 'When snapping coordinates, we need to compare all possible snap targets and choose the best independent @@ -36,14 +38,14 @@ End Type 'To improve performance, snap-to settings are cached locally (instead of traveling out to ' the user preference engine on every call). -Private m_SnapGlobal As Boolean, m_SnapToCanvasEdge As Boolean, m_SnapDistance As Long +Private m_SnapGlobal As Boolean, m_SnapToCanvasEdge As Boolean, m_SnapToCenterline As Boolean, m_SnapToLayer As Boolean +Private m_SnapDistance As Long 'Returns TRUE if *any* snap-to-edge behaviors are enabled. Useful for skipping all snap checks. Public Function GetSnap_Any() As Boolean GetSnap_Any = m_SnapGlobal If m_SnapGlobal Then - GetSnap_Any = m_SnapToCanvasEdge - 'TODO: OR against other snap options when added + GetSnap_Any = m_SnapToCanvasEdge Or m_SnapToCenterline Or m_SnapToLayer End If End Function @@ -51,6 +53,10 @@ Public Function GetSnap_CanvasEdge() As Boolean GetSnap_CanvasEdge = m_SnapToCanvasEdge End Function +Public Function GetSnap_Centerline() As Boolean + GetSnap_Centerline = m_SnapToCenterline +End Function + Public Function GetSnap_Distance() As Long GetSnap_Distance = m_SnapDistance @@ -69,10 +75,18 @@ Public Function GetSnap_Global() As Boolean GetSnap_Global = m_SnapGlobal End Function +Public Function GetSnap_Layer() As Boolean + GetSnap_Layer = m_SnapToLayer +End Function + Public Sub SetSnap_CanvasEdge(ByVal newState As Boolean) m_SnapToCanvasEdge = newState End Sub +Public Sub SetSnap_Centerline(ByVal newState As Boolean) + m_SnapToCenterline = newState +End Sub + Public Sub SetSnap_Distance(ByVal newDistance As Long) m_SnapDistance = newDistance If (m_SnapDistance < 1) Then m_SnapDistance = 1 @@ -83,6 +97,10 @@ Public Sub SetSnap_Global(ByVal newState As Boolean) m_SnapGlobal = newState End Sub +Public Sub SetSnap_Layer(ByVal newState As Boolean) + m_SnapToLayer = newState +End Sub + 'Toggle one of the "snap to..." settings in the View menu. ' To forcibly set to a specific state (instead of toggling), set the forceInsteadOfToggle param to TRUE. Public Sub ToggleSnapOptions(ByVal snapTarget As PD_SnapTargets, Optional ByVal forceInsteadOfToggle As Boolean = False, Optional ByVal newState As Boolean = True) @@ -103,6 +121,18 @@ Public Sub ToggleSnapOptions(ByVal snapTarget As PD_SnapTargets, Optional ByVal UserPrefs.SetPref_Boolean "Interface", "snap-canvas-edge", newState Menus.SetMenuChecked "snap_canvasedge", newState + Case pdst_Centerline + If (Not forceInsteadOfToggle) Then newState = Not Snap.GetSnap_Centerline() + Snap.SetSnap_Centerline newState + UserPrefs.SetPref_Boolean "Interface", "snap-centerline", newState + Menus.SetMenuChecked "snap_centerline", newState + + Case pdst_Layer + If (Not forceInsteadOfToggle) Then newState = Not Snap.GetSnap_Layer() + Snap.SetSnap_Layer newState + UserPrefs.SetPref_Boolean "Interface", "snap-layer", newState + Menus.SetMenuChecked "snap_layer", newState + End Select End Sub diff --git a/Modules/UserPrefs.bas b/Modules/UserPrefs.bas index 8f6127545..fe484a61b 100644 --- a/Modules/UserPrefs.bas +++ b/Modules/UserPrefs.bas @@ -611,6 +611,8 @@ Public Sub LoadUserSettings() Snap.ToggleSnapOptions pdst_Global, True, UserPrefs.GetPref_Boolean("Interface", "snap-global", True) Snap.ToggleSnapOptions pdst_CanvasEdge, True, UserPrefs.GetPref_Boolean("Interface", "snap-canvas-edge", True) + Snap.ToggleSnapOptions pdst_Centerline, True, UserPrefs.GetPref_Boolean("Interface", "snap-centerline", True) + Snap.ToggleSnapOptions pdst_Layer, True, UserPrefs.GetPref_Boolean("Interface", "snap-layer", True) Snap.SetSnap_Distance UserPrefs.GetPref_Long("Interface", "snap-distance", 8&) 'Users can supply a (secret!) "UIFont" setting in the "Interface" segment if they diff --git a/PhotoDemon.vbp b/PhotoDemon.vbp index 03ede4cfb..06e5edded 100644 --- a/PhotoDemon.vbp +++ b/PhotoDemon.vbp @@ -527,7 +527,7 @@ Description="PhotoDemon Photo Editor" CompatibleMode="0" MajorVer=9 MinorVer=1 -RevisionVer=363 +RevisionVer=364 AutoIncrementVer=1 ServerSupportFiles=0 VersionComments="Copyright 2000-2024 Tanner Helland - photodemon.org" From cd351dffa909d894135f7863f23fd96b410896e2 Mon Sep 17 00:00:00 2001 From: Tanner Date: Fri, 19 Apr 2024 10:10:29 -0600 Subject: [PATCH 13/15] Snap to Centerlines now works! For layers *and* selections! This makes it trivial to snap a smaller layer to the center of the canvas (on either an edge of the smaller layer, or the center of the smaller layer). In a rare bit of good engineering, I can add arbitrary snap targets whenever I want (e.g. next up: layer boundaries), and all objects automagically pick up the "snap" ability for those new targets. No snap engine modifications are required when new snap targets get added. --- Modules/Snap.bas | 138 ++++++++++++++++++++++++++++++++++++++---- Modules/UserPrefs.bas | 2 +- PhotoDemon.vbp | 2 +- 3 files changed, 128 insertions(+), 14 deletions(-) diff --git a/Modules/Snap.bas b/Modules/Snap.bas index eeecf33b9..e7e59a202 100644 --- a/Modules/Snap.bas +++ b/Modules/Snap.bas @@ -3,8 +3,8 @@ Attribute VB_Name = "Snap" 'Snap-to-target Handler 'Copyright 2024-2024 by Tanner Helland 'Created: 16/April/24 -'Last updated: 16/April/24 -'Last update: migrate all snap setting and behavior management to one central place +'Last updated: 19/April/24 +'Last update: add support for snapping to centerlines ' 'In 2024, snap-to-target support was added to various PhotoDemon tools. Thank you to all the users ' who suggested this feature! @@ -32,8 +32,12 @@ End Enum ' Two distances are provided: one each for left/right (or top/bottom). Private Type SnapComparison cValue As Double - cDistance1 As Double - cDistance2 As Double + cDistance1 As Double 'Left/Top distance + cDistance2 As Double 'Right/Bottom distance + cDistanceCX As Double 'X-Center distance (only if enabled) + cDistanceCY As Double 'Y-Center distance (only if enabled) + cCenterComparison As Boolean 'Set to TRUE if center distance is smallest distance; this is relevant for rects + ' and point lists, because we need to snap the *center*, not the boundaries End Type 'To improve performance, snap-to settings are cached locally (instead of traveling out to @@ -232,6 +236,7 @@ Public Sub SnapPointListByMoving(ByRef srcPoints() As PointFloat, ByVal numOfPoi minDistX = .cDistance1 idxSmallestX = i idxSmallestPointX = j + .cCenterComparison = False End If End With Next i @@ -249,18 +254,69 @@ Public Sub SnapPointListByMoving(ByRef srcPoints() As PointFloat, ByVal numOfPoi minDistY = .cDistance1 idxSmallestY = i idxSmallestPointY = j + .cCenterComparison = False End If End With Next i Next j + 'If centerline snapping is enabled, repeat the above steps, but for the center point of the list only + Dim pathTest As pd2DPath, pathRect As RectF + Set pathTest = New pd2DPath + pathTest.AddLines numOfPoints, VarPtr(srcPoints(0)) + pathRect = pathTest.GetPathBoundariesF() + + Dim cX As Double, cY As Double + cX = pathRect.Left + pathRect.Width * 0.5 + cY = pathRect.Top + pathRect.Height * 0.5 + + For i = 0 To numXSnaps - 1 + With xSnaps(i) + .cDistanceCX = PDMath.DistanceOneDimension(cX, .cValue) + If (.cDistanceCX < minDistX) Then + minDistX = .cDistanceCX + idxSmallestX = i + .cCenterComparison = True + End If + End With + Next i + + For i = 0 To numYSnaps - 1 + With ySnaps(i) + .cDistanceCY = PDMath.DistanceOneDimension(cY, .cValue) + If (.cDistanceCY < minDistY) Then + minDistY = .cDistanceCY + idxSmallestY = i + .cCenterComparison = True + End If + End With + Next i + 'Determine the minimum snap distance required for this zoom value. Dim snapThreshold As Double snapThreshold = GetSnapDistanceScaledForZoom() 'If the minimum value falls beneath the minimum snap distance, snap away! - If (minDistX < snapThreshold) Then dstOffsetX = (xSnaps(idxSmallestX).cValue - srcPoints(idxSmallestPointX).x) - If (minDistY < snapThreshold) Then dstOffsetY = (ySnaps(idxSmallestY).cValue - srcPoints(idxSmallestPointY).y) + If (minDistX < snapThreshold) Then + + 'Center comparisons require us to align the center point of the rect + If xSnaps(idxSmallestX).cCenterComparison Then + dstOffsetX = xSnaps(idxSmallestX).cValue - (pathRect.Left + pathRect.Width * 0.5) + Else + dstOffsetX = (xSnaps(idxSmallestX).cValue - srcPoints(idxSmallestPointX).x) + End If + + End If + + If (minDistY < snapThreshold) Then + + 'Center comparisons require us to align the center point of the rect + If ySnaps(idxSmallestY).cCenterComparison Then + dstOffsetY = ySnaps(idxSmallestY).cValue - (pathRect.Top + pathRect.Height * 0.5) + Else + dstOffsetY = (ySnaps(idxSmallestY).cValue - srcPoints(idxSmallestPointY).y) + End If + End If End Sub @@ -303,11 +359,13 @@ Public Sub SnapRectByMoving(ByRef srcRectF As RectF, ByRef dstRectF As RectF) If (.cDistance1 < minDistX) Then minDistX = .cDistance1 idxSmallestX = i + .cCenterComparison = False End If .cDistance2 = PDMath.DistanceOneDimension(compareRectF.Right, .cValue) If (.cDistance2 < minDistX) Then minDistX = .cDistance2 idxSmallestX = i + .cCenterComparison = False End If End With Next i @@ -322,11 +380,40 @@ Public Sub SnapRectByMoving(ByRef srcRectF As RectF, ByRef dstRectF As RectF) If (.cDistance1 < minDistY) Then minDistY = .cDistance1 idxSmallestY = i + .cCenterComparison = False End If .cDistance2 = PDMath.DistanceOneDimension(compareRectF.Bottom, .cValue) If (.cDistance2 < minDistY) Then minDistY = .cDistance2 idxSmallestY = i + .cCenterComparison = False + End If + End With + Next i + + 'If centerline snapping is enabled, repeat the above steps, but for the center point of the rect only + Dim cX As Double, cY As Double + cX = compareRectF.Left + (compareRectF.Right - compareRectF.Left) * 0.5 + cY = compareRectF.Top + (compareRectF.Bottom - compareRectF.Top) * 0.5 + + For i = 0 To numXSnaps - 1 + With xSnaps(i) + .cDistanceCX = PDMath.DistanceOneDimension(cX, .cValue) + If (.cDistanceCX < minDistX) Then + minDistX = .cDistanceCX + idxSmallestX = i + .cCenterComparison = True + End If + End With + Next i + + For i = 0 To numYSnaps - 1 + With ySnaps(i) + .cDistanceCY = PDMath.DistanceOneDimension(cY, .cValue) + If (.cDistanceCY < minDistY) Then + minDistY = .cDistanceCY + idxSmallestY = i + .cCenterComparison = True End If End With Next i @@ -337,18 +424,31 @@ Public Sub SnapRectByMoving(ByRef srcRectF As RectF, ByRef dstRectF As RectF) 'If the minimum value falls beneath the minimum snap distance, snap away! If (minDistX < snapThreshold) Then - If (xSnaps(idxSmallestX).cDistance1 < xSnaps(idxSmallestX).cDistance2) Then - dstRectF.Left = xSnaps(idxSmallestX).cValue + + 'Center comparisons require us to align the center point of the rect + If xSnaps(idxSmallestX).cCenterComparison Then + dstRectF.Left = xSnaps(idxSmallestX).cValue - (compareRectF.Right - compareRectF.Left) * 0.5 + + 'Otherwise, align the left or right boundary of the rect, as relevant Else - dstRectF.Left = xSnaps(idxSmallestX).cValue - dstRectF.Width + If (xSnaps(idxSmallestX).cDistance1 < xSnaps(idxSmallestX).cDistance2) Then + dstRectF.Left = xSnaps(idxSmallestX).cValue + Else + dstRectF.Left = xSnaps(idxSmallestX).cValue - dstRectF.Width + End If End If + End If If (minDistY < snapThreshold) Then - If (ySnaps(idxSmallestY).cDistance1 < ySnaps(idxSmallestY).cDistance2) Then - dstRectF.Top = ySnaps(idxSmallestY).cValue + If ySnaps(idxSmallestY).cCenterComparison Then + dstRectF.Top = ySnaps(idxSmallestY).cValue - (compareRectF.Bottom - compareRectF.Top) * 0.5 Else - dstRectF.Top = ySnaps(idxSmallestY).cValue - dstRectF.Height + If (ySnaps(idxSmallestY).cDistance1 < ySnaps(idxSmallestY).cDistance2) Then + dstRectF.Top = ySnaps(idxSmallestY).cValue + Else + dstRectF.Top = ySnaps(idxSmallestY).cValue - dstRectF.Height + End If End If End If @@ -379,6 +479,13 @@ Private Function GetSnapTargets_X(ByRef dstSnaps() As SnapComparison) As Long End If + 'Centerline (of canvas only; layers is handled below) + If Snap.GetSnap_Centerline() Then + If (UBound(dstSnaps) < GetSnapTargets_X) Then ReDim Preserve dstSnaps(0 To GetSnapTargets_X * 2 - 1) As SnapComparison + dstSnaps(GetSnapTargets_X).cValue = Int(PDImages.GetActiveImage.Width / 2) + GetSnapTargets_X = GetSnapTargets_X + 1 + End If + 'TODO: more snap targets in the future... End Function @@ -404,6 +511,13 @@ Private Function GetSnapTargets_Y(ByRef dstSnaps() As SnapComparison) As Long End If + 'Centerline (of canvas only; layers is handled below) + If Snap.GetSnap_Centerline() Then + If (UBound(dstSnaps) < GetSnapTargets_Y) Then ReDim Preserve dstSnaps(0 To GetSnapTargets_Y * 2 - 1) As SnapComparison + dstSnaps(GetSnapTargets_Y).cValue = Int(PDImages.GetActiveImage.Height / 2) + GetSnapTargets_Y = GetSnapTargets_Y + 1 + End If + 'TODO: more snap targets in the future... End Function diff --git a/Modules/UserPrefs.bas b/Modules/UserPrefs.bas index fe484a61b..e40f16131 100644 --- a/Modules/UserPrefs.bas +++ b/Modules/UserPrefs.bas @@ -611,7 +611,7 @@ Public Sub LoadUserSettings() Snap.ToggleSnapOptions pdst_Global, True, UserPrefs.GetPref_Boolean("Interface", "snap-global", True) Snap.ToggleSnapOptions pdst_CanvasEdge, True, UserPrefs.GetPref_Boolean("Interface", "snap-canvas-edge", True) - Snap.ToggleSnapOptions pdst_Centerline, True, UserPrefs.GetPref_Boolean("Interface", "snap-centerline", True) + Snap.ToggleSnapOptions pdst_Centerline, True, UserPrefs.GetPref_Boolean("Interface", "snap-centerline", False) Snap.ToggleSnapOptions pdst_Layer, True, UserPrefs.GetPref_Boolean("Interface", "snap-layer", True) Snap.SetSnap_Distance UserPrefs.GetPref_Long("Interface", "snap-distance", 8&) diff --git a/PhotoDemon.vbp b/PhotoDemon.vbp index 06e5edded..0aef2febf 100644 --- a/PhotoDemon.vbp +++ b/PhotoDemon.vbp @@ -527,7 +527,7 @@ Description="PhotoDemon Photo Editor" CompatibleMode="0" MajorVer=9 MinorVer=1 -RevisionVer=364 +RevisionVer=366 AutoIncrementVer=1 ServerSupportFiles=0 VersionComments="Copyright 2000-2024 Tanner Helland - photodemon.org" From c135407868c778db9a49b3c00123eb4047b44c7d Mon Sep 17 00:00:00 2001 From: Tanner Date: Fri, 19 Apr 2024 11:03:13 -0600 Subject: [PATCH 14/15] Add snap to layer boundaries (and centers, if enabled) Time to generate some localizations and get this feature merged! --- Modules/Snap.bas | 224 ++++++++++++++++++++++++++++++++--------------- PhotoDemon.vbp | 2 +- 2 files changed, 154 insertions(+), 72 deletions(-) diff --git a/Modules/Snap.bas b/Modules/Snap.bas index e7e59a202..d42884c13 100644 --- a/Modules/Snap.bas +++ b/Modules/Snap.bas @@ -4,7 +4,7 @@ Attribute VB_Name = "Snap" 'Copyright 2024-2024 by Tanner Helland 'Created: 16/April/24 'Last updated: 19/April/24 -'Last update: add support for snapping to centerlines +'Last update: add support for snapping to layer boundaries (and their centerlines, if enabled) ' 'In 2024, snap-to-target support was added to various PhotoDemon tools. Thank you to all the users ' who suggested this feature! @@ -261,37 +261,41 @@ Public Sub SnapPointListByMoving(ByRef srcPoints() As PointFloat, ByVal numOfPoi Next j 'If centerline snapping is enabled, repeat the above steps, but for the center point of the list only - Dim pathTest As pd2DPath, pathRect As RectF - Set pathTest = New pd2DPath - pathTest.AddLines numOfPoints, VarPtr(srcPoints(0)) - pathRect = pathTest.GetPathBoundariesF() - - Dim cX As Double, cY As Double - cX = pathRect.Left + pathRect.Width * 0.5 - cY = pathRect.Top + pathRect.Height * 0.5 - - For i = 0 To numXSnaps - 1 - With xSnaps(i) - .cDistanceCX = PDMath.DistanceOneDimension(cX, .cValue) - If (.cDistanceCX < minDistX) Then - minDistX = .cDistanceCX - idxSmallestX = i - .cCenterComparison = True - End If - End With - Next i - - For i = 0 To numYSnaps - 1 - With ySnaps(i) - .cDistanceCY = PDMath.DistanceOneDimension(cY, .cValue) - If (.cDistanceCY < minDistY) Then - minDistY = .cDistanceCY - idxSmallestY = i - .cCenterComparison = True - End If - End With - Next i - + If Snap.GetSnap_Centerline() Then + + Dim pathTest As pd2DPath, pathRect As RectF + Set pathTest = New pd2DPath + pathTest.AddLines numOfPoints, VarPtr(srcPoints(0)) + pathRect = pathTest.GetPathBoundariesF() + + Dim cx As Double, cy As Double + cx = pathRect.Left + pathRect.Width * 0.5 + cy = pathRect.Top + pathRect.Height * 0.5 + + For i = 0 To numXSnaps - 1 + With xSnaps(i) + .cDistanceCX = PDMath.DistanceOneDimension(cx, .cValue) + If (.cDistanceCX < minDistX) Then + minDistX = .cDistanceCX + idxSmallestX = i + .cCenterComparison = True + End If + End With + Next i + + For i = 0 To numYSnaps - 1 + With ySnaps(i) + .cDistanceCY = PDMath.DistanceOneDimension(cy, .cValue) + If (.cDistanceCY < minDistY) Then + minDistY = .cDistanceCY + idxSmallestY = i + .cCenterComparison = True + End If + End With + Next i + + End If + 'Determine the minimum snap distance required for this zoom value. Dim snapThreshold As Double snapThreshold = GetSnapDistanceScaledForZoom() @@ -392,32 +396,36 @@ Public Sub SnapRectByMoving(ByRef srcRectF As RectF, ByRef dstRectF As RectF) Next i 'If centerline snapping is enabled, repeat the above steps, but for the center point of the rect only - Dim cX As Double, cY As Double - cX = compareRectF.Left + (compareRectF.Right - compareRectF.Left) * 0.5 - cY = compareRectF.Top + (compareRectF.Bottom - compareRectF.Top) * 0.5 - - For i = 0 To numXSnaps - 1 - With xSnaps(i) - .cDistanceCX = PDMath.DistanceOneDimension(cX, .cValue) - If (.cDistanceCX < minDistX) Then - minDistX = .cDistanceCX - idxSmallestX = i - .cCenterComparison = True - End If - End With - Next i - - For i = 0 To numYSnaps - 1 - With ySnaps(i) - .cDistanceCY = PDMath.DistanceOneDimension(cY, .cValue) - If (.cDistanceCY < minDistY) Then - minDistY = .cDistanceCY - idxSmallestY = i - .cCenterComparison = True - End If - End With - Next i - + If Snap.GetSnap_Centerline() Then + + Dim cx As Double, cy As Double + cx = compareRectF.Left + (compareRectF.Right - compareRectF.Left) * 0.5 + cy = compareRectF.Top + (compareRectF.Bottom - compareRectF.Top) * 0.5 + + For i = 0 To numXSnaps - 1 + With xSnaps(i) + .cDistanceCX = PDMath.DistanceOneDimension(cx, .cValue) + If (.cDistanceCX < minDistX) Then + minDistX = .cDistanceCX + idxSmallestX = i + .cCenterComparison = True + End If + End With + Next i + + For i = 0 To numYSnaps - 1 + With ySnaps(i) + .cDistanceCY = PDMath.DistanceOneDimension(cy, .cValue) + If (.cDistanceCY < minDistY) Then + minDistY = .cDistanceCY + idxSmallestY = i + .cCenterComparison = True + End If + End With + Next i + + End If + 'Determine the minimum snap distance required for this zoom value. Dim snapThreshold As Double snapThreshold = GetSnapDistanceScaledForZoom() @@ -469,7 +477,7 @@ Private Function GetSnapTargets_X(ByRef dstSnaps() As SnapComparison) As Long 'Canvas edges first If Snap.GetSnap_CanvasEdge() Then - 'Ensure at space is available in the target array + 'Ensure space is available in the target array If (UBound(dstSnaps) < GetSnapTargets_X + 1) Then ReDim Preserve dstSnaps(0 To GetSnapTargets_X * 2 - 1) As SnapComparison 'Add canvas boundaries to the snap list @@ -477,16 +485,53 @@ Private Function GetSnapTargets_X(ByRef dstSnaps() As SnapComparison) As Long dstSnaps(GetSnapTargets_X + 1).cValue = PDImages.GetActiveImage.Width GetSnapTargets_X = GetSnapTargets_X + 2 + 'Centerline (of canvas only; layers is handled below) + If Snap.GetSnap_Centerline() Then + If (UBound(dstSnaps) < GetSnapTargets_X) Then ReDim Preserve dstSnaps(0 To GetSnapTargets_X * 2 - 1) As SnapComparison + dstSnaps(GetSnapTargets_X).cValue = Int(PDImages.GetActiveImage.Width / 2) + GetSnapTargets_X = GetSnapTargets_X + 1 + End If + End If - 'Centerline (of canvas only; layers is handled below) - If Snap.GetSnap_Centerline() Then - If (UBound(dstSnaps) < GetSnapTargets_X) Then ReDim Preserve dstSnaps(0 To GetSnapTargets_X * 2 - 1) As SnapComparison - dstSnaps(GetSnapTargets_X).cValue = Int(PDImages.GetActiveImage.Width / 2) - GetSnapTargets_X = GetSnapTargets_X + 1 + 'Layer boundaries next + If Snap.GetSnap_Layer() Then + + Dim layerRectF As RectF + + Dim i As Long + For i = 0 To PDImages.GetActiveImage.GetNumOfLayers - 1 + + 'Do *not* snap the active layer (or it will always snap to itself because that's what's closest, lol) + If (i <> PDImages.GetActiveImage.GetActiveLayerIndex) Then + + 'Ignore invisible layers + If PDImages.GetActiveImage.GetActiveLayer.GetLayerVisibility() Then + + 'Ensure space is available in the target array + If (UBound(dstSnaps) < GetSnapTargets_X + 2) Then ReDim Preserve dstSnaps(0 To GetSnapTargets_X * 2 - 1) As SnapComparison + + 'Add layer boundaries to the snap list + PDImages.GetActiveImage.GetLayerByIndex(i).GetLayerBoundaryRect layerRectF + dstSnaps(GetSnapTargets_X).cValue = layerRectF.Left + dstSnaps(GetSnapTargets_X + 1).cValue = layerRectF.Left + layerRectF.Width + GetSnapTargets_X = GetSnapTargets_X + 2 + + 'If centerlines are enabled, add the layer's centerline too + If Snap.GetSnap_Centerline() Then + dstSnaps(GetSnapTargets_X).cValue = layerRectF.Left + layerRectF.Width * 0.5 + GetSnapTargets_X = GetSnapTargets_X + 1 + End If + + End If + + End If + + Next i + End If - 'TODO: more snap targets in the future... + 'TODO: more snap targets in the future...? End Function @@ -509,15 +554,52 @@ Private Function GetSnapTargets_Y(ByRef dstSnaps() As SnapComparison) As Long dstSnaps(GetSnapTargets_Y + 1).cValue = PDImages.GetActiveImage.Height GetSnapTargets_Y = GetSnapTargets_Y + 2 + 'Centerline (of canvas only; layers is handled below) + If Snap.GetSnap_Centerline() Then + If (UBound(dstSnaps) < GetSnapTargets_Y) Then ReDim Preserve dstSnaps(0 To GetSnapTargets_Y * 2 - 1) As SnapComparison + dstSnaps(GetSnapTargets_Y).cValue = Int(PDImages.GetActiveImage.Height / 2) + GetSnapTargets_Y = GetSnapTargets_Y + 1 + End If + End If - 'Centerline (of canvas only; layers is handled below) - If Snap.GetSnap_Centerline() Then - If (UBound(dstSnaps) < GetSnapTargets_Y) Then ReDim Preserve dstSnaps(0 To GetSnapTargets_Y * 2 - 1) As SnapComparison - dstSnaps(GetSnapTargets_Y).cValue = Int(PDImages.GetActiveImage.Height / 2) - GetSnapTargets_Y = GetSnapTargets_Y + 1 + 'Layer boundaries next + If Snap.GetSnap_Layer() Then + + Dim layerRectF As RectF + + Dim i As Long + For i = 0 To PDImages.GetActiveImage.GetNumOfLayers - 1 + + 'Do *not* snap the active layer (or it will always snap to itself because that's what's closest, lol) + If (i <> PDImages.GetActiveImage.GetActiveLayerIndex) Then + + 'Ignore invisible layers + If PDImages.GetActiveImage.GetActiveLayer.GetLayerVisibility() Then + + 'Ensure space is available in the target array + If (UBound(dstSnaps) < GetSnapTargets_Y + 2) Then ReDim Preserve dstSnaps(0 To GetSnapTargets_Y * 2 - 1) As SnapComparison + + 'Add layer boundaries to the snap list + PDImages.GetActiveImage.GetLayerByIndex(i).GetLayerBoundaryRect layerRectF + dstSnaps(GetSnapTargets_Y).cValue = layerRectF.Top + dstSnaps(GetSnapTargets_Y + 1).cValue = layerRectF.Top + layerRectF.Height + GetSnapTargets_Y = GetSnapTargets_Y + 2 + + 'If centerlines are enabled, add the layer's centerline too + If Snap.GetSnap_Centerline() Then + dstSnaps(GetSnapTargets_Y).cValue = layerRectF.Top + layerRectF.Height * 0.5 + GetSnapTargets_Y = GetSnapTargets_Y + 1 + End If + + End If + + End If + + Next i + End If - 'TODO: more snap targets in the future... + 'TODO: more snap targets in the future...? End Function diff --git a/PhotoDemon.vbp b/PhotoDemon.vbp index 0aef2febf..94c21847b 100644 --- a/PhotoDemon.vbp +++ b/PhotoDemon.vbp @@ -527,7 +527,7 @@ Description="PhotoDemon Photo Editor" CompatibleMode="0" MajorVer=9 MinorVer=1 -RevisionVer=366 +RevisionVer=368 AutoIncrementVer=1 ServerSupportFiles=0 VersionComments="Copyright 2000-2024 Tanner Helland - photodemon.org" From 6d0b353317e24beb0ac4d3bbf6f4caf2f344c1ec Mon Sep 17 00:00:00 2001 From: Tanner Date: Fri, 19 Apr 2024 11:15:20 -0600 Subject: [PATCH 15/15] Localizations for new snap features --- App/PhotoDemon/Languages/French.xml | 27 +++++++++++------- App/PhotoDemon/Languages/German.xml | 27 +++++++++++------- App/PhotoDemon/Languages/Indonesian.xml | 27 +++++++++++------- App/PhotoDemon/Languages/Italian.xml | 27 +++++++++++------- App/PhotoDemon/Languages/Macedonian.xml | 27 +++++++++++------- App/PhotoDemon/Languages/Master/MASTER.xml | 27 +++++++++++------- App/PhotoDemon/Languages/Master/Phrases.db | Bin 176780 -> 176810 bytes App/PhotoDemon/Languages/Polish.xml | 27 +++++++++++------- .../Languages/Simplified_Chinese.xml | 27 +++++++++++------- App/PhotoDemon/Languages/Spanish_(Mexico).xml | 27 +++++++++++------- App/PhotoDemon/Languages/Spanish_(Spain).xml | 27 +++++++++++------- .../Languages/Traditional_Chinese.xml | 27 +++++++++++------- App/PhotoDemon/Languages/Turkish.xml | 27 +++++++++++------- App/PhotoDemon/Languages/Vlaams.xml | 27 +++++++++++------- PhotoDemon.vbp | 6 ++-- 15 files changed, 211 insertions(+), 146 deletions(-) diff --git a/App/PhotoDemon/Languages/French.xml b/App/PhotoDemon/Languages/French.xml index e698b0f49..841601f41 100644 --- a/App/PhotoDemon/Languages/French.xml +++ b/App/PhotoDemon/Languages/French.xml @@ -6,7 +6,7 @@ fr-FR Français -6.7.618 +6.7.619 Complete Jean Jacques Piedfort (orig. Frank Donckers) @@ -5559,6 +5559,16 @@ La valeur finale doit être entre %3 and %4. + +Centerlines + + + + +Layers +Calques + + Window Fenêtre @@ -5599,11 +5609,6 @@ La valeur finale doit être entre %3 and %4. Options des outils - -Layers -Calques - - Image tabstrip Ruban d'image @@ -5762,7 +5767,7 @@ La mise à jour est traitée automatiquement en arrière-plan. Vous recevrez une Langue changée avec succès. - + Monochrome Conversion @@ -14278,10 +14283,10 @@ Si vous choisissez de désactiver les mises à jour, n'oubliez pas de visiter ph -2699 +2700 - - - + + + \ No newline at end of file diff --git a/App/PhotoDemon/Languages/German.xml b/App/PhotoDemon/Languages/German.xml index ccb91b27e..ac749a005 100644 --- a/App/PhotoDemon/Languages/German.xml +++ b/App/PhotoDemon/Languages/German.xml @@ -6,7 +6,7 @@ de-DE Deutsch (German) -9.2.313 +9.2.314 Up-to-date rk (ehem. Frank Donckers, Helmut Kuerbiss) @@ -5560,6 +5560,16 @@ Der finale Wert muss zwischen %3 und %4 liegen. + +Centerlines + + + + +Layers +Layer + + Window Fenster @@ -5600,11 +5610,6 @@ Der finale Wert muss zwischen %3 und %4 liegen. Tooloptionen - -Layers -Layer - - Image tabstrip Bild-Tabstrip @@ -5763,7 +5768,7 @@ Das Update wird automatisch im Hintergrund verarbeitet. Sie werden eine neue Ben Sprache erfolgreich geändert. - + Monochrome Conversion @@ -14262,10 +14267,10 @@ Wenn Sie sich dennoch dafür entscheiden, Updates zu deaktivieren, vergessen Sie -2699 +2700 - - - + + + \ No newline at end of file diff --git a/App/PhotoDemon/Languages/Indonesian.xml b/App/PhotoDemon/Languages/Indonesian.xml index 5a2ec5470..83b6b1868 100644 --- a/App/PhotoDemon/Languages/Indonesian.xml +++ b/App/PhotoDemon/Languages/Indonesian.xml @@ -6,7 +6,7 @@ id-ID Bahasa Indonesia (ID) -8.9.1718 +8.9.1719 Terselesaikan Ari Sohandri Putra @@ -5555,6 +5555,16 @@ The final value must be between %3 and %4. + +Centerlines + + + + +Layers +Lapisan + + Window jendela @@ -5595,11 +5605,6 @@ The final value must be between %3 and %4. Opsi Alat - -Layers -Lapisan - - Image tabstrip Tab gambar @@ -5758,7 +5763,7 @@ Pembaruan sedang diproses secara otomatis di latar belakang. Anda akan menerima Bahasa berhasil diubah. - + Monochrome Conversion @@ -14258,10 +14263,10 @@ Jika Anda masih memilih untuk menonaktifkan pembaruan, pastikan untuk mengunjung -2699 +2700 - - - + + + \ No newline at end of file diff --git a/App/PhotoDemon/Languages/Italian.xml b/App/PhotoDemon/Languages/Italian.xml index a1308409f..4ebd74323 100644 --- a/App/PhotoDemon/Languages/Italian.xml +++ b/App/PhotoDemon/Languages/Italian.xml @@ -6,7 +6,7 @@ it-IT Italiano -8.9.1635 +8.9.1636 Completa GioRock, ManfroMarce @@ -5559,6 +5559,16 @@ Il valore finale deve essere compreso tra %3 e %4. + +Centerlines + + + + +Layers +Livelli + + Window Finestra @@ -5599,11 +5609,6 @@ Il valore finale deve essere compreso tra %3 e %4. Opzioni strumenti - -Layers -Livelli - - Image tabstrip Striscia delle immagini @@ -5762,7 +5767,7 @@ L'aggiornamento viene elaborato automaticamente in background. Riceverai una nu Lingua modificata con successo. - + Monochrome Conversion @@ -14263,10 +14268,10 @@ Se si sceglie comunque di disabilitare gli aggiornamenti, non dimenticate di vis -2699 +2700 - - - + + + \ No newline at end of file diff --git a/App/PhotoDemon/Languages/Macedonian.xml b/App/PhotoDemon/Languages/Macedonian.xml index d0bad6483..63d10c870 100644 --- a/App/PhotoDemon/Languages/Macedonian.xml +++ b/App/PhotoDemon/Languages/Macedonian.xml @@ -6,7 +6,7 @@ mk-MK Македонски -8.9.1726 +8.9.1727 80% complete Бобан Ѓерасимоски @@ -5550,6 +5550,16 @@ The final value must be between %3 and %4. + +Centerlines + + + + +Layers +Слоеви + + Window прозорец @@ -5590,11 +5600,6 @@ The final value must be between %3 and %4. Опции на алатот - -Layers -Слоеви - - Image tabstrip лентата со картички слика @@ -5753,7 +5758,7 @@ The update is automatically processing in the background. You will receive a ne Јазик успешно променет. - + Monochrome Conversion @@ -14250,10 +14255,10 @@ If сеуште изберете да го исклучите ажурирања -2699 +2700 - - - + + + \ No newline at end of file diff --git a/App/PhotoDemon/Languages/Master/MASTER.xml b/App/PhotoDemon/Languages/Master/MASTER.xml index ce4408ef9..8396503a4 100644 --- a/App/PhotoDemon/Languages/Master/MASTER.xml +++ b/App/PhotoDemon/Languages/Master/MASTER.xml @@ -6,7 +6,7 @@ en-US English (US) - MASTER COPY - 9.1.355 + 9.1.369 Automatically generated from PhotoDemon's source code Tanner Helland @@ -5511,6 +5511,16 @@ The final value must be between %3 and %4. + +Centerlines + + + + +Layers + + + Window @@ -5551,11 +5561,6 @@ The final value must be between %3 and %4. - -Layers - - - Image tabstrip @@ -5710,7 +5715,7 @@ The update is automatically processing in the background. You will receive a ne - + Monochrome Conversion @@ -14195,10 +14200,10 @@ If you still choose to disable updates, don't forget to visit photodemon.org fro - 2699 + 2700 - - - + + + \ No newline at end of file diff --git a/App/PhotoDemon/Languages/Master/Phrases.db b/App/PhotoDemon/Languages/Master/Phrases.db index 4443f8972260acf3352e9f7833d67b2e37f0377b..e67d22064cd1d53aab690bbfa155e4db028b2c13 100644 GIT binary patch delta 72 zcmV-O0Jr~)tb7e$A0RR91jIj^P@(l|BLuGDsWpZq3Ze??mvEdVw e;PWd61^`!SZe){z=o6Fk^6P^O^tTK20fD{+YaY-5 delta 44 zcmV+{0Mq}ftb7e$A0RR91i?I*O^0OuL1Ob!y@-2fY^tUPW0fD}e C+Y=Z7 diff --git a/App/PhotoDemon/Languages/Polish.xml b/App/PhotoDemon/Languages/Polish.xml index e8d4beda4..d69a62a32 100644 --- a/App/PhotoDemon/Languages/Polish.xml +++ b/App/PhotoDemon/Languages/Polish.xml @@ -6,7 +6,7 @@ pl-PL Polski -9.0.25 +9.0.26 100% complete Ryszard @@ -5557,6 +5557,16 @@ Ostateczna wartość musi zawierać się w przedziale od %3 do %4. + +Centerlines + + + + +Layers +Warstwy + + Window Okno @@ -5597,11 +5607,6 @@ Ostateczna wartość musi zawierać się w przedziale od %3 do %4. Opcje narzędzi - -Layers -Warstwy - - Image tabstrip Pasek obrazu @@ -5757,7 +5762,7 @@ The update is automatically processing in the background. You will receive a ne Język został pomyślnie zmieniony. - + Monochrome Conversion @@ -14262,10 +14267,10 @@ Jeśli nadal decydujesz się na wyłączenie aktualizacji, nie zapomnij odwiedzi -2699 +2700 - - - + + + \ No newline at end of file diff --git a/App/PhotoDemon/Languages/Simplified_Chinese.xml b/App/PhotoDemon/Languages/Simplified_Chinese.xml index b2f628a1a..fe1e11385 100644 --- a/App/PhotoDemon/Languages/Simplified_Chinese.xml +++ b/App/PhotoDemon/Languages/Simplified_Chinese.xml @@ -6,7 +6,7 @@ zh-CN 简体中文(夜间更新版,9.2 build 311) -9.2 build 311.3 +9.2 build 311.4 完成 Charltsing(QQ 564955427) revised on March 15, 2024, ChenLin(QQ:289778005), Lsbdx at 52pojie.cn, shishi @@ -5555,6 +5555,16 @@ The final value must be between %3 and %4. + +Centerlines + + + + +Layers +图层 + + Window 窗口 @@ -5595,11 +5605,6 @@ The final value must be between %3 and %4. 工具选项 - -Layers -图层 - - Image tabstrip 图像选项卡 @@ -5757,7 +5762,7 @@ The update is automatically processing in the background. You will receive a ne 语言切换成功。 - + Monochrome Conversion @@ -14258,11 +14263,11 @@ If you still choose to disable updates, don't forget to visit photodemon.org fro -2699 +2700 - - - + + + diff --git a/App/PhotoDemon/Languages/Spanish_(Mexico).xml b/App/PhotoDemon/Languages/Spanish_(Mexico).xml index 662ec65f8..0dad16d40 100644 --- a/App/PhotoDemon/Languages/Spanish_(Mexico).xml +++ b/App/PhotoDemon/Languages/Spanish_(Mexico).xml @@ -6,7 +6,7 @@ es-MX español (México) -9.0.29 +9.0.30 completo Plinio C Garcia, with help from DeepL.com @@ -5560,6 +5560,16 @@ El valor final debe estar entre %3 y %4. + +Centerlines + + + + +Layers +Capas + + Window Ventana @@ -5600,11 +5610,6 @@ El valor final debe estar entre %3 y %4. Opciones de herramienta - -Layers -Capas - - Image tabstrip Pestañas de imagen @@ -5763,7 +5768,7 @@ La actualización se está procesando automáticamente en segundo plano. Recibi Idioma cambiado correctamente. - + Monochrome Conversion @@ -14264,10 +14269,10 @@ If usted todavía elige desactivar las actualizaciones, no se olvide de visitar -2699 +2700 - - - + + + \ No newline at end of file diff --git a/App/PhotoDemon/Languages/Spanish_(Spain).xml b/App/PhotoDemon/Languages/Spanish_(Spain).xml index 6af71918b..ea6d5821c 100644 --- a/App/PhotoDemon/Languages/Spanish_(Spain).xml +++ b/App/PhotoDemon/Languages/Spanish_(Spain).xml @@ -6,7 +6,7 @@ es-ES español (España) -6.7.340 +6.7.341 completo Tecnorama @@ -5553,6 +5553,16 @@ The final value must be between %3 and %4. + +Centerlines + + + + +Layers +Capas + + Window Ventanas @@ -5593,11 +5603,6 @@ The final value must be between %3 and %4. Opciones de herramientas - -Layers -Capas - - Image tabstrip Pestañas de imagen @@ -5756,7 +5761,7 @@ La actualización se está procesando automáticamente en segundo plano. Recibir Idioma cambiado correctamente. - + Monochrome Conversion @@ -14257,10 +14262,10 @@ Si, con todo y con ello, elige desactivar las actualizaciones, no olvide visitar -2699 +2700 - - - + + + \ No newline at end of file diff --git a/App/PhotoDemon/Languages/Traditional_Chinese.xml b/App/PhotoDemon/Languages/Traditional_Chinese.xml index 112774489..f99e333e9 100644 --- a/App/PhotoDemon/Languages/Traditional_Chinese.xml +++ b/App/PhotoDemon/Languages/Traditional_Chinese.xml @@ -6,7 +6,7 @@ zh-TW 繁體中文 -7.0.300 +7.0.301 incomplete Chiahong Hong @@ -5518,6 +5518,16 @@ The final value must be between %3 and %4. + +Centerlines + + + + +Layers +圖層 + + Window 視窗 @@ -5558,11 +5568,6 @@ The final value must be between %3 and %4. 工具選項 - -Layers -圖層 - - Image tabstrip 影像索引標籤區域 @@ -5721,7 +5726,7 @@ The update is automatically processing in the background. You will receive a ne 語言切換成功。 - + Monochrome Conversion @@ -14208,10 +14213,10 @@ If you still choose to disable updates, don't forget to visit photodemon.org fro -2699 +2700 - - - + + + \ No newline at end of file diff --git a/App/PhotoDemon/Languages/Turkish.xml b/App/PhotoDemon/Languages/Turkish.xml index 42a562bb6..742e3051f 100644 --- a/App/PhotoDemon/Languages/Turkish.xml +++ b/App/PhotoDemon/Languages/Turkish.xml @@ -6,7 +6,7 @@ tr-TR Türkçe (Turkish) -1.0.29 +1.0.30 20% complete Anıl Yılmaz @@ -5536,6 +5536,16 @@ The final value must be between %3 and %4. + +Centerlines + + + + +Layers +Katmanlar + + Window Pencere @@ -5576,11 +5586,6 @@ The final value must be between %3 and %4. Araç Seçenekleri - -Layers -Katmanlar - - Image tabstrip @@ -5735,7 +5740,7 @@ The update is automatically processing in the background. You will receive a ne - + Monochrome Conversion @@ -14220,10 +14225,10 @@ If you still choose to disable updates, don't forget to visit photodemon.org fro -2699 +2700 - - - + + + \ No newline at end of file diff --git a/App/PhotoDemon/Languages/Vlaams.xml b/App/PhotoDemon/Languages/Vlaams.xml index 447b54cd5..b37169901 100644 --- a/App/PhotoDemon/Languages/Vlaams.xml +++ b/App/PhotoDemon/Languages/Vlaams.xml @@ -6,7 +6,7 @@ nl-BE Vlaams (Nederlands) -8.9.1726 +8.9.1727 80% complete Frank Donckers @@ -5560,6 +5560,16 @@ De uiteindelijke waarde moet tussen %3 en %4 liggen. + +Centerlines + + + + +Layers +Lagen + + Window Venster @@ -5600,11 +5610,6 @@ De uiteindelijke waarde moet tussen %3 en %4 liggen. Gereedschapsopties - -Layers -Lagen - - Image tabstrip Afbeeldings tabstrip @@ -5763,7 +5768,7 @@ De update wordt automatisch verwerkt op de achtergrond. U krijgt een nieuwe mede Taal succesvol gewijzigd. - + Monochrome Conversion @@ -14264,10 +14269,10 @@ If u nog steeds kiezen om updates uit te schakelen, vergeet dan niet om photodem -2699 +2700 - - - + + + \ No newline at end of file diff --git a/PhotoDemon.vbp b/PhotoDemon.vbp index 94c21847b..903cd79b2 100644 --- a/PhotoDemon.vbp +++ b/PhotoDemon.vbp @@ -1,6 +1,6 @@ Type=Exe -Reference=*\G{50A7E9B0-70EF-11D1-B75A-00A0C90564FE}#1.0#0#\\?\C:\Windows\SysWOW64\shell32.dll#Microsoft Shell Controls And Automation -Reference=*\G{00020430-0000-0000-C000-000000000046}#2.0#0#\\?\C:\Windows\SysWOW64\stdole2.tlb#OLE Automation +Reference=*\G{50A7E9B0-70EF-11D1-B75A-00A0C90564FE}#1.0#0#..\..\Windows\SysWOW64\shell32.dll#Microsoft Shell Controls And Automation +Reference=*\G{00020430-0000-0000-C000-000000000046}#2.0#0#..\..\Windows\SysWOW64\stdole2.tlb#OLE Automation Module=PDMain; Modules\Main.bas Module=Public_Constants; Modules\PublicConstants.bas Module=Public_EnumsAndTypes; Modules\PublicEnumsAndTypes.bas @@ -527,7 +527,7 @@ Description="PhotoDemon Photo Editor" CompatibleMode="0" MajorVer=9 MinorVer=1 -RevisionVer=368 +RevisionVer=369 AutoIncrementVer=1 ServerSupportFiles=0 VersionComments="Copyright 2000-2024 Tanner Helland - photodemon.org"