Merge branch 'master' into refit_on_delete

This commit is contained in:
Adesh Gupta
2025-07-02 03:36:59 +05:30
committed by GitHub
91 changed files with 2723 additions and 1374 deletions
+1
View File
@@ -5,3 +5,4 @@ perf.data*
profile.json profile.json
flamegraph.svg flamegraph.svg
.idea/ .idea/
.direnv
+1 -1
View File
@@ -51,7 +51,7 @@
libraw libraw
# Tauri dependencies: keep in sync with https://v2.tauri.app/start/prerequisites/ # Tauri dependencies: keep in sync with https://v2.tauri.app/start/prerequisites/#system-dependencies (under the NixOS tab)
at-spi2-atk at-spi2-atk
atkmm atkmm
cairo cairo
Generated
+287 -105
View File
@@ -82,6 +82,12 @@ dependencies = [
"alloc-no-stdlib", "alloc-no-stdlib",
] ]
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]] [[package]]
name = "android-activity" name = "android-activity"
version = "0.5.2" version = "0.5.2"
@@ -486,11 +492,11 @@ dependencies = [
[[package]] [[package]]
name = "block2" name = "block2"
version = "0.6.0" version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d59b4c170e16f0405a2e95aff44432a0d41aa97675f3d52623effe95792a037" checksum = "340d2f0bdb2a43c1d3cd40513185b2bd7def0aa1052f956455114bc98f82dcf2"
dependencies = [ dependencies = [
"objc2 0.6.0", "objc2 0.6.1",
] ]
[[package]] [[package]]
@@ -528,9 +534,9 @@ checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
[[package]] [[package]]
name = "bytemuck" name = "bytemuck"
version = "1.22.0" version = "1.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540" checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422"
dependencies = [ dependencies = [
"bytemuck_derive", "bytemuck_derive",
] ]
@@ -822,6 +828,12 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "color"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ae467d04a8a8aea5d9a49018a6ade2e4221d92968e8ce55a48c0b1164e5f698"
[[package]] [[package]]
name = "color_quant" name = "color_quant"
version = "1.1.0" version = "1.1.0"
@@ -1255,6 +1267,16 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
[[package]]
name = "dispatch2"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec"
dependencies = [
"bitflags 2.9.0",
"objc2 0.6.1",
]
[[package]] [[package]]
name = "displaydoc" name = "displaydoc"
version = "0.2.5" version = "0.2.5"
@@ -1569,6 +1591,25 @@ dependencies = [
"bytemuck", "bytemuck",
] ]
[[package]]
name = "font-types"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02a596f5713680923a2080d86de50fe472fb290693cf0f701187a1c8b36996b7"
dependencies = [
"bytemuck",
]
[[package]]
name = "fontconfig-cache-parser"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7f8afb20c8069fd676d27b214559a337cc619a605d25a87baa90b49a06f3b18"
dependencies = [
"bytemuck",
"thiserror 1.0.69",
]
[[package]] [[package]]
name = "fontconfig-parser" name = "fontconfig-parser"
version = "0.5.7" version = "0.5.7"
@@ -1592,6 +1633,29 @@ dependencies = [
"ttf-parser 0.24.1", "ttf-parser 0.24.1",
] ]
[[package]]
name = "fontique"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39f97079e1293b8c1e9fb03a2875d328bd2ee8f3b95ce62959c0acc04049c708"
dependencies = [
"bytemuck",
"fontconfig-cache-parser",
"hashbrown 0.15.4",
"icu_locid",
"memmap2",
"objc2 0.6.1",
"objc2-core-foundation",
"objc2-core-text",
"objc2-foundation 0.3.1",
"peniko 0.4.0",
"read-fonts 0.29.3",
"roxmltree",
"smallvec",
"windows 0.58.0",
"windows-core 0.58.0",
]
[[package]] [[package]]
name = "foreign-types" name = "foreign-types"
version = "0.3.2" version = "0.3.2"
@@ -2109,7 +2173,7 @@ checksum = "dcf29e94d6d243368b7a56caa16bc213e4f9f8ed38c4d9557069527b5d5281ca"
dependencies = [ dependencies = [
"bitflags 2.9.0", "bitflags 2.9.0",
"gpu-descriptor-types", "gpu-descriptor-types",
"hashbrown 0.15.2", "hashbrown 0.15.4",
] ]
[[package]] [[package]]
@@ -2131,8 +2195,10 @@ dependencies = [
"glam", "glam",
"graph-craft", "graph-craft",
"graphene-application-io", "graphene-application-io",
"graphene-brush",
"graphene-core", "graphene-core",
"graphene-path-bool", "graphene-path-bool",
"graphene-raster-nodes",
"graphene-svg-renderer", "graphene-svg-renderer",
"iai-callgrind", "iai-callgrind",
"js-sys", "js-sys",
@@ -2164,6 +2230,19 @@ dependencies = [
"wgpu", "wgpu",
] ]
[[package]]
name = "graphene-brush"
version = "0.1.0"
dependencies = [
"dyn-any",
"glam",
"graphene-core",
"graphene-raster-nodes",
"node-macro",
"serde",
"tokio",
]
[[package]] [[package]]
name = "graphene-cli" name = "graphene-cli"
version = "0.1.0" version = "0.1.0"
@@ -2197,23 +2276,35 @@ dependencies = [
"image", "image",
"kurbo", "kurbo",
"log", "log",
"math-parser",
"node-macro", "node-macro",
"num-derive", "num-derive",
"num-traits", "num-traits",
"parley",
"petgraph 0.7.1", "petgraph 0.7.1",
"rand 0.9.0", "rand 0.9.0",
"rand_chacha 0.9.0", "rand_chacha 0.9.0",
"rustc-hash 2.1.1", "rustc-hash 2.1.1",
"rustybuzz 0.20.1",
"serde", "serde",
"serde_json", "serde_json",
"skrifa 0.32.0",
"specta", "specta",
"tinyvec", "tinyvec",
"tokio", "tokio",
"wgpu", "wgpu",
] ]
[[package]]
name = "graphene-math-nodes"
version = "0.1.0"
dependencies = [
"glam",
"graphene-core",
"log",
"math-parser",
"node-macro",
"rand 0.9.0",
]
[[package]] [[package]]
name = "graphene-path-bool" name = "graphene-path-bool"
version = "0.1.0" version = "0.1.0"
@@ -2229,6 +2320,27 @@ dependencies = [
"specta", "specta",
] ]
[[package]]
name = "graphene-raster-nodes"
version = "0.1.0"
dependencies = [
"bezier-rs",
"bytemuck",
"dyn-any",
"fastnoise-lite",
"futures",
"glam",
"graphene-core",
"image",
"ndarray",
"node-macro",
"rand 0.9.0",
"rand_chacha 0.9.0",
"serde",
"specta",
"tokio",
]
[[package]] [[package]]
name = "graphene-std" name = "graphene-std"
version = "0.1.0" version = "0.1.0"
@@ -2241,8 +2353,11 @@ dependencies = [
"glam", "glam",
"graph-craft", "graph-craft",
"graphene-application-io", "graphene-application-io",
"graphene-brush",
"graphene-core", "graphene-core",
"graphene-math-nodes",
"graphene-path-bool", "graphene-path-bool",
"graphene-raster-nodes",
"graphene-svg-renderer", "graphene-svg-renderer",
"image", "image",
"log", "log",
@@ -2458,10 +2573,12 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.15.2" version = "0.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
dependencies = [ dependencies = [
"allocator-api2",
"equivalent",
"foldhash", "foldhash",
] ]
@@ -2937,7 +3054,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.15.2", "hashbrown 0.15.4",
"serde", "serde",
] ]
@@ -3192,9 +3309,9 @@ dependencies = [
[[package]] [[package]]
name = "kurbo" name = "kurbo"
version = "0.11.1" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89234b2cc610a7dd927ebde6b41dd1a5d4214cffaef4cf1fb2195d592f92518f" checksum = "1077d333efea6170d9ccb96d3c3026f300ca0773da4938cc4c811daa6df68b0c"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"serde", "serde",
@@ -3270,7 +3387,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"windows-targets 0.48.5", "windows-targets 0.52.6",
] ]
[[package]] [[package]]
@@ -3494,10 +3611,10 @@ dependencies = [
"dpi", "dpi",
"gtk", "gtk",
"keyboard-types", "keyboard-types",
"objc2 0.6.0", "objc2 0.6.1",
"objc2-app-kit", "objc2-app-kit",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-foundation 0.3.0", "objc2-foundation 0.3.1",
"once_cell", "once_cell",
"png", "png",
"serde", "serde",
@@ -3778,9 +3895,9 @@ dependencies = [
[[package]] [[package]]
name = "objc2" name = "objc2"
version = "0.6.0" version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3531f65190d9cff863b77a99857e74c314dd16bf56c538c4b57c7cbc3f3a6e59" checksum = "88c6597e14493ab2e44ce58f2fdecf095a51f12ca57bec060a11c57332520551"
dependencies = [ dependencies = [
"objc2-encode 4.1.0", "objc2-encode 4.1.0",
"objc2-exception-helper", "objc2-exception-helper",
@@ -3793,15 +3910,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5906f93257178e2f7ae069efb89fbd6ee94f0592740b5f8a1512ca498814d0fb" checksum = "5906f93257178e2f7ae069efb89fbd6ee94f0592740b5f8a1512ca498814d0fb"
dependencies = [ dependencies = [
"bitflags 2.9.0", "bitflags 2.9.0",
"block2 0.6.0", "block2 0.6.1",
"libc", "libc",
"objc2 0.6.0", "objc2 0.6.1",
"objc2-cloud-kit", "objc2-cloud-kit",
"objc2-core-data", "objc2-core-data",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-core-graphics", "objc2-core-graphics",
"objc2-core-image", "objc2-core-image",
"objc2-foundation 0.3.0", "objc2-foundation 0.3.1",
"objc2-quartz-core 0.3.0", "objc2-quartz-core 0.3.0",
] ]
@@ -3812,8 +3929,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c1948a9be5f469deadbd6bcb86ad7ff9e47b4f632380139722f7d9840c0d42c" checksum = "6c1948a9be5f469deadbd6bcb86ad7ff9e47b4f632380139722f7d9840c0d42c"
dependencies = [ dependencies = [
"bitflags 2.9.0", "bitflags 2.9.0",
"objc2 0.6.0", "objc2 0.6.1",
"objc2-foundation 0.3.0", "objc2-foundation 0.3.1",
] ]
[[package]] [[package]]
@@ -3823,18 +3940,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f860f8e841f6d32f754836f51e6bc7777cd7e7053cf18528233f6811d3eceb4" checksum = "1f860f8e841f6d32f754836f51e6bc7777cd7e7053cf18528233f6811d3eceb4"
dependencies = [ dependencies = [
"bitflags 2.9.0", "bitflags 2.9.0",
"objc2 0.6.0", "objc2 0.6.1",
"objc2-foundation 0.3.0", "objc2-foundation 0.3.1",
] ]
[[package]] [[package]]
name = "objc2-core-foundation" name = "objc2-core-foundation"
version = "0.3.0" version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daeaf60f25471d26948a1c2f840e3f7d86f4109e3af4e8e4b5cd70c39690d925" checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166"
dependencies = [ dependencies = [
"bitflags 2.9.0", "bitflags 2.9.0",
"objc2 0.6.0", "dispatch2",
"objc2 0.6.1",
] ]
[[package]] [[package]]
@@ -3844,7 +3962,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dca602628b65356b6513290a21a6405b4d4027b8b250f0b98dddbb28b7de02" checksum = "f8dca602628b65356b6513290a21a6405b4d4027b8b250f0b98dddbb28b7de02"
dependencies = [ dependencies = [
"bitflags 2.9.0", "bitflags 2.9.0",
"objc2 0.6.0", "objc2 0.6.1",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-io-surface", "objc2-io-surface",
] ]
@@ -3855,8 +3973,18 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ffa6bea72bf42c78b0b34e89c0bafac877d5f80bf91e159a5d96ea7f693ca56" checksum = "6ffa6bea72bf42c78b0b34e89c0bafac877d5f80bf91e159a5d96ea7f693ca56"
dependencies = [ dependencies = [
"objc2 0.6.0", "objc2 0.6.1",
"objc2-foundation 0.3.0", "objc2-foundation 0.3.1",
]
[[package]]
name = "objc2-core-text"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ba833d4a1cb1aac330f8c973fd92b6ff1858e4aef5cdd00a255eefb28022fb5"
dependencies = [
"bitflags 2.9.0",
"objc2-core-foundation",
] ]
[[package]] [[package]]
@@ -3894,14 +4022,14 @@ dependencies = [
[[package]] [[package]]
name = "objc2-foundation" name = "objc2-foundation"
version = "0.3.0" version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a21c6c9014b82c39515db5b396f91645182611c97d24637cf56ac01e5f8d998" checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c"
dependencies = [ dependencies = [
"bitflags 2.9.0", "bitflags 2.9.0",
"block2 0.6.0", "block2 0.6.1",
"libc", "libc",
"objc2 0.6.0", "objc2 0.6.1",
"objc2-core-foundation", "objc2-core-foundation",
] ]
@@ -3912,7 +4040,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "161a8b87e32610086e1a7a9e9ec39f84459db7b3a0881c1f16ca5a2605581c19" checksum = "161a8b87e32610086e1a7a9e9ec39f84459db7b3a0881c1f16ca5a2605581c19"
dependencies = [ dependencies = [
"bitflags 2.9.0", "bitflags 2.9.0",
"objc2 0.6.0", "objc2 0.6.1",
"objc2-core-foundation", "objc2-core-foundation",
] ]
@@ -3948,8 +4076,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fb3794501bb1bee12f08dcad8c61f2a5875791ad1c6f47faa71a0f033f20071" checksum = "6fb3794501bb1bee12f08dcad8c61f2a5875791ad1c6f47faa71a0f033f20071"
dependencies = [ dependencies = [
"bitflags 2.9.0", "bitflags 2.9.0",
"objc2 0.6.0", "objc2 0.6.1",
"objc2-foundation 0.3.0", "objc2-foundation 0.3.1",
] ]
[[package]] [[package]]
@@ -3959,9 +4087,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "777a571be14a42a3990d4ebedaeb8b54cd17377ec21b92e8200ac03797b3bee1" checksum = "777a571be14a42a3990d4ebedaeb8b54cd17377ec21b92e8200ac03797b3bee1"
dependencies = [ dependencies = [
"bitflags 2.9.0", "bitflags 2.9.0",
"objc2 0.6.0", "objc2 0.6.1",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-foundation 0.3.0", "objc2-foundation 0.3.1",
] ]
[[package]] [[package]]
@@ -3971,11 +4099,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b717127e4014b0f9f3e8bba3d3f2acec81f1bde01f656823036e823ed2c94dce" checksum = "b717127e4014b0f9f3e8bba3d3f2acec81f1bde01f656823036e823ed2c94dce"
dependencies = [ dependencies = [
"bitflags 2.9.0", "bitflags 2.9.0",
"block2 0.6.0", "block2 0.6.1",
"objc2 0.6.0", "objc2 0.6.1",
"objc2-app-kit", "objc2-app-kit",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-foundation 0.3.0", "objc2-foundation 0.3.1",
] ]
[[package]] [[package]]
@@ -4137,6 +4265,19 @@ dependencies = [
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[package]]
name = "parley"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13e57638545cf2ba4c3e72cc5715e53b1880b829cc3dbefda3d1700c58efe723"
dependencies = [
"fontique",
"hashbrown 0.15.4",
"peniko 0.4.0",
"skrifa 0.31.3",
"swash",
]
[[package]] [[package]]
name = "paste" name = "paste"
version = "1.0.15" version = "1.0.15"
@@ -4168,7 +4309,18 @@ name = "peniko"
version = "0.2.0" version = "0.2.0"
source = "git+https://github.com/linebender/peniko.git?rev=d114c62#d114c6292dbcfb03e7360692198be423168a0edd" source = "git+https://github.com/linebender/peniko.git?rev=d114c62#d114c6292dbcfb03e7360692198be423168a0edd"
dependencies = [ dependencies = [
"color", "color 0.1.0",
"kurbo",
"smallvec",
]
[[package]]
name = "peniko"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f9529efd019889b2a205193c14ffb6e2839b54ed9d2720674f10f4b04d87ac9"
dependencies = [
"color 0.3.1",
"kurbo", "kurbo",
"smallvec", "smallvec",
] ]
@@ -4956,7 +5108,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f9e8a4f503e5c8750e4cd3b32a4e090035c46374b305a15c70bad833dca05f" checksum = "f6f9e8a4f503e5c8750e4cd3b32a4e090035c46374b305a15c70bad833dca05f"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"font-types", "font-types 0.8.3",
]
[[package]]
name = "read-fonts"
version = "0.29.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04ca636dac446b5664bd16c069c00a9621806895b8bb02c2dc68542b23b8f25d"
dependencies = [
"bytemuck",
"font-types 0.9.0",
]
[[package]]
name = "read-fonts"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "192735ef611aac958468e670cb98432c925426f3cb71521fda202130f7388d91"
dependencies = [
"bytemuck",
"font-types 0.9.0",
] ]
[[package]] [[package]]
@@ -5249,26 +5421,8 @@ dependencies = [
"log", "log",
"smallvec", "smallvec",
"ttf-parser 0.24.1", "ttf-parser 0.24.1",
"unicode-bidi-mirroring 0.3.0", "unicode-bidi-mirroring",
"unicode-ccc 0.3.0", "unicode-ccc",
"unicode-properties",
"unicode-script",
]
[[package]]
name = "rustybuzz"
version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702"
dependencies = [
"bitflags 2.9.0",
"bytemuck",
"core_maths",
"log",
"smallvec",
"ttf-parser 0.25.1",
"unicode-bidi-mirroring 0.4.0",
"unicode-ccc 0.4.0",
"unicode-properties", "unicode-properties",
"unicode-script", "unicode-script",
] ]
@@ -5403,9 +5557,9 @@ dependencies = [
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.218" version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
@@ -5434,9 +5588,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.218" version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -5640,7 +5794,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cc1aa86c26dbb1b63875a7180aa0819709b33348eb5b1491e4321fae388179d" checksum = "8cc1aa86c26dbb1b63875a7180aa0819709b33348eb5b1491e4321fae388179d"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"read-fonts", "read-fonts 0.25.3",
]
[[package]]
name = "skrifa"
version = "0.31.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbeb4ca4399663735553a09dd17ce7e49a0a0203f03b706b39628c4d913a8607"
dependencies = [
"bytemuck",
"read-fonts 0.29.3",
]
[[package]]
name = "skrifa"
version = "0.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6d632b5a73f566303dbeabd344dc3e716fd4ddc9a70d6fc8ea8e6f06617da97"
dependencies = [
"bytemuck",
"read-fonts 0.30.1",
] ]
[[package]] [[package]]
@@ -5663,9 +5837,9 @@ dependencies = [
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.14.0" version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
dependencies = [ dependencies = [
"serde", "serde",
] ]
@@ -5883,6 +6057,17 @@ dependencies = [
"siphasher 1.0.1", "siphasher 1.0.1",
] ]
[[package]]
name = "swash"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f745de914febc7c9ab4388dfaf94bbc87e69f57bb41133a9b0c84d4be49856f3"
dependencies = [
"skrifa 0.31.3",
"yazi",
"zeno",
]
[[package]] [[package]]
name = "swift-rs" name = "swift-rs"
version = "1.0.7" version = "1.0.7"
@@ -5993,9 +6178,9 @@ dependencies = [
"ndk 0.9.0", "ndk 0.9.0",
"ndk-context", "ndk-context",
"ndk-sys 0.6.0+11769913", "ndk-sys 0.6.0+11769913",
"objc2 0.6.0", "objc2 0.6.1",
"objc2-app-kit", "objc2-app-kit",
"objc2-foundation 0.3.0", "objc2-foundation 0.3.1",
"once_cell", "once_cell",
"parking_lot", "parking_lot",
"raw-window-handle", "raw-window-handle",
@@ -6048,9 +6233,9 @@ dependencies = [
"log", "log",
"mime", "mime",
"muda", "muda",
"objc2 0.6.0", "objc2 0.6.1",
"objc2-app-kit", "objc2-app-kit",
"objc2-foundation 0.3.0", "objc2-foundation 0.3.1",
"percent-encoding", "percent-encoding",
"plist", "plist",
"raw-window-handle", "raw-window-handle",
@@ -6254,9 +6439,9 @@ dependencies = [
"http", "http",
"jni", "jni",
"log", "log",
"objc2 0.6.0", "objc2 0.6.1",
"objc2-app-kit", "objc2-app-kit",
"objc2-foundation 0.3.0", "objc2-foundation 0.3.1",
"once_cell", "once_cell",
"percent-encoding", "percent-encoding",
"raw-window-handle", "raw-window-handle",
@@ -6676,11 +6861,11 @@ dependencies = [
"dirs", "dirs",
"libappindicator", "libappindicator",
"muda", "muda",
"objc2 0.6.0", "objc2 0.6.1",
"objc2-app-kit", "objc2-app-kit",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-core-graphics", "objc2-core-graphics",
"objc2-foundation 0.3.0", "objc2-foundation 0.3.1",
"once_cell", "once_cell",
"png", "png",
"serde", "serde",
@@ -6708,9 +6893,6 @@ name = "ttf-parser"
version = "0.25.1" version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
dependencies = [
"core_maths",
]
[[package]] [[package]]
name = "typeid" name = "typeid"
@@ -6783,24 +6965,12 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64af057ad7466495ca113126be61838d8af947f41d93a949980b2389a118082f" checksum = "64af057ad7466495ca113126be61838d8af947f41d93a949980b2389a118082f"
[[package]]
name = "unicode-bidi-mirroring"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe"
[[package]] [[package]]
name = "unicode-ccc" name = "unicode-ccc"
version = "0.3.0" version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "260bc6647b3893a9a90668360803a15f96b85a5257b1c3a0c3daf6ae2496de42" checksum = "260bc6647b3893a9a90668360803a15f96b85a5257b1c3a0c3daf6ae2496de42"
[[package]]
name = "unicode-ccc"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.18" version = "1.0.18"
@@ -6888,7 +7058,7 @@ dependencies = [
"log", "log",
"pico-args", "pico-args",
"roxmltree", "roxmltree",
"rustybuzz 0.18.0", "rustybuzz",
"simplecss", "simplecss",
"siphasher 1.0.1", "siphasher 1.0.1",
"strict-num", "strict-num",
@@ -6959,9 +7129,9 @@ dependencies = [
"bytemuck", "bytemuck",
"futures-intrusive", "futures-intrusive",
"log", "log",
"peniko", "peniko 0.2.0",
"png", "png",
"skrifa", "skrifa 0.26.6",
"static_assertions", "static_assertions",
"thiserror 2.0.12", "thiserror 2.0.12",
"vello_encoding", "vello_encoding",
@@ -6976,8 +7146,8 @@ source = "git+https://github.com/linebender/vello.git?rev=3275ec8#3275ec85d83118
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"guillotiere", "guillotiere",
"peniko", "peniko 0.2.0",
"skrifa", "skrifa 0.26.6",
"smallvec", "smallvec",
] ]
@@ -7544,10 +7714,10 @@ version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c"
dependencies = [ dependencies = [
"objc2 0.6.0", "objc2 0.6.1",
"objc2-app-kit", "objc2-app-kit",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-foundation 0.3.0", "objc2-foundation 0.3.1",
"raw-window-handle", "raw-window-handle",
"windows-sys 0.59.0", "windows-sys 0.59.0",
"windows-version", "windows-version",
@@ -8065,7 +8235,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b19b78efae8b853c6c817e8752fc1dbf9cab8a8ffe9c30f399bd750ccf0f0730" checksum = "b19b78efae8b853c6c817e8752fc1dbf9cab8a8ffe9c30f399bd750ccf0f0730"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"block2 0.6.0", "block2 0.6.1",
"cookie", "cookie",
"crossbeam-channel", "crossbeam-channel",
"dpi", "dpi",
@@ -8079,10 +8249,10 @@ dependencies = [
"kuchikiki", "kuchikiki",
"libc", "libc",
"ndk 0.9.0", "ndk 0.9.0",
"objc2 0.6.0", "objc2 0.6.1",
"objc2-app-kit", "objc2-app-kit",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-foundation 0.3.0", "objc2-foundation 0.3.1",
"objc2-ui-kit", "objc2-ui-kit",
"objc2-web-kit", "objc2-web-kit",
"once_cell", "once_cell",
@@ -8187,6 +8357,12 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
[[package]]
name = "yazi"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5"
[[package]] [[package]]
name = "yoke" name = "yoke"
version = "0.7.5" version = "0.7.5"
@@ -8211,6 +8387,12 @@ dependencies = [
"synstructure", "synstructure",
] ]
[[package]]
name = "zeno"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524"
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.7.35" version = "0.7.35"
+12 -1
View File
@@ -5,11 +5,14 @@ members = [
"frontend/wasm", "frontend/wasm",
"frontend/src-tauri", "frontend/src-tauri",
"node-graph/gapplication-io", "node-graph/gapplication-io",
"node-graph/gbrush",
"node-graph/gcore", "node-graph/gcore",
"node-graph/gstd", "node-graph/gstd",
"node-graph/gmath-nodes",
"node-graph/gpath-bool", "node-graph/gpath-bool",
"node-graph/graph-craft", "node-graph/graph-craft",
"node-graph/graphene-cli", "node-graph/graphene-cli",
"node-graph/graster-nodes",
"node-graph/gsvg-renderer", "node-graph/gsvg-renderer",
"node-graph/interpreted-executor", "node-graph/interpreted-executor",
"node-graph/node-macro", "node-graph/node-macro",
@@ -23,11 +26,14 @@ members = [
default-members = [ default-members = [
"editor", "editor",
"frontend/wasm", "frontend/wasm",
"node-graph/gbrush",
"node-graph/gcore", "node-graph/gcore",
"node-graph/gstd", "node-graph/gstd",
"node-graph/gmath-nodes",
"node-graph/gpath-bool", "node-graph/gpath-bool",
"node-graph/graph-craft", "node-graph/graph-craft",
"node-graph/graphene-cli", "node-graph/graphene-cli",
"node-graph/graster-nodes",
"node-graph/gsvg-renderer", "node-graph/gsvg-renderer",
"node-graph/interpreted-executor", "node-graph/interpreted-executor",
"node-graph/node-macro", "node-graph/node-macro",
@@ -42,9 +48,12 @@ preprocessor = { path = "node-graph/preprocessor"}
math-parser = { path = "libraries/math-parser" } math-parser = { path = "libraries/math-parser" }
path-bool = { path = "libraries/path-bool" } path-bool = { path = "libraries/path-bool" }
graphene-application-io = { path = "node-graph/gapplication-io" } graphene-application-io = { path = "node-graph/gapplication-io" }
graphene-brush = { path = "node-graph/gbrush" }
graphene-core = { path = "node-graph/gcore" } graphene-core = { path = "node-graph/gcore" }
graphene-math-nodes = { path = "node-graph/gmath-nodes" }
graphene-path-bool = { path = "node-graph/gpath-bool" } graphene-path-bool = { path = "node-graph/gpath-bool" }
graph-craft = { path = "node-graph/graph-craft" } graph-craft = { path = "node-graph/graph-craft" }
graphene-raster-nodes = { path = "node-graph/graster-nodes" }
graphene-std = { path = "node-graph/gstd" } graphene-std = { path = "node-graph/gstd" }
graphene-svg-renderer = { path = "node-graph/gsvg-renderer" } graphene-svg-renderer = { path = "node-graph/gsvg-renderer" }
interpreted-executor = { path = "node-graph/interpreted-executor" } interpreted-executor = { path = "node-graph/interpreted-executor" }
@@ -113,7 +122,8 @@ rand_chacha = "0.9"
glam = { version = "0.29", default-features = false, features = ["serde", "scalar-math", "debug-glam-assert"] } glam = { version = "0.29", default-features = false, features = ["serde", "scalar-math", "debug-glam-assert"] }
base64 = "0.22" base64 = "0.22"
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp"] } image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp"] }
rustybuzz = "0.20" parley = "0.5.0"
skrifa = "0.32.0"
pretty_assertions = "1.4.1" pretty_assertions = "1.4.1"
fern = { version = "0.7", features = ["colored"] } fern = { version = "0.7", features = ["colored"] }
num_enum = "0.7" num_enum = "0.7"
@@ -143,6 +153,7 @@ half = { version = "2.4.1", default-features = false, features = ["bytemuck", "s
tinyvec = { version = "1" } tinyvec = { version = "1" }
criterion = { version = "0.5", features = ["html_reports"] } criterion = { version = "0.5", features = ["html_reports"] }
iai-callgrind = { version = "0.12.3" } iai-callgrind = { version = "0.12.3" }
ndarray = "0.16.1"
[profile.dev] [profile.dev]
opt-level = 1 opt-level = 1
+15 -11
View File
@@ -12,7 +12,7 @@
**Graphite is a free, open source vector and raster graphics engine, [available now](https://editor.graphite.rs) in alpha. Get creative with a fully nondestructive editing workflow that combines layer-based compositing with node-based generative design.** **Graphite is a free, open source vector and raster graphics engine, [available now](https://editor.graphite.rs) in alpha. Get creative with a fully nondestructive editing workflow that combines layer-based compositing with node-based generative design.**
Having begun life as a vector editor, Graphite continues evolving into a generalized, all-in-one graphics toolbox that's built more like a game engine than a conventional creative app. The editor's tools wrap its node graph core, providing user-friendly workflows for vector, raster, and beyond. Photo editing, motion graphics, digital painting, desktop publishing, and VFX compositing are additional competencies from the [roadmap](https://graphite.rs/features/#roadmap) making Graphite into a highly versatile content creation tool. Having begun life as a vector editor, Graphite continues evolving into a generalized, all-in-one graphics toolbox that's built more like a game engine than a conventional creative app. The editor's tools wrap its node graph core, providing user-friendly workflows for vector, raster, and beyond. Photo editing, motion graphics, digital painting, desktop publishing, and VFX compositing are additional competencies on the planned [roadmap](https://graphite.rs/features/#roadmap) making Graphite into a highly versatile content creation tool.
Learn more from the [website](https://graphite.rs/), subscribe to the [newsletter](https://graphite.rs/#newsletter), consider [volunteering](https://graphite.rs/volunteer/) or [donating](https://graphite.rs/donate/), and remember to give this repository a ⭐! Learn more from the [website](https://graphite.rs/), subscribe to the [newsletter](https://graphite.rs/#newsletter), consider [volunteering](https://graphite.rs/volunteer/) or [donating](https://graphite.rs/donate/), and remember to give this repository a ⭐!
@@ -58,22 +58,26 @@ Learn more from the [website](https://graphite.rs/), subscribe to the [newslette
</a> </a>
<br /><br /> <br /><br />
https://github.com/user-attachments/assets/f4604aea-e8f1-45ce-9218-46ddc666f11d
## Support our mission ❤️
Graphite is 100% community built and funded. Please become a part of keeping the project alive and thriving with a [donation](https://graphite.rs/donate/) if you share a belief in our **mission**:
> Graphite strives to unshackle the creativity of every budding artist and seasoned professional by building the best comprehensive art and design tool that's accessible to all.
>
> Mission success will come when Graphite is an industry standard. A cohesive product vision and focus on innovation over imitation is the strategy that will make that possible.
## Screenshots ## Screenshots
!["Isometric Fountain" vector artwork](https://static.graphite.rs/content/index/gui-demo-node-graph-isometric-fountain.png) ![Made using nondestructive boolean operations and procedural polka dot patterns](https://github.com/user-attachments/assets/decb7011-18c2-4c68-82af-d1fa5064244a)
!["Marbled Mandelbrot" fractal raster artwork](https://static.graphite.rs/content/index/gui-demo-fractal__3.png) ![Mandelbrot fractal filled with a noise pattern, procedurally generated and infinitely scalable](https://github.com/user-attachments/assets/9e023997-185b-4f43-a724-797d308d9e7b)
![Design for a magazine spread, a preview of the upcoming focus on desktop publishing](https://github.com/user-attachments/assets/90eca551-5868-4f8d-9016-33958bf96345)
## Contributing/building the code ## Contributing/building the code
Are you a graphics programmer or Rust developer? Graphite aims to be one of the most approachable projects for putting your engineering skills to use in the world of open source. See [instructions here](https://graphite.rs/volunteer/guide/) for setting up the project and getting started. Are you a graphics programmer or Rust developer? Graphite aims to be one of the most approachable projects for putting your engineering skills to use in the world of open source. See [instructions here](https://graphite.rs/volunteer/guide/) for setting up the project and getting started.
*By submitting code for inclusion in the project, you are agreeing to license your changes under the Apache 2.0 license, and that you have the authority to do so. Some directories may have other licenses, like dual-licensed MIT/Apache 2.0, and code submissions to those directories mean you agree to the applicable license(s).* *By submitting code for inclusion in the project, you are agreeing to license your changes under the Apache 2.0 license, and that you have the authority to do so. Some directories may have other licenses, like dual-licensed MIT/Apache 2.0, and code submissions to those directories mean you agree to the applicable license(s).*
## Support our mission ❤️
Graphite is 100% community built and funded. Please become a part of keeping our project alive and thriving with a [donation](https://graphite.rs/donate/) if you share a belief in our mission:
> Graphite strives to unshackle the creativity of every budding artist and seasoned professional by building the best comprehensive art and design tool that's accessible to all.
>
> Mission success will come when Graphite is an industry standard. A cohesive product vision and focus on innovation over imitation is the strategy that will make that possible.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -36,9 +36,9 @@ thiserror = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
bezier-rs = { workspace = true } bezier-rs = { workspace = true }
kurbo = { workspace = true }
futures = { workspace = true } futures = { workspace = true }
glam = { workspace = true } glam = { workspace = true }
kurbo = { workspace = true }
derivative = { workspace = true } derivative = { workspace = true }
specta = { workspace = true } specta = { workspace = true }
dyn-any = { workspace = true } dyn-any = { workspace = true }
+2 -1
View File
@@ -102,7 +102,7 @@ pub const MANIPULATOR_GROUP_MARKER_SIZE: f64 = 6.;
pub const SELECTION_THRESHOLD: f64 = 10.; pub const SELECTION_THRESHOLD: f64 = 10.;
pub const HIDE_HANDLE_DISTANCE: f64 = 3.; pub const HIDE_HANDLE_DISTANCE: f64 = 3.;
pub const HANDLE_ROTATE_SNAP_ANGLE: f64 = 15.; pub const HANDLE_ROTATE_SNAP_ANGLE: f64 = 15.;
pub const SEGMENT_INSERTION_DISTANCE: f64 = 8.; pub const SEGMENT_INSERTION_DISTANCE: f64 = 5.;
pub const SEGMENT_OVERLAY_SIZE: f64 = 10.; pub const SEGMENT_OVERLAY_SIZE: f64 = 10.;
pub const HANDLE_LENGTH_FACTOR: f64 = 0.5; pub const HANDLE_LENGTH_FACTOR: f64 = 0.5;
@@ -133,6 +133,7 @@ pub const SCALE_EFFECT: f64 = 0.5;
// COLORS // COLORS
pub const COLOR_OVERLAY_BLUE: &str = "#00a8ff"; pub const COLOR_OVERLAY_BLUE: &str = "#00a8ff";
pub const COLOR_OVERLAY_BLUE_50: &str = "rgba(0, 168, 255, 0.5)";
pub const COLOR_OVERLAY_YELLOW: &str = "#ffc848"; pub const COLOR_OVERLAY_YELLOW: &str = "#ffc848";
pub const COLOR_OVERLAY_GREEN: &str = "#63ce63"; pub const COLOR_OVERLAY_GREEN: &str = "#63ce63";
pub const COLOR_OVERLAY_RED: &str = "#ef5454"; pub const COLOR_OVERLAY_RED: &str = "#ef5454";
@@ -214,7 +214,7 @@ pub fn input_mappings() -> Mapping {
entry!(KeyDown(Delete); modifiers=[Shift], action_dispatch=PathToolMessage::DeleteAndRefit), entry!(KeyDown(Delete); modifiers=[Shift], action_dispatch=PathToolMessage::DeleteAndRefit),
entry!(KeyDown(Backspace); modifiers=[Shift], action_dispatch=PathToolMessage::DeleteAndRefit), entry!(KeyDown(Backspace); modifiers=[Shift], action_dispatch=PathToolMessage::DeleteAndRefit),
entry!(KeyDownNoRepeat(Tab); action_dispatch=PathToolMessage::SwapSelectedHandles), entry!(KeyDownNoRepeat(Tab); action_dispatch=PathToolMessage::SwapSelectedHandles),
entry!(KeyDown(MouseLeft); action_dispatch=PathToolMessage::MouseDown { extend_selection: Shift, lasso_select: Control, handle_drag_from_anchor: Alt, drag_restore_handle: Control }), entry!(KeyDown(MouseLeft); action_dispatch=PathToolMessage::MouseDown { extend_selection: Shift, lasso_select: Control, handle_drag_from_anchor: Alt, drag_restore_handle: Control, molding_in_segment_edit: KeyA }),
entry!(KeyDown(MouseRight); action_dispatch=PathToolMessage::RightClick), entry!(KeyDown(MouseRight); action_dispatch=PathToolMessage::RightClick),
entry!(KeyDown(Escape); action_dispatch=PathToolMessage::Escape), entry!(KeyDown(Escape); action_dispatch=PathToolMessage::Escape),
entry!(KeyDown(KeyG); action_dispatch=PathToolMessage::GRS { key: KeyG }), entry!(KeyDown(KeyG); action_dispatch=PathToolMessage::GRS { key: KeyG }),
@@ -6,12 +6,12 @@ use bezier_rs::Subpath;
use glam::{DAffine2, DVec2, IVec2}; use glam::{DAffine2, DVec2, IVec2};
use graph_craft::document::NodeId; use graph_craft::document::NodeId;
use graphene_std::Artboard; use graphene_std::Artboard;
use graphene_std::brush::brush_stroke::BrushStroke;
use graphene_std::raster::BlendMode; use graphene_std::raster::BlendMode;
use graphene_std::raster_types::{CPU, RasterDataTable}; use graphene_std::raster_types::{CPU, RasterDataTable};
use graphene_std::text::{Font, TypesettingConfig}; use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::PointId; use graphene_std::vector::PointId;
use graphene_std::vector::VectorModificationType; use graphene_std::vector::VectorModificationType;
use graphene_std::vector::brush_stroke::BrushStroke;
use graphene_std::vector::style::{Fill, Stroke}; use graphene_std::vector::style::{Fill, Stroke};
#[impl_message(Message, DocumentMessage, GraphOperation)] #[impl_message(Message, DocumentMessage, GraphOperation)]
@@ -9,10 +9,10 @@ use graph_craft::concrete;
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput}; use graph_craft::document::{NodeId, NodeInput};
use graphene_std::Artboard; use graphene_std::Artboard;
use graphene_std::brush::brush_stroke::BrushStroke;
use graphene_std::raster::BlendMode; use graphene_std::raster::BlendMode;
use graphene_std::raster_types::{CPU, RasterDataTable}; use graphene_std::raster_types::{CPU, RasterDataTable};
use graphene_std::text::{Font, TypesettingConfig}; use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::brush_stroke::BrushStroke;
use graphene_std::vector::style::{Fill, Stroke}; use graphene_std::vector::style::{Fill, Stroke};
use graphene_std::vector::{PointId, VectorModificationType}; use graphene_std::vector::{PointId, VectorModificationType};
use graphene_std::vector::{VectorData, VectorDataTable}; use graphene_std::vector::{VectorData, VectorDataTable};
@@ -190,6 +190,7 @@ impl<'a> ModifyInputsContext<'a> {
Some(NodeInput::value(TaggedValue::F64(typesetting.character_spacing), false)), Some(NodeInput::value(TaggedValue::F64(typesetting.character_spacing), false)),
Some(NodeInput::value(TaggedValue::OptionalF64(typesetting.max_width), false)), Some(NodeInput::value(TaggedValue::OptionalF64(typesetting.max_width), false)),
Some(NodeInput::value(TaggedValue::OptionalF64(typesetting.max_height), false)), Some(NodeInput::value(TaggedValue::OptionalF64(typesetting.max_height), false)),
Some(NodeInput::value(TaggedValue::F64(typesetting.tilt), false)),
]); ]);
let text_id = NodeId::new(); let text_id = NodeId::new();
@@ -16,8 +16,8 @@ use graph_craft::ProtoNodeIdentifier;
use graph_craft::concrete; use graph_craft::concrete;
use graph_craft::document::value::*; use graph_craft::document::value::*;
use graph_craft::document::*; use graph_craft::document::*;
use graphene_std::ops::XY; use graphene_std::brush::brush_cache::BrushCache;
use graphene_std::raster::brush_cache::BrushCache; use graphene_std::extract_xy::XY;
use graphene_std::raster::{CellularDistanceFunction, CellularReturnType, Color, DomainWarpType, FractalType, NoiseType, RedGreenBlueAlpha}; use graphene_std::raster::{CellularDistanceFunction, CellularReturnType, Color, DomainWarpType, FractalType, NoiseType, RedGreenBlueAlpha};
use graphene_std::raster_types::{CPU, RasterDataTable}; use graphene_std::raster_types::{CPU, RasterDataTable};
use graphene_std::text::{Font, TypesettingConfig}; use graphene_std::text::{Font, TypesettingConfig};
@@ -960,13 +960,13 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
nodes: [ nodes: [
DocumentNode { DocumentNode {
inputs: vec![NodeInput::network(concrete!(RasterDataTable<CPU>), 0), NodeInput::value(TaggedValue::XY(XY::X), false)], inputs: vec![NodeInput::network(concrete!(RasterDataTable<CPU>), 0), NodeInput::value(TaggedValue::XY(XY::X), false)],
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::ops::ExtractXyNode")), implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::extract_xy::ExtractXyNode")),
manual_composition: Some(generic!(T)), manual_composition: Some(generic!(T)),
..Default::default() ..Default::default()
}, },
DocumentNode { DocumentNode {
inputs: vec![NodeInput::network(concrete!(RasterDataTable<CPU>), 0), NodeInput::value(TaggedValue::XY(XY::Y), false)], inputs: vec![NodeInput::network(concrete!(RasterDataTable<CPU>), 0), NodeInput::value(TaggedValue::XY(XY::Y), false)],
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::ops::ExtractXyNode")), implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::extract_xy::ExtractXyNode")),
manual_composition: Some(generic!(T)), manual_composition: Some(generic!(T)),
..Default::default() ..Default::default()
}, },
@@ -1016,7 +1016,9 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default() ..Default::default()
}, },
}, },
description: Cow::Borrowed("TODO"), description: Cow::Borrowed(
"Decomposes the X and Y components of a 2D coordinate.\n\nThe inverse of this node is \"Coordinate Value\", which can have either or both its X and Y exposed as graph inputs.",
),
properties: None, properties: None,
}, },
// TODO: Remove this and just use the proto node definition directly // TODO: Remove this and just use the proto node definition directly
@@ -1030,7 +1032,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
nodes: vec![DocumentNode { nodes: vec![DocumentNode {
inputs: vec![ inputs: vec![
NodeInput::network(concrete!(RasterDataTable<CPU>), 0), NodeInput::network(concrete!(RasterDataTable<CPU>), 0),
NodeInput::network(concrete!(Vec<graphene_std::vector::brush_stroke::BrushStroke>), 1), NodeInput::network(concrete!(Vec<brush::brush_stroke::BrushStroke>), 1),
NodeInput::network(concrete!(BrushCache), 2), NodeInput::network(concrete!(BrushCache), 2),
], ],
manual_composition: Some(concrete!(Context)), manual_composition: Some(concrete!(Context)),
@@ -1556,6 +1558,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
NodeInput::value(TaggedValue::F64(TypesettingConfig::default().character_spacing), false), NodeInput::value(TaggedValue::F64(TypesettingConfig::default().character_spacing), false),
NodeInput::value(TaggedValue::OptionalF64(TypesettingConfig::default().max_width), false), NodeInput::value(TaggedValue::OptionalF64(TypesettingConfig::default().max_width), false),
NodeInput::value(TaggedValue::OptionalF64(TypesettingConfig::default().max_height), false), NodeInput::value(TaggedValue::OptionalF64(TypesettingConfig::default().max_height), false),
NodeInput::value(TaggedValue::F64(TypesettingConfig::default().tilt), false),
], ],
..Default::default() ..Default::default()
}, },
@@ -1577,6 +1580,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
"Line Height", "Line Height",
"TODO", "TODO",
WidgetOverride::Number(NumberInputSettings { WidgetOverride::Number(NumberInputSettings {
unit: Some("x".to_string()),
min: Some(0.), min: Some(0.),
step: Some(0.1), step: Some(0.1),
..Default::default() ..Default::default()
@@ -1586,6 +1590,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
"Character Spacing", "Character Spacing",
"TODO", "TODO",
WidgetOverride::Number(NumberInputSettings { WidgetOverride::Number(NumberInputSettings {
unit: Some(" px".to_string()),
min: Some(0.), min: Some(0.),
step: Some(0.1), step: Some(0.1),
..Default::default() ..Default::default()
@@ -1595,6 +1600,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
"Max Width", "Max Width",
"TODO", "TODO",
WidgetOverride::Number(NumberInputSettings { WidgetOverride::Number(NumberInputSettings {
unit: Some(" px".to_string()),
min: Some(1.), min: Some(1.),
blank_assist: false, blank_assist: false,
..Default::default() ..Default::default()
@@ -1604,11 +1610,22 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
"Max Height", "Max Height",
"TODO", "TODO",
WidgetOverride::Number(NumberInputSettings { WidgetOverride::Number(NumberInputSettings {
unit: Some(" px".to_string()),
min: Some(1.), min: Some(1.),
blank_assist: false, blank_assist: false,
..Default::default() ..Default::default()
}), }),
), ),
PropertiesRow::with_override(
"Tilt",
"Faux italic",
WidgetOverride::Number(NumberInputSettings {
min: Some(-85.),
max: Some(85.),
unit: Some("°".to_string()),
..Default::default()
}),
),
], ],
output_names: vec!["Vector".to_string()], output_names: vec!["Vector".to_string()],
..Default::default() ..Default::default()
@@ -1841,7 +1858,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
NodeInput::network(concrete!(graphene_std::vector::VectorDataTable), 0), NodeInput::network(concrete!(graphene_std::vector::VectorDataTable), 0),
NodeInput::network(concrete!(vector::misc::PointSpacingType), 1), NodeInput::network(concrete!(vector::misc::PointSpacingType), 1),
NodeInput::network(concrete!(f64), 2), NodeInput::network(concrete!(f64), 2),
NodeInput::network(concrete!(f64), 3), NodeInput::network(concrete!(u32), 3),
NodeInput::network(concrete!(f64), 4), NodeInput::network(concrete!(f64), 4),
NodeInput::network(concrete!(f64), 5), NodeInput::network(concrete!(f64), 5),
NodeInput::network(concrete!(bool), 6), NodeInput::network(concrete!(bool), 6),
@@ -1880,7 +1897,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
NodeInput::value(TaggedValue::VectorData(graphene_std::vector::VectorDataTable::default()), true), NodeInput::value(TaggedValue::VectorData(graphene_std::vector::VectorDataTable::default()), true),
NodeInput::value(TaggedValue::PointSpacingType(Default::default()), false), NodeInput::value(TaggedValue::PointSpacingType(Default::default()), false),
NodeInput::value(TaggedValue::F64(100.), false), NodeInput::value(TaggedValue::F64(100.), false),
NodeInput::value(TaggedValue::F64(100.), false), NodeInput::value(TaggedValue::U32(100), false),
NodeInput::value(TaggedValue::F64(0.), false), NodeInput::value(TaggedValue::F64(0.), false),
NodeInput::value(TaggedValue::F64(0.), false), NodeInput::value(TaggedValue::F64(0.), false),
NodeInput::value(TaggedValue::Bool(false), false), NodeInput::value(TaggedValue::Bool(false), false),
@@ -12,7 +12,7 @@ use graph_craft::Type;
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput}; use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput};
use graphene_std::animation::RealTimeMode; use graphene_std::animation::RealTimeMode;
use graphene_std::ops::XY; use graphene_std::extract_xy::XY;
use graphene_std::path_bool::BooleanOperation; use graphene_std::path_bool::BooleanOperation;
use graphene_std::raster::curve::Curve; use graphene_std::raster::curve::Curve;
use graphene_std::raster::{ use graphene_std::raster::{
@@ -1427,6 +1427,7 @@ pub(crate) fn rectangle_properties(node_id: NodeId, context: &mut NodeProperties
} else { } else {
NumberInput::default() NumberInput::default()
.value(Some(uniform_val)) .value(Some(uniform_val))
.unit(" px")
.on_update(update_value(move |x: &NumberInput| TaggedValue::F64(x.value.unwrap()), node_id, CornerRadiusInput::<f64>::INDEX)) .on_update(update_value(move |x: &NumberInput| TaggedValue::F64(x.value.unwrap()), node_id, CornerRadiusInput::<f64>::INDEX))
.on_commit(commit_value) .on_commit(commit_value)
.widget_holder() .widget_holder()
@@ -1840,7 +1841,7 @@ pub fn offset_path_properties(node_id: NodeId, context: &mut NodePropertiesConte
} }
pub fn math_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> { pub fn math_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
use graphene_std::ops::math::*; use graphene_std::math_nodes::math::*;
let document_node = match get_document_node(node_id, context) { let document_node = match get_document_node(node_id, context) {
Ok(document_node) => document_node, Ok(document_node) => document_node,
@@ -124,8 +124,19 @@ pub fn path_overlays(document: &DocumentMessageHandler, draw_handles: DrawHandle
overlay_context.outline_vector(&vector_data, transform); overlay_context.outline_vector(&vector_data, transform);
} }
// Get the selected segments and then add a bold line overlay on them
for (segment_id, bezier, _, _) in vector_data.segment_bezier_iter() {
let Some(selected_shape_state) = shape_editor.selected_shape_state.get_mut(&layer) else {
continue;
};
if selected_shape_state.is_segment_selected(segment_id) {
overlay_context.outline_select_bezier(bezier, transform);
}
}
let selected = shape_editor.selected_shape_state.get(&layer); let selected = shape_editor.selected_shape_state.get(&layer);
let is_selected = |point: ManipulatorPointId| selected.is_some_and(|selected| selected.is_selected(point)); let is_selected = |point: ManipulatorPointId| selected.is_some_and(|selected| selected.is_point_selected(point));
if display_handles { if display_handles {
let opposite_handles_data: Vec<(PointId, SegmentId)> = shape_editor.selected_points().filter_map(|point_id| vector_data.adjacent_segment(point_id)).collect(); let opposite_handles_data: Vec<(PointId, SegmentId)> = shape_editor.selected_points().filter_map(|point_id| vector_data.adjacent_segment(point_id)).collect();
@@ -187,7 +198,7 @@ pub fn path_endpoint_overlays(document: &DocumentMessageHandler, shape_editor: &
//let document_to_viewport = document.navigation_handler.calculate_offset_transform(overlay_context.size / 2., &document.document_ptz); //let document_to_viewport = document.navigation_handler.calculate_offset_transform(overlay_context.size / 2., &document.document_ptz);
let transform = document.metadata().transform_to_viewport(layer); let transform = document.metadata().transform_to_viewport(layer);
let selected = shape_editor.selected_shape_state.get(&layer); let selected = shape_editor.selected_shape_state.get(&layer);
let is_selected = |selected: Option<&SelectedLayerState>, point: ManipulatorPointId| selected.is_some_and(|selected| selected.is_selected(point)); let is_selected = |selected: Option<&SelectedLayerState>, point: ManipulatorPointId| selected.is_some_and(|selected| selected.is_point_selected(point));
for point in vector_data.extendable_points(preferences.vector_meshes) { for point in vector_data.extendable_points(preferences.vector_meshes) {
let Some(position) = vector_data.point_domain.position_from_id(point) else { continue }; let Some(position) = vector_data.point_domain.position_from_id(point) else { continue };
@@ -1,7 +1,7 @@
use super::utility_functions::overlay_canvas_context; use super::utility_functions::overlay_canvas_context;
use crate::consts::{ use crate::consts::{
COLOR_OVERLAY_BLUE, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COLOR_OVERLAY_WHITE, COLOR_OVERLAY_YELLOW, COMPASS_ROSE_ARROW_SIZE, COMPASS_ROSE_HOVER_RING_DIAMETER, COMPASS_ROSE_MAIN_RING_DIAMETER, COLOR_OVERLAY_BLUE, COLOR_OVERLAY_BLUE_50, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COLOR_OVERLAY_WHITE, COLOR_OVERLAY_YELLOW, COMPASS_ROSE_ARROW_SIZE, COMPASS_ROSE_HOVER_RING_DIAMETER,
COMPASS_ROSE_RING_INNER_DIAMETER, MANIPULATOR_GROUP_MARKER_SIZE, PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, COMPASS_ROSE_MAIN_RING_DIAMETER, COMPASS_ROSE_RING_INNER_DIAMETER, MANIPULATOR_GROUP_MARKER_SIZE, PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER,
}; };
use crate::messages::prelude::Message; use crate::messages::prelude::Message;
use bezier_rs::{Bezier, Subpath}; use bezier_rs::{Bezier, Subpath};
@@ -581,6 +581,35 @@ impl OverlayContext {
self.end_dpi_aware_transform(); self.end_dpi_aware_transform();
} }
/// Used by the path tool segment mode in order to show the selected segments.
pub fn outline_select_bezier(&mut self, bezier: Bezier, transform: DAffine2) {
self.start_dpi_aware_transform();
self.render_context.begin_path();
self.bezier_command(bezier, transform, true);
self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE);
self.render_context.set_line_width(4.);
self.render_context.stroke();
self.render_context.set_line_width(1.);
self.end_dpi_aware_transform();
}
pub fn outline_overlay_bezier(&mut self, bezier: Bezier, transform: DAffine2) {
self.start_dpi_aware_transform();
self.render_context.begin_path();
self.bezier_command(bezier, transform, true);
self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE_50);
self.render_context.set_line_width(4.);
self.render_context.stroke();
self.render_context.set_line_width(1.);
self.end_dpi_aware_transform();
}
fn bezier_command(&self, bezier: Bezier, transform: DAffine2, move_to: bool) { fn bezier_command(&self, bezier: Bezier, transform: DAffine2, move_to: bool) {
self.start_dpi_aware_transform(); self.start_dpi_aware_transform();
@@ -1161,8 +1161,7 @@ impl NodeNetworkInterface {
.and_then(|node_metadata| node_metadata.persistent_metadata.input_properties.get(index)) .and_then(|node_metadata| node_metadata.persistent_metadata.input_properties.get(index))
} }
pub fn insert_input_properties_row(&mut self, node_id: &NodeId, index: usize, network_path: &[NodeId]) { pub fn insert_input_properties_row(&mut self, node_id: &NodeId, index: usize, network_path: &[NodeId], row: PropertiesRow) {
let row = ("", "TODO").into();
let _ = self let _ = self
.node_metadata_mut(node_id, network_path) .node_metadata_mut(node_id, network_path)
.map(|node_metadata| node_metadata.persistent_metadata.input_properties.insert(index - 1, row)); .map(|node_metadata| node_metadata.persistent_metadata.input_properties.insert(index - 1, row));
@@ -9,9 +9,7 @@ use crate::messages::tool::common_functionality::shape_editor::ShapeState;
use crate::messages::tool::utility_types::ToolType; use crate::messages::tool::utility_types::ToolType;
use glam::{DAffine2, DMat2, DVec2}; use glam::{DAffine2, DMat2, DVec2};
use graphene_std::renderer::Quad; use graphene_std::renderer::Quad;
use graphene_std::vector::VectorModificationType; use graphene_std::vector::{HandleExt, HandleId, ManipulatorPointId, PointId, VectorModificationType};
use graphene_std::vector::{HandleExt, ManipulatorPointId};
use graphene_std::vector::{HandleId, PointId};
use std::collections::{HashMap, VecDeque}; use std::collections::{HashMap, VecDeque};
use std::f64::consts::PI; use std::f64::consts::PI;
@@ -88,6 +86,18 @@ impl OriginalTransforms {
let Some(selected_points) = shape_editor.selected_points_in_layer(layer) else { let Some(selected_points) = shape_editor.selected_points_in_layer(layer) else {
continue; continue;
}; };
let Some(selected_segments) = shape_editor.selected_segments_in_layer(layer) else {
continue;
};
let mut selected_points = selected_points.clone();
for (segment_id, _, start, end) in vector_data.segment_bezier_iter() {
if selected_segments.contains(&segment_id) {
selected_points.insert(ManipulatorPointId::Anchor(start));
selected_points.insert(ManipulatorPointId::Anchor(end));
}
}
// Anchors also move their handles // Anchors also move their handles
let anchor_ids = selected_points.iter().filter_map(|point| point.as_anchor()); let anchor_ids = selected_points.iter().filter_map(|point| point.as_anchor());
@@ -604,7 +614,7 @@ impl<'a> Selected<'a> {
responses.add(GraphOperationMessage::Vector { layer, modification_type }); responses.add(GraphOperationMessage::Vector { layer, modification_type });
} }
if transform_operation.is_some_and(|transform_operation| matches!(transform_operation, TransformOperation::Scaling(_))) && initial_points.anchors.len() > 1 { if transform_operation.is_some_and(|transform_operation| matches!(transform_operation, TransformOperation::Scaling(_))) && (initial_points.anchors.len() == 2) {
return; return;
} }
@@ -1,6 +1,7 @@
// TODO: Eventually remove this document upgrade code // TODO: Eventually remove this document upgrade code
// This file contains lots of hacky code for upgrading old documents to the new format // This file contains lots of hacky code for upgrading old documents to the new format
use super::document::utility_types::network_interface::{NumberInputSettings, PropertiesRow, WidgetOverride};
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type; use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, OutputConnector}; use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, OutputConnector};
@@ -24,38 +25,113 @@ const REPLACEMENTS: &[(&str, &str)] = &[
("graphene_core::ConstructArtboardNode", "graphene_core::graphic_element::ToArtboardNode"), ("graphene_core::ConstructArtboardNode", "graphene_core::graphic_element::ToArtboardNode"),
("graphene_core::ToGraphicElementNode", "graphene_core::graphic_element::ToElementNode"), ("graphene_core::ToGraphicElementNode", "graphene_core::graphic_element::ToElementNode"),
("graphene_core::ToGraphicGroupNode", "graphene_core::graphic_element::ToGroupNode"), ("graphene_core::ToGraphicGroupNode", "graphene_core::graphic_element::ToGroupNode"),
// math_nodes
("graphene_core::ops::MathNode", "graphene_math_nodes::MathNode"),
("graphene_core::ops::AddNode", "graphene_math_nodes::AddNode"),
("graphene_core::ops::SubtractNode", "graphene_math_nodes::SubtractNode"),
("graphene_core::ops::MultiplyNode", "graphene_math_nodes::MultiplyNode"),
("graphene_core::ops::DivideNode", "graphene_math_nodes::DivideNode"),
("graphene_core::ops::ModuloNode", "graphene_math_nodes::ModuloNode"),
("graphene_core::ops::ExponentNode", "graphene_math_nodes::ExponentNode"),
("graphene_core::ops::RootNode", "graphene_math_nodes::RootNode"),
("graphene_core::ops::LogarithmNode", "graphene_math_nodes::LogarithmNode"),
("graphene_core::ops::SineNode", "graphene_math_nodes::SineNode"),
("graphene_core::ops::CosineNode", "graphene_math_nodes::CosineNode"),
("graphene_core::ops::TangentNode", "graphene_math_nodes::TangentNode"),
("graphene_core::ops::SineInverseNode", "graphene_math_nodes::SineInverseNode"),
("graphene_core::ops::CosineInverseNode", "graphene_math_nodes::CosineInverseNode"),
("graphene_core::ops::TangentInverseNode", "graphene_math_nodes::TangentInverseNode"),
("graphene_core::ops::RandomNode", "graphene_math_nodes::RandomNode"),
("graphene_core::ops::ToU32Node", "graphene_math_nodes::ToU32Node"),
("graphene_core::ops::ToU64Node", "graphene_math_nodes::ToU64Node"),
("graphene_core::ops::ToF64Node", "graphene_math_nodes::ToF64Node"),
("graphene_core::ops::RoundNode", "graphene_math_nodes::RoundNode"),
("graphene_core::ops::FloorNode", "graphene_math_nodes::FloorNode"),
("graphene_core::ops::CeilingNode", "graphene_math_nodes::CeilingNode"),
("graphene_core::ops::MinNode", "graphene_math_nodes::MinNode"),
("graphene_core::ops::MaxNode", "graphene_math_nodes::MaxNode"),
("graphene_core::ops::ClampNode", "graphene_math_nodes::ClampNode"),
("graphene_core::ops::EqualsNode", "graphene_math_nodes::EqualsNode"),
("graphene_core::ops::NotEqualsNode", "graphene_math_nodes::NotEqualsNode"),
("graphene_core::ops::LessThanNode", "graphene_math_nodes::LessThanNode"),
("graphene_core::ops::GreaterThanNode", "graphene_math_nodes::GreaterThanNode"),
("graphene_core::ops::LogicalOrNode", "graphene_math_nodes::LogicalOrNode"),
("graphene_core::ops::LogicalAndNode", "graphene_math_nodes::LogicalAndNode"),
("graphene_core::ops::LogicalNotNode", "graphene_math_nodes::LogicalNotNode"),
("graphene_core::ops::BoolValueNode", "graphene_math_nodes::BoolValueNode"),
("graphene_core::ops::NumberValueNode", "graphene_math_nodes::NumberValueNode"),
("graphene_core::ops::PercentageValueNode", "graphene_math_nodes::PercentageValueNode"),
("graphene_core::ops::CoordinateValueNode", "graphene_math_nodes::CoordinateValueNode"),
("graphene_core::ops::ColorValueNode", "graphene_math_nodes::ColorValueNode"),
("graphene_core::ops::GradientValueNode", "graphene_math_nodes::GradientValueNode"),
("graphene_core::ops::StringValueNode", "graphene_math_nodes::StringValueNode"),
("graphene_core::ops::DotProductNode", "graphene_math_nodes::DotProductNode"),
// debug
("graphene_core::ops::SizeOfNode", "graphene_core::debug::SizeOfNode"),
("graphene_core::ops::SomeNode", "graphene_core::debug::SomeNode"),
("graphene_core::ops::UnwrapNode", "graphene_core::debug::UnwrapNode"),
("graphene_core::ops::CloneNode", "graphene_core::debug::CloneNode"),
// ???
("graphene_core::ops::ExtractXyNode", "graphene_core::extract_xy::ExtractXyNode"),
("graphene_core::logic::LogicAndNode", "graphene_core::ops::LogicAndNode"), ("graphene_core::logic::LogicAndNode", "graphene_core::ops::LogicAndNode"),
("graphene_core::logic::LogicNotNode", "graphene_core::ops::LogicNotNode"), ("graphene_core::logic::LogicNotNode", "graphene_core::ops::LogicNotNode"),
("graphene_core::logic::LogicOrNode", "graphene_core::ops::LogicOrNode"), ("graphene_core::logic::LogicOrNode", "graphene_core::ops::LogicOrNode"),
("graphene_core::ops::ConstructVector2", "graphene_core::ops::CoordinateValueNode"), ("graphene_core::ops::ConstructVector2", "graphene_core::ops::CoordinateValueNode"),
("graphene_core::ops::Vector2ValueNode", "graphene_core::ops::CoordinateValueNode"), ("graphene_core::ops::Vector2ValueNode", "graphene_core::ops::CoordinateValueNode"),
("graphene_core::raster::BlackAndWhiteNode", "graphene_core::raster::adjustments::BlackAndWhiteNode"),
("graphene_core::raster::BlendNode", "graphene_core::raster::adjustments::BlendNode"),
("graphene_core::raster::BlendModeNode", "graphene_core::blending_nodes::BlendModeNode"), ("graphene_core::raster::BlendModeNode", "graphene_core::blending_nodes::BlendModeNode"),
("graphene_core::raster::OpacityNode", "graphene_core::blending_nodes::OpacityNode"), ("graphene_core::raster::OpacityNode", "graphene_core::blending_nodes::OpacityNode"),
("graphene_core::raster::BlendingNode", "graphene_core::blending_nodes::BlendingNode"), ("graphene_core::raster::BlendingNode", "graphene_core::blending_nodes::BlendingNode"),
("graphene_core::raster::ChannelMixerNode", "graphene_core::raster::adjustments::ChannelMixerNode"),
("graphene_core::raster::adjustments::ColorOverlayNode", "graphene_core::raster::adjustments::ColorOverlayNode"),
("graphene_core::raster::ExposureNode", "graphene_core::raster::adjustments::ExposureNode"),
("graphene_core::raster::ExtractChannelNode", "graphene_core::raster::adjustments::ExtractChannelNode"),
("graphene_core::raster::GradientMapNode", "graphene_core::raster::adjustments::GradientMapNode"),
("graphene_core::raster::HueSaturationNode", "graphene_core::raster::adjustments::HueSaturationNode"),
("graphene_core::vector::GenerateHandlesNode", "graphene_core::vector::AutoTangentsNode"), ("graphene_core::vector::GenerateHandlesNode", "graphene_core::vector::AutoTangentsNode"),
("graphene_core::vector::RemoveHandlesNode", "graphene_core::vector::AutoTangentsNode"), ("graphene_core::vector::RemoveHandlesNode", "graphene_core::vector::AutoTangentsNode"),
("graphene_core::raster::InvertNode", "graphene_core::raster::adjustments::InvertNode"), // raster::adjustments
("graphene_core::raster::InvertRGBNode", "graphene_core::raster::adjustments::InvertNode"), ("graphene_core::raster::adjustments::LuminanceNode", "graphene_raster_nodes::adjustments::LuminanceNode"),
("graphene_core::raster::LevelsNode", "graphene_core::raster::adjustments::LevelsNode"), ("graphene_core::raster::LuminanceNode", "graphene_raster_nodes::adjustments::LuminanceNode"),
("graphene_core::raster::LuminanceNode", "graphene_core::raster::adjustments::LuminanceNode"), ("graphene_core::raster::adjustments::ExtractChannelNode", "graphene_raster_nodes::adjustments::ExtractChannelNode"),
("graphene_core::raster::ExtractOpaqueNode", "graphene_core::raster::adjustments::MakeOpaqueNode"), ("graphene_core::raster::ExtractChannelNode", "graphene_raster_nodes::adjustments::ExtractChannelNode"),
("graphene_core::raster::PosterizeNode", "graphene_core::raster::adjustments::PosterizeNode"), ("graphene_core::raster::adjustments::MakeOpaqueNode", "graphene_raster_nodes::adjustments::MakeOpaqueNode"),
("graphene_core::raster::ThresholdNode", "graphene_core::raster::adjustments::ThresholdNode"), ("graphene_core::raster::ExtractOpaqueNode", "graphene_raster_nodes::adjustments::MakeOpaqueNode"),
("graphene_core::raster::VibranceNode", "graphene_core::raster::adjustments::VibranceNode"), (
"graphene_core::raster::adjustments::BrightnessContrastNode",
"graphene_raster_nodes::adjustments::BrightnessContrastNode",
),
("graphene_core::raster::adjustments::LevelsNode", "graphene_raster_nodes::adjustments::LevelsNode"),
("graphene_core::raster::LevelsNode", "graphene_raster_nodes::adjustments::LevelsNode"),
("graphene_core::raster::adjustments::BlackAndWhiteNode", "graphene_raster_nodes::adjustments::BlackAndWhiteNode"),
("graphene_core::raster::BlackAndWhiteNode", "graphene_raster_nodes::adjustments::BlackAndWhiteNode"),
("graphene_core::raster::adjustments::HueSaturationNode", "graphene_raster_nodes::adjustments::HueSaturationNode"),
("graphene_core::raster::HueSaturationNode", "graphene_raster_nodes::adjustments::HueSaturationNode"),
("graphene_core::raster::adjustments::InvertNode", "graphene_raster_nodes::adjustments::InvertNode"),
("graphene_core::raster::InvertNode", "graphene_raster_nodes::adjustments::InvertNode"),
("graphene_core::raster::InvertRGBNode", "graphene_raster_nodes::adjustments::InvertNode"),
("graphene_core::raster::adjustments::ThresholdNode", "graphene_raster_nodes::adjustments::ThresholdNode"),
("graphene_core::raster::ThresholdNode", "graphene_raster_nodes::adjustments::ThresholdNode"),
("graphene_core::raster::adjustments::BlendNode", "graphene_raster_nodes::adjustments::BlendNode"),
("graphene_core::raster::BlendNode", "graphene_raster_nodes::adjustments::BlendNode"),
("graphene_core::raster::BlendColorPairNode", "graphene_raster_nodes::adjustments::BlendColorPairNode"),
("graphene_core::raster::adjustments::BlendColorsNode", "graphene_raster_nodes::adjustments::BlendColorsNode"),
("graphene_core::raster::BlendColorsNode", "graphene_raster_nodes::adjustments::BlendColorsNode"),
("graphene_core::raster::adjustments::GradientMapNode", "graphene_raster_nodes::adjustments::GradientMapNode"),
("graphene_core::raster::GradientMapNode", "graphene_raster_nodes::adjustments::GradientMapNode"),
("graphene_core::raster::adjustments::VibranceNode", "graphene_raster_nodes::adjustments::VibranceNode"),
("graphene_core::raster::VibranceNode", "graphene_raster_nodes::adjustments::VibranceNode"),
("graphene_core::raster::adjustments::ChannelMixerNode", "graphene_raster_nodes::adjustments::ChannelMixerNode"),
("graphene_core::raster::ChannelMixerNode", "graphene_raster_nodes::adjustments::ChannelMixerNode"),
("graphene_core::raster::adjustments::SelectiveColorNode", "graphene_raster_nodes::adjustments::SelectiveColorNode"),
("graphene_core::raster::adjustments::PosterizeNode", "graphene_raster_nodes::adjustments::PosterizeNode"),
("graphene_core::raster::PosterizeNode", "graphene_raster_nodes::adjustments::PosterizeNode"),
("graphene_core::raster::adjustments::ExposureNode", "graphene_raster_nodes::adjustments::ExposureNode"),
("graphene_core::raster::ExposureNode", "graphene_raster_nodes::adjustments::ExposureNode"),
("graphene_core::raster::adjustments::GenerateCurvesNode", "graphene_raster_nodes::generate_curves::GenerateCurvesNode"),
("graphene_core::raster::adjustments::ColorOverlayNode", "graphene_raster_nodes::generate_curves::ColorOverlayNode"),
// text
("graphene_core::text::TextGeneratorNode", "graphene_core::text::TextNode"), ("graphene_core::text::TextGeneratorNode", "graphene_core::text::TextNode"),
// transform
("graphene_core::transform::SetTransformNode", "graphene_core::transform_nodes::ReplaceTransformNode"), ("graphene_core::transform::SetTransformNode", "graphene_core::transform_nodes::ReplaceTransformNode"),
("graphene_core::transform::ReplaceTransformNode", "graphene_core::transform_nodes::ReplaceTransformNode"), ("graphene_core::transform::ReplaceTransformNode", "graphene_core::transform_nodes::ReplaceTransformNode"),
("graphene_core::transform::TransformNode", "graphene_core::transform_nodes::TransformNode"), ("graphene_core::transform::TransformNode", "graphene_core::transform_nodes::TransformNode"),
("graphene_core::transform::BoundlessFootprintNode", "graphene_core::transform_nodes::BoundlessFootprintNode"), ("graphene_core::transform::BoundlessFootprintNode", "graphene_core::transform_nodes::BoundlessFootprintNode"),
("graphene_core::transform::FreezeRealTimeNode", "graphene_core::transform_nodes::FreezeRealTimeNode"), ("graphene_core::transform::FreezeRealTimeNode", "graphene_core::transform_nodes::FreezeRealTimeNode"),
// ???
("graphene_core::vector::SplinesFromPointsNode", "graphene_core::vector::SplineNode"), ("graphene_core::vector::SplinesFromPointsNode", "graphene_core::vector::SplineNode"),
("graphene_core::vector::generator_nodes::EllipseGenerator", "graphene_core::vector::generator_nodes::EllipseNode"), ("graphene_core::vector::generator_nodes::EllipseGenerator", "graphene_core::vector::generator_nodes::EllipseNode"),
("graphene_core::vector::generator_nodes::LineGenerator", "graphene_core::vector::generator_nodes::LineNode"), ("graphene_core::vector::generator_nodes::LineGenerator", "graphene_core::vector::generator_nodes::LineNode"),
@@ -71,6 +147,10 @@ const REPLACEMENTS: &[(&str, &str)] = &[
("graphene_std::raster::MaskImageNode", "graphene_std::raster::MaskNode"), ("graphene_std::raster::MaskImageNode", "graphene_std::raster::MaskNode"),
("graphene_core::vector::FlattenVectorElementsNode", "graphene_core::vector::FlattenPathNode"), ("graphene_core::vector::FlattenVectorElementsNode", "graphene_core::vector::FlattenPathNode"),
("graphene_std::vector::BooleanOperationNode", "graphene_path_bool::BooleanOperationNode"), ("graphene_std::vector::BooleanOperationNode", "graphene_path_bool::BooleanOperationNode"),
// brush
("graphene_std::brush::BrushStampGeneratorNode", "graphene_brush::brush::BrushStampGeneratorNode"),
("graphene_std::brush::BlitNode", "graphene_brush::brush::BlitNode"),
("graphene_std::brush::BrushNode", "graphene_brush::brush::BrushNode"),
]; ];
pub fn document_migration_string_preprocessing(document_serialized_content: String) -> String { pub fn document_migration_string_preprocessing(document_serialized_content: String) -> String {
@@ -263,8 +343,8 @@ pub fn document_migration_upgrades(document: &mut DocumentMessageHandler, reset_
let node_definition = resolve_document_node_type(reference).unwrap(); let node_definition = resolve_document_node_type(reference).unwrap();
let document_node = node_definition.default_node_template().document_node; let document_node = node_definition.default_node_template().document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone()); document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
document.network_interface.insert_input_properties_row(node_id, 8, network_path); document.network_interface.insert_input_properties_row(node_id, 8, network_path, ("", "TODO").into());
document.network_interface.insert_input_properties_row(node_id, 9, network_path); document.network_interface.insert_input_properties_row(node_id, 9, network_path, ("", "TODO").into());
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path); let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
let align_input = NodeInput::value(TaggedValue::StrokeAlign(StrokeAlign::Center), false); let align_input = NodeInput::value(TaggedValue::StrokeAlign(StrokeAlign::Center), false);
@@ -357,7 +437,7 @@ pub fn document_migration_upgrades(document: &mut DocumentMessageHandler, reset_
} }
// Upgrade Text node to include line height and character spacing, which were previously hardcoded to 1, from https://github.com/GraphiteEditor/Graphite/pull/2016 // Upgrade Text node to include line height and character spacing, which were previously hardcoded to 1, from https://github.com/GraphiteEditor/Graphite/pull/2016
if reference == "Text" && inputs_count != 8 { if reference == "Text" && inputs_count != 9 {
let node_definition = resolve_document_node_type(reference).unwrap(); let node_definition = resolve_document_node_type(reference).unwrap();
let document_node = node_definition.default_node_template().document_node; let document_node = node_definition.default_node_template().document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone()); document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
@@ -388,12 +468,44 @@ pub fn document_migration_upgrades(document: &mut DocumentMessageHandler, reset_
); );
document.network_interface.set_input( document.network_interface.set_input(
&InputConnector::node(*node_id, 6), &InputConnector::node(*node_id, 6),
NodeInput::value(TaggedValue::OptionalF64(TypesettingConfig::default().max_width), false), if inputs_count >= 7 {
old_inputs[6].clone()
} else {
NodeInput::value(TaggedValue::OptionalF64(TypesettingConfig::default().max_width), false)
},
network_path, network_path,
); );
document.network_interface.set_input( document.network_interface.set_input(
&InputConnector::node(*node_id, 7), &InputConnector::node(*node_id, 7),
NodeInput::value(TaggedValue::OptionalF64(TypesettingConfig::default().max_height), false), if inputs_count >= 8 {
old_inputs[7].clone()
} else {
NodeInput::value(TaggedValue::OptionalF64(TypesettingConfig::default().max_height), false)
},
network_path,
);
document.network_interface.insert_input_properties_row(
node_id,
9,
network_path,
PropertiesRow::with_override(
"Tilt",
"Faux italic",
WidgetOverride::Number(NumberInputSettings {
min: Some(-85.),
max: Some(85.),
unit: Some("°".to_string()),
..Default::default()
}),
),
);
document.network_interface.set_input(
&InputConnector::node(*node_id, 8),
if inputs_count >= 9 {
old_inputs[8].clone()
} else {
NodeInput::value(TaggedValue::F64(TypesettingConfig::default().tilt), false)
},
network_path, network_path,
); );
} }
@@ -666,17 +778,62 @@ pub fn document_migration_upgrades(document: &mut DocumentMessageHandler, reset_
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path); let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
let new_spacing_value = NodeInput::value(TaggedValue::PointSpacingType(graphene_std::vector::misc::PointSpacingType::Separation), false); let new_spacing_value = NodeInput::value(TaggedValue::PointSpacingType(graphene_std::vector::misc::PointSpacingType::Separation), false);
let new_quantity_value = NodeInput::value(TaggedValue::U32(100), false);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 1), new_spacing_value, network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 1), new_spacing_value, network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[1].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[1].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 3), old_inputs[1].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 3), new_quantity_value, network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 4), old_inputs[2].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 4), old_inputs[2].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 5), old_inputs[3].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 5), old_inputs[3].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 6), old_inputs[4].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 6), old_inputs[4].clone(), network_path);
document.network_interface.replace_reference_name(node_id, network_path, "Sample Polyline".to_string()); document.network_interface.replace_reference_name(node_id, network_path, "Sample Polyline".to_string());
} }
// Make the "Quantity" parameter a u32 instead of f64
if reference == "Sample Polyline" {
let node_definition = resolve_document_node_type("Sample Polyline").unwrap();
let new_node_template = node_definition.default_node_template();
let document_node = new_node_template.document_node;
// Get the inputs, obtain the quantity value, and put the inputs back
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
let quantity_value = old_inputs.get(3).cloned();
let _ = document.network_interface.replace_inputs(node_id, old_inputs, network_path);
if let Some(NodeInput::Value { tagged_value, exposed }) = quantity_value {
if let TaggedValue::F64(value) = *tagged_value {
let new_quantity_value = NodeInput::value(TaggedValue::U32(value as u32), exposed);
document.network_interface.set_input(&InputConnector::node(*node_id, 3), new_quantity_value, network_path);
}
}
}
// Make the "Grid" node, if its input of index 3 is a DVec2 for "angles" instead of a u32 for the "columns" input that now succeeds "angles", move the angle to index 5 (after "columns" and "rows")
if reference == "Grid" && inputs_count == 6 {
let node_definition = resolve_document_node_type(reference).unwrap();
let new_node_template = node_definition.default_node_template();
let document_node = new_node_template.document_node;
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
let index_3_value = old_inputs.get(3).cloned();
if let Some(NodeInput::Value { tagged_value, exposed: _ }) = index_3_value {
if matches!(*tagged_value, TaggedValue::DVec2(_)) {
// Move index 3 to the end
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 3), old_inputs[4].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 4), old_inputs[5].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 5), old_inputs[3].clone(), network_path);
} else {
// Swap it back if we're not changing anything
let _ = document.network_interface.replace_inputs(node_id, old_inputs, network_path);
}
}
}
} }
// Ensure layers are positioned as stacks if they are upstream siblings of another layer // Ensure layers are positioned as stacks if they are upstream siblings of another layer
@@ -924,11 +924,11 @@ impl PortfolioMessageHandler {
let result = self.executor.poll_node_graph_evaluation(active_document, responses); let result = self.executor.poll_node_graph_evaluation(active_document, responses);
if result.is_err() { if result.is_err() {
let error = r#" let error = r#"
<rect x="50%" y="50%" width="480" height="100" transform="translate(-240 -50)" rx="4" fill="var(--color-error-red)" /> <rect x="50%" y="50%" width="460" height="100" transform="translate(-230 -50)" rx="4" fill="var(--color-warning-yellow)" />
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-size="18" fill="var(--color-2-mildblack)"> <text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-size="18" fill="var(--color-2-mildblack)">
<tspan x="50%" dy="-24" font-weight="bold">The document cannot be rendered in its current state.</tspan> <tspan x="50%" dy="-24" font-weight="bold">The document cannot render in its current state.</tspan>
<tspan x="50%" dy="24">Check for error details in the node graph, which can be</tspan> <tspan x="50%" dy="24">Undo to go back, if available, or check for error details</tspan>
<tspan x="50%" dy="24">opened with the viewport's top right <tspan font-style="italic">Node Graph</tspan> button.</tspan> <tspan x="50%" dy="24">by clicking the <tspan font-style="italic">Node Graph</tspan> button up at the top right.</tspan>
/text>"# /text>"#
// It's a mystery why the `/text>` tag above needs to be missing its `<`, but when it exists it prints the `<` character in the text. However this works with it removed. // It's a mystery why the `/text>` tag above needs to be missing its `<`, but when it exists it prints the `<` character in the text. However this works with it removed.
.to_string(); .to_string();
@@ -361,6 +361,7 @@ pub fn get_text(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInter
let Some(&TaggedValue::F64(character_spacing)) = inputs[5].as_value() else { return None }; let Some(&TaggedValue::F64(character_spacing)) = inputs[5].as_value() else { return None };
let Some(&TaggedValue::OptionalF64(max_width)) = inputs[6].as_value() else { return None }; let Some(&TaggedValue::OptionalF64(max_width)) = inputs[6].as_value() else { return None };
let Some(&TaggedValue::OptionalF64(max_height)) = inputs[7].as_value() else { return None }; let Some(&TaggedValue::OptionalF64(max_height)) = inputs[7].as_value() else { return None };
let Some(&TaggedValue::F64(tilt)) = inputs[8].as_value() else { return None };
let typesetting = TypesettingConfig { let typesetting = TypesettingConfig {
font_size, font_size,
@@ -368,6 +369,7 @@ pub fn get_text(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInter
max_width, max_width,
character_spacing, character_spacing,
max_height, max_height,
tilt,
}; };
Some((text, font, typesetting)) Some((text, font, typesetting))
} }
@@ -1,14 +1,15 @@
use super::graph_modification_utils::merge_layers; use super::graph_modification_utils::merge_layers;
use super::snapping::{SnapCache, SnapCandidatePoint, SnapData, SnapManager, SnappedPoint}; use super::snapping::{SnapCache, SnapCandidatePoint, SnapData, SnapManager, SnappedPoint};
use super::utility_functions::{adjust_handle_colinearity, calculate_segment_angle, restore_g1_continuity, restore_previous_handle_position}; use super::utility_functions::{adjust_handle_colinearity, calculate_bezier_bbox, calculate_segment_angle, restore_g1_continuity, restore_previous_handle_position};
use crate::consts::HANDLE_LENGTH_FACTOR; use crate::consts::HANDLE_LENGTH_FACTOR;
use crate::messages::portfolio::document::overlays::utility_functions::selected_segments; use crate::messages::portfolio::document::overlays::utility_functions::selected_segments;
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier}; use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
use crate::messages::portfolio::document::utility_types::misc::{PathSnapSource, SnapSource}; use crate::messages::portfolio::document::utility_types::misc::{PathSnapSource, SnapSource};
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface; use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
use crate::messages::preferences::SelectionMode;
use crate::messages::prelude::*; use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::snapping::SnapTypeConfiguration; use crate::messages::tool::common_functionality::snapping::SnapTypeConfiguration;
use crate::messages::tool::common_functionality::utility_functions::{find_refit_handle_lengths, is_visible_point}; use crate::messages::tool::common_functionality::utility_functions::{find_refit_handle_lengths, is_intersecting, is_visible_point};
use crate::messages::tool::tool_messages::path_tool::{PathOverlayMode, PointSelectState}; use crate::messages::tool::tool_messages::path_tool::{PathOverlayMode, PointSelectState};
use bezier_rs::{Bezier, BezierHandles, Subpath, TValue}; use bezier_rs::{Bezier, BezierHandles, Subpath, TValue};
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
@@ -45,6 +46,7 @@ pub enum ManipulatorAngle {
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct SelectedLayerState { pub struct SelectedLayerState {
selected_points: HashSet<ManipulatorPointId>, selected_points: HashSet<ManipulatorPointId>,
selected_segments: HashSet<SegmentId>,
/// Keeps track of the current state; helps avoid unnecessary computation when called by [`ShapeState`]. /// Keeps track of the current state; helps avoid unnecessary computation when called by [`ShapeState`].
ignore_handles: bool, ignore_handles: bool,
ignore_anchors: bool, ignore_anchors: bool,
@@ -54,11 +56,27 @@ pub struct SelectedLayerState {
} }
impl SelectedLayerState { impl SelectedLayerState {
pub fn selected(&self) -> impl Iterator<Item = ManipulatorPointId> + '_ { pub fn selected_points(&self) -> impl Iterator<Item = ManipulatorPointId> + '_ {
self.selected_points.iter().copied() self.selected_points.iter().copied()
} }
pub fn is_selected(&self, point: ManipulatorPointId) -> bool { pub fn selected_segments(&self) -> impl Iterator<Item = SegmentId> + '_ {
self.selected_segments.iter().copied()
}
pub fn selected_points_count(&self) -> usize {
self.selected_points.len()
}
pub fn selected_segments_count(&self) -> usize {
self.selected_segments.len()
}
pub fn is_segment_selected(&self, segment: SegmentId) -> bool {
self.selected_segments.contains(&segment)
}
pub fn is_point_selected(&self, point: ManipulatorPointId) -> bool {
self.selected_points.contains(&point) self.selected_points.contains(&point)
} }
@@ -66,10 +84,26 @@ impl SelectedLayerState {
self.selected_points.insert(point); self.selected_points.insert(point);
} }
pub fn select_segment(&mut self, segment: SegmentId) {
self.selected_segments.insert(segment);
}
pub fn deselect_point(&mut self, point: ManipulatorPointId) { pub fn deselect_point(&mut self, point: ManipulatorPointId) {
self.selected_points.remove(&point); self.selected_points.remove(&point);
} }
pub fn deselect_segment(&mut self, segment: SegmentId) {
self.selected_segments.remove(&segment);
}
pub fn clear_points(&mut self) {
self.selected_points.clear();
}
pub fn clear_segments(&mut self) {
self.selected_segments.clear();
}
pub fn ignore_handles(&mut self, status: bool) { pub fn ignore_handles(&mut self, status: bool) {
if self.ignore_handles != status { if self.ignore_handles != status {
return; return;
@@ -101,14 +135,6 @@ impl SelectedLayerState {
self.ignored_anchor_points.clear(); self.ignored_anchor_points.clear();
} }
} }
pub fn clear_points(&mut self) {
self.selected_points.clear();
}
pub fn selected_points_count(&self) -> usize {
self.selected_points.len()
}
} }
pub type SelectedShapeState = HashMap<LayerNodeIdentifier, SelectedLayerState>; pub type SelectedShapeState = HashMap<LayerNodeIdentifier, SelectedLayerState>;
@@ -128,6 +154,12 @@ pub struct SelectedPointsInfo {
pub vector_data: VectorData, pub vector_data: VectorData,
} }
#[derive(Debug)]
pub struct SelectedSegmentsInfo {
pub segments: Vec<SegmentId>,
pub vector_data: VectorData,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ManipulatorPointInfo { pub struct ManipulatorPointInfo {
pub layer: LayerNodeIdentifier, pub layer: LayerNodeIdentifier,
@@ -136,6 +168,7 @@ pub struct ManipulatorPointInfo {
pub type OpposingHandleLengths = HashMap<LayerNodeIdentifier, HashMap<HandleId, f64>>; pub type OpposingHandleLengths = HashMap<LayerNodeIdentifier, HashMap<HandleId, f64>>;
#[derive(Clone)]
pub struct ClosestSegment { pub struct ClosestSegment {
layer: LayerNodeIdentifier, layer: LayerNodeIdentifier,
segment: SegmentId, segment: SegmentId,
@@ -159,6 +192,10 @@ impl ClosestSegment {
self.points self.points
} }
pub fn bezier(&self) -> Bezier {
self.bezier
}
pub fn closest_point_document(&self) -> DVec2 { pub fn closest_point_document(&self) -> DVec2 {
self.bezier.evaluate(TValue::Parametric(self.t)) self.bezier.evaluate(TValue::Parametric(self.t))
} }
@@ -473,7 +510,7 @@ impl ShapeState {
if let Some(id) = selected.as_anchor() { if let Some(id) = selected.as_anchor() {
for neighbor in vector_data.connected_points(id) { for neighbor in vector_data.connected_points(id) {
if state.is_selected(ManipulatorPointId::Anchor(neighbor)) { if state.is_point_selected(ManipulatorPointId::Anchor(neighbor)) {
continue; continue;
} }
let Some(position) = vector_data.point_domain.position_from_id(neighbor) else { continue }; let Some(position) = vector_data.point_domain.position_from_id(neighbor) else { continue };
@@ -512,38 +549,30 @@ impl ShapeState {
let point_position = manipulator_point_id.get_position(&vector_data)?; let point_position = manipulator_point_id.get_position(&vector_data)?;
let selected_shape_state = self.selected_shape_state.get(&layer)?; let selected_shape_state = self.selected_shape_state.get(&layer)?;
let already_selected = selected_shape_state.is_selected(manipulator_point_id); let already_selected = selected_shape_state.is_point_selected(manipulator_point_id);
// Should we select or deselect the point?
let new_selected = if already_selected { !extend_selection } else { true };
// Offset to snap the selected point to the cursor // Offset to snap the selected point to the cursor
let offset = mouse_position - network_interface.document_metadata().transform_to_viewport(layer).transform_point2(point_position); let offset = mouse_position - network_interface.document_metadata().transform_to_viewport(layer).transform_point2(point_position);
// This is selecting the manipulator only for now, next to generalize to points // This is selecting the manipulator only for now, next to generalize to points
if new_selected {
let retain_existing_selection = extend_selection || already_selected;
if !retain_existing_selection {
self.deselect_all_points();
}
// Add to the selected points let retain_existing_selection = extend_selection || already_selected;
let selected_shape_state = self.selected_shape_state.get_mut(&layer)?; if !retain_existing_selection {
selected_shape_state.select_point(manipulator_point_id); self.deselect_all_points();
self.deselect_all_segments();
let points = self
.selected_shape_state
.iter()
.flat_map(|(layer, state)| state.selected_points.iter().map(|&point_id| ManipulatorPointInfo { layer: *layer, point_id }))
.collect();
return Some(Some(SelectedPointsInfo { points, offset, vector_data }));
} else {
let selected_shape_state = self.selected_shape_state.get_mut(&layer)?;
selected_shape_state.deselect_point(manipulator_point_id);
return Some(None);
} }
// Add to the selected points (deselect is managed in DraggingState, DragStop)
let selected_shape_state = self.selected_shape_state.get_mut(&layer)?;
selected_shape_state.select_point(manipulator_point_id);
let points = self
.selected_shape_state
.iter()
.flat_map(|(layer, state)| state.selected_points.iter().map(|&point_id| ManipulatorPointInfo { layer: *layer, point_id }))
.collect();
return Some(Some(SelectedPointsInfo { points, offset, vector_data }));
} }
None None
} }
@@ -555,11 +584,16 @@ impl ShapeState {
select_threshold: f64, select_threshold: f64,
path_overlay_mode: PathOverlayMode, path_overlay_mode: PathOverlayMode,
frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>, frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>,
point_editing_mode: bool,
) -> Option<(bool, Option<SelectedPointsInfo>)> { ) -> Option<(bool, Option<SelectedPointsInfo>)> {
if self.selected_shape_state.is_empty() { if self.selected_shape_state.is_empty() {
return None; return None;
} }
if !point_editing_mode {
return None;
}
if let Some((layer, manipulator_point_id)) = self.find_nearest_point_indices(network_interface, mouse_position, select_threshold) { if let Some((layer, manipulator_point_id)) = self.find_nearest_point_indices(network_interface, mouse_position, select_threshold) {
let vector_data = network_interface.compute_modified_vector(layer)?; let vector_data = network_interface.compute_modified_vector(layer)?;
let point_position = manipulator_point_id.get_position(&vector_data)?; let point_position = manipulator_point_id.get_position(&vector_data)?;
@@ -572,7 +606,7 @@ impl ShapeState {
} }
let selected_shape_state = self.selected_shape_state.get(&layer)?; let selected_shape_state = self.selected_shape_state.get(&layer)?;
let already_selected = selected_shape_state.is_selected(manipulator_point_id); let already_selected = selected_shape_state.is_point_selected(manipulator_point_id);
// Offset to snap the selected point to the cursor // Offset to snap the selected point to the cursor
let offset = mouse_position - network_interface.document_metadata().transform_to_viewport(layer).transform_point2(point_position); let offset = mouse_position - network_interface.document_metadata().transform_to_viewport(layer).transform_point2(point_position);
@@ -630,7 +664,7 @@ impl ShapeState {
// Select all connected points // Select all connected points
while let Some(point) = selected_stack.pop() { while let Some(point) = selected_stack.pop() {
let anchor_point = ManipulatorPointId::Anchor(point); let anchor_point = ManipulatorPointId::Anchor(point);
if !state.is_selected(anchor_point) { if !state.is_point_selected(anchor_point) {
state.select_point(anchor_point); state.select_point(anchor_point);
selected_stack.extend(vector_data.connected_points(point)); selected_stack.extend(vector_data.connected_points(point));
} }
@@ -671,6 +705,13 @@ impl ShapeState {
} }
} }
/// Deselects all segments across every selected layer
pub fn deselect_all_segments(&mut self) {
for state in self.selected_shape_state.values_mut() {
state.selected_segments.clear()
}
}
pub fn update_selected_anchors_status(&mut self, status: bool) { pub fn update_selected_anchors_status(&mut self, status: bool) {
for state in self.selected_shape_state.values_mut() { for state in self.selected_shape_state.values_mut() {
self.ignore_anchors = !status; self.ignore_anchors = !status;
@@ -736,10 +777,18 @@ impl ShapeState {
self.selected_shape_state.values().flat_map(|state| &state.selected_points) self.selected_shape_state.values().flat_map(|state| &state.selected_points)
} }
pub fn selected_segments(&self) -> impl Iterator<Item = &'_ SegmentId> {
self.selected_shape_state.values().flat_map(|state| &state.selected_segments)
}
pub fn selected_points_in_layer(&self, layer: LayerNodeIdentifier) -> Option<&HashSet<ManipulatorPointId>> { pub fn selected_points_in_layer(&self, layer: LayerNodeIdentifier) -> Option<&HashSet<ManipulatorPointId>> {
self.selected_shape_state.get(&layer).map(|state| &state.selected_points) self.selected_shape_state.get(&layer).map(|state| &state.selected_points)
} }
pub fn selected_segments_in_layer(&self, layer: LayerNodeIdentifier) -> Option<&HashSet<SegmentId>> {
self.selected_shape_state.get(&layer).map(|state| &state.selected_segments)
}
pub fn move_primary(&self, segment: SegmentId, delta: DVec2, layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) { pub fn move_primary(&self, segment: SegmentId, delta: DVec2, layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) {
responses.add(GraphOperationMessage::Vector { responses.add(GraphOperationMessage::Vector {
layer, layer,
@@ -766,7 +815,7 @@ impl ShapeState {
let Some((start, _end, bezier)) = vector_data.segment_points_from_id(segment) else { continue }; let Some((start, _end, bezier)) = vector_data.segment_points_from_id(segment) else { continue };
if let BezierHandles::Quadratic { handle } = bezier.handles { if let BezierHandles::Quadratic { handle } = bezier.handles {
if selected.is_some_and(|selected| selected.is_selected(ManipulatorPointId::Anchor(start))) { if selected.is_some_and(|selected| selected.is_point_selected(ManipulatorPointId::Anchor(start))) {
continue; continue;
} }
@@ -850,6 +899,9 @@ impl ShapeState {
let non_zero_handles = handles.iter().filter(|handle| handle.length(vector_data) > 1e-6).count(); let non_zero_handles = handles.iter().filter(|handle| handle.length(vector_data) > 1e-6).count();
let handle_segments = handles.iter().map(|handles| handles.segment).collect::<Vec<_>>(); let handle_segments = handles.iter().map(|handles| handles.segment).collect::<Vec<_>>();
// Check if the anchor is connected to linear segments and has no handles
let linear_segments = vector_data.connected_linear_segments(point_id) != 0;
// Grab the next and previous manipulator groups by simply looking at the next / previous index // Grab the next and previous manipulator groups by simply looking at the next / previous index
let points = handles.iter().map(|handle| vector_data.other_point(handle.segment, point_id)); let points = handles.iter().map(|handle| vector_data.other_point(handle.segment, point_id));
let anchor_positions = points let anchor_positions = points
@@ -891,7 +943,7 @@ impl ShapeState {
handle_direction *= -1.; handle_direction *= -1.;
} }
if non_zero_handles != 0 { if non_zero_handles != 0 && !linear_segments {
let [a, b] = handles.as_slice() else { return }; let [a, b] = handles.as_slice() else { return };
let (non_zero_handle, zero_handle) = if a.length(vector_data) > 1e-6 { (a, b) } else { (b, a) }; let (non_zero_handle, zero_handle) = if a.length(vector_data) > 1e-6 { (a, b) } else { (b, a) };
let Some(direction) = non_zero_handle let Some(direction) = non_zero_handle
@@ -1012,9 +1064,9 @@ impl ShapeState {
} }
} }
/// Move the selected points by dragging the mouse. /// Move the selected points and segments by dragging the mouse.
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn move_selected_points( pub fn move_selected_points_and_segments(
&self, &self,
handle_lengths: Option<OpposingHandleLengths>, handle_lengths: Option<OpposingHandleLengths>,
document: &DocumentMessageHandler, document: &DocumentMessageHandler,
@@ -1040,7 +1092,17 @@ impl ShapeState {
}; };
let delta = delta_transform.inverse().transform_vector2(delta); let delta = delta_transform.inverse().transform_vector2(delta);
for &point in state.selected_points.iter() { // Make a new collection of anchor points which needs to be moved
let mut affected_points = state.selected_points.clone();
for (segment_id, _, start, end) in vector_data.segment_bezier_iter() {
if state.is_segment_selected(segment_id) {
affected_points.insert(ManipulatorPointId::Anchor(start));
affected_points.insert(ManipulatorPointId::Anchor(end));
}
}
for &point in affected_points.iter() {
if self.is_point_ignored(&point) { if self.is_point_ignored(&point) {
continue; continue;
} }
@@ -1055,7 +1117,7 @@ impl ShapeState {
}; };
let Some(anchor_id) = point.get_anchor(&vector_data) else { continue }; let Some(anchor_id) = point.get_anchor(&vector_data) else { continue };
if state.is_selected(ManipulatorPointId::Anchor(anchor_id)) { if state.is_point_selected(ManipulatorPointId::Anchor(anchor_id)) {
continue; continue;
} }
@@ -1074,7 +1136,7 @@ impl ShapeState {
continue; continue;
} }
if state.is_selected(other.to_manipulator_point()) { if state.is_point_selected(other.to_manipulator_point()) {
// If two colinear handles are being dragged at the same time but not the anchor, it is necessary to break the colinear state. // If two colinear handles are being dragged at the same time but not the anchor, it is necessary to break the colinear state.
let handles = [handle, other]; let handles = [handle, other];
let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false }; let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false };
@@ -1125,12 +1187,12 @@ impl ShapeState {
// ii) The anchor is not selected. // ii) The anchor is not selected.
let anchor = handles[0].to_manipulator_point().get_anchor(&vector_data)?; let anchor = handles[0].to_manipulator_point().get_anchor(&vector_data)?;
let anchor_selected = state.is_selected(ManipulatorPointId::Anchor(anchor)); let anchor_selected = state.is_point_selected(ManipulatorPointId::Anchor(anchor));
if anchor_selected { if anchor_selected {
return None; return None;
} }
let handles_selected = handles.map(|handle| state.is_selected(handle.to_manipulator_point())); let handles_selected = handles.map(|handle| state.is_point_selected(handle.to_manipulator_point()));
let other = match handles_selected { let other = match handles_selected {
[true, false] => handles[1], [true, false] => handles[1],
@@ -1212,11 +1274,15 @@ impl ShapeState {
continue; continue;
}; };
let selected_segments = &state.selected_segments;
for point in std::mem::take(&mut state.selected_points) { for point in std::mem::take(&mut state.selected_points) {
match point { match point {
ManipulatorPointId::Anchor(anchor) => { ManipulatorPointId::Anchor(anchor) => {
if let Some(handles) = Self::dissolve_anchor(anchor, responses, layer, &vector_data) { if let Some(handles) = Self::dissolve_anchor(anchor, responses, layer, &vector_data) {
missing_anchors.insert(anchor, handles); if !vector_data.all_connected(anchor).any(|a| selected_segments.contains(&a.segment)) {
missing_anchors.insert(anchor, handles);
}
} }
deleted_anchors.insert(anchor); deleted_anchors.insert(anchor);
} }
@@ -1286,6 +1352,8 @@ impl ShapeState {
continue; continue;
} }
// Avoid reconnecting to points which have adjacent segments selected
// Grab the handles from the opposite side of the segment(s) being deleted and make it relative to the anchor // Grab the handles from the opposite side of the segment(s) being deleted and make it relative to the anchor
let [handle_start, handle_end] = [start, end].map(|(handle, _, _)| { let [handle_start, handle_end] = [start, end].map(|(handle, _, _)| {
let handle = handle.opposite(); let handle = handle.opposite();
@@ -1357,6 +1425,20 @@ impl ShapeState {
} }
} }
pub fn delete_selected_segments(&mut self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
for (&layer, state) in &self.selected_shape_state {
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else {
continue;
};
for (segment, _, start, end) in vector_data.segment_bezier_iter() {
if state.selected_segments.contains(&segment) {
self.dissolve_segment(responses, layer, &vector_data, segment, [start, end]);
}
}
}
}
pub fn break_path_at_selected_point(&self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) { pub fn break_path_at_selected_point(&self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
for (&layer, state) in &self.selected_shape_state { for (&layer, state) in &self.selected_shape_state {
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { continue }; let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { continue };
@@ -1746,10 +1828,13 @@ impl ShapeState {
.filter(|&handle| anchor.abs_diff_eq(handle, 1e-5)) .filter(|&handle| anchor.abs_diff_eq(handle, 1e-5))
.count(); .count();
// Check if the anchor is connected to linear segments.
let one_or_more_segment_linear = vector_data.connected_linear_segments(id) != 0;
// Check by comparing the handle positions to the anchor if this manipulator group is a point // Check by comparing the handle positions to the anchor if this manipulator group is a point
for point in self.selected_points() { for point in self.selected_points() {
let Some(point_id) = point.as_anchor() else { continue }; let Some(point_id) = point.as_anchor() else { continue };
if positions != 0 { if positions != 0 || one_or_more_segment_linear {
self.convert_manipulator_handles_to_colinear(&vector_data, point_id, responses, layer); self.convert_manipulator_handles_to_colinear(&vector_data, point_id, responses, layer);
} else { } else {
for handle in vector_data.all_connected(point_id) { for handle in vector_data.all_connected(point_id) {
@@ -1801,6 +1886,7 @@ impl ShapeState {
false false
} }
#[allow(clippy::too_many_arguments)]
pub fn select_all_in_shape( pub fn select_all_in_shape(
&mut self, &mut self,
network_interface: &NodeNetworkInterface, network_interface: &NodeNetworkInterface,
@@ -1808,13 +1894,17 @@ impl ShapeState {
selection_change: SelectionChange, selection_change: SelectionChange,
path_overlay_mode: PathOverlayMode, path_overlay_mode: PathOverlayMode,
frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>, frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>,
select_segments: bool,
// Here, "selection mode" represents touched or enclosed, not to be confused with editing modes
selection_mode: SelectionMode,
) { ) {
let selected_points = self.selected_points().cloned().collect::<HashSet<_>>(); let selected_points = self.selected_points().cloned().collect::<HashSet<_>>();
let selected_segments = selected_segments(network_interface, self); let selected_segments = selected_segments(network_interface, self);
for (&layer, state) in &mut self.selected_shape_state { for (&layer, state) in &mut self.selected_shape_state {
if selection_change == SelectionChange::Clear { if selection_change == SelectionChange::Clear {
state.clear_points() state.clear_points();
state.clear_segments();
} }
let vector_data = network_interface.compute_modified_vector(layer); let vector_data = network_interface.compute_modified_vector(layer);
@@ -1840,7 +1930,46 @@ impl ShapeState {
None None
}; };
// Selection segments
for (id, bezier, _, _) in vector_data.segment_bezier_iter() { for (id, bezier, _, _) in vector_data.segment_bezier_iter() {
if select_segments {
// Select segments if they lie inside the bounding box or lasso polygon
let segment_bbox = calculate_bezier_bbox(bezier);
let bottom_left = transform.transform_point2(segment_bbox[0]);
let top_right = transform.transform_point2(segment_bbox[1]);
let select = match selection_shape {
SelectionShape::Box(quad) => {
let enclosed = quad[0].min(quad[1]).cmple(bottom_left).all() && quad[0].max(quad[1]).cmpge(top_right).all();
match selection_mode {
SelectionMode::Enclosed => enclosed,
_ => {
// Check for intersection with the segment
enclosed || is_intersecting(bezier, quad, transform)
}
}
}
SelectionShape::Lasso(_) => {
let polygon = polygon_subpath.as_ref().expect("If `selection_shape` is a polygon then subpath is constructed beforehand.");
// Sample 10 points on the bezier and check if all or some lie inside the polygon
let points = bezier.compute_lookup_table(Some(10), None);
match selection_mode {
SelectionMode::Enclosed => points.map(|p| transform.transform_point2(p)).all(|p| polygon.contains_point(p)),
_ => points.map(|p| transform.transform_point2(p)).any(|p| polygon.contains_point(p)),
}
}
};
if select {
match selection_change {
SelectionChange::Shrink => state.deselect_segment(id),
_ => state.select_segment(id),
}
}
}
// Selecting handles
for (position, id) in [(bezier.handle_start(), ManipulatorPointId::PrimaryHandle(id)), (bezier.handle_end(), ManipulatorPointId::EndHandle(id))] { for (position, id) in [(bezier.handle_start(), ManipulatorPointId::PrimaryHandle(id)), (bezier.handle_end(), ManipulatorPointId::EndHandle(id))] {
let Some(position) = position else { continue }; let Some(position) = position else { continue };
let transformed_position = transform.transform_point2(position); let transformed_position = transform.transform_point2(position);
@@ -1873,6 +2002,7 @@ impl ShapeState {
} }
} }
// Checking for selection of anchor points
for (&id, &position) in vector_data.point_domain.ids().iter().zip(vector_data.point_domain.positions()) { for (&id, &position) in vector_data.point_domain.ids().iter().zip(vector_data.point_domain.positions()) {
let transformed_position = transform.transform_point2(position); let transformed_position = transform.transform_point2(position);
@@ -8,11 +8,12 @@ use crate::messages::tool::common_functionality::graph_modification_utils::get_t
use crate::messages::tool::common_functionality::transformation_cage::SelectedEdges; use crate::messages::tool::common_functionality::transformation_cage::SelectedEdges;
use crate::messages::tool::tool_messages::path_tool::PathOverlayMode; use crate::messages::tool::tool_messages::path_tool::PathOverlayMode;
use crate::messages::tool::utility_types::ToolType; use crate::messages::tool::utility_types::ToolType;
use bezier_rs::Bezier; use bezier_rs::{Bezier, BezierHandles};
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use graphene_std::renderer::Quad; use graphene_std::renderer::Quad;
use graphene_std::text::{FontCache, load_face}; use graphene_std::text::{FontCache, load_font};
use graphene_std::vector::{HandleExt, HandleId, ManipulatorPointId, PointId, SegmentId, VectorData, VectorModificationType}; use graphene_std::vector::{HandleExt, HandleId, ManipulatorPointId, PointId, SegmentId, VectorData, VectorModificationType};
use kurbo::{CubicBez, Line, ParamCurveExtrema, PathSeg, Point, QuadBez};
/// Determines if a path should be extended. Goal in viewport space. Returns the path and if it is extending from the start, if applicable. /// Determines if a path should be extended. Goal in viewport space. Returns the path and if it is extending from the start, if applicable.
pub fn should_extend( pub fn should_extend(
@@ -69,8 +70,8 @@ pub fn text_bounding_box(layer: LayerNodeIdentifier, document: &DocumentMessageH
return Quad::from_box([DVec2::ZERO, DVec2::ZERO]); return Quad::from_box([DVec2::ZERO, DVec2::ZERO]);
}; };
let buzz_face = font_cache.get(font).map(|data| load_face(data)); let font_data = font_cache.get(font).map(|data| load_font(data));
let far = graphene_std::text::bounding_box(text, buzz_face.as_ref(), typesetting, false); let far = graphene_std::text::bounding_box(text, font_data, typesetting, false);
Quad::from_box([DVec2::ZERO, far]) Quad::from_box([DVec2::ZERO, far])
} }
@@ -204,6 +205,71 @@ pub fn is_visible_point(
} }
} }
/// Function to find the bounding box of bezier (uses method from kurbo)
pub fn calculate_bezier_bbox(bezier: Bezier) -> [DVec2; 2] {
let start = Point::new(bezier.start.x, bezier.start.y);
let end = Point::new(bezier.end.x, bezier.end.y);
let bbox = match bezier.handles {
BezierHandles::Cubic { handle_start, handle_end } => {
let p1 = Point::new(handle_start.x, handle_start.y);
let p2 = Point::new(handle_end.x, handle_end.y);
CubicBez::new(start, p1, p2, end).bounding_box()
}
BezierHandles::Quadratic { handle } => {
let p1 = Point::new(handle.x, handle.y);
QuadBez::new(start, p1, end).bounding_box()
}
BezierHandles::Linear => Line::new(start, end).bounding_box(),
};
[DVec2::new(bbox.x0, bbox.y0), DVec2::new(bbox.x1, bbox.y1)]
}
pub fn is_intersecting(bezier: Bezier, quad: [DVec2; 2], transform: DAffine2) -> bool {
let to_layerspace = transform.inverse();
let quad = [to_layerspace.transform_point2(quad[0]), to_layerspace.transform_point2(quad[1])];
let start = Point::new(bezier.start.x, bezier.start.y);
let end = Point::new(bezier.end.x, bezier.end.y);
let segment = match bezier.handles {
BezierHandles::Cubic { handle_start, handle_end } => {
let p1 = Point::new(handle_start.x, handle_start.y);
let p2 = Point::new(handle_end.x, handle_end.y);
PathSeg::Cubic(CubicBez::new(start, p1, p2, end))
}
BezierHandles::Quadratic { handle } => {
let p1 = Point::new(handle.x, handle.y);
PathSeg::Quad(QuadBez::new(start, p1, end))
}
BezierHandles::Linear => PathSeg::Line(Line::new(start, end)),
};
// Create a list of all the sides
let sides = [
Line::new((quad[0].x, quad[0].y), (quad[1].x, quad[0].y)),
Line::new((quad[0].x, quad[0].y), (quad[0].x, quad[1].y)),
Line::new((quad[1].x, quad[1].y), (quad[1].x, quad[0].y)),
Line::new((quad[1].x, quad[1].y), (quad[0].x, quad[1].y)),
];
let mut is_intersecting = false;
for line in sides {
let intersections = segment.intersect_line(line);
let mut intersects = false;
for intersection in intersections {
if intersection.line_t <= 1. && intersection.line_t >= 0. && intersection.segment_t <= 1. && intersection.segment_t >= 0. {
// There is a valid intersection point
intersects = true;
break;
}
}
if intersects {
is_intersecting = true;
break;
}
}
is_intersecting
}
#[allow(clippy::too_many_arguments)]
pub fn resize_bounds( pub fn resize_bounds(
document: &DocumentMessageHandler, document: &DocumentMessageHandler,
responses: &mut VecDeque<Message>, responses: &mut VecDeque<Message>,
@@ -221,7 +287,7 @@ pub fn resize_bounds(
let snap = Some(SizeSnapData { let snap = Some(SizeSnapData {
manager: snap_manager, manager: snap_manager,
points: snap_candidates, points: snap_candidates,
snap_data: SnapData::ignore(document, input, &dragging_layers), snap_data: SnapData::ignore(document, input, dragging_layers),
}); });
let (position, size) = movement.new_size(input.mouse.position, bounds.original_bound_transform, center, constrain, snap); let (position, size) = movement.new_size(input.mouse.position, bounds.original_bound_transform, center, constrain, snap);
let (delta, mut pivot) = movement.bounds_to_scale_transform(position, size); let (delta, mut pivot) = movement.bounds_to_scale_transform(position, size);
@@ -238,11 +304,12 @@ pub fn resize_bounds(
} }
}); });
let mut selected = Selected::new(&mut bounds.original_transforms, &mut pivot, &dragging_layers, responses, &document.network_interface, None, &tool, None); let mut selected = Selected::new(&mut bounds.original_transforms, &mut pivot, dragging_layers, responses, &document.network_interface, None, &tool, None);
selected.apply_transformation(bounds.original_bound_transform * transformation * bounds.original_bound_transform.inverse(), None); selected.apply_transformation(bounds.original_bound_transform * transformation * bounds.original_bound_transform.inverse(), None);
} }
} }
#[allow(clippy::too_many_arguments)]
pub fn rotate_bounds( pub fn rotate_bounds(
document: &DocumentMessageHandler, document: &DocumentMessageHandler,
responses: &mut VecDeque<Message>, responses: &mut VecDeque<Message>,
@@ -280,7 +347,7 @@ pub fn rotate_bounds(
let mut selected = Selected::new( let mut selected = Selected::new(
&mut bounds.original_transforms, &mut bounds.original_transforms,
&mut bounds.center_of_transformation, &mut bounds.center_of_transformation,
&dragging_layers, dragging_layers,
responses, responses,
&document.network_interface, &document.network_interface,
None, None,
@@ -313,7 +380,7 @@ pub fn skew_bounds(
} }
}); });
let mut selected = Selected::new(&mut bounds.original_transforms, &mut pivot, &layers, responses, &document.network_interface, None, &tool, None); let mut selected = Selected::new(&mut bounds.original_transforms, &mut pivot, layers, responses, &document.network_interface, None, &tool, None);
selected.apply_transformation(bounds.original_bound_transform * transformation * bounds.original_bound_transform.inverse(), None); selected.apply_transformation(bounds.original_bound_transform * transformation * bounds.original_bound_transform.inverse(), None);
} }
} }
@@ -365,7 +432,7 @@ pub fn transforming_transform_cage(
let mut selected = Selected::new( let mut selected = Selected::new(
&mut bounds.original_transforms, &mut bounds.original_transforms,
&mut bounds.center_of_transformation, &mut bounds.center_of_transformation,
&layers_dragging, layers_dragging,
responses, responses,
&document.network_interface, &document.network_interface,
None, None,
@@ -423,7 +490,7 @@ pub fn transforming_transform_cage(
} }
// No resize, rotate, or skew // No resize, rotate, or skew
return (false, false, false); (false, false, false)
} }
/// Calculates similarity metric between new bezier curve and two old beziers by using sampled points. /// Calculates similarity metric between new bezier curve and two old beziers by using sampled points.
@@ -8,8 +8,8 @@ use crate::messages::tool::common_functionality::color_selector::{ToolColorOptio
use graph_craft::document::NodeId; use graph_craft::document::NodeId;
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
use graphene_std::Color; use graphene_std::Color;
use graphene_std::brush::brush_stroke::{BrushInputSample, BrushStroke, BrushStyle};
use graphene_std::raster::BlendMode; use graphene_std::raster::BlendMode;
use graphene_std::vector::brush_stroke::{BrushInputSample, BrushStroke, BrushStyle};
const BRUSH_MAX_SIZE: f64 = 5000.; const BRUSH_MAX_SIZE: f64 = 5000.;
@@ -32,6 +32,7 @@ pub struct PathTool {
#[derive(Default)] #[derive(Default)]
pub struct PathToolOptions { pub struct PathToolOptions {
path_overlay_mode: PathOverlayMode, path_overlay_mode: PathOverlayMode,
path_editing_mode: PathEditingMode,
} }
#[impl_message(Message, ToolMessage, Path)] #[impl_message(Message, ToolMessage, Path)]
@@ -70,6 +71,7 @@ pub enum PathToolMessage {
lasso_select: Key, lasso_select: Key,
handle_drag_from_anchor: Key, handle_drag_from_anchor: Key,
drag_restore_handle: Key, drag_restore_handle: Key,
molding_in_segment_edit: Key,
}, },
NudgeSelectedPoints { NudgeSelectedPoints {
delta_x: f64, delta_x: f64,
@@ -117,9 +119,26 @@ pub enum PathOverlayMode {
FrontierHandles = 2, FrontierHandles = 2,
} }
#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)]
pub struct PathEditingMode {
point_editing_mode: bool,
segment_editing_mode: bool,
}
impl Default for PathEditingMode {
fn default() -> Self {
Self {
point_editing_mode: true,
segment_editing_mode: false,
}
}
}
#[derive(PartialEq, Eq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)] #[derive(PartialEq, Eq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum PathOptionsUpdate { pub enum PathOptionsUpdate {
OverlayModeType(PathOverlayMode), OverlayModeType(PathOverlayMode),
PointEditingMode { enabled: bool },
SegmentEditingMode { enabled: bool },
} }
impl ToolMetadata for PathTool { impl ToolMetadata for PathTool {
@@ -204,6 +223,19 @@ impl LayoutHolder for PathTool {
.for_checkbox(&mut checkbox_id) .for_checkbox(&mut checkbox_id)
.widget_holder(); .widget_holder();
let point_editing_mode = CheckboxInput::new(self.options.path_editing_mode.point_editing_mode)
// TODO(Keavon): Replace with a real icon
.icon("Dot")
.tooltip("Point Editing Mode")
.on_update(|input| PathToolMessage::UpdateOptions(PathOptionsUpdate::PointEditingMode { enabled: input.checked }).into())
.widget_holder();
let segment_editing_mode = CheckboxInput::new(self.options.path_editing_mode.segment_editing_mode)
// TODO(Keavon): Replace with a real icon
.icon("Remove")
.tooltip("Segment Editing Mode")
.on_update(|input| PathToolMessage::UpdateOptions(PathOptionsUpdate::SegmentEditingMode { enabled: input.checked }).into())
.widget_holder();
let path_overlay_mode_widget = RadioInput::new(vec![ let path_overlay_mode_widget = RadioInput::new(vec![
RadioEntryData::new("all") RadioEntryData::new("all")
.icon("HandleVisibilityAll") .icon("HandleVisibilityAll")
@@ -228,8 +260,12 @@ impl LayoutHolder for PathTool {
y_location, y_location,
unrelated_seperator.clone(), unrelated_seperator.clone(),
colinear_handle_checkbox, colinear_handle_checkbox,
related_seperator, related_seperator.clone(),
colinear_handles_label, colinear_handles_label,
unrelated_seperator.clone(),
point_editing_mode,
related_seperator.clone(),
segment_editing_mode,
unrelated_seperator, unrelated_seperator,
path_overlay_mode_widget, path_overlay_mode_widget,
], ],
@@ -247,6 +283,14 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
self.options.path_overlay_mode = overlay_mode_type; self.options.path_overlay_mode = overlay_mode_type;
responses.add(OverlaysMessage::Draw); responses.add(OverlaysMessage::Draw);
} }
PathOptionsUpdate::PointEditingMode { enabled } => {
self.options.path_editing_mode.point_editing_mode = enabled;
responses.add(OverlaysMessage::Draw);
}
PathOptionsUpdate::SegmentEditingMode { enabled } => {
self.options.path_editing_mode.segment_editing_mode = enabled;
responses.add(OverlaysMessage::Draw);
}
}, },
ToolMessage::Path(PathToolMessage::ClosePath) => { ToolMessage::Path(PathToolMessage::ClosePath) => {
responses.add(DocumentMessage::AddTransaction); responses.add(DocumentMessage::AddTransaction);
@@ -409,6 +453,7 @@ struct PathToolData {
angle: f64, angle: f64,
opposite_handle_position: Option<DVec2>, opposite_handle_position: Option<DVec2>,
last_clicked_point_was_selected: bool, last_clicked_point_was_selected: bool,
last_clicked_segment_was_selected: bool,
snapping_axis: Option<Axis>, snapping_axis: Option<Axis>,
alt_clicked_on_anchor: bool, alt_clicked_on_anchor: bool,
alt_dragging_from_anchor: bool, alt_dragging_from_anchor: bool,
@@ -420,6 +465,7 @@ struct PathToolData {
frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>, frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>,
adjacent_anchor_offset: Option<DVec2>, adjacent_anchor_offset: Option<DVec2>,
sliding_point_info: Option<SlidingPointInfo>, sliding_point_info: Option<SlidingPointInfo>,
started_drawing_from_inside: bool,
} }
impl PathToolData { impl PathToolData {
@@ -480,6 +526,7 @@ impl PathToolData {
self.selection_status = selection_status; self.selection_status = selection_status;
} }
// TODO: This function is for basic point select mode. We definitely need to make a new one for the segment select mode.
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn mouse_down( fn mouse_down(
&mut self, &mut self,
@@ -491,7 +538,10 @@ impl PathToolData {
lasso_select: bool, lasso_select: bool,
handle_drag_from_anchor: bool, handle_drag_from_anchor: bool,
drag_zero_handle: bool, drag_zero_handle: bool,
molding_in_segment_edit: bool,
path_overlay_mode: PathOverlayMode, path_overlay_mode: PathOverlayMode,
segment_editing_mode: bool,
point_editing_mode: bool,
) -> PathToolFsmState { ) -> PathToolFsmState {
self.double_click_handled = false; self.double_click_handled = false;
self.opposing_handle_lengths = None; self.opposing_handle_lengths = None;
@@ -514,6 +564,7 @@ impl PathToolData {
SELECTION_THRESHOLD, SELECTION_THRESHOLD,
path_overlay_mode, path_overlay_mode,
self.frontier_handles_info.clone(), self.frontier_handles_info.clone(),
point_editing_mode,
) { ) {
responses.add(DocumentMessage::StartTransaction); responses.add(DocumentMessage::StartTransaction);
@@ -597,25 +648,51 @@ impl PathToolData {
} }
PathToolFsmState::Dragging(self.dragging_state) PathToolFsmState::Dragging(self.dragging_state)
} }
// We didn't find a point nearby, so we will see if there is a segment to insert a point on // We didn't find a point nearby, so we will see if there is a segment to select or insert a point on
else if let Some(closed_segment) = &mut self.segment { else if let Some(segment) = shape_editor.upper_closest_segment(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD) {
responses.add(DocumentMessage::StartTransaction); responses.add(DocumentMessage::StartTransaction);
// Calculating and storing handle positions if segment_editing_mode && !molding_in_segment_edit {
let handle1 = ManipulatorPointId::PrimaryHandle(closed_segment.segment()); let layer = segment.layer();
let handle2 = ManipulatorPointId::EndHandle(closed_segment.segment()); let segment_id = segment.segment();
let already_selected = shape_editor.selected_shape_state.get(&layer).is_some_and(|state| state.is_segment_selected(segment_id));
self.last_clicked_segment_was_selected = already_selected;
if let Some(vector_data) = document.network_interface.compute_modified_vector(closed_segment.layer()) { if !(already_selected && extend_selection) {
if let (Some(pos1), Some(pos2)) = (handle1.get_position(&vector_data), handle2.get_position(&vector_data)) { let retain_existing_selection = extend_selection || already_selected;
self.molding_info = Some((pos1, pos2)) if !retain_existing_selection {
shape_editor.deselect_all_segments();
shape_editor.deselect_all_points();
}
// Add to selected segments
if let Some(selected_shape_state) = shape_editor.selected_shape_state.get_mut(&layer) {
selected_shape_state.select_segment(segment_id);
}
} }
}
PathToolFsmState::MoldingSegment self.drag_start_pos = input.mouse.position;
let viewport_to_document = document.metadata().document_to_viewport.inverse();
self.previous_mouse_position = viewport_to_document.transform_point2(input.mouse.position);
responses.add(OverlaysMessage::Draw);
PathToolFsmState::Dragging(self.dragging_state)
} else {
let handle1 = ManipulatorPointId::PrimaryHandle(segment.segment());
let handle2 = ManipulatorPointId::EndHandle(segment.segment());
if let Some(vector_data) = document.network_interface.compute_modified_vector(segment.layer()) {
if let (Some(pos1), Some(pos2)) = (handle1.get_position(&vector_data), handle2.get_position(&vector_data)) {
self.molding_info = Some((pos1, pos2))
}
}
PathToolFsmState::MoldingSegment
}
} }
// We didn't find a segment, so consider selecting the nearest shape instead // We didn't find a segment, so consider selecting the nearest shape instead and start drawing
else if let Some(layer) = document.click(input) { else if let Some(layer) = document.click(input) {
shape_editor.deselect_all_points(); shape_editor.deselect_all_points();
shape_editor.deselect_all_segments();
if extend_selection { if extend_selection {
responses.add(NodeGraphMessage::SelectedNodesAdd { nodes: vec![layer.to_node()] }); responses.add(NodeGraphMessage::SelectedNodesAdd { nodes: vec![layer.to_node()] });
} else { } else {
@@ -624,9 +701,10 @@ impl PathToolData {
self.drag_start_pos = input.mouse.position; self.drag_start_pos = input.mouse.position;
self.previous_mouse_position = document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position); self.previous_mouse_position = document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position);
responses.add(DocumentMessage::StartTransaction); self.started_drawing_from_inside = true;
PathToolFsmState::Dragging(self.dragging_state) let selection_shape = if lasso_select { SelectionShapeType::Lasso } else { SelectionShapeType::Box };
PathToolFsmState::Drawing { selection_shape }
} }
// Start drawing // Start drawing
else { else {
@@ -648,7 +726,7 @@ impl PathToolData {
let transform = document.metadata().transform_to_document(layer); let transform = document.metadata().transform_to_document(layer);
let mut layer_manipulators = HashSet::with_hasher(NoHashBuilder); let mut layer_manipulators = HashSet::with_hasher(NoHashBuilder);
for point in state.selected() { for point in state.selected_points() {
let Some(anchor) = point.get_anchor(&vector_data) else { continue }; let Some(anchor) = point.get_anchor(&vector_data) else { continue };
layer_manipulators.insert(anchor); layer_manipulators.insert(anchor);
let Some([handle1, handle2]) = point.get_handle_pair(&vector_data) else { continue }; let Some([handle1, handle2]) = point.get_handle_pair(&vector_data) else { continue };
@@ -752,7 +830,7 @@ impl PathToolData {
} }
// Only count selected handles // Only count selected handles
let selected_handle = selection.selected().next()?.as_handle()?; let selected_handle = selection.selected_points().next()?.as_handle()?;
let handle_id = selected_handle.to_manipulator_point(); let handle_id = selected_handle.to_manipulator_point();
let layer_to_document = document.metadata().transform_to_document(*layer); let layer_to_document = document.metadata().transform_to_document(*layer);
@@ -879,7 +957,7 @@ impl PathToolData {
let drag_start = self.drag_start_pos; let drag_start = self.drag_start_pos;
let opposite_delta = drag_start - current_mouse; let opposite_delta = drag_start - current_mouse;
shape_editor.move_selected_points(None, document, opposite_delta, false, true, false, None, false, responses); shape_editor.move_selected_points_and_segments(None, document, opposite_delta, false, true, false, None, false, responses);
// Calculate the projected delta and shift the points along that delta // Calculate the projected delta and shift the points along that delta
let delta = current_mouse - drag_start; let delta = current_mouse - drag_start;
@@ -891,7 +969,7 @@ impl PathToolData {
_ => DVec2::new(delta.x, 0.), _ => DVec2::new(delta.x, 0.),
}; };
shape_editor.move_selected_points(None, document, projected_delta, false, true, false, None, false, responses); shape_editor.move_selected_points_and_segments(None, document, projected_delta, false, true, false, None, false, responses);
} }
fn stop_snap_along_axis(&mut self, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) { fn stop_snap_along_axis(&mut self, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
@@ -907,12 +985,12 @@ impl PathToolData {
_ => DVec2::new(opposite_delta.x, 0.), _ => DVec2::new(opposite_delta.x, 0.),
}; };
shape_editor.move_selected_points(None, document, opposite_projected_delta, false, true, false, None, false, responses); shape_editor.move_selected_points_and_segments(None, document, opposite_projected_delta, false, true, false, None, false, responses);
// Calculate what actually would have been the original delta for the point, and apply that // Calculate what actually would have been the original delta for the point, and apply that
let delta = current_mouse - drag_start; let delta = current_mouse - drag_start;
shape_editor.move_selected_points(None, document, delta, false, true, false, None, false, responses); shape_editor.move_selected_points_and_segments(None, document, delta, false, true, false, None, false, responses);
self.snapping_axis = None; self.snapping_axis = None;
} }
@@ -934,6 +1012,28 @@ impl PathToolData {
tangent_vector.try_normalize() tangent_vector.try_normalize()
} }
fn update_closest_segment(&mut self, shape_editor: &mut ShapeState, position: DVec2, document: &DocumentMessageHandler, path_overlay_mode: PathOverlayMode) {
// Check if there is no point nearby
if shape_editor
.find_nearest_visible_point_indices(&document.network_interface, position, SELECTION_THRESHOLD, path_overlay_mode, self.frontier_handles_info.clone())
.is_some()
{
self.segment = None;
}
// If already hovering on a segment, then recalculate its closest point
else if let Some(closest_segment) = &mut self.segment {
closest_segment.update_closest_point(document.metadata(), position);
if closest_segment.too_far(position, SEGMENT_INSERTION_DISTANCE) {
self.segment = None;
}
}
// If not, check that if there is some closest segment or not
else if let Some(closest_segment) = shape_editor.upper_closest_segment(&document.network_interface, position, SEGMENT_INSERTION_DISTANCE) {
self.segment = Some(closest_segment);
}
}
fn start_sliding_point(&mut self, shape_editor: &mut ShapeState, document: &DocumentMessageHandler) -> bool { fn start_sliding_point(&mut self, shape_editor: &mut ShapeState, document: &DocumentMessageHandler) -> bool {
let single_anchor_selected = shape_editor.selected_points().count() == 1 && shape_editor.selected_points().any(|point| matches!(point, ManipulatorPointId::Anchor(_))); let single_anchor_selected = shape_editor.selected_points().count() == 1 && shape_editor.selected_points().any(|point| matches!(point, ManipulatorPointId::Anchor(_)));
@@ -1196,7 +1296,7 @@ impl PathToolData {
self.temporary_colinear_handles = false; self.temporary_colinear_handles = false;
skip_opposite = true; skip_opposite = true;
} }
shape_editor.move_selected_points(handle_lengths, document, snapped_delta, equidistant, true, was_alt_dragging, opposite, skip_opposite, responses); shape_editor.move_selected_points_and_segments(handle_lengths, document, snapped_delta, equidistant, true, was_alt_dragging, opposite, skip_opposite, responses);
self.previous_mouse_position += document_to_viewport.inverse().transform_vector2(snapped_delta); self.previous_mouse_position += document_to_viewport.inverse().transform_vector2(snapped_delta);
} else { } else {
let Some(axis) = self.snapping_axis else { return }; let Some(axis) = self.snapping_axis else { return };
@@ -1205,7 +1305,7 @@ impl PathToolData {
Axis::Y => DVec2::new(0., unsnapped_delta.y), Axis::Y => DVec2::new(0., unsnapped_delta.y),
_ => DVec2::new(unsnapped_delta.x, 0.), _ => DVec2::new(unsnapped_delta.x, 0.),
}; };
shape_editor.move_selected_points(handle_lengths, document, projected_delta, equidistant, true, false, opposite, false, responses); shape_editor.move_selected_points_and_segments(handle_lengths, document, projected_delta, equidistant, true, false, opposite, false, responses);
self.previous_mouse_position += document_to_viewport.inverse().transform_vector2(unsnapped_delta); self.previous_mouse_position += document_to_viewport.inverse().transform_vector2(unsnapped_delta);
} }
@@ -1229,7 +1329,7 @@ impl Fsm for PathToolFsmState {
fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionHandlerData, tool_options: &Self::ToolOptions, responses: &mut VecDeque<Message>) -> Self { fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionHandlerData, tool_options: &Self::ToolOptions, responses: &mut VecDeque<Message>) -> Self {
let ToolActionHandlerData { document, input, shape_editor, .. } = tool_action_data; let ToolActionHandlerData { document, input, shape_editor, .. } = tool_action_data;
update_dynamic_hints(self, responses, shape_editor, document, tool_data); update_dynamic_hints(self, responses, shape_editor, document, tool_data, tool_options);
let ToolMessage::Path(event) = event else { return self }; let ToolMessage::Path(event) = event else { return self };
match (self, event) { match (self, event) {
@@ -1313,48 +1413,43 @@ impl Fsm for PathToolFsmState {
match self { match self {
Self::Ready => { Self::Ready => {
// Check if there is no point nearby tool_data.update_closest_segment(shape_editor, input.mouse.position, document, tool_options.path_overlay_mode);
if shape_editor
.find_nearest_visible_point_indices(
&document.network_interface,
input.mouse.position,
SELECTION_THRESHOLD,
tool_options.path_overlay_mode,
tool_data.frontier_handles_info.clone(),
)
.is_some()
{
tool_data.segment = None;
}
// If already hovering on a segment, then recalculate its closest point
else if let Some(closest_segment) = &mut tool_data.segment {
closest_segment.update_closest_point(document.metadata(), input.mouse.position);
if closest_segment.too_far(input.mouse.position, SEGMENT_INSERTION_DISTANCE) {
tool_data.segment = None;
}
}
// If not, check that if there is some closest segment or not
else if let Some(closest_segment) = shape_editor.upper_closest_segment(&document.network_interface, input.mouse.position, SEGMENT_INSERTION_DISTANCE) {
tool_data.segment = Some(closest_segment);
}
if let Some(closest_segment) = &tool_data.segment { if let Some(closest_segment) = &tool_data.segment {
let perp = closest_segment.calculate_perp(document); if tool_options.path_editing_mode.segment_editing_mode {
let point = closest_segment.closest_point(document.metadata()); let transform = document.metadata().transform_to_viewport(closest_segment.layer());
// Draw an X on the segment overlay_context.outline_overlay_bezier(closest_segment.bezier(), transform);
if tool_data.delete_segment_pressed {
let angle = 45_f64.to_radians();
let tilted_line = DVec2::from_angle(angle).rotate(perp);
let tilted_perp = tilted_line.perp();
overlay_context.line(point - tilted_line * SEGMENT_OVERLAY_SIZE, point + tilted_line * SEGMENT_OVERLAY_SIZE, Some(COLOR_OVERLAY_BLUE), None); // Draw the anchors again
overlay_context.line(point - tilted_perp * SEGMENT_OVERLAY_SIZE, point + tilted_perp * SEGMENT_OVERLAY_SIZE, Some(COLOR_OVERLAY_BLUE), None); let display_anchors = overlay_context.visibility_settings.anchors();
} if display_anchors {
// Draw a line on the segment let start_pos = transform.transform_point2(closest_segment.bezier().start);
else { let end_pos = transform.transform_point2(closest_segment.bezier().end);
overlay_context.line(point - perp * SEGMENT_OVERLAY_SIZE, point + perp * SEGMENT_OVERLAY_SIZE, Some(COLOR_OVERLAY_BLUE), None); let start_id = closest_segment.points()[0];
let end_id = closest_segment.points()[1];
if let Some(shape_state) = shape_editor.selected_shape_state.get_mut(&closest_segment.layer()) {
overlay_context.manipulator_anchor(start_pos, shape_state.is_point_selected(ManipulatorPointId::Anchor(start_id)), None);
overlay_context.manipulator_anchor(end_pos, shape_state.is_point_selected(ManipulatorPointId::Anchor(end_id)), None);
}
}
} else {
let perp = closest_segment.calculate_perp(document);
let point = closest_segment.closest_point(document.metadata());
// Draw an X on the segment
if tool_data.delete_segment_pressed {
let angle = 45_f64.to_radians();
let tilted_line = DVec2::from_angle(angle).rotate(perp);
let tilted_perp = tilted_line.perp();
overlay_context.line(point - tilted_line * SEGMENT_OVERLAY_SIZE, point + tilted_line * SEGMENT_OVERLAY_SIZE, Some(COLOR_OVERLAY_BLUE), None);
overlay_context.line(point - tilted_perp * SEGMENT_OVERLAY_SIZE, point + tilted_perp * SEGMENT_OVERLAY_SIZE, Some(COLOR_OVERLAY_BLUE), None);
}
// Draw a line on the segment
else {
overlay_context.line(point - perp * SEGMENT_OVERLAY_SIZE, point + perp * SEGMENT_OVERLAY_SIZE, Some(COLOR_OVERLAY_BLUE), None);
}
} }
} }
} }
@@ -1374,11 +1469,13 @@ impl Fsm for PathToolFsmState {
let quad = tool_data.selection_quad(document.metadata()); let quad = tool_data.selection_quad(document.metadata());
let polygon = &tool_data.lasso_polygon; let polygon = &tool_data.lasso_polygon;
match (selection_shape, selection_mode) { match (selection_shape, selection_mode, tool_data.started_drawing_from_inside) {
(SelectionShapeType::Box, SelectionMode::Enclosed) => overlay_context.dashed_quad(quad, None, fill_color, Some(4.), Some(4.), Some(0.5)), // Don't draw box if it is from inside a shape and selection just began
(SelectionShapeType::Lasso, SelectionMode::Enclosed) => overlay_context.dashed_polygon(polygon, None, fill_color, Some(4.), Some(4.), Some(0.5)), (SelectionShapeType::Box, SelectionMode::Enclosed, false) => overlay_context.dashed_quad(quad, None, fill_color, Some(4.), Some(4.), Some(0.5)),
(SelectionShapeType::Box, _) => overlay_context.quad(quad, None, fill_color), (SelectionShapeType::Lasso, SelectionMode::Enclosed, _) => overlay_context.dashed_polygon(polygon, None, fill_color, Some(4.), Some(4.), Some(0.5)),
(SelectionShapeType::Lasso, _) => overlay_context.polygon(polygon, None, fill_color), (SelectionShapeType::Box, _, false) => overlay_context.quad(quad, None, fill_color),
(SelectionShapeType::Lasso, _, _) => overlay_context.polygon(polygon, None, fill_color),
(SelectionShapeType::Box, _, _) => {}
} }
} }
Self::Dragging(_) => { Self::Dragging(_) => {
@@ -1424,12 +1521,14 @@ impl Fsm for PathToolFsmState {
lasso_select, lasso_select,
handle_drag_from_anchor, handle_drag_from_anchor,
drag_restore_handle, drag_restore_handle,
molding_in_segment_edit,
}, },
) => { ) => {
let extend_selection = input.keyboard.get(extend_selection as usize); let extend_selection = input.keyboard.get(extend_selection as usize);
let lasso_select = input.keyboard.get(lasso_select as usize); let lasso_select = input.keyboard.get(lasso_select as usize);
let handle_drag_from_anchor = input.keyboard.get(handle_drag_from_anchor as usize); let handle_drag_from_anchor = input.keyboard.get(handle_drag_from_anchor as usize);
let drag_zero_handle = input.keyboard.get(drag_restore_handle as usize); let drag_zero_handle = input.keyboard.get(drag_restore_handle as usize);
let molding_in_segment_edit = input.keyboard.get(molding_in_segment_edit as usize);
tool_data.selection_mode = None; tool_data.selection_mode = None;
tool_data.lasso_polygon.clear(); tool_data.lasso_polygon.clear();
@@ -1443,7 +1542,10 @@ impl Fsm for PathToolFsmState {
lasso_select, lasso_select,
handle_drag_from_anchor, handle_drag_from_anchor,
drag_zero_handle, drag_zero_handle,
molding_in_segment_edit,
tool_options.path_overlay_mode, tool_options.path_overlay_mode,
tool_options.path_editing_mode.segment_editing_mode,
tool_options.path_editing_mode.point_editing_mode,
) )
} }
( (
@@ -1459,6 +1561,7 @@ impl Fsm for PathToolFsmState {
}, },
) => { ) => {
tool_data.previous_mouse_position = document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position); tool_data.previous_mouse_position = document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position);
tool_data.started_drawing_from_inside = false;
if selection_shape == SelectionShapeType::Lasso { if selection_shape == SelectionShapeType::Lasso {
extend_lasso(&mut tool_data.lasso_polygon, input.mouse.position); extend_lasso(&mut tool_data.lasso_polygon, input.mouse.position);
@@ -1520,12 +1623,6 @@ impl Fsm for PathToolFsmState {
tool_data.handle_drag_toggle = true; tool_data.handle_drag_toggle = true;
} }
if tool_data.selection_status.is_none() {
if let Some(layer) = document.click(input) {
shape_editor.select_all_anchors_in_layer(document, layer);
}
}
let anchor_and_handle_toggled = input.keyboard.get(move_anchor_with_handles as usize); let anchor_and_handle_toggled = input.keyboard.get(move_anchor_with_handles as usize);
let initial_press = anchor_and_handle_toggled && !tool_data.select_anchor_toggled; let initial_press = anchor_and_handle_toggled && !tool_data.select_anchor_toggled;
let released_from_toggle = tool_data.select_anchor_toggled && !anchor_and_handle_toggled; let released_from_toggle = tool_data.select_anchor_toggled && !anchor_and_handle_toggled;
@@ -1711,6 +1808,11 @@ impl Fsm for PathToolFsmState {
if tool_data.drag_start_pos == previous_mouse { if tool_data.drag_start_pos == previous_mouse {
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![] }); responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![] });
} else { } else {
let selection_mode = match tool_action_data.preferences.get_selection_mode() {
SelectionMode::Directional => tool_data.calculate_selection_mode_from_direction(document.metadata()),
selection_mode => selection_mode,
};
match selection_shape { match selection_shape {
SelectionShapeType::Box => { SelectionShapeType::Box => {
let bbox = [tool_data.drag_start_pos, previous_mouse]; let bbox = [tool_data.drag_start_pos, previous_mouse];
@@ -1720,6 +1822,8 @@ impl Fsm for PathToolFsmState {
selection_change, selection_change,
tool_options.path_overlay_mode, tool_options.path_overlay_mode,
tool_data.frontier_handles_info.clone(), tool_data.frontier_handles_info.clone(),
tool_options.path_editing_mode.segment_editing_mode,
selection_mode,
); );
} }
SelectionShapeType::Lasso => shape_editor.select_all_in_shape( SelectionShapeType::Lasso => shape_editor.select_all_in_shape(
@@ -1728,6 +1832,8 @@ impl Fsm for PathToolFsmState {
selection_change, selection_change,
tool_options.path_overlay_mode, tool_options.path_overlay_mode,
tool_data.frontier_handles_info.clone(), tool_data.frontier_handles_info.clone(),
tool_options.path_editing_mode.segment_editing_mode,
selection_mode,
), ),
} }
} }
@@ -1739,6 +1845,7 @@ impl Fsm for PathToolFsmState {
(PathToolFsmState::Dragging { .. }, PathToolMessage::Escape | PathToolMessage::RightClick) => { (PathToolFsmState::Dragging { .. }, PathToolMessage::Escape | PathToolMessage::RightClick) => {
if tool_data.handle_drag_toggle && tool_data.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD { if tool_data.handle_drag_toggle && tool_data.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD {
shape_editor.deselect_all_points(); shape_editor.deselect_all_points();
shape_editor.deselect_all_segments();
shape_editor.select_points_by_manipulator_id(&tool_data.saved_points_before_handle_drag); shape_editor.select_points_by_manipulator_id(&tool_data.saved_points_before_handle_drag);
tool_data.saved_points_before_handle_drag.clear(); tool_data.saved_points_before_handle_drag.clear();
@@ -1787,8 +1894,17 @@ impl Fsm for PathToolFsmState {
let document_to_viewport = document.metadata().document_to_viewport; let document_to_viewport = document.metadata().document_to_viewport;
let previous_mouse = document_to_viewport.transform_point2(tool_data.previous_mouse_position); let previous_mouse = document_to_viewport.transform_point2(tool_data.previous_mouse_position);
if tool_data.drag_start_pos == previous_mouse {
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![] }); let selection_mode = match tool_action_data.preferences.get_selection_mode() {
SelectionMode::Directional => tool_data.calculate_selection_mode_from_direction(document.metadata()),
selection_mode => selection_mode,
};
if tool_data.drag_start_pos.distance(previous_mouse) < 1e-8 {
// If click happens inside of a shape then don't set selected nodes to empty
if document.click(input).is_none() {
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![] });
}
} else { } else {
match selection_shape { match selection_shape {
SelectionShapeType::Box => { SelectionShapeType::Box => {
@@ -1799,6 +1915,8 @@ impl Fsm for PathToolFsmState {
select_kind, select_kind,
tool_options.path_overlay_mode, tool_options.path_overlay_mode,
tool_data.frontier_handles_info.clone(), tool_data.frontier_handles_info.clone(),
tool_options.path_editing_mode.segment_editing_mode,
selection_mode,
); );
} }
SelectionShapeType::Lasso => shape_editor.select_all_in_shape( SelectionShapeType::Lasso => shape_editor.select_all_in_shape(
@@ -1807,6 +1925,8 @@ impl Fsm for PathToolFsmState {
select_kind, select_kind,
tool_options.path_overlay_mode, tool_options.path_overlay_mode,
tool_data.frontier_handles_info.clone(), tool_data.frontier_handles_info.clone(),
tool_options.path_editing_mode.segment_editing_mode,
selection_mode,
), ),
} }
} }
@@ -1827,32 +1947,31 @@ impl Fsm for PathToolFsmState {
tool_data.frontier_handles_info.clone(), tool_data.frontier_handles_info.clone(),
); );
let nearest_segment = tool_data.segment.clone();
if let Some(segment) = &mut tool_data.segment { if let Some(segment) = &mut tool_data.segment {
if !drag_occurred && !tool_data.molding_segment { let segment_mode = tool_options.path_editing_mode.segment_editing_mode;
if !drag_occurred && !tool_data.molding_segment && !segment_mode {
if tool_data.delete_segment_pressed { if tool_data.delete_segment_pressed {
if let Some(vector_data) = document.network_interface.compute_modified_vector(segment.layer()) { if let Some(vector_data) = document.network_interface.compute_modified_vector(segment.layer()) {
shape_editor.dissolve_segment(responses, segment.layer(), &vector_data, segment.segment(), segment.points()); shape_editor.dissolve_segment(responses, segment.layer(), &vector_data, segment.segment(), segment.points());
responses.add(DocumentMessage::EndTransaction);
} }
} else { } else {
segment.adjusted_insert_and_select(shape_editor, responses, extend_selection); segment.adjusted_insert_and_select(shape_editor, responses, extend_selection);
responses.add(DocumentMessage::EndTransaction);
} }
} else {
responses.add(DocumentMessage::EndTransaction);
} }
tool_data.segment = None; tool_data.segment = None;
tool_data.molding_info = None; tool_data.molding_info = None;
tool_data.molding_segment = false; tool_data.molding_segment = false;
tool_data.temporary_adjacent_handles_while_molding = None; tool_data.temporary_adjacent_handles_while_molding = None;
return PathToolFsmState::Ready;
} }
let segment_mode = tool_options.path_editing_mode.segment_editing_mode;
if let Some((layer, nearest_point)) = nearest_point { if let Some((layer, nearest_point)) = nearest_point {
let clicked_selected = shape_editor.selected_points().any(|&point| nearest_point == point);
if !drag_occurred && extend_selection { if !drag_occurred && extend_selection {
let clicked_selected = shape_editor.selected_points().any(|&point| nearest_point == point);
if clicked_selected && tool_data.last_clicked_point_was_selected { if clicked_selected && tool_data.last_clicked_point_was_selected {
shape_editor.selected_shape_state.entry(layer).or_default().deselect_point(nearest_point); shape_editor.selected_shape_state.entry(layer).or_default().deselect_point(nearest_point);
} else { } else {
@@ -1860,6 +1979,49 @@ impl Fsm for PathToolFsmState {
} }
responses.add(OverlaysMessage::Draw); responses.add(OverlaysMessage::Draw);
} }
if !drag_occurred && !extend_selection && clicked_selected {
if tool_data.saved_points_before_anchor_convert_smooth_sharp.is_empty() {
tool_data.saved_points_before_anchor_convert_smooth_sharp = shape_editor.selected_points().copied().collect::<HashSet<_>>();
}
shape_editor.deselect_all_points();
shape_editor.deselect_all_segments();
shape_editor.selected_shape_state.entry(layer).or_default().select_point(nearest_point);
responses.add(OverlaysMessage::Draw);
}
}
// Segment editing mode
else if let Some(nearest_segment) = nearest_segment {
if segment_mode {
let clicked_selected = shape_editor.selected_segments().any(|&segment| segment == nearest_segment.segment());
if !drag_occurred && extend_selection {
if clicked_selected && tool_data.last_clicked_segment_was_selected {
shape_editor
.selected_shape_state
.entry(nearest_segment.layer())
.or_default()
.deselect_segment(nearest_segment.segment());
} else {
shape_editor.selected_shape_state.entry(nearest_segment.layer()).or_default().select_segment(nearest_segment.segment());
}
responses.add(OverlaysMessage::Draw);
}
if !drag_occurred && !extend_selection && clicked_selected {
shape_editor.deselect_all_segments();
shape_editor.deselect_all_points();
shape_editor.selected_shape_state.entry(nearest_segment.layer()).or_default().select_segment(nearest_segment.segment());
responses.add(OverlaysMessage::Draw);
}
}
}
// Deselect all points if the user clicks the filled region of the shape
else if tool_data.drag_start_pos.distance(input.mouse.position) <= DRAG_THRESHOLD {
shape_editor.deselect_all_points();
shape_editor.deselect_all_segments();
} }
if tool_data.temporary_colinear_handles { if tool_data.temporary_colinear_handles {
@@ -1885,25 +2047,6 @@ impl Fsm for PathToolFsmState {
tool_data.select_anchor_toggled = false; tool_data.select_anchor_toggled = false;
} }
if let Some((layer, nearest_point)) = nearest_point {
if !drag_occurred && !extend_selection {
let clicked_selected = shape_editor.selected_points().any(|&point| nearest_point == point);
if clicked_selected {
if tool_data.saved_points_before_anchor_convert_smooth_sharp.is_empty() {
tool_data.saved_points_before_anchor_convert_smooth_sharp = shape_editor.selected_points().copied().collect::<HashSet<_>>();
}
shape_editor.deselect_all_points();
shape_editor.selected_shape_state.entry(layer).or_default().select_point(nearest_point);
responses.add(OverlaysMessage::Draw);
}
}
}
// Deselect all points if the user clicks the filled region of the shape
else if tool_data.drag_start_pos.distance(input.mouse.position) <= DRAG_THRESHOLD {
shape_editor.deselect_all_points();
}
tool_data.snapping_axis = None; tool_data.snapping_axis = None;
tool_data.sliding_point_info = None; tool_data.sliding_point_info = None;
@@ -1919,8 +2062,8 @@ impl Fsm for PathToolFsmState {
(_, PathToolMessage::Delete) => { (_, PathToolMessage::Delete) => {
// Delete the selected points and clean up overlays // Delete the selected points and clean up overlays
responses.add(DocumentMessage::AddTransaction); responses.add(DocumentMessage::AddTransaction);
// shape_editor.delete_selected_points(document, responses);
shape_editor.delete_selected_points(document, responses, false); shape_editor.delete_selected_points(document, responses, false);
shape_editor.delete_selected_segments(document, responses);
responses.add(PathToolMessage::SelectionChanged); responses.add(PathToolMessage::SelectionChanged);
PathToolFsmState::Ready PathToolFsmState::Ready
@@ -1969,6 +2112,7 @@ impl Fsm for PathToolFsmState {
if let Some(layer) = document.click(input) { if let Some(layer) = document.click(input) {
// Select all points in the layer // Select all points in the layer
shape_editor.select_connected_anchors(document, layer, input.mouse.position); shape_editor.select_connected_anchors(document, layer, input.mouse.position);
responses.add(OverlaysMessage::Draw);
} }
PathToolFsmState::Ready PathToolFsmState::Ready
@@ -1978,7 +2122,7 @@ impl Fsm for PathToolFsmState {
PathToolFsmState::Ready PathToolFsmState::Ready
} }
(_, PathToolMessage::NudgeSelectedPoints { delta_x, delta_y }) => { (_, PathToolMessage::NudgeSelectedPoints { delta_x, delta_y }) => {
shape_editor.move_selected_points( shape_editor.move_selected_points_and_segments(
tool_data.opposing_handle_lengths.take(), tool_data.opposing_handle_lengths.take(),
document, document,
(delta_x, delta_y).into(), (delta_x, delta_y).into(),
@@ -2058,10 +2202,6 @@ enum SelectionStatus {
} }
impl SelectionStatus { impl SelectionStatus {
fn is_none(&self) -> bool {
self == &SelectionStatus::None
}
fn as_one(&self) -> Option<&SingleSelectedPoint> { fn as_one(&self) -> Option<&SingleSelectedPoint> {
match self { match self {
SelectionStatus::One(one) => Some(one), SelectionStatus::One(one) => Some(one),
@@ -2191,9 +2331,7 @@ fn calculate_lock_angle(
} }
fn check_handle_over_adjacent_anchor(handle_id: ManipulatorPointId, vector_data: &VectorData) -> Option<PointId> { fn check_handle_over_adjacent_anchor(handle_id: ManipulatorPointId, vector_data: &VectorData) -> Option<PointId> {
let Some((anchor, handle_position)) = handle_id.get_anchor(&vector_data).zip(handle_id.get_position(vector_data)) else { let (anchor, handle_position) = handle_id.get_anchor(vector_data).zip(handle_id.get_position(vector_data))?;
return None;
};
let check_if_close = |point_id: &PointId| { let check_if_close = |point_id: &PointId| {
let Some(anchor_position) = vector_data.point_domain.position_from_id(*point_id) else { let Some(anchor_position) = vector_data.point_domain.position_from_id(*point_id) else {
@@ -2202,7 +2340,7 @@ fn check_handle_over_adjacent_anchor(handle_id: ManipulatorPointId, vector_data:
(anchor_position - handle_position).length() < 10. (anchor_position - handle_position).length() < 10.
}; };
vector_data.connected_points(anchor).find(|point| check_if_close(point)) vector_data.connected_points(anchor).find(check_if_close)
} }
fn calculate_adjacent_anchor_tangent( fn calculate_adjacent_anchor_tangent(
currently_dragged_handle: ManipulatorPointId, currently_dragged_handle: ManipulatorPointId,
@@ -2258,7 +2396,7 @@ fn calculate_adjacent_anchor_tangent(
}; };
let angle = shared_segment_handle let angle = shared_segment_handle
.get_position(&vector_data) .get_position(vector_data)
.zip(adjacent_anchor_position) .zip(adjacent_anchor_position)
.map(|(handle, anchor)| -(handle - anchor).angle_to(DVec2::X)); .map(|(handle, anchor)| -(handle - anchor).angle_to(DVec2::X));
@@ -2269,7 +2407,14 @@ fn calculate_adjacent_anchor_tangent(
} }
} }
fn update_dynamic_hints(state: PathToolFsmState, responses: &mut VecDeque<Message>, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, tool_data: &PathToolData) { fn update_dynamic_hints(
state: PathToolFsmState,
responses: &mut VecDeque<Message>,
shape_editor: &mut ShapeState,
document: &DocumentMessageHandler,
tool_data: &PathToolData,
tool_options: &PathToolOptions,
) {
// Condinting based on currently selected segment if it has any one g1 continuous handle // Condinting based on currently selected segment if it has any one g1 continuous handle
let hint_data = match state { let hint_data = match state {
@@ -2304,12 +2449,27 @@ fn update_dynamic_hints(state: PathToolFsmState, responses: &mut VecDeque<Messag
drag_selected_hints.push(HintInfo::multi_keys([[Key::Control], [Key::Shift]], "Slide").prepend_plus()); drag_selected_hints.push(HintInfo::multi_keys([[Key::Control], [Key::Shift]], "Slide").prepend_plus());
} }
let mut hint_data = vec![ let mut hint_data = match (tool_data.segment.is_some(), tool_options.path_editing_mode.segment_editing_mode) {
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Select Point"), HintInfo::keys([Key::Shift], "Extend").prepend_plus()]), (true, true) => {
HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Select Area"), HintInfo::keys([Key::Control], "Lasso").prepend_plus()]), vec![
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Insert Point on Segment")]), HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Select Segment"), HintInfo::keys([Key::Shift], "Extend").prepend_plus()]),
HintGroup(vec![HintInfo::keys_and_mouse([Key::Alt], MouseMotion::Lmb, "Delete Segment")]), HintGroup(vec![HintInfo::keys_and_mouse([Key::KeyA], MouseMotion::Lmb, "Mold Segment")]),
]; ]
}
(true, false) => {
vec![
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Insert Point on Segment")]),
HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Mold Segment")]),
HintGroup(vec![HintInfo::keys_and_mouse([Key::Alt], MouseMotion::Lmb, "Delete Segment")]),
]
}
(false, _) => {
vec![
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Select Point"), HintInfo::keys([Key::Shift], "Extend").prepend_plus()]),
HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Select Area"), HintInfo::keys([Key::Control], "Lasso").prepend_plus()]),
]
}
};
if at_least_one_anchor_selected { if at_least_one_anchor_selected {
// TODO: Dynamically show either "Smooth" or "Sharp" based on the current state // TODO: Dynamically show either "Smooth" or "Sharp" based on the current state
@@ -1402,6 +1402,7 @@ impl Fsm for SelectToolFsmState {
responses.add(DocumentMessage::AbortTransaction); responses.add(DocumentMessage::AbortTransaction);
tool_data.snap_manager.cleanup(responses); tool_data.snap_manager.cleanup(responses);
tool_data.axis_align = false; tool_data.axis_align = false;
tool_data.lasso_polygon.clear();
responses.add(OverlaysMessage::Draw); responses.add(OverlaysMessage::Draw);
let selection = tool_data.nested_selection_behavior; let selection = tool_data.nested_selection_behavior;
@@ -1422,6 +1423,7 @@ impl Fsm for SelectToolFsmState {
responses.add(DocumentMessage::AbortTransaction); responses.add(DocumentMessage::AbortTransaction);
tool_data.snap_manager.cleanup(responses); tool_data.snap_manager.cleanup(responses);
tool_data.lasso_polygon.clear();
responses.add(OverlaysMessage::Draw); responses.add(OverlaysMessage::Draw);
let selection = tool_data.nested_selection_behavior; let selection = tool_data.nested_selection_behavior;
@@ -143,7 +143,7 @@ impl LayoutHolder for ShapeTool {
true, true,
|_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::FillColor(None)).into(), |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::FillColor(None)).into(),
|color_type: ToolColorType| WidgetCallback::new(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::FillColorType(color_type.clone())).into()), |color_type: ToolColorType| WidgetCallback::new(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::FillColorType(color_type.clone())).into()),
|color: &ColorInput| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::FillColor(color.value.as_solid())).into(), |color: &ColorInput| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(),
)); ));
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
@@ -154,7 +154,7 @@ impl LayoutHolder for ShapeTool {
true, true,
|_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::StrokeColor(None)).into(), |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::StrokeColor(None)).into(),
|color_type: ToolColorType| WidgetCallback::new(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::StrokeColorType(color_type.clone())).into()), |color_type: ToolColorType| WidgetCallback::new(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|color: &ColorInput| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::StrokeColor(color.value.as_solid())).into(), |color: &ColorInput| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(),
)); ));
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
widgets.push(create_weight_widget(self.options.line_weight)); widgets.push(create_weight_widget(self.options.line_weight));
@@ -18,7 +18,7 @@ use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput}; use graph_craft::document::{NodeId, NodeInput};
use graphene_std::Color; use graphene_std::Color;
use graphene_std::renderer::Quad; use graphene_std::renderer::Quad;
use graphene_std::text::{Font, FontCache, TypesettingConfig, lines_clipping, load_face}; use graphene_std::text::{Font, FontCache, TypesettingConfig, lines_clipping, load_font};
use graphene_std::vector::style::Fill; use graphene_std::vector::style::Fill;
#[derive(Default)] #[derive(Default)]
@@ -35,6 +35,7 @@ pub struct TextOptions {
font_name: String, font_name: String,
font_style: String, font_style: String,
fill: ToolColorOptions, fill: ToolColorOptions,
tilt: f64,
} }
impl Default for TextOptions { impl Default for TextOptions {
@@ -42,10 +43,11 @@ impl Default for TextOptions {
Self { Self {
font_size: 24., font_size: 24.,
line_height_ratio: 1.2, line_height_ratio: 1.2,
character_spacing: 1., character_spacing: 0.,
font_name: graphene_std::consts::DEFAULT_FONT_FAMILY.into(), font_name: graphene_std::consts::DEFAULT_FONT_FAMILY.into(),
font_style: graphene_std::consts::DEFAULT_FONT_STYLE.into(), font_style: graphene_std::consts::DEFAULT_FONT_STYLE.into(),
fill: ToolColorOptions::new_primary(), fill: ToolColorOptions::new_primary(),
tilt: 0.,
} }
} }
} }
@@ -468,8 +470,8 @@ impl Fsm for TextToolFsmState {
transform: document.metadata().transform_to_viewport(tool_data.layer).to_cols_array(), transform: document.metadata().transform_to_viewport(tool_data.layer).to_cols_array(),
}); });
if let Some(editing_text) = tool_data.editing_text.as_mut() { if let Some(editing_text) = tool_data.editing_text.as_mut() {
let buzz_face = font_cache.get(&editing_text.font).map(|data| load_face(data)); let font_data = font_cache.get(&editing_text.font).map(|data| load_font(data));
let far = graphene_std::text::bounding_box(&tool_data.new_text, buzz_face.as_ref(), editing_text.typesetting, false); let far = graphene_std::text::bounding_box(&tool_data.new_text, font_data, editing_text.typesetting, false);
if far.x != 0. && far.y != 0. { if far.x != 0. && far.y != 0. {
let quad = Quad::from_box([DVec2::ZERO, far]); let quad = Quad::from_box([DVec2::ZERO, far]);
let transformed_quad = document.metadata().transform_to_viewport(tool_data.layer) * quad; let transformed_quad = document.metadata().transform_to_viewport(tool_data.layer) * quad;
@@ -517,8 +519,8 @@ impl Fsm for TextToolFsmState {
// Draw red overlay if text is clipped // Draw red overlay if text is clipped
let transformed_quad = layer_transform * bounds; let transformed_quad = layer_transform * bounds;
if let Some((text, font, typesetting)) = graph_modification_utils::get_text(layer.unwrap(), &document.network_interface) { if let Some((text, font, typesetting)) = graph_modification_utils::get_text(layer.unwrap(), &document.network_interface) {
let buzz_face = font_cache.get(font).map(|data| load_face(data)); let font_data = font_cache.get(font).map(|data| load_font(data));
if lines_clipping(text.as_str(), buzz_face, typesetting) { if lines_clipping(text.as_str(), font_data, typesetting) {
overlay_context.line(transformed_quad.0[2], transformed_quad.0[3], Some(COLOR_OVERLAY_RED), Some(3.)); overlay_context.line(transformed_quad.0[2], transformed_quad.0[3], Some(COLOR_OVERLAY_RED), Some(3.));
} }
} }
@@ -784,6 +786,7 @@ impl Fsm for TextToolFsmState {
max_width: constraint_size.map(|size| size.x), max_width: constraint_size.map(|size| size.x),
character_spacing: tool_options.character_spacing, character_spacing: tool_options.character_spacing,
max_height: constraint_size.map(|size| size.y), max_height: constraint_size.map(|size| size.y),
tilt: tool_options.tilt,
}, },
font: Font::new(tool_options.font_name.clone(), tool_options.font_style.clone()), font: Font::new(tool_options.font_name.clone(), tool_options.font_style.clone()),
color: tool_options.fill.active_color(), color: tool_options.fill.active_color(),
@@ -180,14 +180,28 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
*selected.pivot = selected.mean_average_of_pivots(); *selected.pivot = selected.mean_average_of_pivots();
self.local_pivot = document.metadata().document_to_viewport.inverse().transform_point2(*selected.pivot); self.local_pivot = document.metadata().document_to_viewport.inverse().transform_point2(*selected.pivot);
self.grab_target = document.metadata().document_to_viewport.inverse().transform_point2(selected.mean_average_of_pivots()); self.grab_target = document.metadata().document_to_viewport.inverse().transform_point2(selected.mean_average_of_pivots());
} else if let Some(vector_data) = selected_layers.first().and_then(|&layer| document.network_interface.compute_modified_vector(layer)) { }
// Here vector data from all layers is not considered which can be a problem in pivot calculation
else if let Some(vector_data) = selected_layers.first().and_then(|&layer| document.network_interface.compute_modified_vector(layer)) {
*selected.original_transforms = OriginalTransforms::default(); *selected.original_transforms = OriginalTransforms::default();
let viewspace = document.metadata().transform_to_viewport(selected_layers[0]); let viewspace = document.metadata().transform_to_viewport(selected_layers[0]);
let selected_points = shape_editor.selected_points().collect::<Vec<_>>();
let selected_segments = shape_editor.selected_segments().collect::<HashSet<_>>();
let mut affected_points = shape_editor.selected_points().copied().collect::<Vec<_>>();
for (segment_id, _, start, end) in vector_data.segment_bezier_iter() {
if selected_segments.contains(&segment_id) {
affected_points.push(ManipulatorPointId::Anchor(start));
affected_points.push(ManipulatorPointId::Anchor(end));
}
}
let affected_point_refs = affected_points.iter().collect();
let get_location = |point: &&ManipulatorPointId| point.get_position(&vector_data).map(|position| viewspace.transform_point2(position)); let get_location = |point: &&ManipulatorPointId| point.get_position(&vector_data).map(|position| viewspace.transform_point2(position));
if let Some((new_pivot, grab_target)) = calculate_pivot(&selected_points, &vector_data, viewspace, |point: &ManipulatorPointId| get_location(&point)) { if let Some((new_pivot, grab_target)) = calculate_pivot(&affected_point_refs, &vector_data, viewspace, |point: &ManipulatorPointId| get_location(&point)) {
*selected.pivot = new_pivot; *selected.pivot = new_pivot;
self.local_pivot = document_to_viewport.inverse().transform_point2(*selected.pivot); self.local_pivot = document_to_viewport.inverse().transform_point2(*selected.pivot);
@@ -390,7 +404,8 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
} }
TransformLayerMessage::BeginGRS { transform_type } => { TransformLayerMessage::BeginGRS { transform_type } => {
let selected_points: Vec<&ManipulatorPointId> = shape_editor.selected_points().collect(); let selected_points: Vec<&ManipulatorPointId> = shape_editor.selected_points().collect();
if (using_path_tool && selected_points.is_empty()) let selected_segments = shape_editor.selected_segments().collect::<Vec<_>>();
if (using_path_tool && selected_points.is_empty() && selected_segments.is_empty())
|| (!using_path_tool && !using_select_tool && !using_pen_tool && !using_shape_tool) || (!using_path_tool && !using_select_tool && !using_pen_tool && !using_shape_tool)
|| selected_layers.is_empty() || selected_layers.is_empty()
|| transform_type.equivalent_to(self.transform_operation) || transform_type.equivalent_to(self.transform_operation)
+1 -3
View File
@@ -227,9 +227,7 @@ impl NodeRuntime {
} }
async fn update_network(&mut self, mut graph: NodeNetwork) -> Result<ResolvedDocumentNodeTypesDelta, String> { async fn update_network(&mut self, mut graph: NodeNetwork) -> Result<ResolvedDocumentNodeTypesDelta, String> {
if cfg!(not(test)) { preprocessor::expand_network(&mut graph, &self.substitutions);
preprocessor::expand_network(&mut graph, &self.substitutions);
}
let scoped_network = wrap_network_in_scope(graph, self.editor_api.clone()); let scoped_network = wrap_network_in_scope(graph, self.editor_api.clone());
+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
<circle cx="6" cy="6" r="1.5" />
</svg>

After

Width:  |  Height:  |  Size: 102 B

+2
View File
@@ -104,6 +104,8 @@
--color-f-white-rgb: 255, 255, 255; --color-f-white-rgb: 255, 255, 255;
--color-error-red: #d6536e; --color-error-red: #d6536e;
--color-error-red-rgb: 214, 83, 110; --color-error-red-rgb: 214, 83, 110;
--color-warning-yellow: #d5aa43;
--color-warning-yellow-rgb: 213, 170, 67;
--color-data-general: #cfcfcf; --color-data-general: #cfcfcf;
--color-data-general-dim: #8a8a8a; --color-data-general-dim: #8a8a8a;
@@ -760,6 +760,8 @@
.text-input { .text-input {
word-break: break-all; word-break: break-all;
unicode-bidi: plaintext;
text-align: left;
} }
.text-input div { .text-input div {
@@ -773,6 +775,8 @@
overflow-wrap: anywhere; overflow-wrap: anywhere;
white-space: pre-wrap; white-space: pre-wrap;
word-break: normal; word-break: normal;
unicode-bidi: plaintext;
text-align: left;
display: inline-block; display: inline-block;
// Workaround to force Chrome to display the flashing text entry cursor when text is empty // Workaround to force Chrome to display the flashing text entry cursor when text is empty
padding-left: 1px; padding-left: 1px;
@@ -30,6 +30,7 @@
let open = false; let open = false;
$: watchSelectedIndex(selectedIndex); $: watchSelectedIndex(selectedIndex);
$: watchEntries(entries);
$: watchActiveEntry(activeEntry); $: watchActiveEntry(activeEntry);
$: watchOpen(open); $: watchOpen(open);
@@ -38,7 +39,13 @@
} }
// Called only when `selectedIndex` is changed from outside this component // Called only when `selectedIndex` is changed from outside this component
function watchSelectedIndex(_?: number) { function watchSelectedIndex(_?: typeof selectedIndex) {
activeEntrySkipWatcher = true;
activeEntry = makeActiveEntry();
}
// Called only when `entries` is changed from outside this component
function watchEntries(_?: typeof entries) {
activeEntrySkipWatcher = true; activeEntrySkipWatcher = true;
activeEntry = makeActiveEntry(); activeEntry = makeActiveEntry();
} }
@@ -158,6 +158,7 @@
background: none; background: none;
color: var(--color-e-nearwhite); color: var(--color-e-nearwhite);
caret-color: var(--color-e-nearwhite); caret-color: var(--color-e-nearwhite);
unicode-bidi: plaintext;
&::selection { &::selection {
background-color: var(--color-4-dimgray); background-color: var(--color-4-dimgray);
+2
View File
@@ -13,6 +13,7 @@ import Checkmark from "@graphite-frontend/assets/icon-12px-solid/checkmark.svg";
import Clipped from "@graphite-frontend/assets/icon-12px-solid/clipped.svg"; import Clipped from "@graphite-frontend/assets/icon-12px-solid/clipped.svg";
import CloseX from "@graphite-frontend/assets/icon-12px-solid/close-x.svg"; import CloseX from "@graphite-frontend/assets/icon-12px-solid/close-x.svg";
import Delay from "@graphite-frontend/assets/icon-12px-solid/delay.svg"; import Delay from "@graphite-frontend/assets/icon-12px-solid/delay.svg";
import Dot from "@graphite-frontend/assets/icon-12px-solid/dot.svg";
import DropdownArrow from "@graphite-frontend/assets/icon-12px-solid/dropdown-arrow.svg"; import DropdownArrow from "@graphite-frontend/assets/icon-12px-solid/dropdown-arrow.svg";
import Edit12px from "@graphite-frontend/assets/icon-12px-solid/edit-12px.svg"; import Edit12px from "@graphite-frontend/assets/icon-12px-solid/edit-12px.svg";
import Empty12px from "@graphite-frontend/assets/icon-12px-solid/empty-12px.svg"; import Empty12px from "@graphite-frontend/assets/icon-12px-solid/empty-12px.svg";
@@ -55,6 +56,7 @@ const SOLID_12PX = {
Clipped: { svg: Clipped, size: 12 }, Clipped: { svg: Clipped, size: 12 },
CloseX: { svg: CloseX, size: 12 }, CloseX: { svg: CloseX, size: 12 },
Delay: { svg: Delay, size: 12 }, Delay: { svg: Delay, size: 12 },
Dot: { svg: Dot, size: 12 },
DropdownArrow: { svg: DropdownArrow, size: 12 }, DropdownArrow: { svg: DropdownArrow, size: 12 },
Edit12px: { svg: Edit12px, size: 12 }, Edit12px: { svg: Edit12px, size: 12 },
Empty12px: { svg: Empty12px, size: 12 }, Empty12px: { svg: Empty12px, size: 12 },
+28
View File
@@ -0,0 +1,28 @@
[package]
name = "graphene-brush"
version = "0.1.0"
edition = "2024"
description = "graphene brush"
authors = ["Graphite Authors <contact@graphite.rs>"]
license = "MIT OR Apache-2.0"
[features]
default = ["serde"]
serde = ["dep:serde"]
[dependencies]
# Local dependencies
dyn-any = { workspace = true }
graphene-core = { workspace = true }
graphene-raster-nodes = { workspace = true }
node-macro = { workspace = true }
# Workspace dependencies
glam = { workspace = true }
# Optional workspace dependencies
serde = { workspace = true, optional = true, features = ["derive"] }
[dev-dependencies]
# Workspace dependencies
tokio = { workspace = true }
@@ -1,19 +1,21 @@
use crate::raster::{empty_image, extend_image_to_bounds}; use crate::brush_cache::BrushCache;
use crate::brush_stroke::{BrushStroke, BrushStyle};
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use graph_craft::generic::FnNode; use graphene_core::blending::BlendMode;
use graph_craft::proto::FutureWrapperNode;
use graphene_core::bounds::BoundingBox; use graphene_core::bounds::BoundingBox;
use graphene_core::color::{Alpha, Color, Pixel, Sample};
use graphene_core::generic::FnNode;
use graphene_core::instances::Instance; use graphene_core::instances::Instance;
use graphene_core::math::bbox::{AxisAlignedBbox, Bbox}; use graphene_core::math::bbox::{AxisAlignedBbox, Bbox};
use graphene_core::raster::adjustments::blend_colors; use graphene_core::raster::BitmapMut;
use graphene_core::raster::brush_cache::BrushCache;
use graphene_core::raster::image::Image; use graphene_core::raster::image::Image;
use graphene_core::raster::{Alpha, BitmapMut, BlendMode, Color, Pixel, Sample};
use graphene_core::raster_types::{CPU, Raster, RasterDataTable}; use graphene_core::raster_types::{CPU, Raster, RasterDataTable};
use graphene_core::registry::FutureWrapperNode;
use graphene_core::transform::Transform; use graphene_core::transform::Transform;
use graphene_core::value::ClonedNode; use graphene_core::value::ClonedNode;
use graphene_core::vector::brush_stroke::{BrushStroke, BrushStyle}; use graphene_core::{Ctx, Node};
use graphene_core::{Ctx, GraphicElement, Node}; use graphene_raster_nodes::adjustments::blend_colors;
use graphene_raster_nodes::std_nodes::{empty_image, extend_image_to_bounds};
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
pub struct BrushStampGenerator<P: Pixel + Alpha> { pub struct BrushStampGenerator<P: Pixel + Alpha> {
@@ -50,13 +52,13 @@ impl<P: Pixel + Alpha> Sample for BrushStampGenerator<P> {
return None; return None;
}; };
use graphene_core::raster::Channel; use graphene_core::color::Channel;
Some(self.color.multiplied_alpha(P::AlphaChannel::from_linear(result))) Some(self.color.multiplied_alpha(P::AlphaChannel::from_linear(result)))
} }
} }
#[node_macro::node(skip_impl)] #[node_macro::node(skip_impl)]
fn brush_stamp_generator(diameter: f64, color: Color, hardness: f64, flow: f64) -> BrushStampGenerator<Color> { fn brush_stamp_generator(#[unit(" px")] diameter: f64, color: Color, hardness: f64, flow: f64) -> BrushStampGenerator<Color> {
// Diameter // Diameter
let radius = diameter / 2.; let radius = diameter / 2.;
@@ -78,7 +80,6 @@ fn brush_stamp_generator(diameter: f64, color: Color, hardness: f64, flow: f64)
fn blit<BlendFn>(mut target: RasterDataTable<CPU>, texture: Raster<CPU>, positions: Vec<DVec2>, blend_mode: BlendFn) -> RasterDataTable<CPU> fn blit<BlendFn>(mut target: RasterDataTable<CPU>, texture: Raster<CPU>, positions: Vec<DVec2>, blend_mode: BlendFn) -> RasterDataTable<CPU>
where where
BlendFn: for<'any_input> Node<'any_input, (Color, Color), Output = Color>, BlendFn: for<'any_input> Node<'any_input, (Color, Color), Output = Color>,
GraphicElement: From<Raster<CPU>>,
{ {
if positions.is_empty() { if positions.is_empty() {
return target; return target;
@@ -239,7 +240,6 @@ async fn brush(_: impl Ctx, mut image_frame_table: RasterDataTable<CPU>, strokes
let target = core::mem::take(&mut brush_plan.first_stroke_texture); let target = core::mem::take(&mut brush_plan.first_stroke_texture);
extend_image_to_bounds((), target.to_table(), stroke_to_layer) extend_image_to_bounds((), target.to_table(), stroke_to_layer)
} else { } else {
use crate::raster::empty_image;
empty_image((), stroke_to_layer, Color::TRANSPARENT) empty_image((), stroke_to_layer, Color::TRANSPARENT)
// EmptyImageNode::new(CopiedNode::new(stroke_to_layer), CopiedNode::new(Color::TRANSPARENT)).eval(()) // EmptyImageNode::new(CopiedNode::new(stroke_to_layer), CopiedNode::new(Color::TRANSPARENT)).eval(())
}; };
@@ -393,7 +393,7 @@ mod test {
(), (),
RasterDataTable::<CPU>::new(Raster::new_cpu(Image::<Color>::default())), RasterDataTable::<CPU>::new(Raster::new_cpu(Image::<Color>::default())),
vec![BrushStroke { vec![BrushStroke {
trace: vec![crate::vector::brush_stroke::BrushInputSample { position: DVec2::ZERO }], trace: vec![crate::brush_stroke::BrushInputSample { position: DVec2::ZERO }],
style: BrushStyle { style: BrushStyle {
color: Color::BLACK, color: Color::BLACK,
diameter: 20., diameter: 20.,
@@ -1,9 +1,9 @@
use crate::instances::Instance; use crate::brush_stroke::BrushStroke;
use crate::raster_types::CPU; use crate::brush_stroke::BrushStyle;
use crate::raster_types::Raster;
use crate::vector::brush_stroke::BrushStroke;
use crate::vector::brush_stroke::BrushStyle;
use dyn_any::DynAny; use dyn_any::DynAny;
use graphene_core::instances::Instance;
use graphene_core::raster_types::CPU;
use graphene_core::raster_types::Raster;
use std::collections::HashMap; use std::collections::HashMap;
use std::hash::Hash; use std::hash::Hash;
use std::sync::Arc; use std::sync::Arc;
@@ -15,11 +15,11 @@ struct BrushCacheImpl {
prev_input: Vec<BrushStroke>, prev_input: Vec<BrushStroke>,
// The strokes that have been fully processed and blended into the background. // The strokes that have been fully processed and blended into the background.
#[serde(deserialize_with = "crate::graphene_core::raster::image::migrate_image_frame_instance")] #[serde(deserialize_with = "graphene_core::raster::image::migrate_image_frame_instance")]
background: Instance<Raster<CPU>>, background: Instance<Raster<CPU>>,
#[serde(deserialize_with = "crate::graphene_core::raster::image::migrate_image_frame_instance")] #[serde(deserialize_with = "graphene_core::raster::image::migrate_image_frame_instance")]
blended_image: Instance<Raster<CPU>>, blended_image: Instance<Raster<CPU>>,
#[serde(deserialize_with = "crate::graphene_core::raster::image::migrate_image_frame_instance")] #[serde(deserialize_with = "graphene_core::raster::image::migrate_image_frame_instance")]
last_stroke_texture: Instance<Raster<CPU>>, last_stroke_texture: Instance<Raster<CPU>>,
// A cache for brush textures. // A cache for brush textures.
@@ -1,8 +1,8 @@
use crate::Color;
use crate::math::bbox::AxisAlignedBbox;
use crate::raster::BlendMode;
use dyn_any::DynAny; use dyn_any::DynAny;
use glam::DVec2; use glam::DVec2;
use graphene_core::blending::BlendMode;
use graphene_core::color::Color;
use graphene_core::math::bbox::AxisAlignedBbox;
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
/// The style of a brush. /// The style of a brush.
+3
View File
@@ -0,0 +1,3 @@
pub mod brush;
pub mod brush_cache;
pub mod brush_stroke;
+2 -4
View File
@@ -14,9 +14,6 @@ wgpu = ["dep:wgpu"]
dealloc_nodes = [] dealloc_nodes = []
[dependencies] [dependencies]
# Local dependencies
math-parser = { workspace = true }
# Workspace dependencies # Workspace dependencies
bytemuck = { workspace = true } bytemuck = { workspace = true }
node-macro = { workspace = true } node-macro = { workspace = true }
@@ -32,10 +29,11 @@ ctor = { workspace = true }
rand_chacha = { workspace = true } rand_chacha = { workspace = true }
bezier-rs = { workspace = true } bezier-rs = { workspace = true }
specta = { workspace = true } specta = { workspace = true }
rustybuzz = { workspace = true }
image = { workspace = true } image = { workspace = true }
half = { workspace = true } half = { workspace = true }
tinyvec = { workspace = true } tinyvec = { workspace = true }
parley = { workspace = true }
skrifa = { workspace = true }
kurbo = { workspace = true } kurbo = { workspace = true }
log = { workspace = true } log = { workspace = true }
base64 = { workspace = true } base64 = { workspace = true }
+35
View File
@@ -0,0 +1,35 @@
use crate::raster_types::{CPU, RasterDataTable};
use crate::vector::VectorDataTable;
use crate::{Color, Ctx};
use glam::{DAffine2, DVec2};
#[node_macro::node(category("Debug"), name("Log to Console"))]
fn log_to_console<T: std::fmt::Debug>(_: impl Ctx, #[implementations(String, bool, f64, u32, u64, DVec2, VectorDataTable, DAffine2, Color, Option<Color>)] value: T) -> T {
// KEEP THIS `debug!()` - It acts as the output for the debug node itself
log::debug!("{:#?}", value);
value
}
/// Meant for debugging purposes, not general use. Returns the size of the input type in bytes.
#[node_macro::node(category("Debug"))]
fn size_of(_: impl Ctx, ty: crate::Type) -> Option<usize> {
ty.size()
}
/// Meant for debugging purposes, not general use. Wraps the input value in the Some variant of an Option.
#[node_macro::node(category("Debug"))]
fn some<T>(_: impl Ctx, #[implementations(f64, f32, u32, u64, String, Color)] input: T) -> Option<T> {
Some(input)
}
/// Meant for debugging purposes, not general use. Unwraps the input value from an Option, returning the default value if the input is None.
#[node_macro::node(category("Debug"))]
fn unwrap<T: Default>(_: impl Ctx, #[implementations(Option<f64>, Option<f32>, Option<u32>, Option<u64>, Option<String>, Option<Color>)] input: Option<T>) -> T {
input.unwrap_or_default()
}
/// Meant for debugging purposes, not general use. Clones the input value.
#[node_macro::node(category("Debug"))]
fn clone<'i, T: Clone + 'i>(_: impl Ctx, #[implementations(&RasterDataTable<CPU>)] value: &'i T) -> T {
value.clone()
}
+23
View File
@@ -0,0 +1,23 @@
use crate::Ctx;
use dyn_any::DynAny;
use glam::{DVec2, IVec2, UVec2};
/// Obtains the X or Y component of a coordinate point.
///
/// The inverse of this node is "Coordinate Value", which can have either or both its X and Y exposed as graph inputs.
#[node_macro::node(name("Extract XY"), category("Math: Vector"))]
fn extract_xy<T: Into<DVec2>>(_: impl Ctx, #[implementations(DVec2, IVec2, UVec2)] vector: T, axis: XY) -> f64 {
match axis {
XY::X => vector.into().x,
XY::Y => vector.into().y,
}
}
/// The X or Y component of a coordinate.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type, serde::Serialize, serde::Deserialize)]
#[widget(Dropdown)]
pub enum XY {
#[default]
X,
Y,
}
+50
View File
@@ -551,3 +551,53 @@ impl From<GraphicGroupTable> for GraphicElement {
pub trait ToGraphicElement { pub trait ToGraphicElement {
fn to_graphic_element(&self) -> GraphicElement; fn to_graphic_element(&self) -> GraphicElement;
} }
/// Returns the value at the specified index in the collection.
/// If that index has no value, the type's default value is returned.
#[node_macro::node(category("General"))]
fn index<T: AtIndex + Clone + Default>(
_: impl Ctx,
/// The collection of data, such as a list or table.
#[implementations(
Vec<Color>,
Vec<Option<Color>>,
Vec<f64>, Vec<u64>,
Vec<DVec2>,
VectorDataTable,
RasterDataTable<CPU>,
GraphicGroupTable,
)]
collection: T,
/// The index of the item to retrieve, starting from 0 for the first item.
index: u32,
) -> T::Output
where
T::Output: Clone + Default,
{
collection.at_index(index as usize).unwrap_or_default()
}
pub trait AtIndex {
type Output;
fn at_index(&self, index: usize) -> Option<Self::Output>;
}
impl<T: Clone> AtIndex for Vec<T> {
type Output = T;
fn at_index(&self, index: usize) -> Option<Self::Output> {
self.get(index).cloned()
}
}
impl<T: Clone> AtIndex for Instances<T> {
type Output = Instances<T>;
fn at_index(&self, index: usize) -> Option<Self::Output> {
let mut result_table = Self::default();
if let Some(row) = self.instance_ref_iter().nth(index) {
result_table.push(row.to_instance_cloned());
Some(result_table)
} else {
None
}
}
}
+2
View File
@@ -8,6 +8,8 @@ pub mod bounds;
pub mod color; pub mod color;
pub mod consts; pub mod consts;
pub mod context; pub mod context;
pub mod debug;
pub mod extract_xy;
pub mod generic; pub mod generic;
pub mod gradient; pub mod gradient;
mod graphic_element; mod graphic_element;
+27 -10
View File
@@ -1,14 +1,13 @@
use crate::ArtboardGroupTable;
use crate::Color;
use crate::GraphicElement;
use crate::GraphicGroupTable;
use crate::gradient::GradientStops;
use crate::raster_types::{CPU, GPU, RasterDataTable};
use crate::vector::VectorDataTable; use crate::vector::VectorDataTable;
use crate::{Color, Context, Ctx}; use crate::{Context, Ctx};
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
#[node_macro::node(category("Debug"), name("Log to Console"))]
fn log_to_console<T: std::fmt::Debug>(_: impl Ctx, #[implementations(String, bool, f64, u32, u64, DVec2, VectorDataTable, DAffine2, Color, Option<Color>)] value: T) -> T {
// KEEP THIS `debug!()` - It acts as the output for the debug node itself
log::debug!("{:#?}", value);
value
}
#[node_macro::node(category("Text"))] #[node_macro::node(category("Text"))]
fn to_string<T: std::fmt::Debug>(_: impl Ctx, #[implementations(String, bool, f64, u32, u64, DVec2, VectorDataTable, DAffine2)] value: T) -> String { fn to_string<T: std::fmt::Debug>(_: impl Ctx, #[implementations(String, bool, f64, u32, u64, DVec2, VectorDataTable, DAffine2)] value: T) -> String {
format!("{:?}", value) format!("{:?}", value)
@@ -45,24 +44,42 @@ async fn switch<T, C: Send + 'n + Clone>(
#[implementations( #[implementations(
Context -> String, Context -> String,
Context -> bool, Context -> bool,
Context -> f32,
Context -> f64, Context -> f64,
Context -> u32, Context -> u32,
Context -> u64, Context -> u64,
Context -> DVec2, Context -> DVec2,
Context -> VectorDataTable,
Context -> DAffine2, Context -> DAffine2,
Context -> ArtboardGroupTable,
Context -> VectorDataTable,
Context -> GraphicGroupTable,
Context -> RasterDataTable<CPU>,
Context -> RasterDataTable<GPU>,
Context -> GraphicElement,
Context -> Color,
Context -> Option<Color>,
Context -> GradientStops,
)] )]
if_true: impl Node<C, Output = T>, if_true: impl Node<C, Output = T>,
#[expose] #[expose]
#[implementations( #[implementations(
Context -> String, Context -> String,
Context -> bool, Context -> bool,
Context -> f32,
Context -> f64, Context -> f64,
Context -> u32, Context -> u32,
Context -> u64, Context -> u64,
Context -> DVec2, Context -> DVec2,
Context -> VectorDataTable,
Context -> DAffine2, Context -> DAffine2,
Context -> ArtboardGroupTable,
Context -> VectorDataTable,
Context -> GraphicGroupTable,
Context -> RasterDataTable<CPU>,
Context -> RasterDataTable<GPU>,
Context -> GraphicElement,
Context -> Color,
Context -> Option<Color>,
Context -> GradientStops,
)] )]
if_false: impl Node<C, Output = T>, if_false: impl Node<C, Output = T>,
) -> T { ) -> T {
+10 -571
View File
@@ -1,511 +1,5 @@
use crate::Ctx; use crate::Node;
use crate::raster_types::{CPU, RasterDataTable};
use crate::registry::types::{Fraction, Percentage};
use crate::vector::style::GradientStops;
use crate::{Color, Node};
use dyn_any::DynAny;
use glam::{DVec2, IVec2, UVec2};
use math_parser::ast;
use math_parser::context::{EvalContext, NothingMap, ValueProvider};
use math_parser::value::{Number, Value};
use num_traits::Pow;
use rand::{Rng, SeedableRng};
use std::marker::PhantomData; use std::marker::PhantomData;
use std::ops::{Add, Div, Mul, Rem, Sub};
/// The struct that stores the context for the maths parser.
/// This is currently just limited to supplying `a` and `b` until we add better node graph support and UI for variadic inputs.
struct MathNodeContext {
a: f64,
b: f64,
}
impl ValueProvider for MathNodeContext {
fn get_value(&self, name: &str) -> Option<Value> {
if name.eq_ignore_ascii_case("a") {
Some(Value::from_f64(self.a))
} else if name.eq_ignore_ascii_case("b") {
Some(Value::from_f64(self.b))
} else {
None
}
}
}
/// Calculates a mathematical expression with input values "A" and "B"
#[node_macro::node(category("Math: Arithmetic"), properties("math_properties"))]
fn math<U: num_traits::float::Float>(
_: impl Ctx,
/// The value of "A" when calculating the expression
#[implementations(f64, f32)]
operand_a: U,
/// A math expression that may incorporate "A" and/or "B", such as "sqrt(A + B) - B^2"
#[default(A + B)]
expression: String,
/// The value of "B" when calculating the expression
#[implementations(f64, f32)]
#[default(1.)]
operand_b: U,
) -> U {
let (node, _unit) = match ast::Node::try_parse_from_str(&expression) {
Ok(expr) => expr,
Err(e) => {
warn!("Invalid expression: `{expression}`\n{e:?}");
return U::from(0.).unwrap();
}
};
let context = EvalContext::new(
MathNodeContext {
a: operand_a.to_f64().unwrap(),
b: operand_b.to_f64().unwrap(),
},
NothingMap,
);
let value = match node.eval(&context) {
Ok(value) => value,
Err(e) => {
warn!("Expression evaluation error: {e:?}");
return U::from(0.).unwrap();
}
};
let Value::Number(num) = value;
match num {
Number::Real(val) => U::from(val).unwrap(),
Number::Complex(c) => U::from(c.re).unwrap(),
}
}
/// The addition operation (+) calculates the sum of two numbers.
#[node_macro::node(category("Math: Arithmetic"))]
fn add<U: Add<T>, T>(
_: impl Ctx,
#[implementations(f64, &f64, f64, &f64, f32, &f32, f32, &f32, u32, &u32, u32, &u32, DVec2, f64, DVec2)] augend: U,
#[implementations(f64, f64, &f64, &f64, f32, f32, &f32, &f32, u32, u32, &u32, &u32, DVec2, DVec2, f64)] addend: T,
) -> <U as Add<T>>::Output {
augend + addend
}
/// The subtraction operation (-) calculates the difference between two numbers.
#[node_macro::node(category("Math: Arithmetic"))]
fn subtract<U: Sub<T>, T>(
_: impl Ctx,
#[implementations(f64, &f64, f64, &f64, f32, &f32, f32, &f32, u32, &u32, u32, &u32, DVec2, f64, DVec2)] minuend: U,
#[implementations(f64, f64, &f64, &f64, f32, f32, &f32, &f32, u32, u32, &u32, &u32, DVec2, DVec2, f64)] subtrahend: T,
) -> <U as Sub<T>>::Output {
minuend - subtrahend
}
/// The multiplication operation (×) calculates the product of two numbers.
#[node_macro::node(category("Math: Arithmetic"))]
fn multiply<U: Mul<T>, T>(
_: impl Ctx,
#[implementations(f64, &f64, f64, &f64, f32, &f32, f32, &f32, u32, &u32, u32, &u32, DVec2, f64, DVec2)] multiplier: U,
#[default(1.)]
#[implementations(f64, f64, &f64, &f64, f32, f32, &f32, &f32, u32, u32, &u32, &u32, DVec2, DVec2, f64)]
multiplicand: T,
) -> <U as Mul<T>>::Output {
multiplier * multiplicand
}
/// The division operation (÷) calculates the quotient of two numbers.
///
/// Produces 0 if the denominator is 0.
#[node_macro::node(category("Math: Arithmetic"))]
fn divide<U: Div<T> + Default + PartialEq, T: Default + PartialEq>(
_: impl Ctx,
#[implementations(f64, f64, f32, f32, u32, u32, DVec2, DVec2, f64)] numerator: U,
#[default(1.)]
#[implementations(f64, f64, f32, f32, u32, u32, DVec2, f64, DVec2)]
denominator: T,
) -> <U as Div<T>>::Output
where
<U as Div<T>>::Output: Default,
{
if denominator == T::default() {
return <U as Div<T>>::Output::default();
}
numerator / denominator
}
/// The modulo operation (%) calculates the remainder from the division of two numbers. The sign of the result shares the sign of the numerator unless "Always Positive" is enabled.
#[node_macro::node(category("Math: Arithmetic"))]
fn modulo<U: Rem<T, Output: Add<T, Output: Rem<T, Output = U::Output>>>, T: Copy>(
_: impl Ctx,
#[implementations(f64, &f64, f64, &f64, f32, &f32, f32, &f32, u32, &u32, u32, &u32, DVec2, DVec2, f64)] numerator: U,
#[default(2.)]
#[implementations(f64, f64, &f64, &f64, f32, f32, &f32, &f32, u32, u32, &u32, &u32, DVec2, f64, DVec2)]
modulus: T,
always_positive: bool,
) -> <U as Rem<T>>::Output {
if always_positive { (numerator % modulus + modulus) % modulus } else { numerator % modulus }
}
/// The exponent operation (^) calculates the result of raising a number to a power.
#[node_macro::node(category("Math: Arithmetic"))]
fn exponent<U: Pow<T>, T>(
_: impl Ctx,
#[implementations(f64, &f64, f64, &f64, f32, &f32, f32, &f32, u32, &u32, u32, &u32)] base: U,
#[default(2.)]
#[implementations(f64, f64, &f64, &f64, f32, f32, &f32, &f32, u32, u32, &u32, &u32)]
power: T,
) -> <U as num_traits::Pow<T>>::Output {
base.pow(power)
}
/// The square root operation (√) calculates the nth root of a number, equivalent to raising the number to the power of 1/n.
#[node_macro::node(category("Math: Arithmetic"))]
fn root<U: num_traits::float::Float>(
_: impl Ctx,
#[default(2.)]
#[implementations(f64, f32)]
radicand: U,
#[default(2.)]
#[implementations(f64, f32)]
degree: U,
) -> U {
if degree == U::from(2.).unwrap() {
radicand.sqrt()
} else if degree == U::from(3.).unwrap() {
radicand.cbrt()
} else {
radicand.powf(U::from(1.).unwrap() / degree)
}
}
/// The logarithmic function (log) calculates the logarithm of a number with a specified base. If the natural logarithm function (ln) is desired, set the base to "e".
#[node_macro::node(category("Math: Arithmetic"))]
fn logarithm<U: num_traits::float::Float>(
_: impl Ctx,
#[implementations(f64, f32)] value: U,
#[default(2.)]
#[implementations(f64, f32)]
base: U,
) -> U {
if base == U::from(2.).unwrap() {
value.log2()
} else if base == U::from(10.).unwrap() {
value.log10()
} else if base - U::from(std::f64::consts::E).unwrap() < U::epsilon() * U::from(1e6).unwrap() {
value.ln()
} else {
value.log(base)
}
}
/// The sine trigonometric function (sin) calculates the ratio of the angle's opposite side length to its hypotenuse length.
#[node_macro::node(category("Math: Trig"))]
fn sine<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] theta: U, radians: bool) -> U {
if radians { theta.sin() } else { theta.to_radians().sin() }
}
/// The cosine trigonometric function (cos) calculates the ratio of the angle's adjacent side length to its hypotenuse length.
#[node_macro::node(category("Math: Trig"))]
fn cosine<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] theta: U, radians: bool) -> U {
if radians { theta.cos() } else { theta.to_radians().cos() }
}
/// The tangent trigonometric function (tan) calculates the ratio of the angle's opposite side length to its adjacent side length.
#[node_macro::node(category("Math: Trig"))]
fn tangent<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] theta: U, radians: bool) -> U {
if radians { theta.tan() } else { theta.to_radians().tan() }
}
/// The inverse sine trigonometric function (asin) calculates the angle whose sine is the specified value.
#[node_macro::node(category("Math: Trig"))]
fn sine_inverse<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U, radians: bool) -> U {
if radians { value.asin() } else { value.asin().to_degrees() }
}
/// The inverse cosine trigonometric function (acos) calculates the angle whose cosine is the specified value.
#[node_macro::node(category("Math: Trig"))]
fn cosine_inverse<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U, radians: bool) -> U {
if radians { value.acos() } else { value.acos().to_degrees() }
}
/// The inverse tangent trigonometric function (atan or atan2, depending on input type) calculates:
/// atan: the angle whose tangent is the specified scalar number.
/// atan2: the angle of a ray from the origin to the specified coordinate.
#[node_macro::node(category("Math: Trig"))]
fn tangent_inverse<U: TangentInverse>(_: impl Ctx, #[implementations(f64, f32, DVec2)] value: U, radians: bool) -> U::Output {
value.atan(radians)
}
pub trait TangentInverse {
type Output: num_traits::float::Float;
fn atan(self, radians: bool) -> Self::Output;
}
impl TangentInverse for f32 {
type Output = f32;
fn atan(self, radians: bool) -> Self::Output {
if radians { self.atan() } else { self.atan().to_degrees() }
}
}
impl TangentInverse for f64 {
type Output = f64;
fn atan(self, radians: bool) -> Self::Output {
if radians { self.atan() } else { self.atan().to_degrees() }
}
}
impl TangentInverse for DVec2 {
type Output = f64;
fn atan(self, radians: bool) -> Self::Output {
if radians { self.y.atan2(self.x) } else { self.y.atan2(self.x).to_degrees() }
}
}
/// The random function (rand) converts a seed into a random number within the specified range, inclusive of the minimum and exclusive of the maximum. The minimum and maximum values are automatically swapped if they are reversed.
#[node_macro::node(category("Math: Numeric"))]
fn random<U: num_traits::float::Float>(
_: impl Ctx,
_primary: (),
seed: u64,
#[implementations(f64, f32)]
#[default(0.)]
min: U,
#[implementations(f64, f32)]
#[default(1.)]
max: U,
) -> f64 {
let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
let result = rng.random::<f64>();
let (min, max) = if min < max { (min, max) } else { (max, min) };
let (min, max) = (min.to_f64().unwrap(), max.to_f64().unwrap());
result * (max - min) + min
}
/// Convert a number to an integer of the type u32, which may be the required type for certain node inputs. This will be removed in the future when automatic type conversion is implemented.
#[node_macro::node(name("To u32"), category("Math: Numeric"))]
fn to_u32<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U) -> u32 {
let value = U::clamp(value, U::from(0.).unwrap(), U::from(u32::MAX as f64).unwrap());
value.to_u32().unwrap()
}
/// Convert a number to an integer of the type u64, which may be the required type for certain node inputs. This will be removed in the future when automatic type conversion is implemented.
#[node_macro::node(name("To u64"), category("Math: Numeric"))]
fn to_u64<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U) -> u64 {
let value = U::clamp(value, U::from(0.).unwrap(), U::from(u64::MAX as f64).unwrap());
value.to_u64().unwrap()
}
/// Convert an integer to a decimal number of the type f64, which may be the required type for certain node inputs. This will be removed in the future when automatic type conversion is implemented.
#[node_macro::node(name("To f64"), category("Math: Numeric"))]
fn to_f64<U: num_traits::int::PrimInt>(_: impl Ctx, #[implementations(u32, u64)] value: U) -> f64 {
value.to_f64().unwrap()
}
/// The rounding function (round) maps an input value to its nearest whole number. Halfway values are rounded away from zero.
#[node_macro::node(category("Math: Numeric"))]
fn round<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U) -> U {
value.round()
}
/// The floor function (floor) reduces an input value to its nearest larger whole number, unless the input number is already whole.
#[node_macro::node(category("Math: Numeric"))]
fn floor<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U) -> U {
value.floor()
}
/// The ceiling function (ceil) increases an input value to its nearest smaller whole number, unless the input number is already whole.
#[node_macro::node(category("Math: Numeric"))]
fn ceiling<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U) -> U {
value.ceil()
}
/// The absolute value function (abs) removes the negative sign from an input value, if present.
#[node_macro::node(category("Math: Numeric"))]
fn absolute_value<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U) -> U {
value.abs()
}
/// The minimum function (min) picks the smaller of two numbers.
#[node_macro::node(category("Math: Numeric"))]
fn min<T: std::cmp::PartialOrd>(_: impl Ctx, #[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] value: T, #[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] other_value: T) -> T {
if value < other_value { value } else { other_value }
}
/// The maximum function (max) picks the larger of two numbers.
#[node_macro::node(category("Math: Numeric"))]
fn max<T: std::cmp::PartialOrd>(_: impl Ctx, #[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] value: T, #[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] other_value: T) -> T {
if value > other_value { value } else { other_value }
}
/// The clamp function (clamp) restricts a number to a specified range between a minimum and maximum value. The minimum and maximum values are automatically swapped if they are reversed.
#[node_macro::node(category("Math: Numeric"))]
fn clamp<T: std::cmp::PartialOrd>(
_: impl Ctx,
#[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] value: T,
#[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] min: T,
#[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] max: T,
) -> T {
let (min, max) = if min < max { (min, max) } else { (max, min) };
if value < min {
min
} else if value > max {
max
} else {
value
}
}
/// The equality operation (==) compares two values and returns true if they are equal, or false if they are not.
#[node_macro::node(category("Math: Logic"))]
fn equals<U: std::cmp::PartialEq<T>, T>(
_: impl Ctx,
#[implementations(f64, &f64, f32, &f32, u32, &u32, DVec2, &DVec2, &str)] value: T,
#[implementations(f64, &f64, f32, &f32, u32, &u32, DVec2, &DVec2, &str)] other_value: U,
) -> bool {
other_value == value
}
/// The inequality operation (!=) compares two values and returns true if they are not equal, or false if they are.
#[node_macro::node(category("Math: Logic"))]
fn not_equals<U: std::cmp::PartialEq<T>, T>(
_: impl Ctx,
#[implementations(f64, &f64, f32, &f32, u32, &u32, DVec2, &DVec2, &str)] value: T,
#[implementations(f64, &f64, f32, &f32, u32, &u32, DVec2, &DVec2, &str)] other_value: U,
) -> bool {
other_value != value
}
/// The less-than operation (<) compares two values and returns true if the first value is less than the second, or false if it is not.
/// If enabled with "Or Equal", the less-than-or-equal operation (<=) will be used instead.
#[node_macro::node(category("Math: Logic"))]
fn less_than<T: std::cmp::PartialOrd<T>>(
_: impl Ctx,
#[implementations(f64, &f64, f32, &f32, u32, &u32)] value: T,
#[implementations(f64, &f64, f32, &f32, u32, &u32)] other_value: T,
or_equal: bool,
) -> bool {
if or_equal { value <= other_value } else { value < other_value }
}
/// The greater-than operation (>) compares two values and returns true if the first value is greater than the second, or false if it is not.
/// If enabled with "Or Equal", the greater-than-or-equal operation (>=) will be used instead.
#[node_macro::node(category("Math: Logic"))]
fn greater_than<T: std::cmp::PartialOrd<T>>(
_: impl Ctx,
#[implementations(f64, &f64, f32, &f32, u32, &u32)] value: T,
#[implementations(f64, &f64, f32, &f32, u32, &u32)] other_value: T,
or_equal: bool,
) -> bool {
if or_equal { value >= other_value } else { value > other_value }
}
/// The logical or operation (||) returns true if either of the two inputs are true, or false if both are false.
#[node_macro::node(category("Math: Logic"))]
fn logical_or(_: impl Ctx, value: bool, other_value: bool) -> bool {
value || other_value
}
/// The logical and operation (&&) returns true if both of the two inputs are true, or false if any are false.
#[node_macro::node(category("Math: Logic"))]
fn logical_and(_: impl Ctx, value: bool, other_value: bool) -> bool {
value && other_value
}
/// The logical not operation (!) reverses true and false value of the input.
#[node_macro::node(category("Math: Logic"))]
fn logical_not(_: impl Ctx, input: bool) -> bool {
!input
}
/// Constructs a bool value which may be set to true or false.
#[node_macro::node(category("Value"))]
fn bool_value(_: impl Ctx, _primary: (), #[name("Bool")] bool_value: bool) -> bool {
bool_value
}
/// Constructs a number value which may be set to any real number.
#[node_macro::node(category("Value"))]
fn number_value(_: impl Ctx, _primary: (), number: f64) -> f64 {
number
}
/// Constructs a number value which may be set to any value from 0% to 100% by dragging the slider.
#[node_macro::node(category("Value"))]
fn percentage_value(_: impl Ctx, _primary: (), percentage: Percentage) -> f64 {
percentage
}
/// Constructs a two-dimensional vector value which may be set to any XY coordinate.
#[node_macro::node(category("Value"))]
fn coordinate_value(_: impl Ctx, _primary: (), x: f64, y: f64) -> DVec2 {
DVec2::new(x, y)
}
/// Constructs a color value which may be set to any color, or no color.
#[node_macro::node(category("Value"))]
fn color_value(_: impl Ctx, _primary: (), #[default(Color::BLACK)] color: Option<Color>) -> Option<Color> {
color
}
/// Gets the color at the specified position along the gradient, given a position from 0 (left) to 1 (right).
#[node_macro::node(category("Color"))]
fn sample_gradient(_: impl Ctx, _primary: (), gradient: GradientStops, position: Fraction) -> Color {
let position = position.clamp(0., 1.);
gradient.evaluate(position)
}
/// Constructs a gradient value which may be set to any sequence of color stops to represent the transition between colors.
#[node_macro::node(category("Value"))]
fn gradient_value(_: impl Ctx, _primary: (), gradient: GradientStops) -> GradientStops {
gradient
}
/// Constructs a string value which may be set to any plain text.
#[node_macro::node(category("Value"))]
fn string_value(_: impl Ctx, _primary: (), string: String) -> String {
string
}
/// Meant for debugging purposes, not general use. Returns the size of the input type in bytes.
#[node_macro::node(category("Debug"))]
fn size_of(_: impl Ctx, ty: crate::Type) -> Option<usize> {
ty.size()
}
/// Meant for debugging purposes, not general use. Wraps the input value in the Some variant of an Option.
#[node_macro::node(category("Debug"))]
fn some<T>(_: impl Ctx, #[implementations(f64, f32, u32, u64, String, Color)] input: T) -> Option<T> {
Some(input)
}
/// Meant for debugging purposes, not general use. Unwraps the input value from an Option, returning the default value if the input is None.
#[node_macro::node(category("Debug"))]
fn unwrap<T: Default>(_: impl Ctx, #[implementations(Option<f64>, Option<f32>, Option<u32>, Option<u64>, Option<String>, Option<Color>)] input: Option<T>) -> T {
input.unwrap_or_default()
}
/// Meant for debugging purposes, not general use. Clones the input value.
#[node_macro::node(category("Debug"))]
fn clone<'i, T: Clone + 'i>(_: impl Ctx, #[implementations(&RasterDataTable<CPU>)] value: &'i T) -> T {
value.clone()
}
#[node_macro::node(category("Math: Vector"))]
fn dot_product(_: impl Ctx, vector_a: DVec2, vector_b: DVec2) -> f64 {
vector_a.dot(vector_b)
}
/// Obtain the X or Y component of a coordinate.
#[node_macro::node(name("Extract XY"), category("Math: Vector"))]
fn extract_xy<T: Into<DVec2>>(_: impl Ctx, #[implementations(DVec2, IVec2, UVec2)] vector: T, axis: XY) -> f64 {
match axis {
XY::X => vector.into().x,
XY::Y => vector.into().y,
}
}
/// The X or Y component of a coordinate.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type, serde::Serialize, serde::Deserialize)]
#[widget(Dropdown)]
pub enum XY {
#[default]
X,
Y,
}
// TODO: Rename to "Passthrough" // TODO: Rename to "Passthrough"
/// Passes-through the input value without changing it. This is useful for rerouting wires for organization purposes. /// Passes-through the input value without changing it. This is useful for rerouting wires for organization purposes.
@@ -579,9 +73,17 @@ pub trait Convert<T>: Sized {
fn convert(self) -> T; fn convert(self) -> T;
} }
impl<T: ToString> Convert<String> for T {
/// Converts this type into a `String` using its `ToString` implementation.
#[inline]
fn convert(self) -> String {
self.to_string()
}
}
/// Implements the [`Convert`] trait for conversion between the cartesian product of Rust's primitive numeric types. /// Implements the [`Convert`] trait for conversion between the cartesian product of Rust's primitive numeric types.
macro_rules! impl_convert { macro_rules! impl_convert {
($from:ty,$to:ty) => { ($from:ty, $to:ty) => {
impl Convert<$to> for $from { impl Convert<$to> for $from {
fn convert(self) -> $to { fn convert(self) -> $to {
self as $to self as $to
@@ -644,72 +146,9 @@ impl<'input, I: 'input + Convert<_O> + Sync + Send, _O: 'input> Node<'input, I>
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use crate::generic::*;
#[test]
pub fn dot_product_function() {
let vector_a = DVec2::new(1., 2.);
let vector_b = DVec2::new(3., 4.);
assert_eq!(dot_product((), vector_a, vector_b), 11.);
}
#[test]
fn test_basic_expression() {
let result = math((), 0., "2 + 2".to_string(), 0.);
assert_eq!(result, 4.);
}
#[test]
fn test_complex_expression() {
let result = math((), 0., "(5 * 3) + (10 / 2)".to_string(), 0.);
assert_eq!(result, 20.);
}
#[test]
fn test_default_expression() {
let result = math((), 0., "0".to_string(), 0.);
assert_eq!(result, 0.);
}
#[test]
fn test_invalid_expression() {
let result = math((), 0., "invalid".to_string(), 0.);
assert_eq!(result, 0.);
}
#[test] #[test]
pub fn identity_node() { pub fn identity_node() {
assert_eq!(identity(&4), &4); assert_eq!(identity(&4), &4);
} }
#[test]
pub fn foo() {
let fnn = FnNode::new(|(a, b)| (b, a));
assert_eq!(fnn.eval((1u32, 2u32)), (2, 1));
}
#[test]
pub fn add_vectors() {
assert_eq!(super::add((), DVec2::ONE, DVec2::ONE), DVec2::ONE * 2.);
}
#[test]
pub fn subtract_f64() {
assert_eq!(super::subtract((), 5_f64, 3_f64), 2.);
}
#[test]
pub fn divide_vectors() {
assert_eq!(super::divide((), DVec2::ONE, 2_f64), DVec2::ONE / 2.);
}
#[test]
pub fn modulo_positive() {
assert_eq!(super::modulo((), -5_f64, 2_f64, true), 1_f64);
}
#[test]
pub fn modulo_negative() {
assert_eq!(super::modulo((), -5_f64, 2_f64, false), -1_f64);
}
} }
-4
View File
@@ -12,13 +12,9 @@ pub mod color {
pub use super::*; pub use super::*;
} }
pub mod adjustments;
pub mod brush_cache;
pub mod curve;
pub mod image; pub mod image;
pub use self::image::Image; pub use self::image::Image;
pub use adjustments::*;
pub trait Bitmap { pub trait Bitmap {
type Pixel: Pixel; type Pixel: Pixel;
+9 -1
View File
@@ -291,7 +291,15 @@ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) ->
*image_frame_table.instance_mut_iter().next().unwrap().alpha_blending = alpha_blending; *image_frame_table.instance_mut_iter().next().unwrap().alpha_blending = alpha_blending;
image_frame_table image_frame_table
} }
FormatVersions::ImageFrame(image_frame) => RasterDataTable::new(Raster::new_cpu(image_frame.instance_ref_iter().next().unwrap().instance.image.clone())), FormatVersions::ImageFrame(image_frame) => RasterDataTable::new(Raster::new_cpu(
image_frame
.instance_ref_iter()
.next()
.unwrap_or(Instances::new(ImageFrame::default()).instance_ref_iter().next().unwrap())
.instance
.image
.clone(),
)),
FormatVersions::ImageFrameTable(image_frame_table) => RasterDataTable::new(Raster::new_cpu(image_frame_table.instance_ref_iter().next().unwrap().instance.clone())), FormatVersions::ImageFrameTable(image_frame_table) => RasterDataTable::new(Raster::new_cpu(image_frame_table.instance_ref_iter().next().unwrap().instance.clone())),
FormatVersions::RasterDataTable(raster_data_table) => raster_data_table, FormatVersions::RasterDataTable(raster_data_table) => raster_data_table,
}) })
+23 -1
View File
@@ -57,6 +57,10 @@ impl Raster<CPU> {
let RasterStorage::Cpu(cpu) = self.data else { unreachable!() }; let RasterStorage::Cpu(cpu) = self.data else { unreachable!() };
cpu cpu
} }
pub fn is_empty(&self) -> bool {
let data = self.data();
data.height == 0 || data.width == 0
}
} }
impl Default for Raster<CPU> { impl Default for Raster<CPU> {
fn default() -> Self { fn default() -> Self {
@@ -93,6 +97,10 @@ impl Raster<GPU> {
let RasterStorage::Gpu(gpu) = &self.data else { unreachable!() }; let RasterStorage::Gpu(gpu) = &self.data else { unreachable!() };
gpu.clone() gpu.clone()
} }
pub fn is_empty(&self) -> bool {
let data = self.data();
data.width() == 0 || data.height() == 0
}
} }
#[cfg(feature = "wgpu")] #[cfg(feature = "wgpu")]
impl Deref for Raster<GPU> { impl Deref for Raster<GPU> {
@@ -104,9 +112,23 @@ impl Deref for Raster<GPU> {
} }
pub type RasterDataTable<Storage> = Instances<Raster<Storage>>; pub type RasterDataTable<Storage> = Instances<Raster<Storage>>;
impl<S: Storage> BoundingBox for RasterDataTable<S> { // TODO: Make this not dupliated
impl BoundingBox for RasterDataTable<CPU> {
fn bounding_box(&self, transform: DAffine2, _include_stroke: bool) -> Option<[DVec2; 2]> { fn bounding_box(&self, transform: DAffine2, _include_stroke: bool) -> Option<[DVec2; 2]> {
self.instance_ref_iter() self.instance_ref_iter()
.filter(|instance| !instance.instance.is_empty()) // Eliminate empty images
.flat_map(|instance| {
let transform = transform * *instance.transform;
(transform.matrix2.determinant() != 0.).then(|| (transform * Quad::from_box([DVec2::ZERO, DVec2::ONE])).bounding_box())
})
.reduce(Quad::combine_bounds)
}
}
impl BoundingBox for RasterDataTable<GPU> {
fn bounding_box(&self, transform: DAffine2, _include_stroke: bool) -> Option<[DVec2; 2]> {
self.instance_ref_iter()
.filter(|instance| !instance.instance.is_empty()) // Eliminate empty images
.flat_map(|instance| { .flat_map(|instance| {
let transform = transform * *instance.transform; let transform = transform * *instance.transform;
(transform.matrix2.determinant() != 0.).then(|| (transform * Quad::from_box([DVec2::ZERO, DVec2::ONE])).bounding_box()) (transform.matrix2.determinant() != 0.).then(|| (transform * Quad::from_box([DVec2::ZERO, DVec2::ONE])).bounding_box())
+3 -2
View File
@@ -22,11 +22,12 @@ impl Default for Font {
/// A cache of all loaded font data and preview urls along with the default font (send from `init_app` in `editor_api.rs`) /// A cache of all loaded font data and preview urls along with the default font (send from `init_app` in `editor_api.rs`)
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default, PartialEq, DynAny)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default, PartialEq, DynAny)]
pub struct FontCache { pub struct FontCache {
/// Actual font file data used for rendering a font with ttf_parser and rustybuzz /// Actual font file data used for rendering a font
font_file_data: HashMap<Font, Vec<u8>>, font_file_data: HashMap<Font, Vec<u8>>,
/// Web font preview URLs used for showing fonts when live editing /// Web font preview URLs used for showing fonts when live editing
preview_urls: HashMap<Font, String>, preview_urls: HashMap<Font, String>,
} }
impl FontCache { impl FontCache {
/// Returns the font family name if the font is cached, otherwise returns the fallback font family name if that is cached /// Returns the font family name if the font is cached, otherwise returns the fallback font family name if that is cached
pub fn resolve_font<'a>(&'a self, font: &'a Font) -> Option<&'a Font> { pub fn resolve_font<'a>(&'a self, font: &'a Font) -> Option<&'a Font> {
@@ -40,7 +41,7 @@ impl FontCache {
} }
/// Try to get the bytes for a font /// Try to get the bytes for a font
pub fn get<'a>(&'a self, font: &Font) -> Option<&'a Vec<u8>> { pub fn get(&self, font: &Font) -> Option<&Vec<u8>> {
self.resolve_font(font).and_then(|font| self.font_file_data.get(font)) self.resolve_font(font).and_then(|font| self.font_file_data.get(font))
} }
+145 -165
View File
@@ -1,29 +1,67 @@
use crate::vector::PointId; use crate::vector::PointId;
use bezier_rs::{ManipulatorGroup, Subpath}; use bezier_rs::{ManipulatorGroup, Subpath};
use glam::DVec2; use core::cell::RefCell;
use rustybuzz::ttf_parser::{GlyphId, OutlineBuilder}; use glam::{DAffine2, DVec2};
use rustybuzz::{GlyphBuffer, UnicodeBuffer}; use parley::fontique::Blob;
use parley::{Alignment, AlignmentOptions, FontContext, GlyphRun, Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty};
use skrifa::GlyphId;
use skrifa::instance::{LocationRef, NormalizedCoord, Size};
use skrifa::outline::{DrawSettings, OutlinePen};
use skrifa::raw::FontRef as ReadFontsRef;
use skrifa::{MetadataProvider, OutlineGlyph};
use std::sync::Arc;
struct Builder { // Thread-local storage avoids expensive re-initialization of font and layout contexts
// across multiple text rendering operations within the same thread
thread_local! {
static FONT_CONTEXT: RefCell<FontContext> = RefCell::new(FontContext::new());
static LAYOUT_CONTEXT: RefCell<LayoutContext<()>> = RefCell::new(LayoutContext::new());
}
struct PathBuilder {
current_subpath: Subpath<PointId>, current_subpath: Subpath<PointId>,
glyph_subpaths: Vec<Subpath<PointId>>,
other_subpaths: Vec<Subpath<PointId>>, other_subpaths: Vec<Subpath<PointId>>,
text_cursor: DVec2, origin: DVec2,
offset: DVec2,
ascender: f64,
scale: f64, scale: f64,
id: PointId, id: PointId,
} }
impl Builder { impl PathBuilder {
fn point(&self, x: f32, y: f32) -> DVec2 { fn point(&self, x: f32, y: f32) -> DVec2 {
self.text_cursor + self.offset + DVec2::new(x as f64, self.ascender - y as f64) * self.scale // Y-axis inversion converts from font coordinate system (Y-up) to graphics coordinate system (Y-down)
DVec2::new(self.origin.x + x as f64, self.origin.y - y as f64) * self.scale
}
fn set_origin(&mut self, x: f64, y: f64) {
self.origin = DVec2::new(x, y);
}
fn draw_glyph(&mut self, glyph: &OutlineGlyph<'_>, size: f32, normalized_coords: &[NormalizedCoord], style_skew: Option<DAffine2>, skew: DAffine2) {
let location_ref = LocationRef::new(normalized_coords);
let settings = DrawSettings::unhinted(Size::new(size), location_ref);
glyph.draw(settings, self).unwrap();
// Apply transforms in correct order: style-based skew first, then user-requested skew
// This ensures font synthesis (italic) is applied before user transformations
for glyph_subpath in &mut self.glyph_subpaths {
if let Some(style_skew) = style_skew {
glyph_subpath.apply_transform(style_skew);
}
glyph_subpath.apply_transform(skew);
}
if !self.glyph_subpaths.is_empty() {
self.other_subpaths.extend(core::mem::take(&mut self.glyph_subpaths));
}
} }
} }
impl OutlineBuilder for Builder { impl OutlinePen for PathBuilder {
fn move_to(&mut self, x: f32, y: f32) { fn move_to(&mut self, x: f32, y: f32) {
if !self.current_subpath.is_empty() { if !self.current_subpath.is_empty() {
self.other_subpaths.push(std::mem::replace(&mut self.current_subpath, Subpath::new(Vec::new(), false))); self.glyph_subpaths.push(std::mem::replace(&mut self.current_subpath, Subpath::new(Vec::new(), false)));
} }
self.current_subpath.push_manipulator_group(ManipulatorGroup::new_anchor_with_id(self.point(x, y), self.id.next_id())); self.current_subpath.push_manipulator_group(ManipulatorGroup::new_anchor_with_id(self.point(x, y), self.id.next_id()));
} }
@@ -47,36 +85,10 @@ impl OutlineBuilder for Builder {
fn close(&mut self) { fn close(&mut self) {
self.current_subpath.set_closed(true); self.current_subpath.set_closed(true);
self.other_subpaths.push(std::mem::replace(&mut self.current_subpath, Subpath::new(Vec::new(), false))); self.glyph_subpaths.push(std::mem::replace(&mut self.current_subpath, Subpath::new(Vec::new(), false)));
} }
} }
fn font_properties(buzz_face: &rustybuzz::Face, font_size: f64, line_height_ratio: f64) -> (f64, f64, UnicodeBuffer) {
let scale = (buzz_face.units_per_em() as f64).recip() * font_size;
let line_height = font_size * line_height_ratio;
let buffer = UnicodeBuffer::new();
(scale, line_height, buffer)
}
fn push_str(buffer: &mut UnicodeBuffer, word: &str) {
buffer.push_str(word);
}
fn wrap_word(max_width: Option<f64>, glyph_buffer: &GlyphBuffer, font_size: f64, character_spacing: f64, x_pos: f64, space_glyph: Option<GlyphId>) -> bool {
if let Some(max_width) = max_width {
// We don't word wrap spaces (to match the browser)
let all_glyphs = glyph_buffer.glyph_positions().iter().zip(glyph_buffer.glyph_infos());
let non_space_glyphs = all_glyphs.take_while(|(_, info)| space_glyph != Some(GlyphId(info.glyph_id as u16)));
let word_length: f64 = non_space_glyphs.map(|(pos, _)| pos.x_advance as f64 * character_spacing).sum();
let scaled_word_length = word_length * font_size;
if scaled_word_length + x_pos > max_width {
return true;
}
}
false
}
#[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)] #[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
pub struct TypesettingConfig { pub struct TypesettingConfig {
pub font_size: f64, pub font_size: f64,
@@ -84,6 +96,7 @@ pub struct TypesettingConfig {
pub character_spacing: f64, pub character_spacing: f64,
pub max_width: Option<f64>, pub max_width: Option<f64>,
pub max_height: Option<f64>, pub max_height: Option<f64>,
pub tilt: f64,
} }
impl Default for TypesettingConfig { impl Default for TypesettingConfig {
@@ -91,163 +104,130 @@ impl Default for TypesettingConfig {
Self { Self {
font_size: 24., font_size: 24.,
line_height_ratio: 1.2, line_height_ratio: 1.2,
character_spacing: 1., character_spacing: 0.,
max_width: None, max_width: None,
max_height: None, max_height: None,
tilt: 0.,
} }
} }
} }
pub fn to_path(str: &str, buzz_face: Option<rustybuzz::Face>, typesetting: TypesettingConfig) -> Vec<Subpath<PointId>> { fn render_glyph_run(glyph_run: &GlyphRun<'_, ()>, path_builder: &mut PathBuilder, tilt: f64) {
let Some(buzz_face) = buzz_face else { return vec![] }; let mut run_x = glyph_run.offset();
let space_glyph = buzz_face.glyph_index(' '); let run_y = glyph_run.baseline();
let (scale, line_height, mut buffer) = font_properties(&buzz_face, typesetting.font_size, typesetting.line_height_ratio); let run = glyph_run.run();
let mut builder = Builder { // User-requested tilt applied around baseline to avoid vertical displacement
// Translation ensures rotation point is at the baseline, not origin
let skew = DAffine2::from_translation(DVec2::new(0., run_y as f64))
* DAffine2::from_cols_array(&[1., 0., -tilt.to_radians().tan(), 1., 0., 0.])
* DAffine2::from_translation(DVec2::new(0., -run_y as f64));
let synthesis = run.synthesis();
// Font synthesis (e.g., synthetic italic) applied separately from user transforms
// This preserves the distinction between font styling and user transformations
let style_skew = synthesis.skew().map(|angle| {
DAffine2::from_translation(DVec2::new(0., run_y as f64))
* DAffine2::from_cols_array(&[1., 0., -angle.to_radians().tan() as f64, 1., 0., 0.])
* DAffine2::from_translation(DVec2::new(0., -run_y as f64))
});
let font = run.font();
let font_size = run.font_size();
let normalized_coords = run.normalized_coords().iter().map(|coord| NormalizedCoord::from_bits(*coord)).collect::<Vec<_>>();
// TODO: This can be cached for better performance
let font_collection_ref = font.data.as_ref();
let font_ref = ReadFontsRef::from_index(font_collection_ref, font.index).unwrap();
let outlines = font_ref.outline_glyphs();
for glyph in glyph_run.glyphs() {
let glyph_x = run_x + glyph.x;
let glyph_y = run_y - glyph.y;
run_x += glyph.advance;
let glyph_id = GlyphId::from(glyph.id);
if let Some(glyph_outline) = outlines.get(glyph_id) {
path_builder.set_origin(glyph_x as f64, glyph_y as f64);
path_builder.draw_glyph(&glyph_outline, font_size, &normalized_coords, style_skew, skew);
}
}
}
fn layout_text(str: &str, font_data: Option<Blob<u8>>, typesetting: TypesettingConfig) -> Option<Layout<()>> {
let font_cx = FONT_CONTEXT.with(Clone::clone);
let mut font_cx = font_cx.borrow_mut();
let layout_cx = LAYOUT_CONTEXT.with(Clone::clone);
let mut layout_cx = layout_cx.borrow_mut();
let font_family = font_data.and_then(|font_data| {
font_cx
.collection
.register_fonts(font_data, None)
.first()
.and_then(|(family_id, _)| font_cx.collection.family_name(*family_id).map(String::from))
})?;
const DISPLAY_SCALE: f32 = 1.;
let mut builder = layout_cx.ranged_builder(&mut font_cx, str, DISPLAY_SCALE, true);
builder.push_default(StyleProperty::FontSize(typesetting.font_size as f32));
builder.push_default(StyleProperty::LetterSpacing(typesetting.character_spacing as f32));
builder.push_default(StyleProperty::FontStack(parley::FontStack::Single(parley::FontFamily::Named(std::borrow::Cow::Owned(font_family)))));
builder.push_default(LineHeight::FontSizeRelative(typesetting.line_height_ratio as f32));
let mut layout: Layout<()> = builder.build(str);
layout.break_all_lines(typesetting.max_width.map(|mw| mw as f32));
layout.align(typesetting.max_width.map(|max_w| max_w as f32), Alignment::Left, AlignmentOptions::default());
Some(layout)
}
pub fn to_path(str: &str, font_data: Option<Blob<u8>>, typesetting: TypesettingConfig) -> Vec<Subpath<PointId>> {
let Some(layout) = layout_text(str, font_data, typesetting) else { return Vec::new() };
let mut path_builder = PathBuilder {
current_subpath: Subpath::new(Vec::new(), false), current_subpath: Subpath::new(Vec::new(), false),
glyph_subpaths: Vec::new(),
other_subpaths: Vec::new(), other_subpaths: Vec::new(),
text_cursor: DVec2::ZERO, origin: DVec2::ZERO,
offset: DVec2::ZERO, scale: layout.scale() as f64,
ascender: (buzz_face.ascender() as f64 / buzz_face.height() as f64) * typesetting.font_size / scale,
scale,
id: PointId::ZERO, id: PointId::ZERO,
}; };
for line in str.split('\n') { for line in layout.lines() {
for (index, word) in SplitWordsIncludingSpaces::new(line).enumerate() { for item in line.items() {
push_str(&mut buffer, word); if let PositionedLayoutItem::GlyphRun(glyph_run) = item {
let glyph_buffer = rustybuzz::shape(&buzz_face, &[], buffer); render_glyph_run(&glyph_run, &mut path_builder, typesetting.tilt);
// Don't wrap the first word
if index != 0 && wrap_word(typesetting.max_width, &glyph_buffer, scale, typesetting.character_spacing, builder.text_cursor.x, space_glyph) {
builder.text_cursor = DVec2::new(0., builder.text_cursor.y + line_height);
} }
for (glyph_position, glyph_info) in glyph_buffer.glyph_positions().iter().zip(glyph_buffer.glyph_infos()) {
let glyph_id = GlyphId(glyph_info.glyph_id as u16);
if let Some(max_width) = typesetting.max_width {
if space_glyph != Some(glyph_id) && builder.text_cursor.x + (glyph_position.x_advance as f64 * builder.scale * typesetting.character_spacing) >= max_width {
builder.text_cursor = DVec2::new(0., builder.text_cursor.y + line_height);
}
}
// Clip when the height is exceeded
if typesetting.max_height.is_some_and(|max_height| builder.text_cursor.y > max_height - line_height) {
return builder.other_subpaths;
}
builder.offset = DVec2::new(glyph_position.x_offset as f64, glyph_position.y_offset as f64) * builder.scale;
buzz_face.outline_glyph(glyph_id, &mut builder);
if !builder.current_subpath.is_empty() {
builder.other_subpaths.push(std::mem::replace(&mut builder.current_subpath, Subpath::new(Vec::new(), false)));
}
builder.text_cursor += DVec2::new(glyph_position.x_advance as f64 * typesetting.character_spacing, glyph_position.y_advance as f64) * builder.scale;
}
buffer = glyph_buffer.clear();
} }
builder.text_cursor = DVec2::new(0., builder.text_cursor.y + line_height);
} }
builder.other_subpaths path_builder.other_subpaths
} }
pub fn bounding_box(str: &str, buzz_face: Option<&rustybuzz::Face>, typesetting: TypesettingConfig, for_clipping_test: bool) -> DVec2 { pub fn bounding_box(str: &str, font_data: Option<Blob<u8>>, typesetting: TypesettingConfig, for_clipping_test: bool) -> DVec2 {
// Show blank layer if font has not loaded
let Some(buzz_face) = buzz_face else { return DVec2::ZERO };
let space_glyph = buzz_face.glyph_index(' ');
let (scale, line_height, mut buffer) = font_properties(buzz_face, typesetting.font_size, typesetting.line_height_ratio);
let [mut text_cursor, mut bounds] = [DVec2::ZERO; 2];
if !for_clipping_test { if !for_clipping_test {
if let (Some(max_height), Some(max_width)) = (typesetting.max_height, typesetting.max_width) { if let (Some(max_height), Some(max_width)) = (typesetting.max_height, typesetting.max_width) {
return DVec2::new(max_width, max_height); return DVec2::new(max_width, max_height);
} }
} }
for line in str.split('\n') { let Some(layout) = layout_text(str, font_data, typesetting) else { return DVec2::ZERO };
for (index, word) in SplitWordsIncludingSpaces::new(line).enumerate() {
push_str(&mut buffer, word);
let glyph_buffer = rustybuzz::shape(buzz_face, &[], buffer); DVec2::new(layout.full_width() as f64, layout.height() as f64)
// Don't wrap the first word
if index != 0 && wrap_word(typesetting.max_width, &glyph_buffer, scale, typesetting.character_spacing, text_cursor.x, space_glyph) {
text_cursor = DVec2::new(0., text_cursor.y + line_height);
}
for (glyph_position, glyph_info) in glyph_buffer.glyph_positions().iter().zip(glyph_buffer.glyph_infos()) {
let glyph_id = GlyphId(glyph_info.glyph_id as u16);
if let Some(max_width) = typesetting.max_width {
if space_glyph != Some(glyph_id) && text_cursor.x + (glyph_position.x_advance as f64 * scale * typesetting.character_spacing) >= max_width {
text_cursor = DVec2::new(0., text_cursor.y + line_height);
}
}
text_cursor += DVec2::new(glyph_position.x_advance as f64 * typesetting.character_spacing, glyph_position.y_advance as f64) * scale;
bounds = bounds.max(text_cursor + DVec2::new(0., line_height));
}
buffer = glyph_buffer.clear();
}
text_cursor = DVec2::new(0., text_cursor.y + line_height);
bounds = bounds.max(text_cursor);
}
if !for_clipping_test {
if let Some(max_width) = typesetting.max_width {
bounds.x = max_width;
}
if let Some(max_height) = typesetting.max_height {
bounds.y = max_height;
}
}
bounds
} }
pub fn load_face(data: &[u8]) -> rustybuzz::Face<'_> { pub fn load_font(data: &[u8]) -> Blob<u8> {
rustybuzz::Face::from_slice(data, 0).expect("Loading font failed") Blob::new(Arc::new(data.to_vec()))
} }
pub fn lines_clipping(str: &str, buzz_face: Option<rustybuzz::Face>, typesetting: TypesettingConfig) -> bool { pub fn lines_clipping(str: &str, font_data: Option<Blob<u8>>, typesetting: TypesettingConfig) -> bool {
let Some(max_height) = typesetting.max_height else { return false }; let Some(max_height) = typesetting.max_height else { return false };
let bounds = bounding_box(str, buzz_face.as_ref(), typesetting, true); let bounds = bounding_box(str, font_data, typesetting, true);
max_height < bounds.y max_height < bounds.y
} }
struct SplitWordsIncludingSpaces<'a> {
text: &'a str,
start_byte: usize,
}
impl<'a> SplitWordsIncludingSpaces<'a> {
pub fn new(text: &'a str) -> Self {
Self { text, start_byte: 0 }
}
}
impl<'a> Iterator for SplitWordsIncludingSpaces<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<Self::Item> {
let mut eaten_chars = self.text[self.start_byte..].char_indices().skip_while(|(_, c)| *c != ' ').skip_while(|(_, c)| *c == ' ');
let start_byte = self.start_byte;
self.start_byte = eaten_chars.next().map_or(self.text.len(), |(offset, _)| self.start_byte + offset);
(self.start_byte > start_byte).then(|| self.text.get(start_byte..self.start_byte)).flatten()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn split_words_including_spaces() {
let mut split_words = SplitWordsIncludingSpaces::new("hello world .");
assert_eq!(split_words.next(), Some("hello "));
assert_eq!(split_words.next(), Some("world "));
assert_eq!(split_words.next(), Some("."));
assert_eq!(split_words.next(), None);
}
}
+3 -2
View File
@@ -19,10 +19,10 @@ async fn transform<T: 'n + 'static>(
translate: DVec2, translate: DVec2,
rotate: f64, rotate: f64,
scale: DVec2, scale: DVec2,
shear: DVec2, skew: DVec2,
_pivot: DVec2, _pivot: DVec2,
) -> Instances<T> { ) -> Instances<T> {
let matrix = DAffine2::from_scale_angle_translation(scale, rotate, translate) * DAffine2::from_cols_array(&[1., shear.y, shear.x, 1., 0., 0.]); let matrix = DAffine2::from_scale_angle_translation(scale, rotate, translate) * DAffine2::from_cols_array(&[1., skew.y, skew.x, 1., 0., 0.]);
let footprint = ctx.try_footprint().copied(); let footprint = ctx.try_footprint().copied();
@@ -70,6 +70,7 @@ async fn boundless_footprint<T: 'n + 'static>(
transform_target.eval(ctx.into_context()).await transform_target.eval(ctx.into_context()).await
} }
#[node_macro::node(category("Debug"))] #[node_macro::node(category("Debug"))]
async fn freeze_real_time<T: 'n + 'static>( async fn freeze_real_time<T: 'n + 'static>(
ctx: impl Ctx + CloneVarArgs + ExtractAll, ctx: impl Ctx + CloneVarArgs + ExtractAll,
@@ -86,6 +86,7 @@ async fn instance_position(ctx: impl Ctx + ExtractVarArgs) -> DVec2 {
Default::default() Default::default()
} }
// TODO: Make this return a u32 instead of an f64, but we ned to improve math-related compatibility with integer types first.
#[node_macro::node(category("Instancing"), path(graphene_core::vector))] #[node_macro::node(category("Instancing"), path(graphene_core::vector))]
async fn instance_index(ctx: impl Ctx + ExtractIndex) -> f64 { async fn instance_index(ctx: impl Ctx + ExtractIndex) -> f64 {
match ctx.try_index() { match ctx.try_index() {
@@ -99,7 +100,7 @@ async fn instance_index(ctx: impl Ctx + ExtractIndex) -> f64 {
mod test { mod test {
use super::*; use super::*;
use crate::Node; use crate::Node;
use crate::ops::ExtractXyNode; use crate::extract_xy::{ExtractXyNode, XY};
use crate::vector::VectorData; use crate::vector::VectorData;
use bezier_rs::Subpath; use bezier_rs::Subpath;
use glam::DVec2; use glam::DVec2;
@@ -121,7 +122,7 @@ mod test {
let owned = OwnedContextImpl::default().into_context(); let owned = OwnedContextImpl::default().into_context();
let rect = crate::vector::generator_nodes::RectangleNode::new( let rect = crate::vector::generator_nodes::RectangleNode::new(
FutureWrapperNode(()), FutureWrapperNode(()),
ExtractXyNode::new(InstancePositionNode {}, FutureWrapperNode(crate::ops::XY::Y)), ExtractXyNode::new(InstancePositionNode {}, FutureWrapperNode(XY::Y)),
FutureWrapperNode(2_f64), FutureWrapperNode(2_f64),
FutureWrapperNode(false), FutureWrapperNode(false),
FutureWrapperNode(0_f64), FutureWrapperNode(0_f64),
+42 -14
View File
@@ -37,7 +37,13 @@ impl CornerRadius for [f64; 4] {
} }
#[node_macro::node(category("Vector: Shape"))] #[node_macro::node(category("Vector: Shape"))]
fn circle(_: impl Ctx, _primary: (), #[default(50.)] radius: f64) -> VectorDataTable { fn circle(
_: impl Ctx,
_primary: (),
#[unit(" px")]
#[default(50.)]
radius: f64,
) -> VectorDataTable {
let radius = radius.abs(); let radius = radius.abs();
VectorDataTable::new(VectorData::from_subpath(Subpath::new_ellipse(DVec2::splat(-radius), DVec2::splat(radius)))) VectorDataTable::new(VectorData::from_subpath(Subpath::new_ellipse(DVec2::splat(-radius), DVec2::splat(radius))))
} }
@@ -46,7 +52,9 @@ fn circle(_: impl Ctx, _primary: (), #[default(50.)] radius: f64) -> VectorDataT
fn arc( fn arc(
_: impl Ctx, _: impl Ctx,
_primary: (), _primary: (),
#[default(50.)] radius: f64, #[unit(" px")]
#[default(50.)]
radius: f64,
start_angle: Angle, start_angle: Angle,
#[default(270.)] #[default(270.)]
#[range((0., 360.))] #[range((0., 360.))]
@@ -66,7 +74,16 @@ fn arc(
} }
#[node_macro::node(category("Vector: Shape"))] #[node_macro::node(category("Vector: Shape"))]
fn ellipse(_: impl Ctx, _primary: (), #[default(50)] radius_x: f64, #[default(25)] radius_y: f64) -> VectorDataTable { fn ellipse(
_: impl Ctx,
_primary: (),
#[unit(" px")]
#[default(50)]
radius_x: f64,
#[unit(" px")]
#[default(25)]
radius_y: f64,
) -> VectorDataTable {
let radius = DVec2::new(radius_x, radius_y); let radius = DVec2::new(radius_x, radius_y);
let corner1 = -radius; let corner1 = -radius;
let corner2 = radius; let corner2 = radius;
@@ -87,8 +104,12 @@ fn ellipse(_: impl Ctx, _primary: (), #[default(50)] radius_x: f64, #[default(25
fn rectangle<T: CornerRadius>( fn rectangle<T: CornerRadius>(
_: impl Ctx, _: impl Ctx,
_primary: (), _primary: (),
#[default(100)] width: f64, #[unit(" px")]
#[default(100)] height: f64, #[default(100)]
width: f64,
#[unit(" px")]
#[default(100)]
height: f64,
_individual_corner_radii: bool, // TODO: Move this to the bottom once we have a migration capability _individual_corner_radii: bool, // TODO: Move this to the bottom once we have a migration capability
#[implementations(f64, [f64; 4])] corner_radius: T, #[implementations(f64, [f64; 4])] corner_radius: T,
#[default(true)] clamped: bool, #[default(true)] clamped: bool,
@@ -104,7 +125,9 @@ fn regular_polygon<T: AsU64>(
#[hard_min(3.)] #[hard_min(3.)]
#[implementations(u32, u64, f64)] #[implementations(u32, u64, f64)]
sides: T, sides: T,
#[default(50)] radius: f64, #[unit(" px")]
#[default(50)]
radius: f64,
) -> VectorDataTable { ) -> VectorDataTable {
let points = sides.as_u64(); let points = sides.as_u64();
let radius: f64 = radius * 2.; let radius: f64 = radius * 2.;
@@ -119,8 +142,12 @@ fn star<T: AsU64>(
#[hard_min(2.)] #[hard_min(2.)]
#[implementations(u32, u64, f64)] #[implementations(u32, u64, f64)]
sides: T, sides: T,
#[default(50)] radius_1: f64, #[unit(" px")]
#[default(25)] radius_2: f64, #[default(50)]
radius_1: f64,
#[unit(" px")]
#[default(25)]
radius_2: f64,
) -> VectorDataTable { ) -> VectorDataTable {
let points = sides.as_u64(); let points = sides.as_u64();
let diameter: f64 = radius_1 * 2.; let diameter: f64 = radius_1 * 2.;
@@ -130,7 +157,7 @@ fn star<T: AsU64>(
} }
#[node_macro::node(category("Vector: Shape"))] #[node_macro::node(category("Vector: Shape"))]
fn line(_: impl Ctx, _primary: (), #[default((0., -50.))] start: PixelSize, #[default((0., 50.))] end: PixelSize) -> VectorDataTable { fn line(_: impl Ctx, _primary: (), #[default(0., 0.)] start: PixelSize, #[default(100., 100.)] end: PixelSize) -> VectorDataTable {
VectorDataTable::new(VectorData::from_subpath(Subpath::new_line(start, end))) VectorDataTable::new(VectorData::from_subpath(Subpath::new_line(start, end)))
} }
@@ -153,13 +180,14 @@ fn grid<T: GridSpacing>(
_: impl Ctx, _: impl Ctx,
_primary: (), _primary: (),
grid_type: GridType, grid_type: GridType,
#[unit(" px")]
#[hard_min(0.)] #[hard_min(0.)]
#[default(10)] #[default(10)]
#[implementations(f64, DVec2)] #[implementations(f64, DVec2)]
spacing: T, spacing: T,
#[default(30., 30.)] angles: DVec2,
#[default(10)] columns: u32, #[default(10)] columns: u32,
#[default(10)] rows: u32, #[default(10)] rows: u32,
#[default(30., 30.)] angles: DVec2,
) -> VectorDataTable { ) -> VectorDataTable {
let (x_spacing, y_spacing) = spacing.as_dvec2().into(); let (x_spacing, y_spacing) = spacing.as_dvec2().into();
let (angle_a, angle_b) = angles.into(); let (angle_a, angle_b) = angles.into();
@@ -251,11 +279,11 @@ mod tests {
#[test] #[test]
fn isometric_grid_test() { fn isometric_grid_test() {
// Doesn't crash with weird angles // Doesn't crash with weird angles
grid((), (), GridType::Isometric, 0., (0., 0.).into(), 5, 5); grid((), (), GridType::Isometric, 0., 5, 5, (0., 0.).into());
grid((), (), GridType::Isometric, 90., (90., 90.).into(), 5, 5); grid((), (), GridType::Isometric, 90., 5, 5, (90., 90.).into());
// Works properly // Works properly
let grid = grid((), (), GridType::Isometric, 10., (30., 30.).into(), 5, 5); let grid = grid((), (), GridType::Isometric, 10., 5, 5, (30., 30.).into());
assert_eq!(grid.instance_ref_iter().next().unwrap().instance.point_domain.ids().len(), 5 * 5); assert_eq!(grid.instance_ref_iter().next().unwrap().instance.point_domain.ids().len(), 5 * 5);
assert_eq!(grid.instance_ref_iter().next().unwrap().instance.segment_bezier_iter().count(), 4 * 5 + 4 * 9); assert_eq!(grid.instance_ref_iter().next().unwrap().instance.segment_bezier_iter().count(), 4 * 5 + 4 * 9);
for (_, bezier, _, _) in grid.instance_ref_iter().next().unwrap().instance.segment_bezier_iter() { for (_, bezier, _, _) in grid.instance_ref_iter().next().unwrap().instance.segment_bezier_iter() {
@@ -270,7 +298,7 @@ mod tests {
#[test] #[test]
fn skew_isometric_grid_test() { fn skew_isometric_grid_test() {
let grid = grid((), (), GridType::Isometric, 10., (40., 30.).into(), 5, 5); let grid = grid((), (), GridType::Isometric, 10., 5, 5, (40., 30.).into());
assert_eq!(grid.instance_ref_iter().next().unwrap().instance.point_domain.ids().len(), 5 * 5); assert_eq!(grid.instance_ref_iter().next().unwrap().instance.point_domain.ids().len(), 5 * 5);
assert_eq!(grid.instance_ref_iter().next().unwrap().instance.segment_bezier_iter().count(), 4 * 5 + 4 * 9); assert_eq!(grid.instance_ref_iter().next().unwrap().instance.segment_bezier_iter().count(), 4 * 5 + 4 * 9);
for (_, bezier, _, _) in grid.instance_ref_iter().next().unwrap().instance.segment_bezier_iter() { for (_, bezier, _, _) in grid.instance_ref_iter().next().unwrap().instance.segment_bezier_iter() {
-1
View File
@@ -1,5 +1,4 @@
pub mod algorithms; pub mod algorithms;
pub mod brush_stroke;
pub mod click_target; pub mod click_target;
pub mod generator_nodes; pub mod generator_nodes;
pub mod misc; pub mod misc;
+10 -2
View File
@@ -11,13 +11,14 @@ use crate::transform::Transform;
use crate::vector::click_target::{ClickTargetType, FreePoint}; use crate::vector::click_target::{ClickTargetType, FreePoint};
use crate::{AlphaBlending, Color, GraphicGroupTable}; use crate::{AlphaBlending, Color, GraphicGroupTable};
pub use attributes::*; pub use attributes::*;
use bezier_rs::ManipulatorGroup; use bezier_rs::{BezierHandles, ManipulatorGroup};
use core::borrow::Borrow;
use core::hash::Hash;
use dyn_any::DynAny; use dyn_any::DynAny;
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
pub use indexed::VectorDataIndex; pub use indexed::VectorDataIndex;
use kurbo::{Affine, Rect, Shape}; use kurbo::{Affine, Rect, Shape};
pub use modification::*; pub use modification::*;
use std::borrow::Borrow;
use std::collections::HashMap; use std::collections::HashMap;
// TODO: Eventually remove this migration document upgrade code // TODO: Eventually remove this migration document upgrade code
@@ -333,6 +334,13 @@ impl VectorData {
index.flat_map(|index| self.segment_domain.connected_points(index).map(|index| self.point_domain.ids()[index])) index.flat_map(|index| self.segment_domain.connected_points(index).map(|index| self.point_domain.ids()[index]))
} }
/// Returns the number of linear segments connected to the given point.
pub fn connected_linear_segments(&self, point_id: PointId) -> usize {
self.segment_bezier_iter()
.filter(|(_, bez, start, end)| ((*start == point_id || *end == point_id) && matches!(bez.handles, BezierHandles::Linear)))
.count()
}
/// Get an array slice of all segment IDs. /// Get an array slice of all segment IDs.
pub fn segment_ids(&self) -> &[SegmentId] { pub fn segment_ids(&self) -> &[SegmentId] {
self.segment_domain.ids() self.segment_domain.ids()
+23 -10
View File
@@ -65,6 +65,7 @@ async fn assign_colors<T>(
randomize: bool, randomize: bool,
#[widget(ParsedWidgetOverride::Custom = "assign_colors_seed")] #[widget(ParsedWidgetOverride::Custom = "assign_colors_seed")]
/// The seed used for randomization. /// The seed used for randomization.
/// Seed to determine unique variations on the randomized color selection.
seed: SeedValue, seed: SeedValue,
#[widget(ParsedWidgetOverride::Custom = "assign_colors_repeat_every")] #[widget(ParsedWidgetOverride::Custom = "assign_colors_repeat_every")]
/// The number of elements to span across the gradient before repeating. A 0 value will span the entire gradient once. /// The number of elements to span across the gradient before repeating. A 0 value will span the entire gradient once.
@@ -165,6 +166,7 @@ async fn stroke<C: Into<Option<Color>> + 'n + Send, V>(
#[default(Color::BLACK)] #[default(Color::BLACK)]
/// The stroke color. /// The stroke color.
color: C, color: C,
#[unit(" px")]
#[default(2.)] #[default(2.)]
/// The stroke weight. /// The stroke weight.
weight: f64, weight: f64,
@@ -183,6 +185,7 @@ async fn stroke<C: Into<Option<Color>> + 'n + Send, V>(
/// The stroke dash lengths. Each length forms a distance in a pattern where the first length is a dash, the second is a gap, and so on. If the list is an odd length, the pattern repeats with solid-gap roles reversed. /// The stroke dash lengths. Each length forms a distance in a pattern where the first length is a dash, the second is a gap, and so on. If the list is an odd length, the pattern repeats with solid-gap roles reversed.
dash_lengths: Vec<f64>, dash_lengths: Vec<f64>,
/// The phase offset distance from the starting point of the dash pattern. /// The phase offset distance from the starting point of the dash pattern.
#[unit(" px")]
dash_offset: f64, dash_offset: f64,
) -> Instances<V> ) -> Instances<V>
where where
@@ -253,7 +256,9 @@ async fn circular_repeat<I: 'n + Send + Clone>(
// TODO: Implement other GraphicElementRendered types. // TODO: Implement other GraphicElementRendered types.
#[implementations(GraphicGroupTable, VectorDataTable, RasterDataTable<CPU>)] instance: Instances<I>, #[implementations(GraphicGroupTable, VectorDataTable, RasterDataTable<CPU>)] instance: Instances<I>,
angle_offset: Angle, angle_offset: Angle,
#[default(5)] radius: f64, #[unit(" px")]
#[default(5)]
radius: f64,
#[default(5)] instances: IntegerCount, #[default(5)] instances: IntegerCount,
) -> Instances<I> { ) -> Instances<I> {
let count = instances.max(1); let count = instances.max(1);
@@ -363,7 +368,7 @@ async fn mirror<I: 'n + Send + Clone>(
_: impl Ctx, _: impl Ctx,
#[implementations(GraphicGroupTable, VectorDataTable, RasterDataTable<CPU>)] instance: Instances<I>, #[implementations(GraphicGroupTable, VectorDataTable, RasterDataTable<CPU>)] instance: Instances<I>,
#[default(ReferencePoint::Center)] relative_to_bounds: ReferencePoint, #[default(ReferencePoint::Center)] relative_to_bounds: ReferencePoint,
offset: f64, #[unit(" px")] offset: f64,
#[range((-90., 90.))] angle: Angle, #[range((-90., 90.))] angle: Angle,
#[default(true)] keep_original: bool, #[default(true)] keep_original: bool,
) -> Instances<I> ) -> Instances<I>
@@ -1139,10 +1144,10 @@ async fn sample_polyline(
_: impl Ctx, _: impl Ctx,
vector_data: VectorDataTable, vector_data: VectorDataTable,
spacing: PointSpacingType, spacing: PointSpacingType,
separation: f64, #[unit(" px")] separation: f64,
quantity: f64, quantity: u32,
start_offset: f64, #[unit(" px")] start_offset: f64,
stop_offset: f64, #[unit(" px")] stop_offset: f64,
adaptive_spacing: bool, adaptive_spacing: bool,
subpath_segment_lengths: Vec<f64>, subpath_segment_lengths: Vec<f64>,
) -> VectorDataTable { ) -> VectorDataTable {
@@ -1182,7 +1187,7 @@ async fn sample_polyline(
let amount = match spacing { let amount = match spacing {
PointSpacingType::Separation => separation, PointSpacingType::Separation => separation,
PointSpacingType::Quantity => quantity, PointSpacingType::Quantity => quantity as f64,
}; };
let Some(mut sample_bezpath) = sample_polyline_on_bezpath(bezpath, spacing, amount, start_offset, stop_offset, adaptive_spacing, current_bezpath_segments_length) else { let Some(mut sample_bezpath) = sample_polyline_on_bezpath(bezpath, spacing, amount, start_offset, stop_offset, adaptive_spacing, current_bezpath_segments_length) else {
continue; continue;
@@ -1388,6 +1393,7 @@ async fn tangent_on_path(
async fn poisson_disk_points( async fn poisson_disk_points(
_: impl Ctx, _: impl Ctx,
vector_data: VectorDataTable, vector_data: VectorDataTable,
#[unit(" px")]
#[default(10.)] #[default(10.)]
#[hard_min(0.01)] #[hard_min(0.01)]
separation_disk_diameter: f64, separation_disk_diameter: f64,
@@ -1498,7 +1504,14 @@ async fn spline(_: impl Ctx, vector_data: VectorDataTable) -> VectorDataTable {
} }
#[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector))] #[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector))]
async fn jitter_points(_: impl Ctx, vector_data: VectorDataTable, #[default(5.)] amount: f64, seed: SeedValue) -> VectorDataTable { async fn jitter_points(
_: impl Ctx,
vector_data: VectorDataTable,
#[unit(" px")]
#[default(5.)]
amount: f64,
seed: SeedValue,
) -> VectorDataTable {
let mut result_table = VectorDataTable::default(); let mut result_table = VectorDataTable::default();
for mut vector_data_instance in vector_data.instance_iter() { for mut vector_data_instance in vector_data.instance_iter() {
@@ -2080,7 +2093,7 @@ mod test {
#[tokio::test] #[tokio::test]
async fn sample_polyline() { async fn sample_polyline() {
let path = Subpath::from_bezier(&Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::ZERO, DVec2::X * 100., DVec2::X * 100.)); let path = Subpath::from_bezier(&Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::ZERO, DVec2::X * 100., DVec2::X * 100.));
let sample_polyline = super::sample_polyline(Footprint::default(), vector_node(path), PointSpacingType::Separation, 30., 0., 0., 0., false, vec![100.]).await; let sample_polyline = super::sample_polyline(Footprint::default(), vector_node(path), PointSpacingType::Separation, 30., 0, 0., 0., false, vec![100.]).await;
let sample_polyline = sample_polyline.instance_ref_iter().next().unwrap().instance; let sample_polyline = sample_polyline.instance_ref_iter().next().unwrap().instance;
assert_eq!(sample_polyline.point_domain.positions().len(), 4); assert_eq!(sample_polyline.point_domain.positions().len(), 4);
for (pos, expected) in sample_polyline.point_domain.positions().iter().zip([DVec2::X * 0., DVec2::X * 30., DVec2::X * 60., DVec2::X * 90.]) { for (pos, expected) in sample_polyline.point_domain.positions().iter().zip([DVec2::X * 0., DVec2::X * 30., DVec2::X * 60., DVec2::X * 90.]) {
@@ -2090,7 +2103,7 @@ mod test {
#[tokio::test] #[tokio::test]
async fn sample_polyline_adaptive_spacing() { async fn sample_polyline_adaptive_spacing() {
let path = Subpath::from_bezier(&Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::ZERO, DVec2::X * 100., DVec2::X * 100.)); let path = Subpath::from_bezier(&Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::ZERO, DVec2::X * 100., DVec2::X * 100.));
let sample_polyline = super::sample_polyline(Footprint::default(), vector_node(path), PointSpacingType::Separation, 18., 0., 45., 10., true, vec![100.]).await; let sample_polyline = super::sample_polyline(Footprint::default(), vector_node(path), PointSpacingType::Separation, 18., 0, 45., 10., true, vec![100.]).await;
let sample_polyline = sample_polyline.instance_ref_iter().next().unwrap().instance; let sample_polyline = sample_polyline.instance_ref_iter().next().unwrap().instance;
assert_eq!(sample_polyline.point_domain.positions().len(), 4); assert_eq!(sample_polyline.point_domain.positions().len(), 4);
for (pos, expected) in sample_polyline.point_domain.positions().iter().zip([DVec2::X * 45., DVec2::X * 60., DVec2::X * 75., DVec2::X * 90.]) { for (pos, expected) in sample_polyline.point_domain.positions().iter().zip([DVec2::X * 45., DVec2::X * 60., DVec2::X * 75., DVec2::X * 90.]) {
+17
View File
@@ -0,0 +1,17 @@
[package]
name = "graphene-math-nodes"
version = "0.1.0"
edition = "2024"
description = "graphene math nodes"
authors = ["Graphite Authors <contact@graphite.rs>"]
license = "MIT OR Apache-2.0"
[dependencies]
graphene-core = { workspace = true }
node-macro = { workspace = true }
# Workspace dependencies
glam = { workspace = true }
rand = { workspace = true }
math-parser = { workspace = true }
log = { workspace = true }
+682
View File
@@ -0,0 +1,682 @@
use glam::DVec2;
use graphene_core::gradient::GradientStops;
use graphene_core::registry::types::{Fraction, Percentage};
use graphene_core::{Color, Ctx, num_traits};
use log::warn;
use math_parser::ast;
use math_parser::context::{EvalContext, NothingMap, ValueProvider};
use math_parser::value::{Number, Value};
use num_traits::Pow;
use rand::{Rng, SeedableRng};
use std::ops::{Add, Div, Mul, Rem, Sub};
/// The struct that stores the context for the maths parser.
/// This is currently just limited to supplying `a` and `b` until we add better node graph support and UI for variadic inputs.
struct MathNodeContext {
a: f64,
b: f64,
}
impl ValueProvider for MathNodeContext {
fn get_value(&self, name: &str) -> Option<Value> {
if name.eq_ignore_ascii_case("a") {
Some(Value::from_f64(self.a))
} else if name.eq_ignore_ascii_case("b") {
Some(Value::from_f64(self.b))
} else {
None
}
}
}
/// Calculates a mathematical expression with input values "A" and "B"
#[node_macro::node(category("Math: Arithmetic"), properties("math_properties"))]
fn math<U: num_traits::float::Float>(
_: impl Ctx,
/// The value of "A" when calculating the expression
#[implementations(f64, f32)]
operand_a: U,
/// A math expression that may incorporate "A" and/or "B", such as "sqrt(A + B) - B^2"
#[default(A + B)]
expression: String,
/// The value of "B" when calculating the expression
#[implementations(f64, f32)]
#[default(1.)]
operand_b: U,
) -> U {
let (node, _unit) = match ast::Node::try_parse_from_str(&expression) {
Ok(expr) => expr,
Err(e) => {
warn!("Invalid expression: `{expression}`\n{e:?}");
return U::from(0.).unwrap();
}
};
let context = EvalContext::new(
MathNodeContext {
a: operand_a.to_f64().unwrap(),
b: operand_b.to_f64().unwrap(),
},
NothingMap,
);
let value = match node.eval(&context) {
Ok(value) => value,
Err(e) => {
warn!("Expression evaluation error: {e:?}");
return U::from(0.).unwrap();
}
};
let Value::Number(num) = value;
match num {
Number::Real(val) => U::from(val).unwrap(),
Number::Complex(c) => U::from(c.re).unwrap(),
}
}
/// The addition operation (+) calculates the sum of two numbers.
#[node_macro::node(category("Math: Arithmetic"))]
fn add<U: Add<T>, T>(
_: impl Ctx,
/// The left-hand side of the addition operation.
#[implementations(f64, f32, u32, DVec2, f64, DVec2)]
augend: U,
/// The right-hand side of the addition operation.
#[implementations(f64, f32, u32, DVec2, DVec2, f64)]
addend: T,
) -> <U as Add<T>>::Output {
augend + addend
}
/// The subtraction operation (-) calculates the difference between two numbers.
#[node_macro::node(category("Math: Arithmetic"))]
fn subtract<U: Sub<T>, T>(
_: impl Ctx,
/// The left-hand side of the subtraction operation.
#[implementations(f64, f32, u32, DVec2, f64, DVec2)]
minuend: U,
/// The right-hand side of the subtraction operation.
#[implementations(f64, f32, u32, DVec2, DVec2, f64)]
subtrahend: T,
) -> <U as Sub<T>>::Output {
minuend - subtrahend
}
/// The multiplication operation (×) calculates the product of two numbers.
#[node_macro::node(category("Math: Arithmetic"))]
fn multiply<U: Mul<T>, T>(
_: impl Ctx,
/// The left-hand side of the multiplication operation.
#[implementations(f64, f32, u32, DVec2, f64, DVec2)]
multiplier: U,
/// The right-hand side of the multiplication operation.
#[default(1.)]
#[implementations(f64, f32, u32, DVec2, DVec2, f64)]
multiplicand: T,
) -> <U as Mul<T>>::Output {
multiplier * multiplicand
}
/// The division operation (÷) calculates the quotient of two numbers.
///
/// Produces 0 if the denominator is 0.
#[node_macro::node(category("Math: Arithmetic"))]
fn divide<U: Div<T> + Default + PartialEq, T: Default + PartialEq>(
_: impl Ctx,
/// The left-hand side of the division operation.
#[implementations(f64, f64, f32, f32, u32, u32, DVec2, DVec2, f64)]
numerator: U,
/// The right-hand side of the division operation.
#[default(1.)]
#[implementations(f64, f64, f32, f32, u32, u32, DVec2, f64, DVec2)]
denominator: T,
) -> <U as Div<T>>::Output
where
<U as Div<T>>::Output: Default,
{
if denominator == T::default() {
return <U as Div<T>>::Output::default();
}
numerator / denominator
}
/// The modulo operation (%) calculates the remainder from the division of two numbers. The sign of the result shares the sign of the numerator unless "Always Positive" is enabled.
#[node_macro::node(category("Math: Arithmetic"))]
fn modulo<U: Rem<T, Output: Add<T, Output: Rem<T, Output = U::Output>>>, T: Copy>(
_: impl Ctx,
/// The left-hand side of the modulo operation.
#[implementations(f64, f32, u32, DVec2, DVec2, f64)]
numerator: U,
/// The right-hand side of the modulo operation.
#[default(2.)]
#[implementations(f64, f32, u32, DVec2, f64, DVec2)]
modulus: T,
/// Ensures the result will always be positive, even if the numerator is negative.
#[default(true)]
always_positive: bool,
) -> <U as Rem<T>>::Output {
if always_positive { (numerator % modulus + modulus) % modulus } else { numerator % modulus }
}
/// The exponent operation (^) calculates the result of raising a number to a power.
#[node_macro::node(category("Math: Arithmetic"))]
fn exponent<U: Pow<T>, T>(
_: impl Ctx,
/// The base number that will be raised to the power.
#[implementations(f64, f32, u32)]
base: U,
/// The power to which the base number will be raised.
#[default(2.)]
#[implementations(f64, f32, u32)]
power: T,
) -> <U as num_traits::Pow<T>>::Output {
base.pow(power)
}
/// The square root operation (√) calculates the nth root of a number, equivalent to raising the number to the power of 1/n.
#[node_macro::node(category("Math: Arithmetic"))]
fn root<U: num_traits::float::Float>(
_: impl Ctx,
/// The number for which the nth root will be calculated.
#[default(2.)]
#[implementations(f64, f32)]
radicand: U,
/// The degree of the root to be calculated. Square root is 2, cube root is 3, and so on.
#[default(2.)]
#[implementations(f64, f32)]
degree: U,
) -> U {
if degree == U::from(2.).unwrap() {
radicand.sqrt()
} else if degree == U::from(3.).unwrap() {
radicand.cbrt()
} else {
radicand.powf(U::from(1.).unwrap() / degree)
}
}
/// The logarithmic function (log) calculates the logarithm of a number with a specified base. If the natural logarithm function (ln) is desired, set the base to "e".
#[node_macro::node(category("Math: Arithmetic"))]
fn logarithm<U: num_traits::float::Float>(
_: impl Ctx,
/// The number for which the logarithm will be calculated.
#[implementations(f64, f32)]
value: U,
/// The base of the logarithm, such as 2 (binary), 10 (decimal), and e (natural logarithm).
#[default(2.)]
#[implementations(f64, f32)]
base: U,
) -> U {
if base == U::from(2.).unwrap() {
value.log2()
} else if base == U::from(10.).unwrap() {
value.log10()
} else if base - U::from(std::f64::consts::E).unwrap() < U::epsilon() * U::from(1e6).unwrap() {
value.ln()
} else {
value.log(base)
}
}
/// The sine trigonometric function (sin) calculates the ratio of the angle's opposite side length to its hypotenuse length.
#[node_macro::node(category("Math: Trig"))]
fn sine<U: num_traits::float::Float>(
_: impl Ctx,
/// The given angle.
#[implementations(f64, f32)]
theta: U,
/// Whether the given angle should be interpreted as radians instead of degrees.
radians: bool,
) -> U {
if radians { theta.sin() } else { theta.to_radians().sin() }
}
/// The cosine trigonometric function (cos) calculates the ratio of the angle's adjacent side length to its hypotenuse length.
#[node_macro::node(category("Math: Trig"))]
fn cosine<U: num_traits::float::Float>(
_: impl Ctx,
/// The given angle.
#[implementations(f64, f32)]
theta: U,
/// Whether the given angle should be interpreted as radians instead of degrees.
radians: bool,
) -> U {
if radians { theta.cos() } else { theta.to_radians().cos() }
}
/// The tangent trigonometric function (tan) calculates the ratio of the angle's opposite side length to its adjacent side length.
#[node_macro::node(category("Math: Trig"))]
fn tangent<U: num_traits::float::Float>(
_: impl Ctx,
/// The given angle.
#[implementations(f64, f32)]
theta: U,
/// Whether the given angle should be interpreted as radians instead of degrees.
radians: bool,
) -> U {
if radians { theta.tan() } else { theta.to_radians().tan() }
}
/// The inverse sine trigonometric function (asin) calculates the angle whose sine is the specified value.
#[node_macro::node(category("Math: Trig"))]
fn sine_inverse<U: num_traits::float::Float>(
_: impl Ctx,
/// The given value for which the angle will be calculated. Must be in the range [-1, 1] or else the result will be NaN.
#[implementations(f64, f32)]
value: U,
/// Whether the resulting angle should be given in as radians instead of degrees.
radians: bool,
) -> U {
if radians { value.asin() } else { value.asin().to_degrees() }
}
/// The inverse cosine trigonometric function (acos) calculates the angle whose cosine is the specified value.
#[node_macro::node(category("Math: Trig"))]
fn cosine_inverse<U: num_traits::float::Float>(
_: impl Ctx,
/// The given value for which the angle will be calculated. Must be in the range [-1, 1] or else the result will be NaN.
#[implementations(f64, f32)]
value: U,
/// Whether the resulting angle should be given in as radians instead of degrees.
radians: bool,
) -> U {
if radians { value.acos() } else { value.acos().to_degrees() }
}
/// The inverse tangent trigonometric function (atan or atan2, depending on input type) calculates:
/// atan: the angle whose tangent is the specified scalar number.
/// atan2: the angle of a ray from the origin to the specified coordinate.
///
/// The resulting angle is always in the range [0°, 180°] or, in radians, [-π/2, π/2].
#[node_macro::node(category("Math: Trig"))]
fn tangent_inverse<U: TangentInverse>(
_: impl Ctx,
/// The given value for which the angle will be calculated.
#[implementations(f64, f32, DVec2)]
value: U,
/// Whether the resulting angle should be given in as radians instead of degrees.
radians: bool,
) -> U::Output {
value.atan(radians)
}
pub trait TangentInverse {
type Output: num_traits::float::Float;
fn atan(self, radians: bool) -> Self::Output;
}
impl TangentInverse for f32 {
type Output = f32;
fn atan(self, radians: bool) -> Self::Output {
if radians { self.atan() } else { self.atan().to_degrees() }
}
}
impl TangentInverse for f64 {
type Output = f64;
fn atan(self, radians: bool) -> Self::Output {
if radians { self.atan() } else { self.atan().to_degrees() }
}
}
impl TangentInverse for DVec2 {
type Output = f64;
fn atan(self, radians: bool) -> Self::Output {
if radians { self.y.atan2(self.x) } else { self.y.atan2(self.x).to_degrees() }
}
}
/// The random function (rand) converts a seed into a random number within the specified range, inclusive of the minimum and exclusive of the maximum. The minimum and maximum values are automatically swapped if they are reversed.
#[node_macro::node(category("Math: Numeric"))]
fn random<U: num_traits::float::Float>(
_: impl Ctx,
_primary: (),
/// Seed to determine the unique variation of which number will be generated.
seed: u64,
/// The smaller end of the range within which the random number will be generated.
#[implementations(f64, f32)]
#[default(0.)]
min: U,
/// The larger end of the range within which the random number will be generated.
#[implementations(f64, f32)]
#[default(1.)]
max: U,
) -> f64 {
let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
let result = rng.random::<f64>();
let (min, max) = if min < max { (min, max) } else { (max, min) };
let (min, max) = (min.to_f64().unwrap(), max.to_f64().unwrap());
result * (max - min) + min
}
/// Convert a number to an integer of the type u32, which may be the required type for certain node inputs. This will be removed in the future when automatic type conversion is implemented.
#[node_macro::node(name("To u32"), category("Math: Numeric"))]
fn to_u32<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U) -> u32 {
let value = U::clamp(value, U::from(0.).unwrap(), U::from(u32::MAX as f64).unwrap());
value.to_u32().unwrap()
}
/// Convert a number to an integer of the type u64, which may be the required type for certain node inputs. This will be removed in the future when automatic type conversion is implemented.
#[node_macro::node(name("To u64"), category("Math: Numeric"))]
fn to_u64<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U) -> u64 {
let value = U::clamp(value, U::from(0.).unwrap(), U::from(u64::MAX as f64).unwrap());
value.to_u64().unwrap()
}
/// Convert an integer to a decimal number of the type f64, which may be the required type for certain node inputs. This will be removed in the future when automatic type conversion is implemented.
#[node_macro::node(name("To f64"), category("Math: Numeric"))]
fn to_f64<U: num_traits::int::PrimInt>(_: impl Ctx, #[implementations(u32, u64)] value: U) -> f64 {
value.to_f64().unwrap()
}
/// The rounding function (round) maps an input value to its nearest whole number. Halfway values are rounded away from zero.
#[node_macro::node(category("Math: Numeric"))]
fn round<U: num_traits::float::Float>(
_: impl Ctx,
/// The number which will be rounded.
#[implementations(f64, f32)]
value: U,
) -> U {
value.round()
}
/// The floor function (floor) rounds down an input value to the nearest whole number, unless the input number is already whole.
#[node_macro::node(category("Math: Numeric"))]
fn floor<U: num_traits::float::Float>(
_: impl Ctx,
/// The number which will be rounded down.
#[implementations(f64, f32)]
value: U,
) -> U {
value.floor()
}
/// The ceiling function (ceil) rounds up an input value to the nearest whole number, unless the input number is already whole.
#[node_macro::node(category("Math: Numeric"))]
fn ceiling<U: num_traits::float::Float>(
_: impl Ctx,
/// The number which will be rounded up.
#[implementations(f64, f32)]
value: U,
) -> U {
value.ceil()
}
/// The absolute value function (abs) removes the negative sign from an input value, if present.
#[node_macro::node(category("Math: Numeric"))]
fn absolute_value<U: num_traits::float::Float>(
_: impl Ctx,
/// The number which will be made positive.
#[implementations(f64, f32)]
value: U,
) -> U {
value.abs()
}
/// The minimum function (min) picks the smaller of two numbers.
#[node_macro::node(category("Math: Numeric"))]
fn min<T: std::cmp::PartialOrd>(
_: impl Ctx,
/// One of the two numbers, of which the lesser will be returned.
#[implementations(f64, f32, u32, &str)]
value: T,
/// The other of the two numbers, of which the lesser will be returned.
#[implementations(f64, f32, u32, &str)]
other_value: T,
) -> T {
if value < other_value { value } else { other_value }
}
/// The maximum function (max) picks the larger of two numbers.
#[node_macro::node(category("Math: Numeric"))]
fn max<T: std::cmp::PartialOrd>(
_: impl Ctx,
/// One of the two numbers, of which the greater will be returned.
#[implementations(f64, f32, u32, &str)]
value: T,
/// The other of the two numbers, of which the greater will be returned.
#[implementations(f64, f32, u32, &str)]
other_value: T,
) -> T {
if value > other_value { value } else { other_value }
}
/// The clamp function (clamp) restricts a number to a specified range between a minimum and maximum value. The minimum and maximum values are automatically swapped if they are reversed.
#[node_macro::node(category("Math: Numeric"))]
fn clamp<T: std::cmp::PartialOrd>(
_: impl Ctx,
/// The number to be clamped, which will be restricted to the range between the minimum and maximum values.
#[implementations(f64, f32, u32, &str)]
value: T,
/// The left (smaller) side of the range. The output will never be less than this number.
#[implementations(f64, f32, u32, &str)]
min: T,
/// The right (greater) side of the range. The output will never be greater than this number.
#[implementations(f64, f32, u32, &str)]
max: T,
) -> T {
let (min, max) = if min < max { (min, max) } else { (max, min) };
if value < min {
min
} else if value > max {
max
} else {
value
}
}
/// The equality operation (==) compares two values and returns true if they are equal, or false if they are not.
#[node_macro::node(category("Math: Logic"))]
fn equals<U: std::cmp::PartialEq<T>, T>(
_: impl Ctx,
/// One of the two numbers to compare for equality.
#[implementations(f64, f32, u32, DVec2, &str)]
value: T,
/// The other of the two numbers to compare for equality.
#[implementations(f64, f32, u32, DVec2, &str)]
other_value: U,
) -> bool {
other_value == value
}
/// The inequality operation (!=) compares two values and returns true if they are not equal, or false if they are.
#[node_macro::node(category("Math: Logic"))]
fn not_equals<U: std::cmp::PartialEq<T>, T>(
_: impl Ctx,
/// One of the two numbers to compare for inequality.
#[implementations(f64, f32, u32, DVec2, &str)]
value: T,
/// The other of the two numbers to compare for inequality.
#[implementations(f64, f32, u32, DVec2, &str)]
other_value: U,
) -> bool {
other_value != value
}
/// The less-than operation (<) compares two values and returns true if the first value is less than the second, or false if it is not.
/// If enabled with "Or Equal", the less-than-or-equal operation (<=) will be used instead.
#[node_macro::node(category("Math: Logic"))]
fn less_than<T: std::cmp::PartialOrd<T>>(
_: impl Ctx,
/// The number on the left-hand side of the comparison.
#[implementations(f64, f32, u32)]
value: T,
/// The number on the right-hand side of the comparison.
#[implementations(f64, f32, u32)]
other_value: T,
/// Uses the less-than-or-equal operation (<=) instead of the less-than operation (<).
or_equal: bool,
) -> bool {
if or_equal { value <= other_value } else { value < other_value }
}
/// The greater-than operation (>) compares two values and returns true if the first value is greater than the second, or false if it is not.
/// If enabled with "Or Equal", the greater-than-or-equal operation (>=) will be used instead.
#[node_macro::node(category("Math: Logic"))]
fn greater_than<T: std::cmp::PartialOrd<T>>(
_: impl Ctx,
/// The number on the left-hand side of the comparison.
#[implementations(f64, f32, u32)]
value: T,
/// The number on the right-hand side of the comparison.
#[implementations(f64, f32, u32)]
other_value: T,
/// Uses the greater-than-or-equal operation (>=) instead of the greater-than operation (>).
or_equal: bool,
) -> bool {
if or_equal { value >= other_value } else { value > other_value }
}
/// The logical or operation (||) returns true if either of the two inputs are true, or false if both are false.
#[node_macro::node(category("Math: Logic"))]
fn logical_or(
_: impl Ctx,
/// One of the two boolean values, either of which may be true for the node to output true.
value: bool,
/// The other of the two boolean values, either of which may be true for the node to output true.
other_value: bool,
) -> bool {
value || other_value
}
/// The logical and operation (&&) returns true if both of the two inputs are true, or false if any are false.
#[node_macro::node(category("Math: Logic"))]
fn logical_and(
_: impl Ctx,
/// One of the two boolean values, both of which must be true for the node to output true.
value: bool,
/// The other of the two boolean values, both of which must be true for the node to output true.
other_value: bool,
) -> bool {
value && other_value
}
/// The logical not operation (!) reverses true and false value of the input.
#[node_macro::node(category("Math: Logic"))]
fn logical_not(
_: impl Ctx,
/// The boolean value to be reversed.
input: bool,
) -> bool {
!input
}
/// Constructs a bool value which may be set to true or false.
#[node_macro::node(category("Value"))]
fn bool_value(_: impl Ctx, _primary: (), #[name("Bool")] bool_value: bool) -> bool {
bool_value
}
/// Constructs a number value which may be set to any real number.
#[node_macro::node(category("Value"))]
fn number_value(_: impl Ctx, _primary: (), number: f64) -> f64 {
number
}
/// Constructs a number value which may be set to any value from 0% to 100% by dragging the slider.
#[node_macro::node(category("Value"))]
fn percentage_value(_: impl Ctx, _primary: (), percentage: Percentage) -> f64 {
percentage
}
/// Constructs a two-dimensional vector value which may be set to any XY coordinate.
#[node_macro::node(category("Value"))]
fn coordinate_value(_: impl Ctx, _primary: (), x: f64, y: f64) -> DVec2 {
DVec2::new(x, y)
}
/// Constructs a color value which may be set to any color, or no color.
#[node_macro::node(category("Value"))]
fn color_value(_: impl Ctx, _primary: (), #[default(Color::BLACK)] color: Option<Color>) -> Option<Color> {
color
}
/// Gets the color at the specified position along the gradient, given a position from 0 (left) to 1 (right).
#[node_macro::node(category("Color"))]
fn sample_gradient(_: impl Ctx, _primary: (), gradient: GradientStops, position: Fraction) -> Color {
let position = position.clamp(0., 1.);
gradient.evaluate(position)
}
/// Constructs a gradient value which may be set to any sequence of color stops to represent the transition between colors.
#[node_macro::node(category("Value"))]
fn gradient_value(_: impl Ctx, _primary: (), gradient: GradientStops) -> GradientStops {
gradient
}
/// Constructs a string value which may be set to any plain text.
#[node_macro::node(category("Value"))]
fn string_value(_: impl Ctx, _primary: (), string: String) -> String {
string
}
#[node_macro::node(category("Math: Vector"))]
fn dot_product(_: impl Ctx, vector_a: DVec2, vector_b: DVec2) -> f64 {
vector_a.dot(vector_b)
}
#[cfg(test)]
mod test {
use super::*;
use graphene_core::Node;
use graphene_core::generic::FnNode;
#[test]
pub fn dot_product_function() {
let vector_a = DVec2::new(1., 2.);
let vector_b = DVec2::new(3., 4.);
assert_eq!(dot_product((), vector_a, vector_b), 11.);
}
#[test]
fn test_basic_expression() {
let result = math((), 0., "2 + 2".to_string(), 0.);
assert_eq!(result, 4.);
}
#[test]
fn test_complex_expression() {
let result = math((), 0., "(5 * 3) + (10 / 2)".to_string(), 0.);
assert_eq!(result, 20.);
}
#[test]
fn test_default_expression() {
let result = math((), 0., "0".to_string(), 0.);
assert_eq!(result, 0.);
}
#[test]
fn test_invalid_expression() {
let result = math((), 0., "invalid".to_string(), 0.);
assert_eq!(result, 0.);
}
#[test]
pub fn foo() {
let fnn = FnNode::new(|(a, b)| (b, a));
assert_eq!(fnn.eval((1u32, 2u32)), (2, 1));
}
#[test]
pub fn add_vectors() {
assert_eq!(super::add((), DVec2::ONE, DVec2::ONE), DVec2::ONE * 2.);
}
#[test]
pub fn subtract_f64() {
assert_eq!(super::subtract((), 5_f64, 3_f64), 2.);
}
#[test]
pub fn divide_vectors() {
assert_eq!(super::divide((), DVec2::ONE, 2_f64), DVec2::ONE / 2.);
}
#[test]
pub fn modulo_positive() {
assert_eq!(super::modulo((), -5_f64, 2_f64, true), 1_f64);
}
#[test]
pub fn modulo_negative() {
assert_eq!(super::modulo((), -5_f64, 2_f64, false), -1_f64);
}
}
+2
View File
@@ -17,8 +17,10 @@ loading = ["serde_json"]
dyn-any = { workspace = true } dyn-any = { workspace = true }
graphene-core = { workspace = true } graphene-core = { workspace = true }
graphene-path-bool = { workspace = true } graphene-path-bool = { workspace = true }
graphene-brush = { workspace = true }
graphene-application-io = { workspace = true } graphene-application-io = { workspace = true }
graphene-svg-renderer = { workspace = true } graphene-svg-renderer = { workspace = true }
graphene-raster-nodes = { workspace = true }
# Workspace dependencies # Workspace dependencies
log = { workspace = true } log = { workspace = true }
+16 -16
View File
@@ -5,8 +5,8 @@ use dyn_any::DynAny;
pub use dyn_any::StaticType; pub use dyn_any::StaticType;
pub use glam::{DAffine2, DVec2, IVec2, UVec2}; pub use glam::{DAffine2, DVec2, IVec2, UVec2};
use graphene_application_io::SurfaceFrame; use graphene_application_io::SurfaceFrame;
use graphene_core::raster::brush_cache::BrushCache; use graphene_brush::brush_cache::BrushCache;
use graphene_core::raster::{BlendMode, LuminanceCalculation}; use graphene_brush::brush_stroke::BrushStroke;
use graphene_core::raster_types::CPU; use graphene_core::raster_types::CPU;
use graphene_core::transform::ReferencePoint; use graphene_core::transform::ReferencePoint;
use graphene_core::uuid::NodeId; use graphene_core::uuid::NodeId;
@@ -209,29 +209,29 @@ tagged_value! {
#[serde(alias = "GradientPositions")] // TODO: Eventually remove this alias document upgrade code #[serde(alias = "GradientPositions")] // TODO: Eventually remove this alias document upgrade code
GradientStops(graphene_core::vector::style::GradientStops), GradientStops(graphene_core::vector::style::GradientStops),
Font(graphene_core::text::Font), Font(graphene_core::text::Font),
BrushStrokes(Vec<graphene_core::vector::brush_stroke::BrushStroke>), BrushStrokes(Vec<BrushStroke>),
BrushCache(BrushCache), BrushCache(BrushCache),
DocumentNode(DocumentNode), DocumentNode(DocumentNode),
Curve(graphene_core::raster::curve::Curve), Curve(graphene_raster_nodes::curve::Curve),
Footprint(graphene_core::transform::Footprint), Footprint(graphene_core::transform::Footprint),
VectorModification(Box<graphene_core::vector::VectorModification>), VectorModification(Box<graphene_core::vector::VectorModification>),
FontCache(Arc<graphene_core::text::FontCache>), FontCache(Arc<graphene_core::text::FontCache>),
// ========== // ==========
// ENUM TYPES // ENUM TYPES
// ========== // ==========
BlendMode(BlendMode), BlendMode(graphene_core::blending::BlendMode),
LuminanceCalculation(LuminanceCalculation), LuminanceCalculation(graphene_raster_nodes::adjustments::LuminanceCalculation),
XY(graphene_core::ops::XY), XY(graphene_core::extract_xy::XY),
RedGreenBlue(graphene_core::raster::RedGreenBlue), RedGreenBlue(graphene_raster_nodes::adjustments::RedGreenBlue),
RedGreenBlueAlpha(graphene_core::raster::RedGreenBlueAlpha), RedGreenBlueAlpha(graphene_raster_nodes::adjustments::RedGreenBlueAlpha),
RealTimeMode(graphene_core::animation::RealTimeMode), RealTimeMode(graphene_core::animation::RealTimeMode),
NoiseType(graphene_core::raster::NoiseType), NoiseType(graphene_raster_nodes::adjustments::NoiseType),
FractalType(graphene_core::raster::FractalType), FractalType(graphene_raster_nodes::adjustments::FractalType),
CellularDistanceFunction(graphene_core::raster::CellularDistanceFunction), CellularDistanceFunction(graphene_raster_nodes::adjustments::CellularDistanceFunction),
CellularReturnType(graphene_core::raster::CellularReturnType), CellularReturnType(graphene_raster_nodes::adjustments::CellularReturnType),
DomainWarpType(graphene_core::raster::DomainWarpType), DomainWarpType(graphene_raster_nodes::adjustments::DomainWarpType),
RelativeAbsolute(graphene_core::raster::RelativeAbsolute), RelativeAbsolute(graphene_raster_nodes::adjustments::RelativeAbsolute),
SelectiveColorChoice(graphene_core::raster::SelectiveColorChoice), SelectiveColorChoice(graphene_raster_nodes::adjustments::SelectiveColorChoice),
GridType(graphene_core::vector::misc::GridType), GridType(graphene_core::vector::misc::GridType),
ArcType(graphene_core::vector::misc::ArcType), ArcType(graphene_core::vector::misc::ArcType),
MergeByDistanceAlgorithm(graphene_core::vector::misc::MergeByDistanceAlgorithm), MergeByDistanceAlgorithm(graphene_core::vector::misc::MergeByDistanceAlgorithm),
+35
View File
@@ -0,0 +1,35 @@
[package]
name = "graphene-raster-nodes"
version = "0.1.0"
edition = "2024"
description = "graphene raster data format"
authors = ["Graphite Authors <contact@graphite.rs>"]
license = "MIT OR Apache-2.0"
[features]
default = ["serde"]
serde = ["dep:serde"]
[dependencies]
# Local dependencies
dyn-any = { workspace = true }
graphene-core = { workspace = true }
node-macro = { workspace = true }
# Workspace dependencies
glam = { workspace = true }
specta = { workspace = true }
image = { workspace = true }
bytemuck = { workspace = true }
ndarray = { workspace = true }
bezier-rs = { workspace = true }
rand = { workspace = true }
rand_chacha = { workspace = true }
fastnoise-lite = { workspace = true }
# Optional workspace dependencies
serde = { workspace = true, optional = true, features = ["derive"] }
[dev-dependencies]
tokio = { workspace = true }
futures = { workspace = true }
@@ -1,16 +1,16 @@
#![allow(clippy::too_many_arguments)] #![allow(clippy::too_many_arguments)]
use crate::GraphicElement; use crate::curve::CubicSplines;
use crate::blending::BlendMode;
use crate::raster::curve::{CubicSplines, CurveManipulatorGroup};
use crate::raster::curve::{Curve, ValueMapperNode};
use crate::raster::image::Image;
use crate::raster::{Channel, Color, Pixel};
use crate::raster_types::{CPU, Raster, RasterDataTable};
use crate::registry::types::{Angle, Percentage, SignedPercentage};
use crate::vector::style::GradientStops;
use crate::{Ctx, Node};
use dyn_any::DynAny; use dyn_any::DynAny;
use graphene_core::Node;
use graphene_core::blending::BlendMode;
use graphene_core::color::Color;
use graphene_core::color::Pixel;
use graphene_core::context::Ctx;
use graphene_core::gradient::GradientStops;
use graphene_core::raster::image::Image;
use graphene_core::raster_types::{CPU, Raster, RasterDataTable};
use graphene_core::registry::types::{Angle, Percentage, SignedPercentage};
use std::cmp::Ordering; use std::cmp::Ordering;
use std::fmt::Debug; use std::fmt::Debug;
@@ -576,10 +576,7 @@ impl Adjust<Color> for GradientStops {
} }
} }
} }
impl Adjust<Color> for RasterDataTable<CPU> impl Adjust<Color> for RasterDataTable<CPU> {
where
GraphicElement: From<Image<Color>>,
{
fn adjust(&mut self, map_fn: impl Fn(&Color) -> Color) { fn adjust(&mut self, map_fn: impl Fn(&Color) -> Color) {
for instance in self.instance_mut_iter() { for instance in self.instance_mut_iter() {
for c in instance.instance.data_mut().data.iter_mut() { for c in instance.instance.data_mut().data.iter_mut() {
@@ -1130,48 +1127,6 @@ async fn exposure<T: Adjust<Color>>(
input input
} }
const WINDOW_SIZE: usize = 1024;
#[node_macro::node(category(""))]
fn generate_curves<C: Channel + crate::raster::Linear>(_: impl Ctx, curve: Curve, #[implementations(f32, f64)] _target_format: C) -> ValueMapperNode<C> {
use bezier_rs::{Bezier, TValue};
let [mut pos, mut param]: [[f32; 2]; 2] = [[0.; 2], curve.first_handle];
let mut lut = vec![C::from_f64(0.); WINDOW_SIZE];
let end = CurveManipulatorGroup {
anchor: [1.; 2],
handles: [curve.last_handle, [0.; 2]],
};
for sample in curve.manipulator_groups.iter().chain(std::iter::once(&end)) {
let [x0, y0, x1, y1, x2, y2, x3, y3] = [pos[0], pos[1], param[0], param[1], sample.handles[0][0], sample.handles[0][1], sample.anchor[0], sample.anchor[1]].map(f64::from);
let bezier = Bezier::from_cubic_coordinates(x0, y0, x1, y1, x2, y2, x3, y3);
let [left, right] = [pos[0], sample.anchor[0]].map(|c| c.clamp(0., 1.));
let lut_index_left: usize = (left * (lut.len() - 1) as f32).floor() as _;
let lut_index_right: usize = (right * (lut.len() - 1) as f32).ceil() as _;
for index in lut_index_left..=lut_index_right {
let x = index as f64 / (lut.len() - 1) as f64;
let y = if x <= x0 {
y0
} else if x >= x3 {
y3
} else {
bezier.find_tvalues_for_x(x)
.next()
.map(|t| bezier.evaluate(TValue::Parametric(t.clamp(0., 1.))).y)
// Fall back to a very bad approximation if Bezier-rs fails
.unwrap_or_else(|| (x - x0) / (x3 - x0) * (y3 - y0) + y0)
};
lut[index] = C::from_f64(y);
}
pos = sample.anchor;
param = sample.handles[1];
}
ValueMapperNode::new(lut)
}
#[node_macro::node(category("Raster: Adjustment"))] #[node_macro::node(category("Raster: Adjustment"))]
fn color_overlay<T: Adjust<Color>>( fn color_overlay<T: Adjust<Color>>(
_: impl Ctx, _: impl Ctx,
@@ -1224,10 +1179,10 @@ fn color_overlay<T: Adjust<Color>>(
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::Color; use graphene_core::blending::BlendMode;
use crate::blending::BlendMode; use graphene_core::color::Color;
use crate::raster::image::Image; use graphene_core::raster::image::Image;
use crate::raster_types::{Raster, RasterDataTable}; use graphene_core::raster_types::{Raster, RasterDataTable};
#[tokio::test] #[tokio::test]
async fn color_overlay_multiply() { async fn color_overlay_multiply() {
@@ -1,6 +1,7 @@
use super::{Channel, Linear, LuminanceMut};
use crate::Node;
use dyn_any::{DynAny, StaticType, StaticTypeSized}; use dyn_any::{DynAny, StaticType, StaticTypeSized};
use graphene_core::Node;
use graphene_core::color::{Channel, Linear, LuminanceMut};
use std::hash::{Hash, Hasher};
use std::ops::{Add, Mul, Sub}; use std::ops::{Add, Mul, Sub};
#[derive(Debug, Clone, PartialEq, DynAny, specta::Type, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, PartialEq, DynAny, specta::Type, serde::Serialize, serde::Deserialize)]
@@ -23,8 +24,8 @@ impl Default for Curve {
} }
} }
impl std::hash::Hash for Curve { impl Hash for Curve {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) { fn hash<H: Hasher>(&self, state: &mut H) {
self.manipulator_groups.hash(state); self.manipulator_groups.hash(state);
[self.first_handle, self.last_handle].iter().flatten().for_each(|f| f.to_bits().hash(state)); [self.first_handle, self.last_handle].iter().flatten().for_each(|f| f.to_bits().hash(state));
} }
@@ -36,8 +37,8 @@ pub struct CurveManipulatorGroup {
pub handles: [[f32; 2]; 2], pub handles: [[f32; 2]; 2],
} }
impl std::hash::Hash for CurveManipulatorGroup { impl Hash for CurveManipulatorGroup {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) { fn hash<H: Hasher>(&self, state: &mut H) {
for c in self.handles.iter().chain([&self.anchor]).flatten() { for c in self.handles.iter().chain([&self.anchor]).flatten() {
c.to_bits().hash(state); c.to_bits().hash(state);
} }
@@ -1,7 +1,7 @@
use graph_craft::proto::types::Percentage; use graphene_core::context::Ctx;
use graphene_core::Ctx;
use graphene_core::raster::image::Image; use graphene_core::raster::image::Image;
use graphene_core::raster_types::{CPU, Raster, RasterDataTable}; use graphene_core::raster_types::{CPU, Raster, RasterDataTable};
use graphene_core::registry::types::Percentage;
use image::{DynamicImage, GenericImage, GenericImageView, GrayImage, ImageBuffer, Luma, Rgba, RgbaImage}; use image::{DynamicImage, GenericImage, GenericImageView, GrayImage, ImageBuffer, Luma, Rgba, RgbaImage};
use ndarray::{Array2, ArrayBase, Dim, OwnedRepr}; use ndarray::{Array2, ArrayBase, Dim, OwnedRepr};
use std::cmp::{max, min}; use std::cmp::{max, min};
@@ -15,7 +15,7 @@ async fn dehaze(_: impl Ctx, image_frame: RasterDataTable<CPU>, strength: Percen
// Prepare the image data for processing // Prepare the image data for processing
let image_data = bytemuck::cast_vec(image.data.clone()); let image_data = bytemuck::cast_vec(image.data.clone());
let image_buffer = image::Rgba32FImage::from_raw(image.width, image.height, image_data).expect("Failed to convert internal image format into image-rs data type."); let image_buffer = image::Rgba32FImage::from_raw(image.width, image.height, image_data).expect("Failed to convert internal image format into image-rs data type.");
let dynamic_image: image::DynamicImage = image_buffer.into(); let dynamic_image: DynamicImage = image_buffer.into();
// Run the dehaze algorithm // Run the dehaze algorithm
let dehazed_dynamic_image = dehaze_image(dynamic_image, strength / 100.); let dehazed_dynamic_image = dehaze_image(dynamic_image, strength / 100.);
@@ -1,8 +1,9 @@
use graph_craft::proto::types::PixelLength; use graphene_core::color::Color;
use graphene_core::context::Ctx;
use graphene_core::raster::image::Image; use graphene_core::raster::image::Image;
use graphene_core::raster::{Bitmap, BitmapMut}; use graphene_core::raster::{Bitmap, BitmapMut};
use graphene_core::raster_types::{CPU, Raster, RasterDataTable}; use graphene_core::raster_types::{CPU, Raster, RasterDataTable};
use graphene_core::{Color, Ctx}; use graphene_core::registry::types::PixelLength;
/// Blurs the image with a Gaussian or blur kernel filter. /// Blurs the image with a Gaussian or blur kernel filter.
#[node_macro::node(category("Raster: Filter"))] #[node_macro::node(category("Raster: Filter"))]
@@ -0,0 +1,46 @@
//! requires bezier-rs
use crate::curve::{Curve, CurveManipulatorGroup, ValueMapperNode};
use bezier_rs::{Bezier, TValue};
use graphene_core::color::{Channel, Linear};
use graphene_core::context::Ctx;
const WINDOW_SIZE: usize = 1024;
#[node_macro::node(category(""))]
fn generate_curves<C: Channel + Linear>(_: impl Ctx, curve: Curve, #[implementations(f32, f64)] _target_format: C) -> ValueMapperNode<C> {
let [mut pos, mut param]: [[f32; 2]; 2] = [[0.; 2], curve.first_handle];
let mut lut = vec![C::from_f64(0.); WINDOW_SIZE];
let end = CurveManipulatorGroup {
anchor: [1.; 2],
handles: [curve.last_handle, [0.; 2]],
};
for sample in curve.manipulator_groups.iter().chain(std::iter::once(&end)) {
let [x0, y0, x1, y1, x2, y2, x3, y3] = [pos[0], pos[1], param[0], param[1], sample.handles[0][0], sample.handles[0][1], sample.anchor[0], sample.anchor[1]].map(f64::from);
let bezier = Bezier::from_cubic_coordinates(x0, y0, x1, y1, x2, y2, x3, y3);
let [left, right] = [pos[0], sample.anchor[0]].map(|c| c.clamp(0., 1.));
let lut_index_left: usize = (left * (lut.len() - 1) as f32).floor() as _;
let lut_index_right: usize = (right * (lut.len() - 1) as f32).ceil() as _;
for index in lut_index_left..=lut_index_right {
let x = index as f64 / (lut.len() - 1) as f64;
let y = if x <= x0 {
y0
} else if x >= x3 {
y3
} else {
bezier.find_tvalues_for_x(x)
.next()
.map(|t| bezier.evaluate(TValue::Parametric(t.clamp(0., 1.))).y)
// Fall back to a very bad approximation if Bezier-rs fails
.unwrap_or_else(|| (x - x0) / (x3 - x0) * (y3 - y0) + y0)
};
lut[index] = C::from_f64(y);
}
pos = sample.anchor;
param = sample.handles[1];
}
ValueMapperNode::new(lut)
}
@@ -1,5 +1,6 @@
use graphene_core::color::Color;
use graphene_core::context::Ctx;
use graphene_core::raster_types::{CPU, RasterDataTable}; use graphene_core::raster_types::{CPU, RasterDataTable};
use graphene_core::{Color, Ctx};
#[node_macro::node(category("Color"))] #[node_macro::node(category("Color"))]
async fn image_color_palette( async fn image_color_palette(
+7
View File
@@ -0,0 +1,7 @@
pub mod adjustments;
pub mod curve;
pub mod dehaze;
pub mod filter;
pub mod generate_curves;
pub mod image_color_palette;
pub mod std_nodes;
@@ -1,12 +1,17 @@
use crate::adjustments::{CellularDistanceFunction, CellularReturnType, DomainWarpType, FractalType, NoiseType};
use dyn_any::DynAny; use dyn_any::DynAny;
use fastnoise_lite; use fastnoise_lite;
use glam::{DAffine2, DVec2, Vec2}; use glam::{DAffine2, DVec2, Vec2};
use graphene_core::blending::AlphaBlending;
use graphene_core::color::Color;
use graphene_core::color::{Alpha, AlphaMut, Channel, LinearChannel, Luminance, RGBMut};
use graphene_core::context::{Ctx, ExtractFootprint};
use graphene_core::instances::Instance; use graphene_core::instances::Instance;
use graphene_core::math::bbox::Bbox; use graphene_core::math::bbox::Bbox;
pub use graphene_core::raster::*; use graphene_core::raster::image::Image;
use graphene_core::raster::{Bitmap, BitmapMut};
use graphene_core::raster_types::{CPU, Raster, RasterDataTable}; use graphene_core::raster_types::{CPU, Raster, RasterDataTable};
use graphene_core::transform::Transform; use graphene_core::transform::Transform;
use graphene_core::{Ctx, ExtractFootprint};
use rand::prelude::*; use rand::prelude::*;
use rand_chacha::ChaCha8Rng; use rand_chacha::ChaCha8Rng;
use std::fmt::Debug; use std::fmt::Debug;
@@ -25,7 +30,7 @@ impl From<std::io::Error> for Error {
} }
#[node_macro::node(category("Debug: Raster"))] #[node_macro::node(category("Debug: Raster"))]
fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: RasterDataTable<CPU>) -> RasterDataTable<CPU> { pub fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: RasterDataTable<CPU>) -> RasterDataTable<CPU> {
let mut result_table = RasterDataTable::default(); let mut result_table = RasterDataTable::default();
for mut image_frame_instance in image_frame.instance_iter() { for mut image_frame_instance in image_frame.instance_iter() {
@@ -92,7 +97,7 @@ fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: RasterDa
} }
#[node_macro::node(category("Raster: Channels"))] #[node_macro::node(category("Raster: Channels"))]
fn combine_channels( pub fn combine_channels(
_: impl Ctx, _: impl Ctx,
_primary: (), _primary: (),
#[expose] red: RasterDataTable<CPU>, #[expose] red: RasterDataTable<CPU>,
@@ -181,7 +186,7 @@ fn combine_channels(
} }
#[node_macro::node(category("Raster"))] #[node_macro::node(category("Raster"))]
fn mask( pub fn mask(
_: impl Ctx, _: impl Ctx,
/// The image to be masked. /// The image to be masked.
image: RasterDataTable<CPU>, image: RasterDataTable<CPU>,
@@ -231,7 +236,7 @@ fn mask(
} }
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
fn extend_image_to_bounds(_: impl Ctx, image: RasterDataTable<CPU>, bounds: DAffine2) -> RasterDataTable<CPU> { pub fn extend_image_to_bounds(_: impl Ctx, image: RasterDataTable<CPU>, bounds: DAffine2) -> RasterDataTable<CPU> {
let mut result_table = RasterDataTable::default(); let mut result_table = RasterDataTable::default();
for mut image_instance in image.instance_iter() { for mut image_instance in image.instance_iter() {
@@ -284,7 +289,7 @@ fn extend_image_to_bounds(_: impl Ctx, image: RasterDataTable<CPU>, bounds: DAff
} }
#[node_macro::node(category("Debug: Raster"))] #[node_macro::node(category("Debug: Raster"))]
fn empty_image(_: impl Ctx, transform: DAffine2, color: Color) -> RasterDataTable<CPU> { pub fn empty_image(_: impl Ctx, transform: DAffine2, color: Color) -> RasterDataTable<CPU> {
let width = transform.transform_vector2(DVec2::new(1., 0.)).length() as u32; let width = transform.transform_vector2(DVec2::new(1., 0.)).length() as u32;
let height = transform.transform_vector2(DVec2::new(0., 1.)).length() as u32; let height = transform.transform_vector2(DVec2::new(0., 1.)).length() as u32;
@@ -301,13 +306,13 @@ fn empty_image(_: impl Ctx, transform: DAffine2, color: Color) -> RasterDataTabl
/// Constructs a raster image. /// Constructs a raster image.
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
fn image_value(_: impl Ctx, _primary: (), image: RasterDataTable<CPU>) -> RasterDataTable<CPU> { pub fn image_value(_: impl Ctx, _primary: (), image: RasterDataTable<CPU>) -> RasterDataTable<CPU> {
image image
} }
#[node_macro::node(category("Raster: Pattern"))] #[node_macro::node(category("Raster: Pattern"))]
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn noise_pattern( pub fn noise_pattern(
ctx: impl ExtractFootprint + Ctx, ctx: impl ExtractFootprint + Ctx,
_primary: (), _primary: (),
clip: bool, clip: bool,
@@ -463,7 +468,7 @@ fn noise_pattern(
} }
#[node_macro::node(category("Raster: Pattern"))] #[node_macro::node(category("Raster: Pattern"))]
fn mandelbrot(ctx: impl ExtractFootprint + Send) -> RasterDataTable<CPU> { pub fn mandelbrot(ctx: impl ExtractFootprint + Send) -> RasterDataTable<CPU> {
let footprint = ctx.footprint(); let footprint = ctx.footprint();
let viewport_bounds = footprint.viewport_bounds_in_local_space(); let viewport_bounds = footprint.viewport_bounds_in_local_space();
+3
View File
@@ -29,8 +29,11 @@ graph-craft = { workspace = true }
wgpu-executor = { workspace = true } wgpu-executor = { workspace = true }
graphene-core = { workspace = true } graphene-core = { workspace = true }
graphene-path-bool = { workspace = true } graphene-path-bool = { workspace = true }
graphene-math-nodes = { workspace = true }
graphene-svg-renderer = { workspace = true } graphene-svg-renderer = { workspace = true }
graphene-application-io = { workspace = true } graphene-application-io = { workspace = true }
graphene-raster-nodes = { workspace = true }
graphene-brush = { workspace = true }
# Workspace dependencies # Workspace dependencies
fastnoise-lite = { workspace = true } fastnoise-lite = { workspace = true }
+10 -6
View File
@@ -1,22 +1,26 @@
pub mod any; pub mod any;
pub mod brush;
pub mod dehaze;
pub mod filter;
pub mod http; pub mod http;
pub mod image_color_palette;
pub mod raster;
pub mod text; pub mod text;
#[cfg(feature = "wasm")] #[cfg(feature = "wasm")]
pub mod wasm_application_io; pub mod wasm_application_io;
pub use graphene_application_io as application_io; pub use graphene_application_io as application_io;
pub use graphene_brush as brush;
pub use graphene_core::vector; pub use graphene_core::vector;
pub use graphene_core::*; pub use graphene_core::*;
pub use graphene_math_nodes as math_nodes;
pub use graphene_path_bool as path_bool; pub use graphene_path_bool as path_bool;
pub use graphene_raster_nodes as raster_nodes;
/// stop gap solution until all `Quad` and `Rect` paths have been replaced with their absolute ones /// stop gap solutions until all paths have been replaced with their absolute ones
pub mod renderer { pub mod renderer {
pub use graphene_core::math::quad::Quad; pub use graphene_core::math::quad::Quad;
pub use graphene_core::math::rect::Rect; pub use graphene_core::math::rect::Rect;
pub use graphene_svg_renderer::*; pub use graphene_svg_renderer::*;
} }
pub mod raster {
pub use graphene_core::raster::*;
pub use graphene_raster_nodes::adjustments::*;
pub use graphene_raster_nodes::*;
}
+22 -8
View File
@@ -9,23 +9,37 @@ fn text<'i: 'n>(
editor: &'i WasmEditorApi, editor: &'i WasmEditorApi,
text: String, text: String,
font_name: Font, font_name: Font,
#[default(24.)] font_size: f64, #[unit(" px")]
#[default(1.2)] line_height_ratio: f64, #[default(24.)]
#[default(1.)] character_spacing: f64, font_size: f64,
#[default(None)] max_width: Option<f64>, #[unit("x")]
#[default(None)] max_height: Option<f64>, #[default(1.2)]
line_height_ratio: f64,
#[unit(" px")]
#[default(0.)]
character_spacing: f64,
#[unit(" px")]
#[default(None)]
max_width: Option<f64>,
#[unit(" px")]
#[default(None)]
max_height: Option<f64>,
#[unit("°")]
#[default(0.)]
tilt: f64,
) -> VectorDataTable { ) -> VectorDataTable {
let buzz_face = editor.font_cache.get(&font_name).map(|data| load_face(data));
let typesetting = TypesettingConfig { let typesetting = TypesettingConfig {
font_size, font_size,
line_height_ratio, line_height_ratio,
character_spacing, character_spacing,
max_width, max_width,
max_height, max_height,
tilt,
}; };
let result = VectorData::from_subpaths(to_path(&text, buzz_face, typesetting), false); let font_data = editor.font_cache.get(&font_name).map(|f| load_font(f));
let result = VectorData::from_subpaths(to_path(&text, font_data, typesetting), false);
VectorDataTable::new(result) VectorDataTable::new(result)
} }
+20 -12
View File
@@ -289,19 +289,21 @@ impl GraphicElementRendered for GraphicGroupTable {
let mut layer = false; let mut layer = false;
let bounds = self let blend_mode = match render_params.view_mode {
.instance_ref_iter() ViewMode::Outline => peniko::Mix::Normal,
.filter_map(|element| element.instance.bounding_box(transform, true)) _ => alpha_blending.blend_mode.to_peniko(),
.reduce(Quad::combine_bounds); };
if let Some(bounds) = bounds { let mut bounds = None;
let blend_mode = match render_params.view_mode {
ViewMode::Outline => peniko::Mix::Normal,
_ => alpha_blending.blend_mode.to_peniko(),
};
let factor = if render_params.for_mask { 1. } else { alpha_blending.fill }; let factor = if render_params.for_mask { 1. } else { alpha_blending.fill };
let opacity = alpha_blending.opacity * factor; let opacity = alpha_blending.opacity * factor;
if opacity < 1. || (render_params.view_mode != ViewMode::Outline && alpha_blending.blend_mode != BlendMode::default()) { if opacity < 1. || (render_params.view_mode != ViewMode::Outline && alpha_blending.blend_mode != BlendMode::default()) {
bounds = self
.instance_ref_iter()
.filter_map(|element| element.instance.bounding_box(transform, true))
.reduce(Quad::combine_bounds);
if let Some(bounds) = bounds {
scene.push_layer( scene.push_layer(
peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver), peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver),
opacity, opacity,
@@ -321,6 +323,12 @@ impl GraphicElementRendered for GraphicGroupTable {
if !next_clips { if !next_clips {
mask_instance_state = None; mask_instance_state = None;
} }
if !layer {
bounds = self
.instance_ref_iter()
.filter_map(|element| element.instance.bounding_box(transform, true))
.reduce(Quad::combine_bounds);
}
if let Some(bounds) = bounds { if let Some(bounds) = bounds {
let rect = kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y); let rect = kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y);
+2 -1
View File
@@ -12,6 +12,7 @@ static NODE_ID: AtomicU64 = AtomicU64::new(0);
pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStream2> { pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStream2> {
let ParsedNodeFn { let ParsedNodeFn {
vis,
attributes, attributes,
fn_name, fn_name,
struct_name, struct_name,
@@ -345,7 +346,7 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
/// Underlying implementation for [#struct_name] /// Underlying implementation for [#struct_name]
#[inline] #[inline]
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub(crate) #async_keyword fn #fn_name <'n, #(#fn_generics,)*> (#input_ident: #input_type #(, #field_idents: #field_types)*) -> #output_type #where_clause #body #vis #async_keyword fn #fn_name <'n, #(#fn_generics,)*> (#input_ident: #input_type #(, #field_idents: #field_types)*) -> #output_type #where_clause #body
#[automatically_derived] #[automatically_derived]
impl<'n, #(#fn_generics,)* #(#struct_generics,)* #(#future_idents,)*> #graphene_core::Node<'n, #input_type> for #mod_name::#struct_name<#(#struct_generics,)*> impl<'n, #(#fn_generics,)* #(#struct_generics,)* #(#future_idents,)*> #graphene_core::Node<'n, #input_type> for #mod_name::#struct_name<#(#struct_generics,)*>
+12 -2
View File
@@ -7,8 +7,8 @@ use syn::punctuated::Punctuated;
use syn::spanned::Spanned; use syn::spanned::Spanned;
use syn::token::{Comma, RArrow}; use syn::token::{Comma, RArrow};
use syn::{ use syn::{
AttrStyle, Attribute, Error, Expr, ExprTuple, FnArg, GenericParam, Ident, ItemFn, Lit, LitFloat, LitInt, LitStr, Meta, Pat, PatIdent, PatType, Path, ReturnType, Type, TypeParam, WhereClause, AttrStyle, Attribute, Error, Expr, ExprTuple, FnArg, GenericParam, Ident, ItemFn, Lit, LitFloat, LitInt, LitStr, Meta, Pat, PatIdent, PatType, Path, ReturnType, Type, TypeParam, Visibility,
parse_quote, WhereClause, parse_quote,
}; };
use crate::codegen::generate_node_code; use crate::codegen::generate_node_code;
@@ -22,6 +22,7 @@ pub(crate) struct Implementation {
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct ParsedNodeFn { pub(crate) struct ParsedNodeFn {
pub(crate) vis: Visibility,
pub(crate) attributes: NodeFnAttributes, pub(crate) attributes: NodeFnAttributes,
pub(crate) fn_name: Ident, pub(crate) fn_name: Ident,
pub(crate) struct_name: Ident, pub(crate) struct_name: Ident,
@@ -263,6 +264,7 @@ fn parse_node_fn(attr: TokenStream2, item: TokenStream2) -> syn::Result<ParsedNo
let attributes = syn::parse2::<NodeFnAttributes>(attr.clone()).map_err(|e| Error::new(e.span(), format!("Failed to parse node_fn attributes: {}", e)))?; let attributes = syn::parse2::<NodeFnAttributes>(attr.clone()).map_err(|e| Error::new(e.span(), format!("Failed to parse node_fn attributes: {}", e)))?;
let input_fn = syn::parse2::<ItemFn>(item.clone()).map_err(|e| Error::new(e.span(), format!("Failed to parse function: {}. Make sure it's a valid Rust function.", e)))?; let input_fn = syn::parse2::<ItemFn>(item.clone()).map_err(|e| Error::new(e.span(), format!("Failed to parse function: {}. Make sure it's a valid Rust function.", e)))?;
let vis = input_fn.vis;
let fn_name = input_fn.sig.ident.clone(); let fn_name = input_fn.sig.ident.clone();
let struct_name = format_ident!("{}", fn_name.to_string().to_case(Case::Pascal)); let struct_name = format_ident!("{}", fn_name.to_string().to_case(Case::Pascal));
let mod_name = fn_name.clone(); let mod_name = fn_name.clone();
@@ -297,6 +299,7 @@ fn parse_node_fn(attr: TokenStream2, item: TokenStream2) -> syn::Result<ParsedNo
.fold(String::new(), |acc, b| acc + &b + "\n"); .fold(String::new(), |acc, b| acc + &b + "\n");
Ok(ParsedNodeFn { Ok(ParsedNodeFn {
vis,
attributes, attributes,
fn_name, fn_name,
struct_name, struct_name,
@@ -748,6 +751,7 @@ mod tests {
let parsed = parse_node_fn(attr, input).unwrap(); let parsed = parse_node_fn(attr, input).unwrap();
let expected = ParsedNodeFn { let expected = ParsedNodeFn {
vis: Visibility::Inherited,
attributes: NodeFnAttributes { attributes: NodeFnAttributes {
category: Some(parse_quote!("Math: Arithmetic")), category: Some(parse_quote!("Math: Arithmetic")),
display_name: None, display_name: None,
@@ -808,6 +812,7 @@ mod tests {
let parsed = parse_node_fn(attr, input).unwrap(); let parsed = parse_node_fn(attr, input).unwrap();
let expected = ParsedNodeFn { let expected = ParsedNodeFn {
vis: Visibility::Inherited,
attributes: NodeFnAttributes { attributes: NodeFnAttributes {
category: Some(parse_quote!("General")), category: Some(parse_quote!("General")),
display_name: None, display_name: None,
@@ -879,6 +884,7 @@ mod tests {
let parsed = parse_node_fn(attr, input).unwrap(); let parsed = parse_node_fn(attr, input).unwrap();
let expected = ParsedNodeFn { let expected = ParsedNodeFn {
vis: Visibility::Inherited,
attributes: NodeFnAttributes { attributes: NodeFnAttributes {
category: Some(parse_quote!("Vector: Shape")), category: Some(parse_quote!("Vector: Shape")),
display_name: None, display_name: None,
@@ -935,6 +941,7 @@ mod tests {
let parsed = parse_node_fn(attr, input).unwrap(); let parsed = parse_node_fn(attr, input).unwrap();
let expected = ParsedNodeFn { let expected = ParsedNodeFn {
vis: Visibility::Inherited,
attributes: NodeFnAttributes { attributes: NodeFnAttributes {
category: Some(parse_quote!("Raster: Adjustment")), category: Some(parse_quote!("Raster: Adjustment")),
display_name: None, display_name: None,
@@ -1003,6 +1010,7 @@ mod tests {
let parsed = parse_node_fn(attr, input).unwrap(); let parsed = parse_node_fn(attr, input).unwrap();
let expected = ParsedNodeFn { let expected = ParsedNodeFn {
vis: Visibility::Inherited,
attributes: NodeFnAttributes { attributes: NodeFnAttributes {
category: Some(parse_quote!("Math: Arithmetic")), category: Some(parse_quote!("Math: Arithmetic")),
display_name: None, display_name: None,
@@ -1059,6 +1067,7 @@ mod tests {
let parsed = parse_node_fn(attr, input).unwrap(); let parsed = parse_node_fn(attr, input).unwrap();
let expected = ParsedNodeFn { let expected = ParsedNodeFn {
vis: Visibility::Inherited,
attributes: NodeFnAttributes { attributes: NodeFnAttributes {
category: Some(parse_quote!("IO")), category: Some(parse_quote!("IO")),
display_name: None, display_name: None,
@@ -1115,6 +1124,7 @@ mod tests {
let parsed = parse_node_fn(attr, input).unwrap(); let parsed = parse_node_fn(attr, input).unwrap();
let expected = ParsedNodeFn { let expected = ParsedNodeFn {
vis: Visibility::Inherited,
attributes: NodeFnAttributes { attributes: NodeFnAttributes {
category: Some(parse_quote!("Custom")), category: Some(parse_quote!("Custom")),
display_name: Some(parse_quote!("CustomNode2")), display_name: Some(parse_quote!("CustomNode2")),
+1 -1
View File
@@ -61,7 +61,7 @@ pub fn generate_node_substitutions() -> HashMap<String, DocumentNode> {
( (
NodeId(i as u64), NodeId(i as u64),
match inputs.len() { match inputs.len() {
1 => { 1 if false => {
let input = inputs.iter().next().unwrap(); let input = inputs.iter().next().unwrap();
let input_ty = input.nested_type(); let input_ty = input.nested_type();
+34 -20
View File
@@ -164,9 +164,23 @@ meta_description = "Open source free software. A vector graphics creativity suit
--- ---
<!-- As a new entrant to the open source digital content creation landscape, Graphite has a unique formula for success: --> <div class="diptych sizzle-video">
<div class="block text">
Starting life as a vector editor, Graphite is evolving into a generalized, all-in-one graphics toolbox that's built more like a game engine than a conventional creative app. The editor's tools wrap its node graph core, providing user-friendly workflows for vector, raster, and beyond. Starting life as a vector editor, Graphite is evolving into a generalized, all-in-one graphics toolbox that's built more like a game engine than a conventional creative app. The editor's tools wrap its node graph core, providing user-friendly workflows for vector, raster, animation, and beyond.
<a href="https://editor.graphite.rs" class="button arrow">Start creating</a>
</div>
<div class="block video">
<video loop muted playsinline disablepictureinpicture disableremoteplayback data-auto-play preload="none" poster="https://static.graphite.rs/content/index/sizzle-compilation-poster.avif">
<source src="https://static.graphite.rs/content/index/sizzle-compilation.webm" type="video/webm" />
<source src="https://static.graphite.rs/content/index/sizzle-compilation.mp4" type="video/mp4" />
</video>
</div>
</div>
</div> </div>
<div class="block workflows"> <div class="block workflows">
@@ -276,7 +290,23 @@ Once it's ready to shine, Graphite's code architecture is structured to deliver
</div> </div>
</section> </section>
<!-- ▙ OVERVIEW ▟ --> <!-- ▙ OVERVIEW ▟ -->
<!-- --> <!-- -->
<!-- ▛ DONATE ▜ -->
<section id="donate" class="block">
<div class="block">
<h2 class="heart">Support the mission</h2>
Free software doesn't grow on trees! Chip in your share of the (very real) development costs so you're not leaving others to pick up the tab. Becoming a member (or giving a one-time donation) lets you help maintain Graphite's sustainability and independence.
<a href="/donate" class="button arrow">Become a member</a>
</div>
</section>
<!-- ▙ DONATE ▟ -->
<!-- -->
<!-- ▛ PROCEDURALISM ▜ --> <!-- ▛ PROCEDURALISM ▜ -->
<section id="proceduralism" class="feature-box-outer"> <section id="proceduralism" class="feature-box-outer">
<div class="feature-box-inner"> <div class="feature-box-inner">
@@ -363,23 +393,7 @@ Graphite's representation of artwork as a node graph lets you customize, compose
</div> </div>
</section> </section>
<!-- ▙ PROCEDURALISM ▟ --> <!-- ▙ PROCEDURALISM ▟ -->
<!-- --> <!-- -->
<!-- ▛ DONATE ▜ -->
<section id="donate" class="block">
<div class="block">
## Support the mission
Free software doesn't grow on trees! Chip in your share of the (very real) development costs so you're not leaving others to pick up the tab. Becoming a member (or giving a one-time donation) lets you help maintain Graphite's sustainability and independence.
<a href="/donate" class="button arrow">Become a member</a>
</div>
</section>
<!-- ▙ DONATE ▟ -->
<!-- -->
<!-- ▛ NEWSLETTER ▜ --> <!-- ▛ NEWSLETTER ▜ -->
<section id="newsletter" class="feature-box-narrow"> <section id="newsletter" class="feature-box-narrow">
<div id="newsletter-success"><!-- Used only as a URL hash fragment anchor --></div> <div id="newsletter-success"><!-- Used only as a URL hash fragment anchor --></div>
+16 -1
View File
@@ -2,7 +2,8 @@
title = "Graphite features" title = "Graphite features"
[extra] [extra]
css = ["/page/features.css", "/component/feature-box.css", "/component/feature-icons.css"] css = ["/page/features.css", "/component/feature-box.css", "/component/feature-icons.css", "/component/youtube-embed.css"]
js = ["/js/youtube-embed.js"]
+++ +++
<section> <section>
@@ -17,6 +18,20 @@ In 2025, stay tuned for performance improvements, native multiplatform desktop a
</div> </div>
</section> </section>
<section>
<div class="block">
<div class="block video-container">
<div>
<div class="youtube-embed aspect-16x9">
<img data-youtube-embed="ZUbcwUC5lxA" loading="lazy" src="https://static.graphite.rs/content/features/podcast-interview-youtube.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" alt="Rust-Powered Graphics Editor: How Graphite's Syntax Trees Revolutionize Image Editing" />
</div>
</div>
</div>
</div>
</section>
<section> <section>
<div class="diptych"> <div class="diptych">
@@ -35,7 +35,9 @@ Comments should usually be placed on a separate line above the code they are ref
## Blank lines ## Blank lines
Please make a habit of grouping together related lines of code in blocks separated by blank lines. If you have dozens of lines comprising a single unbroken block of logic, you are likely not splitting it apart enough to aid readability. Find sensible places to partition the logic and insert blank lines between each. Roughly 10% of the code you write should ideally be blank lines, otherwise you are likely underutilizing them at the expense of readability. Please make a habit of grouping together related lines of code in blocks separated by blank lines. These are like your paragraphs if you were writing a novel — they greatly aid readability and your copy editor would have significant concerns with your writing if they were absent.
If you have dozens of lines comprising a single unbroken block of logic, you are likely not splitting it apart enough to aid readability. Find sensible places to partition the logic and insert blank lines between each. Roughly 10% of the code you write should ideally be blank lines, otherwise you are likely underutilizing them at the expense of readability.
## Imports ## Imports
+13 -13
View File
@@ -151,19 +151,6 @@ body > .page {
.heart.heart { .heart.heart {
// The same color is also used below in the SVG after the `%23` (URL-encoded `#`) // The same color is also used below in the SVG after the `%23` (URL-encoded `#`)
color: #cc304f; color: #cc304f;
&::after {
content: "";
background-image: url('data:image/svg+xml;utf8,\
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M8,15C5.12471,9.753694 0.5,8.795225 0.5,4.736524 C0.5,-0.507473 7.468734,0 8,4.967381 C8.531266,0 15.5,-0.507473 15.5,4.736524 C15.5,8.795225 10.87529,9.753694 8,15z" fill="%23cc304f" /></svg>\
');
display: inline-block;
width: 0.75em;
height: 0.75em;
margin-left: 0.25em;
margin-bottom: -0.1em;
vertical-align: baseline;
}
} }
@media screen and (max-width: 1200px) { @media screen and (max-width: 1200px) {
@@ -683,6 +670,19 @@ hr,
} }
} }
.heart::after {
content: "";
background-image: url('data:image/svg+xml;utf8,\
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M8,15C5.12471,9.753694 0.5,8.795225 0.5,4.736524 C0.5,-0.507473 7.468734,0 8,4.967381 C8.531266,0 15.5,-0.507473 15.5,4.736524 C15.5,8.795225 10.87529,9.753694 8,15z" fill="%23cc304f" /></svg>\
');
display: inline-block;
width: 0.75em;
height: 0.75em;
margin-left: 0.25em;
margin-bottom: -0.1em;
vertical-align: baseline;
}
// blockquote { // blockquote {
// padding: 32px 80px; // padding: 32px 80px;
// background: rgba(0, 0, 0, 0.0625); // background: rgba(0, 0, 0, 0.0625);
+10
View File
@@ -1,3 +1,13 @@
.video-container {
background: var(--color-fog);
> div {
margin: calc(20 * var(--variable-px)) auto;
width: 100%;
max-width: 800px;
}
}
#roadmap { #roadmap {
width: 100%; width: 100%;
text-align: center; text-align: center;
+63 -19
View File
@@ -18,23 +18,24 @@
#tagline { #tagline {
h1 { h1 {
span { span {
position: relative; position: relative;
&::after { &::after {
content: ""; content: "";
pointer-events: none; pointer-events: none;
position: absolute; position: absolute;
left: 0; left: 0;
right: 0; right: 0;
top: 100%; top: 100%;
// Dimensions: 480x40 // Dimensions: 480x40
height: 100%; height: 100%;
margin-top: -0.2em; margin-top: -0.2em;
background: url("https://static.graphite.rs/textures/text-sketch-underline.png"); background: url("https://static.graphite.rs/textures/text-sketch-underline.png");
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: contain; background-size: contain;
}
} }
}} }
p { p {
font-size: 1.2rem; font-size: 1.2rem;
@@ -112,14 +113,60 @@
// ▛ OVERVIEW ▜ // ▛ OVERVIEW ▜
#overview { #overview {
background-color: var(--color-cloud); background-color: var(--color-cloud);
.sizzle-video {
display: flex;
flex-wrap: nowrap;
max-width: 100%;
.block {
min-width: 0;
flex-direction: row;
&.text {
flex: 1 4 100%;
flex-direction: column;
p:has(.button) {
margin-top: 20px;
}
}
&.video {
flex: 0 1 fit-content;
}
}
@media screen and (max-width: 900px) {
flex-wrap: wrap;
.block.video {
flex: 1 1 100%;
justify-content: center;
}
}
@media screen and (max-width: 1100px) {
p:has(.button) {
display: none;
}
}
}
} }
// ▙ OVERVIEW ▟ // ▙ OVERVIEW ▟
// ▛ DONATE ▜
#donate {
h2 {
color: #cc304f;
}
}
// ▙ DONATE ▟
// ▛ PROCEDURALISM ▜ // ▛ PROCEDURALISM ▜
#proceduralism { #proceduralism {
background-color: var(--color-slate); background-color: var(--color-slate);
color: white; color: white;
margin-top: 0;
.diptych { .diptych {
background: black; background: black;
@@ -183,9 +230,6 @@
} }
// ▙ PROCEDURALISM ▟ // ▙ PROCEDURALISM ▟
// ▛ DONATE ▜
// ▙ DONATE ▟
// ▛ NEWSLETTER ▜ // ▛ NEWSLETTER ▜
#newsletter { #newsletter {
background-color: var(--color-peach); background-color: var(--color-peach);