mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-04 13:30:48 +00:00
Merge branch 'master' into path_copy_paste
This commit is contained in:
commit
4a5f8c7d3b
39 changed files with 1281 additions and 569 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -5,3 +5,4 @@ perf.data*
|
|||
profile.json
|
||||
flamegraph.svg
|
||||
.idea/
|
||||
.direnv
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
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
|
||||
atkmm
|
||||
cairo
|
||||
|
|
340
Cargo.lock
generated
340
Cargo.lock
generated
|
@ -82,6 +82,12 @@ dependencies = [
|
|||
"alloc-no-stdlib",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "android-activity"
|
||||
version = "0.5.2"
|
||||
|
@ -486,11 +492,11 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "block2"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d59b4c170e16f0405a2e95aff44432a0d41aa97675f3d52623effe95792a037"
|
||||
checksum = "340d2f0bdb2a43c1d3cd40513185b2bd7def0aa1052f956455114bc98f82dcf2"
|
||||
dependencies = [
|
||||
"objc2 0.6.0",
|
||||
"objc2 0.6.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -528,9 +534,9 @@ checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
|
|||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.22.0"
|
||||
version = "1.23.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540"
|
||||
checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422"
|
||||
dependencies = [
|
||||
"bytemuck_derive",
|
||||
]
|
||||
|
@ -822,6 +828,12 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "color"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ae467d04a8a8aea5d9a49018a6ade2e4221d92968e8ce55a48c0b1164e5f698"
|
||||
|
||||
[[package]]
|
||||
name = "color_quant"
|
||||
version = "1.1.0"
|
||||
|
@ -1255,6 +1267,16 @@ version = "0.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.5"
|
||||
|
@ -1569,6 +1591,25 @@ dependencies = [
|
|||
"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]]
|
||||
name = "fontconfig-parser"
|
||||
version = "0.5.7"
|
||||
|
@ -1592,6 +1633,29 @@ dependencies = [
|
|||
"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]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
|
@ -2109,7 +2173,7 @@ checksum = "dcf29e94d6d243368b7a56caa16bc213e4f9f8ed38c4d9557069527b5d5281ca"
|
|||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"gpu-descriptor-types",
|
||||
"hashbrown 0.15.2",
|
||||
"hashbrown 0.15.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2200,13 +2264,14 @@ dependencies = [
|
|||
"node-macro",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"parley",
|
||||
"petgraph 0.7.1",
|
||||
"rand 0.9.0",
|
||||
"rand_chacha 0.9.0",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustybuzz 0.20.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"skrifa 0.32.0",
|
||||
"specta",
|
||||
"tinyvec",
|
||||
"tokio",
|
||||
|
@ -2470,10 +2535,12 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
|||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.2"
|
||||
version = "0.15.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
||||
checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
|
@ -2949,7 +3016,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.15.2",
|
||||
"hashbrown 0.15.4",
|
||||
"serde",
|
||||
]
|
||||
|
||||
|
@ -3204,9 +3271,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "kurbo"
|
||||
version = "0.11.1"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89234b2cc610a7dd927ebde6b41dd1a5d4214cffaef4cf1fb2195d592f92518f"
|
||||
checksum = "1077d333efea6170d9ccb96d3c3026f300ca0773da4938cc4c811daa6df68b0c"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"serde",
|
||||
|
@ -3282,7 +3349,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-targets 0.48.5",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -3506,10 +3573,10 @@ dependencies = [
|
|||
"dpi",
|
||||
"gtk",
|
||||
"keyboard-types",
|
||||
"objc2 0.6.0",
|
||||
"objc2 0.6.1",
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation 0.3.0",
|
||||
"objc2-foundation 0.3.1",
|
||||
"once_cell",
|
||||
"png",
|
||||
"serde",
|
||||
|
@ -3790,9 +3857,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "objc2"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3531f65190d9cff863b77a99857e74c314dd16bf56c538c4b57c7cbc3f3a6e59"
|
||||
checksum = "88c6597e14493ab2e44ce58f2fdecf095a51f12ca57bec060a11c57332520551"
|
||||
dependencies = [
|
||||
"objc2-encode 4.1.0",
|
||||
"objc2-exception-helper",
|
||||
|
@ -3805,15 +3872,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "5906f93257178e2f7ae069efb89fbd6ee94f0592740b5f8a1512ca498814d0fb"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"block2 0.6.0",
|
||||
"block2 0.6.1",
|
||||
"libc",
|
||||
"objc2 0.6.0",
|
||||
"objc2 0.6.1",
|
||||
"objc2-cloud-kit",
|
||||
"objc2-core-data",
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-graphics",
|
||||
"objc2-core-image",
|
||||
"objc2-foundation 0.3.0",
|
||||
"objc2-foundation 0.3.1",
|
||||
"objc2-quartz-core 0.3.0",
|
||||
]
|
||||
|
||||
|
@ -3824,8 +3891,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "6c1948a9be5f469deadbd6bcb86ad7ff9e47b4f632380139722f7d9840c0d42c"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"objc2 0.6.0",
|
||||
"objc2-foundation 0.3.0",
|
||||
"objc2 0.6.1",
|
||||
"objc2-foundation 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -3835,18 +3902,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "1f860f8e841f6d32f754836f51e6bc7777cd7e7053cf18528233f6811d3eceb4"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"objc2 0.6.0",
|
||||
"objc2-foundation 0.3.0",
|
||||
"objc2 0.6.1",
|
||||
"objc2-foundation 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-foundation"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "daeaf60f25471d26948a1c2f840e3f7d86f4109e3af4e8e4b5cd70c39690d925"
|
||||
checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"objc2 0.6.0",
|
||||
"dispatch2",
|
||||
"objc2 0.6.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -3856,7 +3924,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "f8dca602628b65356b6513290a21a6405b4d4027b8b250f0b98dddbb28b7de02"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"objc2 0.6.0",
|
||||
"objc2 0.6.1",
|
||||
"objc2-core-foundation",
|
||||
"objc2-io-surface",
|
||||
]
|
||||
|
@ -3867,8 +3935,18 @@ version = "0.3.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ffa6bea72bf42c78b0b34e89c0bafac877d5f80bf91e159a5d96ea7f693ca56"
|
||||
dependencies = [
|
||||
"objc2 0.6.0",
|
||||
"objc2-foundation 0.3.0",
|
||||
"objc2 0.6.1",
|
||||
"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]]
|
||||
|
@ -3906,14 +3984,14 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "objc2-foundation"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a21c6c9014b82c39515db5b396f91645182611c97d24637cf56ac01e5f8d998"
|
||||
checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"block2 0.6.0",
|
||||
"block2 0.6.1",
|
||||
"libc",
|
||||
"objc2 0.6.0",
|
||||
"objc2 0.6.1",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
|
@ -3924,7 +4002,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "161a8b87e32610086e1a7a9e9ec39f84459db7b3a0881c1f16ca5a2605581c19"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"objc2 0.6.0",
|
||||
"objc2 0.6.1",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
|
@ -3960,8 +4038,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "6fb3794501bb1bee12f08dcad8c61f2a5875791ad1c6f47faa71a0f033f20071"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"objc2 0.6.0",
|
||||
"objc2-foundation 0.3.0",
|
||||
"objc2 0.6.1",
|
||||
"objc2-foundation 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -3971,9 +4049,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "777a571be14a42a3990d4ebedaeb8b54cd17377ec21b92e8200ac03797b3bee1"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"objc2 0.6.0",
|
||||
"objc2 0.6.1",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation 0.3.0",
|
||||
"objc2-foundation 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -3983,11 +4061,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "b717127e4014b0f9f3e8bba3d3f2acec81f1bde01f656823036e823ed2c94dce"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"block2 0.6.0",
|
||||
"objc2 0.6.0",
|
||||
"block2 0.6.1",
|
||||
"objc2 0.6.1",
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation 0.3.0",
|
||||
"objc2-foundation 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -4149,6 +4227,19 @@ dependencies = [
|
|||
"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]]
|
||||
name = "paste"
|
||||
version = "1.0.15"
|
||||
|
@ -4180,7 +4271,18 @@ name = "peniko"
|
|||
version = "0.2.0"
|
||||
source = "git+https://github.com/linebender/peniko.git?rev=d114c62#d114c6292dbcfb03e7360692198be423168a0edd"
|
||||
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",
|
||||
"smallvec",
|
||||
]
|
||||
|
@ -4968,7 +5070,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "f6f9e8a4f503e5c8750e4cd3b32a4e090035c46374b305a15c70bad833dca05f"
|
||||
dependencies = [
|
||||
"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]]
|
||||
|
@ -5261,26 +5383,8 @@ dependencies = [
|
|||
"log",
|
||||
"smallvec",
|
||||
"ttf-parser 0.24.1",
|
||||
"unicode-bidi-mirroring 0.3.0",
|
||||
"unicode-ccc 0.3.0",
|
||||
"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-bidi-mirroring",
|
||||
"unicode-ccc",
|
||||
"unicode-properties",
|
||||
"unicode-script",
|
||||
]
|
||||
|
@ -5415,9 +5519,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.218"
|
||||
version = "1.0.219"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60"
|
||||
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
@ -5446,9 +5550,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.218"
|
||||
version = "1.0.219"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b"
|
||||
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -5652,7 +5756,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "8cc1aa86c26dbb1b63875a7180aa0819709b33348eb5b1491e4321fae388179d"
|
||||
dependencies = [
|
||||
"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]]
|
||||
|
@ -5675,9 +5799,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.14.0"
|
||||
version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
@ -5895,6 +6019,17 @@ dependencies = [
|
|||
"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]]
|
||||
name = "swift-rs"
|
||||
version = "1.0.7"
|
||||
|
@ -6005,9 +6140,9 @@ dependencies = [
|
|||
"ndk 0.9.0",
|
||||
"ndk-context",
|
||||
"ndk-sys 0.6.0+11769913",
|
||||
"objc2 0.6.0",
|
||||
"objc2 0.6.1",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation 0.3.0",
|
||||
"objc2-foundation 0.3.1",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"raw-window-handle",
|
||||
|
@ -6060,9 +6195,9 @@ dependencies = [
|
|||
"log",
|
||||
"mime",
|
||||
"muda",
|
||||
"objc2 0.6.0",
|
||||
"objc2 0.6.1",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation 0.3.0",
|
||||
"objc2-foundation 0.3.1",
|
||||
"percent-encoding",
|
||||
"plist",
|
||||
"raw-window-handle",
|
||||
|
@ -6266,9 +6401,9 @@ dependencies = [
|
|||
"http",
|
||||
"jni",
|
||||
"log",
|
||||
"objc2 0.6.0",
|
||||
"objc2 0.6.1",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation 0.3.0",
|
||||
"objc2-foundation 0.3.1",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"raw-window-handle",
|
||||
|
@ -6688,11 +6823,11 @@ dependencies = [
|
|||
"dirs",
|
||||
"libappindicator",
|
||||
"muda",
|
||||
"objc2 0.6.0",
|
||||
"objc2 0.6.1",
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-graphics",
|
||||
"objc2-foundation 0.3.0",
|
||||
"objc2-foundation 0.3.1",
|
||||
"once_cell",
|
||||
"png",
|
||||
"serde",
|
||||
|
@ -6720,9 +6855,6 @@ name = "ttf-parser"
|
|||
version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
|
||||
dependencies = [
|
||||
"core_maths",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typeid"
|
||||
|
@ -6795,24 +6927,12 @@ version = "0.3.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64af057ad7466495ca113126be61838d8af947f41d93a949980b2389a118082f"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi-mirroring"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ccc"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "260bc6647b3893a9a90668360803a15f96b85a5257b1c3a0c3daf6ae2496de42"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ccc"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.18"
|
||||
|
@ -6900,7 +7020,7 @@ dependencies = [
|
|||
"log",
|
||||
"pico-args",
|
||||
"roxmltree",
|
||||
"rustybuzz 0.18.0",
|
||||
"rustybuzz",
|
||||
"simplecss",
|
||||
"siphasher 1.0.1",
|
||||
"strict-num",
|
||||
|
@ -6971,9 +7091,9 @@ dependencies = [
|
|||
"bytemuck",
|
||||
"futures-intrusive",
|
||||
"log",
|
||||
"peniko",
|
||||
"peniko 0.2.0",
|
||||
"png",
|
||||
"skrifa",
|
||||
"skrifa 0.26.6",
|
||||
"static_assertions",
|
||||
"thiserror 2.0.12",
|
||||
"vello_encoding",
|
||||
|
@ -6988,8 +7108,8 @@ source = "git+https://github.com/linebender/vello.git?rev=3275ec8#3275ec85d83118
|
|||
dependencies = [
|
||||
"bytemuck",
|
||||
"guillotiere",
|
||||
"peniko",
|
||||
"skrifa",
|
||||
"peniko 0.2.0",
|
||||
"skrifa 0.26.6",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
|
@ -7556,10 +7676,10 @@ version = "0.6.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c"
|
||||
dependencies = [
|
||||
"objc2 0.6.0",
|
||||
"objc2 0.6.1",
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation 0.3.0",
|
||||
"objc2-foundation 0.3.1",
|
||||
"raw-window-handle",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-version",
|
||||
|
@ -8077,7 +8197,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "b19b78efae8b853c6c817e8752fc1dbf9cab8a8ffe9c30f399bd750ccf0f0730"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"block2 0.6.0",
|
||||
"block2 0.6.1",
|
||||
"cookie",
|
||||
"crossbeam-channel",
|
||||
"dpi",
|
||||
|
@ -8091,10 +8211,10 @@ dependencies = [
|
|||
"kuchikiki",
|
||||
"libc",
|
||||
"ndk 0.9.0",
|
||||
"objc2 0.6.0",
|
||||
"objc2 0.6.1",
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation 0.3.0",
|
||||
"objc2-foundation 0.3.1",
|
||||
"objc2-ui-kit",
|
||||
"objc2-web-kit",
|
||||
"once_cell",
|
||||
|
@ -8199,6 +8319,12 @@ version = "1.0.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
|
||||
|
||||
[[package]]
|
||||
name = "yazi"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5"
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.7.5"
|
||||
|
@ -8223,6 +8349,12 @@ dependencies = [
|
|||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeno"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524"
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.35"
|
||||
|
|
|
@ -116,7 +116,8 @@ rand_chacha = "0.9"
|
|||
glam = { version = "0.29", default-features = false, features = ["serde", "scalar-math", "debug-glam-assert"] }
|
||||
base64 = "0.22"
|
||||
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"
|
||||
fern = { version = "0.7", features = ["colored"] }
|
||||
num_enum = "0.7"
|
||||
|
|
26
README.md
26
README.md
|
@ -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.**
|
||||
|
||||
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 ⭐!
|
||||
|
||||
|
@ -58,22 +58,26 @@ Learn more from the [website](https://graphite.rs/), subscribe to the [newslette
|
|||
</a>
|
||||
<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
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||
*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.
|
||||
|
|
|
@ -36,9 +36,9 @@ thiserror = { workspace = true }
|
|||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
bezier-rs = { workspace = true }
|
||||
kurbo = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
glam = { workspace = true }
|
||||
kurbo = { workspace = true }
|
||||
derivative = { workspace = true }
|
||||
specta = { workspace = true }
|
||||
dyn-any = { workspace = true }
|
||||
|
|
|
@ -102,7 +102,7 @@ pub const MANIPULATOR_GROUP_MARKER_SIZE: f64 = 6.;
|
|||
pub const SELECTION_THRESHOLD: f64 = 10.;
|
||||
pub const HIDE_HANDLE_DISTANCE: f64 = 3.;
|
||||
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 HANDLE_LENGTH_FACTOR: f64 = 0.5;
|
||||
|
||||
|
@ -133,6 +133,7 @@ pub const SCALE_EFFECT: f64 = 0.5;
|
|||
|
||||
// COLORS
|
||||
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_GREEN: &str = "#63ce63";
|
||||
pub const COLOR_OVERLAY_RED: &str = "#ef5454";
|
||||
|
|
|
@ -214,7 +214,7 @@ pub fn input_mappings() -> Mapping {
|
|||
entry!(KeyDown(KeyX); modifiers=[Accel], action_dispatch=PathToolMessage::Cut { clipboard: Clipboard::Device }),
|
||||
entry!(KeyDown(KeyC); modifiers=[Accel], action_dispatch=PathToolMessage::Copy { clipboard: Clipboard::Device }),
|
||||
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(Escape); action_dispatch=PathToolMessage::Escape),
|
||||
entry!(KeyDown(KeyG); action_dispatch=PathToolMessage::GRS { key: KeyG }),
|
||||
|
|
|
@ -190,6 +190,7 @@ impl<'a> ModifyInputsContext<'a> {
|
|||
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_height), false)),
|
||||
Some(NodeInput::value(TaggedValue::F64(typesetting.tilt), false)),
|
||||
]);
|
||||
|
||||
let text_id = NodeId::new();
|
||||
|
|
|
@ -960,13 +960,13 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
|||
nodes: [
|
||||
DocumentNode {
|
||||
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)),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
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)),
|
||||
..Default::default()
|
||||
},
|
||||
|
@ -1556,6 +1556,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
|||
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_height), false),
|
||||
NodeInput::value(TaggedValue::F64(TypesettingConfig::default().tilt), false),
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
|
@ -1577,6 +1578,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
|||
"Line Height",
|
||||
"TODO",
|
||||
WidgetOverride::Number(NumberInputSettings {
|
||||
unit: Some("x".to_string()),
|
||||
min: Some(0.),
|
||||
step: Some(0.1),
|
||||
..Default::default()
|
||||
|
@ -1586,6 +1588,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
|||
"Character Spacing",
|
||||
"TODO",
|
||||
WidgetOverride::Number(NumberInputSettings {
|
||||
unit: Some(" px".to_string()),
|
||||
min: Some(0.),
|
||||
step: Some(0.1),
|
||||
..Default::default()
|
||||
|
@ -1595,6 +1598,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
|||
"Max Width",
|
||||
"TODO",
|
||||
WidgetOverride::Number(NumberInputSettings {
|
||||
unit: Some(" px".to_string()),
|
||||
min: Some(1.),
|
||||
blank_assist: false,
|
||||
..Default::default()
|
||||
|
@ -1604,11 +1608,22 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
|||
"Max Height",
|
||||
"TODO",
|
||||
WidgetOverride::Number(NumberInputSettings {
|
||||
unit: Some(" px".to_string()),
|
||||
min: Some(1.),
|
||||
blank_assist: false,
|
||||
..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()],
|
||||
..Default::default()
|
||||
|
|
|
@ -124,8 +124,19 @@ pub fn path_overlays(document: &DocumentMessageHandler, draw_handles: DrawHandle
|
|||
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 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 {
|
||||
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 transform = document.metadata().transform_to_viewport(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) {
|
||||
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 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,
|
||||
COMPASS_ROSE_RING_INNER_DIAMETER, MANIPULATOR_GROUP_MARKER_SIZE, PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_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_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 bezier_rs::{Bezier, Subpath};
|
||||
|
@ -581,6 +581,35 @@ impl OverlayContext {
|
|||
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) {
|
||||
self.start_dpi_aware_transform();
|
||||
|
||||
|
|
|
@ -1161,8 +1161,7 @@ impl NodeNetworkInterface {
|
|||
.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]) {
|
||||
let row = ("", "TODO").into();
|
||||
pub fn insert_input_properties_row(&mut self, node_id: &NodeId, index: usize, network_path: &[NodeId], row: PropertiesRow) {
|
||||
let _ = self
|
||||
.node_metadata_mut(node_id, network_path)
|
||||
.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 glam::{DAffine2, DMat2, DVec2};
|
||||
use graphene_std::renderer::Quad;
|
||||
use graphene_std::vector::VectorModificationType;
|
||||
use graphene_std::vector::{HandleExt, ManipulatorPointId};
|
||||
use graphene_std::vector::{HandleId, PointId};
|
||||
use graphene_std::vector::{HandleExt, HandleId, ManipulatorPointId, PointId, VectorModificationType};
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::f64::consts::PI;
|
||||
|
||||
|
@ -88,6 +86,18 @@ impl OriginalTransforms {
|
|||
let Some(selected_points) = shape_editor.selected_points_in_layer(layer) else {
|
||||
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
|
||||
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 });
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// TODO: Eventually remove this document upgrade code
|
||||
// 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::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, OutputConnector};
|
||||
|
@ -308,8 +309,8 @@ pub fn document_migration_upgrades(document: &mut DocumentMessageHandler, reset_
|
|||
let node_definition = resolve_document_node_type(reference).unwrap();
|
||||
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.insert_input_properties_row(node_id, 8, network_path);
|
||||
document.network_interface.insert_input_properties_row(node_id, 9, 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, ("", "TODO").into());
|
||||
|
||||
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);
|
||||
|
@ -402,7 +403,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
|
||||
if reference == "Text" && inputs_count != 8 {
|
||||
if reference == "Text" && inputs_count != 9 {
|
||||
let node_definition = resolve_document_node_type(reference).unwrap();
|
||||
let document_node = node_definition.default_node_template().document_node;
|
||||
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
|
||||
|
@ -433,12 +434,44 @@ pub fn document_migration_upgrades(document: &mut DocumentMessageHandler, reset_
|
|||
);
|
||||
document.network_interface.set_input(
|
||||
&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,
|
||||
);
|
||||
document.network_interface.set_input(
|
||||
&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,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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::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::F64(tilt)) = inputs[8].as_value() else { return None };
|
||||
|
||||
let typesetting = TypesettingConfig {
|
||||
font_size,
|
||||
|
@ -368,6 +369,7 @@ pub fn get_text(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInter
|
|||
max_width,
|
||||
character_spacing,
|
||||
max_height,
|
||||
tilt,
|
||||
};
|
||||
Some((text, font, typesetting))
|
||||
}
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
use super::graph_modification_utils::merge_layers;
|
||||
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::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::misc::{PathSnapSource, SnapSource};
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
|
||||
use crate::messages::preferences::SelectionMode;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::snapping::SnapTypeConfiguration;
|
||||
use crate::messages::tool::common_functionality::utility_functions::is_visible_point;
|
||||
use crate::messages::tool::common_functionality::utility_functions::{is_intersecting, is_visible_point};
|
||||
use crate::messages::tool::tool_messages::path_tool::{PathOverlayMode, PointSelectState};
|
||||
use bezier_rs::{Bezier, BezierHandles, Subpath, TValue};
|
||||
use glam::{DAffine2, DVec2};
|
||||
|
@ -45,6 +46,7 @@ pub enum ManipulatorAngle {
|
|||
#[derive(Clone, Debug, Default)]
|
||||
pub struct SelectedLayerState {
|
||||
selected_points: HashSet<ManipulatorPointId>,
|
||||
selected_segments: HashSet<SegmentId>,
|
||||
/// Keeps track of the current state; helps avoid unnecessary computation when called by [`ShapeState`].
|
||||
ignore_handles: bool,
|
||||
ignore_anchors: bool,
|
||||
|
@ -57,11 +59,27 @@ impl SelectedLayerState {
|
|||
pub fn is_empty(&self) -> bool {
|
||||
self.selected_points.is_empty()
|
||||
}
|
||||
pub fn selected(&self) -> impl Iterator<Item = ManipulatorPointId> + '_ {
|
||||
pub fn selected_points(&self) -> impl Iterator<Item = ManipulatorPointId> + '_ {
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -69,10 +87,26 @@ impl SelectedLayerState {
|
|||
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) {
|
||||
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) {
|
||||
if self.ignore_handles != status {
|
||||
return;
|
||||
|
@ -104,14 +138,6 @@ impl SelectedLayerState {
|
|||
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>;
|
||||
|
@ -131,6 +157,12 @@ pub struct SelectedPointsInfo {
|
|||
pub vector_data: VectorData,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SelectedSegmentsInfo {
|
||||
pub segments: Vec<SegmentId>,
|
||||
pub vector_data: VectorData,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct ManipulatorPointInfo {
|
||||
pub layer: LayerNodeIdentifier,
|
||||
|
@ -139,6 +171,7 @@ pub struct ManipulatorPointInfo {
|
|||
|
||||
pub type OpposingHandleLengths = HashMap<LayerNodeIdentifier, HashMap<HandleId, f64>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ClosestSegment {
|
||||
layer: LayerNodeIdentifier,
|
||||
segment: SegmentId,
|
||||
|
@ -162,6 +195,10 @@ impl ClosestSegment {
|
|||
self.points
|
||||
}
|
||||
|
||||
pub fn bezier(&self) -> Bezier {
|
||||
self.bezier
|
||||
}
|
||||
|
||||
pub fn closest_point_document(&self) -> DVec2 {
|
||||
self.bezier.evaluate(TValue::Parametric(self.t))
|
||||
}
|
||||
|
@ -476,7 +513,7 @@ impl ShapeState {
|
|||
|
||||
if let Some(id) = selected.as_anchor() {
|
||||
for neighbor in vector_data.connected_points(id) {
|
||||
if state.is_selected(ManipulatorPointId::Anchor(neighbor)) {
|
||||
if state.is_point_selected(ManipulatorPointId::Anchor(neighbor)) {
|
||||
continue;
|
||||
}
|
||||
let Some(position) = vector_data.point_domain.position_from_id(neighbor) else { continue };
|
||||
|
@ -515,38 +552,30 @@ impl ShapeState {
|
|||
let point_position = manipulator_point_id.get_position(&vector_data)?;
|
||||
|
||||
let selected_shape_state = self.selected_shape_state.get(&layer)?;
|
||||
let already_selected = selected_shape_state.is_selected(manipulator_point_id);
|
||||
|
||||
// Should we select or deselect the point?
|
||||
let new_selected = if already_selected { !extend_selection } else { true };
|
||||
let already_selected = selected_shape_state.is_point_selected(manipulator_point_id);
|
||||
|
||||
// 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);
|
||||
|
||||
// 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 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 }));
|
||||
} else {
|
||||
let selected_shape_state = self.selected_shape_state.get_mut(&layer)?;
|
||||
selected_shape_state.deselect_point(manipulator_point_id);
|
||||
|
||||
return Some(None);
|
||||
let retain_existing_selection = extend_selection || already_selected;
|
||||
if !retain_existing_selection {
|
||||
self.deselect_all_points();
|
||||
self.deselect_all_segments();
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
@ -558,11 +587,16 @@ impl ShapeState {
|
|||
select_threshold: f64,
|
||||
path_overlay_mode: PathOverlayMode,
|
||||
frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>,
|
||||
point_editing_mode: bool,
|
||||
) -> Option<(bool, Option<SelectedPointsInfo>)> {
|
||||
if self.selected_shape_state.is_empty() {
|
||||
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) {
|
||||
let vector_data = network_interface.compute_modified_vector(layer)?;
|
||||
let point_position = manipulator_point_id.get_position(&vector_data)?;
|
||||
|
@ -575,7 +609,7 @@ impl ShapeState {
|
|||
}
|
||||
|
||||
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
|
||||
let offset = mouse_position - network_interface.document_metadata().transform_to_viewport(layer).transform_point2(point_position);
|
||||
|
@ -633,7 +667,7 @@ impl ShapeState {
|
|||
// Select all connected points
|
||||
while let Some(point) = selected_stack.pop() {
|
||||
let anchor_point = ManipulatorPointId::Anchor(point);
|
||||
if !state.is_selected(anchor_point) {
|
||||
if !state.is_point_selected(anchor_point) {
|
||||
state.select_point(anchor_point);
|
||||
selected_stack.extend(vector_data.connected_points(point));
|
||||
}
|
||||
|
@ -674,6 +708,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) {
|
||||
for state in self.selected_shape_state.values_mut() {
|
||||
self.ignore_anchors = !status;
|
||||
|
@ -739,10 +780,18 @@ impl ShapeState {
|
|||
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>> {
|
||||
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>) {
|
||||
responses.add(GraphOperationMessage::Vector {
|
||||
layer,
|
||||
|
@ -769,7 +818,7 @@ impl ShapeState {
|
|||
let Some((start, _end, bezier)) = vector_data.segment_points_from_id(segment) else { continue };
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -853,6 +902,9 @@ impl ShapeState {
|
|||
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<_>>();
|
||||
|
||||
// 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
|
||||
let points = handles.iter().map(|handle| vector_data.other_point(handle.segment, point_id));
|
||||
let anchor_positions = points
|
||||
|
@ -894,7 +946,7 @@ impl ShapeState {
|
|||
handle_direction *= -1.;
|
||||
}
|
||||
|
||||
if non_zero_handles != 0 {
|
||||
if non_zero_handles != 0 && !linear_segments {
|
||||
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 Some(direction) = non_zero_handle
|
||||
|
@ -1015,9 +1067,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)]
|
||||
pub fn move_selected_points(
|
||||
pub fn move_selected_points_and_segments(
|
||||
&self,
|
||||
handle_lengths: Option<OpposingHandleLengths>,
|
||||
document: &DocumentMessageHandler,
|
||||
|
@ -1043,7 +1095,17 @@ impl ShapeState {
|
|||
};
|
||||
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) {
|
||||
continue;
|
||||
}
|
||||
|
@ -1058,7 +1120,7 @@ impl ShapeState {
|
|||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -1077,7 +1139,7 @@ impl ShapeState {
|
|||
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.
|
||||
let handles = [handle, other];
|
||||
let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false };
|
||||
|
@ -1128,12 +1190,12 @@ impl ShapeState {
|
|||
// ii) The anchor is not selected.
|
||||
|
||||
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 {
|
||||
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 {
|
||||
[true, false] => handles[1],
|
||||
|
@ -1211,11 +1273,15 @@ impl ShapeState {
|
|||
continue;
|
||||
};
|
||||
|
||||
let selected_segments = &state.selected_segments;
|
||||
|
||||
for point in std::mem::take(&mut state.selected_points) {
|
||||
match point {
|
||||
ManipulatorPointId::Anchor(anchor) => {
|
||||
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);
|
||||
}
|
||||
|
@ -1260,6 +1326,8 @@ impl ShapeState {
|
|||
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
|
||||
let [handle_start, handle_end] = [start, end].map(|(handle, _)| {
|
||||
let handle = handle.opposite();
|
||||
|
@ -1307,6 +1375,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>) {
|
||||
for (&layer, state) in &self.selected_shape_state {
|
||||
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { continue };
|
||||
|
@ -1696,10 +1778,13 @@ impl ShapeState {
|
|||
.filter(|&handle| anchor.abs_diff_eq(handle, 1e-5))
|
||||
.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
|
||||
for point in self.selected_points() {
|
||||
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);
|
||||
} else {
|
||||
for handle in vector_data.all_connected(point_id) {
|
||||
|
@ -1751,6 +1836,7 @@ impl ShapeState {
|
|||
false
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn select_all_in_shape(
|
||||
&mut self,
|
||||
network_interface: &NodeNetworkInterface,
|
||||
|
@ -1758,13 +1844,17 @@ impl ShapeState {
|
|||
selection_change: SelectionChange,
|
||||
path_overlay_mode: PathOverlayMode,
|
||||
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_segments = selected_segments(network_interface, self);
|
||||
|
||||
for (&layer, state) in &mut self.selected_shape_state {
|
||||
if selection_change == SelectionChange::Clear {
|
||||
state.clear_points()
|
||||
state.clear_points();
|
||||
state.clear_segments();
|
||||
}
|
||||
|
||||
let vector_data = network_interface.compute_modified_vector(layer);
|
||||
|
@ -1790,7 +1880,46 @@ impl ShapeState {
|
|||
None
|
||||
};
|
||||
|
||||
// Selection segments
|
||||
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))] {
|
||||
let Some(position) = position else { continue };
|
||||
let transformed_position = transform.transform_point2(position);
|
||||
|
@ -1823,6 +1952,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()) {
|
||||
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::tool_messages::path_tool::PathOverlayMode;
|
||||
use crate::messages::tool::utility_types::ToolType;
|
||||
use bezier_rs::Bezier;
|
||||
use bezier_rs::{Bezier, BezierHandles};
|
||||
use glam::{DAffine2, DVec2};
|
||||
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 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.
|
||||
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]);
|
||||
};
|
||||
|
||||
let buzz_face = font_cache.get(font).map(|data| load_face(data));
|
||||
let far = graphene_std::text::bounding_box(text, buzz_face.as_ref(), typesetting, false);
|
||||
let font_data = font_cache.get(font).map(|data| load_font(data));
|
||||
let far = graphene_std::text::bounding_box(text, font_data, typesetting, false);
|
||||
|
||||
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(
|
||||
document: &DocumentMessageHandler,
|
||||
responses: &mut VecDeque<Message>,
|
||||
|
@ -221,7 +287,7 @@ pub fn resize_bounds(
|
|||
let snap = Some(SizeSnapData {
|
||||
manager: snap_manager,
|
||||
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 (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);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn rotate_bounds(
|
||||
document: &DocumentMessageHandler,
|
||||
responses: &mut VecDeque<Message>,
|
||||
|
@ -280,7 +347,7 @@ pub fn rotate_bounds(
|
|||
let mut selected = Selected::new(
|
||||
&mut bounds.original_transforms,
|
||||
&mut bounds.center_of_transformation,
|
||||
&dragging_layers,
|
||||
dragging_layers,
|
||||
responses,
|
||||
&document.network_interface,
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -365,7 +432,7 @@ pub fn transforming_transform_cage(
|
|||
let mut selected = Selected::new(
|
||||
&mut bounds.original_transforms,
|
||||
&mut bounds.center_of_transformation,
|
||||
&layers_dragging,
|
||||
layers_dragging,
|
||||
responses,
|
||||
&document.network_interface,
|
||||
None,
|
||||
|
@ -423,7 +490,7 @@ pub fn transforming_transform_cage(
|
|||
}
|
||||
|
||||
// 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.
|
||||
|
|
|
@ -33,6 +33,7 @@ pub struct PathTool {
|
|||
#[derive(Default)]
|
||||
pub struct PathToolOptions {
|
||||
path_overlay_mode: PathOverlayMode,
|
||||
path_editing_mode: PathEditingMode,
|
||||
}
|
||||
|
||||
#[impl_message(Message, ToolMessage, Path)]
|
||||
|
@ -70,6 +71,7 @@ pub enum PathToolMessage {
|
|||
lasso_select: Key,
|
||||
handle_drag_from_anchor: Key,
|
||||
drag_restore_handle: Key,
|
||||
molding_in_segment_edit: Key,
|
||||
},
|
||||
NudgeSelectedPoints {
|
||||
delta_x: f64,
|
||||
|
@ -123,9 +125,26 @@ pub enum PathOverlayMode {
|
|||
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)]
|
||||
pub enum PathOptionsUpdate {
|
||||
OverlayModeType(PathOverlayMode),
|
||||
PointEditingMode { enabled: bool },
|
||||
SegmentEditingMode { enabled: bool },
|
||||
}
|
||||
|
||||
impl ToolMetadata for PathTool {
|
||||
|
@ -210,6 +229,19 @@ impl LayoutHolder for PathTool {
|
|||
.for_checkbox(&mut checkbox_id)
|
||||
.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![
|
||||
RadioEntryData::new("all")
|
||||
.icon("HandleVisibilityAll")
|
||||
|
@ -234,8 +266,12 @@ impl LayoutHolder for PathTool {
|
|||
y_location,
|
||||
unrelated_seperator.clone(),
|
||||
colinear_handle_checkbox,
|
||||
related_seperator,
|
||||
related_seperator.clone(),
|
||||
colinear_handles_label,
|
||||
unrelated_seperator.clone(),
|
||||
point_editing_mode,
|
||||
related_seperator.clone(),
|
||||
segment_editing_mode,
|
||||
unrelated_seperator,
|
||||
path_overlay_mode_widget,
|
||||
],
|
||||
|
@ -253,6 +289,14 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
|
|||
self.options.path_overlay_mode = overlay_mode_type;
|
||||
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) => {
|
||||
responses.add(DocumentMessage::AddTransaction);
|
||||
|
@ -416,6 +460,7 @@ struct PathToolData {
|
|||
angle: f64,
|
||||
opposite_handle_position: Option<DVec2>,
|
||||
last_clicked_point_was_selected: bool,
|
||||
last_clicked_segment_was_selected: bool,
|
||||
snapping_axis: Option<Axis>,
|
||||
alt_clicked_on_anchor: bool,
|
||||
alt_dragging_from_anchor: bool,
|
||||
|
@ -427,6 +472,7 @@ struct PathToolData {
|
|||
frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>,
|
||||
adjacent_anchor_offset: Option<DVec2>,
|
||||
sliding_point_info: Option<SlidingPointInfo>,
|
||||
started_drawing_from_inside: bool,
|
||||
}
|
||||
|
||||
impl PathToolData {
|
||||
|
@ -487,6 +533,7 @@ impl PathToolData {
|
|||
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)]
|
||||
fn mouse_down(
|
||||
&mut self,
|
||||
|
@ -498,7 +545,10 @@ impl PathToolData {
|
|||
lasso_select: bool,
|
||||
handle_drag_from_anchor: bool,
|
||||
drag_zero_handle: bool,
|
||||
molding_in_segment_edit: bool,
|
||||
path_overlay_mode: PathOverlayMode,
|
||||
segment_editing_mode: bool,
|
||||
point_editing_mode: bool,
|
||||
) -> PathToolFsmState {
|
||||
self.double_click_handled = false;
|
||||
self.opposing_handle_lengths = None;
|
||||
|
@ -521,6 +571,7 @@ impl PathToolData {
|
|||
SELECTION_THRESHOLD,
|
||||
path_overlay_mode,
|
||||
self.frontier_handles_info.clone(),
|
||||
point_editing_mode,
|
||||
) {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
|
@ -604,25 +655,51 @@ impl PathToolData {
|
|||
}
|
||||
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
|
||||
else if let Some(closed_segment) = &mut self.segment {
|
||||
// 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(segment) = shape_editor.upper_closest_segment(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD) {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
// Calculating and storing handle positions
|
||||
let handle1 = ManipulatorPointId::PrimaryHandle(closed_segment.segment());
|
||||
let handle2 = ManipulatorPointId::EndHandle(closed_segment.segment());
|
||||
if segment_editing_mode && !molding_in_segment_edit {
|
||||
let layer = segment.layer();
|
||||
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 let (Some(pos1), Some(pos2)) = (handle1.get_position(&vector_data), handle2.get_position(&vector_data)) {
|
||||
self.molding_info = Some((pos1, pos2))
|
||||
if !(already_selected && extend_selection) {
|
||||
let retain_existing_selection = extend_selection || already_selected;
|
||||
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) {
|
||||
shape_editor.deselect_all_points();
|
||||
shape_editor.deselect_all_segments();
|
||||
if extend_selection {
|
||||
responses.add(NodeGraphMessage::SelectedNodesAdd { nodes: vec![layer.to_node()] });
|
||||
} else {
|
||||
|
@ -631,9 +708,10 @@ impl PathToolData {
|
|||
self.drag_start_pos = 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
|
||||
else {
|
||||
|
@ -655,7 +733,7 @@ impl PathToolData {
|
|||
let transform = document.metadata().transform_to_document(layer);
|
||||
|
||||
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 };
|
||||
layer_manipulators.insert(anchor);
|
||||
let Some([handle1, handle2]) = point.get_handle_pair(&vector_data) else { continue };
|
||||
|
@ -759,7 +837,7 @@ impl PathToolData {
|
|||
}
|
||||
|
||||
// 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 layer_to_document = document.metadata().transform_to_document(*layer);
|
||||
|
@ -886,7 +964,7 @@ impl PathToolData {
|
|||
let drag_start = self.drag_start_pos;
|
||||
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
|
||||
let delta = current_mouse - drag_start;
|
||||
|
@ -898,7 +976,7 @@ impl PathToolData {
|
|||
_ => 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>) {
|
||||
|
@ -914,12 +992,12 @@ impl PathToolData {
|
|||
_ => 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
|
||||
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;
|
||||
}
|
||||
|
@ -941,6 +1019,28 @@ impl PathToolData {
|
|||
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 {
|
||||
let single_anchor_selected = shape_editor.selected_points().count() == 1 && shape_editor.selected_points().any(|point| matches!(point, ManipulatorPointId::Anchor(_)));
|
||||
|
||||
|
@ -1203,7 +1303,7 @@ impl PathToolData {
|
|||
self.temporary_colinear_handles = false;
|
||||
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);
|
||||
} else {
|
||||
let Some(axis) = self.snapping_axis else { return };
|
||||
|
@ -1212,7 +1312,7 @@ impl PathToolData {
|
|||
Axis::Y => DVec2::new(0., unsnapped_delta.y),
|
||||
_ => 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);
|
||||
}
|
||||
|
||||
|
@ -1236,7 +1336,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 {
|
||||
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 };
|
||||
match (self, event) {
|
||||
|
@ -1320,48 +1420,43 @@ impl Fsm for PathToolFsmState {
|
|||
|
||||
match self {
|
||||
Self::Ready => {
|
||||
// Check if there is no point nearby
|
||||
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);
|
||||
}
|
||||
tool_data.update_closest_segment(shape_editor, input.mouse.position, document, tool_options.path_overlay_mode);
|
||||
|
||||
if let Some(closest_segment) = &tool_data.segment {
|
||||
let perp = closest_segment.calculate_perp(document);
|
||||
let point = closest_segment.closest_point(document.metadata());
|
||||
if tool_options.path_editing_mode.segment_editing_mode {
|
||||
let transform = document.metadata().transform_to_viewport(closest_segment.layer());
|
||||
|
||||
// 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.outline_overlay_bezier(closest_segment.bezier(), transform);
|
||||
|
||||
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);
|
||||
// Draw the anchors again
|
||||
let display_anchors = overlay_context.visibility_settings.anchors();
|
||||
if display_anchors {
|
||||
let start_pos = transform.transform_point2(closest_segment.bezier().start);
|
||||
let end_pos = transform.transform_point2(closest_segment.bezier().end);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1381,11 +1476,13 @@ impl Fsm for PathToolFsmState {
|
|||
let quad = tool_data.selection_quad(document.metadata());
|
||||
let polygon = &tool_data.lasso_polygon;
|
||||
|
||||
match (selection_shape, selection_mode) {
|
||||
(SelectionShapeType::Box, SelectionMode::Enclosed) => overlay_context.dashed_quad(quad, None, fill_color, Some(4.), Some(4.), Some(0.5)),
|
||||
(SelectionShapeType::Lasso, SelectionMode::Enclosed) => overlay_context.dashed_polygon(polygon, None, fill_color, Some(4.), Some(4.), Some(0.5)),
|
||||
(SelectionShapeType::Box, _) => overlay_context.quad(quad, None, fill_color),
|
||||
(SelectionShapeType::Lasso, _) => overlay_context.polygon(polygon, None, fill_color),
|
||||
match (selection_shape, selection_mode, tool_data.started_drawing_from_inside) {
|
||||
// Don't draw box if it is from inside a shape and selection just began
|
||||
(SelectionShapeType::Box, SelectionMode::Enclosed, false) => overlay_context.dashed_quad(quad, None, fill_color, Some(4.), Some(4.), Some(0.5)),
|
||||
(SelectionShapeType::Lasso, SelectionMode::Enclosed, _) => overlay_context.dashed_polygon(polygon, None, fill_color, Some(4.), Some(4.), Some(0.5)),
|
||||
(SelectionShapeType::Box, _, false) => overlay_context.quad(quad, None, fill_color),
|
||||
(SelectionShapeType::Lasso, _, _) => overlay_context.polygon(polygon, None, fill_color),
|
||||
(SelectionShapeType::Box, _, _) => {}
|
||||
}
|
||||
}
|
||||
Self::Dragging(_) => {
|
||||
|
@ -1431,12 +1528,14 @@ impl Fsm for PathToolFsmState {
|
|||
lasso_select,
|
||||
handle_drag_from_anchor,
|
||||
drag_restore_handle,
|
||||
molding_in_segment_edit,
|
||||
},
|
||||
) => {
|
||||
let extend_selection = input.keyboard.get(extend_selection 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 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.lasso_polygon.clear();
|
||||
|
@ -1450,7 +1549,10 @@ impl Fsm for PathToolFsmState {
|
|||
lasso_select,
|
||||
handle_drag_from_anchor,
|
||||
drag_zero_handle,
|
||||
molding_in_segment_edit,
|
||||
tool_options.path_overlay_mode,
|
||||
tool_options.path_editing_mode.segment_editing_mode,
|
||||
tool_options.path_editing_mode.point_editing_mode,
|
||||
)
|
||||
}
|
||||
(
|
||||
|
@ -1466,6 +1568,7 @@ impl Fsm for PathToolFsmState {
|
|||
},
|
||||
) => {
|
||||
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 {
|
||||
extend_lasso(&mut tool_data.lasso_polygon, input.mouse.position);
|
||||
|
@ -1527,12 +1630,6 @@ impl Fsm for PathToolFsmState {
|
|||
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 initial_press = anchor_and_handle_toggled && !tool_data.select_anchor_toggled;
|
||||
let released_from_toggle = tool_data.select_anchor_toggled && !anchor_and_handle_toggled;
|
||||
|
@ -1718,6 +1815,11 @@ impl Fsm for PathToolFsmState {
|
|||
if tool_data.drag_start_pos == previous_mouse {
|
||||
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![] });
|
||||
} 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 {
|
||||
SelectionShapeType::Box => {
|
||||
let bbox = [tool_data.drag_start_pos, previous_mouse];
|
||||
|
@ -1727,6 +1829,8 @@ impl Fsm for PathToolFsmState {
|
|||
selection_change,
|
||||
tool_options.path_overlay_mode,
|
||||
tool_data.frontier_handles_info.clone(),
|
||||
tool_options.path_editing_mode.segment_editing_mode,
|
||||
selection_mode,
|
||||
);
|
||||
}
|
||||
SelectionShapeType::Lasso => shape_editor.select_all_in_shape(
|
||||
|
@ -1735,6 +1839,8 @@ impl Fsm for PathToolFsmState {
|
|||
selection_change,
|
||||
tool_options.path_overlay_mode,
|
||||
tool_data.frontier_handles_info.clone(),
|
||||
tool_options.path_editing_mode.segment_editing_mode,
|
||||
selection_mode,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
@ -1746,6 +1852,7 @@ impl Fsm for PathToolFsmState {
|
|||
(PathToolFsmState::Dragging { .. }, PathToolMessage::Escape | PathToolMessage::RightClick) => {
|
||||
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_segments();
|
||||
shape_editor.select_points_by_manipulator_id(&tool_data.saved_points_before_handle_drag);
|
||||
|
||||
tool_data.saved_points_before_handle_drag.clear();
|
||||
|
@ -1794,8 +1901,17 @@ impl Fsm for PathToolFsmState {
|
|||
|
||||
let document_to_viewport = document.metadata().document_to_viewport;
|
||||
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 {
|
||||
match selection_shape {
|
||||
SelectionShapeType::Box => {
|
||||
|
@ -1806,6 +1922,8 @@ impl Fsm for PathToolFsmState {
|
|||
select_kind,
|
||||
tool_options.path_overlay_mode,
|
||||
tool_data.frontier_handles_info.clone(),
|
||||
tool_options.path_editing_mode.segment_editing_mode,
|
||||
selection_mode,
|
||||
);
|
||||
}
|
||||
SelectionShapeType::Lasso => shape_editor.select_all_in_shape(
|
||||
|
@ -1814,6 +1932,8 @@ impl Fsm for PathToolFsmState {
|
|||
select_kind,
|
||||
tool_options.path_overlay_mode,
|
||||
tool_data.frontier_handles_info.clone(),
|
||||
tool_options.path_editing_mode.segment_editing_mode,
|
||||
selection_mode,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
@ -1834,32 +1954,31 @@ impl Fsm for PathToolFsmState {
|
|||
tool_data.frontier_handles_info.clone(),
|
||||
);
|
||||
|
||||
let nearest_segment = tool_data.segment.clone();
|
||||
|
||||
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 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());
|
||||
responses.add(DocumentMessage::EndTransaction);
|
||||
}
|
||||
} else {
|
||||
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.molding_info = None;
|
||||
tool_data.molding_segment = false;
|
||||
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 {
|
||||
let clicked_selected = shape_editor.selected_points().any(|&point| nearest_point == point);
|
||||
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 {
|
||||
shape_editor.selected_shape_state.entry(layer).or_default().deselect_point(nearest_point);
|
||||
} else {
|
||||
|
@ -1867,6 +1986,49 @@ impl Fsm for PathToolFsmState {
|
|||
}
|
||||
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 {
|
||||
|
@ -1892,25 +2054,6 @@ impl Fsm for PathToolFsmState {
|
|||
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.sliding_point_info = None;
|
||||
|
||||
|
@ -1926,6 +2069,7 @@ impl Fsm for PathToolFsmState {
|
|||
(_, PathToolMessage::Delete) => {
|
||||
// Delete the selected points and clean up overlays
|
||||
responses.add(DocumentMessage::AddTransaction);
|
||||
shape_editor.delete_selected_segments(document, responses);
|
||||
shape_editor.delete_selected_points(document, responses);
|
||||
responses.add(PathToolMessage::SelectionChanged);
|
||||
|
||||
|
@ -2024,6 +2168,7 @@ impl Fsm for PathToolFsmState {
|
|||
if let Some(layer) = document.click(input) {
|
||||
// Select all points in the layer
|
||||
shape_editor.select_connected_anchors(document, layer, input.mouse.position);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
}
|
||||
|
||||
PathToolFsmState::Ready
|
||||
|
@ -2033,7 +2178,7 @@ impl Fsm for PathToolFsmState {
|
|||
PathToolFsmState::Ready
|
||||
}
|
||||
(_, PathToolMessage::NudgeSelectedPoints { delta_x, delta_y }) => {
|
||||
shape_editor.move_selected_points(
|
||||
shape_editor.move_selected_points_and_segments(
|
||||
tool_data.opposing_handle_lengths.take(),
|
||||
document,
|
||||
(delta_x, delta_y).into(),
|
||||
|
@ -2113,10 +2258,6 @@ enum SelectionStatus {
|
|||
}
|
||||
|
||||
impl SelectionStatus {
|
||||
fn is_none(&self) -> bool {
|
||||
self == &SelectionStatus::None
|
||||
}
|
||||
|
||||
fn as_one(&self) -> Option<&SingleSelectedPoint> {
|
||||
match self {
|
||||
SelectionStatus::One(one) => Some(one),
|
||||
|
@ -2246,9 +2387,7 @@ fn calculate_lock_angle(
|
|||
}
|
||||
|
||||
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 {
|
||||
return None;
|
||||
};
|
||||
let (anchor, handle_position) = handle_id.get_anchor(vector_data).zip(handle_id.get_position(vector_data))?;
|
||||
|
||||
let check_if_close = |point_id: &PointId| {
|
||||
let Some(anchor_position) = vector_data.point_domain.position_from_id(*point_id) else {
|
||||
|
@ -2257,7 +2396,7 @@ fn check_handle_over_adjacent_anchor(handle_id: ManipulatorPointId, vector_data:
|
|||
(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(
|
||||
currently_dragged_handle: ManipulatorPointId,
|
||||
|
@ -2313,7 +2452,7 @@ fn calculate_adjacent_anchor_tangent(
|
|||
};
|
||||
|
||||
let angle = shared_segment_handle
|
||||
.get_position(&vector_data)
|
||||
.get_position(vector_data)
|
||||
.zip(adjacent_anchor_position)
|
||||
.map(|(handle, anchor)| -(handle - anchor).angle_to(DVec2::X));
|
||||
|
||||
|
@ -2324,7 +2463,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
|
||||
|
||||
let hint_data = match state {
|
||||
|
@ -2358,12 +2504,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());
|
||||
}
|
||||
|
||||
let mut hint_data = 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()]),
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Insert Point on Segment")]),
|
||||
HintGroup(vec![HintInfo::keys_and_mouse([Key::Alt], MouseMotion::Lmb, "Delete Segment")]),
|
||||
];
|
||||
let mut hint_data = match (tool_data.segment.is_some(), tool_options.path_editing_mode.segment_editing_mode) {
|
||||
(true, true) => {
|
||||
vec![
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Select Segment"), HintInfo::keys([Key::Shift], "Extend").prepend_plus()]),
|
||||
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 {
|
||||
// TODO: Dynamically show either "Smooth" or "Sharp" based on the current state
|
||||
|
|
|
@ -18,7 +18,7 @@ use graph_craft::document::value::TaggedValue;
|
|||
use graph_craft::document::{NodeId, NodeInput};
|
||||
use graphene_std::Color;
|
||||
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;
|
||||
|
||||
#[derive(Default)]
|
||||
|
@ -35,6 +35,7 @@ pub struct TextOptions {
|
|||
font_name: String,
|
||||
font_style: String,
|
||||
fill: ToolColorOptions,
|
||||
tilt: f64,
|
||||
}
|
||||
|
||||
impl Default for TextOptions {
|
||||
|
@ -42,10 +43,11 @@ impl Default for TextOptions {
|
|||
Self {
|
||||
font_size: 24.,
|
||||
line_height_ratio: 1.2,
|
||||
character_spacing: 1.,
|
||||
character_spacing: 0.,
|
||||
font_name: graphene_std::consts::DEFAULT_FONT_FAMILY.into(),
|
||||
font_style: graphene_std::consts::DEFAULT_FONT_STYLE.into(),
|
||||
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(),
|
||||
});
|
||||
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 far = graphene_std::text::bounding_box(&tool_data.new_text, buzz_face.as_ref(), editing_text.typesetting, false);
|
||||
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, font_data, editing_text.typesetting, false);
|
||||
if far.x != 0. && far.y != 0. {
|
||||
let quad = Quad::from_box([DVec2::ZERO, far]);
|
||||
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
|
||||
let transformed_quad = layer_transform * bounds;
|
||||
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));
|
||||
if lines_clipping(text.as_str(), buzz_face, typesetting) {
|
||||
let font_data = font_cache.get(font).map(|data| load_font(data));
|
||||
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.));
|
||||
}
|
||||
}
|
||||
|
@ -784,6 +786,7 @@ impl Fsm for TextToolFsmState {
|
|||
max_width: constraint_size.map(|size| size.x),
|
||||
character_spacing: tool_options.character_spacing,
|
||||
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()),
|
||||
color: tool_options.fill.active_color(),
|
||||
|
|
|
@ -180,14 +180,28 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
|||
*selected.pivot = selected.mean_average_of_pivots();
|
||||
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());
|
||||
} 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();
|
||||
|
||||
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));
|
||||
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;
|
||||
|
||||
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 } => {
|
||||
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)
|
||||
|| selected_layers.is_empty()
|
||||
|| transform_type.equivalent_to(self.transform_operation)
|
||||
|
|
|
@ -227,9 +227,7 @@ impl NodeRuntime {
|
|||
}
|
||||
|
||||
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());
|
||||
|
||||
|
|
3
frontend/assets/icon-12px-solid/dot.svg
Normal file
3
frontend/assets/icon-12px-solid/dot.svg
Normal 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 |
|
@ -760,6 +760,8 @@
|
|||
|
||||
.text-input {
|
||||
word-break: break-all;
|
||||
unicode-bidi: plaintext;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.text-input div {
|
||||
|
@ -773,6 +775,8 @@
|
|||
overflow-wrap: anywhere;
|
||||
white-space: pre-wrap;
|
||||
word-break: normal;
|
||||
unicode-bidi: plaintext;
|
||||
text-align: left;
|
||||
display: inline-block;
|
||||
// Workaround to force Chrome to display the flashing text entry cursor when text is empty
|
||||
padding-left: 1px;
|
||||
|
|
|
@ -158,6 +158,7 @@
|
|||
background: none;
|
||||
color: var(--color-e-nearwhite);
|
||||
caret-color: var(--color-e-nearwhite);
|
||||
unicode-bidi: plaintext;
|
||||
|
||||
&::selection {
|
||||
background-color: var(--color-4-dimgray);
|
||||
|
|
|
@ -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 CloseX from "@graphite-frontend/assets/icon-12px-solid/close-x.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 Edit12px from "@graphite-frontend/assets/icon-12px-solid/edit-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 },
|
||||
CloseX: { svg: CloseX, size: 12 },
|
||||
Delay: { svg: Delay, size: 12 },
|
||||
Dot: { svg: Dot, size: 12 },
|
||||
DropdownArrow: { svg: DropdownArrow, size: 12 },
|
||||
Edit12px: { svg: Edit12px, size: 12 },
|
||||
Empty12px: { svg: Empty12px, size: 12 },
|
||||
|
|
|
@ -29,10 +29,11 @@ ctor = { workspace = true }
|
|||
rand_chacha = { workspace = true }
|
||||
bezier-rs = { workspace = true }
|
||||
specta = { workspace = true }
|
||||
rustybuzz = { workspace = true }
|
||||
image = { workspace = true }
|
||||
half = { workspace = true }
|
||||
tinyvec = { workspace = true }
|
||||
parley = { workspace = true }
|
||||
skrifa = { workspace = true }
|
||||
kurbo = { workspace = true }
|
||||
log = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
|
|
|
@ -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`)
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default, PartialEq, DynAny)]
|
||||
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>>,
|
||||
/// Web font preview URLs used for showing fonts when live editing
|
||||
preview_urls: HashMap<Font, String>,
|
||||
}
|
||||
|
||||
impl FontCache {
|
||||
/// 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> {
|
||||
|
@ -40,7 +41,7 @@ impl FontCache {
|
|||
}
|
||||
|
||||
/// 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))
|
||||
}
|
||||
|
||||
|
|
|
@ -1,29 +1,67 @@
|
|||
use crate::vector::PointId;
|
||||
use bezier_rs::{ManipulatorGroup, Subpath};
|
||||
use glam::DVec2;
|
||||
use rustybuzz::ttf_parser::{GlyphId, OutlineBuilder};
|
||||
use rustybuzz::{GlyphBuffer, UnicodeBuffer};
|
||||
use core::cell::RefCell;
|
||||
use glam::{DAffine2, DVec2};
|
||||
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>,
|
||||
glyph_subpaths: Vec<Subpath<PointId>>,
|
||||
other_subpaths: Vec<Subpath<PointId>>,
|
||||
text_cursor: DVec2,
|
||||
offset: DVec2,
|
||||
ascender: f64,
|
||||
origin: DVec2,
|
||||
scale: f64,
|
||||
id: PointId,
|
||||
}
|
||||
|
||||
impl Builder {
|
||||
impl PathBuilder {
|
||||
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) {
|
||||
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()));
|
||||
}
|
||||
|
@ -47,36 +85,10 @@ impl OutlineBuilder for Builder {
|
|||
|
||||
fn close(&mut self) {
|
||||
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)]
|
||||
pub struct TypesettingConfig {
|
||||
pub font_size: f64,
|
||||
|
@ -84,6 +96,7 @@ pub struct TypesettingConfig {
|
|||
pub character_spacing: f64,
|
||||
pub max_width: Option<f64>,
|
||||
pub max_height: Option<f64>,
|
||||
pub tilt: f64,
|
||||
}
|
||||
|
||||
impl Default for TypesettingConfig {
|
||||
|
@ -91,163 +104,130 @@ impl Default for TypesettingConfig {
|
|||
Self {
|
||||
font_size: 24.,
|
||||
line_height_ratio: 1.2,
|
||||
character_spacing: 1.,
|
||||
character_spacing: 0.,
|
||||
max_width: None,
|
||||
max_height: None,
|
||||
tilt: 0.,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_path(str: &str, buzz_face: Option<rustybuzz::Face>, typesetting: TypesettingConfig) -> Vec<Subpath<PointId>> {
|
||||
let Some(buzz_face) = buzz_face else { return vec![] };
|
||||
let space_glyph = buzz_face.glyph_index(' ');
|
||||
fn render_glyph_run(glyph_run: &GlyphRun<'_, ()>, path_builder: &mut PathBuilder, tilt: f64) {
|
||||
let mut run_x = glyph_run.offset();
|
||||
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),
|
||||
glyph_subpaths: Vec::new(),
|
||||
other_subpaths: Vec::new(),
|
||||
text_cursor: DVec2::ZERO,
|
||||
offset: DVec2::ZERO,
|
||||
ascender: (buzz_face.ascender() as f64 / buzz_face.height() as f64) * typesetting.font_size / scale,
|
||||
scale,
|
||||
origin: DVec2::ZERO,
|
||||
scale: layout.scale() as f64,
|
||||
id: PointId::ZERO,
|
||||
};
|
||||
|
||||
for line in str.split('\n') {
|
||||
for (index, word) in SplitWordsIncludingSpaces::new(line).enumerate() {
|
||||
push_str(&mut buffer, word);
|
||||
let glyph_buffer = rustybuzz::shape(&buzz_face, &[], buffer);
|
||||
|
||||
// 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 line in layout.lines() {
|
||||
for item in line.items() {
|
||||
if let PositionedLayoutItem::GlyphRun(glyph_run) = item {
|
||||
render_glyph_run(&glyph_run, &mut path_builder, typesetting.tilt);
|
||||
}
|
||||
|
||||
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 {
|
||||
// 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];
|
||||
pub fn bounding_box(str: &str, font_data: Option<Blob<u8>>, typesetting: TypesettingConfig, for_clipping_test: bool) -> DVec2 {
|
||||
if !for_clipping_test {
|
||||
if let (Some(max_height), Some(max_width)) = (typesetting.max_height, typesetting.max_width) {
|
||||
return DVec2::new(max_width, max_height);
|
||||
}
|
||||
}
|
||||
|
||||
for line in str.split('\n') {
|
||||
for (index, word) in SplitWordsIncludingSpaces::new(line).enumerate() {
|
||||
push_str(&mut buffer, word);
|
||||
let Some(layout) = layout_text(str, font_data, typesetting) else { return DVec2::ZERO };
|
||||
|
||||
let glyph_buffer = rustybuzz::shape(buzz_face, &[], buffer);
|
||||
|
||||
// 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
|
||||
DVec2::new(layout.full_width() as f64, layout.height() as f64)
|
||||
}
|
||||
|
||||
pub fn load_face(data: &[u8]) -> rustybuzz::Face<'_> {
|
||||
rustybuzz::Face::from_slice(data, 0).expect("Loading font failed")
|
||||
pub fn load_font(data: &[u8]) -> Blob<u8> {
|
||||
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 bounds = bounding_box(str, buzz_face.as_ref(), typesetting, true);
|
||||
let bounds = bounding_box(str, font_data, typesetting, true);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,10 +19,10 @@ async fn transform<T: 'n + 'static>(
|
|||
translate: DVec2,
|
||||
rotate: f64,
|
||||
scale: DVec2,
|
||||
shear: DVec2,
|
||||
skew: DVec2,
|
||||
_pivot: DVec2,
|
||||
) -> 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();
|
||||
|
||||
|
|
|
@ -11,13 +11,14 @@ use crate::transform::Transform;
|
|||
use crate::vector::click_target::{ClickTargetType, FreePoint};
|
||||
use crate::{AlphaBlending, Color, GraphicGroupTable};
|
||||
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 glam::{DAffine2, DVec2};
|
||||
pub use indexed::VectorDataIndex;
|
||||
use kurbo::{Affine, Rect, Shape};
|
||||
pub use modification::*;
|
||||
use std::borrow::Borrow;
|
||||
use std::collections::HashMap;
|
||||
|
||||
// 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]))
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub fn segment_ids(&self) -> &[SegmentId] {
|
||||
self.segment_domain.ids()
|
||||
|
|
|
@ -9,23 +9,37 @@ fn text<'i: 'n>(
|
|||
editor: &'i WasmEditorApi,
|
||||
text: String,
|
||||
font_name: Font,
|
||||
#[default(24.)] font_size: f64,
|
||||
#[default(1.2)] line_height_ratio: f64,
|
||||
#[default(1.)] character_spacing: f64,
|
||||
#[default(None)] max_width: Option<f64>,
|
||||
#[default(None)] max_height: Option<f64>,
|
||||
#[unit(" px")]
|
||||
#[default(24.)]
|
||||
font_size: f64,
|
||||
#[unit("x")]
|
||||
#[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 {
|
||||
let buzz_face = editor.font_cache.get(&font_name).map(|data| load_face(data));
|
||||
|
||||
let typesetting = TypesettingConfig {
|
||||
font_size,
|
||||
line_height_ratio,
|
||||
character_spacing,
|
||||
max_width,
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -61,7 +61,7 @@ pub fn generate_node_substitutions() -> HashMap<String, DocumentNode> {
|
|||
(
|
||||
NodeId(i as u64),
|
||||
match inputs.len() {
|
||||
1 => {
|
||||
1 if false => {
|
||||
let input = inputs.iter().next().unwrap();
|
||||
let input_ty = input.nested_type();
|
||||
|
||||
|
|
|
@ -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 class="block workflows">
|
||||
|
@ -276,7 +290,23 @@ Once it's ready to shine, Graphite's code architecture is structured to deliver
|
|||
</div>
|
||||
</section>
|
||||
<!-- ▙ 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 ▜ -->
|
||||
<section id="proceduralism" class="feature-box-outer">
|
||||
<div class="feature-box-inner">
|
||||
|
@ -363,23 +393,7 @@ Graphite's representation of artwork as a node graph lets you customize, compose
|
|||
</div>
|
||||
</section>
|
||||
<!-- ▙ 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 ▜ -->
|
||||
<section id="newsletter" class="feature-box-narrow">
|
||||
<div id="newsletter-success"><!-- Used only as a URL hash fragment anchor --></div>
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
title = "Graphite features"
|
||||
|
||||
[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>
|
||||
|
@ -17,6 +18,20 @@ In 2025, stay tuned for performance improvements, native multiplatform desktop a
|
|||
</div>
|
||||
</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>
|
||||
|
||||
<div class="diptych">
|
||||
|
|
|
@ -35,7 +35,9 @@ Comments should usually be placed on a separate line above the code they are ref
|
|||
|
||||
## 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
|
||||
|
||||
|
|
|
@ -151,19 +151,6 @@ body > .page {
|
|||
.heart.heart {
|
||||
// The same color is also used below in the SVG after the `%23` (URL-encoded `#`)
|
||||
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) {
|
||||
|
@ -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 {
|
||||
// padding: 32px 80px;
|
||||
// background: rgba(0, 0, 0, 0.0625);
|
||||
|
|
|
@ -1,3 +1,13 @@
|
|||
.video-container {
|
||||
background: var(--color-fog);
|
||||
|
||||
> div {
|
||||
margin: calc(20 * var(--variable-px)) auto;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
}
|
||||
}
|
||||
|
||||
#roadmap {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
|
|
|
@ -18,24 +18,25 @@
|
|||
#tagline {
|
||||
h1 {
|
||||
span {
|
||||
position: relative;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 100%;
|
||||
// Dimensions: 480x40
|
||||
height: 100%;
|
||||
margin-top: -0.2em;
|
||||
background: url("https://static.graphite.rs/textures/text-sketch-underline.png");
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
&::after {
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 100%;
|
||||
// Dimensions: 480x40
|
||||
height: 100%;
|
||||
margin-top: -0.2em;
|
||||
background: url("https://static.graphite.rs/textures/text-sketch-underline.png");
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
}
|
||||
}}
|
||||
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.2rem;
|
||||
|
||||
|
@ -112,15 +113,61 @@
|
|||
// ▛ OVERVIEW ▜
|
||||
#overview {
|
||||
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 ▟
|
||||
|
||||
// ▛ DONATE ▜
|
||||
#donate {
|
||||
h2 {
|
||||
color: #cc304f;
|
||||
}
|
||||
}
|
||||
// ▙ DONATE ▟
|
||||
|
||||
// ▛ PROCEDURALISM ▜
|
||||
#proceduralism {
|
||||
background-color: var(--color-slate);
|
||||
color: white;
|
||||
margin-top: 0;
|
||||
|
||||
|
||||
.diptych {
|
||||
background: black;
|
||||
color: var(--color-fog);
|
||||
|
@ -183,9 +230,6 @@
|
|||
}
|
||||
// ▙ PROCEDURALISM ▟
|
||||
|
||||
// ▛ DONATE ▜
|
||||
// ▙ DONATE ▟
|
||||
|
||||
// ▛ NEWSLETTER ▜
|
||||
#newsletter {
|
||||
background-color: var(--color-peach);
|
||||
|
@ -355,7 +399,7 @@
|
|||
margin: calc(20 * var(--variable-px)) auto;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
|
||||
|
||||
+ p {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
@ -377,7 +421,7 @@
|
|||
// ▛ RECENT NEWS ▜
|
||||
#recent-news {
|
||||
background-color: var(--color-parchment);
|
||||
|
||||
|
||||
.banner img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue