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"