diff --git a/App/PhotoDemon/Languages/French.xml b/App/PhotoDemon/Languages/French.xml index 2b904eeb4..841601f41 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.619 Complete Jean Jacques Piedfort (orig. Frank Donckers) @@ -5544,6 +5544,31 @@ La valeur finale doit être entre %3 and %4. Montrer la barre des statuts + +Snap + + + + +Snap to + + + + +Canvas edges + + + + +Centerlines + + + + +Layers +Calques + + Window Fenêtre @@ -5584,11 +5609,6 @@ La valeur finale doit être entre %3 and %4. Options des outils - -Layers -Calques - - Image tabstrip Ruban d'image @@ -5747,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 @@ -13377,6 +13397,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 +14073,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 +14283,10 @@ Si vous choisissez de désactiver les mises à jour, n'oubliez pas de visiter ph -2695 +2700 - - - + + + \ No newline at end of file diff --git a/App/PhotoDemon/Languages/German.xml b/App/PhotoDemon/Languages/German.xml index 7fe4e6655..ac749a005 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.314 Up-to-date rk (ehem. Frank Donckers, Helmut Kuerbiss) @@ -5545,6 +5545,31 @@ Der finale Wert muss zwischen %3 und %4 liegen. Statusleiste zeigen + +Snap + + + + +Snap to + + + + +Canvas edges + + + + +Centerlines + + + + +Layers +Layer + + Window Fenster @@ -5585,11 +5610,6 @@ Der finale Wert muss zwischen %3 und %4 liegen. Tooloptionen - -Layers -Layer - - Image tabstrip Bild-Tabstrip @@ -5748,7 +5768,7 @@ Das Update wird automatisch im Hintergrund verarbeitet. Sie werden eine neue Ben Sprache erfolgreich geändert. - + Monochrome Conversion @@ -13370,6 +13390,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 +14057,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 +14267,10 @@ Wenn Sie sich dennoch dafür entscheiden, Updates zu deaktivieren, vergessen Sie -2695 +2700 - - - + + + \ No newline at end of file diff --git a/App/PhotoDemon/Languages/Indonesian.xml b/App/PhotoDemon/Languages/Indonesian.xml index fd5df69be..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.1717 +8.9.1719 Terselesaikan Ari Sohandri Putra @@ -5540,6 +5540,31 @@ The final value must be between %3 and %4. + +Snap + + + + +Snap to + + + + +Canvas edges + + + + +Centerlines + + + + +Layers +Lapisan + + Window jendela @@ -5580,11 +5605,6 @@ The final value must be between %3 and %4. Opsi Alat - -Layers -Lapisan - - Image tabstrip Tab gambar @@ -5743,7 +5763,7 @@ Pembaruan sedang diproses secara otomatis di latar belakang. Anda akan menerima Bahasa berhasil diubah. - + Monochrome Conversion @@ -13366,6 +13386,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 +14053,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 +14263,10 @@ Jika Anda masih memilih untuk menonaktifkan pembaruan, pastikan untuk mengunjung -2695 +2700 - - - + + + \ No newline at end of file diff --git a/App/PhotoDemon/Languages/Italian.xml b/App/PhotoDemon/Languages/Italian.xml index 57a13b83c..4ebd74323 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.1636 Completa GioRock, ManfroMarce @@ -5544,6 +5544,31 @@ Il valore finale deve essere compreso tra %3 e %4. Mostra barra di stato + +Snap + + + + +Snap to + + + + +Canvas edges + + + + +Centerlines + + + + +Layers +Livelli + + Window Finestra @@ -5584,11 +5609,6 @@ Il valore finale deve essere compreso tra %3 e %4. Opzioni strumenti - -Layers -Livelli - - Image tabstrip Striscia delle immagini @@ -5747,7 +5767,7 @@ L'aggiornamento viene elaborato automaticamente in background. Riceverai una nu Lingua modificata con successo. - + Monochrome Conversion @@ -13371,6 +13391,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 +14058,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 +14268,10 @@ Se si sceglie comunque di disabilitare gli aggiornamenti, non dimenticate di vis -2695 +2700 - - - + + + \ No newline at end of file diff --git a/App/PhotoDemon/Languages/Macedonian.xml b/App/PhotoDemon/Languages/Macedonian.xml index 3543e73a3..63d10c870 100644 --- a/App/PhotoDemon/Languages/Macedonian.xml +++ b/App/PhotoDemon/Languages/Macedonian.xml @@ -6,7 +6,7 @@ mk-MK Македонски -8.9.1725 +8.9.1727 80% complete Бобан Ѓерасимоски @@ -5535,6 +5535,31 @@ The final value must be between %3 and %4. + +Snap + + + + +Snap to + + + + +Canvas edges + + + + +Centerlines + + + + +Layers +Слоеви + + Window прозорец @@ -5575,11 +5600,6 @@ The final value must be between %3 and %4. Опции на алатот - -Layers -Слоеви - - Image tabstrip лентата со картички слика @@ -5738,7 +5758,7 @@ The update is automatically processing in the background. You will receive a ne Јазик успешно променет. - + Monochrome Conversion @@ -13362,6 +13382,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 +14045,7 @@ If сеуште изберете да го исклучите ажурирања Оваа нова локација привремена папка нема да се случи се додека не рестартирам програмата. - + Reset all library options @@ -14230,10 +14255,10 @@ If сеуште изберете да го исклучите ажурирања -2695 +2700 - - - + + + \ No newline at end of file diff --git a/App/PhotoDemon/Languages/Master/MASTER.xml b/App/PhotoDemon/Languages/Master/MASTER.xml index f4cfec408..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.347 + 9.1.369 Automatically generated from PhotoDemon's source code Tanner Helland @@ -5496,6 +5496,31 @@ The final value must be between %3 and %4. + +Snap + + + + +Snap to + + + + +Canvas edges + + + + +Centerlines + + + + +Layers + + + Window @@ -5536,11 +5561,6 @@ The final value must be between %3 and %4. - -Layers - - - Image tabstrip @@ -5695,7 +5715,7 @@ The update is automatically processing in the background. You will receive a ne - + Monochrome Conversion @@ -13309,6 +13329,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 +13990,7 @@ If you still choose to disable updates, don't forget to visit photodemon.org fro - + Reset all library options @@ -14175,10 +14200,10 @@ If you still choose to disable updates, don't forget to visit photodemon.org fro - 2695 + 2700 - - - + + + \ No newline at end of file diff --git a/App/PhotoDemon/Languages/Master/Phrases.db b/App/PhotoDemon/Languages/Master/Phrases.db index 787b085d6..e67d22064 100644 Binary files a/App/PhotoDemon/Languages/Master/Phrases.db and b/App/PhotoDemon/Languages/Master/Phrases.db differ diff --git a/App/PhotoDemon/Languages/Polish.xml b/App/PhotoDemon/Languages/Polish.xml index 081e9ca1c..d69a62a32 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.26 100% complete Ryszard @@ -5542,6 +5542,31 @@ Ostateczna wartość musi zawierać się w przedziale od %3 do %4. Pokaż pasek stanu + +Snap + + + + +Snap to + + + + +Canvas edges + + + + +Centerlines + + + + +Layers +Warstwy + + Window Okno @@ -5582,11 +5607,6 @@ Ostateczna wartość musi zawierać się w przedziale od %3 do %4. Opcje narzędzi - -Layers -Warstwy - - Image tabstrip Pasek obrazu @@ -5742,7 +5762,7 @@ The update is automatically processing in the background. You will receive a ne Język został pomyślnie zmieniony. - + Monochrome Conversion @@ -13370,6 +13390,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 +14057,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 +14267,10 @@ Jeśli nadal decydujesz się na wyłączenie aktualizacji, nie zapomnij odwiedzi -2695 +2700 - - - + + + \ No newline at end of file diff --git a/App/PhotoDemon/Languages/Simplified_Chinese.xml b/App/PhotoDemon/Languages/Simplified_Chinese.xml index 7620aeabc..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.2 +9.2 build 311.4 完成 Charltsing(QQ 564955427) revised on March 15, 2024, ChenLin(QQ:289778005), Lsbdx at 52pojie.cn, shishi @@ -5540,6 +5540,31 @@ The final value must be between %3 and %4. 显示状态栏 + +Snap + + + + +Snap to + + + + +Canvas edges + + + + +Centerlines + + + + +Layers +图层 + + Window 窗口 @@ -5580,11 +5605,6 @@ The final value must be between %3 and %4. 工具选项 - -Layers -图层 - - Image tabstrip 图像选项卡 @@ -5742,7 +5762,7 @@ The update is automatically processing in the background. You will receive a ne 语言切换成功。 - + Monochrome Conversion @@ -13366,6 +13386,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 +14053,7 @@ If you still choose to disable updates, don't forget to visit photodemon.org fro 新的临时文件夹目录将在重新启动程序后生效。 - + Reset all library options @@ -14238,11 +14263,11 @@ If you still choose to disable updates, don't forget to visit photodemon.org fro -2695 +2700 - - - + + + diff --git a/App/PhotoDemon/Languages/Spanish_(Mexico).xml b/App/PhotoDemon/Languages/Spanish_(Mexico).xml index aa692dec5..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.28 +9.0.30 completo Plinio C Garcia, with help from DeepL.com @@ -5545,6 +5545,31 @@ El valor final debe estar entre %3 y %4. Mostrar barra de estado + +Snap + + + + +Snap to + + + + +Canvas edges + + + + +Centerlines + + + + +Layers +Capas + + Window Ventana @@ -5585,11 +5610,6 @@ El valor final debe estar entre %3 y %4. Opciones de herramienta - -Layers -Capas - - Image tabstrip Pestañas de imagen @@ -5748,7 +5768,7 @@ La actualización se está procesando automáticamente en segundo plano. Recibi Idioma cambiado correctamente. - + Monochrome Conversion @@ -13372,6 +13392,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 +14059,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 +14269,10 @@ If usted todavía elige desactivar las actualizaciones, no se olvide de visitar -2695 +2700 - - - + + + \ No newline at end of file diff --git a/App/PhotoDemon/Languages/Spanish_(Spain).xml b/App/PhotoDemon/Languages/Spanish_(Spain).xml index 999b0edf3..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.339 +6.7.341 completo Tecnorama @@ -5538,6 +5538,31 @@ The final value must be between %3 and %4. + +Snap + + + + +Snap to + + + + +Canvas edges + + + + +Centerlines + + + + +Layers +Capas + + Window Ventanas @@ -5578,11 +5603,6 @@ The final value must be between %3 and %4. Opciones de herramientas - -Layers -Capas - - Image tabstrip Pestañas de imagen @@ -5741,7 +5761,7 @@ La actualización se está procesando automáticamente en segundo plano. Recibir Idioma cambiado correctamente. - + Monochrome Conversion @@ -13365,6 +13385,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 +14052,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 +14262,10 @@ Si, con todo y con ello, elige desactivar las actualizaciones, no olvide visitar -2695 +2700 - - - + + + \ No newline at end of file diff --git a/App/PhotoDemon/Languages/Traditional_Chinese.xml b/App/PhotoDemon/Languages/Traditional_Chinese.xml index a85e9f414..f99e333e9 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.301 incomplete Chiahong Hong @@ -5503,6 +5503,31 @@ The final value must be between %3 and %4. 顯示狀態列 + +Snap + + + + +Snap to + + + + +Canvas edges + + + + +Centerlines + + + + +Layers +圖層 + + Window 視窗 @@ -5543,11 +5568,6 @@ The final value must be between %3 and %4. 工具選項 - -Layers -圖層 - - Image tabstrip 影像索引標籤區域 @@ -5706,7 +5726,7 @@ The update is automatically processing in the background. You will receive a ne 語言切換成功。 - + Monochrome Conversion @@ -13320,6 +13340,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 +14003,7 @@ If you still choose to disable updates, don't forget to visit photodemon.org fro - + Reset all library options @@ -14188,10 +14213,10 @@ If you still choose to disable updates, don't forget to visit photodemon.org fro -2695 +2700 - - - + + + \ No newline at end of file diff --git a/App/PhotoDemon/Languages/Turkish.xml b/App/PhotoDemon/Languages/Turkish.xml index 5bd0ebf01..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.28 +1.0.30 20% complete Anıl Yılmaz @@ -5521,6 +5521,31 @@ The final value must be between %3 and %4. + +Snap + + + + +Snap to + + + + +Canvas edges + + + + +Centerlines + + + + +Layers +Katmanlar + + Window Pencere @@ -5561,11 +5586,6 @@ The final value must be between %3 and %4. Araç Seçenekleri - -Layers -Katmanlar - - Image tabstrip @@ -5720,7 +5740,7 @@ The update is automatically processing in the background. You will receive a ne - + Monochrome Conversion @@ -13334,6 +13354,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 +14015,7 @@ If you still choose to disable updates, don't forget to visit photodemon.org fro - + Reset all library options @@ -14200,10 +14225,10 @@ If you still choose to disable updates, don't forget to visit photodemon.org fro -2695 +2700 - - - + + + \ No newline at end of file diff --git a/App/PhotoDemon/Languages/Vlaams.xml b/App/PhotoDemon/Languages/Vlaams.xml index b5c0bf7f6..b37169901 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.1727 80% complete Frank Donckers @@ -5545,6 +5545,31 @@ De uiteindelijke waarde moet tussen %3 en %4 liggen. Statusbalk tonen + +Snap + + + + +Snap to + + + + +Canvas edges + + + + +Centerlines + + + + +Layers +Lagen + + Window Venster @@ -5585,11 +5610,6 @@ De uiteindelijke waarde moet tussen %3 en %4 liggen. Gereedschapsopties - -Layers -Lagen - - Image tabstrip Afbeeldings tabstrip @@ -5748,7 +5768,7 @@ De update wordt automatisch verwerkt op de achtergrond. U krijgt een nieuwe mede Taal succesvol gewijzigd. - + Monochrome Conversion @@ -13372,6 +13392,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 +14059,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 +14269,10 @@ If u nog steeds kiezen om updates uit te schakelen, vergeet dan niet om photodem -2695 +2700 - - - + + + \ No newline at end of file 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/pdSelection.cls b/Classes/pdSelection.cls index 3cab4e978..3ef521be2 100644 --- a/Classes/pdSelection.cls +++ b/Classes/pdSelection.cls @@ -1271,6 +1271,36 @@ 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) 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 + 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 @@ -1345,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 @@ -1369,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 @@ -1426,7 +1492,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/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/Forms/MainWindow.frm b/Forms/MainWindow.frm index 5926dba6f..cea085057 100644 --- a/Forms/MainWindow.frm +++ b/Forms/MainWindow.frm @@ -1729,6 +1729,30 @@ 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" + Index = 9 + End + Begin VB.Menu MnuView + Caption = "Snap to" + Index = 10 + Begin VB.Menu MnuSnap + 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 Caption = "Window" @@ -3733,6 +3757,17 @@ 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" + Case 1 + Actions.LaunchAction_ByName "snap_centerline" + Case 2 + Actions.LaunchAction_ByName "snap_layer" + End Select +End Sub + Private Sub MnuSpecificZoom_Click(Index As Integer) Select Case Index Case 0 @@ -3821,13 +3856,19 @@ 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 Actions.LaunchAction_ByName "view_rulers" Case 7 Actions.LaunchAction_ByName "view_statusbar" + Case 8 + '(separator) + Case 9 + 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 3279ab46c..6e300a76d 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 + 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) 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..e037c73ca 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,26 @@ 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_global" + Snap.ToggleSnapOptions pdst_Global + + 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 @@ -1968,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 @@ -1985,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 @@ -2000,6 +2013,11 @@ 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 "snap_centerline", vbNullString + AddAction "snap_layer", vbNullString + 'AddAction "window_toolbox" AddAction "window_displaytoolbox", vbNullString AddAction "window_displaytoolcategories", 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 11c304e27..09b35f0fc 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,10 @@ 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", 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 4fb895e1f..75749f683 100644 --- a/Modules/Menus.bas +++ b/Modules/Menus.bas @@ -590,6 +590,12 @@ 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", "snap_global", 8, 9 + AddMenuItem "Snap to", "snap_top", 8, 10, allowInSearches:=False + 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 @@ -1655,8 +1661,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 @@ -1689,10 +1698,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 c662ec47b..46973199b 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/ ' 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/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/Snap.bas b/Modules/Snap.bas new file mode 100644 index 000000000..d42884c13 --- /dev/null +++ b/Modules/Snap.bas @@ -0,0 +1,605 @@ +Attribute VB_Name = "Snap" +'*************************************************************************** +'Snap-to-target Handler +'Copyright 2024-2024 by Tanner Helland +'Created: 16/April/24 +'Last updated: 19/April/24 +'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! +' +'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 + pdst_Centerline = 2 + pdst_Layer = 3 +End Enum + +#If False Then + 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 +' 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 '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 +' the user preference engine on every call). +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 Or m_SnapToCenterline Or m_SnapToLayer + End If +End Function + +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 + + '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 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 + 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 + +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) + + '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 + + 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 + +'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 + .cCenterComparison = False + 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 + .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 + 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() + + 'If the minimum value falls beneath the minimum snap distance, snap away! + 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 + +'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 + .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 + + '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 + .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 + 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() + + 'If the minimum value falls beneath the minimum snap distance, snap away! + If (minDistX < snapThreshold) Then + + '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 + 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).cCenterComparison Then + dstRectF.Top = ySnaps(idxSmallestY).cValue - (compareRectF.Bottom - compareRectF.Top) * 0.5 + Else + 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 + +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 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 + + '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 + + '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...? + +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 + + '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 + + '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...? + +End Function diff --git a/Modules/Tools.bas b/Modules/Tools.bas index 44a437a6a..73d752565 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. @@ -253,11 +253,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 Snap.GetSnap_Any() Then + + Select Case m_CurPOI + Case poi_CornerNW, poi_CornerNE, poi_CornerSW, poi_CornerSE + srcPtF.x = curImageX + srcPtF.y = curImageY + Snap.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. @@ -270,7 +291,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 + 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 @@ -298,8 +319,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 @@ -350,6 +371,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# @@ -409,10 +431,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 @@ -428,9 +451,29 @@ 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 - + + '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 + 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 0c0d9755f..e40f16131 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,12 @@ Public Sub LoadUserSettings() Tools.SetToolSetting_HighResMouse UserPrefs.GetPref_Boolean("Tools", "HighResMouseInput", True) m_CanvasColor = Colors.GetRGBLongFromHex(UserPrefs.GetPref_String("Interface", "CanvasColor", "#a0a0a0")) + 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", False) + 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 ' 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..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 @@ -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=347 +RevisionVer=369 AutoIncrementVer=1 ServerSupportFiles=0 VersionComments="Copyright 2000-2024 Tanner Helland - photodemon.org"