Merge branch 'master' into fix-range

This commit is contained in:
mTvare
2025-07-01 07:57:00 +05:30
committed by GitHub
280 changed files with 15589 additions and 28122 deletions
+2 -2
View File
@@ -5,8 +5,6 @@ on:
branches:
- master
pull_request:
branches:
- master
env:
CARGO_TERM_COLOR: always
INDEX_HTML_HEAD_REPLACEMENT: <script defer data-domain="dev.graphite.rs" data-api="https://graphite.rs/visit/event" src="https://graphite.rs/visit/script.hash.js"></script>
@@ -84,6 +82,8 @@ jobs:
mold -run cargo fmt --all -- --check
- name: 🦀 Build Rust code
env:
RUSTFLAGS: -Dwarnings
run: |
mold -run cargo build --all-features
@@ -2,7 +2,6 @@ name: Clippy Check
on:
pull_request:
branches: [master]
types: [opened, reopened, synchronize, ready_for_review]
jobs:
@@ -2,7 +2,6 @@ name: Profiling Changes
on:
pull_request:
branches: [master]
env:
CARGO_TERM_COLOR: always
-2
View File
@@ -7,8 +7,6 @@ on:
paths:
- "libraries/rawkit/**"
pull_request:
branches:
- master
paths:
- "libraries/rawkit/**"
-2
View File
@@ -7,8 +7,6 @@ on:
paths:
- website/**
pull_request:
branches:
- master
paths:
- website/**
env:
+1 -1
View File
@@ -4,4 +4,4 @@ target/
perf.data*
profile.json
flamegraph.svg
.idea/
Generated
+79 -222
View File
@@ -230,15 +230,6 @@ dependencies = [
"libloading 0.8.6",
]
[[package]]
name = "async-mutex"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479db852db25d9dbf6204e6cb6253698f175c15726470f78af0d918e99d6156e"
dependencies = [
"event-listener",
]
[[package]]
name = "atk"
version = "0.18.2"
@@ -385,7 +376,6 @@ dependencies = [
"dyn-any",
"glam",
"kurbo",
"log",
"serde",
]
@@ -864,37 +854,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "compilation-client"
version = "0.1.0"
dependencies = [
"anyhow",
"dyn-any",
"gpu-compiler-bin-wrapper",
"gpu-executor",
"graph-craft",
"graphene-core",
"reqwest",
"serde_json",
"wgpu-executor",
]
[[package]]
name = "compilation-server"
version = "0.1.0"
dependencies = [
"anyhow",
"axum",
"futures",
"gpu-compiler-bin-wrapper",
"graph-craft",
"serde",
"serde_json",
"tempfile",
"tokio",
"tower-http",
]
[[package]]
name = "concurrent-queue"
version = "2.5.0"
@@ -1390,7 +1349,6 @@ version = "0.3.1"
dependencies = [
"dyn-any-derive",
"glam",
"log",
"reqwest",
]
@@ -1503,12 +1461,6 @@ dependencies = [
"num-traits",
]
[[package]]
name = "event-listener"
version = "2.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
[[package]]
name = "exr"
version = "1.73.0"
@@ -2030,8 +1982,6 @@ version = "0.29.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc46dd3ec48fdd8e693a98d2b8bafae273a2d54c1de02a2a7e3d57d501f39677"
dependencies = [
"bytemuck",
"libm",
"serde",
]
@@ -2151,19 +2101,6 @@ dependencies = [
"windows 0.58.0",
]
[[package]]
name = "gpu-compiler-bin-wrapper"
version = "0.1.0"
dependencies = [
"anyhow",
"gpu-executor",
"graph-craft",
"log",
"serde",
"serde_json",
"wgpu-executor",
]
[[package]]
name = "gpu-descriptor"
version = "0.3.1"
@@ -2184,41 +2121,22 @@ dependencies = [
"bitflags 2.9.0",
]
[[package]]
name = "gpu-executor"
version = "0.1.0"
dependencies = [
"anyhow",
"base64 0.22.1",
"bytemuck",
"dyn-any",
"futures",
"glam",
"graphene-core",
"log",
"node-macro",
"num-traits",
"serde",
"web-sys",
]
[[package]]
name = "graph-craft"
version = "0.1.0"
dependencies = [
"base64 0.22.1",
"bezier-rs",
"bytemuck",
"criterion",
"dyn-any",
"futures",
"glam",
"graph-craft",
"graphene-application-io",
"graphene-core",
"graphene-path-bool",
"graphene-svg-renderer",
"iai-callgrind",
"js-sys",
"log",
"num-traits",
"pretty_assertions",
"reqwest",
"rustc-hash 2.1.1",
@@ -2228,35 +2146,39 @@ dependencies = [
"tokio",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"wgpu-executor",
"winit",
]
[[package]]
name = "graphene-application-io"
version = "0.1.0"
dependencies = [
"dyn-any",
"glam",
"graphene-core",
"log",
"serde",
"web-sys",
"wgpu",
]
[[package]]
name = "graphene-cli"
version = "0.1.0"
dependencies = [
"bezier-rs",
"bitflags 2.9.0",
"chrono",
"clap",
"dyn-any",
"fern",
"futures",
"glam",
"gpu-executor",
"graph-craft",
"graphene-core",
"graphene-std",
"image",
"interpreted-executor",
"log",
"serde",
"serde_json",
"preprocessor",
"tokio",
"wasm-bindgen",
"wgpu",
"wgpu-executor",
]
@@ -2273,10 +2195,8 @@ dependencies = [
"glam",
"half",
"image",
"js-sys",
"kurbo",
"log",
"math-parser",
"node-macro",
"num-derive",
"num-traits",
@@ -2288,57 +2208,83 @@ dependencies = [
"serde",
"serde_json",
"specta",
"spirv-std",
"tinyvec",
"tokio",
"usvg",
"vello",
"wasm-bindgen",
"web-sys",
"wgpu",
]
[[package]]
name = "graphene-math-nodes"
version = "0.1.0"
dependencies = [
"glam",
"graphene-core",
"log",
"math-parser",
"node-macro",
"rand 0.9.0",
]
[[package]]
name = "graphene-path-bool"
version = "0.1.0"
dependencies = [
"bezier-rs",
"dyn-any",
"glam",
"graphene-core",
"log",
"node-macro",
"path-bool",
"serde",
"specta",
]
[[package]]
name = "graphene-std"
version = "0.1.0"
dependencies = [
"base64 0.22.1",
"bezier-rs",
"bytemuck",
"compilation-client",
"dyn-any",
"fastnoise-lite",
"futures",
"glam",
"gpu-compiler-bin-wrapper",
"gpu-executor",
"graph-craft",
"graphene-application-io",
"graphene-core",
"graphene-math-nodes",
"graphene-path-bool",
"graphene-svg-renderer",
"image",
"image-compare",
"js-sys",
"log",
"ndarray",
"node-macro",
"path-bool",
"rand 0.9.0",
"rand_chacha 0.9.0",
"reqwest",
"resvg",
"rustc-hash 2.1.1",
"serde",
"serde_json",
"tokio",
"url",
"usvg",
"vello",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"wgpu",
"wgpu-executor",
"wgpu-types",
"winit",
]
[[package]]
name = "graphene-svg-renderer"
version = "0.1.0"
dependencies = [
"base64 0.22.1",
"bezier-rs",
"dyn-any",
"glam",
"graphene-core",
"log",
"num-traits",
"serde",
"usvg",
"vello",
]
[[package]]
@@ -2352,8 +2298,6 @@ dependencies = [
"graphite-editor",
"log",
"ron",
"serde",
"serde_json",
"tauri",
"tauri-build",
"tauri-plugin-http",
@@ -2365,26 +2309,23 @@ dependencies = [
name = "graphite-editor"
version = "0.0.0"
dependencies = [
"async-mutex",
"bezier-rs",
"bitflags 2.9.0",
"convert_case 0.7.1",
"derivative",
"dyn-any",
"env_logger",
"futures",
"glam",
"gpu-executor",
"graph-craft",
"graphene-core",
"graphene-std",
"graphite-proc-macros",
"image",
"interpreted-executor",
"js-sys",
"kurbo",
"log",
"num_enum",
"once_cell",
"preprocessor",
"ron",
"serde",
"serde_json",
@@ -2403,7 +2344,6 @@ dependencies = [
name = "graphite-proc-macros"
version = "0.0.0"
dependencies = [
"convert_case 0.7.1",
"graphite-editor",
"proc-macro2",
"quote",
@@ -2415,16 +2355,12 @@ dependencies = [
name = "graphite-wasm"
version = "0.0.0"
dependencies = [
"bezier-rs",
"futures",
"glam",
"graph-craft",
"graphene-core",
"graphene-std",
"graphite-editor",
"js-sys",
"log",
"math-parser",
"ron",
"serde",
"serde-wasm-bindgen",
"wasm-bindgen",
@@ -2963,18 +2899,6 @@ dependencies = [
"zune-jpeg",
]
[[package]]
name = "image-compare"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96cd73af13ae2e7220a1c02fe7d6bb53be50612ba7fabbb5c88e7753645f1f3c"
dependencies = [
"image",
"itertools 0.12.1",
"rayon",
"thiserror 1.0.69",
]
[[package]]
name = "image-webp"
version = "0.1.3"
@@ -3063,17 +2987,13 @@ dependencies = [
"dyn-any",
"futures",
"glam",
"glob",
"gpu-executor",
"graph-craft",
"graphene-core",
"graphene-path-bool",
"graphene-std",
"iai-callgrind",
"log",
"num-traits",
"once_cell",
"serde",
"wgpu",
"wgpu-executor",
]
@@ -3478,7 +3398,6 @@ version = "0.0.0"
dependencies = [
"criterion",
"lazy_static",
"log",
"num-complex",
"pest",
"pest_derive",
@@ -3811,7 +3730,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
"libm",
]
[[package]]
@@ -3835,15 +3753,6 @@ dependencies = [
"syn 2.0.99",
]
[[package]]
name = "nvtx"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad2e855e8019f99e4b94ac33670eb4e4f570a2e044f3749a0b2c7f83b841e52c"
dependencies = [
"cc",
]
[[package]]
name = "objc"
version = "0.2.7"
@@ -4610,6 +4519,15 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "preprocessor"
version = "0.1.0"
dependencies = [
"graph-craft",
"graphene-std",
"interpreted-executor",
]
[[package]]
name = "presser"
version = "0.3.1"
@@ -5691,15 +5609,6 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook-registry"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
dependencies = [
"libc",
]
[[package]]
name = "simd-adler32"
version = "0.3.7"
@@ -5906,34 +5815,6 @@ dependencies = [
"bitflags 2.9.0",
]
[[package]]
name = "spirv-std"
version = "0.9.0"
source = "git+https://github.com/Rust-GPU/rust-gpu.git#6e2c84d4fe64e32df4c060c5a7f3e35a32e45421"
dependencies = [
"bitflags 1.3.2",
"glam",
"num-traits",
"spirv-std-macros",
"spirv-std-types",
]
[[package]]
name = "spirv-std-macros"
version = "0.9.0"
source = "git+https://github.com/Rust-GPU/rust-gpu.git#6e2c84d4fe64e32df4c060c5a7f3e35a32e45421"
dependencies = [
"proc-macro2",
"quote",
"spirv-std-types",
"syn 2.0.99",
]
[[package]]
name = "spirv-std-types"
version = "0.9.0"
source = "git+https://github.com/Rust-GPU/rust-gpu.git#6e2c84d4fe64e32df4c060c5a7f3e35a32e45421"
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
@@ -6643,9 +6524,7 @@ dependencies = [
"bytes",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.52.0",
@@ -6767,20 +6646,6 @@ dependencies = [
"tracing",
]
[[package]]
name = "tower-http"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697"
dependencies = [
"bitflags 2.9.0",
"bytes",
"http",
"pin-project-lite",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-layer"
version = "0.3.3"
@@ -7585,21 +7450,13 @@ name = "wgpu-executor"
version = "0.1.0"
dependencies = [
"anyhow",
"base64 0.22.1",
"bytemuck",
"dyn-any",
"futures",
"futures-intrusive",
"glam",
"gpu-executor",
"graphene-application-io",
"graphene-core",
"half",
"log",
"graphene-svg-renderer",
"node-macro",
"num-traits",
"nvtx",
"serde",
"spirv",
"vello",
"web-sys",
"wgpu",
+62 -24
View File
@@ -4,31 +4,33 @@ members = [
"proc-macros",
"frontend/wasm",
"frontend/src-tauri",
"node-graph/gapplication-io",
"node-graph/gcore",
"node-graph/gstd",
"node-graph/gmath-nodes",
"node-graph/gpath-bool",
"node-graph/graph-craft",
"node-graph/graphene-cli",
"node-graph/gsvg-renderer",
"node-graph/interpreted-executor",
"node-graph/node-macro",
"node-graph/compilation-server",
"node-graph/compilation-client",
"node-graph/wgpu-executor",
"node-graph/gpu-executor",
"node-graph/gpu-compiler/gpu-compiler-bin-wrapper",
"node-graph/preprocessor",
"libraries/dyn-any",
"libraries/path-bool",
"libraries/bezier-rs",
"libraries/math-parser",
"website/other/bezier-rs-demos/wasm",
]
exclude = ["node-graph/gpu-compiler"]
default-members = [
"editor",
"frontend/wasm",
"node-graph/gcore",
"node-graph/gstd",
"node-graph/gmath-nodes",
"node-graph/gpath-bool",
"node-graph/graph-craft",
"node-graph/graphene-cli",
"node-graph/gsvg-renderer",
"node-graph/interpreted-executor",
"node-graph/node-macro",
]
@@ -36,19 +38,26 @@ resolver = "2"
[workspace.dependencies]
# Local dependencies
dyn-any = { path = "libraries/dyn-any", features = ["derive", "glam", "reqwest"] }
graphene-core = { path = "node-graph/gcore" }
graph-craft = { path = "node-graph/graph-craft", features = ["serde"] }
wgpu-executor = { path = "node-graph/wgpu-executor" }
bezier-rs = { path = "libraries/bezier-rs", features = ["dyn-any"] }
path-bool = { path = "libraries/path-bool", default-features = false }
bezier-rs = { path = "libraries/bezier-rs", features = ["dyn-any", "serde"] }
dyn-any = { path = "libraries/dyn-any", features = ["derive", "glam", "reqwest", "log-bad-types", "rc"] }
preprocessor = { path = "node-graph/preprocessor"}
math-parser = { path = "libraries/math-parser" }
path-bool = { path = "libraries/path-bool" }
graphene-application-io = { path = "node-graph/gapplication-io" }
graphene-core = { path = "node-graph/gcore" }
graphene-math-nodes = { path = "node-graph/gmath-nodes" }
graphene-path-bool = { path = "node-graph/gpath-bool" }
graph-craft = { path = "node-graph/graph-craft" }
graphene-std = { path = "node-graph/gstd" }
graphene-svg-renderer = { path = "node-graph/gsvg-renderer" }
interpreted-executor = { path = "node-graph/interpreted-executor" }
node-macro = { path = "node-graph/node-macro" }
wgpu-executor = { path = "node-graph/wgpu-executor" }
graphite-proc-macros = { path = "proc-macros" }
# Workspace dependencies
rustc-hash = "2.0"
bytemuck = { version = "1.13", features = ["derive"] }
async-trait = "0.1"
serde = { version = "1.0", features = ["derive", "rc"] }
serde_json = "1.0"
serde-wasm-bindgen = "0.6"
@@ -60,36 +69,54 @@ bitflags = { version = "2.4", features = ["serde"] }
ctor = "0.2"
convert_case = "0.7"
derivative = "2.2"
tempfile = "3.6"
thiserror = "2"
anyhow = "1.0"
proc-macro2 = "1"
proc-macro2 = { version = "1", features = [ "span-locations" ] }
quote = "1.0"
axum = "0.8"
chrono = "0.4"
ron = "0.8"
fastnoise-lite = "1.1"
spirv-std = { git = "https://github.com/Rust-GPU/rust-gpu.git" }
wgpu-types = "23"
wgpu = "23"
wgpu = { version = "23", features = [
# We don't have wgpu on multiple threads (yet) https://github.com/gfx-rs/wgpu/blob/trunk/CHANGELOG.md#wgpu-types-now-send-sync-on-wasm
"fragile-send-sync-non-atomic-wasm",
"spirv",
"strict_asserts",
] }
once_cell = "1.13" # Remove when `core::cell::LazyCell` (<https://doc.rust-lang.org/core/cell/struct.LazyCell.html>) is stabilized in Rust 1.80 and we bump our MSRV
wasm-bindgen = "=0.2.100" # NOTICE: ensure this stays in sync with the `wasm-bindgen-cli` version in `website/content/volunteer/guide/project-setup/_index.md`. We pin this version because wasm-bindgen upgrades may break various things.
wasm-bindgen-futures = "0.4"
js-sys = "=0.3.77"
web-sys = "=0.3.77"
web-sys = { version = "=0.3.77", features = [
"Document",
"DomRect",
"Element",
"HtmlCanvasElement",
"CanvasRenderingContext2d",
"CanvasPattern",
"OffscreenCanvas",
"OffscreenCanvasRenderingContext2d",
"TextMetrics",
"Window",
"IdleRequestOptions",
"ImageData",
"Navigator",
"Gpu",
"HtmlImageElement",
"ImageBitmapRenderingContext",
] }
winit = "0.29"
url = "2.5"
tokio = { version = "1.29", features = ["fs", "io-std"] }
tokio = { version = "1.29", features = ["fs", "macros", "io-std", "rt"] }
vello = { git = "https://github.com/linebender/vello.git", rev = "3275ec8" } # TODO switch back to stable when a release is made
resvg = "0.44"
usvg = "0.44"
rand = { version = "0.9", default-features = false }
rand = { version = "0.9", default-features = false, features = ["std_rng"] }
rand_chacha = "0.9"
glam = { version = "0.29", default-features = false, features = ["serde"] }
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"] }
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp"] }
rustybuzz = "0.20"
spirv = "0.3"
pretty_assertions = "1.4.1"
fern = { version = "0.7", features = ["colored"] }
num_enum = "0.7"
@@ -103,11 +130,22 @@ specta = { version = "2.0.0-rc.22", features = [
syn = { version = "2.0", default-features = false, features = [
"full",
"derive",
"parsing",
"printing",
"visit-mut",
"visit",
"clone-impls",
"extra-traits",
"proc-macro",
] }
kurbo = { version = "0.11.0", features = ["serde"] }
petgraph = { version = "0.7.1", default-features = false, features = [
"graphmap",
] }
half = { version = "2.4.1", default-features = false, features = ["bytemuck", "serde"] }
tinyvec = { version = "1" }
criterion = { version = "0.5", features = ["html_reports"] }
iai-callgrind = { version = "0.12.3" }
[profile.dev]
opt-level = 1
+15 -11
View File
@@ -12,7 +12,7 @@
**Graphite is a free, open source vector and raster graphics engine, [available now](https://editor.graphite.rs) in alpha. Get creative with a fully nondestructive editing workflow that combines layer-based compositing with node-based generative design.**
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
!["Isometric Fountain" vector artwork](https://static.graphite.rs/content/index/gui-demo-node-graph-isometric-fountain.png)
![Made using nondestructive boolean operations and procedural polka dot patterns](https://github.com/user-attachments/assets/decb7011-18c2-4c68-82af-d1fa5064244a)
!["Marbled Mandelbrot" fractal raster artwork](https://static.graphite.rs/content/index/gui-demo-fractal__3.png)
![Mandelbrot fractal filled with a noise pattern, procedurally generated and infinitely scalable](https://github.com/user-attachments/assets/9e023997-185b-4f43-a724-797d308d9e7b)
![Design for a magazine spread, a preview of the upcoming focus on desktop publishing](https://github.com/user-attachments/assets/90eca551-5868-4f8d-9016-33958bf96345)
## Contributing/building the code
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.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+12 -33
View File
@@ -13,66 +13,45 @@ license = "Apache-2.0"
[features]
default = ["wasm"]
wasm = ["wasm-bindgen", "graphene-std/wasm", "wasm-bindgen-futures"]
gpu = [
"interpreted-executor/gpu",
"graphene-std/gpu",
"graphene-core/gpu",
"wgpu-executor",
"gpu-executor",
]
gpu = ["interpreted-executor/gpu", "wgpu-executor"]
tauri = ["ron", "decouple-execution"]
decouple-execution = []
resvg = ["graphene-std/resvg"]
vello = ["graphene-std/vello", "resvg", "graphene-core/vello"]
vello = ["graphene-std/vello", "resvg"]
ron = ["dep:ron"]
[dependencies]
# Local dependencies
graphite-proc-macros = { path = "../proc-macros" }
graph-craft = { path = "../node-graph/graph-craft" }
interpreted-executor = { path = "../node-graph/interpreted-executor", features = [
"serde",
] }
graphene-core = { path = "../node-graph/gcore" }
graphene-std = { path = "../node-graph/gstd", features = ["serde"] }
graphite-proc-macros = { workspace = true }
graph-craft = { workspace = true }
interpreted-executor = { workspace = true }
graphene-std = { workspace = true }
preprocessor = { workspace = true }
# Workspace dependencies
js-sys = { workspace = true }
log = { workspace = true }
bitflags = { workspace = true }
convert_case = { workspace = true }
thiserror = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
bezier-rs = { workspace = true }
kurbo = { workspace = true }
futures = { workspace = true }
glam = { workspace = true, features = ["serde", "debug-glam-assert"] }
glam = { workspace = true }
derivative = { workspace = true }
specta = { workspace = true }
image = { workspace = true, features = ["bmp", "png"] }
dyn-any = { workspace = true }
num_enum = { workspace = true }
usvg = { workspace = true }
once_cell = { workspace = true }
web-sys = { workspace = true, features = [
"Document",
"DomRect",
"Element",
"HtmlCanvasElement",
"CanvasRenderingContext2d",
"CanvasPattern",
"OffscreenCanvas",
"OffscreenCanvasRenderingContext2d",
"TextMetrics",
] }
web-sys = { workspace = true }
# Required dependencies
async-mutex = "1.4.0"
spin = "0.9.8"
# Optional local dependencies
wgpu-executor = { path = "../node-graph/wgpu-executor", optional = true }
gpu-executor = { path = "../node-graph/gpu-executor", optional = true }
wgpu-executor = { workspace = true, optional = true }
# Optional workspace dependencies
wasm-bindgen = { workspace = true, optional = true }
@@ -83,7 +62,7 @@ ron = { workspace = true, optional = true }
# Workspace dependencies
env_logger = { workspace = true }
futures = { workspace = true }
tokio = { workspace = true, features = ["rt", "macros"] }
tokio = { workspace = true }
[lints.rust]
# TODO: figure out why we check these features when they do not exist
+1 -51
View File
@@ -1,6 +1,6 @@
use crate::dispatcher::Dispatcher;
use crate::messages::prelude::*;
pub use graphene_core::uuid::*;
pub use graphene_std::uuid::*;
// TODO: serialize with serde to save the current editor state
pub struct Editor {
@@ -55,53 +55,3 @@ pub fn commit_info_localized(localized_commit_date: &str) -> String {
localized_commit_date
)
}
// #[cfg(test)]
// mod test {
// use crate::messages::input_mapper::utility_types::input_mouse::ViewportBounds;
// use crate::messages::prelude::*;
// // TODO: Fix and reenable
// #[ignore]
// #[test]
// fn debug_ub() {
// use super::Message;
// let mut editor = super::Editor::new();
// let mut responses = Vec::new();
// let messages: Vec<Message> = vec![
// Message::Init,
// Message::Preferences(PreferencesMessage::Load {
// preferences: r#"{ "imaginate_server_hostname": "http://localhost:7860/", "imaginate_refresh_frequency": 1, "zoom_with_scroll": false }"#.to_string(),
// }),
// PortfolioMessage::OpenDocumentFileWithId {
// document_id: DocumentId(0),
// document_name: "".into(),
// document_is_auto_saved: true,
// document_is_saved: true,
// document_serialized_content: r#" [removed until test is reenabled] "#.into(),
// to_front: false,
// }
// .into(),
// InputPreprocessorMessage::BoundsOfViewports {
// bounds_of_viewports: vec![ViewportBounds::from_slice(&[0., 0., 1920., 1080.])],
// }
// .into(),
// ];
// use futures::executor::block_on;
// for message in messages {
// block_on(crate::node_graph_executor::run_node_graph());
// let mut res = VecDeque::new();
// editor.poll_node_graph_evaluation(&mut res).expect("poll_node_graph_evaluation failed");
// let res = editor.handle_message(message);
// responses.push(res);
// }
// let responses = responses.pop().unwrap();
// // let trigger_message = responses[responses.len() - 2].clone();
// println!("responses: {responses:#?}");
// }
// }
+11 -1
View File
@@ -89,6 +89,8 @@ pub const MIN_LENGTH_FOR_RESIZE_TO_INCLUDE_INTERIOR: f64 = 40.;
///
/// The motion of the user's cursor by an `x` pixel offset results in `x * scale_factor` pixels of offset on the other side.
pub const MAXIMUM_ALT_SCALE_FACTOR: f64 = 25.;
/// The width or height that the transform cage needs before it is considered to have no width or height.
pub const MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT: f64 = 1e-4;
// SKEW TRIANGLES
pub const SKEW_TRIANGLE_SIZE: f64 = 7.;
@@ -100,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 = 7.5;
pub const SEGMENT_INSERTION_DISTANCE: f64 = 5.;
pub const SEGMENT_OVERLAY_SIZE: f64 = 10.;
pub const HANDLE_LENGTH_FACTOR: f64 = 0.5;
@@ -117,6 +119,13 @@ pub const LINE_ROTATE_SNAP_ANGLE: f64 = 15.;
pub const BRUSH_SIZE_CHANGE_KEYBOARD: f64 = 5.;
pub const DEFAULT_BRUSH_SIZE: f64 = 20.;
// GIZMOS
pub const POINT_RADIUS_HANDLE_SNAP_THRESHOLD: f64 = 8.;
pub const POINT_RADIUS_HANDLE_SEGMENT_THRESHOLD: f64 = 7.9;
pub const NUMBER_OF_POINTS_DIAL_SPOKE_EXTENSION: f64 = 1.2;
pub const NUMBER_OF_POINTS_DIAL_SPOKE_LENGTH: f64 = 10.;
pub const GIZMO_HIDE_THRESHOLD: f64 = 20.;
// SCROLLBARS
pub const SCROLLBAR_SPACING: f64 = 0.1;
pub const ASYMPTOTIC_EFFECT: f64 = 0.5;
@@ -124,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";
+9 -2
View File
@@ -498,9 +498,14 @@ mod test {
println!("-------------------------------------------------");
println!("Failed test due to receiving a DisplayDialogError while loading a Graphite demo file.");
println!();
println!("NOTE:");
println!("Document upgrading isn't performed in tests like when opening in the actual editor.");
println!("You may need to open and re-save a document in the editor to apply its migrations.");
println!();
println!("DisplayDialogError details:");
println!();
println!("Description: {value}");
println!("Description:");
println!("{value}");
println!("-------------------------------------------------");
println!();
@@ -538,7 +543,9 @@ mod test {
});
// Check if the graph renders
editor.eval_graph().await;
if let Err(e) = editor.eval_graph().await {
print_problem_to_terminal_on_failure(&format!("Failed to evaluate the graph for document '{document_name}':\n{e}"));
}
for response in responses {
// Check for the existence of the file format incompatibility warning dialog after opening the test file
+1 -1
View File
@@ -6,4 +6,4 @@ pub use animation_message::{AnimationMessage, AnimationMessageDiscriminant};
#[doc(inline)]
pub use animation_message_handler::AnimationMessageHandler;
pub use graphene_core::application_io::TimingInformation;
pub use graphene_std::application_io::TimingInformation;
@@ -26,7 +26,7 @@ impl MessageHandler<NewDocumentDialogMessage, ()> for NewDocumentDialogMessageHa
responses.add(Message::StartBuffer);
responses.add(GraphOperationMessage::NewArtboard {
id: NodeId::new(),
artboard: graphene_core::Artboard::new(IVec2::ZERO, self.dimensions.as_ivec2()),
artboard: graphene_std::Artboard::new(IVec2::ZERO, self.dimensions.as_ivec2()),
});
}
@@ -187,7 +187,8 @@ impl PreferencesDialogMessageHandler {
];
let mut checkbox_id = CheckboxId::default();
let vector_mesh_tooltip = "Allow tools to produce vector meshes, where more than two segments can connect to an anchor point.\n\nCurrently this does not properly handle line joins and fills.";
let vector_mesh_tooltip =
"Allow tools to produce vector meshes, where more than two segments can connect to an anchor point.\n\nCurrently this does not properly handle stroke joins and fills.";
let vector_meshes = vec![
Separator::new(SeparatorType::Unrelated).widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
@@ -203,27 +204,6 @@ impl PreferencesDialogMessageHandler {
.widget_holder(),
];
// TODO: Reenable when Imaginate is restored
// let imaginate_server_hostname = vec![
// TextLabel::new("Imaginate").min_width(60).italic(true).widget_holder(),
// TextLabel::new("Server Hostname").table_align(true).widget_holder(),
// TextInput::new(&preferences.imaginate_server_hostname)
// .min_width(200)
// .on_update(|text_input: &TextInput| PreferencesMessage::ImaginateServerHostname { hostname: text_input.value.clone() }.into())
// .widget_holder(),
// ];
// let imaginate_refresh_frequency = vec![
// TextLabel::new("").min_width(60).widget_holder(),
// TextLabel::new("Refresh Frequency").table_align(true).widget_holder(),
// NumberInput::new(Some(preferences.imaginate_refresh_frequency))
// .unit(" seconds")
// .min(0.)
// .max((1_u64 << f64::MANTISSA_DIGITS) as f64)
// .min_width(200)
// .on_update(|number_input: &NumberInput| PreferencesMessage::ImaginateRefreshFrequency { seconds: number_input.value.unwrap() }.into())
// .widget_holder(),
// ];
Layout::WidgetLayout(WidgetLayout::new(vec![
LayoutGroup::Row { widgets: navigation_header },
LayoutGroup::Row { widgets: zoom_rate_label },
@@ -237,8 +217,6 @@ impl PreferencesDialogMessageHandler {
LayoutGroup::Row { widgets: graph_wire_style },
LayoutGroup::Row { widgets: use_vello },
LayoutGroup::Row { widgets: vector_meshes },
// LayoutGroup::Row { widgets: imaginate_server_hostname },
// LayoutGroup::Row { widgets: imaginate_refresh_frequency },
]))
}
@@ -7,8 +7,8 @@ use crate::messages::portfolio::document::utility_types::nodes::{JsRawBuffer, La
use crate::messages::prelude::*;
use crate::messages::tool::utility_types::HintData;
use graph_craft::document::NodeId;
use graphene_core::raster::color::Color;
use graphene_core::text::Font;
use graphene_std::raster::color::Color;
use graphene_std::text::Font;
#[impl_message(Message, Frontend)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
@@ -99,19 +99,6 @@ pub enum FrontendMessage {
#[serde(rename = "copyText")]
copy_text: String,
},
// TODO: Eventually remove this document upgrade code
TriggerUpgradeDocumentToVectorManipulationFormat {
#[serde(rename = "documentId")]
document_id: DocumentId,
#[serde(rename = "documentName")]
document_name: String,
#[serde(rename = "documentIsAutoSaved")]
document_is_auto_saved: bool,
#[serde(rename = "documentIsSaved")]
document_is_saved: bool,
#[serde(rename = "documentSerializedContent")]
document_serialized_content: String,
},
TriggerVisitLink {
url: String,
},
@@ -171,40 +171,40 @@ pub fn input_mappings() -> Mapping {
entry!(KeyDown(MouseRight); action_dispatch=GradientToolMessage::Abort),
entry!(KeyDown(Escape); action_dispatch=GradientToolMessage::Abort),
//
// RectangleToolMessage
entry!(KeyDown(MouseLeft); action_dispatch=RectangleToolMessage::DragStart),
entry!(KeyUp(MouseLeft); action_dispatch=RectangleToolMessage::DragStop),
entry!(KeyDown(MouseRight); action_dispatch=RectangleToolMessage::Abort),
entry!(KeyDown(Escape); action_dispatch=RectangleToolMessage::Abort),
entry!(PointerMove; refresh_keys=[Alt, Shift], action_dispatch=RectangleToolMessage::PointerMove { center: Alt, lock_ratio: Shift }),
//
// ImaginateToolMessage
// entry!(KeyDown(MouseLeft); action_dispatch=ImaginateToolMessage::DragStart),
// entry!(KeyUp(MouseLeft); action_dispatch=ImaginateToolMessage::DragStop),
// entry!(KeyDown(MouseRight); action_dispatch=ImaginateToolMessage::Abort),
// entry!(KeyDown(Escape); action_dispatch=ImaginateToolMessage::Abort),
// entry!(PointerMove; refresh_keys=[Alt, Shift], action_dispatch=ImaginateToolMessage::Resize { center: Alt, lock_ratio: Shift }),
//
// EllipseToolMessage
entry!(KeyDown(MouseLeft); action_dispatch=EllipseToolMessage::DragStart),
entry!(KeyUp(MouseLeft); action_dispatch=EllipseToolMessage::DragStop),
entry!(KeyDown(MouseRight); action_dispatch=EllipseToolMessage::Abort),
entry!(KeyDown(Escape); action_dispatch=EllipseToolMessage::Abort),
entry!(PointerMove; refresh_keys=[Alt, Shift], action_dispatch=EllipseToolMessage::PointerMove { center: Alt, lock_ratio: Shift }),
//
// PolygonToolMessage
entry!(KeyDown(MouseLeft); action_dispatch=PolygonToolMessage::DragStart),
entry!(KeyUp(MouseLeft); action_dispatch=PolygonToolMessage::DragStop),
entry!(KeyDown(MouseRight); action_dispatch=PolygonToolMessage::Abort),
entry!(KeyDown(Escape); action_dispatch=PolygonToolMessage::Abort),
entry!(PointerMove; refresh_keys=[Alt, Shift], action_dispatch=PolygonToolMessage::PointerMove { center: Alt, lock_ratio: Shift }),
//
// LineToolMessage
entry!(KeyDown(MouseLeft); action_dispatch=LineToolMessage::DragStart),
entry!(KeyUp(MouseLeft); action_dispatch=LineToolMessage::DragStop),
entry!(KeyDown(MouseRight); action_dispatch=LineToolMessage::Abort),
entry!(KeyDown(Escape); action_dispatch=LineToolMessage::Abort),
entry!(PointerMove; refresh_keys=[Control, Alt, Shift], action_dispatch=LineToolMessage::PointerMove { center: Alt, lock_angle: Control, snap_angle: Shift }),
// ShapeToolMessage
entry!(KeyDown(MouseLeft); action_dispatch=ShapeToolMessage::DragStart),
entry!(KeyUp(MouseLeft); action_dispatch=ShapeToolMessage::DragStop),
entry!(KeyDown(MouseRight); action_dispatch=ShapeToolMessage::Abort),
entry!(KeyDown(Escape); action_dispatch=ShapeToolMessage::Abort),
entry!(KeyDown(BracketLeft); action_dispatch=ShapeToolMessage::DecreaseSides),
entry!(KeyDown(BracketRight); action_dispatch=ShapeToolMessage::IncreaseSides),
entry!(PointerMove; refresh_keys=[Alt, Shift, Control], action_dispatch=ShapeToolMessage::PointerMove([Alt, Shift, Control, Shift])),
entry!(KeyDown(ArrowUp); modifiers=[Shift, ArrowLeft], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: -BIG_NUDGE_AMOUNT, delta_y: -BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
entry!(KeyDown(ArrowUp); modifiers=[Shift, ArrowRight], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: BIG_NUDGE_AMOUNT, delta_y: -BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
entry!(KeyDown(ArrowUp); modifiers=[Shift], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: 0., delta_y: -BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
entry!(KeyDown(ArrowDown); modifiers=[Shift, ArrowLeft], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: -BIG_NUDGE_AMOUNT, delta_y: BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
entry!(KeyDown(ArrowDown); modifiers=[Shift, ArrowRight], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: BIG_NUDGE_AMOUNT, delta_y: BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
entry!(KeyDown(ArrowDown); modifiers=[Shift], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: 0., delta_y: BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
entry!(KeyDown(ArrowLeft); modifiers=[Shift, ArrowUp], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: -BIG_NUDGE_AMOUNT, delta_y: -BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
entry!(KeyDown(ArrowLeft); modifiers=[Shift, ArrowDown], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: -BIG_NUDGE_AMOUNT, delta_y: BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
entry!(KeyDown(ArrowLeft); modifiers=[Shift], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: -BIG_NUDGE_AMOUNT, delta_y: 0., resize: Alt, resize_opposite_corner: Control }),
entry!(KeyDown(ArrowRight); modifiers=[Shift, ArrowUp], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: BIG_NUDGE_AMOUNT, delta_y: -BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
entry!(KeyDown(ArrowRight); modifiers=[Shift, ArrowDown], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: BIG_NUDGE_AMOUNT, delta_y: BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
entry!(KeyDown(ArrowRight); modifiers=[Shift], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: BIG_NUDGE_AMOUNT, delta_y: 0., resize: Alt, resize_opposite_corner: Control }),
entry!(KeyDown(ArrowUp); modifiers=[ArrowLeft], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: -NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
entry!(KeyDown(ArrowUp); modifiers=[ArrowRight], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
entry!(KeyDown(ArrowUp); action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: 0., delta_y: -NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
entry!(KeyDown(ArrowDown); modifiers=[ArrowLeft], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: -NUDGE_AMOUNT, delta_y: NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
entry!(KeyDown(ArrowDown); modifiers=[ArrowRight], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
entry!(KeyDown(ArrowDown); action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: 0., delta_y: NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
entry!(KeyDown(ArrowLeft); modifiers=[ArrowUp], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: -NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
entry!(KeyDown(ArrowLeft); modifiers=[ArrowDown], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: -NUDGE_AMOUNT, delta_y: NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
entry!(KeyDown(ArrowLeft); action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: -NUDGE_AMOUNT, delta_y: 0., resize: Alt, resize_opposite_corner: Control }),
entry!(KeyDown(ArrowRight); modifiers=[ArrowUp], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
entry!(KeyDown(ArrowRight); modifiers=[ArrowDown], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
entry!(KeyDown(ArrowRight); action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: 0., resize: Alt, resize_opposite_corner: Control }),
entry!(KeyDown(ArrowUp); action_dispatch=ShapeToolMessage::IncreaseSides),
entry!(KeyDown(ArrowDown); action_dispatch=ShapeToolMessage::DecreaseSides),
//
// PathToolMessage
entry!(KeyDown(Delete); modifiers=[Accel], action_dispatch=PathToolMessage::DeleteAndBreakPath),
@@ -212,13 +212,13 @@ pub fn input_mappings() -> Mapping {
entry!(KeyDown(Delete); modifiers=[Shift], action_dispatch=PathToolMessage::BreakPath),
entry!(KeyDown(Backspace); modifiers=[Shift], action_dispatch=PathToolMessage::BreakPath),
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 }),
entry!(KeyDown(KeyR); action_dispatch=PathToolMessage::GRS { key: KeyR }),
entry!(KeyDown(KeyS); action_dispatch=PathToolMessage::GRS { key: KeyS }),
entry!(PointerMove; refresh_keys=[KeyC, Space, Control, Shift, Alt], action_dispatch=PathToolMessage::PointerMove { toggle_colinear: KeyC, equidistant: Alt, move_anchor_with_handles: Space, snap_angle: Shift, lock_angle: Control, delete_segment: Alt }),
entry!(PointerMove; refresh_keys=[KeyC, Space, Control, Shift, Alt], action_dispatch=PathToolMessage::PointerMove { toggle_colinear: KeyC, equidistant: Alt, move_anchor_with_handles: Space, snap_angle: Shift, lock_angle: Control, delete_segment: Alt, break_colinear_molding: Alt }),
entry!(KeyDown(Delete); action_dispatch=PathToolMessage::Delete),
entry!(KeyDown(KeyA); modifiers=[Accel], action_dispatch=PathToolMessage::SelectAllAnchors),
entry!(KeyDown(KeyA); modifiers=[Accel, Shift], action_dispatch=PathToolMessage::DeselectAllPoints),
@@ -308,10 +308,10 @@ pub fn input_mappings() -> Mapping {
entry!(KeyDown(KeyA); action_dispatch=ToolMessage::ActivateToolPath),
entry!(KeyDown(KeyP); action_dispatch=ToolMessage::ActivateToolPen),
entry!(KeyDown(KeyN); action_dispatch=ToolMessage::ActivateToolFreehand),
entry!(KeyDown(KeyL); action_dispatch=ToolMessage::ActivateToolLine),
entry!(KeyDown(KeyM); action_dispatch=ToolMessage::ActivateToolRectangle),
entry!(KeyDown(KeyE); action_dispatch=ToolMessage::ActivateToolEllipse),
entry!(KeyDown(KeyY); action_dispatch=ToolMessage::ActivateToolPolygon),
entry!(KeyDown(KeyL); action_dispatch=ToolMessage::ActivateToolShapeLine),
entry!(KeyDown(KeyM); action_dispatch=ToolMessage::ActivateToolShapeRectangle),
entry!(KeyDown(KeyE); action_dispatch=ToolMessage::ActivateToolShapeEllipse),
entry!(KeyDown(KeyY); action_dispatch=ToolMessage::ActivateToolShape),
entry!(KeyDown(KeyB); action_dispatch=ToolMessage::ActivateToolBrush),
entry!(KeyDown(KeyX); modifiers=[Accel, Shift], action_dispatch=ToolMessage::ResetColors),
entry!(KeyDown(KeyX); modifiers=[Shift], action_dispatch=ToolMessage::SwapColors),
@@ -1,8 +1,8 @@
use crate::messages::input_mapper::utility_types::input_keyboard::KeysGroup;
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::prelude::*;
use graphene_core::raster::color::Color;
use graphene_core::text::Font;
use graphene_std::raster::color::Color;
use graphene_std::text::Font;
use graphene_std::vector::style::{FillChoice, GradientStops};
use serde_json::Value;
@@ -121,7 +121,10 @@ impl LayoutMessageHandler {
};
(|| {
let update_value = value.as_object().expect("ColorInput update was not of type: object");
let Some(update_value) = value.as_object() else {
warn!("ColorInput update was not of type: object");
return Message::NoOp;
};
// None
let is_none = update_value.get("none").and_then(|x| x.as_bool());
@@ -154,7 +157,8 @@ impl LayoutMessageHandler {
return (color_button.on_update.callback)(color_button);
}
panic!("ColorInput update was not able to be parsed with color data: {color_button:?}");
warn!("ColorInput update was not able to be parsed with color data: {color_button:?}");
Message::NoOp
})()
}
};
@@ -1,8 +1,8 @@
use crate::messages::input_mapper::utility_types::misc::ActionKeys;
use crate::messages::layout::utility_types::widget_prelude::*;
use derivative::*;
use graphene_core::Color;
use graphene_core::raster::curve::Curve;
use graphene_std::Color;
use graphene_std::raster::curve::Curve;
use graphene_std::transform::ReferencePoint;
use graphite_proc_macros::WidgetBuilder;
use once_cell::sync::OnceCell;
@@ -56,7 +56,7 @@ pub struct CheckboxId(Arc<OnceCell<u64>>);
impl CheckboxId {
pub fn fill(&mut self) {
let _ = self.0.set(graphene_core::uuid::generate_uuid());
let _ = self.0.set(graphene_std::uuid::generate_uuid());
}
}
impl specta::Type for CheckboxId {
@@ -8,15 +8,16 @@ use crate::messages::portfolio::utility_types::PanelType;
use crate::messages::prelude::*;
use glam::DAffine2;
use graph_craft::document::NodeId;
use graphene_core::Color;
use graphene_core::raster::BlendMode;
use graphene_core::raster::Image;
use graphene_core::vector::style::ViewMode;
use graphene_std::renderer::ClickTarget;
use graphene_std::Color;
use graphene_std::raster::BlendMode;
use graphene_std::raster::Image;
use graphene_std::transform::Footprint;
use graphene_std::vector::click_target::ClickTarget;
use graphene_std::vector::style::ViewMode;
#[impl_message(Message, PortfolioMessage, Document)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
#[derive(derivative::Derivative, Clone, serde::Serialize, serde::Deserialize)]
#[derivative(Debug, PartialEq)]
pub enum DocumentMessage {
Noop,
// Sub-messages
@@ -72,13 +73,6 @@ pub enum DocumentMessage {
GroupSelectedLayers {
group_folder_type: GroupFolderType,
},
// ImaginateGenerate {
// imaginate_node: Vec<NodeId>,
// },
// ImaginateRandom {
// imaginate_node: Vec<NodeId>,
// then_generate: bool,
// },
MoveSelectedLayersTo {
parent: LayerNodeIdentifier,
insert_index: usize,
@@ -121,6 +115,9 @@ pub enum DocumentMessage {
SelectedLayersReorder {
relative_index_offset: isize,
},
ClipLayer {
id: NodeId,
},
SelectLayer {
id: NodeId,
ctrl: bool,
@@ -142,6 +139,9 @@ pub enum DocumentMessage {
SetOpacityForSelectedLayers {
opacity: f64,
},
SetFillForSelectedLayers {
fill: f64,
},
SetOverlaysVisibility {
visible: bool,
overlays_type: Option<OverlaysType>,
@@ -151,6 +151,7 @@ pub enum DocumentMessage {
},
SetSnapping {
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
closure: Option<for<'a> fn(&'a mut SnappingState) -> &'a mut bool>,
snapping_state: bool,
},
@@ -20,7 +20,7 @@ use crate::messages::portfolio::document::utility_types::network_interface::{Flo
use crate::messages::portfolio::document::utility_types::nodes::RawBuffer;
use crate::messages::portfolio::utility_types::PersistentData;
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils::{self, get_blend_mode, get_opacity};
use crate::messages::tool::common_functionality::graph_modification_utils::{self, get_blend_mode, get_fill, get_opacity};
use crate::messages::tool::tool_messages::select_tool::SelectToolPointerKeys;
use crate::messages::tool::tool_messages::tool_prelude::Key;
use crate::messages::tool::utility_types::ToolType;
@@ -29,11 +29,13 @@ use bezier_rs::Subpath;
use glam::{DAffine2, DVec2, IVec2};
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput, NodeNetwork, OldNodeNetwork};
use graphene_core::raster::BlendMode;
use graphene_core::raster::image::ImageFrameTable;
use graphene_core::vector::style::ViewMode;
use graphene_std::renderer::{ClickTarget, Quad};
use graphene_std::vector::{PointId, path_bool_lib};
use graphene_std::math::quad::Quad;
use graphene_std::path_bool::{boolean_intersect, path_bool_lib};
use graphene_std::raster::BlendMode;
use graphene_std::raster_types::{Raster, RasterDataTable};
use graphene_std::vector::PointId;
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
use graphene_std::vector::style::ViewMode;
use std::time::Duration;
pub struct DocumentMessageData<'a> {
@@ -613,37 +615,6 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: new_folders });
}
}
// DocumentMessage::ImaginateGenerate { imaginate_node } => {
// let random_value = generate_uuid();
// responses.add(NodeGraphMessage::SetInputValue {
// node_id: *imaginate_node.last().unwrap(),
// // Needs to match the index of the seed parameter in `pub const IMAGINATE_NODE: DocumentNodeDefinition` in `document_node_type.rs`
// input_index: 17,
// value: graph_craft::document::value::TaggedValue::U64(random_value),
// });
// responses.add(PortfolioMessage::SubmitGraphRender { document_id, ignore_hash: false });
// }
// DocumentMessage::ImaginateRandom { imaginate_node, then_generate } => {
// // Generate a random seed. We only want values between -2^53 and 2^53, because integer values
// // outside of this range can get rounded in f64
// let random_bits = generate_uuid();
// let random_value = ((random_bits >> 11) as f64).copysign(f64::from_bits(random_bits & (1 << 63)));
// responses.add(DocumentMessage::AddTransaction);
// // Set a random seed input
// responses.add(NodeGraphMessage::SetInputValue {
// node_id: *imaginate_node.last().unwrap(),
// // Needs to match the index of the seed parameter in `pub const IMAGINATE_NODE: DocumentNodeDefinition` in `document_node_type.rs`
// input_index: 3,
// value: graph_craft::document::value::TaggedValue::F64(random_value),
// });
// // Generate the image
// if then_generate {
// responses.add(DocumentMessage::ImaginateGenerate { imaginate_node });
// }
// }
DocumentMessage::MoveSelectedLayersTo { parent, insert_index } => {
if !self.selection_network_path.is_empty() {
log::error!("Moving selected layers is only supported for the Document Network");
@@ -690,38 +661,41 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
.iter()
.map(|layer| {
if layer.parent(self.metadata()) != Some(parent) {
(*layer, 0)
} else {
let upstream_selected_siblings = layer
.downstream_siblings(self.network_interface.document_metadata())
.filter(|sibling| {
sibling != layer
&& layers_to_move.iter().any(|layer| {
layer == sibling
&& layer
.parent(self.metadata())
.is_some_and(|parent| parent.children(self.metadata()).position(|child| child == *layer) < Some(insert_index))
})
})
.count();
(*layer, upstream_selected_siblings)
return (*layer, 0);
}
let upstream_selected_siblings = layer
.downstream_siblings(self.network_interface.document_metadata())
.filter(|sibling| {
sibling != layer
&& layers_to_move.iter().any(|layer| {
layer == sibling
&& layer
.parent(self.metadata())
.is_some_and(|parent| parent.children(self.metadata()).position(|child| child == *layer) < Some(insert_index))
})
})
.count();
(*layer, upstream_selected_siblings)
})
.collect::<Vec<_>>();
responses.add(DocumentMessage::AddTransaction);
for (layer_index, (layer_to_move, insert_offset)) in layers_to_move_with_insert_offset.into_iter().enumerate() {
let calculated_insert_index = insert_index + layer_index - insert_offset;
responses.add(NodeGraphMessage::MoveLayerToStack {
layer: layer_to_move,
parent,
insert_index: calculated_insert_index,
insert_index: insert_index + layer_index - insert_offset,
});
if layer_to_move.parent(self.metadata()) != Some(parent) {
// TODO: Fix this so it works when dragging a layer into a group parent which has a Transform node, which used to work before #2689 caused this regression by removing the empty VectorData table row.
// TODO: See #2688 for this issue.
let layer_local_transform = self.network_interface.document_metadata().transform_to_viewport(layer_to_move);
let undo_transform = self.network_interface.document_metadata().transform_to_viewport(parent).inverse();
let transform = undo_transform * layer_local_transform;
responses.add(GraphOperationMessage::TransformSet {
layer: layer_to_move,
transform,
@@ -861,7 +835,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
responses.add(DocumentMessage::AddTransaction);
let layer = graph_modification_utils::new_image_layer(ImageFrameTable::new(image), layer_node_id, self.new_layer_parent(true), responses);
let layer = graph_modification_utils::new_image_layer(RasterDataTable::new(Raster::new_cpu(image)), layer_node_id, self.new_layer_parent(true), responses);
if let Some(name) = name {
responses.add(NodeGraphMessage::SetDisplayName {
@@ -1079,6 +1053,12 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
DocumentMessage::SelectedLayersReorder { relative_index_offset } => {
self.selected_layers_reorder(relative_index_offset, responses);
}
DocumentMessage::ClipLayer { id } => {
let layer = LayerNodeIdentifier::new(id, &self.network_interface, &[]);
responses.add(DocumentMessage::AddTransaction);
responses.add(GraphOperationMessage::ClipModeToggle { layer });
}
DocumentMessage::SelectLayer { id, ctrl, shift } => {
let layer = LayerNodeIdentifier::new(id, &self.network_interface, &[]);
@@ -1173,6 +1153,12 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
responses.add(GraphOperationMessage::OpacitySet { layer, opacity });
}
}
DocumentMessage::SetFillForSelectedLayers { fill } => {
let fill = fill.clamp(0., 1.);
for layer in self.network_interface.selected_nodes().selected_layers_except_artboards(&self.network_interface) {
responses.add(GraphOperationMessage::BlendingFillSet { layer, fill });
}
}
DocumentMessage::SetOverlaysVisibility { visible, overlays_type } => {
let visibility_settings = &mut self.overlays_visibility_settings;
let overlays_type = match overlays_type {
@@ -1577,7 +1563,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
impl DocumentMessageHandler {
/// Runs an intersection test with all layers and a viewport space quad
pub fn intersect_quad<'a>(&'a self, viewport_quad: graphene_core::renderer::Quad, ipp: &InputPreprocessorMessageHandler) -> impl Iterator<Item = LayerNodeIdentifier> + use<'a> {
pub fn intersect_quad<'a>(&'a self, viewport_quad: graphene_std::renderer::Quad, ipp: &InputPreprocessorMessageHandler) -> impl Iterator<Item = LayerNodeIdentifier> + use<'a> {
let document_to_viewport = self.navigation_handler.calculate_offset_transform(ipp.viewport_bounds.center(), &self.document_ptz);
let document_quad = document_to_viewport.inverse() * viewport_quad;
@@ -1585,7 +1571,7 @@ impl DocumentMessageHandler {
}
/// Runs an intersection test with all layers and a viewport space quad; ignoring artboards
pub fn intersect_quad_no_artboards<'a>(&'a self, viewport_quad: graphene_core::renderer::Quad, ipp: &InputPreprocessorMessageHandler) -> impl Iterator<Item = LayerNodeIdentifier> + use<'a> {
pub fn intersect_quad_no_artboards<'a>(&'a self, viewport_quad: graphene_std::renderer::Quad, ipp: &InputPreprocessorMessageHandler) -> impl Iterator<Item = LayerNodeIdentifier> + use<'a> {
self.intersect_quad(viewport_quad, ipp).filter(|layer| !self.network_interface.is_artboard(&layer.to_node(), &[]))
}
@@ -1602,7 +1588,7 @@ impl DocumentMessageHandler {
self.intersect_polygon(viewport_polygon, ipp).filter(|layer| !self.network_interface.is_artboard(&layer.to_node(), &[]))
}
pub fn is_layer_fully_inside(&self, layer: &LayerNodeIdentifier, quad: graphene_core::renderer::Quad) -> bool {
pub fn is_layer_fully_inside(&self, layer: &LayerNodeIdentifier, quad: graphene_std::renderer::Quad) -> bool {
// Get the bounding box of the layer in document space
let Some(bounding_box) = self.metadata().bounding_box_viewport(*layer) else { return false };
@@ -1633,10 +1619,17 @@ impl DocumentMessageHandler {
let layer_transform = self.network_interface.document_metadata().transform_to_document(*layer);
layer_click_targets.is_some_and(|targets| {
targets.iter().all(|target| {
let mut subpath = target.subpath().clone();
subpath.apply_transform(layer_transform);
subpath.is_inside_subpath(&viewport_polygon, None, None)
targets.iter().all(|target| match target.target_type() {
ClickTargetType::Subpath(subpath) => {
let mut subpath = subpath.clone();
subpath.apply_transform(layer_transform);
subpath.is_inside_subpath(&viewport_polygon, None, None)
}
ClickTargetType::FreePoint(point) => {
let mut point = *point;
point.apply_transform(layer_transform);
viewport_polygon.contains_point(point.position)
}
})
})
}
@@ -1683,13 +1676,28 @@ impl DocumentMessageHandler {
self.click_list(ipp).last()
}
pub fn click_based_on_position(&self, mouse_snapped_positon: DVec2) -> Option<LayerNodeIdentifier> {
ClickXRayIter::new(&self.network_interface, XRayTarget::Point(mouse_snapped_positon))
.filter(move |&layer| !self.network_interface.is_artboard(&layer.to_node(), &[]))
.skip_while(|&layer| layer == LayerNodeIdentifier::ROOT_PARENT)
.scan(true, |last_had_children, layer| {
if *last_had_children {
*last_had_children = layer.has_children(self.network_interface.document_metadata());
Some(layer)
} else {
None
}
})
.last()
}
/// Get the combined bounding box of the click targets of the selected visible layers in viewport space
pub fn selected_visible_layers_bounding_box_viewport(&self) -> Option<[DVec2; 2]> {
self.network_interface
.selected_nodes()
.selected_visible_layers(&self.network_interface)
.filter_map(|layer| self.metadata().bounding_box_viewport(layer))
.reduce(graphene_core::renderer::Quad::combine_bounds)
.reduce(graphene_std::renderer::Quad::combine_bounds)
}
pub fn selected_visible_and_unlock_layers_bounding_box_viewport(&self) -> Option<[DVec2; 2]> {
@@ -1697,7 +1705,7 @@ impl DocumentMessageHandler {
.selected_nodes()
.selected_visible_and_unlocked_layers(&self.network_interface)
.filter_map(|layer| self.metadata().bounding_box_viewport(layer))
.reduce(graphene_core::renderer::Quad::combine_bounds)
.reduce(graphene_std::renderer::Quad::combine_bounds)
}
pub fn document_network(&self) -> &NodeNetwork {
@@ -2522,38 +2530,47 @@ impl DocumentMessageHandler {
let selected_layers_except_artboards = selected_nodes.selected_layers_except_artboards(&self.network_interface);
// Look up the current opacity and blend mode of the selected layers (if any), and split the iterator into the first tuple and the rest.
let mut opacity_and_blend_mode = selected_layers_except_artboards.map(|layer| {
let mut blending_options = selected_layers_except_artboards.map(|layer| {
(
get_opacity(layer, &self.network_interface).unwrap_or(100.),
get_fill(layer, &self.network_interface).unwrap_or(100.),
get_blend_mode(layer, &self.network_interface).unwrap_or_default(),
)
});
let first_opacity_and_blend_mode = opacity_and_blend_mode.next();
let result_opacity_and_blend_mode = opacity_and_blend_mode;
let first_blending_options = blending_options.next();
let result_blending_options = blending_options;
// If there are no selected layers, disable the opacity and blend mode widgets.
let disabled = first_opacity_and_blend_mode.is_none();
let disabled = first_blending_options.is_none();
// Amongst the selected layers, check if the opacities and blend modes are identical across all layers.
// The result is setting `option` and `blend_mode` to Some value if all their values are identical, or None if they are not.
// If identical, we display the value in the widget. If not, we display a dash indicating dissimilarity.
let (opacity, blend_mode) = first_opacity_and_blend_mode
.map(|(first_opacity, first_blend_mode)| {
let (opacity, fill, blend_mode) = first_blending_options
.map(|(first_opacity, first_fill, first_blend_mode)| {
let mut opacity_identical = true;
let mut fill_identical = true;
let mut blend_mode_identical = true;
for (opacity, blend_mode) in result_opacity_and_blend_mode {
for (opacity, fill, blend_mode) in result_blending_options {
if (opacity - first_opacity).abs() > (f64::EPSILON * 100.) {
opacity_identical = false;
}
if (fill - first_fill).abs() > (f64::EPSILON * 100.) {
fill_identical = false;
}
if blend_mode != first_blend_mode {
blend_mode_identical = false;
}
}
(opacity_identical.then_some(first_opacity), blend_mode_identical.then_some(first_blend_mode))
(
opacity_identical.then_some(first_opacity),
fill_identical.then_some(first_fill),
blend_mode_identical.then_some(first_blend_mode),
)
})
.unwrap_or((None, None));
.unwrap_or((None, None, None));
let blend_mode_menu_entries = BlendMode::list_svg_subset()
.iter()
@@ -2612,6 +2629,28 @@ impl DocumentMessageHandler {
.max_width(100)
.tooltip("Opacity")
.widget_holder(),
Separator::new(SeparatorType::Related).widget_holder(),
NumberInput::new(fill)
.label("Fill")
.unit("%")
.display_decimal_places(0)
.disabled(disabled)
.min(0.)
.max(100.)
.range_min(Some(0.))
.range_max(Some(100.))
.mode_range()
.on_update(|number_input: &NumberInput| {
if let Some(value) = number_input.value {
DocumentMessage::SetFillForSelectedLayers { fill: value / 100. }.into()
} else {
Message::NoOp
}
})
.on_commit(|_| DocumentMessage::AddTransaction.into())
.max_width(100)
.tooltip("Fill")
.widget_holder(),
];
let layers_panel_control_bar_left = WidgetLayout::new(vec![LayoutGroup::Row { widgets }]);
@@ -2838,7 +2877,7 @@ impl DocumentMessageHandler {
/// Create a network interface with a single export
fn default_document_network_interface() -> NodeNetworkInterface {
let mut network_interface = NodeNetworkInterface::default();
network_interface.add_export(TaggedValue::ArtboardGroup(graphene_core::ArtboardGroupTable::default()), -1, "", &[]);
network_interface.add_export(TaggedValue::ArtboardGroup(graphene_std::ArtboardGroupTable::default()), -1, "", &[]);
network_interface
}
@@ -2876,7 +2915,14 @@ fn click_targets_to_path_lib_segments<'a>(click_targets: impl Iterator<Item = &'
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => path_bool_lib::PathSegment::Cubic(bezier.start, handle_start, handle_end, bezier.end),
};
click_targets
.flat_map(|target| target.subpath().iter())
.filter_map(|target| {
if let ClickTargetType::Subpath(subpath) = target.target_type() {
Some(subpath.iter())
} else {
None
}
})
.flatten()
.map(|bezier| segment(bezier.apply_transformation(|x| transform.transform_point2(x))))
.collect()
}
@@ -2917,7 +2963,7 @@ impl<'a> ClickXRayIter<'a> {
// We do this on this using the target area to reduce computation (as the target area is usually very simple).
if clip && intersects {
let clip_path = click_targets_to_path_lib_segments(click_targets.iter().flat_map(|x| x.iter()), transform);
let subtracted = graphene_std::vector::boolean_intersect(path, clip_path).into_iter().flatten().collect::<Vec<_>>();
let subtracted = boolean_intersect(path, clip_path).into_iter().flatten().collect::<Vec<_>>();
if subtracted.is_empty() {
use_children = false;
} else {
@@ -3234,6 +3280,8 @@ mod document_message_handler_tests {
assert_eq!(rect_grandparent, folder2, "Rectangle's grandparent should be folder2");
}
// TODO: Fix https://github.com/GraphiteEditor/Graphite/issues/2688 and reenable this as part of that fix.
#[ignore]
#[tokio::test]
async fn test_moving_layers_retains_transforms() {
let mut editor = EditorTestUtils::create();
@@ -3292,7 +3340,7 @@ mod document_message_handler_tests {
let document = editor.active_document();
let rect_bbox_before = document.metadata().bounding_box_viewport(rect_layer).unwrap();
// Moving rectangle from folder1 --> folder2
// Moving rectangle from folder1 to folder2
editor.handle_message(DocumentMessage::MoveSelectedLayersTo { parent: folder2, insert_index: 0 }).await;
// Rectangle's viewport position after moving
@@ -3300,10 +3348,16 @@ mod document_message_handler_tests {
let rect_bbox_after = document.metadata().bounding_box_viewport(rect_layer).unwrap();
// Verifing the rectangle maintains approximately the same position in viewport space
let before_center = (rect_bbox_before[0] + rect_bbox_before[1]) / 2.;
let after_center = (rect_bbox_after[0] + rect_bbox_after[1]) / 2.;
let distance = before_center.distance(after_center);
let before_center = (rect_bbox_before[0] + rect_bbox_before[1]) / 2.; // TODO: Should be: DVec2(0., -25.), regression (#2688) causes it to be: DVec2(100., 25.)
let after_center = (rect_bbox_after[0] + rect_bbox_after[1]) / 2.; // TODO: Should be: DVec2(0., -25.), regression (#2688) causes it to be: DVec2(200., 75.)
let distance = before_center.distance(after_center); // TODO: Should be: 0., regression (#2688) causes it to be: 111.80339887498948
assert!(distance < 1., "Rectangle should maintain its viewport position after moving between transformed groups");
assert!(
distance < 1.,
"Rectangle should maintain its viewport position after moving between transformed groups.\n\
Before: {before_center:?}\n\
After: {after_center:?}\n\
Dist: {distance} (should be < 1)"
);
}
}
@@ -5,14 +5,14 @@ use crate::messages::prelude::*;
use bezier_rs::Subpath;
use glam::{DAffine2, DVec2, IVec2};
use graph_craft::document::NodeId;
use graphene_core::raster::BlendMode;
use graphene_core::raster::image::ImageFrameTable;
use graphene_core::text::{Font, TypesettingConfig};
use graphene_core::vector::PointId;
use graphene_core::vector::VectorModificationType;
use graphene_core::vector::brush_stroke::BrushStroke;
use graphene_core::vector::style::{Fill, Stroke};
use graphene_core::{Artboard, Color};
use graphene_std::Artboard;
use graphene_std::raster::BlendMode;
use graphene_std::raster_types::{CPU, RasterDataTable};
use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::PointId;
use graphene_std::vector::VectorModificationType;
use graphene_std::vector::brush_stroke::BrushStroke;
use graphene_std::vector::style::{Fill, Stroke};
#[impl_message(Message, DocumentMessage, GraphOperation)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
@@ -21,6 +21,10 @@ pub enum GraphOperationMessage {
layer: LayerNodeIdentifier,
fill: Fill,
},
BlendingFillSet {
layer: LayerNodeIdentifier,
fill: f64,
},
OpacitySet {
layer: LayerNodeIdentifier,
opacity: f64,
@@ -29,6 +33,9 @@ pub enum GraphOperationMessage {
layer: LayerNodeIdentifier,
blend_mode: BlendMode,
},
ClipModeToggle {
layer: LayerNodeIdentifier,
},
StrokeSet {
layer: LayerNodeIdentifier,
stroke: Stroke,
@@ -66,13 +73,13 @@ pub enum GraphOperationMessage {
},
NewBitmapLayer {
id: NodeId,
image_frame: ImageFrameTable<Color>,
image_frame: RasterDataTable<CPU>,
parent: LayerNodeIdentifier,
insert_index: usize,
},
NewBooleanOperationLayer {
id: NodeId,
operation: graphene_std::vector::misc::BooleanOperation,
operation: graphene_std::path_bool::BooleanOperation,
parent: LayerNodeIdentifier,
insert_index: usize,
},
@@ -5,13 +5,14 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeNetworkInterface, OutputConnector};
use crate::messages::portfolio::document::utility_types::nodes::CollapsedLayers;
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils::get_clip_mode;
use glam::{DAffine2, DVec2, IVec2};
use graph_craft::document::{NodeId, NodeInput};
use graphene_core::Color;
use graphene_core::renderer::Quad;
use graphene_core::text::{Font, TypesettingConfig};
use graphene_core::vector::style::{Fill, Gradient, GradientStops, GradientType, LineCap, LineJoin, Stroke};
use graphene_std::vector::convert_usvg_path;
use graphene_std::Color;
use graphene_std::renderer::Quad;
use graphene_std::renderer::convert_usvg_path::convert_usvg_path;
use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::style::{Fill, Gradient, GradientStops, GradientType, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
#[derive(Debug, Clone)]
struct ArtboardInfo {
@@ -41,6 +42,11 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
modify_inputs.fill_set(fill);
}
}
GraphOperationMessage::BlendingFillSet { layer, fill } => {
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
modify_inputs.blending_fill_set(fill);
}
}
GraphOperationMessage::OpacitySet { layer, opacity } => {
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
modify_inputs.opacity_set(opacity);
@@ -51,6 +57,12 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
modify_inputs.blend_mode_set(blend_mode);
}
}
GraphOperationMessage::ClipModeToggle { layer } => {
let clip_mode = get_clip_mode(layer, network_interface);
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
modify_inputs.clip_mode_toggle(clip_mode);
}
}
GraphOperationMessage::StrokeSet { layer, stroke } => {
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
modify_inputs.stroke_set(stroke);
@@ -362,7 +374,7 @@ fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node,
warn!("Skip image")
}
usvg::Node::Text(text) => {
let font = Font::new(graphene_core::consts::DEFAULT_FONT_FAMILY.to_string(), graphene_core::consts::DEFAULT_FONT_STYLE.to_string());
let font = Font::new(graphene_std::consts::DEFAULT_FONT_FAMILY.to_string(), graphene_std::consts::DEFAULT_FONT_STYLE.to_string());
modify_inputs.insert_text(text.chunks().iter().map(|chunk| chunk.text()).collect(), font, TypesettingConfig::default(), layer);
modify_inputs.fill_set(Fill::Solid(Color::BLACK));
}
@@ -376,18 +388,20 @@ fn apply_usvg_stroke(stroke: &usvg::Stroke, modify_inputs: &mut ModifyInputsCont
weight: stroke.width().get() as f64,
dash_lengths: stroke.dasharray().as_ref().map(|lengths| lengths.iter().map(|&length| length as f64).collect()).unwrap_or_default(),
dash_offset: stroke.dashoffset() as f64,
line_cap: match stroke.linecap() {
usvg::LineCap::Butt => LineCap::Butt,
usvg::LineCap::Round => LineCap::Round,
usvg::LineCap::Square => LineCap::Square,
cap: match stroke.linecap() {
usvg::LineCap::Butt => StrokeCap::Butt,
usvg::LineCap::Round => StrokeCap::Round,
usvg::LineCap::Square => StrokeCap::Square,
},
line_join: match stroke.linejoin() {
usvg::LineJoin::Miter => LineJoin::Miter,
usvg::LineJoin::MiterClip => LineJoin::Miter,
usvg::LineJoin::Round => LineJoin::Round,
usvg::LineJoin::Bevel => LineJoin::Bevel,
join: match stroke.linejoin() {
usvg::LineJoin::Miter => StrokeJoin::Miter,
usvg::LineJoin::MiterClip => StrokeJoin::Miter,
usvg::LineJoin::Round => StrokeJoin::Round,
usvg::LineJoin::Bevel => StrokeJoin::Bevel,
},
line_join_miter_limit: stroke.miterlimit().get() as f64,
join_miter_limit: stroke.miterlimit().get() as f64,
align: StrokeAlign::Center,
paint_order: PaintOrder::StrokeAbove,
transform,
non_scaling: false,
})
@@ -3,7 +3,7 @@ use bezier_rs::Subpath;
use glam::{DAffine2, DVec2};
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput};
use graphene_core::vector::PointId;
use graphene_std::vector::PointId;
/// Convert an affine transform into the tuple `(scale, angle, translation, shear)` assuming `shear.y = 0`.
pub fn compute_scale_angle_translation_shear(transform: DAffine2) -> (DVec2, f64, DVec2, DVec2) {
@@ -91,44 +91,48 @@ pub fn get_current_normalized_pivot(inputs: &[NodeInput]) -> DVec2 {
if let Some(&TaggedValue::DVec2(pivot)) = inputs[5].as_value() { pivot } else { DVec2::splat(0.5) }
}
/// ![](https://files.keavon.com/-/OptimisticSpotlessTinamou/capture.png)
///
/// Source:
/// ```tex
/// \begin{bmatrix}
/// S_{x}\cos(\theta)-S_{y}\sin(\theta)H_{y} & S_{x}\cos(\theta)H_{x}-S_{y}\sin(\theta) & T_{x}\\
/// S_{x}\sin(\theta)+S_{y}\cos(\theta)H_{y} & S_{x}\sin(\theta)H_{x}+S_{y}\cos(\theta) & T_{y}\\
/// 0 & 0 & 1
/// \end{bmatrix}
/// ```
#[test]
fn derive_transform() {
for shear_x in -10..=10 {
let shear_x = (shear_x as f64) / 2.;
for angle in (0..=360).step_by(15) {
let angle = (angle as f64).to_radians();
for scale_x in 1..10 {
let scale_x = (scale_x as f64) / 5.;
for scale_y in 1..10 {
let scale_y = (scale_y as f64) / 5.;
#[cfg(test)]
mod tests {
use super::*;
/// ![](https://files.keavon.com/-/OptimisticSpotlessTinamou/capture.png)
///
/// Source:
/// ```tex
/// \begin{bmatrix}
/// S_{x}\cos(\theta)-S_{y}\sin(\theta)H_{y} & S_{x}\cos(\theta)H_{x}-S_{y}\sin(\theta) & T_{x}\\
/// S_{x}\sin(\theta)+S_{y}\cos(\theta)H_{y} & S_{x}\sin(\theta)H_{x}+S_{y}\cos(\theta) & T_{y}\\
/// 0 & 0 & 1
/// \end{bmatrix}
/// ```
#[test]
fn derive_transform() {
for shear_x in -10..=10 {
let shear_x = (shear_x as f64) / 2.;
for angle in (0..=360).step_by(15) {
let angle = (angle as f64).to_radians();
for scale_x in 1..10 {
let scale_x = (scale_x as f64) / 5.;
for scale_y in 1..10 {
let scale_y = (scale_y as f64) / 5.;
let shear = DVec2::new(shear_x, 0.);
let scale = DVec2::new(scale_x, scale_y);
let translate = DVec2::new(5666., 644.);
let shear = DVec2::new(shear_x, 0.);
let scale = DVec2::new(scale_x, scale_y);
let translate = DVec2::new(5666., 644.);
let original_transform = DAffine2::from_cols(
DVec2::new(scale.x * angle.cos() - scale.y * angle.sin() * shear.y, scale.x * angle.sin() + scale.y * angle.cos() * shear.y),
DVec2::new(scale.x * angle.cos() * shear.x - scale.y * angle.sin(), scale.x * angle.sin() * shear.x + scale.y * angle.cos()),
translate,
);
let original_transform = DAffine2::from_cols(
DVec2::new(scale.x * angle.cos() - scale.y * angle.sin() * shear.y, scale.x * angle.sin() + scale.y * angle.cos() * shear.y),
DVec2::new(scale.x * angle.cos() * shear.x - scale.y * angle.sin(), scale.x * angle.sin() * shear.x + scale.y * angle.cos()),
translate,
);
let (new_scale, new_angle, new_translation, new_shear) = compute_scale_angle_translation_shear(original_transform);
let new_transform = DAffine2::from_scale_angle_translation(new_scale, new_angle, new_translation) * DAffine2::from_cols_array(&[1., new_shear.y, new_shear.x, 1., 0., 0.]);
let (new_scale, new_angle, new_translation, new_shear) = compute_scale_angle_translation_shear(original_transform);
let new_transform = DAffine2::from_scale_angle_translation(new_scale, new_angle, new_translation) * DAffine2::from_cols_array(&[1., new_shear.y, new_shear.x, 1., 0., 0.]);
assert!(
new_transform.abs_diff_eq(original_transform, 1e-10),
"original_transform {original_transform} new_transform {new_transform} / scale {scale} new_scale {new_scale} / angle {angle} new_angle {new_angle} / shear {shear} / new_shear {new_shear}",
);
assert!(
new_transform.abs_diff_eq(original_transform, 1e-10),
"original_transform {original_transform} new_transform {new_transform} / scale {scale} new_scale {new_scale} / angle {angle} new_angle {new_angle} / shear {shear} / new_shear {new_shear}",
);
}
}
}
}
@@ -8,15 +8,15 @@ use glam::{DAffine2, DVec2, IVec2};
use graph_craft::concrete;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput};
use graphene_core::raster::BlendMode;
use graphene_core::raster::image::ImageFrameTable;
use graphene_core::text::{Font, TypesettingConfig};
use graphene_core::vector::brush_stroke::BrushStroke;
use graphene_core::vector::style::{Fill, Stroke};
use graphene_core::vector::{PointId, VectorModificationType};
use graphene_core::{Artboard, Color};
use graphene_std::GraphicGroupTable;
use graphene_std::Artboard;
use graphene_std::raster::BlendMode;
use graphene_std::raster_types::{CPU, RasterDataTable};
use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::brush_stroke::BrushStroke;
use graphene_std::vector::style::{Fill, Stroke};
use graphene_std::vector::{PointId, VectorModificationType};
use graphene_std::vector::{VectorData, VectorDataTable};
use graphene_std::{GraphicGroupTable, NodeInputDecleration};
#[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
pub enum TransformIn {
@@ -58,13 +58,13 @@ impl<'a> ModifyInputsContext<'a> {
/// Non layer nodes directly upstream of a layer are treated as part of that layer. See insert_index == 2 in the diagram
/// -----> Post node
/// | if insert_index == 0, return (Post node, Some(Layer1))
/// -> Layer1
/// -> Layer1
/// ↑ if insert_index == 1, return (Layer1, Some(Layer2))
/// -> Layer2
/// -> Layer2
/// ↑
/// -> NonLayerNode
/// ↑ if insert_index == 2, return (NonLayerNode, Some(Layer3))
/// -> Layer3
/// -> Layer3
/// if insert_index == 3, return (Layer3, None)
pub fn get_post_node_with_index(network_interface: &NodeNetworkInterface, parent: LayerNodeIdentifier, insert_index: usize) -> InputConnector {
let mut post_node_input_connector = if parent == LayerNodeIdentifier::ROOT_PARENT {
@@ -124,7 +124,7 @@ impl<'a> ModifyInputsContext<'a> {
pub fn create_artboard(&mut self, new_id: NodeId, artboard: Artboard) -> LayerNodeIdentifier {
let artboard_node_template = resolve_document_node_type("Artboard").expect("Node").node_template_input_override([
Some(NodeInput::value(TaggedValue::ArtboardGroup(graphene_std::ArtboardGroupTable::default()), true)),
Some(NodeInput::value(TaggedValue::GraphicGroup(graphene_core::GraphicGroupTable::default()), true)),
Some(NodeInput::value(TaggedValue::GraphicGroup(graphene_std::GraphicGroupTable::default()), true)),
Some(NodeInput::value(TaggedValue::IVec2(artboard.location), false)),
Some(NodeInput::value(TaggedValue::IVec2(artboard.dimensions), false)),
Some(NodeInput::value(TaggedValue::Color(artboard.background), false)),
@@ -134,7 +134,7 @@ impl<'a> ModifyInputsContext<'a> {
LayerNodeIdentifier::new(new_id, self.network_interface, &[])
}
pub fn insert_boolean_data(&mut self, operation: graphene_std::vector::misc::BooleanOperation, layer: LayerNodeIdentifier) {
pub fn insert_boolean_data(&mut self, operation: graphene_std::path_bool::BooleanOperation, layer: LayerNodeIdentifier) {
let boolean = resolve_document_node_type("Boolean Operation").expect("Boolean node does not exist").node_template_input_override([
Some(NodeInput::value(TaggedValue::GraphicGroup(graphene_std::GraphicGroupTable::default()), true)),
Some(NodeInput::value(TaggedValue::BooleanOperation(operation), false)),
@@ -209,11 +209,11 @@ impl<'a> ModifyInputsContext<'a> {
self.network_interface.move_node_to_chain_start(&stroke_id, layer, &[]);
}
pub fn insert_image_data(&mut self, image_frame: ImageFrameTable<Color>, layer: LayerNodeIdentifier) {
pub fn insert_image_data(&mut self, image_frame: RasterDataTable<CPU>, layer: LayerNodeIdentifier) {
let transform = resolve_document_node_type("Transform").expect("Transform node does not exist").default_node_template();
let image = resolve_document_node_type("Image")
.expect("Image node does not exist")
.node_template_input_override([Some(NodeInput::value(TaggedValue::None, false)), Some(NodeInput::value(TaggedValue::ImageFrame(image_frame), false))]);
let image = resolve_document_node_type("Image Value")
.expect("ImageValue node does not exist")
.node_template_input_override([Some(NodeInput::value(TaggedValue::None, false)), Some(NodeInput::value(TaggedValue::RasterData(image_frame), false))]);
let image_id = NodeId::new();
self.network_interface.insert_node(image_id, image, &[]);
@@ -289,17 +289,17 @@ impl<'a> ModifyInputsContext<'a> {
log::error!("Node type {} does not exist in ModifyInputsContext::existing_node_id", reference);
return None;
};
// If inserting a path node, insert a flatten vector elements if the type is a graphic group.
// If inserting a path node, insert a Flatten Path if the type is a graphic group.
// TODO: Allow the path node to operate on Graphic Group data by utilizing the reference for each vector data in a group.
if node_definition.identifier == "Path" {
let layer_input_type = self.network_interface.input_type(&InputConnector::node(output_layer.to_node(), 1), &[]).0.nested_type().clone();
if layer_input_type == concrete!(GraphicGroupTable) {
let Some(flatten_vector_elements_definition) = resolve_document_node_type("Flatten Vector Elements") else {
log::error!("Flatten Vector Elements does not exist in ModifyInputsContext::existing_node_id");
let Some(flatten_path_definition) = resolve_document_node_type("Flatten Path") else {
log::error!("Flatten Path does not exist in ModifyInputsContext::existing_node_id");
return None;
};
let node_id = NodeId::new();
self.network_interface.insert_node(node_id, flatten_vector_elements_definition.default_node_template(), &[]);
self.network_interface.insert_node(node_id, flatten_path_definition.default_node_template(), &[]);
self.network_interface.move_node_to_chain_start(&node_id, output_layer, &[]);
}
}
@@ -333,37 +333,52 @@ impl<'a> ModifyInputsContext<'a> {
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Fill(fill), false), false);
}
pub fn blend_mode_set(&mut self, blend_mode: BlendMode) {
let Some(blend_node_id) = self.existing_node_id("Blending", true) else { return };
let input_connector = InputConnector::node(blend_node_id, 1);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::BlendMode(blend_mode), false), false);
}
pub fn opacity_set(&mut self, opacity: f64) {
let Some(opacity_node_id) = self.existing_node_id("Opacity", true) else { return };
let input_connector = InputConnector::node(opacity_node_id, 1);
let Some(blend_node_id) = self.existing_node_id("Blending", true) else { return };
let input_connector = InputConnector::node(blend_node_id, 2);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(opacity * 100.), false), false);
}
pub fn blend_mode_set(&mut self, blend_mode: BlendMode) {
let Some(blend_mode_node_id) = self.existing_node_id("Blend Mode", true) else {
return;
};
let input_connector = InputConnector::node(blend_mode_node_id, 1);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::BlendMode(blend_mode), false), false);
pub fn blending_fill_set(&mut self, fill: f64) {
let Some(blend_node_id) = self.existing_node_id("Blending", true) else { return };
let input_connector = InputConnector::node(blend_node_id, 3);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(fill * 100.), false), false);
}
pub fn clip_mode_toggle(&mut self, clip_mode: Option<bool>) {
let clip = !clip_mode.unwrap_or(false);
let Some(clip_node_id) = self.existing_node_id("Blending", true) else { return };
let input_connector = InputConnector::node(clip_node_id, 4);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Bool(clip), false), false);
}
pub fn stroke_set(&mut self, stroke: Stroke) {
let Some(stroke_node_id) = self.existing_node_id("Stroke", true) else { return };
let input_connector = InputConnector::node(stroke_node_id, 1);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::ColorInput::<Option<graphene_std::Color>>::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::OptionalColor(stroke.color), false), true);
let input_connector = InputConnector::node(stroke_node_id, 2);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::WeightInput::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.weight), false), true);
let input_connector = InputConnector::node(stroke_node_id, 3);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::AlignInput::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::StrokeAlign(stroke.align), false), false);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::CapInput::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::StrokeCap(stroke.cap), false), true);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::JoinInput::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::StrokeJoin(stroke.join), false), true);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::MiterLimitInput::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.join_miter_limit), false), false);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::PaintOrderInput::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::PaintOrder(stroke.paint_order), false), false);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::DashLengthsInput::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::VecF64(stroke.dash_lengths), false), true);
let input_connector = InputConnector::node(stroke_node_id, 4);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::DashOffsetInput::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.dash_offset), false), true);
let input_connector = InputConnector::node(stroke_node_id, 5);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::LineCap(stroke.line_cap), false), true);
let input_connector = InputConnector::node(stroke_node_id, 6);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::LineJoin(stroke.line_join), false), true);
let input_connector = InputConnector::node(stroke_node_id, 7);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.line_join_miter_limit), false), false);
}
/// Update the transform value of the upstream Transform node based a change to its existing value and the given parent transform.
@@ -413,13 +428,10 @@ impl<'a> ModifyInputsContext<'a> {
pub fn transform_set_direct(&mut self, transform: DAffine2, skip_rerender: bool, transform_node_id: Option<NodeId>) {
// If the Transform node didn't exist yet, create it now
let Some(transform_node_id) = transform_node_id.or_else(|| {
// Check if the transform is the identity transform and if so, don't create a new Transform node
if let Some((scale, angle, translation)) = (transform.matrix2.determinant() != 0.).then(|| transform.to_scale_angle_translation()) {
// Check if the transform is the identity transform within an epsilon
if scale.x.abs() < 1e-6 && scale.y.abs() < 1e-6 && angle.abs() < 1e-6 && translation.x.abs() < 1e-6 && translation.y.abs() < 1e-6 {
// We don't want to pollute the graph with an unnecessary Transform node, so we avoid creating and setting it by returning None
return None;
}
// Check if the transform is the identity transform (within an epsilon) and if so, don't create a new Transform node
if transform.abs_diff_eq(DAffine2::IDENTITY, 1e-6) {
// We don't want to pollute the graph with an unnecessary Transform node, so we avoid creating and setting it by returning None
return None;
}
// Create the Transform node
@@ -453,7 +465,7 @@ impl<'a> ModifyInputsContext<'a> {
pub fn brush_modify(&mut self, strokes: Vec<BrushStroke>) {
let Some(brush_node_id) = self.existing_node_id("Brush", true) else { return };
self.set_input_with_refresh(InputConnector::node(brush_node_id, 2), NodeInput::value(TaggedValue::BrushStrokes(strokes), false), false);
self.set_input_with_refresh(InputConnector::node(brush_node_id, 1), NodeInput::value(TaggedValue::BrushStrokes(strokes), false), false);
}
pub fn resize_artboard(&mut self, location: IVec2, dimensions: IVec2) {
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,94 @@
use super::DocumentNodeDefinition;
use crate::messages::portfolio::document::utility_types::network_interface::{DocumentNodePersistentMetadata, NodeTemplate, PropertiesRow, WidgetOverride};
use graph_craft::ProtoNodeIdentifier;
use graph_craft::document::*;
use graphene_std::registry::*;
use graphene_std::*;
use std::collections::HashSet;
pub(super) fn post_process_nodes(mut custom: Vec<DocumentNodeDefinition>) -> Vec<DocumentNodeDefinition> {
// Remove struct generics
for DocumentNodeDefinition { node_template, .. } in custom.iter_mut() {
let NodeTemplate {
document_node: DocumentNode { implementation, .. },
..
} = node_template;
if let DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier { name }) = implementation {
if let Some((new_name, _suffix)) = name.rsplit_once("<") {
*name = Cow::Owned(new_name.to_string())
}
};
}
let node_registry = graphene_core::registry::NODE_REGISTRY.lock().unwrap();
'outer: for (id, metadata) in NODE_METADATA.lock().unwrap().iter() {
for node in custom.iter() {
let DocumentNodeDefinition {
node_template: NodeTemplate {
document_node: DocumentNode { implementation, .. },
..
},
..
} = node;
match implementation {
DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier { name }) if name == id => continue 'outer,
_ => (),
}
}
let NodeMetadata {
display_name,
category,
fields,
description,
properties,
} = metadata;
let Some(implementations) = &node_registry.get(id) else { continue };
let valid_inputs: HashSet<_> = implementations.iter().map(|(_, node_io)| node_io.call_argument.clone()).collect();
let first_node_io = implementations.first().map(|(_, node_io)| node_io).unwrap_or(const { &NodeIOTypes::empty() });
let input_type = if valid_inputs.len() > 1 { &const { generic!(D) } } else { &first_node_io.call_argument };
let output_type = &first_node_io.return_value;
let inputs = preprocessor::node_inputs(fields, first_node_io);
let node = DocumentNodeDefinition {
identifier: display_name,
node_template: NodeTemplate {
document_node: DocumentNode {
inputs,
manual_composition: Some(input_type.clone()),
implementation: DocumentNodeImplementation::ProtoNode(id.clone().into()),
visible: true,
skip_deduplication: false,
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {
// TODO: Store information for input overrides in the node macro
input_properties: fields
.iter()
.map(|f| match f.widget_override {
RegistryWidgetOverride::None => (f.name, f.description).into(),
RegistryWidgetOverride::Hidden => PropertiesRow::with_override(f.name, f.description, WidgetOverride::Hidden),
RegistryWidgetOverride::String(str) => PropertiesRow::with_override(f.name, f.description, WidgetOverride::String(str.to_string())),
RegistryWidgetOverride::Custom(str) => PropertiesRow::with_override(f.name, f.description, WidgetOverride::Custom(str.to_string())),
})
.collect(),
output_names: vec![output_type.to_string()],
has_primary_output: true,
locked: false,
..Default::default()
},
},
category: category.unwrap_or("UNCATEGORIZED"),
description: Cow::Borrowed(description),
properties: *properties,
};
custom.push(node);
}
custom
}
@@ -15,12 +15,14 @@ use crate::messages::portfolio::document::utility_types::network_interface::{
use crate::messages::portfolio::document::utility_types::nodes::{CollapsedLayers, LayerPanelEntry};
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
use crate::messages::tool::common_functionality::graph_modification_utils::get_clip_mode;
use crate::messages::tool::tool_messages::tool_prelude::{Key, MouseMotion};
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
use glam::{DAffine2, DVec2, IVec2};
use graph_craft::document::{DocumentNodeImplementation, NodeId, NodeInput};
use graph_craft::proto::GraphErrors;
use graphene_core::*;
use graphene_std::math::math_ext::QuadExt;
use graphene_std::*;
use renderer::Quad;
use std::cmp::Ordering;
@@ -566,10 +568,12 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
responses.add(DocumentMessage::AddTransaction);
let new_ids: HashMap<_, _> = data.iter().map(|(id, _)| (*id, NodeId::new())).collect();
let nodes: Vec<_> = new_ids.values().copied().collect();
responses.add(NodeGraphMessage::AddNodes {
nodes: data,
new_ids: new_ids.clone(),
});
responses.add(NodeGraphMessage::SelectedNodesSet { nodes })
}
NodeGraphMessage::PointerDown {
shift_click,
@@ -995,11 +999,13 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
responses.add(NodeGraphMessage::TogglePreview { node_id: preview_node });
self.preview_on_mouse_up = None;
}
if let Some(node_to_deselect) = self.deselect_on_pointer_up {
let mut new_selected_nodes = selected_nodes.selected_nodes_ref().clone();
new_selected_nodes.remove(node_to_deselect);
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: new_selected_nodes });
self.deselect_on_pointer_up = None;
if let Some(node_to_deselect) = self.deselect_on_pointer_up.take() {
if !self.drag_start.as_ref().is_some_and(|t| t.1) {
let mut new_selected_nodes = selected_nodes.selected_nodes_ref().clone();
new_selected_nodes.remove(node_to_deselect);
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: new_selected_nodes });
return;
}
}
let point = network_metadata
.persistent_metadata
@@ -1391,12 +1397,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
input,
});
responses.add(PropertiesPanelMessage::Refresh);
if (network_interface
.reference(&node_id, selection_network_path)
.is_none_or(|reference| *reference != Some("Imaginate".to_string())) // TODO: Potentially remove the reference to Imaginate
|| input_index == 0)
&& network_interface.connected_to_output(&node_id, selection_network_path)
{
if !(network_interface.reference(&node_id, selection_network_path).is_none() || input_index == 0) && network_interface.connected_to_output(&node_id, selection_network_path) {
responses.add(NodeGraphMessage::RunDocumentGraph);
}
}
@@ -2438,6 +2439,7 @@ impl NodeGraphMessageHandler {
}
});
let clippable = layer.can_be_clipped(network_interface.document_metadata());
let data = LayerPanelEntry {
id: node_id,
alias: network_interface.display_name(&node_id, &[]),
@@ -2457,6 +2459,8 @@ impl NodeGraphMessageHandler {
selected: selected_layers.contains(&node_id),
ancestor_of_selected: ancestors_of_selected.contains(&node_id),
descendant_of_selected: descendants_of_selected.contains(&node_id),
clipped: get_clip_mode(layer, network_interface).unwrap_or(false) && clippable,
clippable,
};
responses.add(FrontendMessage::UpdateDocumentLayerDetails { data });
}
@@ -11,25 +11,24 @@ use glam::{DAffine2, DVec2, IVec2, UVec2};
use graph_craft::Type;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput};
use graphene_core::raster::curve::Curve;
use graphene_core::raster::image::ImageFrameTable;
use graphene_core::raster::{
use graphene_std::animation::RealTimeMode;
use graphene_std::extract_xy::XY;
use graphene_std::path_bool::BooleanOperation;
use graphene_std::raster::curve::Curve;
use graphene_std::raster::{
BlendMode, CellularDistanceFunction, CellularReturnType, Color, DomainWarpType, FractalType, LuminanceCalculation, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute,
SelectiveColorChoice,
};
use graphene_core::text::Font;
use graphene_core::vector::generator_nodes::grid;
use graphene_core::vector::misc::CentroidType;
use graphene_core::vector::style::{GradientType, LineCap, LineJoin};
use graphene_std::animation::RealTimeMode;
use graphene_std::application_io::TextureFrameTable;
use graphene_std::ops::XY;
use graphene_std::raster_types::{CPU, GPU, RasterDataTable};
use graphene_std::text::Font;
use graphene_std::transform::{Footprint, ReferencePoint};
use graphene_std::vector::VectorDataTable;
use graphene_std::vector::misc::ArcType;
use graphene_std::vector::misc::{BooleanOperation, GridType};
use graphene_std::vector::misc::GridType;
use graphene_std::vector::misc::{ArcType, MergeByDistanceAlgorithm};
use graphene_std::vector::misc::{CentroidType, PointSpacingType};
use graphene_std::vector::style::{Fill, FillChoice, FillType, GradientStops};
use graphene_std::{GraphicGroupTable, NodeInputDecleration, RasterFrame};
use graphene_std::vector::style::{GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
use graphene_std::{GraphicGroupTable, NodeInputDecleration};
pub(crate) fn string_properties(text: &str) -> Vec<LayoutGroup> {
let widget = TextLabel::new(text).widget_holder();
@@ -122,6 +121,9 @@ pub(crate) fn property_from_type(
index: usize,
ty: &Type,
number_options: (Option<f64>, Option<f64>, Option<(f64, f64)>),
unit: Option<&str>,
display_decimal_places: Option<u32>,
step: Option<f64>,
context: &mut NodePropertiesContext,
) -> Result<Vec<LayoutGroup>, Vec<LayoutGroup>> {
let Some(network) = context.network_interface.nested_network(context.selection_network_path) else {
@@ -143,6 +145,15 @@ pub(crate) fn property_from_type(
number_max = Some(range_end);
number_input = number_input.mode_range().min(range_start).max(range_end);
}
if let Some(unit) = unit {
number_input = number_input.unit(unit);
}
if let Some(display_decimal_places) = display_decimal_places {
number_input = number_input.display_decimal_places(display_decimal_places);
}
if let Some(step) = step {
number_input = number_input.step(step);
}
let min = |x: f64| number_min.unwrap_or(x);
let max = |x: f64| number_max.unwrap_or(x);
@@ -156,15 +167,15 @@ pub(crate) fn property_from_type(
// Aliased types (ambiguous values)
Some("Percentage") => number_widget(default_info, number_input.percentage().min(min(0.)).max(max(100.))).into(),
Some("SignedPercentage") => number_widget(default_info, number_input.percentage().min(min(-100.)).max(max(100.))).into(),
Some("Angle") => number_widget(default_info, number_input.mode_range().min(min(-180.)).max(max(180.)).unit("°")).into(),
Some("Multiplier") => number_widget(default_info, number_input.unit("x")).into(),
Some("PixelLength") => number_widget(default_info, number_input.min(min(0.)).unit(" px")).into(),
Some("Angle") => number_widget(default_info, number_input.mode_range().min(min(-180.)).max(max(180.)).unit(unit.unwrap_or("°"))).into(),
Some("Multiplier") => number_widget(default_info, number_input.unit(unit.unwrap_or("x"))).into(),
Some("PixelLength") => number_widget(default_info, number_input.min(min(0.)).unit(unit.unwrap_or(" px"))).into(),
Some("Length") => number_widget(default_info, number_input.min(min(0.))).into(),
Some("Fraction") => number_widget(default_info, number_input.mode_range().min(min(0.)).max(max(1.))).into(),
Some("IntegerCount") => number_widget(default_info, number_input.int().min(min(1.))).into(),
Some("SeedValue") => number_widget(default_info, number_input.int().min(min(0.))).into(),
Some("Resolution") => vector2_widget(default_info, "W", "H", " px", Some(64.)),
Some("PixelSize") => vector2_widget(default_info, "X", "Y", " px", None),
Some("Resolution") => coordinate_widget(default_info, "W", "H", unit.unwrap_or(" px"), Some(64.)),
Some("PixelSize") => coordinate_widget(default_info, "X", "Y", unit.unwrap_or(" px"), None),
// For all other types, use TypeId-based matching
_ => {
@@ -178,19 +189,19 @@ pub(crate) fn property_from_type(
Some(x) if x == TypeId::of::<u64>() => number_widget(default_info, number_input.int().min(min(0.))).into(),
Some(x) if x == TypeId::of::<bool>() => bool_widget(default_info, CheckboxInput::default()).into(),
Some(x) if x == TypeId::of::<String>() => text_widget(default_info).into(),
Some(x) if x == TypeId::of::<DVec2>() => vector2_widget(default_info, "X", "Y", "", None),
Some(x) if x == TypeId::of::<UVec2>() => vector2_widget(default_info, "X", "Y", "", Some(0.)),
Some(x) if x == TypeId::of::<IVec2>() => vector2_widget(default_info, "X", "Y", "", None),
Some(x) if x == TypeId::of::<DVec2>() => coordinate_widget(default_info, "X", "Y", "", None),
Some(x) if x == TypeId::of::<UVec2>() => coordinate_widget(default_info, "X", "Y", "", Some(0.)),
Some(x) if x == TypeId::of::<IVec2>() => coordinate_widget(default_info, "X", "Y", "", None),
// ==========================
// PRIMITIVE COLLECTION TYPES
// ==========================
Some(x) if x == TypeId::of::<Vec<f64>>() => array_of_number_widget(default_info, TextInput::default()).into(),
Some(x) if x == TypeId::of::<Vec<DVec2>>() => array_of_vector2_widget(default_info, TextInput::default()).into(),
Some(x) if x == TypeId::of::<Vec<DVec2>>() => array_of_coordinates_widget(default_info, TextInput::default()).into(),
// ====================
// GRAPHICAL DATA TYPES
// ====================
Some(x) if x == TypeId::of::<VectorDataTable>() => vector_data_widget(default_info).into(),
Some(x) if x == TypeId::of::<RasterFrame>() || x == TypeId::of::<ImageFrameTable<Color>>() || x == TypeId::of::<TextureFrameTable>() => raster_widget(default_info).into(),
Some(x) if x == TypeId::of::<RasterDataTable<CPU>>() || x == TypeId::of::<RasterDataTable<GPU>>() => raster_widget(default_info).into(),
Some(x) if x == TypeId::of::<GraphicGroupTable>() => group_widget(default_info).into(),
// ============
// STRUCT TYPES
@@ -198,7 +209,7 @@ pub(crate) fn property_from_type(
Some(x) if x == TypeId::of::<Color>() => color_widget(default_info, ColorInput::default().allow_none(false)),
Some(x) if x == TypeId::of::<Option<Color>>() => color_widget(default_info, ColorInput::default().allow_none(true)),
Some(x) if x == TypeId::of::<GradientStops>() => color_widget(default_info, ColorInput::default().allow_none(false)),
Some(x) if x == TypeId::of::<Font>() => font_widget(default_info).into(),
Some(x) if x == TypeId::of::<Font>() => font_widget(default_info),
Some(x) if x == TypeId::of::<Curve>() => curve_widget(default_info),
Some(x) if x == TypeId::of::<Footprint>() => footprint_widget(default_info, &mut extra_widgets),
// ===============================
@@ -222,9 +233,13 @@ pub(crate) fn property_from_type(
Some(x) if x == TypeId::of::<DomainWarpType>() => enum_choice::<DomainWarpType>().for_socket(default_info).disabled(false).property_row(),
Some(x) if x == TypeId::of::<RelativeAbsolute>() => enum_choice::<RelativeAbsolute>().for_socket(default_info).disabled(false).property_row(),
Some(x) if x == TypeId::of::<GridType>() => enum_choice::<GridType>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<LineCap>() => enum_choice::<LineCap>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<LineJoin>() => enum_choice::<LineJoin>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<StrokeCap>() => enum_choice::<StrokeCap>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<StrokeJoin>() => enum_choice::<StrokeJoin>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<StrokeAlign>() => enum_choice::<StrokeAlign>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<PaintOrder>() => enum_choice::<PaintOrder>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<ArcType>() => enum_choice::<ArcType>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<MergeByDistanceAlgorithm>() => enum_choice::<MergeByDistanceAlgorithm>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<PointSpacingType>() => enum_choice::<PointSpacingType>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<BooleanOperation>() => enum_choice::<BooleanOperation>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<CentroidType>() => enum_choice::<CentroidType>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<LuminanceCalculation>() => enum_choice::<LuminanceCalculation>().for_socket(default_info).property_row(),
@@ -250,8 +265,8 @@ pub(crate) fn property_from_type(
}
}
Type::Generic(_) => vec![TextLabel::new("Generic type (not supported)").widget_holder()].into(),
Type::Fn(_, out) => return property_from_type(node_id, index, out, number_options, context),
Type::Future(out) => return property_from_type(node_id, index, out, number_options, context),
Type::Fn(_, out) => return property_from_type(node_id, index, out, number_options, unit, display_decimal_places, step, context),
Type::Future(out) => return property_from_type(node_id, index, out, number_options, unit, display_decimal_places, step, context),
};
extra_widgets.push(widgets);
@@ -335,6 +350,15 @@ pub fn reference_point_widget(parameter_widgets_info: ParameterWidgetsInfo, disa
if let Some(&TaggedValue::ReferencePoint(reference_point)) = input.as_non_exposed_value() {
widgets.extend_from_slice(&[
Separator::new(SeparatorType::Unrelated).widget_holder(),
CheckboxInput::new(reference_point != ReferencePoint::None)
.on_update(update_value(
move |x: &CheckboxInput| TaggedValue::ReferencePoint(if x.checked { ReferencePoint::Center } else { ReferencePoint::None }),
node_id,
index,
))
.disabled(disabled)
.widget_holder(),
Separator::new(SeparatorType::Related).widget_holder(),
ReferencePointInput::new(reference_point)
.on_update(update_value(move |x: &ReferencePointInput| TaggedValue::ReferencePoint(x.value), node_id, index))
.disabled(disabled)
@@ -490,7 +514,7 @@ pub fn footprint_widget(parameter_widgets_info: ParameterWidgetsInfo, extra_widg
last.clone()
}
pub fn vector2_widget(parameter_widgets_info: ParameterWidgetsInfo, x: &str, y: &str, unit: &str, min: Option<f64>) -> LayoutGroup {
pub fn coordinate_widget(parameter_widgets_info: ParameterWidgetsInfo, x: &str, y: &str, unit: &str, min: Option<f64>) -> LayoutGroup {
let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info;
let mut widgets = start_widgets(parameter_widgets_info, FrontendGraphDataType::Number);
@@ -633,7 +657,7 @@ pub fn array_of_number_widget(parameter_widgets_info: ParameterWidgetsInfo, text
widgets
}
pub fn array_of_vector2_widget(parameter_widgets_info: ParameterWidgetsInfo, text_props: TextInput) -> Vec<WidgetHolder> {
pub fn array_of_coordinates_widget(parameter_widgets_info: ParameterWidgetsInfo, text_props: TextInput) -> Vec<WidgetHolder> {
let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info;
let mut widgets = start_widgets(parameter_widgets_info, FrontendGraphDataType::Number);
@@ -916,8 +940,7 @@ pub fn get_document_node<'a>(node_id: NodeId, context: &'a NodePropertiesContext
}
pub fn query_node_and_input_info<'a>(node_id: NodeId, input_index: usize, context: &'a NodePropertiesContext<'a>) -> Result<(&'a DocumentNode, &'a str, &'a str), String> {
let node_id2 = node_id.clone();
let document_node = get_document_node(node_id2, context)?;
let document_node = get_document_node(node_id, context)?;
let input_name = context.network_interface.input_name(node_id, input_index, context.selection_network_path).unwrap_or_else(|| {
log::warn!("input name not found in query_node_and_input_info");
""
@@ -959,16 +982,19 @@ pub fn query_noise_pattern_state(node_id: NodeId, context: &NodePropertiesContex
}
pub fn query_assign_colors_randomize(node_id: NodeId, context: &NodePropertiesContext) -> Result<bool, String> {
use graphene_std::vector::assign_colors::*;
let document_node = get_document_node(node_id, context)?;
// This is safe since the node is a proto node and the implementation cannot be changed.
let randomize_index = 5;
Ok(match document_node.inputs.get(randomize_index).and_then(|input| input.as_value()) {
Ok(match document_node.inputs.get(RandomizeInput::INDEX).and_then(|input| input.as_value()) {
Some(TaggedValue::Bool(randomize_enabled)) => *randomize_enabled,
_ => false,
})
}
pub(crate) fn brightness_contrast_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
use graphene_std::raster::brightness_contrast::*;
let document_node = match get_document_node(node_id, context) {
Ok(document_node) => document_node,
Err(err) => {
@@ -978,17 +1004,18 @@ pub(crate) fn brightness_contrast_properties(node_id: NodeId, context: &mut Node
};
// Use Classic
let use_classic_index = 3;
let use_classic = bool_widget(ParameterWidgetsInfo::from_index(document_node, node_id, use_classic_index, true, context), CheckboxInput::default());
let use_classic_value = match document_node.inputs[use_classic_index].as_value() {
let use_classic = bool_widget(
ParameterWidgetsInfo::from_index(document_node, node_id, UseClassicInput::INDEX, true, context),
CheckboxInput::default(),
);
let use_classic_value = match document_node.inputs[UseClassicInput::INDEX].as_value() {
Some(TaggedValue::Bool(use_classic_choice)) => *use_classic_choice,
_ => false,
};
// Brightness
let brightness_index = 1;
let brightness = number_widget(
ParameterWidgetsInfo::from_index(document_node, node_id, brightness_index, true, context),
ParameterWidgetsInfo::from_index(document_node, node_id, BrightnessInput::INDEX, true, context),
NumberInput::default()
.unit("%")
.mode_range()
@@ -998,9 +1025,8 @@ pub(crate) fn brightness_contrast_properties(node_id: NodeId, context: &mut Node
);
// Contrast
let contrast_index = 2;
let contrast = number_widget(
ParameterWidgetsInfo::from_index(document_node, node_id, contrast_index, true, context),
ParameterWidgetsInfo::from_index(document_node, node_id, ContrastInput::INDEX, true, context),
NumberInput::default()
.unit("%")
.mode_range()
@@ -1019,6 +1045,8 @@ pub(crate) fn brightness_contrast_properties(node_id: NodeId, context: &mut Node
}
pub(crate) fn channel_mixer_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
use graphene_std::raster::channel_mixer::*;
let document_node = match get_document_node(node_id, context) {
Ok(document_node) => document_node,
Err(err) => {
@@ -1028,20 +1056,21 @@ pub(crate) fn channel_mixer_properties(node_id: NodeId, context: &mut NodeProper
};
// Monochrome
let monochrome_index = 1;
let is_monochrome = bool_widget(ParameterWidgetsInfo::from_index(document_node, node_id, monochrome_index, true, context), CheckboxInput::default());
let is_monochrome_value = match document_node.inputs[monochrome_index].as_value() {
let is_monochrome = bool_widget(
ParameterWidgetsInfo::from_index(document_node, node_id, MonochromeInput::INDEX, true, context),
CheckboxInput::default(),
);
let is_monochrome_value = match document_node.inputs[MonochromeInput::INDEX].as_value() {
Some(TaggedValue::Bool(monochrome_choice)) => *monochrome_choice,
_ => false,
};
// Output channel choice
let output_channel_index = 18;
let output_channel = enum_choice::<RedGreenBlue>()
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, output_channel_index, true, context))
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, OutputChannelInput::INDEX, true, context))
.exposable(false)
.property_row();
let output_channel_value = match &document_node.inputs[output_channel_index].as_value() {
let output_channel_value = match &document_node.inputs[OutputChannelInput::INDEX].as_value() {
Some(TaggedValue::RedGreenBlue(choice)) => choice,
_ => {
warn!("Channel Mixer node properties panel could not be displayed.");
@@ -1051,10 +1080,10 @@ pub(crate) fn channel_mixer_properties(node_id: NodeId, context: &mut NodeProper
// Output Channel modes
let (red_output_index, green_output_index, blue_output_index, constant_output_index) = match (is_monochrome_value, output_channel_value) {
(true, _) => (2, 3, 4, 5),
(false, RedGreenBlue::Red) => (6, 7, 8, 9),
(false, RedGreenBlue::Green) => (10, 11, 12, 13),
(false, RedGreenBlue::Blue) => (14, 15, 16, 17),
(true, _) => (MonochromeRInput::INDEX, MonochromeGInput::INDEX, MonochromeBInput::INDEX, MonochromeCInput::INDEX),
(false, RedGreenBlue::Red) => (RedRInput::INDEX, RedGInput::INDEX, RedBInput::INDEX, RedCInput::INDEX),
(false, RedGreenBlue::Green) => (GreenRInput::INDEX, GreenGInput::INDEX, GreenBInput::INDEX, GreenCInput::INDEX),
(false, RedGreenBlue::Blue) => (BlueRInput::INDEX, BlueGInput::INDEX, BlueBInput::INDEX, BlueCInput::INDEX),
};
let number_input = NumberInput::default().mode_range().min(-200.).max(200.).unit("%");
let red = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, red_output_index, true, context), number_input.clone());
@@ -1079,6 +1108,8 @@ pub(crate) fn channel_mixer_properties(node_id: NodeId, context: &mut NodeProper
}
pub(crate) fn selective_color_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
use graphene_std::raster::selective_color::*;
let document_node = match get_document_node(node_id, context) {
Ok(document_node) => document_node,
Err(err) => {
@@ -1086,15 +1117,14 @@ pub(crate) fn selective_color_properties(node_id: NodeId, context: &mut NodeProp
return Vec::new();
}
};
// Colors choice
let colors_index = 38;
// Colors choice
let colors = enum_choice::<SelectiveColorChoice>()
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, colors_index, true, context))
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, ColorsInput::INDEX, true, context))
.exposable(false)
.property_row();
let colors_choice_index = match &document_node.inputs[colors_index].as_value() {
let colors_choice = match &document_node.inputs[ColorsInput::INDEX].as_value() {
Some(TaggedValue::SelectiveColorChoice(choice)) => choice,
_ => {
warn!("Selective Color node properties panel could not be displayed.");
@@ -1103,16 +1133,16 @@ pub(crate) fn selective_color_properties(node_id: NodeId, context: &mut NodeProp
};
// CMYK
let (c_index, m_index, y_index, k_index) = match colors_choice_index {
SelectiveColorChoice::Reds => (2, 3, 4, 5),
SelectiveColorChoice::Yellows => (6, 7, 8, 9),
SelectiveColorChoice::Greens => (10, 11, 12, 13),
SelectiveColorChoice::Cyans => (14, 15, 16, 17),
SelectiveColorChoice::Blues => (18, 19, 20, 21),
SelectiveColorChoice::Magentas => (22, 23, 24, 25),
SelectiveColorChoice::Whites => (26, 27, 28, 29),
SelectiveColorChoice::Neutrals => (30, 31, 32, 33),
SelectiveColorChoice::Blacks => (34, 35, 36, 37),
let (c_index, m_index, y_index, k_index) = match colors_choice {
SelectiveColorChoice::Reds => (RCInput::INDEX, RMInput::INDEX, RYInput::INDEX, RKInput::INDEX),
SelectiveColorChoice::Yellows => (YCInput::INDEX, YMInput::INDEX, YYInput::INDEX, YKInput::INDEX),
SelectiveColorChoice::Greens => (GCInput::INDEX, GMInput::INDEX, GYInput::INDEX, GKInput::INDEX),
SelectiveColorChoice::Cyans => (CCInput::INDEX, CMInput::INDEX, CYInput::INDEX, CKInput::INDEX),
SelectiveColorChoice::Blues => (BCInput::INDEX, BMInput::INDEX, BYInput::INDEX, BKInput::INDEX),
SelectiveColorChoice::Magentas => (MCInput::INDEX, MMInput::INDEX, MYInput::INDEX, MKInput::INDEX),
SelectiveColorChoice::Whites => (WCInput::INDEX, WMInput::INDEX, WYInput::INDEX, WKInput::INDEX),
SelectiveColorChoice::Neutrals => (NCInput::INDEX, NMInput::INDEX, NYInput::INDEX, NKInput::INDEX),
SelectiveColorChoice::Blacks => (KCInput::INDEX, KMInput::INDEX, KYInput::INDEX, KKInput::INDEX),
};
let number_input = NumberInput::default().mode_range().min(-100.).max(100.).unit("%");
let cyan = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, c_index, true, context), number_input.clone());
@@ -1121,9 +1151,8 @@ pub(crate) fn selective_color_properties(node_id: NodeId, context: &mut NodeProp
let black = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, k_index, true, context), number_input);
// Mode
let mode_index = 1;
let mode = enum_choice::<RelativeAbsolute>()
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, mode_index, true, context))
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, ModeInput::INDEX, true, context))
.property_row();
vec![
@@ -1139,19 +1168,8 @@ pub(crate) fn selective_color_properties(node_id: NodeId, context: &mut NodeProp
]
}
#[cfg(feature = "gpu")]
pub(crate) fn _gpu_map_properties(parameter_widgets_info: ParameterWidgetsInfo) -> Vec<LayoutGroup> {
let map = text_widget(parameter_widgets_info);
vec![LayoutGroup::Row { widgets: map }]
}
pub(crate) fn grid_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
let grid_type_index = grid::GridTypeInput::INDEX;
let spacing_index = grid::SpacingInput::<f64>::INDEX;
let angles_index = grid::AnglesInput::INDEX;
let rows_index = grid::RowsInput::INDEX;
let columns_index = grid::ColumnsInput::INDEX;
use graphene_std::vector::generator_nodes::grid::*;
let document_node = match get_document_node(node_id, context) {
Ok(document_node) => document_node,
@@ -1161,43 +1179,115 @@ pub(crate) fn grid_properties(node_id: NodeId, context: &mut NodePropertiesConte
}
};
let grid_type = enum_choice::<GridType>()
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, grid_type_index, true, context))
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, GridTypeInput::INDEX, true, context))
.property_row();
let mut widgets = vec![grid_type];
let Some(grid_type_input) = document_node.inputs.get(grid_type_index) else {
let Some(grid_type_input) = document_node.inputs.get(GridTypeInput::INDEX) else {
log::warn!("A widget failed to be built because its node's input index is invalid.");
return vec![];
};
if let Some(&TaggedValue::GridType(grid_type)) = grid_type_input.as_non_exposed_value() {
match grid_type {
GridType::Rectangular => {
let spacing = vector2_widget(ParameterWidgetsInfo::from_index(document_node, node_id, spacing_index, true, context), "W", "H", " px", Some(0.));
let spacing = coordinate_widget(
ParameterWidgetsInfo::from_index(document_node, node_id, SpacingInput::<f64>::INDEX, true, context),
"W",
"H",
" px",
Some(0.),
);
widgets.push(spacing);
}
GridType::Isometric => {
let spacing = LayoutGroup::Row {
widgets: number_widget(
ParameterWidgetsInfo::from_index(document_node, node_id, spacing_index, true, context),
ParameterWidgetsInfo::from_index(document_node, node_id, SpacingInput::<f64>::INDEX, true, context),
NumberInput::default().label("H").min(0.).unit(" px"),
),
};
let angles = vector2_widget(ParameterWidgetsInfo::from_index(document_node, node_id, angles_index, true, context), "", "", "°", None);
let angles = coordinate_widget(ParameterWidgetsInfo::from_index(document_node, node_id, AnglesInput::INDEX, true, context), "", "", "°", None);
widgets.extend([spacing, angles]);
}
}
}
let rows = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, rows_index, true, context), NumberInput::default().min(1.));
let columns = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, columns_index, true, context), NumberInput::default().min(1.));
let columns = number_widget(
ParameterWidgetsInfo::from_index(document_node, node_id, ColumnsInput::INDEX, true, context),
NumberInput::default().min(1.),
);
let rows = number_widget(
ParameterWidgetsInfo::from_index(document_node, node_id, RowsInput::INDEX, true, context),
NumberInput::default().min(1.),
);
widgets.extend([LayoutGroup::Row { widgets: rows }, LayoutGroup::Row { widgets: columns }]);
widgets.extend([LayoutGroup::Row { widgets: columns }, LayoutGroup::Row { widgets: rows }]);
widgets
}
pub(crate) const SAMPLE_POLYLINE_TOOLTIP_SPACING: &str = "Use a point sampling density controlled by a distance between, or specific number of, points.";
pub(crate) const SAMPLE_POLYLINE_TOOLTIP_SEPARATION: &str = "Distance between each instance (exact if 'Adaptive Spacing' is disabled, approximate if enabled).";
pub(crate) const SAMPLE_POLYLINE_TOOLTIP_QUANTITY: &str = "Number of points to place along the path.";
pub(crate) const SAMPLE_POLYLINE_TOOLTIP_START_OFFSET: &str = "Exclude some distance from the start of the path before the first instance.";
pub(crate) const SAMPLE_POLYLINE_TOOLTIP_STOP_OFFSET: &str = "Exclude some distance from the end of the path after the last instance.";
pub(crate) const SAMPLE_POLYLINE_TOOLTIP_ADAPTIVE_SPACING: &str = "Round 'Separation' to a nearby value that divides into the path length evenly.";
pub(crate) fn sample_polyline_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
use graphene_std::vector::sample_polyline::*;
let document_node = match get_document_node(node_id, context) {
Ok(document_node) => document_node,
Err(err) => {
log::error!("Could not get document node in sample_polyline_properties: {err}");
return Vec::new();
}
};
let current_spacing = document_node.inputs.get(SpacingInput::INDEX).and_then(|input| input.as_value()).cloned();
let is_quantity = matches!(current_spacing, Some(TaggedValue::PointSpacingType(PointSpacingType::Quantity)));
let spacing = enum_choice::<PointSpacingType>()
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, SpacingInput::INDEX, true, context))
.property_row();
let separation = number_widget(
ParameterWidgetsInfo::from_index(document_node, node_id, SeparationInput::INDEX, true, context),
NumberInput::default().min(0.).unit(" px"),
);
let quantity = number_widget(
ParameterWidgetsInfo::from_index(document_node, node_id, QuantityInput::INDEX, true, context),
NumberInput::default().min(2.).int(),
);
let start_offset = number_widget(
ParameterWidgetsInfo::from_index(document_node, node_id, StartOffsetInput::INDEX, true, context),
NumberInput::default().min(0.).unit(" px"),
);
let stop_offset = number_widget(
ParameterWidgetsInfo::from_index(document_node, node_id, StopOffsetInput::INDEX, true, context),
NumberInput::default().min(0.).unit(" px"),
);
let adaptive_spacing = bool_widget(
ParameterWidgetsInfo::from_index(document_node, node_id, AdaptiveSpacingInput::INDEX, true, context),
CheckboxInput::default().disabled(is_quantity),
);
vec![
spacing.with_tooltip(SAMPLE_POLYLINE_TOOLTIP_SPACING),
match current_spacing {
Some(TaggedValue::PointSpacingType(PointSpacingType::Separation)) => LayoutGroup::Row { widgets: separation }.with_tooltip(SAMPLE_POLYLINE_TOOLTIP_SEPARATION),
Some(TaggedValue::PointSpacingType(PointSpacingType::Quantity)) => LayoutGroup::Row { widgets: quantity }.with_tooltip(SAMPLE_POLYLINE_TOOLTIP_QUANTITY),
_ => LayoutGroup::Row { widgets: vec![] },
},
LayoutGroup::Row { widgets: start_offset }.with_tooltip(SAMPLE_POLYLINE_TOOLTIP_START_OFFSET),
LayoutGroup::Row { widgets: stop_offset }.with_tooltip(SAMPLE_POLYLINE_TOOLTIP_STOP_OFFSET),
LayoutGroup::Row { widgets: adaptive_spacing }.with_tooltip(SAMPLE_POLYLINE_TOOLTIP_ADAPTIVE_SPACING),
]
}
pub(crate) fn exposure_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
use graphene_std::raster::exposure::*;
let document_node = match get_document_node(node_id, context) {
Ok(document_node) => document_node,
Err(err) => {
@@ -1205,10 +1295,16 @@ pub(crate) fn exposure_properties(node_id: NodeId, context: &mut NodePropertiesC
return Vec::new();
}
};
let exposure = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, 1, true, context), NumberInput::default().min(-20.).max(20.));
let offset = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, 2, true, context), NumberInput::default().min(-0.5).max(0.5));
let exposure = number_widget(
ParameterWidgetsInfo::from_index(document_node, node_id, ExposureInput::INDEX, true, context),
NumberInput::default().min(-20.).max(20.),
);
let offset = number_widget(
ParameterWidgetsInfo::from_index(document_node, node_id, OffsetInput::INDEX, true, context),
NumberInput::default().min(-0.5).max(0.5),
);
let gamma_correction = number_widget(
ParameterWidgetsInfo::from_index(document_node, node_id, 3, true, context),
ParameterWidgetsInfo::from_index(document_node, node_id, GammaCorrectionInput::INDEX, true, context),
NumberInput::default().min(0.01).max(9.99).increment_step(0.1),
);
@@ -1220,6 +1316,8 @@ pub(crate) fn exposure_properties(node_id: NodeId, context: &mut NodePropertiesC
}
pub(crate) fn rectangle_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
use graphene_std::vector::generator_nodes::rectangle::*;
let document_node = match get_document_node(node_id, context) {
Ok(document_node) => document_node,
Err(err) => {
@@ -1227,21 +1325,15 @@ pub(crate) fn rectangle_properties(node_id: NodeId, context: &mut NodeProperties
return Vec::new();
}
};
let size_x_index = 1;
let size_y_index = 2;
let corner_rounding_type_index = 3;
let corner_radius_index = 4;
let clamped_index = 5;
// Size X
let size_x = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, size_x_index, true, context), NumberInput::default());
let size_x = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, WidthInput::INDEX, true, context), NumberInput::default());
// Size Y
let size_y = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, size_y_index, true, context), NumberInput::default());
let size_y = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, HeightInput::INDEX, true, context), NumberInput::default());
// Corner Radius
let mut corner_radius_row_1 = start_widgets(
ParameterWidgetsInfo::from_index(document_node, node_id, corner_radius_index, true, context),
ParameterWidgetsInfo::from_index(document_node, node_id, CornerRadiusInput::<f64>::INDEX, true, context),
FrontendGraphDataType::Number,
);
corner_radius_row_1.push(Separator::new(SeparatorType::Unrelated).widget_holder());
@@ -1250,13 +1342,13 @@ pub(crate) fn rectangle_properties(node_id: NodeId, context: &mut NodeProperties
corner_radius_row_2.push(TextLabel::new("").widget_holder());
add_blank_assist(&mut corner_radius_row_2);
let Some(input) = document_node.inputs.get(corner_rounding_type_index) else {
let Some(input) = document_node.inputs.get(IndividualCornerRadiiInput::INDEX) else {
log::warn!("A widget failed to be built because its node's input index is invalid.");
return vec![];
};
if let Some(&TaggedValue::Bool(is_individual)) = input.as_non_exposed_value() {
// Values
let Some(input) = document_node.inputs.get(corner_radius_index) else {
let Some(input) = document_node.inputs.get(CornerRadiusInput::<f64>::INDEX) else {
log::warn!("A widget failed to be built because its node's input index is invalid.");
return vec![];
};
@@ -1278,13 +1370,13 @@ pub(crate) fn rectangle_properties(node_id: NodeId, context: &mut NodeProperties
Message::Batched(Box::new([
NodeGraphMessage::SetInputValue {
node_id,
input_index: corner_rounding_type_index,
input_index: IndividualCornerRadiiInput::INDEX,
value: TaggedValue::Bool(false),
}
.into(),
NodeGraphMessage::SetInputValue {
node_id,
input_index: corner_radius_index,
input_index: CornerRadiusInput::<f64>::INDEX,
value: TaggedValue::F64(uniform_val),
}
.into(),
@@ -1297,13 +1389,13 @@ pub(crate) fn rectangle_properties(node_id: NodeId, context: &mut NodeProperties
Message::Batched(Box::new([
NodeGraphMessage::SetInputValue {
node_id,
input_index: corner_rounding_type_index,
input_index: IndividualCornerRadiiInput::INDEX,
value: TaggedValue::Bool(true),
}
.into(),
NodeGraphMessage::SetInputValue {
node_id,
input_index: corner_radius_index,
input_index: CornerRadiusInput::<f64>::INDEX,
value: TaggedValue::F64Array4(individual_val),
}
.into(),
@@ -1330,12 +1422,12 @@ pub(crate) fn rectangle_properties(node_id: NodeId, context: &mut NodeProperties
};
TextInput::default()
.value(individual_val.iter().map(|v| v.to_string()).collect::<Vec<_>>().join(", "))
.on_update(optionally_update_value(move |x: &TextInput| from_string(&x.value), node_id, corner_radius_index))
.on_update(optionally_update_value(move |x: &TextInput| from_string(&x.value), node_id, CornerRadiusInput::<f64>::INDEX))
.widget_holder()
} else {
NumberInput::default()
.value(Some(uniform_val))
.on_update(update_value(move |x: &NumberInput| TaggedValue::F64(x.value.unwrap()), node_id, corner_radius_index))
.on_update(update_value(move |x: &NumberInput| TaggedValue::F64(x.value.unwrap()), node_id, CornerRadiusInput::<f64>::INDEX))
.on_commit(commit_value)
.widget_holder()
};
@@ -1343,7 +1435,7 @@ pub(crate) fn rectangle_properties(node_id: NodeId, context: &mut NodeProperties
}
// Clamped
let clamped = bool_widget(ParameterWidgetsInfo::from_index(document_node, node_id, clamped_index, true, context), CheckboxInput::default());
let clamped = bool_widget(ParameterWidgetsInfo::from_index(document_node, node_id, ClampedInput::INDEX, true, context), CheckboxInput::default());
vec![
LayoutGroup::Row { widgets: size_x },
@@ -1387,15 +1479,21 @@ pub(crate) fn generate_node_properties(node_id: NodeId, context: &mut NodeProper
};
let mut number_options = (None, None, None);
let mut display_decimal_places = None;
let mut step = None;
let mut unit_suffix = None;
let input_type = match implementation {
DocumentNodeImplementation::ProtoNode(proto_node_identifier) => 'early_return: {
if let Some(field) = graphene_core::registry::NODE_METADATA
if let Some(field) = graphene_std::registry::NODE_METADATA
.lock()
.unwrap()
.get(&proto_node_identifier.name.clone().into_owned())
.and_then(|metadata| metadata.fields.get(input_index))
{
number_options = (field.number_min, field.number_max, field.number_mode_range);
display_decimal_places = field.number_display_decimal_places;
unit_suffix = field.unit;
step = field.number_step;
if let Some(ref default) = field.default_type {
break 'early_return default.clone();
}
@@ -1409,7 +1507,7 @@ pub(crate) fn generate_node_properties(node_id: NodeId, context: &mut NodeProper
let mut input_types = implementations
.keys()
.filter_map(|item| item.inputs.get(input_index))
.filter(|ty| property_from_type(node_id, input_index, ty, number_options, context).is_ok())
.filter(|ty| property_from_type(node_id, input_index, ty, number_options, unit_suffix, display_decimal_places, step, context).is_ok())
.collect::<Vec<_>>();
input_types.sort_by_key(|ty| ty.type_name());
let input_type = input_types.first().cloned();
@@ -1423,7 +1521,7 @@ pub(crate) fn generate_node_properties(node_id: NodeId, context: &mut NodeProper
_ => context.network_interface.input_type(&InputConnector::node(node_id, input_index), context.selection_network_path).0,
};
property_from_type(node_id, input_index, &input_type, number_options, context).unwrap_or_else(|value| value)
property_from_type(node_id, input_index, &input_type, number_options, unit_suffix, display_decimal_places, step, context).unwrap_or_else(|value| value)
});
layout.extend(row);
@@ -1464,6 +1562,8 @@ pub(crate) fn generate_node_properties(node_id: NodeId, context: &mut NodeProper
/// Fill Node Widgets LayoutGroup
pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
use graphene_std::vector::fill::*;
let document_node = match get_document_node(node_id, context) {
Ok(document_node) => document_node,
Err(err) => {
@@ -1471,16 +1571,16 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
return Vec::new();
}
};
let fill_index = 1;
let backup_color_index = 2;
let backup_gradient_index = 3;
let mut widgets_first_row = start_widgets(ParameterWidgetsInfo::from_index(document_node, node_id, fill_index, true, context), FrontendGraphDataType::General);
let mut widgets_first_row = start_widgets(
ParameterWidgetsInfo::from_index(document_node, node_id, FillInput::<Color>::INDEX, true, context),
FrontendGraphDataType::General,
);
let (fill, backup_color, backup_gradient) = if let (Some(TaggedValue::Fill(fill)), &Some(&TaggedValue::OptionalColor(backup_color)), Some(TaggedValue::Gradient(backup_gradient))) = (
&document_node.inputs[fill_index].as_value(),
&document_node.inputs[backup_color_index].as_value(),
&document_node.inputs[backup_gradient_index].as_value(),
&document_node.inputs[FillInput::<Color>::INDEX].as_value(),
&document_node.inputs[BackupColorInput::INDEX].as_value(),
&document_node.inputs[BackupGradientInput::INDEX].as_value(),
) {
(fill, backup_color, backup_gradient)
} else {
@@ -1499,26 +1599,26 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
match &fill2 {
Fill::None => NodeGraphMessage::SetInputValue {
node_id,
input_index: backup_color_index,
input_index: BackupColorInput::INDEX,
value: TaggedValue::OptionalColor(None),
}
.into(),
Fill::Solid(color) => NodeGraphMessage::SetInputValue {
node_id,
input_index: backup_color_index,
input_index: BackupColorInput::INDEX,
value: TaggedValue::OptionalColor(Some(*color)),
}
.into(),
Fill::Gradient(gradient) => NodeGraphMessage::SetInputValue {
node_id,
input_index: backup_gradient_index,
input_index: BackupGradientInput::INDEX,
value: TaggedValue::Gradient(gradient.clone()),
}
.into(),
},
NodeGraphMessage::SetInputValue {
node_id,
input_index: fill_index,
input_index: FillInput::<Color>::INDEX,
value: TaggedValue::Fill(x.value.to_fill(fill2.as_gradient())),
}
.into(),
@@ -1546,7 +1646,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
}
},
node_id,
fill_index,
FillInput::<Color>::INDEX,
))
.widget_holder();
row.push(Separator::new(SeparatorType::Unrelated).widget_holder());
@@ -1557,11 +1657,11 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
let entries = vec![
RadioEntryData::new("solid")
.label("Solid")
.on_update(update_value(move |_| TaggedValue::Fill(backup_color_fill.clone()), node_id, fill_index))
.on_update(update_value(move |_| TaggedValue::Fill(backup_color_fill.clone()), node_id, FillInput::<Color>::INDEX))
.on_commit(commit_value),
RadioEntryData::new("gradient")
.label("Gradient")
.on_update(update_value(move |_| TaggedValue::Fill(backup_gradient_fill.clone()), node_id, fill_index))
.on_update(update_value(move |_| TaggedValue::Fill(backup_gradient_fill.clone()), node_id, FillInput::<Color>::INDEX))
.on_commit(commit_value),
];
@@ -1596,7 +1696,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
}
},
node_id,
fill_index,
FillInput::<Color>::INDEX,
))
.widget_holder();
row.push(Separator::new(SeparatorType::Unrelated).widget_holder());
@@ -1617,7 +1717,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
TaggedValue::Fill(Fill::Gradient(new_gradient))
},
node_id,
fill_index,
FillInput::<Color>::INDEX,
))
.on_commit(commit_value),
RadioEntryData::new("Radial")
@@ -1629,7 +1729,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
TaggedValue::Fill(Fill::Gradient(new_gradient))
},
node_id,
fill_index,
FillInput::<Color>::INDEX,
))
.on_commit(commit_value),
];
@@ -1646,6 +1746,8 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
}
pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
use graphene_std::vector::stroke::*;
let document_node = match get_document_node(node_id, context) {
Ok(document_node) => document_node,
Err(err) => {
@@ -1653,57 +1755,64 @@ pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) -
return Vec::new();
}
};
let color_index = 1;
let weight_index = 2;
let dash_lengths_index = 3;
let dash_offset_index = 4;
let line_cap_index = 5;
let line_join_index = 6;
let miter_limit_index = 7;
let color = color_widget(ParameterWidgetsInfo::from_index(document_node, node_id, color_index, true, context), ColorInput::default());
let color = color_widget(
ParameterWidgetsInfo::from_index(document_node, node_id, ColorInput::<Option<Color>>::INDEX, true, context),
crate::messages::layout::utility_types::widgets::button_widgets::ColorInput::default(),
);
let weight = number_widget(
ParameterWidgetsInfo::from_index(document_node, node_id, weight_index, true, context),
ParameterWidgetsInfo::from_index(document_node, node_id, WeightInput::INDEX, true, context),
NumberInput::default().unit(" px").min(0.),
);
let dash_lengths_val = match &document_node.inputs[dash_lengths_index].as_value() {
let align = enum_choice::<StrokeAlign>()
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, AlignInput::INDEX, true, context))
.property_row();
let cap = enum_choice::<StrokeCap>()
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, CapInput::INDEX, true, context))
.property_row();
let join = enum_choice::<StrokeJoin>()
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, JoinInput::INDEX, true, context))
.property_row();
let miter_limit = number_widget(
ParameterWidgetsInfo::from_index(document_node, node_id, MiterLimitInput::INDEX, true, context),
NumberInput::default().min(0.).disabled({
let join_value = match &document_node.inputs[JoinInput::INDEX].as_value() {
Some(TaggedValue::StrokeJoin(x)) => x,
_ => &StrokeJoin::Miter,
};
join_value != &StrokeJoin::Miter
}),
);
let paint_order = enum_choice::<PaintOrder>()
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, PaintOrderInput::INDEX, true, context))
.property_row();
let dash_lengths_val = match &document_node.inputs[DashLengthsInput::INDEX].as_value() {
Some(TaggedValue::VecF64(x)) => x,
_ => &vec![],
};
let dash_lengths = array_of_number_widget(
ParameterWidgetsInfo::from_index(document_node, node_id, dash_lengths_index, true, context),
ParameterWidgetsInfo::from_index(document_node, node_id, DashLengthsInput::INDEX, true, context),
TextInput::default().centered(true),
);
let number_input = NumberInput::default().unit(" px").disabled(dash_lengths_val.is_empty());
let dash_offset = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, dash_offset_index, true, context), number_input);
let line_cap = enum_choice::<LineCap>()
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, line_cap_index, true, context))
.property_row();
let line_join = enum_choice::<LineJoin>()
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, line_join_index, true, context))
.property_row();
let line_join_val = match &document_node.inputs[line_join_index].as_value() {
Some(TaggedValue::LineJoin(x)) => x,
_ => &LineJoin::Miter,
};
let miter_limit = number_widget(
ParameterWidgetsInfo::from_index(document_node, node_id, miter_limit_index, true, context),
NumberInput::default().min(0.).disabled(line_join_val != &LineJoin::Miter),
);
let dash_offset = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, DashOffsetInput::INDEX, true, context), number_input);
vec![
color,
LayoutGroup::Row { widgets: weight },
align,
cap,
join,
LayoutGroup::Row { widgets: miter_limit },
paint_order,
LayoutGroup::Row { widgets: dash_lengths },
LayoutGroup::Row { widgets: dash_offset },
line_cap,
line_join,
LayoutGroup::Row { widgets: miter_limit },
]
}
pub fn offset_path_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
use graphene_std::vector::offset_path::*;
let document_node = match get_document_node(node_id, context) {
Ok(document_node) => document_node,
Err(err) => {
@@ -1711,28 +1820,28 @@ pub fn offset_path_properties(node_id: NodeId, context: &mut NodePropertiesConte
return Vec::new();
}
};
let distance_index = 1;
let line_join_index = 2;
let miter_limit_index = 3;
let number_input = NumberInput::default().unit(" px");
let distance = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, distance_index, true, context), number_input);
let distance = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, DistanceInput::INDEX, true, context), number_input);
let line_join = enum_choice::<LineJoin>()
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, line_join_index, true, context))
let join = enum_choice::<StrokeJoin>()
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, JoinInput::INDEX, true, context))
.property_row();
let line_join_val = match &document_node.inputs[line_join_index].as_value() {
Some(TaggedValue::LineJoin(x)) => x,
_ => &LineJoin::Miter,
};
let number_input = NumberInput::default().min(0.).disabled(line_join_val != &LineJoin::Miter);
let miter_limit = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, miter_limit_index, true, context), number_input);
let number_input = NumberInput::default().min(0.).disabled({
let join_val = match &document_node.inputs[JoinInput::INDEX].as_value() {
Some(TaggedValue::StrokeJoin(x)) => x,
_ => &StrokeJoin::Miter,
};
join_val != &StrokeJoin::Miter
});
let miter_limit = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, MiterLimitInput::INDEX, true, context), number_input);
vec![LayoutGroup::Row { widgets: distance }, line_join, LayoutGroup::Row { widgets: miter_limit }]
vec![LayoutGroup::Row { widgets: distance }, join, LayoutGroup::Row { widgets: miter_limit }]
}
pub fn math_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
use graphene_std::math_nodes::math::*;
let document_node = match get_document_node(node_id, context) {
Ok(document_node) => document_node,
Err(err) => {
@@ -1741,16 +1850,13 @@ pub fn math_properties(node_id: NodeId, context: &mut NodePropertiesContext) ->
}
};
let expression_index = 1;
let operation_b_index = 2;
let expression = (|| {
let mut widgets = start_widgets(
ParameterWidgetsInfo::from_index(document_node, node_id, expression_index, true, context),
ParameterWidgetsInfo::from_index(document_node, node_id, ExpressionInput::INDEX, true, context),
FrontendGraphDataType::General,
);
let Some(input) = document_node.inputs.get(expression_index) else {
let Some(input) = document_node.inputs.get(ExpressionInput::INDEX) else {
log::warn!("A widget failed to be built because its node's input index is invalid.");
return vec![];
};
@@ -1774,7 +1880,7 @@ pub fn math_properties(node_id: NodeId, context: &mut NodePropertiesContext) ->
})
},
node_id,
expression_index,
ExpressionInput::INDEX,
))
.on_commit(commit_value)
.widget_holder(),
@@ -1782,7 +1888,10 @@ pub fn math_properties(node_id: NodeId, context: &mut NodePropertiesContext) ->
}
widgets
})();
let operand_b = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, operation_b_index, true, context), NumberInput::default());
let operand_b = number_widget(
ParameterWidgetsInfo::from_index(document_node, node_id, OperandBInput::<f64>::INDEX, true, context),
NumberInput::default(),
);
let operand_a_hint = vec![TextLabel::new("(Operand A is the primary input)").widget_holder()];
vec![
@@ -1833,7 +1942,7 @@ pub mod choice {
use crate::messages::portfolio::document::node_graph::utility_types::FrontendGraphDataType;
use crate::messages::tool::tool_messages::tool_prelude::*;
use graph_craft::document::value::TaggedValue;
use graphene_core::registry::{ChoiceTypeStatic, ChoiceWidgetHint};
use graphene_std::registry::{ChoiceTypeStatic, ChoiceWidgetHint};
use std::marker::PhantomData;
pub trait WidgetFactory {
@@ -1890,17 +1999,16 @@ pub mod choice {
C: Fn(&()) -> Message + 'static + Send + Sync,
{
let items = E::list()
.into_iter()
.iter()
.map(|group| {
group
.into_iter()
.iter()
.map(|(item, metadata)| {
let item = item.clone();
let updater = updater_factory();
let committer = committer_factory();
MenuListEntry::new(metadata.name.as_ref())
.label(metadata.label.as_ref())
.on_update(move |_| updater(&item))
.on_update(move |_| updater(item))
.on_commit(committer)
})
.collect()
@@ -1915,14 +2023,12 @@ pub mod choice {
C: Fn(&()) -> Message + 'static + Send + Sync,
{
let items = E::list()
.into_iter()
.map(|group| group.into_iter())
.flatten()
.iter()
.flat_map(|group| group.iter())
.map(|(item, var_meta)| {
let item = item.clone();
let updater = updater_factory();
let committer = committer_factory();
let entry = RadioEntryData::new(var_meta.name.as_ref()).on_update(move |_| updater(&item)).on_commit(committer);
let entry = RadioEntryData::new(var_meta.name.as_ref()).on_update(move |_| updater(item)).on_commit(committer);
match (var_meta.icon.as_deref(), var_meta.docstring.as_deref()) {
(None, None) => entry.label(var_meta.label.as_ref()),
(None, Some(doc)) => entry.label(var_meta.label.as_ref()).tooltip(doc),
@@ -1992,7 +2098,7 @@ pub mod choice {
return LayoutGroup::Row { widgets: vec![] };
};
let input: Option<W::Value> = input.as_non_exposed_value().and_then(|v| <&W::Value as TryFrom<&TaggedValue>>::try_from(v).ok()).map(Clone::clone);
let input: Option<W::Value> = input.as_non_exposed_value().and_then(|v| <&W::Value as TryFrom<&TaggedValue>>::try_from(v).ok()).cloned();
if let Some(current) = input {
let committer = || super::commit_value;
@@ -1,547 +0,0 @@
//! This has all been copied out of node_properties.rs to avoid leaving hundreds of lines of commented out code in that file. It's left here instead for future reference.
// pub fn imaginate_sampling_method(parameter_widgets_info: ParameterWidgetsInfo) -> LayoutGroup {
// let ParameterWidgetsInfo { node_id, index, .. } = parameter_widgets_info;
// vec![
// DropdownInput::new(
// ImaginateSamplingMethod::list()
// .into_iter()
// .map(|method| {
// vec![
// MenuListEntry::new(format!("{:?}", method))
// .label(method.to_string())
// .on_update(update_value(move |_| TaggedValue::ImaginateSamplingMethod(method), node_id, index)),
// ]
// })
// .collect(),
// )
// .widget_holder(),
// ]
// .into()
// }
// pub fn imaginate_mask_starting_fill(parameter_widgets_info: ParameterWidgetsInfo) -> LayoutGroup {
// let ParameterWidgetsInfo { node_id, index, .. } = parameter_widgets_info;
// vec![
// DropdownInput::new(
// ImaginateMaskStartingFill::list()
// .into_iter()
// .map(|fill| {
// vec![
// MenuListEntry::new(format!("{:?}", fill))
// .label(fill.to_string())
// .on_update(update_value(move |_| TaggedValue::ImaginateMaskStartingFill(fill), node_id, index)),
// ]
// })
// .collect(),
// )
// .widget_holder(),
// ]
// .into()
// }
// pub(crate) fn imaginate_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
// let imaginate_node = [context.selection_network_path, &[node_id]].concat();
// let resolve_input = |name: &str| {
// IMAGINATE_NODE
// .default_node_template()
// .persistent_node_metadata
// .input_properties
// .iter()
// .position(|row| row.input_name.as_str() == name)
// .unwrap_or_else(|| panic!("Input {name} not found"))
// };
// let seed_index = resolve_input("Seed");
// let resolution_index = resolve_input("Resolution");
// let samples_index = resolve_input("Samples");
// let sampling_method_index = resolve_input("Sampling Method");
// let text_guidance_index = resolve_input("Prompt Guidance");
// let text_index = resolve_input("Prompt");
// let neg_index = resolve_input("Negative Prompt");
// let base_img_index = resolve_input("Adapt Input Image");
// let img_creativity_index = resolve_input("Image Creativity");
// // let mask_index = resolve_input("Masking Layer");
// // let inpaint_index = resolve_input("Inpaint");
// // let mask_blur_index = resolve_input("Mask Blur");
// // let mask_fill_index = resolve_input("Mask Starting Fill");
// let faces_index = resolve_input("Improve Faces");
// let tiling_index = resolve_input("Tiling");
// let document_node = match get_document_node(node_id, context) {
// Ok(document_node) => document_node,
// Err(err) => {
// log::error!("Could not get document node in imaginate_properties: {err}");
// return Vec::new();
// }
// };
// let controller = &document_node.inputs[resolve_input("Controller")];
// let server_status = {
// let server_status = context.persistent_data.imaginate.server_status();
// let status_text = server_status.to_text();
// let mut widgets = vec![
// TextLabel::new("Server").widget_holder(),
// Separator::new(SeparatorType::Unrelated).widget_holder(),
// IconButton::new("Settings", 24)
// .tooltip("Preferences: Imaginate")
// .on_update(|_| DialogMessage::RequestPreferencesDialog.into())
// .widget_holder(),
// Separator::new(SeparatorType::Unrelated).widget_holder(),
// TextLabel::new(status_text).bold(true).widget_holder(),
// Separator::new(SeparatorType::Related).widget_holder(),
// IconButton::new("Reload", 24)
// .tooltip("Refresh connection status")
// .on_update(|_| PortfolioMessage::ImaginateCheckServerStatus.into())
// .widget_holder(),
// ];
// if let ImaginateServerStatus::Unavailable | ImaginateServerStatus::Failed(_) = server_status {
// widgets.extend([
// Separator::new(SeparatorType::Unrelated).widget_holder(),
// TextButton::new("Server Help")
// .tooltip("Learn how to connect Imaginate to an image generation server")
// .on_update(|_| {
// FrontendMessage::TriggerVisitLink {
// url: "https://github.com/GraphiteEditor/Graphite/discussions/1089".to_string(),
// }
// .into()
// })
// .widget_holder(),
// ]);
// }
// LayoutGroup::Row { widgets }.with_tooltip("Connection status to the server that computes generated images")
// };
// let Some(TaggedValue::ImaginateController(controller)) = controller.as_value() else {
// panic!("Invalid output status input")
// };
// let imaginate_status = controller.get_status();
// let use_base_image = if let Some(&TaggedValue::Bool(use_base_image)) = &document_node.inputs[base_img_index].as_value() {
// use_base_image
// } else {
// true
// };
// let transform_not_connected = false;
// let progress = {
// let mut widgets = vec![TextLabel::new("Progress").widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder()];
// add_blank_assist(&mut widgets);
// let status = imaginate_status.to_text();
// widgets.push(TextLabel::new(status.as_ref()).bold(true).widget_holder());
// LayoutGroup::Row { widgets }.with_tooltip(match imaginate_status {
// ImaginateStatus::Failed(_) => status.as_ref(),
// _ => "When generating, the percentage represents how many sampling steps have so far been processed out of the target number",
// })
// };
// let image_controls = {
// let mut widgets = vec![TextLabel::new("Image").widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder()];
// match &imaginate_status {
// ImaginateStatus::Beginning | ImaginateStatus::Uploading => {
// add_blank_assist(&mut widgets);
// widgets.push(TextButton::new("Beginning...").tooltip("Sending image generation request to the server").disabled(true).widget_holder());
// }
// ImaginateStatus::Generating(_) => {
// add_blank_assist(&mut widgets);
// widgets.push(
// TextButton::new("Terminate")
// .tooltip("Cancel the in-progress image generation and keep the latest progress")
// .on_update({
// let controller = controller.clone();
// move |_| {
// controller.request_termination();
// Message::NoOp
// }
// })
// .widget_holder(),
// );
// }
// ImaginateStatus::Terminating => {
// add_blank_assist(&mut widgets);
// widgets.push(
// TextButton::new("Terminating...")
// .tooltip("Waiting on the final image generated after termination")
// .disabled(true)
// .widget_holder(),
// );
// }
// ImaginateStatus::Ready | ImaginateStatus::ReadyDone | ImaginateStatus::Terminated | ImaginateStatus::Failed(_) => widgets.extend_from_slice(&[
// IconButton::new("Random", 24)
// .tooltip("Generate with a new random seed")
// .on_update({
// let imaginate_node = imaginate_node.clone();
// let controller = controller.clone();
// move |_| {
// controller.trigger_regenerate();
// DocumentMessage::ImaginateRandom {
// imaginate_node: imaginate_node.clone(),
// then_generate: true,
// }
// .into()
// }
// })
// .widget_holder(),
// Separator::new(SeparatorType::Unrelated).widget_holder(),
// TextButton::new("Generate")
// .tooltip("Fill layer frame by generating a new image")
// .on_update({
// let controller = controller.clone();
// let imaginate_node = imaginate_node.clone();
// move |_| {
// controller.trigger_regenerate();
// DocumentMessage::ImaginateGenerate {
// imaginate_node: imaginate_node.clone(),
// }
// .into()
// }
// })
// .widget_holder(),
// Separator::new(SeparatorType::Related).widget_holder(),
// TextButton::new("Clear")
// .tooltip("Remove generated image from the layer frame")
// .disabled(!matches!(imaginate_status, ImaginateStatus::ReadyDone))
// .on_update({
// let controller = controller.clone();
// let imaginate_node = imaginate_node.clone();
// move |_| {
// controller.set_status(ImaginateStatus::Ready);
// DocumentMessage::ImaginateGenerate {
// imaginate_node: imaginate_node.clone(),
// }
// .into()
// }
// })
// .widget_holder(),
// ]),
// }
// LayoutGroup::Row { widgets }.with_tooltip("Buttons that control the image generation process")
// };
// // Requires custom layout for the regenerate button
// let seed = {
// let mut widgets = start_widgets(document_node, node_id, seed_index, "Seed", FrontendGraphDataType::Number, false);
// let Some(input) = document_node.inputs.get(seed_index) else {
// log::warn!("A widget failed to be built because its node's input index is invalid.");
// return vec![];
// };
// if let Some(&TaggedValue::F64(seed)) = &input.as_non_exposed_value() {
// widgets.extend_from_slice(&[
// Separator::new(SeparatorType::Unrelated).widget_holder(),
// IconButton::new("Resync", 24)
// .tooltip("Set a new random seed")
// .on_update({
// let imaginate_node = imaginate_node.clone();
// move |_| {
// DocumentMessage::ImaginateRandom {
// imaginate_node: imaginate_node.clone(),
// then_generate: false,
// }
// .into()
// }
// })
// .widget_holder(),
// Separator::new(SeparatorType::Unrelated).widget_holder(),
// NumberInput::new(Some(seed))
// .int()
// .min(-((1_u64 << f64::MANTISSA_DIGITS) as f64))
// .max((1_u64 << f64::MANTISSA_DIGITS) as f64)
// .on_update(update_value(move |input: &NumberInput| TaggedValue::F64(input.value.unwrap()), node_id, seed_index))
// .on_commit(commit_value)
// .mode(NumberInputMode::Increment)
// .widget_holder(),
// ])
// }
// // Note: Limited by f64. You cannot even have all the possible u64 values :)
// LayoutGroup::Row { widgets }.with_tooltip("Seed determines the random outcome, enabling limitless unique variations")
// };
// // let transform = context
// // .executor
// // .introspect_node_in_network(context.network, &imaginate_node, |network| network.inputs.first().copied(), |frame: &ImageFrame<Color>| frame.transform)
// // .unwrap_or_default();
// let image_size = context
// .executor
// .introspect_node_in_network(
// context.network_interface.document_network().unwrap(),
// &imaginate_node,
// |network| {
// network
// .nodes
// .iter()
// .find(|node| {
// node.1
// .inputs
// .iter()
// .any(|node_input| if let NodeInput::Network { import_index, .. } = node_input { *import_index == 0 } else { false })
// })
// .map(|(node_id, _)| node_id)
// .copied()
// },
// |frame: &IORecord<(), ImageFrame<Color>>| (frame.output.image.width, frame.output.image.height),
// )
// .unwrap_or_default();
// let document_node = match get_document_node(node_id, context) {
// Ok(document_node) => document_node,
// Err(err) => {
// log::error!("Could not get document node in imaginate_properties: {err}");
// return Vec::new();
// }
// };
// let resolution = {
// let mut widgets = start_widgets(document_node, node_id, resolution_index, "Resolution", FrontendGraphDataType::Number, false);
// let round = |size: DVec2| {
// let (x, y) = graphene_std::imaginate::pick_safe_imaginate_resolution(size.into());
// DVec2::new(x as f64, y as f64)
// };
// let Some(input) = document_node.inputs.get(resolution_index) else {
// log::warn!("A widget failed to be built because its node's input index is invalid.");
// return vec![];
// };
// if let Some(&TaggedValue::OptionalDVec2(vec2)) = &input.as_non_exposed_value() {
// let dimensions_is_auto = vec2.is_none();
// let vec2 = vec2.unwrap_or_else(|| round((image_size.0 as f64, image_size.1 as f64).into()));
// widgets.extend_from_slice(&[
// Separator::new(SeparatorType::Unrelated).widget_holder(),
// IconButton::new("FrameAll", 24)
// .tooltip("Set the layer dimensions to this resolution")
// .on_update(move |_| DialogMessage::RequestComingSoonDialog { issue: None }.into())
// .widget_holder(),
// Separator::new(SeparatorType::Unrelated).widget_holder(),
// CheckboxInput::new(!dimensions_is_auto || transform_not_connected)
// .icon("Edit12px")
// .tooltip({
// let message = "Set a custom resolution instead of using the input's dimensions (rounded to the nearest 64)";
// let manual_message = "Set a custom resolution instead of using the input's dimensions (rounded to the nearest 64).\n\
// \n\
// (Resolution must be set manually while the 'Transform' input is disconnected.)";
// if transform_not_connected {
// manual_message
// } else {
// message
// }
// })
// .disabled(transform_not_connected)
// .on_update(update_value(
// move |checkbox_input: &CheckboxInput| TaggedValue::OptionalDVec2(if checkbox_input.checked { Some(vec2) } else { None }),
// node_id,
// resolution_index,
// ))
// .on_commit(commit_value)
// .for_label(checkbox_id.clone())
// .widget_holder(),
// Separator::new(SeparatorType::Related).widget_holder(),
// NumberInput::new(Some(vec2.x))
// .label("W")
// .min(64.)
// .step(64.)
// .unit(" px")
// .disabled(dimensions_is_auto && !transform_not_connected)
// .on_update(update_value(
// move |number_input: &NumberInput| TaggedValue::OptionalDVec2(Some(round(DVec2::new(number_input.value.unwrap(), vec2.y)))),
// node_id,
// resolution_index,
// ))
// .on_commit(commit_value)
// .widget_holder(),
// Separator::new(SeparatorType::Related).widget_holder(),
// NumberInput::new(Some(vec2.y))
// .label("H")
// .min(64.)
// .step(64.)
// .unit(" px")
// .disabled(dimensions_is_auto && !transform_not_connected)
// .on_update(update_value(
// move |number_input: &NumberInput| TaggedValue::OptionalDVec2(Some(round(DVec2::new(vec2.x, number_input.value.unwrap())))),
// node_id,
// resolution_index,
// ))
// .on_commit(commit_value)
// .widget_holder(),
// ])
// }
// LayoutGroup::Row { widgets }.with_tooltip(
// "Width and height of the image that will be generated. Larger resolutions take longer to compute.\n\
// \n\
// 512x512 yields optimal results because the AI is trained to understand that scale best. Larger sizes may tend to integrate the prompt's subject more than once. Small sizes are often incoherent.\n\
// \n\
// Dimensions must be a multiple of 64, so these are set by rounding the layer dimensions. A resolution exceeding 1 megapixel is reduced below that limit because larger sizes may exceed available GPU memory on the server.")
// };
// let sampling_steps = {
// let widgets = number_widget(document_node, node_id, samples_index, "Sampling Steps", NumberInput::default().min(0.).max(150.).int(), true);
// LayoutGroup::Row { widgets }.with_tooltip("Number of iterations to improve the image generation quality, with diminishing returns around 40 when using the Euler A sampling method")
// };
// let sampling_method = {
// let mut widgets = start_widgets(document_node, node_id, sampling_method_index, "Sampling Method", FrontendGraphDataType::General, true);
// let Some(input) = document_node.inputs.get(sampling_method_index) else {
// log::warn!("A widget failed to be built because its node's input index is invalid.");
// return vec![];
// };
// if let Some(&TaggedValue::ImaginateSamplingMethod(sampling_method)) = &input.as_non_exposed_value() {
// let sampling_methods = ImaginateSamplingMethod::list();
// let mut entries = Vec::with_capacity(sampling_methods.len());
// for method in sampling_methods {
// entries.push(
// MenuListEntry::new(format!("{method:?}"))
// .label(method.to_string())
// .on_update(update_value(move |_| TaggedValue::ImaginateSamplingMethod(method), node_id, sampling_method_index))
// .on_commit(commit_value),
// );
// }
// let entries = vec![entries];
// widgets.extend_from_slice(&[
// Separator::new(SeparatorType::Unrelated).widget_holder(),
// DropdownInput::new(entries).selected_index(Some(sampling_method as u32)).widget_holder(),
// ]);
// }
// LayoutGroup::Row { widgets }.with_tooltip("Algorithm used to generate the image during each sampling step")
// };
// let text_guidance = {
// let widgets = number_widget(document_node, node_id, text_guidance_index, "Prompt Guidance", NumberInput::default().min(0.).max(30.), true);
// LayoutGroup::Row { widgets }.with_tooltip(
// "Amplification of the text prompt's influence over the outcome. At 0, the prompt is entirely ignored.\n\
// \n\
// Lower values are more creative and exploratory. Higher values are more literal and uninspired.\n\
// \n\
// This parameter is otherwise known as CFG (classifier-free guidance).",
// )
// };
// let text_prompt = {
// let widgets = text_area_widget(document_node, node_id, text_index, "Prompt", true);
// LayoutGroup::Row { widgets }.with_tooltip(
// "Description of the desired image subject and style.\n\
// \n\
// Include an artist name like \"Rembrandt\" or art medium like \"watercolor\" or \"photography\" to influence the look. List multiple to meld styles.\n\
// \n\
// To boost (or lessen) the importance of a word or phrase, wrap it in parentheses ending with a colon and a multiplier, for example:\n\
// \"Colorless green ideas (sleep:1.3) furiously\"",
// )
// };
// let negative_prompt = {
// let widgets = text_area_widget(document_node, node_id, neg_index, "Negative Prompt", true);
// LayoutGroup::Row { widgets }.with_tooltip("A negative text prompt can be used to list things like objects or colors to avoid")
// };
// let base_image = {
// let widgets = bool_widget(document_node, node_id, base_img_index, "Adapt Input Image", CheckboxInput::default().for_label(checkbox_id.clone()), true);
// LayoutGroup::Row { widgets }.with_tooltip("Generate an image based upon the bitmap data plugged into this node")
// };
// let image_creativity = {
// let props = NumberInput::default().percentage().disabled(!use_base_image);
// let widgets = number_widget(document_node, node_id, img_creativity_index, "Image Creativity", props, true);
// LayoutGroup::Row { widgets }.with_tooltip(
// "Strength of the artistic liberties allowing changes from the input image. The image is unchanged at 0% and completely different at 100%.\n\
// \n\
// This parameter is otherwise known as denoising strength.",
// )
// };
// let mut layout = vec![
// server_status,
// progress,
// image_controls,
// seed,
// resolution,
// sampling_steps,
// sampling_method,
// text_guidance,
// text_prompt,
// negative_prompt,
// base_image,
// image_creativity,
// // layer_mask,
// ];
// // if use_base_image && layer_reference_input_layer_is_some {
// // let in_paint = {
// // let mut widgets = start_widgets(document_node, node_id, inpaint_index, "Inpaint", FrontendGraphDataType::Boolean, true);
// // if let Some(& TaggedValue::Bool(in_paint)
// //)/ } = &document_node.inputs[inpaint_index].as_non_exposed_value()
// // {
// // widgets.extend_from_slice(&[
// // Separator::new(SeparatorType::Unrelated).widget_holder(),
// // RadioInput::new(
// // [(true, "Inpaint"), (false, "Outpaint")]
// // .into_iter()
// // .map(|(paint, name)| RadioEntryData::new(name).label(name).on_update(update_value(move |_| TaggedValue::Bool(paint), node_id, inpaint_index)))
// // .collect(),
// // )
// // .selected_index(Some(1 - in_paint as u32))
// // .widget_holder(),
// // ]);
// // }
// // LayoutGroup::Row { widgets }.with_tooltip(
// // "Constrain image generation to the interior (inpaint) or exterior (outpaint) of the mask, while referencing the other unchanged parts as context imagery.\n\
// // \n\
// // An unwanted part of an image can be replaced by drawing around it with a black shape and inpainting with that mask layer.\n\
// // \n\
// // An image can be uncropped by resizing the Imaginate layer to the target bounds and outpainting with a black rectangle mask matching the original image bounds.",
// // )
// // };
// // let blur_radius = {
// // let number_props = NumberInput::default().unit(" px").min(0.).max(25.).int();
// // let widgets = number_widget(document_node, node_id, mask_blur_index, "Mask Blur", number_props, true);
// // LayoutGroup::Row { widgets }.with_tooltip("Blur radius for the mask. Useful for softening sharp edges to blend the masked area with the rest of the image.")
// // };
// // let mask_starting_fill = {
// // let mut widgets = start_widgets(document_node, node_id, mask_fill_index, "Mask Starting Fill", FrontendGraphDataType::General, true);
// // if let Some(& TaggedValue::ImaginateMaskStartingFill(starting_fill)
// //)/ } = &document_node.inputs[mask_fill_index].as_non_exposed_value()
// // {
// // let mask_fill_content_modes = ImaginateMaskStartingFill::list();
// // let mut entries = Vec::with_capacity(mask_fill_content_modes.len());
// // for mode in mask_fill_content_modes {
// // entries.push(MenuListEntry::new(format!("{mode:?}")).label(mode.to_string()).on_update(update_value(move |_| TaggedValue::ImaginateMaskStartingFill(mode), node_id, mask_fill_index)));
// // }
// // let entries = vec![entries];
// // widgets.extend_from_slice(&[
// // Separator::new(SeparatorType::Unrelated).widget_holder(),
// // DropdownInput::new(entries).selected_index(Some(starting_fill as u32)).widget_holder(),
// // ]);
// // }
// // LayoutGroup::Row { widgets }.with_tooltip(
// // "Begin in/outpainting the masked areas using this fill content as the starting input image.\n\
// // \n\
// // Each option can be visualized by generating with 'Sampling Steps' set to 0.",
// // )
// // };
// // layout.extend_from_slice(&[in_paint, blur_radius, mask_starting_fill]);
// // }
// let improve_faces = {
// let widgets = bool_widget(document_node, node_id, faces_index, "Improve Faces", CheckboxInput::default().for_label(checkbox_id.clone()), true);
// LayoutGroup::Row { widgets }.with_tooltip(
// "Postprocess human (or human-like) faces to look subtly less distorted.\n\
// \n\
// This filter can be used on its own by enabling 'Adapt Input Image' and setting 'Sampling Steps' to 0.",
// )
// };
// let tiling = {
// let widgets = bool_widget(document_node, node_id, tiling_index, "Tiling", CheckboxInput::default().for_label(checkbox_id.clone()), true);
// LayoutGroup::Row { widgets }.with_tooltip("Generate the image so its edges loop seamlessly to make repeatable patterns or textures")
// };
// layout.extend_from_slice(&[improve_faces, tiling]);
// layout
// }
@@ -1,7 +1,7 @@
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, OutputConnector, TypeSource};
use graph_craft::document::NodeId;
use graph_craft::document::value::TaggedValue;
use graphene_core::Type;
use graphene_std::Type;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum FrontendGraphDataType {
@@ -17,7 +17,7 @@ pub enum FrontendGraphDataType {
impl FrontendGraphDataType {
fn with_type(input: &Type) -> Self {
match TaggedValue::from_type_or_none(input) {
TaggedValue::Image(_) | TaggedValue::ImageFrame(_) => Self::Raster,
TaggedValue::Image(_) | TaggedValue::RasterData(_) => Self::Raster,
TaggedValue::Subpaths(_) | TaggedValue::VectorData(_) => Self::VectorData,
TaggedValue::U32(_)
| TaggedValue::U64(_)
@@ -3,8 +3,8 @@ use crate::messages::portfolio::document::overlays::utility_types::OverlayContex
use crate::messages::portfolio::document::utility_types::misc::{GridSnapping, GridType};
use crate::messages::prelude::*;
use glam::DVec2;
use graphene_core::raster::color::Color;
use graphene_core::renderer::Quad;
use graphene_std::raster::color::Color;
use graphene_std::renderer::Quad;
use graphene_std::vector::style::FillChoice;
fn grid_overlay_rectangular(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, spacing: DVec2) {
@@ -236,12 +236,24 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
TextLabel::new("Type").table_align(true).widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
RadioInput::new(vec![
RadioEntryData::new("rectangular")
.label("Rectangular")
.on_update(update_val(grid, |grid, _| grid.grid_type = GridType::RECTANGULAR)),
RadioEntryData::new("isometric")
.label("Isometric")
.on_update(update_val(grid, |grid, _| grid.grid_type = GridType::ISOMETRIC)),
RadioEntryData::new("rectangular").label("Rectangular").on_update(update_val(grid, |grid, _| {
if let GridType::Isometric { y_axis_spacing, angle_a, angle_b } = grid.grid_type {
grid.isometric_y_spacing = y_axis_spacing;
grid.isometric_angle_a = angle_a;
grid.isometric_angle_b = angle_b;
}
grid.grid_type = GridType::Rectangular { spacing: grid.rectangular_spacing };
})),
RadioEntryData::new("isometric").label("Isometric").on_update(update_val(grid, |grid, _| {
if let GridType::Rectangular { spacing } = grid.grid_type {
grid.rectangular_spacing = spacing;
}
grid.grid_type = GridType::Isometric {
y_axis_spacing: grid.isometric_y_spacing,
angle_a: grid.isometric_angle_a,
angle_b: grid.isometric_angle_b,
};
})),
])
.min_width(200)
.selected_index(Some(match grid.grid_type {
@@ -2,10 +2,19 @@ use super::utility_types::{OverlayProvider, empty_provider};
use crate::messages::prelude::*;
#[impl_message(Message, DocumentMessage, Overlays)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
#[derive(derivative::Derivative, Clone, serde::Serialize, serde::Deserialize)]
#[derivative(Debug, PartialEq)]
pub enum OverlaysMessage {
Draw,
// Serde functionality isn't used but is required by the message system macros
AddProvider(#[serde(skip, default = "empty_provider")] OverlayProvider),
RemoveProvider(#[serde(skip, default = "empty_provider")] OverlayProvider),
AddProvider(
#[serde(skip, default = "empty_provider")]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
OverlayProvider,
),
RemoveProvider(
#[serde(skip, default = "empty_provider")]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
OverlayProvider,
),
}
@@ -5,7 +5,7 @@ use crate::messages::tool::common_functionality::shape_editor::{SelectedLayerSta
use crate::messages::tool::tool_messages::tool_prelude::{DocumentMessageHandler, PreferencesMessageHandler};
use bezier_rs::{Bezier, BezierHandles};
use glam::{DAffine2, DVec2};
use graphene_core::vector::ManipulatorPointId;
use graphene_std::vector::ManipulatorPointId;
use graphene_std::vector::{PointId, SegmentId};
use wasm_bindgen::JsCast;
@@ -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,15 +1,16 @@
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};
use core::borrow::Borrow;
use core::f64::consts::{FRAC_PI_2, TAU};
use glam::{DAffine2, DVec2};
use graphene_core::Color;
use graphene_core::renderer::Quad;
use graphene_std::Color;
use graphene_std::math::quad::Quad;
use graphene_std::vector::click_target::ClickTargetType;
use graphene_std::vector::{PointId, SegmentId, VectorData};
use std::collections::HashMap;
use wasm_bindgen::{JsCast, JsValue};
@@ -580,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();
@@ -647,13 +677,24 @@ impl OverlayContext {
self.end_dpi_aware_transform();
}
/// Used by the Select tool to outline a path selected or hovered.
pub fn outline(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: Option<&str>) {
self.push_path(subpaths, transform);
/// Used by the Select tool to outline a path or a free point when selected or hovered.
pub fn outline(&mut self, target_types: impl Iterator<Item = impl Borrow<ClickTargetType>>, transform: DAffine2, color: Option<&str>) {
let mut subpaths: Vec<bezier_rs::Subpath<PointId>> = vec![];
let color = color.unwrap_or(COLOR_OVERLAY_BLUE);
self.render_context.set_stroke_style_str(color);
self.render_context.stroke();
target_types.for_each(|target_type| match target_type.borrow() {
ClickTargetType::FreePoint(point) => {
self.manipulator_anchor(transform.transform_point2(point.position), false, None);
}
ClickTargetType::Subpath(subpath) => subpaths.push(subpath.clone()),
});
if !subpaths.is_empty() {
self.push_path(subpaths.iter(), transform);
let color = color.unwrap_or(COLOR_OVERLAY_BLUE);
self.render_context.set_stroke_style_str(color);
self.render_context.stroke();
}
}
/// Fills the area inside the path. Assumes `color` is in gamma space.
@@ -3,9 +3,9 @@ use crate::messages::portfolio::document::graph_operation::transform_utils;
use crate::messages::portfolio::document::graph_operation::utility_types::ModifyInputsContext;
use glam::{DAffine2, DVec2};
use graph_craft::document::NodeId;
use graphene_core::renderer::ClickTarget;
use graphene_core::renderer::Quad;
use graphene_core::transform::Footprint;
use graphene_std::math::quad::Quad;
use graphene_std::transform::Footprint;
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
use graphene_std::vector::{PointId, VectorData};
use std::collections::{HashMap, HashSet};
use std::num::NonZeroU64;
@@ -134,7 +134,10 @@ impl DocumentMetadata {
pub fn bounding_box_with_transform(&self, layer: LayerNodeIdentifier, transform: DAffine2) -> Option<[DVec2; 2]> {
self.click_targets(layer)?
.iter()
.filter_map(|click_target| click_target.subpath().bounding_box_with_transform(transform))
.filter_map(|click_target| match click_target.target_type() {
ClickTargetType::Subpath(subpath) => subpath.bounding_box_with_transform(transform),
ClickTargetType::FreePoint(_) => click_target.bounding_box_with_transform(transform),
})
.reduce(Quad::combine_bounds)
}
@@ -177,7 +180,16 @@ impl DocumentMetadata {
pub fn layer_outline(&self, layer: LayerNodeIdentifier) -> impl Iterator<Item = &bezier_rs::Subpath<PointId>> {
static EMPTY: Vec<ClickTarget> = Vec::new();
let click_targets = self.click_targets.get(&layer).unwrap_or(&EMPTY);
click_targets.iter().map(ClickTarget::subpath)
click_targets.iter().filter_map(|target| match target.target_type() {
ClickTargetType::Subpath(subpath) => Some(subpath),
_ => None,
})
}
pub fn layer_with_free_points_outline(&self, layer: LayerNodeIdentifier) -> impl Iterator<Item = &ClickTargetType> {
static EMPTY: Vec<ClickTarget> = Vec::new();
let click_targets = self.click_targets.get(&layer).unwrap_or(&EMPTY);
click_targets.iter().map(|target| target.target_type())
}
pub fn is_clip(&self, node: NodeId) -> bool {
@@ -276,8 +288,14 @@ impl LayerNodeIdentifier {
child.ancestors(metadata).any(|ancestor| ancestor == self)
}
/// Is the layer last child of parent group? Used for clipping
pub fn can_be_clipped(self, metadata: &DocumentMetadata) -> bool {
self.parent(metadata)
.map_or(false, |layer| layer.last_child(metadata).expect("Parent accessed via child should have children") != self)
}
/// Iterator over all direct children (excluding self and recursive children)
pub fn children(self, metadata: &DocumentMetadata) -> AxisIter {
pub fn children(self, metadata: &DocumentMetadata) -> AxisIter<'_> {
AxisIter {
layer_node: self.first_child(metadata),
next_node: Self::next_sibling,
@@ -285,7 +303,7 @@ impl LayerNodeIdentifier {
}
}
pub fn downstream_siblings(self, metadata: &DocumentMetadata) -> AxisIter {
pub fn downstream_siblings(self, metadata: &DocumentMetadata) -> AxisIter<'_> {
AxisIter {
layer_node: Some(self),
next_node: Self::previous_sibling,
@@ -294,7 +312,7 @@ impl LayerNodeIdentifier {
}
/// All ancestors of this layer, including self, going to the document root
pub fn ancestors(self, metadata: &DocumentMetadata) -> AxisIter {
pub fn ancestors(self, metadata: &DocumentMetadata) -> AxisIter<'_> {
AxisIter {
layer_node: Some(self),
next_node: Self::parent,
@@ -303,7 +321,7 @@ impl LayerNodeIdentifier {
}
/// Iterator through all the last children, starting from self
pub fn last_children(self, metadata: &DocumentMetadata) -> AxisIter {
pub fn last_children(self, metadata: &DocumentMetadata) -> AxisIter<'_> {
AxisIter {
layer_node: Some(self),
next_node: Self::last_child,
@@ -312,7 +330,7 @@ impl LayerNodeIdentifier {
}
/// Iterator through all descendants, including recursive children (not including self)
pub fn descendants(self, metadata: &DocumentMetadata) -> DescendantsIter {
pub fn descendants(self, metadata: &DocumentMetadata) -> DescendantsIter<'_> {
DescendantsIter {
front: self.first_child(metadata),
back: self.last_child(metadata).and_then(|child| child.last_children(metadata).last()),
@@ -505,49 +523,53 @@ pub struct NodeRelations {
// Helper functions
// ================
#[test]
fn test_tree() {
let mut metadata = DocumentMetadata::default();
let root = LayerNodeIdentifier::ROOT_PARENT;
let metadata = &mut metadata;
root.push_child(metadata, LayerNodeIdentifier::new_unchecked(NodeId(3)));
assert_eq!(root.children(metadata).collect::<Vec<_>>(), vec![LayerNodeIdentifier::new_unchecked(NodeId(3))]);
root.push_child(metadata, LayerNodeIdentifier::new_unchecked(NodeId(6)));
assert_eq!(root.children(metadata).map(LayerNodeIdentifier::to_node).collect::<Vec<_>>(), vec![NodeId(3), NodeId(6)]);
assert_eq!(root.descendants(metadata).map(LayerNodeIdentifier::to_node).collect::<Vec<_>>(), vec![NodeId(3), NodeId(6)]);
LayerNodeIdentifier::new_unchecked(NodeId(3)).add_after(metadata, LayerNodeIdentifier::new_unchecked(NodeId(4)));
LayerNodeIdentifier::new_unchecked(NodeId(3)).add_before(metadata, LayerNodeIdentifier::new_unchecked(NodeId(2)));
LayerNodeIdentifier::new_unchecked(NodeId(6)).add_before(metadata, LayerNodeIdentifier::new_unchecked(NodeId(5)));
LayerNodeIdentifier::new_unchecked(NodeId(6)).add_after(metadata, LayerNodeIdentifier::new_unchecked(NodeId(9)));
LayerNodeIdentifier::new_unchecked(NodeId(6)).push_child(metadata, LayerNodeIdentifier::new_unchecked(NodeId(8)));
LayerNodeIdentifier::new_unchecked(NodeId(6)).push_front_child(metadata, LayerNodeIdentifier::new_unchecked(NodeId(7)));
root.push_front_child(metadata, LayerNodeIdentifier::new_unchecked(NodeId(1)));
assert_eq!(
root.children(metadata).map(LayerNodeIdentifier::to_node).collect::<Vec<_>>(),
vec![NodeId(1), NodeId(2), NodeId(3), NodeId(4), NodeId(5), NodeId(6), NodeId(9)]
);
assert_eq!(
root.descendants(metadata).map(LayerNodeIdentifier::to_node).collect::<Vec<_>>(),
vec![NodeId(1), NodeId(2), NodeId(3), NodeId(4), NodeId(5), NodeId(6), NodeId(7), NodeId(8), NodeId(9)]
);
assert_eq!(
root.descendants(metadata).map(LayerNodeIdentifier::to_node).rev().collect::<Vec<_>>(),
vec![NodeId(9), NodeId(8), NodeId(7), NodeId(6), NodeId(5), NodeId(4), NodeId(3), NodeId(2), NodeId(1)]
);
assert!(root.children(metadata).all(|child| child.parent(metadata) == Some(root)));
LayerNodeIdentifier::new_unchecked(NodeId(6)).delete(metadata);
LayerNodeIdentifier::new_unchecked(NodeId(1)).delete(metadata);
LayerNodeIdentifier::new_unchecked(NodeId(9)).push_child(metadata, LayerNodeIdentifier::new_unchecked(NodeId(10)));
assert_eq!(
root.children(metadata).map(LayerNodeIdentifier::to_node).collect::<Vec<_>>(),
vec![NodeId(2), NodeId(3), NodeId(4), NodeId(5), NodeId(9)]
);
assert_eq!(
root.descendants(metadata).map(LayerNodeIdentifier::to_node).collect::<Vec<_>>(),
vec![NodeId(2), NodeId(3), NodeId(4), NodeId(5), NodeId(9), NodeId(10)]
);
assert_eq!(
root.descendants(metadata).map(LayerNodeIdentifier::to_node).rev().collect::<Vec<_>>(),
vec![NodeId(10), NodeId(9), NodeId(5), NodeId(4), NodeId(3), NodeId(2)]
);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tree() {
let mut metadata = DocumentMetadata::default();
let root = LayerNodeIdentifier::ROOT_PARENT;
let metadata = &mut metadata;
root.push_child(metadata, LayerNodeIdentifier::new_unchecked(NodeId(3)));
assert_eq!(root.children(metadata).collect::<Vec<_>>(), vec![LayerNodeIdentifier::new_unchecked(NodeId(3))]);
root.push_child(metadata, LayerNodeIdentifier::new_unchecked(NodeId(6)));
assert_eq!(root.children(metadata).map(LayerNodeIdentifier::to_node).collect::<Vec<_>>(), vec![NodeId(3), NodeId(6)]);
assert_eq!(root.descendants(metadata).map(LayerNodeIdentifier::to_node).collect::<Vec<_>>(), vec![NodeId(3), NodeId(6)]);
LayerNodeIdentifier::new_unchecked(NodeId(3)).add_after(metadata, LayerNodeIdentifier::new_unchecked(NodeId(4)));
LayerNodeIdentifier::new_unchecked(NodeId(3)).add_before(metadata, LayerNodeIdentifier::new_unchecked(NodeId(2)));
LayerNodeIdentifier::new_unchecked(NodeId(6)).add_before(metadata, LayerNodeIdentifier::new_unchecked(NodeId(5)));
LayerNodeIdentifier::new_unchecked(NodeId(6)).add_after(metadata, LayerNodeIdentifier::new_unchecked(NodeId(9)));
LayerNodeIdentifier::new_unchecked(NodeId(6)).push_child(metadata, LayerNodeIdentifier::new_unchecked(NodeId(8)));
LayerNodeIdentifier::new_unchecked(NodeId(6)).push_front_child(metadata, LayerNodeIdentifier::new_unchecked(NodeId(7)));
root.push_front_child(metadata, LayerNodeIdentifier::new_unchecked(NodeId(1)));
assert_eq!(
root.children(metadata).map(LayerNodeIdentifier::to_node).collect::<Vec<_>>(),
vec![NodeId(1), NodeId(2), NodeId(3), NodeId(4), NodeId(5), NodeId(6), NodeId(9)]
);
assert_eq!(
root.descendants(metadata).map(LayerNodeIdentifier::to_node).collect::<Vec<_>>(),
vec![NodeId(1), NodeId(2), NodeId(3), NodeId(4), NodeId(5), NodeId(6), NodeId(7), NodeId(8), NodeId(9)]
);
assert_eq!(
root.descendants(metadata).map(LayerNodeIdentifier::to_node).rev().collect::<Vec<_>>(),
vec![NodeId(9), NodeId(8), NodeId(7), NodeId(6), NodeId(5), NodeId(4), NodeId(3), NodeId(2), NodeId(1)]
);
assert!(root.children(metadata).all(|child| child.parent(metadata) == Some(root)));
LayerNodeIdentifier::new_unchecked(NodeId(6)).delete(metadata);
LayerNodeIdentifier::new_unchecked(NodeId(1)).delete(metadata);
LayerNodeIdentifier::new_unchecked(NodeId(9)).push_child(metadata, LayerNodeIdentifier::new_unchecked(NodeId(10)));
assert_eq!(
root.children(metadata).map(LayerNodeIdentifier::to_node).collect::<Vec<_>>(),
vec![NodeId(2), NodeId(3), NodeId(4), NodeId(5), NodeId(9)]
);
assert_eq!(
root.descendants(metadata).map(LayerNodeIdentifier::to_node).collect::<Vec<_>>(),
vec![NodeId(2), NodeId(3), NodeId(4), NodeId(5), NodeId(9), NodeId(10)]
);
assert_eq!(
root.descendants(metadata).map(LayerNodeIdentifier::to_node).rev().collect::<Vec<_>>(),
vec![NodeId(10), NodeId(9), NodeId(5), NodeId(4), NodeId(3), NodeId(2)]
);
}
}
@@ -1,4 +1,4 @@
use graphene_core::raster::color::Color;
use graphene_std::raster::color::Color;
use thiserror::Error;
/// The error type used by the Graphite editor.
@@ -1,6 +1,6 @@
use crate::consts::COLOR_OVERLAY_GRAY;
use glam::DVec2;
use graphene_core::raster::Color;
use graphene_std::raster::Color;
use std::fmt;
#[repr(transparent)]
@@ -176,17 +176,11 @@ pub enum GridType {
impl Default for GridType {
fn default() -> Self {
Self::RECTANGULAR
Self::Rectangular { spacing: DVec2::ONE }
}
}
impl GridType {
pub const RECTANGULAR: Self = GridType::Rectangular { spacing: DVec2::ONE };
pub const ISOMETRIC: Self = GridType::Isometric {
y_axis_spacing: 1.,
angle_a: 30.,
angle_b: 30.,
};
pub fn rectangular_spacing(&mut self) -> Option<&mut DVec2> {
match self {
Self::Rectangular { spacing } => Some(spacing),
@@ -218,6 +212,10 @@ impl GridType {
pub struct GridSnapping {
pub origin: DVec2,
pub grid_type: GridType,
pub rectangular_spacing: DVec2,
pub isometric_y_spacing: f64,
pub isometric_angle_a: f64,
pub isometric_angle_b: f64,
pub grid_color: Color,
pub dot_display: bool,
}
@@ -227,6 +225,10 @@ impl Default for GridSnapping {
Self {
origin: DVec2::ZERO,
grid_type: Default::default(),
rectangular_spacing: DVec2::ONE,
isometric_y_spacing: 1.,
isometric_angle_a: 30.,
isometric_angle_b: 30.,
grid_color: Color::from_rgb_str(COLOR_OVERLAY_GRAY.strip_prefix('#').unwrap()).unwrap(),
dot_display: false,
}
@@ -691,5 +693,5 @@ impl PTZ {
#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum GroupFolderType {
Layer,
BooleanOperation(graphene_std::vector::misc::BooleanOperation),
BooleanOperation(graphene_std::path_bool::BooleanOperation),
}
@@ -12,8 +12,9 @@ use glam::{DAffine2, DVec2, IVec2};
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork, OldDocumentNodeImplementation, OldNodeNetwork};
use graph_craft::{Type, concrete};
use graphene_std::renderer::{ClickTarget, Quad};
use graphene_std::math::quad::Quad;
use graphene_std::transform::Footprint;
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
use graphene_std::vector::{PointId, VectorData, VectorModificationType};
use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypes;
use interpreted_executor::node_registry::NODE_REGISTRY;
@@ -485,7 +486,7 @@ impl NodeNetworkInterface {
InputConnector::Export(export_index) => {
let Some((encapsulating_node_id, encapsulating_node_id_path)) = network_path.split_last() else {
// The outermost network export defaults to an ArtboardGroupTable.
return Some((concrete!(graphene_core::ArtboardGroupTable), TypeSource::OuterMostExportDefault));
return Some((concrete!(graphene_std::ArtboardGroupTable), TypeSource::OuterMostExportDefault));
};
let output_type = self.output_types(encapsulating_node_id, encapsulating_node_id_path).into_iter().nth(export_index).flatten();
@@ -1160,6 +1161,13 @@ 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();
let _ = self
.node_metadata_mut(node_id, network_path)
.map(|node_metadata| node_metadata.persistent_metadata.input_properties.insert(index - 1, row));
}
pub fn input_metadata(&self, node_id: &NodeId, index: usize, field: &str, network_path: &[NodeId]) -> Option<&Value> {
let Some(input_row) = self.input_properties_row(node_id, index, network_path) else {
log::error!("Could not get input_row in get_input_metadata");
@@ -2120,7 +2128,7 @@ impl NodeNetworkInterface {
let bounding_box_top_right = DVec2::new((all_nodes_bounding_box[1].x / 24. + 0.5).floor() * 24., (all_nodes_bounding_box[0].y / 24. + 0.5).floor() * 24.) + offset_from_top_right;
let export_top_right: DVec2 = DVec2::new(viewport_top_right.x.max(bounding_box_top_right.x), viewport_top_right.y.min(bounding_box_top_right.y));
let add_export_center = export_top_right + DVec2::new(0., network.exports.len() as f64 * 24.);
let add_export = ClickTarget::new(
let add_export = ClickTarget::new_with_subpath(
Subpath::new_rounded_rect(add_export_center - DVec2::new(12., 12.), add_export_center + DVec2::new(12., 12.), [3.; 4]),
0.,
);
@@ -2146,7 +2154,7 @@ impl NodeNetworkInterface {
let bounding_box_top_left = DVec2::new((all_nodes_bounding_box[0].x / 24. + 0.5).floor() * 24., (all_nodes_bounding_box[0].y / 24. + 0.5).floor() * 24.) + offset_from_top_left;
let import_top_left = DVec2::new(viewport_top_left.x.min(bounding_box_top_left.x), viewport_top_left.y.min(bounding_box_top_left.y));
let add_import_center = import_top_left + DVec2::new(0., self.number_of_displayed_imports(network_path) as f64 * 24.);
let add_import = ClickTarget::new(
let add_import = ClickTarget::new_with_subpath(
Subpath::new_rounded_rect(add_import_center - DVec2::new(12., 12.), add_import_center + DVec2::new(12., 12.), [3.; 4]),
0.,
);
@@ -2165,8 +2173,8 @@ impl NodeNetworkInterface {
let reorder_import_center = (import_bounding_box[0] + import_bounding_box[1]) / 2. + DVec2::new(-12., 0.);
let remove_import_center = reorder_import_center + DVec2::new(-12., 0.);
let reorder_import = ClickTarget::new(Subpath::new_rect(reorder_import_center - DVec2::new(3., 4.), reorder_import_center + DVec2::new(3., 4.)), 0.);
let remove_import = ClickTarget::new(Subpath::new_rect(remove_import_center - DVec2::new(8., 8.), remove_import_center + DVec2::new(8., 8.)), 0.);
let reorder_import = ClickTarget::new_with_subpath(Subpath::new_rect(reorder_import_center - DVec2::new(3., 4.), reorder_import_center + DVec2::new(3., 4.)), 0.);
let remove_import = ClickTarget::new_with_subpath(Subpath::new_rect(remove_import_center - DVec2::new(8., 8.), remove_import_center + DVec2::new(8., 8.)), 0.);
reorder_imports_exports.insert_custom_output_port(*import_index, reorder_import);
remove_imports_exports.insert_custom_output_port(*import_index, remove_import);
@@ -2180,8 +2188,8 @@ impl NodeNetworkInterface {
let reorder_export_center = (export_bounding_box[0] + export_bounding_box[1]) / 2. + DVec2::new(12., 0.);
let remove_export_center = reorder_export_center + DVec2::new(12., 0.);
let reorder_export = ClickTarget::new(Subpath::new_rect(reorder_export_center - DVec2::new(3., 4.), reorder_export_center + DVec2::new(3., 4.)), 0.);
let remove_export = ClickTarget::new(Subpath::new_rect(remove_export_center - DVec2::new(8., 8.), remove_export_center + DVec2::new(8., 8.)), 0.);
let reorder_export = ClickTarget::new_with_subpath(Subpath::new_rect(reorder_export_center - DVec2::new(3., 4.), reorder_export_center + DVec2::new(3., 4.)), 0.);
let remove_export = ClickTarget::new_with_subpath(Subpath::new_rect(remove_export_center - DVec2::new(8., 8.), remove_export_center + DVec2::new(8., 8.)), 0.);
reorder_imports_exports.insert_custom_input_port(*export_index, reorder_export);
remove_imports_exports.insert_custom_input_port(*export_index, remove_export);
@@ -2572,7 +2580,7 @@ impl NodeNetworkInterface {
let radius = 3.;
let subpath = bezier_rs::Subpath::new_rounded_rect(node_click_target_top_left, node_click_target_bottom_right, [radius; 4]);
let node_click_target = ClickTarget::new(subpath, 0.);
let node_click_target = ClickTarget::new_with_subpath(subpath, 0.);
DocumentNodeClickTargets {
node_click_target,
@@ -2597,12 +2605,12 @@ impl NodeNetworkInterface {
// Update visibility button click target
let visibility_offset = node_top_left + DVec2::new(width as f64, 24.);
let subpath = Subpath::new_rounded_rect(DVec2::new(-12., -12.) + visibility_offset, DVec2::new(12., 12.) + visibility_offset, [3.; 4]);
let visibility_click_target = ClickTarget::new(subpath, 0.);
let visibility_click_target = ClickTarget::new_with_subpath(subpath, 0.);
// Update grip button click target, which is positioned to the left of the left most icon
let grip_offset_right_edge = node_top_left + DVec2::new(width as f64 - (GRID_SIZE as f64) / 2., 24.);
let subpath = Subpath::new_rounded_rect(DVec2::new(-8., -12.) + grip_offset_right_edge, DVec2::new(0., 12.) + grip_offset_right_edge, [0.; 4]);
let grip_click_target = ClickTarget::new(subpath, 0.);
let grip_click_target = ClickTarget::new_with_subpath(subpath, 0.);
// Create layer click target, which is contains the layer and the chain background
let chain_width_grid_spaces = self.chain_width(node_id, network_path);
@@ -2611,7 +2619,7 @@ impl NodeNetworkInterface {
let chain_top_left = node_top_left - DVec2::new((chain_width_grid_spaces * crate::consts::GRID_SIZE) as f64, 0.);
let radius = 10.;
let subpath = bezier_rs::Subpath::new_rounded_rect(chain_top_left, node_bottom_right, [radius; 4]);
let node_click_target = ClickTarget::new(subpath, 0.);
let node_click_target = ClickTarget::new_with_subpath(subpath, 0.);
DocumentNodeClickTargets {
node_click_target,
@@ -2804,20 +2812,29 @@ impl NodeNetworkInterface {
if let (Some(import_export_click_targets), Some(node_click_targets)) = (self.import_export_ports(network_path).cloned(), self.node_click_targets(&node_id, network_path)) {
let mut node_path = String::new();
let _ = node_click_targets.node_click_target.subpath().subpath_to_svg(&mut node_path, DAffine2::IDENTITY);
if let ClickTargetType::Subpath(subpath) = node_click_targets.node_click_target.target_type() {
let _ = subpath.subpath_to_svg(&mut node_path, DAffine2::IDENTITY);
}
all_node_click_targets.push((node_id, node_path));
for port in node_click_targets.port_click_targets.click_targets().chain(import_export_click_targets.click_targets()) {
let mut port_path = String::new();
let _ = port.subpath().subpath_to_svg(&mut port_path, DAffine2::IDENTITY);
port_click_targets.push(port_path);
if let ClickTargetType::Subpath(subpath) = port.target_type() {
let mut port_path = String::new();
let _ = subpath.subpath_to_svg(&mut port_path, DAffine2::IDENTITY);
port_click_targets.push(port_path);
}
}
if let NodeTypeClickTargets::Layer(layer_metadata) = &node_click_targets.node_type_metadata {
let mut port_path = String::new();
let _ = layer_metadata.visibility_click_target.subpath().subpath_to_svg(&mut port_path, DAffine2::IDENTITY);
icon_click_targets.push(port_path);
let mut port_path = String::new();
let _ = layer_metadata.grip_click_target.subpath().subpath_to_svg(&mut port_path, DAffine2::IDENTITY);
icon_click_targets.push(port_path);
if let ClickTargetType::Subpath(subpath) = layer_metadata.visibility_click_target.target_type() {
let mut port_path = String::new();
let _ = subpath.subpath_to_svg(&mut port_path, DAffine2::IDENTITY);
icon_click_targets.push(port_path);
}
if let ClickTargetType::Subpath(subpath) = layer_metadata.grip_click_target.target_type() {
let mut port_path = String::new();
let _ = subpath.subpath_to_svg(&mut port_path, DAffine2::IDENTITY);
icon_click_targets.push(port_path);
}
}
}
});
@@ -2872,9 +2889,11 @@ impl NodeNetworkInterface {
.chain(modify_import_export_click_targets.remove_imports_exports.click_targets())
.chain(modify_import_export_click_targets.reorder_imports_exports.click_targets())
{
let mut remove_string = String::new();
let _ = click_target.subpath().subpath_to_svg(&mut remove_string, DAffine2::IDENTITY);
modify_import_export.push(remove_string);
if let ClickTargetType::Subpath(subpath) = click_target.target_type() {
let mut remove_string = String::new();
let _ = subpath.subpath_to_svg(&mut remove_string, DAffine2::IDENTITY);
modify_import_export.push(remove_string);
}
}
}
FrontendClickTargets {
@@ -3113,7 +3132,7 @@ impl NodeNetworkInterface {
self.node_click_targets(node_id, network_path)
.and_then(|transient_node_metadata| transient_node_metadata.node_click_target.bounding_box())
})
.reduce(graphene_core::renderer::Quad::combine_bounds)
.reduce(graphene_std::renderer::Quad::combine_bounds)
}
/// Gets the bounding box in viewport coordinates for each node in the node graph
@@ -3174,8 +3193,8 @@ impl NodeNetworkInterface {
self.document_metadata
.click_targets
.get(&layer)
.map(|click| click.iter().map(ClickTarget::subpath))
.map(|subpaths| VectorData::from_subpaths(subpaths, true))
.map(|click| click.iter().map(ClickTarget::target_type))
.map(|target_types| VectorData::from_target_types(target_types, true))
}
/// Loads the structure of layer nodes from a node graph.
@@ -3805,6 +3824,15 @@ impl NodeNetworkInterface {
node_metadata.persistent_metadata.network_metadata = metadata.network_metadata;
}
/// Keep metadata in sync with the new implementation if this is used by anything other than the upgrade scripts
pub fn replace_reference_name(&mut self, node_id: &NodeId, network_path: &[NodeId], reference_name: String) {
let Some(node_metadata) = self.node_metadata_mut(node_id, network_path) else {
log::error!("Could not get node metadata in replace_reference_name");
return;
};
node_metadata.persistent_metadata.reference = Some(reference_name);
}
/// Keep metadata in sync with the new implementation if this is used by anything other than the upgrade scripts
pub fn set_manual_compostion(&mut self, node_id: &NodeId, network_path: &[NodeId], manual_composition: Option<Type>) {
let Some(network) = self.network_mut(network_path) else {
@@ -5884,7 +5912,7 @@ impl Ports {
fn insert_input_port_at_center(&mut self, input_index: usize, center: DVec2) {
let subpath = Subpath::new_ellipse(center - DVec2::new(8., 8.), center + DVec2::new(8., 8.));
self.insert_custom_input_port(input_index, ClickTarget::new(subpath, 0.));
self.insert_custom_input_port(input_index, ClickTarget::new_with_subpath(subpath, 0.));
}
fn insert_custom_input_port(&mut self, input_index: usize, click_target: ClickTarget) {
@@ -5893,7 +5921,7 @@ impl Ports {
fn insert_output_port_at_center(&mut self, output_index: usize, center: DVec2) {
let subpath = Subpath::new_ellipse(center - DVec2::new(8., 8.), center + DVec2::new(8., 8.));
self.insert_custom_output_port(output_index, ClickTarget::new(subpath, 0.));
self.insert_custom_output_port(output_index, ClickTarget::new_with_subpath(subpath, 0.));
}
fn insert_custom_output_port(&mut self, output_index: usize, click_target: ClickTarget) {
@@ -6488,6 +6516,12 @@ pub struct NodePersistentMetadata {
position: NodePosition,
}
impl NodePersistentMetadata {
pub fn new(position: NodePosition) -> Self {
Self { position }
}
}
/// A layer can either be position as Absolute or in a Stack
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum LayerPosition {
@@ -55,6 +55,8 @@ pub struct LayerPanelEntry {
pub ancestor_of_selected: bool,
#[serde(rename = "descendantOfSelected")]
pub descendant_of_selected: bool,
pub clipped: bool,
pub clippable: bool,
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, PartialEq, Eq, specta::Type)]
@@ -8,10 +8,8 @@ use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::shape_editor::ShapeState;
use crate::messages::tool::utility_types::ToolType;
use glam::{DAffine2, DMat2, DVec2};
use graphene_core::renderer::Quad;
use graphene_core::vector::ManipulatorPointId;
use graphene_core::vector::VectorModificationType;
use graphene_std::vector::{HandleId, PointId};
use graphene_std::renderer::Quad;
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;
}
@@ -635,17 +645,17 @@ impl<'a> Selected<'a> {
}
pub fn apply_transformation(&mut self, transformation: DAffine2, transform_operation: Option<TransformOperation>) {
if !self.selected.is_empty() {
// TODO: Cache the result of `shallowest_unique_layers` to avoid this heavy computation every frame of movement, see https://github.com/GraphiteEditor/Graphite/pull/481
for layer in self.network_interface.shallowest_unique_layers(&[]) {
match &mut self.original_transforms {
OriginalTransforms::Layer(layer_transforms) => {
Self::transform_layer(self.network_interface.document_metadata(), layer, layer_transforms.get(&layer), transformation, self.responses)
}
OriginalTransforms::Path(path_transforms) => {
if let Some(initial_points) = path_transforms.get_mut(&layer) {
Self::transform_path(self.network_interface.document_metadata(), layer, initial_points, transformation, self.responses, transform_operation)
}
if self.selected.is_empty() {
return;
}
// TODO: Cache the result of `shallowest_unique_layers` to avoid this heavy computation every frame of movement, see https://github.com/GraphiteEditor/Graphite/pull/481
for layer in self.network_interface.shallowest_unique_layers(&[]) {
match &mut self.original_transforms {
OriginalTransforms::Layer(layer_transforms) => Self::transform_layer(self.network_interface.document_metadata(), layer, layer_transforms.get(&layer), transformation, self.responses),
OriginalTransforms::Path(path_transforms) => {
if let Some(initial_points) = path_transforms.get_mut(&layer) {
Self::transform_path(self.network_interface.document_metadata(), layer, initial_points, transformation, self.responses, transform_operation)
}
}
}
@@ -0,0 +1,752 @@
// TODO: Eventually remove this document upgrade code
// This file contains lots of hacky code for upgrading old documents to the new format
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};
use crate::messages::prelude::DocumentMessageHandler;
use bezier_rs::Subpath;
use glam::IVec2;
use graph_craft::document::{DocumentNodeImplementation, NodeInput, value::TaggedValue};
use graphene_std::text::TypesettingConfig;
use graphene_std::uuid::NodeId;
use graphene_std::vector::style::{Fill, FillType, Gradient, PaintOrder, StrokeAlign};
use graphene_std::vector::{VectorData, VectorDataTable};
use std::collections::HashMap;
const TEXT_REPLACEMENTS: &[(&str, &str)] = &[
("graphene_core::vector::vector_nodes::SamplePointsNode", "graphene_core::vector::SamplePolylineNode"),
("graphene_core::vector::vector_nodes::SubpathSegmentLengthsNode", "graphene_core::vector::SubpathSegmentLengthsNode"),
];
const REPLACEMENTS: &[(&str, &str)] = &[
("graphene_core::AddArtboardNode", "graphene_core::graphic_element::AppendArtboardNode"),
("graphene_core::ConstructArtboardNode", "graphene_core::graphic_element::ToArtboardNode"),
("graphene_core::ToGraphicElementNode", "graphene_core::graphic_element::ToElementNode"),
("graphene_core::ToGraphicGroupNode", "graphene_core::graphic_element::ToGroupNode"),
("graphene_core::ops::MathNode", "graphene_math_nodes::MathNode"),
("graphene_core::ops::AddNode", "graphene_math_nodes::AddNode"),
("graphene_core::ops::SubtractNode", "graphene_math_nodes::SubtractNode"),
("graphene_core::ops::MultiplyNode", "graphene_math_nodes::MultiplyNode"),
("graphene_core::ops::DivideNode", "graphene_math_nodes::DivideNode"),
("graphene_core::ops::ModuloNode", "graphene_math_nodes::ModuloNode"),
("graphene_core::ops::ExponentNode", "graphene_math_nodes::ExponentNode"),
("graphene_core::ops::RootNode", "graphene_math_nodes::RootNode"),
("graphene_core::ops::LogarithmNode", "graphene_math_nodes::LogarithmNode"),
("graphene_core::ops::SineNode", "graphene_math_nodes::SineNode"),
("graphene_core::ops::CosineNode", "graphene_math_nodes::CosineNode"),
("graphene_core::ops::TangentNode", "graphene_math_nodes::TangentNode"),
("graphene_core::ops::SineInverseNode", "graphene_math_nodes::SineInverseNode"),
("graphene_core::ops::CosineInverseNode", "graphene_math_nodes::CosineInverseNode"),
("graphene_core::ops::TangentInverseNode", "graphene_math_nodes::TangentInverseNode"),
("graphene_core::ops::RandomNode", "graphene_math_nodes::RandomNode"),
("graphene_core::ops::ToU32Node", "graphene_math_nodes::ToU32Node"),
("graphene_core::ops::ToU64Node", "graphene_math_nodes::ToU64Node"),
("graphene_core::ops::ToF64Node", "graphene_math_nodes::ToF64Node"),
("graphene_core::ops::RoundNode", "graphene_math_nodes::RoundNode"),
("graphene_core::ops::FloorNode", "graphene_math_nodes::FloorNode"),
("graphene_core::ops::CeilingNode", "graphene_math_nodes::CeilingNode"),
("graphene_core::ops::MinNode", "graphene_math_nodes::MinNode"),
("graphene_core::ops::MaxNode", "graphene_math_nodes::MaxNode"),
("graphene_core::ops::ClampNode", "graphene_math_nodes::ClampNode"),
("graphene_core::ops::EqualsNode", "graphene_math_nodes::EqualsNode"),
("graphene_core::ops::NotEqualsNode", "graphene_math_nodes::NotEqualsNode"),
("graphene_core::ops::LessThanNode", "graphene_math_nodes::LessThanNode"),
("graphene_core::ops::GreaterThanNode", "graphene_math_nodes::GreaterThanNode"),
("graphene_core::ops::LogicalOrNode", "graphene_math_nodes::LogicalOrNode"),
("graphene_core::ops::LogicalAndNode", "graphene_math_nodes::LogicalAndNode"),
("graphene_core::ops::LogicalNotNode", "graphene_math_nodes::LogicalNotNode"),
("graphene_core::ops::BoolValueNode", "graphene_math_nodes::BoolValueNode"),
("graphene_core::ops::NumberValueNode", "graphene_math_nodes::NumberValueNode"),
("graphene_core::ops::PercentageValueNode", "graphene_math_nodes::PercentageValueNode"),
("graphene_core::ops::CoordinateValueNode", "graphene_math_nodes::CoordinateValueNode"),
("graphene_core::ops::ColorValueNode", "graphene_math_nodes::ColorValueNode"),
("graphene_core::ops::GradientValueNode", "graphene_math_nodes::GradientValueNode"),
("graphene_core::ops::StringValueNode", "graphene_math_nodes::StringValueNode"),
("graphene_core::ops::DotProductNode", "graphene_math_nodes::DotProductNode"),
("graphene_core::ops::SizeOfNode", "graphene_core::debug::SizeOfNode"),
("graphene_core::ops::SomeNode", "graphene_core::debug::SomeNode"),
("graphene_core::ops::UnwrapNode", "graphene_core::debug::UnwrapNode"),
("graphene_core::ops::CloneNode", "graphene_core::debug::CloneNode"),
("graphene_core::ops::ExtractXyNode", "graphene_core::extract_xy::ExtractXyNode"),
("graphene_core::logic::LogicAndNode", "graphene_core::ops::LogicAndNode"),
("graphene_core::logic::LogicNotNode", "graphene_core::ops::LogicNotNode"),
("graphene_core::logic::LogicOrNode", "graphene_core::ops::LogicOrNode"),
("graphene_core::ops::ConstructVector2", "graphene_core::ops::CoordinateValueNode"),
("graphene_core::ops::Vector2ValueNode", "graphene_core::ops::CoordinateValueNode"),
("graphene_core::raster::BlackAndWhiteNode", "graphene_core::raster::adjustments::BlackAndWhiteNode"),
("graphene_core::raster::BlendNode", "graphene_core::raster::adjustments::BlendNode"),
("graphene_core::raster::BlendModeNode", "graphene_core::blending_nodes::BlendModeNode"),
("graphene_core::raster::OpacityNode", "graphene_core::blending_nodes::OpacityNode"),
("graphene_core::raster::BlendingNode", "graphene_core::blending_nodes::BlendingNode"),
("graphene_core::raster::ChannelMixerNode", "graphene_core::raster::adjustments::ChannelMixerNode"),
("graphene_core::raster::adjustments::ColorOverlayNode", "graphene_core::raster::adjustments::ColorOverlayNode"),
("graphene_core::raster::ExposureNode", "graphene_core::raster::adjustments::ExposureNode"),
("graphene_core::raster::ExtractChannelNode", "graphene_core::raster::adjustments::ExtractChannelNode"),
("graphene_core::raster::GradientMapNode", "graphene_core::raster::adjustments::GradientMapNode"),
("graphene_core::raster::HueSaturationNode", "graphene_core::raster::adjustments::HueSaturationNode"),
("graphene_core::vector::GenerateHandlesNode", "graphene_core::vector::AutoTangentsNode"),
("graphene_core::vector::RemoveHandlesNode", "graphene_core::vector::AutoTangentsNode"),
("graphene_core::raster::InvertNode", "graphene_core::raster::adjustments::InvertNode"),
("graphene_core::raster::InvertRGBNode", "graphene_core::raster::adjustments::InvertNode"),
("graphene_core::raster::LevelsNode", "graphene_core::raster::adjustments::LevelsNode"),
("graphene_core::raster::LuminanceNode", "graphene_core::raster::adjustments::LuminanceNode"),
("graphene_core::raster::ExtractOpaqueNode", "graphene_core::raster::adjustments::MakeOpaqueNode"),
("graphene_core::raster::PosterizeNode", "graphene_core::raster::adjustments::PosterizeNode"),
("graphene_core::raster::ThresholdNode", "graphene_core::raster::adjustments::ThresholdNode"),
("graphene_core::raster::VibranceNode", "graphene_core::raster::adjustments::VibranceNode"),
("graphene_core::text::TextGeneratorNode", "graphene_core::text::TextNode"),
("graphene_core::transform::SetTransformNode", "graphene_core::transform_nodes::ReplaceTransformNode"),
("graphene_core::transform::ReplaceTransformNode", "graphene_core::transform_nodes::ReplaceTransformNode"),
("graphene_core::transform::TransformNode", "graphene_core::transform_nodes::TransformNode"),
("graphene_core::transform::BoundlessFootprintNode", "graphene_core::transform_nodes::BoundlessFootprintNode"),
("graphene_core::transform::FreezeRealTimeNode", "graphene_core::transform_nodes::FreezeRealTimeNode"),
("graphene_core::vector::SplinesFromPointsNode", "graphene_core::vector::SplineNode"),
("graphene_core::vector::generator_nodes::EllipseGenerator", "graphene_core::vector::generator_nodes::EllipseNode"),
("graphene_core::vector::generator_nodes::LineGenerator", "graphene_core::vector::generator_nodes::LineNode"),
("graphene_core::vector::generator_nodes::RectangleGenerator", "graphene_core::vector::generator_nodes::RectangleNode"),
(
"graphene_core::vector::generator_nodes::RegularPolygonGenerator",
"graphene_core::vector::generator_nodes::RegularPolygonNode",
),
("graphene_core::vector::generator_nodes::StarGenerator", "graphene_core::vector::generator_nodes::StarNode"),
("graphene_std::executor::BlendGpuImageNode", "graphene_std::gpu_nodes::BlendGpuImageNode"),
("graphene_std::raster::SampleNode", "graphene_std::raster::SampleImageNode"),
("graphene_core::transform::CullNode", "graphene_core::ops::IdentityNode"),
("graphene_std::raster::MaskImageNode", "graphene_std::raster::MaskNode"),
("graphene_core::vector::FlattenVectorElementsNode", "graphene_core::vector::FlattenPathNode"),
("graphene_std::vector::BooleanOperationNode", "graphene_path_bool::BooleanOperationNode"),
];
pub fn document_migration_string_preprocessing(document_serialized_content: String) -> String {
TEXT_REPLACEMENTS
.iter()
.fold(document_serialized_content, |document_serialized_content, (old, new)| document_serialized_content.replace(old, new))
}
pub fn document_migration_reset_node_definition(document_serialized_content: &str) -> bool {
// Upgrade a document being opened to use fresh copies of all nodes
if document_serialized_content.contains("node_output_index") {
return true;
}
// Upgrade layer implementation from https://github.com/GraphiteEditor/Graphite/pull/1946 (see also `fn fix_nodes()` in `main.rs` of Graphene CLI)
if document_serialized_content.contains("graphene_core::ConstructLayerNode") || document_serialized_content.contains("graphene_core::AddArtboardNode") {
return true;
}
false
}
pub fn document_migration_upgrades(document: &mut DocumentMessageHandler, reset_node_definitions_on_open: bool) {
let mut network = document.network_interface.document_network().clone();
network.generate_node_paths(&[]);
// Apply string replacements to each node
let node_ids: Vec<_> = network.recursive_nodes().map(|(&id, node)| (id, node.original_location.path.clone().unwrap())).collect();
for (node_id, path) in &node_ids {
let network_path: Vec<_> = path.iter().copied().take(path.len() - 1).collect();
if let Some(DocumentNodeImplementation::ProtoNode(protonode_id)) = document
.network_interface
.nested_network(&network_path)
.unwrap()
.nodes
.get(node_id)
.map(|node| node.implementation.clone())
{
for (old, new) in REPLACEMENTS {
let node_path_without_type_args = protonode_id.name.split('<').next();
if node_path_without_type_args == Some(old) {
document
.network_interface
.replace_implementation(node_id, &network_path, DocumentNodeImplementation::ProtoNode(new.to_string().into()));
document.network_interface.set_manual_compostion(node_id, &network_path, Some(graph_craft::Type::Generic("T".into())));
}
}
}
}
if reset_node_definitions_on_open {
// This can be used, if uncommented, to upgrade demo artwork with outdated document node internals from their definitions. Delete when it's no longer needed.
// Used for upgrading old internal networks for demo artwork nodes. Will reset all node internals for any opened file
for node_id in &document
.network_interface
.document_network_metadata()
.persistent_metadata
.node_metadata
.keys()
.cloned()
.collect::<Vec<NodeId>>()
{
if let Some(reference) = document
.network_interface
.document_network_metadata()
.persistent_metadata
.node_metadata
.get(node_id)
.and_then(|node| node.persistent_metadata.reference.as_ref())
{
let Some(node_definition) = resolve_document_node_type(reference) else { continue };
let default_definition_node = node_definition.default_node_template();
document.network_interface.replace_implementation(node_id, &[], default_definition_node.document_node.implementation);
document
.network_interface
.replace_implementation_metadata(node_id, &[], default_definition_node.persistent_node_metadata);
document.network_interface.set_manual_compostion(node_id, &[], default_definition_node.document_node.manual_composition);
}
}
}
if document
.network_interface
.document_network_metadata()
.persistent_metadata
.node_metadata
.iter()
.any(|(node_id, node)| node.persistent_metadata.reference.as_ref().is_some_and(|reference| reference == "Output") && *node_id == NodeId(0))
{
document.network_interface.delete_nodes(vec![NodeId(0)], true, &[]);
}
let mut network = document.network_interface.document_network().clone();
network.generate_node_paths(&[]);
let node_ids: Vec<_> = network.recursive_nodes().map(|(&id, node)| (id, node.original_location.path.clone().unwrap())).collect();
// Apply upgrades to each node
for (node_id, path) in &node_ids {
let network_path: Vec<_> = path.iter().copied().take(path.len() - 1).collect();
let network_path = &network_path;
let Some(node) = document.network_interface.nested_network(network_path).unwrap().nodes.get(node_id).cloned() else {
log::error!("could not get node in deserialize_document");
continue;
};
// Upgrade old nodes to use `Context` instead of `()` or `Footprint` for manual composition
if node.manual_composition == Some(graph_craft::concrete!(())) || node.manual_composition == Some(graph_craft::concrete!(graphene_std::transform::Footprint)) {
document
.network_interface
.set_manual_compostion(node_id, network_path, graph_craft::concrete!(graphene_std::Context).into());
}
let Some(node_metadata) = document.network_interface.network_metadata(network_path).unwrap().persistent_metadata.node_metadata.get(node_id) else {
log::error!("could not get node metadata for node {node_id} in deserialize_document");
continue;
};
let Some(ref reference) = node_metadata.persistent_metadata.reference.clone() else {
// TODO: Investigate if this should be an expected case, because currently it runs hundreds of times normally.
// TODO: Either delete the commented out error below if this is normal, or fix the underlying issue if this is not expected.
// log::error!("could not get reference in deserialize_document");
continue;
};
let inputs_count = node.inputs.len();
// Upgrade Fill nodes to the format change in #1778
if reference == "Fill" && inputs_count == 8 {
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());
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
let Some(fill_type) = old_inputs[1].as_value().cloned() else { continue };
let TaggedValue::FillType(fill_type) = fill_type else { continue };
let Some(solid_color) = old_inputs[2].as_value().cloned() else { continue };
let TaggedValue::OptionalColor(solid_color) = solid_color else { continue };
let Some(gradient_type) = old_inputs[3].as_value().cloned() else { continue };
let TaggedValue::GradientType(gradient_type) = gradient_type else { continue };
let Some(start) = old_inputs[4].as_value().cloned() else { continue };
let TaggedValue::DVec2(start) = start else { continue };
let Some(end) = old_inputs[5].as_value().cloned() else { continue };
let TaggedValue::DVec2(end) = end else { continue };
let Some(transform) = old_inputs[6].as_value().cloned() else { continue };
let TaggedValue::DAffine2(transform) = transform else { continue };
let Some(positions) = old_inputs[7].as_value().cloned() else { continue };
let TaggedValue::GradientStops(positions) = positions else { continue };
let fill = match (fill_type, solid_color) {
(FillType::Solid, None) => Fill::None,
(FillType::Solid, Some(color)) => Fill::Solid(color),
(FillType::Gradient, _) => Fill::Gradient(Gradient {
stops: positions,
gradient_type,
start,
end,
transform,
}),
};
document
.network_interface
.set_input(&InputConnector::node(*node_id, 1), NodeInput::value(TaggedValue::Fill(fill.clone()), false), network_path);
match fill {
Fill::None => {
document
.network_interface
.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::OptionalColor(None), false), network_path);
}
Fill::Solid(color) => {
document
.network_interface
.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::OptionalColor(Some(color)), false), network_path);
}
Fill::Gradient(gradient) => {
document
.network_interface
.set_input(&InputConnector::node(*node_id, 3), NodeInput::value(TaggedValue::Gradient(gradient), false), network_path);
}
}
}
// Upgrade Stroke node to reorder parameters and add "Align" and "Paint Order" (#2644)
if reference == "Stroke" && inputs_count == 8 {
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);
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
let align_input = NodeInput::value(TaggedValue::StrokeAlign(StrokeAlign::Center), false);
let paint_order_input = NodeInput::value(TaggedValue::PaintOrder(PaintOrder::StrokeAbove), false);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 3), align_input, network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 4), old_inputs[5].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 5), old_inputs[6].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 6), old_inputs[7].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 7), paint_order_input, network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 8), old_inputs[3].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 9), old_inputs[4].clone(), network_path);
}
// Rename the old "Splines from Points" node to "Spline" and upgrade it to the new "Spline" node
if reference == "Splines from Points" {
document.network_interface.set_reference(node_id, network_path, Some("Spline".to_string()));
}
// Upgrade the old "Spline" node to the new "Spline" node
if reference == "Spline" {
// Retrieve the proto node identifier and verify it is the old "Spline" node, otherwise skip it if this is the new "Spline" node
let identifier = document
.network_interface
.implementation(node_id, network_path)
.and_then(|implementation| implementation.get_proto_node());
if identifier.map(|identifier| &identifier.name) != Some(&"graphene_core::vector::generator_nodes::SplineNode".into()) {
continue;
}
// Obtain the document node for the given node ID, extract the vector points, and create vector data from the list of points
let node = document.network_interface.document_node(node_id, network_path).unwrap();
let Some(TaggedValue::VecDVec2(points)) = node.inputs.get(1).and_then(|tagged_value| tagged_value.as_value()) else {
log::error!("The old Spline node's input at index 1 is not a TaggedValue::VecDVec2");
continue;
};
let vector_data = VectorData::from_subpath(Subpath::from_anchors_linear(points.to_vec(), false));
// Retrieve the output connectors linked to the "Spline" node's output port
let spline_outputs = document
.network_interface
.outward_wires(network_path)
.unwrap()
.get(&OutputConnector::node(*node_id, 0))
.expect("Vec of InputConnector Spline node is connected to its output port 0.")
.clone();
// Get the node's current position in the graph
let Some(node_position) = document.network_interface.position(node_id, network_path) else {
log::error!("Could not get position of spline node.");
continue;
};
// Get the "Path" node definition and fill it in with the vector data and default vector modification
let path_node_type = resolve_document_node_type("Path").expect("Path node does not exist.");
let path_node = path_node_type.node_template_input_override([
Some(NodeInput::value(TaggedValue::VectorData(VectorDataTable::new(vector_data)), true)),
Some(NodeInput::value(TaggedValue::VectorModification(Default::default()), false)),
]);
// Get the "Spline" node definition and wire it up with the "Path" node as input
let spline_node_type = resolve_document_node_type("Spline").expect("Spline node does not exist.");
let spline_node = spline_node_type.node_template_input_override([Some(NodeInput::node(NodeId(1), 0))]);
// Create a new node group with the "Path" and "Spline" nodes and generate new node IDs for them
let nodes = vec![(NodeId(1), path_node), (NodeId(0), spline_node)];
let new_ids = nodes.iter().map(|(id, _)| (*id, NodeId::new())).collect::<HashMap<_, _>>();
let new_spline_id = *new_ids.get(&NodeId(0)).unwrap();
let new_path_id = *new_ids.get(&NodeId(1)).unwrap();
// Remove the old "Spline" node from the document
document.network_interface.delete_nodes(vec![*node_id], false, network_path);
// Insert the new "Path" and "Spline" nodes into the network interface with generated IDs
document.network_interface.insert_node_group(nodes.clone(), new_ids, network_path);
// Reposition the new "Spline" node to match the original "Spline" node's position
document.network_interface.shift_node(&new_spline_id, node_position, network_path);
// Reposition the new "Path" node with an offset relative to the original "Spline" node's position
document.network_interface.shift_node(&new_path_id, node_position + IVec2::new(-7, 0), network_path);
// Redirect each output connection from the old node to the new "Spline" node's output port
for input_connector in spline_outputs {
document.network_interface.set_input(&input_connector, NodeInput::node(new_spline_id, 0), network_path);
}
}
// 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 {
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());
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 3), old_inputs[3].clone(), network_path);
document.network_interface.set_input(
&InputConnector::node(*node_id, 4),
if inputs_count == 6 {
old_inputs[4].clone()
} else {
NodeInput::value(TaggedValue::F64(TypesettingConfig::default().line_height_ratio), false)
},
network_path,
);
document.network_interface.set_input(
&InputConnector::node(*node_id, 5),
if inputs_count == 6 {
old_inputs[5].clone()
} else {
NodeInput::value(TaggedValue::F64(TypesettingConfig::default().character_spacing), false)
},
network_path,
);
document.network_interface.set_input(
&InputConnector::node(*node_id, 6),
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),
network_path,
);
}
// Upgrade Sine, Cosine, and Tangent nodes to include a boolean input for whether the output should be in radians, which was previously the only option but is now not the default
if (reference == "Sine" || reference == "Cosine" || reference == "Tangent") && inputs_count == 1 {
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());
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document
.network_interface
.set_input(&InputConnector::node(*node_id, 1), NodeInput::value(TaggedValue::Bool(true), false), network_path);
}
// Upgrade the Modulo node to include a boolean input for whether the output should be always positive, which was previously not an option
if reference == "Modulo" && inputs_count == 2 {
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());
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
document
.network_interface
.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::Bool(false), false), network_path);
}
// Upgrade the Mirror node to add the `keep_original` boolean input
if reference == "Mirror" && inputs_count == 3 {
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());
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path);
document
.network_interface
.set_input(&InputConnector::node(*node_id, 3), NodeInput::value(TaggedValue::Bool(true), false), network_path);
}
// Upgrade the Mirror node to add the `reference_point` input and change `offset` from `DVec2` to `f64`
if reference == "Mirror" && inputs_count == 4 {
let node_definition = resolve_document_node_type(reference).unwrap();
let new_node_template = node_definition.default_node_template();
let document_node = new_node_template.document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
document
.network_interface
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
let Some(&TaggedValue::DVec2(old_offset)) = old_inputs[1].as_value() else { return };
let old_offset = if old_offset.x.abs() > old_offset.y.abs() { old_offset.x } else { old_offset.y };
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(
&InputConnector::node(*node_id, 1),
NodeInput::value(TaggedValue::ReferencePoint(graphene_std::transform::ReferencePoint::Center), false),
network_path,
);
document
.network_interface
.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::F64(old_offset), false), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 3), old_inputs[2].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 4), old_inputs[3].clone(), network_path);
}
// Upgrade artboard name being passed as hidden value input to "To Artboard"
if reference == "Artboard" && reset_node_definitions_on_open {
let label = document.network_interface.display_name(node_id, network_path);
document
.network_interface
.set_input(&InputConnector::node(NodeId(0), 1), NodeInput::value(TaggedValue::String(label), false), &[*node_id]);
}
if reference == "Image" && inputs_count == 1 {
let node_definition = crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type(reference).unwrap();
let new_image_node = node_definition.default_node_template();
document.network_interface.replace_implementation(node_id, network_path, new_image_node.document_node.implementation);
// Insert a new empty input for the image
document.network_interface.add_import(TaggedValue::None, false, 0, "Empty", "", &[*node_id]);
document.network_interface.set_reference(node_id, network_path, Some("Image".to_string()));
}
if reference == "Noise Pattern" && inputs_count == 15 {
let node_definition = crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type(reference).unwrap();
let new_noise_pattern_node = node_definition.default_node_template();
document
.network_interface
.replace_implementation(node_id, network_path, new_noise_pattern_node.document_node.implementation);
let old_inputs = document.network_interface.replace_inputs(node_id, new_noise_pattern_node.document_node.inputs.clone(), network_path);
document
.network_interface
.set_input(&InputConnector::node(*node_id, 0), NodeInput::value(TaggedValue::None, false), network_path);
for (i, input) in old_inputs.iter().enumerate() {
document.network_interface.set_input(&InputConnector::node(*node_id, i + 1), input.clone(), network_path);
}
}
if reference == "Instance on Points" && inputs_count == 2 {
let node_definition = resolve_document_node_type(reference).unwrap();
let new_node_template = node_definition.default_node_template();
let document_node = new_node_template.document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
document
.network_interface
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
}
if reference == "Morph" && inputs_count == 4 {
let node_definition = resolve_document_node_type(reference).unwrap();
let new_node_template = node_definition.default_node_template();
let document_node = new_node_template.document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
document
.network_interface
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path);
// We have removed the last input, so we don't add index 3
}
if reference == "Brush" && inputs_count == 4 {
let node_definition = resolve_document_node_type(reference).unwrap();
let new_node_template = node_definition.default_node_template();
let document_node = new_node_template.document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
document
.network_interface
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
// We have removed the second input ("bounds"), so we don't add index 1 and we shift the rest of the inputs down by one
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[2].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[3].clone(), network_path);
}
if reference == "Flatten Vector Elements" {
let node_definition = resolve_document_node_type("Flatten Path").unwrap();
let new_node_template = node_definition.default_node_template();
let document_node = new_node_template.document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
document
.network_interface
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.replace_reference_name(node_id, network_path, "Flatten Path".to_string());
}
if reference == "Remove Handles" {
let node_definition = resolve_document_node_type("Auto-Tangents").unwrap();
let new_node_template = node_definition.default_node_template();
let document_node = new_node_template.document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
document
.network_interface
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document
.network_interface
.set_input(&InputConnector::node(*node_id, 1), NodeInput::value(TaggedValue::F64(0.), false), network_path);
document
.network_interface
.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::Bool(false), false), network_path);
document.network_interface.replace_reference_name(node_id, network_path, "Auto-Tangents".to_string());
}
if reference == "Generate Handles" {
let node_definition = resolve_document_node_type("Auto-Tangents").unwrap();
let new_node_template = node_definition.default_node_template();
let document_node = new_node_template.document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
document
.network_interface
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
document
.network_interface
.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::Bool(true), false), network_path);
document.network_interface.replace_reference_name(node_id, network_path, "Auto-Tangents".to_string());
}
if reference == "Merge by Distance" && inputs_count == 2 {
let node_definition = resolve_document_node_type("Merge by Distance").unwrap();
let new_node_template = node_definition.default_node_template();
let document_node = new_node_template.document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
document
.network_interface
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
document.network_interface.set_input(
&InputConnector::node(*node_id, 2),
NodeInput::value(TaggedValue::MergeByDistanceAlgorithm(graphene_std::vector::misc::MergeByDistanceAlgorithm::Topological), false),
network_path,
);
}
if reference == "Spatial Merge by Distance" {
let node_definition = resolve_document_node_type("Merge by Distance").unwrap();
let new_node_template = node_definition.default_node_template();
let document_node = new_node_template.document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
document
.network_interface
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
document.network_interface.set_input(
&InputConnector::node(*node_id, 2),
NodeInput::value(TaggedValue::MergeByDistanceAlgorithm(graphene_std::vector::misc::MergeByDistanceAlgorithm::Spatial), false),
network_path,
);
document.network_interface.replace_reference_name(node_id, network_path, "Merge by Distance".to_string());
}
if reference == "Sample Points" && inputs_count == 5 {
let node_definition = resolve_document_node_type("Sample Polyline").unwrap();
let new_node_template = node_definition.default_node_template();
let document_node = new_node_template.document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
document
.network_interface
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
let new_spacing_value = NodeInput::value(TaggedValue::PointSpacingType(graphene_std::vector::misc::PointSpacingType::Separation), false);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 1), new_spacing_value, network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[1].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 3), old_inputs[1].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 4), old_inputs[2].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 5), old_inputs[3].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 6), old_inputs[4].clone(), network_path);
document.network_interface.replace_reference_name(node_id, network_path, "Sample Polyline".to_string());
}
}
// Ensure layers are positioned as stacks if they are upstream siblings of another layer
document.network_interface.load_structure();
let all_layers = LayerNodeIdentifier::ROOT_PARENT.descendants(document.network_interface.document_metadata()).collect::<Vec<_>>();
for layer in all_layers {
let Some((downstream_node, input_index)) = document
.network_interface
.outward_wires(&[])
.and_then(|outward_wires| outward_wires.get(&OutputConnector::node(layer.to_node(), 0)))
.and_then(|outward_wires| outward_wires.first())
.and_then(|input_connector| input_connector.node_id().map(|node_id| (node_id, input_connector.input_index())))
else {
continue;
};
// If the downstream node is a layer and the input is the first input and the current layer is not in a stack
if input_index == 0 && document.network_interface.is_layer(&downstream_node, &[]) && !document.network_interface.is_stack(&layer.to_node(), &[]) {
// Ensure the layer is horizontally aligned with the downstream layer to prevent changing the layout of old files
let (Some(layer_position), Some(downstream_position)) = (document.network_interface.position(&layer.to_node(), &[]), document.network_interface.position(&downstream_node, &[])) else {
log::error!("Could not get position for layer {:?} or downstream node {} when opening file", layer.to_node(), downstream_node);
continue;
};
if layer_position.x == downstream_position.x {
document.network_interface.set_stack_position_calculated_offset(&layer.to_node(), &downstream_node, &[]);
}
}
}
}
@@ -4,7 +4,7 @@ use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::utility_types::clipboards::Clipboard;
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, GroupFolderType};
use crate::messages::prelude::*;
use graphene_std::vector::misc::BooleanOperation;
use graphene_std::path_bool::BooleanOperation;
#[derive(Debug, Clone, Default)]
pub struct MenuBarMessageHandler {
@@ -416,7 +416,7 @@ impl LayoutHolder for MenuBarMessageHandler {
action: MenuBarEntry::no_action(),
disabled: no_active_document || !has_selected_layers,
children: MenuBarEntryChildren(vec![{
let list = <BooleanOperation as graphene_core::registry::ChoiceTypeStatic>::list();
let list = <BooleanOperation as graphene_std::registry::ChoiceTypeStatic>::list();
list.into_iter()
.map(|i| i.into_iter())
.flatten()
+1
View File
@@ -2,6 +2,7 @@ mod portfolio_message;
mod portfolio_message_handler;
pub mod document;
pub mod document_migration;
pub mod menu_bar;
pub mod spreadsheet;
pub mod utility_types;
@@ -3,9 +3,9 @@ use super::utility_types::PanelType;
use crate::messages::frontend::utility_types::{ExportBounds, FileType};
use crate::messages::portfolio::document::utility_types::clipboards::Clipboard;
use crate::messages::prelude::*;
use graphene_core::Color;
use graphene_core::raster::Image;
use graphene_core::text::Font;
use graphene_std::Color;
use graphene_std::raster::Image;
use graphene_std::text::Font;
#[impl_message(Message, Portfolio)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
@@ -54,9 +54,6 @@ pub enum PortfolioMessage {
preview_url: String,
data: Vec<u8>,
},
// ImaginateCheckServerStatus,
// ImaginatePollServerStatus,
// ImaginateServerHostname,
Import,
LoadDocumentResources {
document_id: DocumentId,
@@ -1,5 +1,5 @@
use super::document::utility_types::document_metadata::LayerNodeIdentifier;
use super::document::utility_types::network_interface::{self, InputConnector, OutputConnector};
use super::document::utility_types::network_interface;
use super::spreadsheet::SpreadsheetMessageHandler;
use super::utility_types::{PanelType, PersistentData};
use crate::application::generate_uuid;
@@ -11,21 +11,17 @@ use crate::messages::frontend::utility_types::FrontendDocumentDetails;
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::DocumentMessageData;
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
use crate::messages::portfolio::document::utility_types::clipboards::{Clipboard, CopyBufferEntry, INTERNAL_CLIPBOARD_COUNT};
use crate::messages::portfolio::document::utility_types::nodes::SelectedNodes;
use crate::messages::portfolio::document_migration::*;
use crate::messages::preferences::SelectionMode;
use crate::messages::prelude::*;
use crate::messages::tool::utility_types::{HintData, HintGroup, ToolType};
use crate::node_graph_executor::{ExportConfig, NodeGraphExecutor};
use bezier_rs::Subpath;
use glam::{DAffine2, DVec2, IVec2};
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNodeImplementation, NodeId, NodeInput};
use graphene_core::renderer::Quad;
use graphene_core::text::{Font, TypesettingConfig};
use graphene_std::vector::style::{Fill, FillType, Gradient};
use graphene_std::vector::{VectorData, VectorDataTable};
use glam::{DAffine2, DVec2};
use graph_craft::document::NodeId;
use graphene_std::renderer::Quad;
use graphene_std::text::Font;
use std::vec;
pub struct PortfolioMessageData<'a> {
@@ -332,35 +328,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
responses.add(NodeGraphMessage::RunDocumentGraph);
}
}
// PortfolioMessage::ImaginateCheckServerStatus => {
// let server_status = self.persistent_data.imaginate.server_status().clone();
// self.persistent_data.imaginate.poll_server_check();
// #[cfg(target_arch = "wasm32")]
// if let Some(fut) = self.persistent_data.imaginate.initiate_server_check() {
// wasm_bindgen_futures::spawn_local(async move {
// let () = fut.await;
// use wasm_bindgen::prelude::*;
// #[wasm_bindgen(module = "/../frontend/src/editor.ts")]
// extern "C" {
// #[wasm_bindgen(js_name = injectImaginatePollServerStatus)]
// fn inject();
// }
// inject();
// })
// }
// if &server_status != self.persistent_data.imaginate.server_status() {
// responses.add(PropertiesPanelMessage::Refresh);
// }
// }
// PortfolioMessage::ImaginatePollServerStatus => {
// self.persistent_data.imaginate.poll_server_check();
// responses.add(PropertiesPanelMessage::Refresh);
// }
PortfolioMessage::EditorPreferences => self.executor.update_editor_preferences(preferences.editor_preferences()),
// PortfolioMessage::ImaginateServerHostname => {
// self.persistent_data.imaginate.set_host_name(&preferences.imaginate_server_hostname);
// }
PortfolioMessage::Import => {
// This portfolio message wraps the frontend message so it can be listed as an action, which isn't possible for frontend messages
responses.add(FrontendMessage::TriggerImport);
@@ -429,30 +397,18 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
document_serialized_content,
to_front,
} => {
// TODO: Eventually remove this document upgrade code
// This big code block contains lots of hacky code for upgrading old documents to the new format
// Upgrade a document being opened to use fresh copies of all nodes
let replace_implementations_from_definition = reset_node_definitions_on_open || document_serialized_content.contains("node_output_index");
// Upgrade layer implementation from https://github.com/GraphiteEditor/Graphite/pull/1946 (see also `fn fix_nodes()` in `main.rs` of Graphene CLI)
let upgrade_from_before_returning_nested_click_targets =
document_serialized_content.contains("graphene_core::ConstructLayerNode") || document_serialized_content.contains("graphene_core::AddArtboardNode");
let upgrade_vector_manipulation_format = document_serialized_content.contains("ManipulatorGroupIds") && !document_name.contains("__DO_NOT_UPGRADE__");
let document_name = document_name.replace("__DO_NOT_UPGRADE__", "");
const TEXT_REPLACEMENTS: [(&str, &str); 2] = [
("graphene_core::vector::vector_nodes::SamplePointsNode", "graphene_core::vector::SamplePointsNode"),
("graphene_core::vector::vector_nodes::SubpathSegmentLengthsNode", "graphene_core::vector::SubpathSegmentLengthsNode"),
];
let document_serialized_content = TEXT_REPLACEMENTS
.iter()
.fold(document_serialized_content, |document_serialized_content, (old, new)| document_serialized_content.replace(old, new));
// Upgrade the document being opened to use fresh copies of all nodes
let reset_node_definitions_on_open = reset_node_definitions_on_open || document_migration_reset_node_definition(&document_serialized_content);
// Upgrade the document being opened with string replacements on the original JSON
let document_serialized_content = document_migration_string_preprocessing(document_serialized_content);
// Deserialize the document
let document = DocumentMessageHandler::deserialize_document(&document_serialized_content).map(|mut document| {
document.name.clone_from(&document_name);
document
});
// Display an error to the user if the document could not be opened
let mut document = match document {
Ok(document) => document,
Err(e) => {
@@ -467,516 +423,14 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
}
};
const REPLACEMENTS: [(&str, &str); 36] = [
("graphene_core::AddArtboardNode", "graphene_core::graphic_element::AppendArtboardNode"),
("graphene_core::ConstructArtboardNode", "graphene_core::graphic_element::ToArtboardNode"),
("graphene_core::ToGraphicElementNode", "graphene_core::graphic_element::ToElementNode"),
("graphene_core::ToGraphicGroupNode", "graphene_core::graphic_element::ToGroupNode"),
("graphene_core::logic::LogicAndNode", "graphene_core::ops::LogicAndNode"),
("graphene_core::logic::LogicNotNode", "graphene_core::ops::LogicNotNode"),
("graphene_core::logic::LogicOrNode", "graphene_core::ops::LogicOrNode"),
("graphene_core::ops::ConstructVector2", "graphene_core::ops::Vector2ValueNode"),
("graphene_core::raster::BlackAndWhiteNode", "graphene_core::raster::adjustments::BlackAndWhiteNode"),
("graphene_core::raster::BlendNode", "graphene_core::raster::adjustments::BlendNode"),
("graphene_core::raster::ChannelMixerNode", "graphene_core::raster::adjustments::ChannelMixerNode"),
("graphene_core::raster::adjustments::ColorOverlayNode", "graphene_core::raster::adjustments::ColorOverlayNode"),
("graphene_core::raster::ExposureNode", "graphene_core::raster::adjustments::ExposureNode"),
("graphene_core::raster::ExtractChannelNode", "graphene_core::raster::adjustments::ExtractChannelNode"),
("graphene_core::raster::GradientMapNode", "graphene_core::raster::adjustments::GradientMapNode"),
("graphene_core::raster::HueSaturationNode", "graphene_core::raster::adjustments::HueSaturationNode"),
("graphene_core::raster::InvertNode", "graphene_core::raster::adjustments::InvertNode"),
// ("graphene_core::raster::IndexNode", "graphene_core::raster::adjustments::IndexNode"),
("graphene_core::raster::InvertRGBNode", "graphene_core::raster::adjustments::InvertNode"),
("graphene_core::raster::LevelsNode", "graphene_core::raster::adjustments::LevelsNode"),
("graphene_core::raster::LuminanceNode", "graphene_core::raster::adjustments::LuminanceNode"),
("graphene_core::raster::ExtractOpaqueNode", "graphene_core::raster::adjustments::MakeOpaqueNode"),
("graphene_core::raster::PosterizeNode", "graphene_core::raster::adjustments::PosterizeNode"),
("graphene_core::raster::ThresholdNode", "graphene_core::raster::adjustments::ThresholdNode"),
("graphene_core::raster::VibranceNode", "graphene_core::raster::adjustments::VibranceNode"),
("graphene_core::text::TextGeneratorNode", "graphene_core::text::TextNode"),
("graphene_core::transform::SetTransformNode", "graphene_core::transform::ReplaceTransformNode"),
("graphene_core::vector::SplinesFromPointsNode", "graphene_core::vector::SplineNode"),
("graphene_core::vector::generator_nodes::EllipseGenerator", "graphene_core::vector::generator_nodes::EllipseNode"),
("graphene_core::vector::generator_nodes::LineGenerator", "graphene_core::vector::generator_nodes::LineNode"),
("graphene_core::vector::generator_nodes::RectangleGenerator", "graphene_core::vector::generator_nodes::RectangleNode"),
(
"graphene_core::vector::generator_nodes::RegularPolygonGenerator",
"graphene_core::vector::generator_nodes::RegularPolygonNode",
),
("graphene_core::vector::generator_nodes::StarGenerator", "graphene_core::vector::generator_nodes::StarNode"),
("graphene_std::executor::BlendGpuImageNode", "graphene_std::gpu_nodes::BlendGpuImageNode"),
("graphene_std::raster::SampleNode", "graphene_std::raster::SampleImageNode"),
("graphene_core::transform::CullNode", "graphene_core::ops::IdentityNode"),
("graphene_std::raster::MaskImageNode", "graphene_std::raster::MaskNode"),
];
let mut network = document.network_interface.document_network().clone();
network.generate_node_paths(&[]);
let node_ids: Vec<_> = network.recursive_nodes().map(|(&id, node)| (id, node.original_location.path.clone().unwrap())).collect();
// Apply upgrades to each node
for (node_id, path) in &node_ids {
let network_path: Vec<_> = path.iter().copied().take(path.len() - 1).collect();
if let Some(DocumentNodeImplementation::ProtoNode(protonode_id)) = document
.network_interface
.nested_network(&network_path)
.unwrap()
.nodes
.get(node_id)
.map(|node| node.implementation.clone())
{
for (old, new) in REPLACEMENTS {
let node_path_without_type_args = protonode_id.name.split('<').next();
if node_path_without_type_args == Some(old) {
document
.network_interface
.replace_implementation(node_id, &network_path, DocumentNodeImplementation::ProtoNode(new.to_string().into()));
document.network_interface.set_manual_compostion(node_id, &network_path, Some(graph_craft::Type::Generic("T".into())));
}
}
}
}
// Upgrade all old nodes to support editable subgraphs introduced in #1750
if replace_implementations_from_definition || upgrade_from_before_returning_nested_click_targets {
// This can be used, if uncommented, to upgrade demo artwork with outdated document node internals from their definitions. Delete when it's no longer needed.
// Used for upgrading old internal networks for demo artwork nodes. Will reset all node internals for any opened file
for node_id in &document
.network_interface
.document_network_metadata()
.persistent_metadata
.node_metadata
.keys()
.cloned()
.collect::<Vec<NodeId>>()
{
if let Some(reference) = document
.network_interface
.document_network_metadata()
.persistent_metadata
.node_metadata
.get(node_id)
.and_then(|node| node.persistent_metadata.reference.as_ref())
{
let Some(node_definition) = resolve_document_node_type(reference) else { continue };
let default_definition_node = node_definition.default_node_template();
document.network_interface.replace_implementation(node_id, &[], default_definition_node.document_node.implementation);
document
.network_interface
.replace_implementation_metadata(node_id, &[], default_definition_node.persistent_node_metadata);
document.network_interface.set_manual_compostion(node_id, &[], default_definition_node.document_node.manual_composition);
}
}
}
if document
.network_interface
.document_network_metadata()
.persistent_metadata
.node_metadata
.iter()
.any(|(node_id, node)| node.persistent_metadata.reference.as_ref().is_some_and(|reference| reference == "Output") && *node_id == NodeId(0))
{
document.network_interface.delete_nodes(vec![NodeId(0)], true, &[]);
}
let mut network = document.network_interface.document_network().clone();
network.generate_node_paths(&[]);
let node_ids: Vec<_> = network.recursive_nodes().map(|(&id, node)| (id, node.original_location.path.clone().unwrap())).collect();
// Apply upgrades to each node
for (node_id, path) in &node_ids {
let network_path: Vec<_> = path.iter().copied().take(path.len() - 1).collect();
let network_path = &network_path;
let Some(node) = document.network_interface.nested_network(network_path).unwrap().nodes.get(node_id).cloned() else {
log::error!("could not get node in deserialize_document");
continue;
};
// Upgrade old nodes to use `Context` instead of `()` or `Footprint` for manual composition
if node.manual_composition == Some(graph_craft::concrete!(())) || node.manual_composition == Some(graph_craft::concrete!(graphene_std::transform::Footprint)) {
document
.network_interface
.set_manual_compostion(node_id, network_path, graph_craft::concrete!(graphene_std::Context).into());
}
let Some(node_metadata) = document.network_interface.network_metadata(network_path).unwrap().persistent_metadata.node_metadata.get(node_id) else {
log::error!("could not get node metadata for node {node_id} in deserialize_document");
continue;
};
let Some(ref reference) = node_metadata.persistent_metadata.reference.clone() else {
// TODO: Investigate if this should be an expected case, because currently it runs hundreds of times normally.
// TODO: Either delete the commented out error below if this is normal, or fix the underlying issue if this is not expected.
// log::error!("could not get reference in deserialize_document");
continue;
};
let inputs_count = node.inputs.len();
// Upgrade Fill nodes to the format change in #1778
if reference == "Fill" && inputs_count == 8 {
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());
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
let Some(fill_type) = old_inputs[1].as_value().cloned() else { continue };
let TaggedValue::FillType(fill_type) = fill_type else { continue };
let Some(solid_color) = old_inputs[2].as_value().cloned() else { continue };
let TaggedValue::OptionalColor(solid_color) = solid_color else { continue };
let Some(gradient_type) = old_inputs[3].as_value().cloned() else { continue };
let TaggedValue::GradientType(gradient_type) = gradient_type else { continue };
let Some(start) = old_inputs[4].as_value().cloned() else { continue };
let TaggedValue::DVec2(start) = start else { continue };
let Some(end) = old_inputs[5].as_value().cloned() else { continue };
let TaggedValue::DVec2(end) = end else { continue };
let Some(transform) = old_inputs[6].as_value().cloned() else { continue };
let TaggedValue::DAffine2(transform) = transform else { continue };
let Some(positions) = old_inputs[7].as_value().cloned() else { continue };
let TaggedValue::GradientStops(positions) = positions else { continue };
let fill = match (fill_type, solid_color) {
(FillType::Solid, None) => Fill::None,
(FillType::Solid, Some(color)) => Fill::Solid(color),
(FillType::Gradient, _) => Fill::Gradient(Gradient {
stops: positions,
gradient_type,
start,
end,
transform,
}),
};
document
.network_interface
.set_input(&InputConnector::node(*node_id, 1), NodeInput::value(TaggedValue::Fill(fill.clone()), false), network_path);
match fill {
Fill::None => {
document
.network_interface
.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::OptionalColor(None), false), network_path);
}
Fill::Solid(color) => {
document
.network_interface
.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::OptionalColor(Some(color)), false), network_path);
}
Fill::Gradient(gradient) => {
document
.network_interface
.set_input(&InputConnector::node(*node_id, 3), NodeInput::value(TaggedValue::Gradient(gradient), false), network_path);
}
}
}
// Rename the old "Splines from Points" node to "Spline" and upgrade it to the new "Spline" node
if reference == "Splines from Points" {
document.network_interface.set_reference(node_id, network_path, Some("Spline".to_string()));
}
// Upgrade the old "Spline" node to the new "Spline" node
if reference == "Spline" {
// Retrieve the proto node identifier and verify it is the old "Spline" node, otherwise skip it if this is the new "Spline" node
let identifier = document
.network_interface
.implementation(node_id, network_path)
.and_then(|implementation| implementation.get_proto_node());
if identifier.map(|identifier| &identifier.name) != Some(&"graphene_core::vector::generator_nodes::SplineNode".into()) {
continue;
}
// Obtain the document node for the given node ID, extract the vector points, and create vector data from the list of points
let node = document.network_interface.document_node(node_id, network_path).unwrap();
let Some(TaggedValue::VecDVec2(points)) = node.inputs.get(1).and_then(|tagged_value| tagged_value.as_value()) else {
log::error!("The old Spline node's input at index 1 is not a TaggedValue::VecDVec2");
continue;
};
let vector_data = VectorData::from_subpath(Subpath::from_anchors_linear(points.to_vec(), false));
// Retrieve the output connectors linked to the "Spline" node's output port
let spline_outputs = document
.network_interface
.outward_wires(network_path)
.unwrap()
.get(&OutputConnector::node(*node_id, 0))
.expect("Vec of InputConnector Spline node is connected to its output port 0.")
.clone();
// Get the node's current position in the graph
let Some(node_position) = document.network_interface.position(node_id, network_path) else {
log::error!("Could not get position of spline node.");
continue;
};
// Get the "Path" node definition and fill it in with the vector data and default vector modification
let path_node_type = resolve_document_node_type("Path").expect("Path node does not exist.");
let path_node = path_node_type.node_template_input_override([
Some(NodeInput::value(TaggedValue::VectorData(VectorDataTable::new(vector_data)), true)),
Some(NodeInput::value(TaggedValue::VectorModification(Default::default()), false)),
]);
// Get the "Spline" node definition and wire it up with the "Path" node as input
let spline_node_type = resolve_document_node_type("Spline").expect("Spline node does not exist.");
let spline_node = spline_node_type.node_template_input_override([Some(NodeInput::node(NodeId(1), 0))]);
// Create a new node group with the "Path" and "Spline" nodes and generate new node IDs for them
let nodes = vec![(NodeId(1), path_node), (NodeId(0), spline_node)];
let new_ids = nodes.iter().map(|(id, _)| (*id, NodeId::new())).collect::<HashMap<_, _>>();
let new_spline_id = *new_ids.get(&NodeId(0)).unwrap();
let new_path_id = *new_ids.get(&NodeId(1)).unwrap();
// Remove the old "Spline" node from the document
document.network_interface.delete_nodes(vec![*node_id], false, network_path);
// Insert the new "Path" and "Spline" nodes into the network interface with generated IDs
document.network_interface.insert_node_group(nodes.clone(), new_ids, network_path);
// Reposition the new "Spline" node to match the original "Spline" node's position
document.network_interface.shift_node(&new_spline_id, node_position, network_path);
// Reposition the new "Path" node with an offset relative to the original "Spline" node's position
document.network_interface.shift_node(&new_path_id, node_position + IVec2::new(-7, 0), network_path);
// Redirect each output connection from the old node to the new "Spline" node's output port
for input_connector in spline_outputs {
document.network_interface.set_input(&input_connector, NodeInput::node(new_spline_id, 0), network_path);
}
}
// 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 {
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());
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 3), old_inputs[3].clone(), network_path);
document.network_interface.set_input(
&InputConnector::node(*node_id, 4),
if inputs_count == 6 {
old_inputs[4].clone()
} else {
NodeInput::value(TaggedValue::F64(TypesettingConfig::default().line_height_ratio), false)
},
network_path,
);
document.network_interface.set_input(
&InputConnector::node(*node_id, 5),
if inputs_count == 6 {
old_inputs[5].clone()
} else {
NodeInput::value(TaggedValue::F64(TypesettingConfig::default().character_spacing), false)
},
network_path,
);
document.network_interface.set_input(
&InputConnector::node(*node_id, 6),
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),
network_path,
);
}
// Upgrade Sine, Cosine, and Tangent nodes to include a boolean input for whether the output should be in radians, which was previously the only option but is now not the default
if (reference == "Sine" || reference == "Cosine" || reference == "Tangent") && inputs_count == 1 {
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());
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document
.network_interface
.set_input(&InputConnector::node(*node_id, 1), NodeInput::value(TaggedValue::Bool(true), false), network_path);
}
// Upgrade the Modulo node to include a boolean input for whether the output should be always positive, which was previously not an option
if reference == "Modulo" && inputs_count == 2 {
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());
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
document
.network_interface
.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::Bool(false), false), network_path);
}
// Upgrade the Mirror node to add the `keep_original` boolean input
if reference == "Mirror" && inputs_count == 3 {
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());
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path);
document
.network_interface
.set_input(&InputConnector::node(*node_id, 3), NodeInput::value(TaggedValue::Bool(true), false), network_path);
}
// Upgrade the Mirror node to add the `reference_point` input and change `offset` from `DVec2` to `f64`
if reference == "Mirror" && inputs_count == 4 {
let node_definition = resolve_document_node_type(reference).unwrap();
let new_node_template = node_definition.default_node_template();
let document_node = new_node_template.document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
document
.network_interface
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
let Some(&TaggedValue::DVec2(old_offset)) = old_inputs[1].as_value() else { return };
let old_offset = if old_offset.x.abs() > old_offset.y.abs() { old_offset.x } else { old_offset.y };
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(
&InputConnector::node(*node_id, 1),
NodeInput::value(TaggedValue::ReferencePoint(graphene_std::transform::ReferencePoint::Center), false),
network_path,
);
document
.network_interface
.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::F64(old_offset), false), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 3), old_inputs[2].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 4), old_inputs[3].clone(), network_path);
}
// Upgrade artboard name being passed as hidden value input to "To Artboard"
if reference == "Artboard" && upgrade_from_before_returning_nested_click_targets {
let label = document.network_interface.display_name(node_id, network_path);
document
.network_interface
.set_input(&InputConnector::node(NodeId(0), 1), NodeInput::value(TaggedValue::String(label), false), &[*node_id]);
}
if reference == "Image" && inputs_count == 1 {
let node_definition = crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type(reference).unwrap();
let new_image_node = node_definition.default_node_template();
document.network_interface.replace_implementation(node_id, network_path, new_image_node.document_node.implementation);
// Insert a new empty input for the image
document.network_interface.add_import(TaggedValue::None, false, 0, "Empty", "", &[*node_id]);
document.network_interface.set_reference(node_id, network_path, Some("Image".to_string()));
}
if reference == "Noise Pattern" && inputs_count == 15 {
let node_definition = crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type(reference).unwrap();
let new_noise_pattern_node = node_definition.default_node_template();
document
.network_interface
.replace_implementation(node_id, network_path, new_noise_pattern_node.document_node.implementation);
let old_inputs = document.network_interface.replace_inputs(node_id, new_noise_pattern_node.document_node.inputs.clone(), network_path);
document
.network_interface
.set_input(&InputConnector::node(*node_id, 0), NodeInput::value(TaggedValue::None, false), network_path);
for (i, input) in old_inputs.iter().enumerate() {
document.network_interface.set_input(&InputConnector::node(*node_id, i + 1), input.clone(), network_path);
}
}
if reference == "Instance on Points" && inputs_count == 2 {
let node_definition = resolve_document_node_type(reference).unwrap();
let new_node_template = node_definition.default_node_template();
let document_node = new_node_template.document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
document
.network_interface
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
}
if reference == "Morph" && inputs_count == 4 {
let node_definition = resolve_document_node_type(reference).unwrap();
let new_node_template = node_definition.default_node_template();
let document_node = new_node_template.document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
document
.network_interface
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path);
// We have removed the last input, so we don't add index 3
}
}
// TODO: Eventually remove this document upgrade code
// Upgrade document to the new vector manipulation format introduced in #1676
let document_serialized_content = document.serialize_document();
if upgrade_vector_manipulation_format && !document_serialized_content.is_empty() {
responses.add(FrontendMessage::TriggerUpgradeDocumentToVectorManipulationFormat {
document_id,
document_name,
document_is_auto_saved,
document_is_saved,
document_serialized_content,
});
return;
}
// Ensure layers are positioned as stacks if they upstream siblings of another layer
document.network_interface.load_structure();
let all_layers = LayerNodeIdentifier::ROOT_PARENT.descendants(document.network_interface.document_metadata()).collect::<Vec<_>>();
for layer in all_layers {
let Some((downstream_node, input_index)) = document
.network_interface
.outward_wires(&[])
.and_then(|outward_wires| outward_wires.get(&OutputConnector::node(layer.to_node(), 0)))
.and_then(|outward_wires| outward_wires.first())
.and_then(|input_connector| input_connector.node_id().map(|node_id| (node_id, input_connector.input_index())))
else {
continue;
};
// If the downstream node is a layer and the input is the first input and the current layer is not in a stack
if input_index == 0 && document.network_interface.is_layer(&downstream_node, &[]) && !document.network_interface.is_stack(&layer.to_node(), &[]) {
// Ensure the layer is horizontally aligned with the downstream layer to prevent changing the layout of old files
let (Some(layer_position), Some(downstream_position)) =
(document.network_interface.position(&layer.to_node(), &[]), document.network_interface.position(&downstream_node, &[]))
else {
log::error!("Could not get position for layer {:?} or downstream node {} when opening file", layer.to_node(), downstream_node);
continue;
};
if layer_position.x == downstream_position.x {
document.network_interface.set_stack_position_calculated_offset(&layer.to_node(), &downstream_node, &[]);
}
}
}
// Upgrade the document's nodes to be compatible with the latest version
document_migration_upgrades(&mut document, reset_node_definitions_on_open);
// Set the save state of the document based on what's given to us by the caller to this message
document.set_auto_save_state(document_is_auto_saved);
document.set_save_state(document_is_saved);
// Load the document into the portfolio so it opens in the editor
self.load_document(document, document_id, responses, to_front);
}
PortfolioMessage::PasteIntoFolder { clipboard, parent, insert_index } => {
@@ -1444,7 +898,7 @@ impl PortfolioMessageHandler {
responses.add(ToolMessage::DeactivateTools);
} else {
// Load the default font upon creating the first document
let font = Font::new(graphene_core::consts::DEFAULT_FONT_FAMILY.into(), graphene_core::consts::DEFAULT_FONT_STYLE.into());
let font = Font::new(graphene_std::consts::DEFAULT_FONT_FAMILY.into(), graphene_std::consts::DEFAULT_FONT_STYLE.into());
responses.add(FrontendMessage::TriggerFontLoad { font });
}
@@ -3,12 +3,14 @@ use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup,
use crate::messages::prelude::*;
use crate::messages::tool::tool_messages::tool_prelude::*;
use graph_craft::document::NodeId;
use graphene_core::Context;
use graphene_core::GraphicGroupTable;
use graphene_core::instances::Instances;
use graphene_core::memo::IORecord;
use graphene_core::vector::{VectorData, VectorDataTable};
use graphene_core::{Artboard, ArtboardGroupTable, GraphicElement};
use graphene_std::Color;
use graphene_std::Context;
use graphene_std::GraphicGroupTable;
use graphene_std::instances::Instances;
use graphene_std::memo::IORecord;
use graphene_std::raster::Image;
use graphene_std::vector::{VectorData, VectorDataTable};
use graphene_std::{Artboard, ArtboardGroupTable, GraphicElement};
use std::any::Any;
use std::sync::Arc;
@@ -154,7 +156,8 @@ impl InstanceLayout for GraphicElement {
match self {
Self::GraphicGroup(instances) => instances.identifier(),
Self::VectorData(instances) => instances.identifier(),
Self::RasterFrame(_) => "RasterFrame".to_string(),
Self::RasterDataCPU(_) => "RasterDataCPU".to_string(),
Self::RasterDataGPU(_) => "RasterDataGPU".to_string(),
}
}
// Don't put a breadcrumb for GraphicElement
@@ -165,7 +168,8 @@ impl InstanceLayout for GraphicElement {
match self {
Self::GraphicGroup(instances) => instances.layout_with_breadcrumb(data),
Self::VectorData(instances) => instances.layout_with_breadcrumb(data),
Self::RasterFrame(_) => label("Raster frame not supported"),
Self::RasterDataCPU(_) => label("Raster frame not supported"),
Self::RasterDataGPU(_) => label("Raster frame not supported"),
}
}
}
@@ -178,19 +182,42 @@ impl InstanceLayout for VectorData {
format!("Vector Data (points={}, segments={})", self.point_domain.ids().len(), self.segment_domain.ids().len())
}
fn compute_layout(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
let mut rows = Vec::new();
let colinear = self.colinear_manipulators.iter().map(|[a, b]| format!("[{a} / {b}]")).collect::<Vec<_>>().join(", ");
let colinear = if colinear.is_empty() { "None" } else { &colinear };
let style = vec![
TextLabel::new(format!(
"{}\n\nColinear Handle IDs: {}\n\nUpstream Graphic Group Table: {}",
self.style,
colinear,
if self.upstream_graphic_group.is_some() { "Yes" } else { "No" }
))
.multiline(true)
.widget_holder(),
];
let domain_entries = [VectorDataDomain::Points, VectorDataDomain::Segments, VectorDataDomain::Regions]
.into_iter()
.map(|domain| {
RadioEntryData::new(format!("{domain:?}"))
.label(format!("{domain:?}"))
.on_update(move |_| SpreadsheetMessage::ViewVectorDataDomain { domain }.into())
})
.collect();
let domain = vec![RadioInput::new(domain_entries).selected_index(Some(data.vector_data_domain as u32)).widget_holder()];
let mut table_rows = Vec::new();
match data.vector_data_domain {
VectorDataDomain::Points => {
rows.push(column_headings(&["", "position"]));
rows.extend(
table_rows.push(column_headings(&["", "position"]));
table_rows.extend(
self.point_domain
.iter()
.map(|(id, position)| vec![TextLabel::new(format!("{}", id.inner())).widget_holder(), TextLabel::new(format!("{}", position)).widget_holder()]),
);
}
VectorDataDomain::Segments => {
rows.push(column_headings(&["", "start_index", "end_index", "handles"]));
rows.extend(self.segment_domain.iter().map(|(id, start, end, handles)| {
table_rows.push(column_headings(&["", "start_index", "end_index", "handles"]));
table_rows.extend(self.segment_domain.iter().map(|(id, start, end, handles)| {
vec![
TextLabel::new(format!("{}", id.inner())).widget_holder(),
TextLabel::new(format!("{}", start)).widget_holder(),
@@ -200,8 +227,8 @@ impl InstanceLayout for VectorData {
}));
}
VectorDataDomain::Regions => {
rows.push(column_headings(&["", "segment_range", "fill"]));
rows.extend(self.region_domain.iter().map(|(id, segment_range, fill)| {
table_rows.push(column_headings(&["", "segment_range", "fill"]));
table_rows.extend(self.region_domain.iter().map(|(id, segment_range, fill)| {
vec![
TextLabel::new(format!("{}", id.inner())).widget_holder(),
TextLabel::new(format!("{:?}", segment_range)).widget_holder(),
@@ -211,17 +238,20 @@ impl InstanceLayout for VectorData {
}
}
let entries = [VectorDataDomain::Points, VectorDataDomain::Segments, VectorDataDomain::Regions]
.into_iter()
.map(|domain| {
RadioEntryData::new(format!("{domain:?}"))
.label(format!("{domain:?}"))
.on_update(move |_| SpreadsheetMessage::ViewVectorDataDomain { domain }.into())
})
.collect();
vec![LayoutGroup::Row { widgets: style }, LayoutGroup::Row { widgets: domain }, LayoutGroup::Table { rows: table_rows }]
}
}
let domain = vec![RadioInput::new(entries).selected_index(Some(data.vector_data_domain as u32)).widget_holder()];
vec![LayoutGroup::Row { widgets: domain }, LayoutGroup::Table { rows }]
impl InstanceLayout for Image<Color> {
fn type_name() -> &'static str {
"Image"
}
fn identifier(&self) -> String {
format!("Image (width={}, height={})", self.width, self.height)
}
fn compute_layout(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let rows = vec![vec![TextLabel::new(format!("Image (width={}, height={})", self.width, self.height)).widget_holder()]];
vec![LayoutGroup::Table { rows }]
}
}
@@ -261,13 +291,23 @@ impl<T: InstanceLayout> InstanceLayout for Instances<T> {
.instance_ref_iter()
.enumerate()
.map(|(index, instance)| {
let (scale, angle, translation) = instance.transform.to_scale_angle_translation();
let rotation = if angle == -0. { 0. } else { angle.to_degrees() };
let round = |x: f64| (x * 1e3).round() / 1e3;
vec![
TextLabel::new(format!("{}", index)).widget_holder(),
TextButton::new(instance.instance.identifier())
.on_update(move |_| SpreadsheetMessage::PushToInstancePath { index }.into())
.widget_holder(),
TextLabel::new(format!("{}", instance.transform)).widget_holder(),
TextLabel::new(format!("{:?}", instance.alpha_blending)).widget_holder(),
TextLabel::new(format!(
"Location: ({} px, {} px) — Rotation: {rotation:2}° — Scale: ({}x, {}x)",
round(translation.x),
round(translation.y),
round(scale.x),
round(scale.y)
))
.widget_holder(),
TextLabel::new(format!("{}", instance.alpha_blending)).widget_holder(),
TextLabel::new(instance.source_node_id.map_or_else(|| "-".to_string(), |id| format!("{}", id.0))).widget_holder(),
]
})
@@ -4,7 +4,6 @@ use graphene_std::text::FontCache;
pub struct PersistentData {
pub font_cache: FontCache,
pub use_vello: bool,
// pub imaginate: ImaginatePersistentData,
}
#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, serde::Serialize, serde::Deserialize)]
@@ -16,6 +16,4 @@ pub enum PreferencesMessage {
ModifyLayout { zoom_with_scroll: bool },
GraphWireStyle { style: GraphWireStyle },
ViewportZoomWheelRate { rate: f64 },
// ImaginateRefreshFrequency { seconds: f64 },
// ImaginateServerHostname { hostname: String },
}
@@ -7,8 +7,6 @@ use graph_craft::wasm_application_io::EditorPreferences;
#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct PreferencesMessageHandler {
// pub imaginate_server_hostname: String,
// pub imaginate_refresh_frequency: f64,
pub selection_mode: SelectionMode,
pub zoom_with_scroll: bool,
pub use_vello: bool,
@@ -24,7 +22,6 @@ impl PreferencesMessageHandler {
pub fn editor_preferences(&self) -> EditorPreferences {
EditorPreferences {
// imaginate_hostname: self.imaginate_server_hostname.clone(),
use_vello: self.use_vello && self.supports_wgpu(),
}
}
@@ -37,8 +34,6 @@ impl PreferencesMessageHandler {
impl Default for PreferencesMessageHandler {
fn default() -> Self {
Self {
// imaginate_server_hostname: EditorPreferences::default().imaginate_hostname,
// imaginate_refresh_frequency: 1.,
selection_mode: SelectionMode::Touched,
zoom_with_scroll: matches!(MappingVariant::default(), MappingVariant::ZoomWithScroll),
use_vello: EditorPreferences::default().use_vello,
@@ -57,10 +52,6 @@ impl MessageHandler<PreferencesMessage, ()> for PreferencesMessageHandler {
if let Ok(deserialized_preferences) = serde_json::from_str::<PreferencesMessageHandler>(&preferences) {
*self = deserialized_preferences;
// TODO: Reenable when Imaginate is restored
// responses.add(PortfolioMessage::ImaginateServerHostname);
// responses.add(PortfolioMessage::ImaginateCheckServerStatus);
responses.add(PortfolioMessage::EditorPreferences);
responses.add(PortfolioMessage::UpdateVelloPreference);
responses.add(PreferencesMessage::ModifyLayout {
@@ -101,27 +92,6 @@ impl MessageHandler<PreferencesMessage, ()> for PreferencesMessageHandler {
self.viewport_zoom_wheel_rate = rate;
}
}
// TODO: Reenable when Imaginate is restored (and move back up one line since the auto-formatter doesn't like it in that block)
// PreferencesMessage::ImaginateRefreshFrequency { seconds } => {
// self.imaginate_refresh_frequency = seconds;
// responses.add(PortfolioMessage::ImaginateCheckServerStatus);
// responses.add(PortfolioMessage::EditorPreferences);
// }
// PreferencesMessage::ImaginateServerHostname { hostname } => {
// let initial = hostname.clone();
// let has_protocol = hostname.starts_with("http://") || hostname.starts_with("https://");
// let hostname = if has_protocol { hostname } else { "http://".to_string() + &hostname };
// let hostname = if hostname.ends_with('/') { hostname } else { hostname + "/" };
// if hostname != initial {
// refresh_dialog(responses);
// }
// self.imaginate_server_hostname = hostname;
// responses.add(PortfolioMessage::ImaginateServerHostname);
// responses.add(PortfolioMessage::ImaginateCheckServerStatus);
// responses.add(PortfolioMessage::EditorPreferences);
//}
responses.add(FrontendMessage::TriggerSavePreferences { preferences: self.clone() });
}
+1 -5
View File
@@ -34,19 +34,15 @@ pub use crate::messages::broadcast::broadcast_event::{BroadcastEvent, BroadcastE
pub use crate::messages::message::{Message, MessageDiscriminant};
pub use crate::messages::tool::tool_messages::artboard_tool::{ArtboardToolMessage, ArtboardToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::brush_tool::{BrushToolMessage, BrushToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::ellipse_tool::{EllipseToolMessage, EllipseToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::eyedropper_tool::{EyedropperToolMessage, EyedropperToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::fill_tool::{FillToolMessage, FillToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::freehand_tool::{FreehandToolMessage, FreehandToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::gradient_tool::{GradientToolMessage, GradientToolMessageDiscriminant};
// pub use crate::messages::tool::tool_messages::imaginate_tool::{ImaginateToolMessage, ImaginateToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::line_tool::{LineToolMessage, LineToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::navigate_tool::{NavigateToolMessage, NavigateToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::path_tool::{PathToolMessage, PathToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::pen_tool::{PenToolMessage, PenToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::polygon_tool::{PolygonToolMessage, PolygonToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::rectangle_tool::{RectangleToolMessage, RectangleToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::select_tool::{SelectToolMessage, SelectToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::shape_tool::{ShapeToolMessage, ShapeToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::spline_tool::{SplineToolMessage, SplineToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::text_tool::{TextToolMessage, TextToolMessageDiscriminant};
@@ -1,7 +1,7 @@
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::prelude::*;
use graphene_core::Color;
use graphene_std::Color;
use graphene_std::vector::style::FillChoice;
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
@@ -60,14 +60,14 @@ impl ToolColorOptions {
pub fn apply_fill(&self, layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) {
if let Some(color) = self.active_color() {
let fill = graphene_core::vector::style::Fill::solid(color.to_gamma_srgb());
let fill = graphene_std::vector::style::Fill::solid(color.to_gamma_srgb());
responses.add(GraphOperationMessage::FillSet { layer, fill });
}
}
pub fn apply_stroke(&self, weight: f64, layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) {
if let Some(color) = self.active_color() {
let stroke = graphene_core::vector::style::Stroke::new(Some(color.to_gamma_srgb()), weight);
let stroke = graphene_std::vector::style::Stroke::new(Some(color.to_gamma_srgb()), weight);
responses.add(GraphOperationMessage::StrokeSet { layer, stroke });
}
}
@@ -0,0 +1,246 @@
use crate::messages::message::Message;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::prelude::{DocumentMessageHandler, InputPreprocessorMessageHandler};
use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::shape_editor::ShapeState;
use crate::messages::tool::common_functionality::shapes::polygon_shape::PolygonGizmoHandler;
use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeGizmoHandler;
use crate::messages::tool::common_functionality::shapes::star_shape::StarGizmoHandler;
use glam::DVec2;
use std::collections::VecDeque;
/// A unified enum wrapper around all available shape-specific gizmo handlers.
///
/// This abstraction allows `GizmoManager` to interact with different shape gizmos (like Star or Polygon)
/// using a common interface without needing to know the specific shape type at compile time.
///
/// Each variant stores a concrete handler (e.g., `StarGizmoHandler`, `PolygonGizmoHandler`) that implements
/// the shape-specific logic for rendering overlays, responding to input, and modifying shape parameters.
#[derive(Clone, Debug, Default)]
pub enum ShapeGizmoHandlers {
#[default]
None,
Star(StarGizmoHandler),
Polygon(PolygonGizmoHandler),
}
impl ShapeGizmoHandlers {
/// Returns the kind of shape the handler is managing, such as `"star"` or `"polygon"`.
/// Used for grouping logic and distinguishing between handler types at runtime.
pub fn kind(&self) -> &'static str {
match self {
Self::Star(_) => "star",
Self::Polygon(_) => "polygon",
Self::None => "none",
}
}
/// Dispatches interaction state updates to the corresponding shape-specific handler.
pub fn handle_state(&mut self, layer: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
match self {
Self::Star(h) => h.handle_state(layer, mouse_position, document, responses),
Self::Polygon(h) => h.handle_state(layer, mouse_position, document, responses),
Self::None => {}
}
}
/// Checks if any interactive part of the gizmo is currently hovered.
pub fn is_any_gizmo_hovered(&self) -> bool {
match self {
Self::Star(h) => h.is_any_gizmo_hovered(),
Self::Polygon(h) => h.is_any_gizmo_hovered(),
Self::None => false,
}
}
/// Passes the click interaction to the appropriate gizmo handler if one is hovered.
pub fn handle_click(&mut self) {
match self {
Self::Star(h) => h.handle_click(),
Self::Polygon(h) => h.handle_click(),
Self::None => {}
}
}
/// Updates the gizmo state while the user is dragging a handle (e.g., adjusting radius).
pub fn handle_update(&mut self, drag_start: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
match self {
Self::Star(h) => h.handle_update(drag_start, document, input, responses),
Self::Polygon(h) => h.handle_update(drag_start, document, input, responses),
Self::None => {}
}
}
/// Cleans up any state used by the gizmo handler.
pub fn cleanup(&mut self) {
match self {
Self::Star(h) => h.cleanup(),
Self::Polygon(h) => h.cleanup(),
Self::None => {}
}
}
/// Draws overlays like control points or outlines for the shape handled by this gizmo.
pub fn overlays(
&self,
document: &DocumentMessageHandler,
layer: Option<LayerNodeIdentifier>,
input: &InputPreprocessorMessageHandler,
shape_editor: &mut &mut ShapeState,
mouse_position: DVec2,
overlay_context: &mut OverlayContext,
) {
match self {
Self::Star(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context),
Self::Polygon(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context),
Self::None => {}
}
}
/// Draws live-updating overlays during drag interactions for the shape handled by this gizmo.
pub fn dragging_overlays(
&self,
document: &DocumentMessageHandler,
input: &InputPreprocessorMessageHandler,
shape_editor: &mut &mut ShapeState,
mouse_position: DVec2,
overlay_context: &mut OverlayContext,
) {
match self {
Self::Star(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context),
Self::Polygon(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context),
Self::None => {}
}
}
}
/// Central manager that coordinates shape gizmo handlers for interactive editing on the canvas.
///
/// The `GizmoManager` is responsible for detecting which shapes are selected, activating the appropriate
/// shape-specific gizmo, and routing user interactions (hover, click, drag) to the correct handler.
/// It allows editing multiple shapes of the same type or focusing on a single active shape when a gizmo is hovered.
///
/// ## Responsibilities:
/// - Detect which selected layers support shape gizmos (e.g., stars, polygons)
/// - Activate the correct handler and manage state between frames
/// - Route click, hover, and drag events to the proper shape gizmo
/// - Render overlays and dragging visuals
#[derive(Clone, Debug, Default)]
pub struct GizmoManager {
active_shape_handler: Option<ShapeGizmoHandlers>,
layers_handlers: Vec<(ShapeGizmoHandlers, Vec<LayerNodeIdentifier>)>,
}
impl GizmoManager {
/// Detects and returns a shape gizmo handler based on the layer type (e.g., star, polygon).
///
/// Returns `None` if the given layer does not represent a shape with a registered gizmo.
pub fn detect_shape_handler(layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> Option<ShapeGizmoHandlers> {
// Star
if graph_modification_utils::get_star_id(layer, &document.network_interface).is_some() {
return Some(ShapeGizmoHandlers::Star(StarGizmoHandler::default()));
}
// Polygon
if graph_modification_utils::get_polygon_id(layer, &document.network_interface).is_some() {
return Some(ShapeGizmoHandlers::Polygon(PolygonGizmoHandler::default()));
}
None
}
/// Returns `true` if a gizmo is currently active (hovered or being interacted with).
pub fn hovering_over_gizmo(&self) -> bool {
self.active_shape_handler.is_some()
}
/// Called every frame to check selected layers and update the active shape gizmo, if hovered.
///
/// Also groups all shape layers with the same kind of gizmo to support overlays for multi-shape editing.
pub fn handle_actions(&mut self, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
let mut handlers_layer: Vec<(ShapeGizmoHandlers, Vec<LayerNodeIdentifier>)> = Vec::new();
for layer in document.network_interface.selected_nodes().selected_visible_and_unlocked_layers(&document.network_interface) {
if let Some(mut handler) = Self::detect_shape_handler(layer, document) {
handler.handle_state(layer, mouse_position, document, responses);
let is_hovered = handler.is_any_gizmo_hovered();
if is_hovered {
self.layers_handlers.clear();
self.active_shape_handler = Some(handler);
return;
}
// Try to group this handler with others of the same type
if let Some((_, layers)) = handlers_layer.iter_mut().find(|(existing_handler, _)| existing_handler.kind() == handler.kind()) {
layers.push(layer);
} else {
handlers_layer.push((handler, vec![layer]));
}
}
}
self.layers_handlers = handlers_layer;
self.active_shape_handler = None;
}
/// Handles click interactions if a gizmo is active. Returns `true` if a gizmo handled the click.
pub fn handle_click(&mut self) -> bool {
if let Some(handle) = &mut self.active_shape_handler {
handle.handle_click();
return true;
}
false
}
pub fn handle_cleanup(&mut self) {
if let Some(handle) = &mut self.active_shape_handler {
handle.cleanup();
}
}
/// Passes drag update data to the active gizmo to update shape parameters live.
pub fn handle_update(&mut self, drag_start: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
if let Some(handle) = &mut self.active_shape_handler {
handle.handle_update(drag_start, document, input, responses);
}
}
/// Draws overlays for the currently active shape gizmo during a drag interaction.
pub fn dragging_overlays(
&self,
document: &DocumentMessageHandler,
input: &InputPreprocessorMessageHandler,
shape_editor: &mut &mut ShapeState,
mouse_position: DVec2,
overlay_context: &mut OverlayContext,
) {
if let Some(handle) = &self.active_shape_handler {
handle.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context);
}
}
/// Draws overlays for either the active gizmo (if hovered) or all grouped selected gizmos.
///
/// If no single gizmo is active, it renders overlays for all grouped layers with associated handlers.
pub fn overlays(
&self,
document: &DocumentMessageHandler,
input: &InputPreprocessorMessageHandler,
shape_editor: &mut &mut ShapeState,
mouse_position: DVec2,
overlay_context: &mut OverlayContext,
) {
if let Some(handler) = &self.active_shape_handler {
handler.overlays(document, None, input, shape_editor, mouse_position, overlay_context);
return;
}
for (handler, selected_layers) in &self.layers_handlers {
for layer in selected_layers {
handler.overlays(document, Some(*layer), input, shape_editor, mouse_position, overlay_context);
}
}
}
}
@@ -0,0 +1,2 @@
pub mod gizmo_manager;
pub mod shape_gizmos;
@@ -0,0 +1,2 @@
pub mod number_of_points_dial;
pub mod point_radius_handle;
@@ -0,0 +1,209 @@
use crate::consts::{GIZMO_HIDE_THRESHOLD, NUMBER_OF_POINTS_DIAL_SPOKE_EXTENSION, NUMBER_OF_POINTS_DIAL_SPOKE_LENGTH, POINT_RADIUS_HANDLE_SEGMENT_THRESHOLD};
use crate::messages::frontend::utility_types::MouseCursorIcon;
use crate::messages::message::Message;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::InputConnector;
use crate::messages::prelude::Responses;
use crate::messages::prelude::{DocumentMessageHandler, FrontendMessage, InputPreprocessorMessageHandler, NodeGraphMessage};
use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::shape_editor::ShapeState;
use crate::messages::tool::common_functionality::shapes::shape_utility::{extract_polygon_parameters, inside_polygon, inside_star, polygon_outline, polygon_vertex_position, star_outline};
use crate::messages::tool::common_functionality::shapes::shape_utility::{extract_star_parameters, star_vertex_position};
use glam::{DAffine2, DVec2};
use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue;
use std::collections::VecDeque;
use std::f64::consts::TAU;
#[derive(Clone, Debug, Default, PartialEq)]
pub enum NumberOfPointsDialState {
#[default]
Inactive,
Hover,
Dragging,
}
#[derive(Clone, Debug, Default)]
pub struct NumberOfPointsDial {
pub layer: Option<LayerNodeIdentifier>,
pub initial_points: u32,
pub handle_state: NumberOfPointsDialState,
}
impl NumberOfPointsDial {
pub fn cleanup(&mut self) {
self.handle_state = NumberOfPointsDialState::Inactive;
self.layer = None;
}
pub fn update_state(&mut self, state: NumberOfPointsDialState) {
self.handle_state = state;
}
pub fn is_hovering(&self) -> bool {
self.handle_state == NumberOfPointsDialState::Hover
}
pub fn is_dragging(&self) -> bool {
self.handle_state == NumberOfPointsDialState::Dragging
}
pub fn handle_actions(&mut self, layer: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
match &self.handle_state {
NumberOfPointsDialState::Inactive => {
// Star
if let Some((sides, radius1, radius2)) = extract_star_parameters(Some(layer), document) {
let viewport = document.metadata().transform_to_viewport(layer);
let center = viewport.transform_point2(DVec2::ZERO);
let point_on_max_radius = star_vertex_position(viewport, 0, sides, radius1, radius2);
if mouse_position.distance(center) < NUMBER_OF_POINTS_DIAL_SPOKE_LENGTH && point_on_max_radius.distance(center) > GIZMO_HIDE_THRESHOLD {
self.layer = Some(layer);
self.initial_points = sides;
self.update_state(NumberOfPointsDialState::Hover);
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::EWResize });
}
}
// Polygon
if let Some((sides, radius)) = extract_polygon_parameters(Some(layer), document) {
let viewport = document.metadata().transform_to_viewport(layer);
let center = viewport.transform_point2(DVec2::ZERO);
let point_on_max_radius = polygon_vertex_position(viewport, 0, sides, radius);
if mouse_position.distance(center) < NUMBER_OF_POINTS_DIAL_SPOKE_LENGTH && point_on_max_radius.distance(center) > GIZMO_HIDE_THRESHOLD {
self.layer = Some(layer);
self.initial_points = sides;
self.update_state(NumberOfPointsDialState::Hover);
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::EWResize });
}
}
}
NumberOfPointsDialState::Hover | NumberOfPointsDialState::Dragging => {
let Some(layer) = self.layer else { return };
let viewport = document.metadata().transform_to_viewport(layer);
let center = viewport.transform_point2(DVec2::ZERO);
if mouse_position.distance(center) > NUMBER_OF_POINTS_DIAL_SPOKE_LENGTH && matches!(&self.handle_state, NumberOfPointsDialState::Hover) {
self.update_state(NumberOfPointsDialState::Inactive);
self.layer = None;
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default });
}
}
}
}
pub fn overlays(&self, document: &DocumentMessageHandler, layer: Option<LayerNodeIdentifier>, shape_editor: &mut &mut ShapeState, mouse_position: DVec2, overlay_context: &mut OverlayContext) {
match &self.handle_state {
NumberOfPointsDialState::Inactive => {
let Some(layer) = layer else { return };
// Star
if let Some((sides, radius1, radius2)) = extract_star_parameters(Some(layer), document) {
let radius = radius1.max(radius2);
let viewport = document.metadata().transform_to_viewport(layer);
let center = viewport.transform_point2(DVec2::ZERO);
if let Some(closest_segment) = shape_editor.upper_closest_segment(&document.network_interface, mouse_position, POINT_RADIUS_HANDLE_SEGMENT_THRESHOLD) {
if closest_segment.layer() == layer {
return;
}
}
let point_on_max_radius = star_vertex_position(viewport, 0, sides, radius1, radius2);
if inside_star(viewport, sides, radius1, radius2, mouse_position) && point_on_max_radius.distance(center) > GIZMO_HIDE_THRESHOLD {
self.draw_spokes(center, viewport, sides, radius, overlay_context);
return;
}
}
// Polygon
if let Some((sides, radius)) = extract_polygon_parameters(Some(layer), document) {
let viewport = document.metadata().transform_to_viewport(layer);
let center = viewport.transform_point2(DVec2::ZERO);
if let Some(closest_segment) = shape_editor.upper_closest_segment(&document.network_interface, mouse_position, POINT_RADIUS_HANDLE_SEGMENT_THRESHOLD) {
if closest_segment.layer() == layer {
return;
}
}
let point_on_max_radius = polygon_vertex_position(viewport, 0, sides, radius);
if inside_polygon(viewport, sides, radius, mouse_position) && point_on_max_radius.distance(center) > GIZMO_HIDE_THRESHOLD {
self.draw_spokes(center, viewport, sides, radius, overlay_context);
}
}
}
NumberOfPointsDialState::Hover | NumberOfPointsDialState::Dragging => {
let Some(layer) = self.layer else {
return;
};
// Get the star's greater radius or polygon's radius, as well as the number of sides
let Some((sides, radius)) = extract_star_parameters(Some(layer), document)
.map(|(sides, r1, r2)| (sides, r1.max(r2)))
.or_else(|| extract_polygon_parameters(Some(layer), document))
else {
return;
};
let viewport = document.metadata().transform_to_viewport(layer);
let center = viewport.transform_point2(DVec2::ZERO);
// Draw either the star or polygon outline
star_outline(Some(layer), document, overlay_context);
polygon_outline(Some(layer), document, overlay_context);
self.draw_spokes(center, viewport, sides, radius, overlay_context);
}
}
}
fn draw_spokes(&self, center: DVec2, viewport: DAffine2, sides: u32, radius: f64, overlay_context: &mut OverlayContext) {
for i in 0..sides {
let angle = ((i as f64) * TAU) / (sides as f64);
let point = viewport.transform_point2(DVec2 {
x: radius * angle.sin(),
y: -radius * angle.cos(),
});
let Some(direction) = (point - center).try_normalize() else { continue };
// If the user zooms out such that shape is very small hide the gizmo
if point.distance(center) < GIZMO_HIDE_THRESHOLD {
return;
}
let end_point = direction * NUMBER_OF_POINTS_DIAL_SPOKE_LENGTH;
if matches!(self.handle_state, NumberOfPointsDialState::Hover | NumberOfPointsDialState::Dragging) {
overlay_context.line(center, end_point * NUMBER_OF_POINTS_DIAL_SPOKE_EXTENSION + center, None, None);
} else {
overlay_context.line(center, end_point + center, None, None);
}
}
}
pub fn update_number_of_sides(&self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>, drag_start: DVec2) {
let delta = input.mouse.position - document.metadata().document_to_viewport.transform_point2(drag_start);
let sign = (input.mouse.position.x - document.metadata().document_to_viewport.transform_point2(drag_start).x).signum();
let net_delta = (delta.length() / 25.).round() * sign;
let Some(layer) = self.layer else { return };
let Some(node_id) = graph_modification_utils::get_star_id(layer, &document.network_interface).or(graph_modification_utils::get_polygon_id(layer, &document.network_interface)) else {
return;
};
let new_point_count = ((self.initial_points as i32) + (net_delta as i32)).max(3);
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 1),
input: NodeInput::value(TaggedValue::U32(new_point_count as u32), false),
});
responses.add(NodeGraphMessage::RunDocumentGraph);
}
}
@@ -0,0 +1,455 @@
use crate::consts::GIZMO_HIDE_THRESHOLD;
use crate::consts::{COLOR_OVERLAY_RED, POINT_RADIUS_HANDLE_SNAP_THRESHOLD};
use crate::messages::frontend::utility_types::MouseCursorIcon;
use crate::messages::message::Message;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::{overlays::utility_types::OverlayContext, utility_types::network_interface::InputConnector};
use crate::messages::prelude::FrontendMessage;
use crate::messages::prelude::Responses;
use crate::messages::prelude::{DocumentMessageHandler, InputPreprocessorMessageHandler, NodeGraphMessage};
use crate::messages::tool::common_functionality::graph_modification_utils::{self, NodeGraphLayer};
use crate::messages::tool::common_functionality::shapes::shape_utility::{draw_snapping_ticks, extract_polygon_parameters, polygon_outline, polygon_vertex_position, star_outline};
use crate::messages::tool::common_functionality::shapes::shape_utility::{extract_star_parameters, star_vertex_position};
use glam::DVec2;
use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue;
use std::collections::VecDeque;
use std::f64::consts::{FRAC_1_SQRT_2, FRAC_PI_4, PI, SQRT_2};
#[derive(Clone, Debug, Default, PartialEq)]
pub enum PointRadiusHandleState {
#[default]
Inactive,
Hover,
Dragging,
Snapped(usize),
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct PointRadiusHandle {
pub layer: Option<LayerNodeIdentifier>,
point: u32,
radius_index: usize,
snap_radii: Vec<f64>,
initial_radius: f64,
handle_state: PointRadiusHandleState,
}
impl PointRadiusHandle {
pub fn cleanup(&mut self) {
self.handle_state = PointRadiusHandleState::Inactive;
self.snap_radii.clear();
self.layer = None;
}
pub fn hovered(&self) -> bool {
self.handle_state == PointRadiusHandleState::Hover
}
pub fn is_dragging_or_snapped(&self) -> bool {
self.handle_state == PointRadiusHandleState::Dragging || matches!(self.handle_state, PointRadiusHandleState::Snapped(_))
}
pub fn update_state(&mut self, state: PointRadiusHandleState) {
self.handle_state = state;
}
pub fn handle_actions(&mut self, layer: LayerNodeIdentifier, document: &DocumentMessageHandler, mouse_position: DVec2, responses: &mut VecDeque<Message>) {
match &self.handle_state {
PointRadiusHandleState::Inactive => {
// Draw the point handle gizmo for the star shape
if let Some((sides, radius1, radius2)) = extract_star_parameters(Some(layer), document) {
let viewport = document.metadata().transform_to_viewport(layer);
for i in 0..2 * sides {
let (radius, radius_index) = if i % 2 == 0 { (radius1, 2) } else { (radius2, 3) };
let point = star_vertex_position(viewport, i as i32, sides, radius1, radius2);
let center = viewport.transform_point2(DVec2::ZERO);
// If the user zooms out such that shape is very small hide the gizmo
if point.distance(center) < GIZMO_HIDE_THRESHOLD {
return;
}
if point.distance(mouse_position) < 5. {
self.radius_index = radius_index;
self.layer = Some(layer);
self.point = i;
self.snap_radii = Self::calculate_snap_radii(document, layer, radius_index);
self.initial_radius = radius;
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default });
self.update_state(PointRadiusHandleState::Hover);
return;
}
}
}
// Draw the point handle gizmo for the polygon shape
if let Some((sides, radius)) = extract_polygon_parameters(Some(layer), document) {
let viewport = document.metadata().transform_to_viewport(layer);
for i in 0..sides {
let point = polygon_vertex_position(viewport, i as i32, sides, radius);
let center = viewport.transform_point2(DVec2::ZERO);
// If the user zooms out such that shape is very small hide the gizmo
if point.distance(center) < GIZMO_HIDE_THRESHOLD {
return;
}
if point.distance(mouse_position) < 5. {
self.radius_index = 2;
self.layer = Some(layer);
self.point = i;
self.snap_radii.clear();
self.initial_radius = radius;
self.update_state(PointRadiusHandleState::Hover);
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default });
return;
}
}
}
}
PointRadiusHandleState::Dragging | PointRadiusHandleState::Hover => {
let Some(layer) = self.layer else { return };
let viewport = document.metadata().transform_to_viewport(layer);
// Star
if let Some((sides, radius1, radius2)) = extract_star_parameters(Some(layer), document) {
let point = star_vertex_position(viewport, self.point as i32, sides, radius1, radius2);
if matches!(&self.handle_state, PointRadiusHandleState::Hover) && (mouse_position - point).length() > 5. {
self.update_state(PointRadiusHandleState::Inactive);
self.layer = None;
return;
}
}
// Polygon
if let Some((sides, radius)) = extract_polygon_parameters(Some(layer), document) {
let point = polygon_vertex_position(viewport, self.point as i32, sides, radius);
if matches!(&self.handle_state, PointRadiusHandleState::Hover) && (mouse_position - point).length() > 5. {
self.update_state(PointRadiusHandleState::Inactive);
self.layer = None;
}
}
}
PointRadiusHandleState::Snapped(_) => {}
}
}
pub fn overlays(
&self,
selected_star_layer: Option<LayerNodeIdentifier>,
document: &DocumentMessageHandler,
input: &InputPreprocessorMessageHandler,
mouse_position: DVec2,
overlay_context: &mut OverlayContext,
) {
match &self.handle_state {
PointRadiusHandleState::Inactive => {
let Some(layer) = selected_star_layer else { return };
// Draw the point handle gizmo for the star shape
if let Some((sides, radius1, radius2)) = extract_star_parameters(Some(layer), document) {
let viewport = document.metadata().transform_to_viewport(layer);
for i in 0..(2 * sides) {
let point = star_vertex_position(viewport, i as i32, sides, radius1, radius2);
let center = viewport.transform_point2(DVec2::ZERO);
let viewport_diagonal = input.viewport_bounds.size().length();
// If the user zooms out such that shape is very small hide the gizmo
if point.distance(center) < GIZMO_HIDE_THRESHOLD {
return;
}
if point.distance(mouse_position) < 5. {
let Some(direction) = (point - center).try_normalize() else { continue };
overlay_context.manipulator_handle(point, true, None);
let angle = ((i as f64) * PI) / (sides as f64);
overlay_context.line(center, center + direction * viewport_diagonal, None, None);
draw_snapping_ticks(&self.snap_radii, direction, viewport, angle, overlay_context);
return;
}
overlay_context.manipulator_handle(point, false, None);
}
}
// Draw the point handle gizmo for the Polygon shape
if let Some((sides, radius)) = extract_polygon_parameters(Some(layer), document) {
let viewport = document.metadata().transform_to_viewport(layer);
for i in 0..sides {
let point = polygon_vertex_position(viewport, i as i32, sides, radius);
let center = viewport.transform_point2(DVec2::ZERO);
let viewport_diagonal = input.viewport_bounds.size().length();
// If the user zooms out such that shape is very small hide the gizmo
if point.distance(center) < GIZMO_HIDE_THRESHOLD {
return;
}
if point.distance(mouse_position) < 5. {
let Some(direction) = (point - center).try_normalize() else { continue };
overlay_context.manipulator_handle(point, true, None);
overlay_context.line(center, center + direction * viewport_diagonal, None, None);
return;
}
overlay_context.manipulator_handle(point, false, None);
}
}
}
PointRadiusHandleState::Dragging | PointRadiusHandleState::Hover => {
let Some(layer) = self.layer else { return };
let viewport = document.metadata().transform_to_viewport(layer);
let center = viewport.transform_point2(DVec2::ZERO);
let viewport_diagonal = input.viewport_bounds.size().length();
// Star
if let Some((sides, radius1, radius2)) = extract_star_parameters(Some(layer), document) {
let angle = ((self.point as f64) * PI) / (sides as f64);
let point = star_vertex_position(viewport, self.point as i32, sides, radius1, radius2);
let Some(direction) = (point - center).try_normalize() else { return };
// Draws the ray from the center to the dragging point extending till the viewport
overlay_context.manipulator_handle(point, true, None);
overlay_context.line(center, center + direction * viewport_diagonal, None, None);
star_outline(Some(layer), document, overlay_context);
// Make the ticks for snapping
// If dragging to make radius negative don't show the
if (mouse_position - center).dot(direction) < 0. {
return;
}
draw_snapping_ticks(&self.snap_radii, direction, viewport, angle, overlay_context);
return;
}
// Polygon
if let Some((sides, radius)) = extract_polygon_parameters(Some(layer), document) {
let point = polygon_vertex_position(viewport, self.point as i32, sides, radius);
let Some(direction) = (point - center).try_normalize() else { return };
// Draws the ray from the center to the dragging point extending till the viewport
overlay_context.manipulator_handle(point, true, None);
overlay_context.line(center, center + direction * viewport_diagonal, None, None);
polygon_outline(Some(layer), document, overlay_context);
}
}
PointRadiusHandleState::Snapped(snapping_index) => {
let Some(layer) = self.layer else { return };
let Some((sides, radius1, radius2)) = extract_star_parameters(Some(layer), document) else {
return;
};
let viewport = document.metadata().transform_to_viewport(layer);
let center = viewport.transform_point2(DVec2::ZERO);
match snapping_index {
// Make a triangle with previous two points
0 => {
let before_outer_position = star_vertex_position(viewport, (self.point as i32) - 2, sides, radius1, radius2);
let outer_position = star_vertex_position(viewport, (self.point as i32) - 1, sides, radius1, radius2);
let point_position = star_vertex_position(viewport, self.point as i32, sides, radius1, radius2);
overlay_context.line(before_outer_position, outer_position, Some(COLOR_OVERLAY_RED), Some(3.));
overlay_context.line(outer_position, point_position, Some(COLOR_OVERLAY_RED), Some(3.));
let l1 = (before_outer_position - outer_position).length() * 0.2;
let Some(l1_direction) = (before_outer_position - outer_position).try_normalize() else { return };
let Some(l2_direction) = (point_position - outer_position).try_normalize() else { return };
let Some(direction) = (center - outer_position).try_normalize() else { return };
let new_point = SQRT_2 * l1 * direction + outer_position;
let before_outer_position = l1 * l1_direction + outer_position;
let point_position = l1 * l2_direction + outer_position;
overlay_context.line(before_outer_position, new_point, Some(COLOR_OVERLAY_RED), Some(3.));
overlay_context.line(new_point, point_position, Some(COLOR_OVERLAY_RED), Some(3.));
}
1 => {
let before_outer_position = star_vertex_position(viewport, (self.point as i32) - 1, sides, radius1, radius2);
let after_point_position = star_vertex_position(viewport, (self.point as i32) + 1, sides, radius1, radius2);
let point_position = star_vertex_position(viewport, self.point as i32, sides, radius1, radius2);
overlay_context.line(before_outer_position, point_position, Some(COLOR_OVERLAY_RED), Some(3.));
overlay_context.line(point_position, after_point_position, Some(COLOR_OVERLAY_RED), Some(3.));
let l1 = (before_outer_position - point_position).length() * 0.2;
let Some(l1_direction) = (before_outer_position - point_position).try_normalize() else { return };
let Some(l2_direction) = (after_point_position - point_position).try_normalize() else { return };
let Some(direction) = (center - point_position).try_normalize() else { return };
let new_point = SQRT_2 * l1 * direction + point_position;
let before_outer_position = l1 * l1_direction + point_position;
let after_point_position = l1 * l2_direction + point_position;
overlay_context.line(before_outer_position, new_point, Some(COLOR_OVERLAY_RED), Some(3.));
overlay_context.line(new_point, after_point_position, Some(COLOR_OVERLAY_RED), Some(3.));
}
i => {
// Use `self.point` as absolute reference as it matches the index of vertices of the star starting from 0
if i % 2 != 0 {
// Flipped case
let point_position = star_vertex_position(viewport, self.point as i32, sides, radius1, radius2);
let target_index = (1 - (*i as i32)).abs() + (self.point as i32);
let target_point_position = star_vertex_position(viewport, target_index, sides, radius1, radius2);
let mirrored_index = 2 * (self.point as i32) - target_index;
let mirrored = star_vertex_position(viewport, mirrored_index, sides, radius1, radius2);
overlay_context.line(point_position, target_point_position, Some(COLOR_OVERLAY_RED), Some(3.));
overlay_context.line(point_position, mirrored, Some(COLOR_OVERLAY_RED), Some(3.));
} else {
let outer_index = (self.point as i32) - 1;
let outer_position = star_vertex_position(viewport, outer_index, sides, radius1, radius2);
// The vertex which is colinear with the point we are dragging and its previous outer vertex
let target_index = (self.point as i32) + (*i as i32) - 1;
let target_point_position = star_vertex_position(viewport, target_index, sides, radius1, radius2);
let mirrored_index = 2 * outer_index - target_index;
let mirrored = star_vertex_position(viewport, mirrored_index, sides, radius1, radius2);
overlay_context.line(outer_position, target_point_position, Some(COLOR_OVERLAY_RED), Some(3.));
overlay_context.line(outer_position, mirrored, Some(COLOR_OVERLAY_RED), Some(3.));
}
}
}
star_outline(Some(layer), document, overlay_context);
}
}
}
fn calculate_snap_radii(document: &DocumentMessageHandler, layer: LayerNodeIdentifier, radius_index: usize) -> Vec<f64> {
let mut snap_radii = Vec::new();
let Some(node_inputs) = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Star") else {
return snap_radii;
};
let other_index = if radius_index == 3 { 2 } else { 3 };
let Some(&TaggedValue::F64(other_radius)) = node_inputs[other_index].as_value() else {
return snap_radii;
};
let Some(&TaggedValue::U32(sides)) = node_inputs[1].as_value() else {
return snap_radii;
};
// Inner radius for 90°
let b = FRAC_PI_4 * 3. - PI / (sides as f64);
let angle = b.sin();
let required_radius = (other_radius / angle) * FRAC_1_SQRT_2;
snap_radii.push(required_radius);
// Also push the case when the when it length increases more than the other
let flipped = other_radius * angle * SQRT_2;
snap_radii.push(flipped);
for i in 1..sides {
let sides = sides as f64;
let i = i as f64;
let denominator = 2. * ((PI * (i - 1.)) / sides).cos() * ((PI * i) / sides).sin();
let numerator = ((2. * PI * i) / sides).sin();
let factor = numerator / denominator;
if factor < 0. {
break;
}
if other_radius * factor > 1e-6 {
snap_radii.push(other_radius * factor);
}
snap_radii.push((other_radius * 1.) / factor);
}
snap_radii
}
fn check_snapping(&self, new_radius: f64, original_radius: f64) -> Option<(usize, f64)> {
self.snap_radii
.iter()
.enumerate()
.filter(|(_, rad)| (**rad - new_radius).abs() < POINT_RADIUS_HANDLE_SNAP_THRESHOLD)
.min_by(|(i_a, a), (i_b, b)| {
let dist_a = (**a - new_radius).abs();
let dist_b = (**b - new_radius).abs();
// Check if either index is 0 or 1 and prioritize them
match (*i_a == 0 || *i_a == 1, *i_b == 0 || *i_b == 1) {
// `a` is priority index, `b` is not
(true, false) => std::cmp::Ordering::Less,
// `b` is priority index, `a` is not
(false, true) => std::cmp::Ordering::Greater,
// Normal comparison
_ => dist_a.partial_cmp(&dist_b).unwrap_or(std::cmp::Ordering::Equal),
}
})
.map(|(i, rad)| (i, *rad - original_radius))
}
pub fn update_inner_radius(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>, drag_start: DVec2) {
let Some(layer) = self.layer else { return };
let Some(node_id) = graph_modification_utils::get_star_id(layer, &document.network_interface).or(graph_modification_utils::get_polygon_id(layer, &document.network_interface)) else {
return;
};
let viewport_transform = document.network_interface.document_metadata().transform_to_viewport(layer);
let document_transform = document.network_interface.document_metadata().transform_to_document(layer);
let center = viewport_transform.transform_point2(DVec2::ZERO);
let radius_index = self.radius_index;
let original_radius = self.initial_radius;
let delta = viewport_transform.inverse().transform_point2(input.mouse.position) - document_transform.inverse().transform_point2(drag_start);
let radius = document.metadata().document_to_viewport.transform_point2(drag_start) - center;
let projection = delta.project_onto(radius);
let sign = radius.dot(delta).signum();
let mut net_delta = projection.length() * sign;
let new_radius = original_radius + net_delta;
self.update_state(PointRadiusHandleState::Dragging);
if let Some((index, snapped_delta)) = self.check_snapping(new_radius, original_radius) {
net_delta = snapped_delta;
self.update_state(PointRadiusHandleState::Snapped(index));
}
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, radius_index),
input: NodeInput::value(TaggedValue::F64(original_radius + net_delta), false),
});
responses.add(NodeGraphMessage::RunDocumentGraph);
}
}
@@ -8,11 +8,12 @@ use glam::DVec2;
use graph_craft::concrete;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput};
use graphene_core::Color;
use graphene_core::raster::BlendMode;
use graphene_core::raster::image::ImageFrameTable;
use graphene_core::text::{Font, TypesettingConfig};
use graphene_core::vector::style::Gradient;
use graphene_std::Color;
use graphene_std::NodeInputDecleration;
use graphene_std::raster::BlendMode;
use graphene_std::raster_types::{CPU, GPU, RasterDataTable};
use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::style::Gradient;
use graphene_std::vector::{ManipulatorPointId, PointId, SegmentId, VectorModificationType};
use std::collections::VecDeque;
@@ -57,8 +58,10 @@ pub fn merge_layers(document: &DocumentMessageHandler, first_layer: LayerNodeIde
}
// Move the `second_layer` below the `first_layer` for positioning purposes
let first_layer_parent = first_layer.parent(document.metadata()).unwrap();
let first_layer_index = first_layer_parent.children(document.metadata()).position(|child| child == first_layer).unwrap();
let Some(first_layer_parent) = first_layer.parent(document.metadata()) else { return };
let Some(first_layer_index) = first_layer_parent.children(document.metadata()).position(|child| child == first_layer) else {
return;
};
responses.add(NodeGraphMessage::MoveLayerToStack {
layer: second_layer,
parent: first_layer_parent,
@@ -91,9 +94,9 @@ pub fn merge_layers(document: &DocumentMessageHandler, first_layer: LayerNodeIde
delete_children: false,
});
// Add a flatten vector elements node after the merge
// Add a Flatten Path node after the merge
let flatten_node_id = NodeId::new();
let flatten_node = document_node_definitions::resolve_document_node_type("Flatten Vector Elements")
let flatten_node = document_node_definitions::resolve_document_node_type("Flatten Path")
.expect("Failed to create flatten node")
.default_node_template();
responses.add(NodeGraphMessage::InsertNode {
@@ -207,7 +210,7 @@ pub fn new_vector_layer(subpaths: Vec<Subpath<PointId>>, id: NodeId, parent: Lay
}
/// Create a new bitmap layer.
pub fn new_image_layer(image_frame: ImageFrameTable<Color>, id: NodeId, parent: LayerNodeIdentifier, responses: &mut VecDeque<Message>) -> LayerNodeIdentifier {
pub fn new_image_layer(image_frame: RasterDataTable<CPU>, id: NodeId, parent: LayerNodeIdentifier, responses: &mut VecDeque<Message>) -> LayerNodeIdentifier {
let insert_index = 0;
responses.add(GraphOperationMessage::NewBitmapLayer {
id,
@@ -256,7 +259,7 @@ pub fn get_viewport_pivot(layer: LayerNodeIdentifier, network_interface: &NodeNe
network_interface.document_metadata().transform_to_viewport(layer).transform_point2(min + (max - min) * pivot)
}
/// Get the current gradient of a layer from the closest Fill node
/// Get the current gradient of a layer from the closest "Fill" node.
pub fn get_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Gradient> {
let fill_index = 1;
@@ -267,7 +270,7 @@ pub fn get_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkI
Some(gradient.clone())
}
/// Get the current fill of a layer from the closest Fill node
/// Get the current fill of a layer from the closest "Fill" node.
pub fn get_fill_color(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Color> {
let fill_index = 1;
@@ -278,16 +281,16 @@ pub fn get_fill_color(layer: LayerNodeIdentifier, network_interface: &NodeNetwor
Some(color.to_linear_srgb())
}
/// Get the current blend mode of a layer from the closest Blend Mode node
/// Get the current blend mode of a layer from the closest "Blending" node.
pub fn get_blend_mode(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<BlendMode> {
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Blend Mode")?;
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Blending")?;
let TaggedValue::BlendMode(blend_mode) = inputs.get(1)?.as_value()? else {
return None;
};
Some(*blend_mode)
}
/// Get the current opacity of a layer from the closest Opacity node.
/// Get the current opacity of a layer from the closest "Blending" node.
/// This may differ from the actual opacity contained within the data type reaching this layer, because that actual opacity may be:
/// - Multiplied with additional opacity nodes earlier in the chain
/// - Set by an Opacity node with an exposed input value driven by another node
@@ -296,13 +299,29 @@ pub fn get_blend_mode(layer: LayerNodeIdentifier, network_interface: &NodeNetwor
///
/// With those limitations in mind, the intention of this function is to show just the value already present in an upstream Opacity node so that value can be directly edited.
pub fn get_opacity(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<f64> {
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Opacity")?;
let TaggedValue::F64(opacity) = inputs.get(1)?.as_value()? else {
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Blending")?;
let TaggedValue::F64(opacity) = inputs.get(2)?.as_value()? else {
return None;
};
Some(*opacity)
}
pub fn get_clip_mode(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<bool> {
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Blending")?;
let TaggedValue::Bool(clip) = inputs.get(4)?.as_value()? else {
return None;
};
Some(*clip)
}
pub fn get_fill(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<f64> {
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Blending")?;
let TaggedValue::F64(fill) = inputs.get(3)?.as_value()? else {
return None;
};
Some(*fill)
}
pub fn get_fill_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Fill")
}
@@ -354,7 +373,7 @@ pub fn get_text(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInter
}
pub fn get_stroke_width(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<f64> {
let weight_node_input_index = 2;
let weight_node_input_index = graphene_std::vector::stroke::WeightInput::INDEX;
if let TaggedValue::F64(width) = NodeGraphLayer::new(layer, network_interface).find_input("Stroke", weight_node_input_index)? {
Some(*width)
} else {
@@ -424,12 +443,7 @@ impl<'a> NodeGraphLayer<'a> {
/// Check if a layer is a raster layer
pub fn is_raster_layer(layer: LayerNodeIdentifier, network_interface: &mut NodeNetworkInterface) -> bool {
let layer_input_type = network_interface.input_type(&InputConnector::node(layer.to_node(), 1), &[]).0.nested_type().clone();
if layer_input_type == concrete!(graphene_core::raster::image::ImageFrameTable<graphene_core::Color>)
|| layer_input_type == concrete!(graphene_core::application_io::TextureFrameTable)
|| layer_input_type == concrete!(graphene_std::RasterFrame)
{
return true;
}
false
layer_input_type == concrete!(RasterDataTable<CPU>) || layer_input_type == concrete!(RasterDataTable<GPU>)
}
}
@@ -1,11 +1,13 @@
pub mod auto_panning;
pub mod color_selector;
pub mod compass_rose;
pub mod gizmos;
pub mod graph_modification_utils;
pub mod measure;
pub mod pivot;
pub mod resize;
pub mod shape_editor;
pub mod shapes;
pub mod snapping;
pub mod transformation_cage;
pub mod utility_functions;
@@ -8,7 +8,7 @@ use glam::{DAffine2, DVec2, Vec2Swizzles};
#[derive(Clone, Debug, Default)]
pub struct Resize {
/// Stored as a document position so the start doesn't move if the canvas is panned.
drag_start: DVec2,
pub drag_start: DVec2,
pub layer: Option<LayerNodeIdentifier>,
pub snap_manager: SnapManager,
}
@@ -1,20 +1,20 @@
use super::graph_modification_utils::{self, merge_layers};
use super::graph_modification_utils::merge_layers;
use super::snapping::{SnapCache, SnapCandidatePoint, SnapData, SnapManager, SnappedPoint};
use super::utility_functions::calculate_segment_angle;
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};
use graphene_core::transform::Transform;
use graphene_core::vector::{ManipulatorPointId, PointId, VectorData, VectorModificationType};
use graphene_std::vector::{HandleId, SegmentId};
use graphene_std::vector::{HandleExt, HandleId, SegmentId};
use graphene_std::vector::{ManipulatorPointId, PointId, VectorData, VectorModificationType};
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum SelectionChange {
@@ -46,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,
@@ -55,11 +56,27 @@ pub struct SelectedLayerState {
}
impl SelectedLayerState {
pub fn selected(&self) -> impl Iterator<Item = ManipulatorPointId> + '_ {
pub fn selected_points(&self) -> impl Iterator<Item = ManipulatorPointId> + '_ {
self.selected_points.iter().copied()
}
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)
}
@@ -67,10 +84,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;
@@ -102,14 +135,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>;
@@ -129,6 +154,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,
@@ -137,6 +168,7 @@ pub struct ManipulatorPointInfo {
pub type OpposingHandleLengths = HashMap<LayerNodeIdentifier, HashMap<HandleId, f64>>;
#[derive(Clone)]
pub struct ClosestSegment {
layer: LayerNodeIdentifier,
segment: SegmentId,
@@ -145,7 +177,6 @@ pub struct ClosestSegment {
colinear: [Option<HandleId>; 2],
t: f64,
bezier_point_to_viewport: DVec2,
stroke_width: f64,
}
impl ClosestSegment {
@@ -161,10 +192,24 @@ 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))
}
pub fn closest_point_to_viewport(&self) -> DVec2 {
self.bezier_point_to_viewport
}
pub fn closest_point(&self, document_metadata: &DocumentMetadata) -> DVec2 {
let transform = document_metadata.transform_to_viewport(self.layer);
let bezier_point = self.bezier.evaluate(TValue::Parametric(self.t));
transform.transform_point2(bezier_point)
}
/// Updates this [`ClosestSegment`] with the viewport-space location of the closest point on the segment to the given mouse position.
pub fn update_closest_point(&mut self, document_metadata: &DocumentMetadata, mouse_position: DVec2) {
let transform = document_metadata.transform_to_viewport(self.layer);
@@ -182,10 +227,8 @@ impl ClosestSegment {
self.bezier_point_to_viewport.distance_squared(mouse_position)
}
pub fn too_far(&self, mouse_position: DVec2, tolerance: f64, document_metadata: &DocumentMetadata) -> bool {
let dist_sq = self.distance_squared(mouse_position);
let stroke_width = document_metadata.document_to_viewport.decompose_scale().x.max(1.) * self.stroke_width;
(stroke_width + tolerance).powi(2) < dist_sq
pub fn too_far(&self, mouse_position: DVec2, tolerance: f64) -> bool {
tolerance.powi(2) < self.distance_squared(mouse_position)
}
pub fn handle_positions(&self, document_metadata: &DocumentMetadata) -> (Option<DVec2>, Option<DVec2>) {
@@ -202,7 +245,7 @@ impl ClosestSegment {
(first_handle, second_handle)
}
pub fn adjusted_insert(&self, responses: &mut VecDeque<Message>) -> PointId {
pub fn adjusted_insert(&self, responses: &mut VecDeque<Message>) -> (PointId, [SegmentId; 2]) {
let layer = self.layer;
let [first, second] = self.bezier.split(TValue::Parametric(self.t));
@@ -247,11 +290,11 @@ impl ClosestSegment {
responses.add(GraphOperationMessage::Vector { layer, modification_type });
}
midpoint
(midpoint, segment_ids)
}
pub fn adjusted_insert_and_select(&self, shape_editor: &mut ShapeState, responses: &mut VecDeque<Message>, extend_selection: bool) {
let id = self.adjusted_insert(responses);
let (id, _) = self.adjusted_insert(responses);
shape_editor.select_anchor_point_by_id(self.layer, id, extend_selection)
}
@@ -276,6 +319,71 @@ impl ClosestSegment {
.unwrap_or(DVec2::ZERO);
tangent.perp()
}
/// Molding the bezier curve.
/// Returns adjacent handles' [`HandleId`] if colinearity is broken temporarily.
pub fn mold_handle_positions(
&self,
document: &DocumentMessageHandler,
responses: &mut VecDeque<Message>,
(c1, c2): (DVec2, DVec2),
new_b: DVec2,
break_colinear_molding: bool,
temporary_adjacent_handles_while_molding: Option<[Option<HandleId>; 2]>,
) -> Option<[Option<HandleId>; 2]> {
let transform = document.metadata().transform_to_viewport(self.layer);
let start = self.bezier.start;
let end = self.bezier.end;
// Apply the drag delta to the segment's handles
let b = self.bezier_point_to_viewport;
let delta = transform.inverse().transform_vector2(new_b - b);
let (nc1, nc2) = (c1 + delta, c2 + delta);
let handle1 = HandleId::primary(self.segment);
let handle2 = HandleId::end(self.segment);
let layer = self.layer;
let modification_type = handle1.set_relative_position(nc1 - start);
responses.add(GraphOperationMessage::Vector { layer, modification_type });
let modification_type = handle2.set_relative_position(nc2 - end);
responses.add(GraphOperationMessage::Vector { layer, modification_type });
// If adjacent segments have colinear handles, their direction is changed but their handle lengths is preserved
// TODO: Find something which is more appropriate
let vector_data = document.network_interface.compute_modified_vector(self.layer())?;
if break_colinear_molding {
// Disable G1 continuity
let other_handles = [
restore_previous_handle_position(handle1, c1, start, &vector_data, layer, responses),
restore_previous_handle_position(handle2, c2, end, &vector_data, layer, responses),
];
// Store other HandleId in tool data to regain colinearity later
if temporary_adjacent_handles_while_molding.is_some() {
temporary_adjacent_handles_while_molding
} else {
Some(other_handles)
}
} else {
// Move the colinear handles so that colinearity is maintained
adjust_handle_colinearity(handle1, start, nc1, &vector_data, layer, responses);
adjust_handle_colinearity(handle2, end, nc2, &vector_data, layer, responses);
if let Some(adjacent_handles) = temporary_adjacent_handles_while_molding {
if let Some(other_handle1) = adjacent_handles[0] {
restore_g1_continuity(handle1, other_handle1, nc1, start, &vector_data, layer, responses);
}
if let Some(other_handle2) = adjacent_handles[1] {
restore_g1_continuity(handle2, other_handle2, nc2, end, &vector_data, layer, responses);
}
}
None
}
}
}
// TODO Consider keeping a list of selected manipulators to minimize traversals of the layers
@@ -402,7 +510,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 };
@@ -441,38 +549,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
}
@@ -484,11 +584,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)?;
@@ -501,7 +606,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);
@@ -559,7 +664,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));
}
@@ -600,6 +705,13 @@ impl ShapeState {
}
}
/// Deselects all segments across every selected layer
pub fn deselect_all_segments(&mut self) {
for state in self.selected_shape_state.values_mut() {
state.selected_segments.clear()
}
}
pub fn update_selected_anchors_status(&mut self, status: bool) {
for state in self.selected_shape_state.values_mut() {
self.ignore_anchors = !status;
@@ -665,10 +777,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,
@@ -695,7 +815,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;
}
@@ -779,6 +899,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
@@ -820,7 +943,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
@@ -941,9 +1064,9 @@ impl ShapeState {
}
}
/// Move the selected points by dragging the mouse.
/// Move the selected points and segments by dragging the mouse.
#[allow(clippy::too_many_arguments)]
pub fn move_selected_points(
pub fn move_selected_points_and_segments(
&self,
handle_lengths: Option<OpposingHandleLengths>,
document: &DocumentMessageHandler,
@@ -969,7 +1092,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;
}
@@ -984,7 +1117,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;
}
@@ -1003,7 +1136,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 };
@@ -1054,12 +1187,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],
@@ -1137,11 +1270,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);
}
@@ -1186,6 +1323,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();
@@ -1233,6 +1372,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 };
@@ -1445,11 +1598,6 @@ impl ShapeState {
if distance_squared < closest_distance_squared {
closest_distance_squared = distance_squared;
// 0.5 is half the line (center to side) but it's convenient to allow targeting slightly more than half the line width
const STROKE_WIDTH_PERCENT: f64 = 0.7;
let stroke_width = graph_modification_utils::get_stroke_width(layer, network_interface).unwrap_or(1.) as f64 * STROKE_WIDTH_PERCENT;
// Convert to linear if handes are on top of control points
if let bezier_rs::BezierHandles::Cubic { handle_start, handle_end } = bezier.handles {
if handle_start.abs_diff_eq(bezier.start(), f64::EPSILON * 100.) && handle_end.abs_diff_eq(bezier.end(), f64::EPSILON * 100.) {
@@ -1470,7 +1618,6 @@ impl ShapeState {
t,
bezier_point_to_viewport: screenspace,
layer,
stroke_width,
});
}
}
@@ -1628,10 +1775,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) {
@@ -1683,6 +1833,7 @@ impl ShapeState {
false
}
#[allow(clippy::too_many_arguments)]
pub fn select_all_in_shape(
&mut self,
network_interface: &NodeNetworkInterface,
@@ -1690,13 +1841,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);
@@ -1722,7 +1877,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);
@@ -1755,6 +1949,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);
@@ -0,0 +1,181 @@
use super::shape_utility::ShapeToolModifierKey;
use super::*;
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
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, NodeTemplate};
use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::tool_messages::tool_prelude::*;
use glam::DAffine2;
use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue;
use std::collections::VecDeque;
#[derive(Default)]
pub struct Ellipse;
impl Ellipse {
pub fn create_node() -> NodeTemplate {
let node_type = resolve_document_node_type("Ellipse").expect("Ellipse node can't be found");
node_type.node_template_input_override([None, Some(NodeInput::value(TaggedValue::F64(0.5), false)), Some(NodeInput::value(TaggedValue::F64(0.5), false))])
}
pub fn update_shape(
document: &DocumentMessageHandler,
ipp: &InputPreprocessorMessageHandler,
layer: LayerNodeIdentifier,
shape_tool_data: &mut ShapeToolData,
modifier: ShapeToolModifierKey,
responses: &mut VecDeque<Message>,
) {
let [center, lock_ratio, _, _] = modifier;
if let Some([start, end]) = shape_tool_data.data.calculate_points(document, ipp, center, lock_ratio) {
let Some(node_id) = graph_modification_utils::get_ellipse_id(layer, &document.network_interface) else {
return;
};
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 1),
input: NodeInput::value(TaggedValue::F64(((start.x - end.x) / 2.).abs()), false),
});
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 2),
input: NodeInput::value(TaggedValue::F64(((start.y - end.y) / 2.).abs()), false),
});
responses.add(GraphOperationMessage::TransformSet {
layer,
transform: DAffine2::from_translation(start.midpoint(end)),
transform_in: TransformIn::Viewport,
skip_rerender: false,
});
}
}
}
#[cfg(test)]
mod test_ellipse {
pub use crate::test_utils::test_prelude::*;
use glam::DAffine2;
use graphene_std::vector::generator_nodes::ellipse;
#[derive(Debug, PartialEq)]
struct ResolvedEllipse {
radius_x: f64,
radius_y: f64,
transform: DAffine2,
}
async fn get_ellipse(editor: &mut EditorTestUtils) -> Vec<ResolvedEllipse> {
let instrumented = match editor.eval_graph().await {
Ok(instrumented) => instrumented,
Err(e) => panic!("Failed to evaluate graph: {e}"),
};
let document = editor.active_document();
let layers = document.metadata().all_layers();
layers
.filter_map(|layer| {
let node_graph_layer = NodeGraphLayer::new(layer, &document.network_interface);
let ellipse_node = node_graph_layer.upstream_node_id_from_protonode(ellipse::protonode_identifier())?;
Some(ResolvedEllipse {
radius_x: instrumented.grab_protonode_input::<ellipse::RadiusXInput>(&vec![ellipse_node], &editor.runtime).unwrap(),
radius_y: instrumented.grab_protonode_input::<ellipse::RadiusYInput>(&vec![ellipse_node], &editor.runtime).unwrap(),
transform: document.metadata().transform_to_document(layer),
})
})
.collect()
}
#[tokio::test]
async fn ellipse_draw_simple() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor.drag_tool(ToolType::Ellipse, 10., 10., 19., 0., ModifierKeys::empty()).await;
assert_eq!(editor.active_document().metadata().all_layers().count(), 1);
let ellipse = get_ellipse(&mut editor).await;
assert_eq!(ellipse.len(), 1);
assert_eq!(
ellipse[0],
ResolvedEllipse {
radius_x: 4.5,
radius_y: 5.,
transform: DAffine2::from_translation(DVec2::new(14.5, 5.)) // Uses center
}
);
}
#[tokio::test]
async fn ellipse_draw_circle() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor.drag_tool(ToolType::Ellipse, 10., 10., -10., 11., ModifierKeys::SHIFT).await;
let ellipse = get_ellipse(&mut editor).await;
assert_eq!(ellipse.len(), 1);
assert_eq!(
ellipse[0],
ResolvedEllipse {
radius_x: 10.,
radius_y: 10.,
transform: DAffine2::from_translation(DVec2::new(0., 20.)) // Uses center
}
);
}
#[tokio::test]
async fn ellipse_draw_square_rotated() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor
.handle_message(NavigationMessage::CanvasTiltSet {
// 45 degree rotation of content clockwise
angle_radians: f64::consts::FRAC_PI_4,
})
.await;
editor.drag_tool(ToolType::Ellipse, 0., 0., 1., 10., ModifierKeys::SHIFT).await; // Viewport coordinates
let ellipse = get_ellipse(&mut editor).await;
assert_eq!(ellipse.len(), 1);
println!("{ellipse:?}");
assert_eq!(ellipse[0].radius_x, 5.);
assert_eq!(ellipse[0].radius_y, 5.);
assert!(
ellipse[0]
.transform
.abs_diff_eq(DAffine2::from_angle_translation(-f64::consts::FRAC_PI_4, DVec2::X * f64::consts::FRAC_1_SQRT_2 * 10.), 0.001)
);
}
#[tokio::test]
async fn ellipse_draw_center_square_rotated() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor
.handle_message(NavigationMessage::CanvasTiltSet {
// 45 degree rotation of content clockwise
angle_radians: f64::consts::FRAC_PI_4,
})
.await;
editor.drag_tool(ToolType::Ellipse, 0., 0., 1., 10., ModifierKeys::SHIFT | ModifierKeys::ALT).await; // Viewport coordinates
let ellipse = get_ellipse(&mut editor).await;
assert_eq!(ellipse.len(), 1);
assert_eq!(ellipse[0].radius_x, 10.);
assert_eq!(ellipse[0].radius_y, 10.);
assert!(ellipse[0].transform.abs_diff_eq(DAffine2::from_angle(-f64::consts::FRAC_PI_4), 0.001));
}
#[tokio::test]
async fn ellipse_cancel() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor.drag_tool_cancel_rmb(ToolType::Ellipse).await;
let ellipse = get_ellipse(&mut editor).await;
assert_eq!(ellipse.len(), 0);
}
}
@@ -0,0 +1,383 @@
use super::shape_utility::ShapeToolModifierKey;
use crate::consts::{BOUNDS_SELECT_THRESHOLD, LINE_ROTATE_SNAP_ANGLE};
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate};
use crate::messages::tool::common_functionality::graph_modification_utils;
pub use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer;
use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapTypeConfiguration};
use crate::messages::tool::tool_messages::shape_tool::ShapeToolData;
use crate::messages::tool::tool_messages::tool_prelude::*;
use glam::DVec2;
use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue;
use std::collections::VecDeque;
#[derive(Clone, PartialEq, Debug, Default)]
pub enum LineEnd {
#[default]
Start,
End,
}
#[derive(Clone, Debug, Default)]
pub struct LineToolData {
pub drag_start: DVec2,
pub drag_current: DVec2,
pub angle: f64,
pub weight: f64,
pub selected_layers_with_position: HashMap<LayerNodeIdentifier, [DVec2; 2]>,
pub editing_layer: Option<LayerNodeIdentifier>,
pub dragging_endpoint: Option<LineEnd>,
}
#[derive(Default)]
pub struct Line;
impl Line {
pub fn create_node(document: &DocumentMessageHandler, drag_start: DVec2) -> NodeTemplate {
let node_type = resolve_document_node_type("Line").expect("Line node can't be found");
node_type.node_template_input_override([
None,
Some(NodeInput::value(TaggedValue::DVec2(document.metadata().document_to_viewport.transform_point2(drag_start)), false)),
Some(NodeInput::value(TaggedValue::DVec2(document.metadata().document_to_viewport.transform_point2(drag_start)), false)),
])
}
pub fn update_shape(
document: &DocumentMessageHandler,
ipp: &InputPreprocessorMessageHandler,
layer: LayerNodeIdentifier,
shape_tool_data: &mut ShapeToolData,
modifier: ShapeToolModifierKey,
responses: &mut VecDeque<Message>,
) {
let [center, _, lock_angle, snap_angle] = modifier;
shape_tool_data.line_data.drag_current = ipp.mouse.position;
let keyboard = &ipp.keyboard;
let ignore = [layer];
let snap_data = SnapData::ignore(document, ipp, &ignore);
let mut document_points = generate_line(shape_tool_data, snap_data, keyboard.key(lock_angle), keyboard.key(snap_angle), keyboard.key(center));
if shape_tool_data.line_data.dragging_endpoint == Some(LineEnd::Start) {
document_points.swap(0, 1);
}
let Some(node_id) = graph_modification_utils::get_line_id(layer, &document.network_interface) else {
return;
};
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 1),
input: NodeInput::value(TaggedValue::DVec2(document_points[0]), false),
});
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 2),
input: NodeInput::value(TaggedValue::DVec2(document_points[1]), false),
});
responses.add(NodeGraphMessage::RunDocumentGraph);
}
pub fn overlays(document: &DocumentMessageHandler, shape_tool_data: &mut ShapeToolData, overlay_context: &mut OverlayContext) {
shape_tool_data.line_data.selected_layers_with_position = document
.network_interface
.selected_nodes()
.selected_visible_and_unlocked_layers(&document.network_interface)
.filter_map(|layer| {
let node_inputs = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Line")?;
let (Some(&TaggedValue::DVec2(start)), Some(&TaggedValue::DVec2(end))) = (node_inputs[1].as_value(), node_inputs[2].as_value()) else {
return None;
};
let [viewport_start, viewport_end] = [start, end].map(|point| document.metadata().transform_to_viewport(layer).transform_point2(point));
if !start.abs_diff_eq(end, f64::EPSILON * 1000.) {
overlay_context.line(viewport_start, viewport_end, None, None);
overlay_context.square(viewport_start, Some(6.), None, None);
overlay_context.square(viewport_end, Some(6.), None, None);
}
Some((layer, [start, end]))
})
.collect::<HashMap<LayerNodeIdentifier, [DVec2; 2]>>();
}
}
fn generate_line(tool_data: &mut ShapeToolData, snap_data: SnapData, lock_angle: bool, snap_angle: bool, center: bool) -> [DVec2; 2] {
let document_to_viewport = snap_data.document.metadata().document_to_viewport;
let mut document_points = [tool_data.data.drag_start, document_to_viewport.inverse().transform_point2(tool_data.line_data.drag_current)];
let mut angle = -(document_points[1] - document_points[0]).angle_to(DVec2::X);
let mut line_length = (document_points[1] - document_points[0]).length();
if lock_angle {
angle = tool_data.line_data.angle;
} else if snap_angle {
let snap_resolution = LINE_ROTATE_SNAP_ANGLE.to_radians();
angle = (angle / snap_resolution).round() * snap_resolution;
}
tool_data.line_data.angle = angle;
if lock_angle {
let angle_vec = DVec2::new(angle.cos(), angle.sin());
line_length = (document_points[1] - document_points[0]).dot(angle_vec);
}
document_points[1] = document_points[0] + line_length * DVec2::new(angle.cos(), angle.sin());
let constrained = snap_angle || lock_angle;
let snap = &mut tool_data.data.snap_manager;
let near_point = SnapCandidatePoint::handle_neighbors(document_points[1], [tool_data.data.drag_start]);
let far_point = SnapCandidatePoint::handle_neighbors(2. * document_points[0] - document_points[1], [tool_data.data.drag_start]);
let config = SnapTypeConfiguration::default();
if constrained {
let constraint = SnapConstraint::Line {
origin: document_points[0],
direction: document_points[1] - document_points[0],
};
if center {
let snapped = snap.constrained_snap(&snap_data, &near_point, constraint, config);
let snapped_far = snap.constrained_snap(&snap_data, &far_point, constraint, config);
let best = if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far };
document_points[1] = document_points[0] * 2. - best.snapped_point_document;
document_points[0] = best.snapped_point_document;
snap.update_indicator(best);
} else {
let snapped = snap.constrained_snap(&snap_data, &near_point, constraint, config);
document_points[1] = snapped.snapped_point_document;
snap.update_indicator(snapped);
}
} else if center {
let snapped = snap.free_snap(&snap_data, &near_point, config);
let snapped_far = snap.free_snap(&snap_data, &far_point, config);
let best = if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far };
document_points[1] = document_points[0] * 2. - best.snapped_point_document;
document_points[0] = best.snapped_point_document;
snap.update_indicator(best);
} else {
let snapped = snap.free_snap(&snap_data, &near_point, config);
document_points[1] = snapped.snapped_point_document;
snap.update_indicator(snapped);
}
document_points
}
pub fn clicked_on_line_endpoints(layer: LayerNodeIdentifier, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, shape_tool_data: &mut ShapeToolData) -> bool {
let Some(node_inputs) = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Line") else {
return false;
};
let (Some(&TaggedValue::DVec2(document_start)), Some(&TaggedValue::DVec2(document_end))) = (node_inputs[1].as_value(), node_inputs[2].as_value()) else {
return false;
};
let transform = document.metadata().transform_to_viewport(layer);
let viewport_x = transform.transform_vector2(DVec2::X).normalize_or_zero() * BOUNDS_SELECT_THRESHOLD;
let viewport_y = transform.transform_vector2(DVec2::Y).normalize_or_zero() * BOUNDS_SELECT_THRESHOLD;
let threshold_x = transform.inverse().transform_vector2(viewport_x).length();
let threshold_y = transform.inverse().transform_vector2(viewport_y).length();
let drag_start = input.mouse.position;
let [start, end] = [document_start, document_end].map(|point| transform.transform_point2(point));
let start_click = (drag_start.y - start.y).abs() < threshold_y && (drag_start.x - start.x).abs() < threshold_x;
let end_click = (drag_start.y - end.y).abs() < threshold_y && (drag_start.x - end.x).abs() < threshold_x;
if start_click || end_click {
shape_tool_data.line_data.dragging_endpoint = Some(if end_click { LineEnd::End } else { LineEnd::Start });
shape_tool_data.data.drag_start = if end_click { document_start } else { document_end };
shape_tool_data.line_data.editing_layer = Some(layer);
return true;
}
false
}
#[cfg(test)]
mod test_line_tool {
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer;
use crate::test_utils::test_prelude::*;
use glam::DAffine2;
use graph_craft::document::value::TaggedValue;
async fn get_line_node_inputs(editor: &mut EditorTestUtils) -> Option<(DVec2, DVec2)> {
let document = editor.active_document();
let network_interface = &document.network_interface;
let node_id = network_interface
.selected_nodes()
.selected_visible_and_unlocked_layers(network_interface)
.filter_map(|layer| {
let node_inputs = NodeGraphLayer::new(layer, &network_interface).find_node_inputs("Line")?;
let (Some(&TaggedValue::DVec2(start)), Some(&TaggedValue::DVec2(end))) = (node_inputs[1].as_value(), node_inputs[2].as_value()) else {
return None;
};
Some((start, end))
})
.next();
node_id
}
#[tokio::test]
async fn test_line_tool_basicdraw() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor.drag_tool(ToolType::Line, 0., 0., 100., 100., ModifierKeys::empty()).await;
if let Some((start_input, end_input)) = get_line_node_inputs(&mut editor).await {
match (start_input, end_input) {
(start_input, end_input) => {
assert!((start_input - DVec2::ZERO).length() < 1., "Start point should be near (0,0)");
assert!((end_input - DVec2::new(100., 100.)).length() < 1., "End point should be near (100,100)");
}
}
}
}
#[tokio::test]
async fn test_line_tool_with_transformed_viewport() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor.handle_message(NavigationMessage::CanvasZoomSet { zoom_factor: 2. }).await;
editor.handle_message(NavigationMessage::CanvasPan { delta: DVec2::new(100., 50.) }).await;
editor
.handle_message(NavigationMessage::CanvasTiltSet {
angle_radians: (30. as f64).to_radians(),
})
.await;
editor.drag_tool(ToolType::Line, 0., 0., 100., 100., ModifierKeys::empty()).await;
if let Some((start_input, end_input)) = get_line_node_inputs(&mut editor).await {
let document = editor.active_document();
let document_to_viewport = document.metadata().document_to_viewport;
let viewport_to_document = document_to_viewport.inverse();
let expected_start = viewport_to_document.transform_point2(DVec2::ZERO);
let expected_end = viewport_to_document.transform_point2(DVec2::new(100., 100.));
assert!(
(start_input - expected_start).length() < 1.,
"Start point should match expected document coordinates. Got {:?}, expected {:?}",
start_input,
expected_start
);
assert!(
(end_input - expected_end).length() < 1.,
"End point should match expected document coordinates. Got {:?}, expected {:?}",
end_input,
expected_end
);
} else {
panic!("Line was not created successfully with transformed viewport");
}
}
#[tokio::test]
async fn test_line_tool_ctrl_anglelock() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor.drag_tool(ToolType::Line, 0., 0., 100., 100., ModifierKeys::CONTROL).await;
if let Some((start_input, end_input)) = get_line_node_inputs(&mut editor).await {
match (start_input, end_input) {
(start_input, end_input) => {
let line_vec = end_input - start_input;
let original_angle = line_vec.angle_to(DVec2::X);
editor.drag_tool(ToolType::Line, 0., 0., 200., 50., ModifierKeys::CONTROL).await;
if let Some((updated_start, updated_end)) = get_line_node_inputs(&mut editor).await {
match (updated_start, updated_end) {
(updated_start, updated_end) => {
let updated_line_vec = updated_end - updated_start;
let updated_angle = updated_line_vec.angle_to(DVec2::X);
print!("{:?}", original_angle);
print!("{:?}", updated_angle);
assert!(
line_vec.normalize().dot(updated_line_vec.normalize()).abs() - 1. < 1e-6,
"Line angle should be locked when Ctrl is kept pressed"
);
assert!((updated_start - updated_end).length() > 1., "Line should be able to change length when Ctrl is kept pressed");
}
}
}
}
}
}
}
#[tokio::test]
async fn test_line_tool_alt() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor.drag_tool(ToolType::Line, 100., 100., 200., 100., ModifierKeys::ALT).await;
if let Some((start_input, end_input)) = get_line_node_inputs(&mut editor).await {
match (start_input, end_input) {
(start_input, end_input) => {
let expected_start = DVec2::new(0., 100.);
let expected_end = DVec2::new(200., 100.);
assert!((start_input - expected_start).length() < 1., "Start point should be near (0, 100)");
assert!((end_input - expected_end).length() < 1., "End point should be near (200, 100)");
}
}
}
}
#[tokio::test]
async fn test_line_tool_alt_shift_drag() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor.drag_tool(ToolType::Line, 100., 100., 150., 120., ModifierKeys::ALT | ModifierKeys::SHIFT).await;
if let Some((start_input, end_input)) = get_line_node_inputs(&mut editor).await {
match (start_input, end_input) {
(start_input, end_input) => {
let line_vec = end_input - start_input;
let angle_radians = line_vec.angle_to(DVec2::X);
let angle_degrees = angle_radians.to_degrees();
let nearest_angle = (angle_degrees / 15.).round() * 15.;
assert!((angle_degrees - nearest_angle).abs() < 1., "Angle should snap to the nearest 15 degrees");
}
}
}
}
#[tokio::test]
async fn test_line_tool_with_transformed_artboard() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor.drag_tool(ToolType::Artboard, 0., 0., 200., 200., ModifierKeys::empty()).await;
let artboard_id = editor.get_selected_layer().await.expect("Should have selected the artboard");
editor
.handle_message(GraphOperationMessage::TransformChange {
layer: artboard_id,
transform: DAffine2::from_angle(45_f64.to_radians()),
transform_in: TransformIn::Local,
skip_rerender: false,
})
.await;
editor.drag_tool(ToolType::Line, 50., 50., 150., 150., ModifierKeys::empty()).await;
let (start_input, end_input) = get_line_node_inputs(&mut editor).await.expect("Line was not created successfully within transformed artboard");
// The line should still be diagonal with equal change in x and y
let line_vector = end_input - start_input;
// Verifying the line is approximately 100*sqrt(2) units in length (diagonal of 100x100 square)
let line_length = line_vector.length();
assert!(
(line_length - 141.42).abs() < 1., // 100 * sqrt(2) ~= 141.42
"Line length should be approximately 141.42 units. Got: {line_length}"
);
assert!((line_vector.x - 100.).abs() < 1., "X-component of line vector should be approximately 100. Got: {}", line_vector.x);
assert!(
(line_vector.y.abs() - 100.).abs() < 1.,
"Absolute Y-component of line vector should be approximately 100. Got: {}",
line_vector.y.abs()
);
let angle_degrees = line_vector.angle_to(DVec2::X).to_degrees();
assert!((angle_degrees - (-45.)).abs() < 1., "Line angle should be close to -45 degrees. Got: {angle_degrees}");
}
}
@@ -0,0 +1,11 @@
pub mod ellipse_shape;
pub mod line_shape;
pub mod polygon_shape;
pub mod rectangle_shape;
pub mod shape_utility;
pub mod star_shape;
pub use super::shapes::ellipse_shape::Ellipse;
pub use super::shapes::line_shape::{Line, LineEnd};
pub use super::shapes::rectangle_shape::Rectangle;
pub use crate::messages::tool::tool_messages::shape_tool::ShapeToolData;
@@ -0,0 +1,151 @@
use super::shape_utility::ShapeToolModifierKey;
use super::shape_utility::update_radius_sign;
use super::*;
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate};
use crate::messages::tool::common_functionality::gizmos::shape_gizmos::number_of_points_dial::NumberOfPointsDial;
use crate::messages::tool::common_functionality::gizmos::shape_gizmos::number_of_points_dial::NumberOfPointsDialState;
use crate::messages::tool::common_functionality::gizmos::shape_gizmos::point_radius_handle::PointRadiusHandle;
use crate::messages::tool::common_functionality::gizmos::shape_gizmos::point_radius_handle::PointRadiusHandleState;
use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::shape_editor::ShapeState;
use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeGizmoHandler;
use crate::messages::tool::common_functionality::shapes::shape_utility::polygon_outline;
use crate::messages::tool::tool_messages::tool_prelude::*;
use glam::DAffine2;
use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue;
use std::collections::VecDeque;
#[derive(Clone, Debug, Default)]
pub struct PolygonGizmoHandler {
number_of_points_dial: NumberOfPointsDial,
point_radius_handle: PointRadiusHandle,
}
impl ShapeGizmoHandler for PolygonGizmoHandler {
fn is_any_gizmo_hovered(&self) -> bool {
self.number_of_points_dial.is_hovering() || self.point_radius_handle.hovered()
}
fn handle_state(&mut self, selected_star_layer: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
self.number_of_points_dial.handle_actions(selected_star_layer, mouse_position, document, responses);
self.point_radius_handle.handle_actions(selected_star_layer, document, mouse_position, responses);
}
fn handle_click(&mut self) {
if self.number_of_points_dial.is_hovering() {
self.number_of_points_dial.update_state(NumberOfPointsDialState::Dragging);
return;
}
if self.point_radius_handle.hovered() {
self.point_radius_handle.update_state(PointRadiusHandleState::Dragging);
}
}
fn handle_update(&mut self, drag_start: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
if self.number_of_points_dial.is_dragging() {
self.number_of_points_dial.update_number_of_sides(document, input, responses, drag_start);
}
if self.point_radius_handle.is_dragging_or_snapped() {
self.point_radius_handle.update_inner_radius(document, input, responses, drag_start);
}
}
fn overlays(
&self,
document: &DocumentMessageHandler,
selected_polygon_layer: Option<LayerNodeIdentifier>,
input: &InputPreprocessorMessageHandler,
shape_editor: &mut &mut ShapeState,
mouse_position: DVec2,
overlay_context: &mut OverlayContext,
) {
self.number_of_points_dial.overlays(document, selected_polygon_layer, shape_editor, mouse_position, overlay_context);
self.point_radius_handle.overlays(selected_polygon_layer, document, input, mouse_position, overlay_context);
polygon_outline(selected_polygon_layer, document, overlay_context);
}
fn dragging_overlays(
&self,
document: &DocumentMessageHandler,
input: &InputPreprocessorMessageHandler,
shape_editor: &mut &mut ShapeState,
mouse_position: DVec2,
overlay_context: &mut OverlayContext,
) {
if self.number_of_points_dial.is_dragging() {
self.number_of_points_dial.overlays(document, None, shape_editor, mouse_position, overlay_context);
}
if self.point_radius_handle.is_dragging_or_snapped() {
self.point_radius_handle.overlays(None, document, input, mouse_position, overlay_context);
}
}
fn cleanup(&mut self) {
self.number_of_points_dial.cleanup();
self.point_radius_handle.cleanup();
}
}
#[derive(Default)]
pub struct Polygon;
impl Polygon {
pub fn create_node(vertices: u32) -> NodeTemplate {
let node_type = resolve_document_node_type("Regular Polygon").expect("Regular Polygon can't be found");
node_type.node_template_input_override([None, Some(NodeInput::value(TaggedValue::U32(vertices), false)), Some(NodeInput::value(TaggedValue::F64(0.5), false))])
}
pub fn update_shape(
document: &DocumentMessageHandler,
ipp: &InputPreprocessorMessageHandler,
layer: LayerNodeIdentifier,
shape_tool_data: &mut ShapeToolData,
modifier: ShapeToolModifierKey,
responses: &mut VecDeque<Message>,
) {
let [center, lock_ratio, _, _] = modifier;
if let Some([start, end]) = shape_tool_data.data.calculate_points(document, ipp, center, lock_ratio) {
// TODO: We need to determine how to allow the polygon node to make irregular shapes
update_radius_sign(end, start, layer, document, responses);
let dimensions = (start - end).abs();
// We keep the smaller dimension's scale at 1 and scale the other dimension accordingly
let mut scale = DVec2::ONE;
let radius;
if dimensions.x > dimensions.y {
scale.x = dimensions.x / dimensions.y;
radius = dimensions.y / 2.;
} else {
scale.y = dimensions.y / dimensions.x;
radius = dimensions.x / 2.;
}
let Some(node_id) = graph_modification_utils::get_polygon_id(layer, &document.network_interface) else {
return;
};
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 2),
input: NodeInput::value(TaggedValue::F64(radius), false),
});
responses.add(GraphOperationMessage::TransformSet {
layer,
transform: DAffine2::from_scale_angle_translation(scale, 0., (start + end) / 2.),
transform_in: TransformIn::Viewport,
skip_rerender: false,
});
}
}
}
@@ -0,0 +1,54 @@
use super::shape_utility::ShapeToolModifierKey;
use super::*;
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
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, NodeTemplate};
use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::tool_messages::tool_prelude::*;
use glam::DAffine2;
use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue;
use std::collections::VecDeque;
#[derive(Default)]
pub struct Rectangle;
impl Rectangle {
pub fn create_node() -> NodeTemplate {
let node_type = resolve_document_node_type("Rectangle").expect("Rectangle node can't be found");
node_type.node_template_input_override([None, Some(NodeInput::value(TaggedValue::F64(1.), false)), Some(NodeInput::value(TaggedValue::F64(1.), false))])
}
pub fn update_shape(
document: &DocumentMessageHandler,
ipp: &InputPreprocessorMessageHandler,
layer: LayerNodeIdentifier,
shape_tool_data: &mut ShapeToolData,
modifier: ShapeToolModifierKey,
responses: &mut VecDeque<Message>,
) {
let [center, lock_ratio, _, _] = modifier;
if let Some([start, end]) = shape_tool_data.data.calculate_points(document, ipp, center, lock_ratio) {
let Some(node_id) = graph_modification_utils::get_rectangle_id(layer, &document.network_interface) else {
return;
};
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 1),
input: NodeInput::value(TaggedValue::F64((start.x - end.x).abs()), false),
});
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 2),
input: NodeInput::value(TaggedValue::F64((start.y - end.y).abs()), false),
});
responses.add(GraphOperationMessage::TransformSet {
layer,
transform: DAffine2::from_translation(start.midpoint(end)),
transform_in: TransformIn::Viewport,
skip_rerender: false,
});
}
}
}
@@ -0,0 +1,365 @@
use super::ShapeToolData;
use crate::messages::message::Message;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::InputConnector;
use crate::messages::prelude::{DocumentMessageHandler, InputPreprocessorMessageHandler, NodeGraphMessage, Responses};
use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer;
use crate::messages::tool::common_functionality::shape_editor::ShapeState;
use crate::messages::tool::common_functionality::transformation_cage::BoundingBoxManager;
use crate::messages::tool::tool_messages::tool_prelude::Key;
use crate::messages::tool::utility_types::*;
use bezier_rs::Subpath;
use glam::{DAffine2, DMat2, DVec2};
use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue;
use graphene_std::vector::click_target::ClickTargetType;
use graphene_std::vector::misc::dvec2_to_point;
use kurbo::{BezPath, PathEl, Shape};
use std::collections::VecDeque;
use std::f64::consts::{PI, TAU};
#[derive(Debug, Clone, Copy, Eq, PartialEq, Default, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum ShapeType {
#[default]
Polygon = 0,
Star = 1,
Rectangle = 2,
Ellipse = 3,
Line = 4,
}
impl ShapeType {
pub fn name(&self) -> String {
(match self {
Self::Polygon => "Polygon",
Self::Star => "Star",
Self::Rectangle => "Rectangle",
Self::Ellipse => "Ellipse",
Self::Line => "Line",
})
.into()
}
pub fn tooltip(&self) -> String {
(match self {
Self::Line => "Line Tool",
Self::Rectangle => "Rectangle Tool",
Self::Ellipse => "Ellipse Tool",
_ => "",
})
.into()
}
pub fn icon_name(&self) -> String {
(match self {
Self::Line => "VectorLineTool",
Self::Rectangle => "VectorRectangleTool",
Self::Ellipse => "VectorEllipseTool",
_ => "",
})
.into()
}
pub fn tool_type(&self) -> ToolType {
match self {
Self::Line => ToolType::Line,
Self::Rectangle => ToolType::Rectangle,
Self::Ellipse => ToolType::Ellipse,
_ => ToolType::Shape,
}
}
}
pub type ShapeToolModifierKey = [Key; 4];
/// The `ShapeGizmoHandler` trait defines the interactive behavior and overlay logic for shape-specific tools in the editor.
/// A gizmo is a visual handle or control point used to manipulate a shape's properties (e.g., number of sides, radius, angle).
pub trait ShapeGizmoHandler {
/// Called every frame to update the gizmo's interaction state based on the mouse position and selection.
///
/// This includes detecting hover states and preparing interaction flags or visual feedback (e.g., highlighting a hovered handle).
fn handle_state(&mut self, selected_shape_layers: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>);
/// Called when a mouse click occurs over the canvas and a gizmo handle is hovered.
///
/// Used to initiate drag interactions or toggle states on the handle, depending on the tool.
/// For example, a hovered "number of points" handle might enter a "Dragging" state.
fn handle_click(&mut self);
/// Called during a drag interaction to update the shape's parameters in real time.
///
/// For example, a handle might calculate the distance from the drag start to determine a new radius or update the number of points.
fn handle_update(&mut self, drag_start: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>);
/// Draws the static or hover-dependent overlays associated with the gizmo.
///
/// These overlays include visual indicators like shape outlines, control points, and hover highlights.
fn overlays(
&self,
document: &DocumentMessageHandler,
selected_shape_layers: Option<LayerNodeIdentifier>,
input: &InputPreprocessorMessageHandler,
shape_editor: &mut &mut ShapeState,
mouse_position: DVec2,
overlay_context: &mut OverlayContext,
);
/// Draws overlays specifically during a drag operation.
///
/// Used to give real-time visual feedback based on drag progress, such as showing the updated shape preview or snapping guides.
fn dragging_overlays(
&self,
document: &DocumentMessageHandler,
input: &InputPreprocessorMessageHandler,
shape_editor: &mut &mut ShapeState,
mouse_position: DVec2,
overlay_context: &mut OverlayContext,
);
/// Returns `true` if any handle or control point in the gizmo is currently being hovered.
fn is_any_gizmo_hovered(&self) -> bool;
/// Resets or clears any internal state maintained by the gizmo when it is no longer active.
///
/// For example, dragging states or hover flags should be cleared to avoid visual glitches when switching tools or shapes.
fn cleanup(&mut self);
}
/// Center, Lock Ratio, Lock Angle, Snap Angle, Increase/Decrease Side
pub fn update_radius_sign(end: DVec2, start: DVec2, layer: LayerNodeIdentifier, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
let sign_num = if end[1] > start[1] { 1. } else { -1. };
let new_layer = NodeGraphLayer::new(layer, &document.network_interface);
if new_layer.find_input("Regular Polygon", 1).unwrap_or(&TaggedValue::U32(0)).to_u32() % 2 == 1 {
let Some(polygon_node_id) = new_layer.upstream_node_id_from_name("Regular Polygon") else { return };
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(polygon_node_id, 2),
input: NodeInput::value(TaggedValue::F64(sign_num * 0.5), false),
});
return;
}
if new_layer.find_input("Star", 1).unwrap_or(&TaggedValue::U32(0)).to_u32() % 2 == 1 {
let Some(star_node_id) = new_layer.upstream_node_id_from_name("Star") else { return };
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(star_node_id, 2),
input: NodeInput::value(TaggedValue::F64(sign_num * 0.5), false),
});
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(star_node_id, 3),
input: NodeInput::value(TaggedValue::F64(sign_num * 0.25), false),
});
}
}
pub fn transform_cage_overlays(document: &DocumentMessageHandler, tool_data: &mut ShapeToolData, overlay_context: &mut OverlayContext) {
let mut transform = document
.network_interface
.selected_nodes()
.selected_visible_and_unlocked_layers(&document.network_interface)
.find(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[]))
.map(|layer| document.metadata().transform_to_viewport_with_first_transform_node_if_group(layer, &document.network_interface))
.unwrap_or_default();
// Check if the matrix is not invertible
let mut transform_tampered = false;
if transform.matrix2.determinant() == 0. {
transform.matrix2 += DMat2::IDENTITY * 1e-4; // TODO: Is this the cleanest way to handle this?
transform_tampered = true;
}
let bounds = document
.network_interface
.selected_nodes()
.selected_visible_and_unlocked_layers(&document.network_interface)
.filter(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[]))
.filter_map(|layer| {
document
.metadata()
.bounding_box_with_transform(layer, transform.inverse() * document.metadata().transform_to_viewport(layer))
})
.reduce(graphene_std::renderer::Quad::combine_bounds);
if let Some(bounds) = bounds {
let bounding_box_manager = tool_data.bounding_box_manager.get_or_insert(BoundingBoxManager::default());
bounding_box_manager.bounds = bounds;
bounding_box_manager.transform = transform;
bounding_box_manager.transform_tampered = transform_tampered;
bounding_box_manager.render_overlays(overlay_context, true);
} else {
tool_data.bounding_box_manager.take();
}
}
pub fn anchor_overlays(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) {
for layer in document.network_interface.selected_nodes().selected_layers(document.metadata()) {
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { continue };
let transform = document.metadata().transform_to_viewport(layer);
overlay_context.outline_vector(&vector_data, transform);
for (_, &position) in vector_data.point_domain.ids().iter().zip(vector_data.point_domain.positions()) {
overlay_context.manipulator_anchor(transform.transform_point2(position), false, None);
}
}
}
/// Extract the node input values of Star.
/// Returns an option of (sides, radius1, radius2).
pub fn extract_star_parameters(layer: Option<LayerNodeIdentifier>, document: &DocumentMessageHandler) -> Option<(u32, f64, f64)> {
let node_inputs = NodeGraphLayer::new(layer?, &document.network_interface).find_node_inputs("Star")?;
let (Some(&TaggedValue::U32(sides)), Some(&TaggedValue::F64(radius_1)), Some(&TaggedValue::F64(radius_2))) =
(node_inputs.get(1)?.as_value(), node_inputs.get(2)?.as_value(), node_inputs.get(3)?.as_value())
else {
return None;
};
Some((sides, radius_1, radius_2))
}
/// Extract the node input values of Polygon.
/// Returns an option of (sides, radius).
pub fn extract_polygon_parameters(layer: Option<LayerNodeIdentifier>, document: &DocumentMessageHandler) -> Option<(u32, f64)> {
let node_inputs = NodeGraphLayer::new(layer?, &document.network_interface).find_node_inputs("Regular Polygon")?;
let (Some(&TaggedValue::U32(n)), Some(&TaggedValue::F64(radius))) = (node_inputs.get(1)?.as_value(), node_inputs.get(2)?.as_value()) else {
return None;
};
Some((n, radius))
}
/// Calculate the viewport position of as a star vertex given its index
pub fn star_vertex_position(viewport: DAffine2, vertex_index: i32, n: u32, radius1: f64, radius2: f64) -> DVec2 {
let angle = ((vertex_index as f64) * PI) / (n as f64);
let radius = if vertex_index % 2 == 0 { radius1 } else { radius2 };
viewport.transform_point2(DVec2 {
x: radius * angle.sin(),
y: -radius * angle.cos(),
})
}
/// Calculate the viewport position of a polygon vertex given its index
pub fn polygon_vertex_position(viewport: DAffine2, vertex_index: i32, n: u32, radius: f64) -> DVec2 {
let angle = ((vertex_index as f64) * TAU) / (n as f64);
viewport.transform_point2(DVec2 {
x: radius * angle.sin(),
y: -radius * angle.cos(),
})
}
/// Outlines the geometric shape made by star-node
pub fn star_outline(layer: Option<LayerNodeIdentifier>, document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) {
let Some(layer) = layer else { return };
let Some((sides, radius1, radius2)) = extract_star_parameters(Some(layer), document) else {
return;
};
let viewport = document.metadata().transform_to_viewport(layer);
let points = sides as u64;
let diameter: f64 = radius1 * 2.;
let inner_diameter = radius2 * 2.;
let subpath: Vec<ClickTargetType> = vec![ClickTargetType::Subpath(Subpath::new_star_polygon(DVec2::splat(-diameter), points, diameter, inner_diameter))];
overlay_context.outline(subpath.iter(), viewport, None);
}
/// Outlines the geometric shape made by polygon-node
pub fn polygon_outline(layer: Option<LayerNodeIdentifier>, document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) {
let Some(layer) = layer else { return };
let Some((sides, radius)) = extract_polygon_parameters(Some(layer), document) else {
return;
};
let viewport = document.metadata().transform_to_viewport(layer);
let points = sides as u64;
let radius: f64 = radius * 2.;
let subpath: Vec<ClickTargetType> = vec![ClickTargetType::Subpath(Subpath::new_regular_polygon(DVec2::splat(-radius), points, radius))];
overlay_context.outline(subpath.iter(), viewport, None);
}
/// Check if the the cursor is inside the geometric star shape made by the Star node without any upstream node modifications
pub fn inside_star(viewport: DAffine2, n: u32, radius1: f64, radius2: f64, mouse_position: DVec2) -> bool {
let mut paths = Vec::new();
for i in 0..2 * n {
let new_point = dvec2_to_point(star_vertex_position(viewport, i as i32, n, radius1, radius2));
if i == 0 {
paths.push(PathEl::MoveTo(new_point));
} else {
paths.push(PathEl::LineTo(new_point));
}
}
paths.push(PathEl::ClosePath);
let bez_path = BezPath::from_vec(paths);
let (shape, bbox) = (bez_path.clone(), bez_path.bounding_box());
if bbox.x0 > mouse_position.x || bbox.y0 > mouse_position.y || bbox.x1 < mouse_position.x || bbox.y1 < mouse_position.y {
return false;
}
let winding = shape.winding(dvec2_to_point(mouse_position));
// Non-zero fill rule
winding != 0
}
/// Check if the the cursor is inside the geometric polygon shape made by the Polygon node without any upstream node modifications
pub fn inside_polygon(viewport: DAffine2, n: u32, radius: f64, mouse_position: DVec2) -> bool {
let mut paths = Vec::new();
for i in 0..n {
let new_point = dvec2_to_point(polygon_vertex_position(viewport, i as i32, n, radius));
if i == 0 {
paths.push(PathEl::MoveTo(new_point));
} else {
paths.push(PathEl::LineTo(new_point));
}
}
paths.push(PathEl::ClosePath);
let bez_path = BezPath::from_vec(paths);
let (shape, bbox) = (bez_path.clone(), bez_path.bounding_box());
if bbox.x0 > mouse_position.x || bbox.y0 > mouse_position.y || bbox.x1 < mouse_position.x || bbox.y1 < mouse_position.y {
return false;
}
let winding = shape.winding(dvec2_to_point(mouse_position));
// Non-zero fill rule
winding != 0
}
pub fn draw_snapping_ticks(snap_radii: &[f64], direction: DVec2, viewport: DAffine2, angle: f64, overlay_context: &mut OverlayContext) {
for &snapped_radius in snap_radii {
let Some(tick_direction) = direction.perp().try_normalize() else {
return;
};
let tick_position = viewport.transform_point2(DVec2 {
x: snapped_radius * angle.sin(),
y: -snapped_radius * angle.cos(),
});
overlay_context.line(tick_position, tick_position + tick_direction * 5., None, Some(2.));
overlay_context.line(tick_position, tick_position - tick_direction * 5., None, Some(2.));
}
}
@@ -0,0 +1,158 @@
use super::shape_utility::{ShapeToolModifierKey, update_radius_sign};
use super::*;
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate};
use crate::messages::tool::common_functionality::gizmos::shape_gizmos::number_of_points_dial::{NumberOfPointsDial, NumberOfPointsDialState};
use crate::messages::tool::common_functionality::gizmos::shape_gizmos::point_radius_handle::{PointRadiusHandle, PointRadiusHandleState};
use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::shape_editor::ShapeState;
use crate::messages::tool::common_functionality::shapes::shape_utility::{ShapeGizmoHandler, star_outline};
use crate::messages::tool::tool_messages::tool_prelude::*;
use core::f64;
use glam::DAffine2;
use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue;
use std::collections::VecDeque;
#[derive(Clone, Debug, Default)]
pub struct StarGizmoHandler {
number_of_points_dial: NumberOfPointsDial,
point_radius_handle: PointRadiusHandle,
}
impl ShapeGizmoHandler for StarGizmoHandler {
fn is_any_gizmo_hovered(&self) -> bool {
self.number_of_points_dial.is_hovering() || self.point_radius_handle.hovered()
}
fn handle_state(&mut self, selected_star_layer: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
self.number_of_points_dial.handle_actions(selected_star_layer, mouse_position, document, responses);
self.point_radius_handle.handle_actions(selected_star_layer, document, mouse_position, responses);
}
fn handle_click(&mut self) {
if self.number_of_points_dial.is_hovering() {
self.number_of_points_dial.update_state(NumberOfPointsDialState::Dragging);
return;
}
if self.point_radius_handle.hovered() {
self.point_radius_handle.update_state(PointRadiusHandleState::Dragging);
}
}
fn handle_update(&mut self, drag_start: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
if self.number_of_points_dial.is_dragging() {
self.number_of_points_dial.update_number_of_sides(document, input, responses, drag_start);
}
if self.point_radius_handle.is_dragging_or_snapped() {
self.point_radius_handle.update_inner_radius(document, input, responses, drag_start);
}
}
fn overlays(
&self,
document: &DocumentMessageHandler,
selected_star_layer: Option<LayerNodeIdentifier>,
input: &InputPreprocessorMessageHandler,
shape_editor: &mut &mut ShapeState,
mouse_position: DVec2,
overlay_context: &mut OverlayContext,
) {
self.number_of_points_dial.overlays(document, selected_star_layer, shape_editor, mouse_position, overlay_context);
self.point_radius_handle.overlays(selected_star_layer, document, input, mouse_position, overlay_context);
star_outline(selected_star_layer, document, overlay_context);
}
fn dragging_overlays(
&self,
document: &DocumentMessageHandler,
input: &InputPreprocessorMessageHandler,
shape_editor: &mut &mut ShapeState,
mouse_position: DVec2,
overlay_context: &mut OverlayContext,
) {
if self.number_of_points_dial.is_dragging() {
self.number_of_points_dial.overlays(document, None, shape_editor, mouse_position, overlay_context);
}
if self.point_radius_handle.is_dragging_or_snapped() {
self.point_radius_handle.overlays(None, document, input, mouse_position, overlay_context);
}
}
fn cleanup(&mut self) {
self.number_of_points_dial.cleanup();
self.point_radius_handle.cleanup();
}
}
#[derive(Default)]
pub struct Star;
impl Star {
pub fn create_node(vertices: u32) -> NodeTemplate {
let node_type = resolve_document_node_type("Star").expect("Star node can't be found");
node_type.node_template_input_override([
None,
Some(NodeInput::value(TaggedValue::U32(vertices), false)),
Some(NodeInput::value(TaggedValue::F64(0.5), false)),
Some(NodeInput::value(TaggedValue::F64(0.25), false)),
])
}
pub fn update_shape(
document: &DocumentMessageHandler,
ipp: &InputPreprocessorMessageHandler,
layer: LayerNodeIdentifier,
shape_tool_data: &mut ShapeToolData,
modifier: ShapeToolModifierKey,
responses: &mut VecDeque<Message>,
) {
let [center, lock_ratio, _, _] = modifier;
if let Some([start, end]) = shape_tool_data.data.calculate_points(document, ipp, center, lock_ratio) {
// TODO: We need to determine how to allow the polygon node to make irregular shapes
update_radius_sign(end, start, layer, document, responses);
let dimensions = (start - end).abs();
// We keep the smaller dimension's scale at 1 and scale the other dimension accordingly
let mut scale = DVec2::ONE;
let radius: f64;
if dimensions.x > dimensions.y {
scale.x = dimensions.x / dimensions.y;
radius = dimensions.y / 2.;
} else {
scale.y = dimensions.y / dimensions.x;
radius = dimensions.x / 2.;
}
let Some(node_id) = graph_modification_utils::get_star_id(layer, &document.network_interface) else {
return;
};
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 2),
input: NodeInput::value(TaggedValue::F64(radius), false),
});
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 3),
input: NodeInput::value(TaggedValue::F64(radius / 2.), false),
});
responses.add(GraphOperationMessage::TransformSet {
layer,
transform: DAffine2::from_scale_angle_translation(scale, 0., (start + end) / 2.),
transform_in: TransformIn::Viewport,
skip_rerender: false,
});
}
}
}
@@ -13,10 +13,10 @@ pub use alignment_snapper::*;
use bezier_rs::TValue;
pub use distribution_snapper::*;
use glam::{DAffine2, DVec2};
use graphene_core::renderer::Quad;
use graphene_core::vector::PointId;
use graphene_std::renderer::Quad;
use graphene_std::renderer::Rect;
use graphene_std::vector::NoHashBuilder;
use graphene_std::vector::PointId;
pub use grid_snapper::*;
pub use layer_snapper::*;
pub use snap_results::*;
@@ -250,6 +250,10 @@ impl SnapManager {
self.update_indicator(snapped);
}
pub fn indicator_pos(&self) -> Option<DVec2> {
self.indicator.as_ref().map(|point| point.snapped_point_document)
}
fn find_best_snap(snap_data: &mut SnapData, point: &SnapCandidatePoint, snap_results: SnapResults, constrained: bool, off_screen: bool, to_path: bool) -> SnappedPoint {
let mut snapped_points = Vec::new();
let document = snap_data.document;
@@ -449,7 +453,11 @@ impl SnapManager {
if let Some(ind) = &self.indicator {
for layer in &ind.outline_layers {
let &Some(layer) = layer else { continue };
overlay_context.outline(snap_data.document.metadata().layer_outline(layer), snap_data.document.metadata().transform_to_viewport(layer), None);
overlay_context.outline(
snap_data.document.metadata().layer_with_free_points_outline(layer),
snap_data.document.metadata().transform_to_viewport(layer),
None,
);
}
if let Some(quad) = ind.target_bounds {
overlay_context.quad(to_viewport * quad, None, None);
@@ -1,7 +1,7 @@
use super::*;
use crate::messages::portfolio::document::utility_types::misc::*;
use glam::{DAffine2, DVec2};
use graphene_core::renderer::Quad;
use graphene_std::renderer::Quad;
#[derive(Clone, Debug, Default)]
pub struct AlignmentSnapper {
@@ -70,7 +70,7 @@ impl AlignmentSnapper {
if let Some(quad) = target_point.quad.map(|q| q.0) {
if quad[0] == quad[3] && quad[1] == quad[2] && quad[0] == target_point.document_point {
let [p1, p2, ..] = quad;
let direction = (p2 - p1).normalize();
let Some(direction) = (p2 - p1).try_normalize() else { return };
let normal = DVec2::new(-direction.y, direction.x);
for endpoint in [p1, p2] {
@@ -2,7 +2,7 @@ use super::*;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::misc::*;
use glam::DVec2;
use graphene_core::renderer::Quad;
use graphene_std::renderer::Quad;
use std::collections::VecDeque;
#[derive(Clone, Debug, Default)]
@@ -382,317 +382,321 @@ impl DistributionSnapper {
}
}
#[test]
fn merge_intersecting_test() {
let mut rectangles = vec![Rect::from_square(DVec2::ZERO, 2.), Rect::from_square(DVec2::new(10., 0.), 2.)];
DistributionSnapper::merge_intersecting(&mut rectangles);
assert_eq!(rectangles.len(), 2);
let mut rectangles = vec![
Rect::from_square(DVec2::ZERO, 2.),
Rect::from_square(DVec2::new(1., 0.), 2.),
Rect::from_square(DVec2::new(10., 0.), 2.),
Rect::from_square(DVec2::new(11., 0.), 2.),
];
DistributionSnapper::merge_intersecting(&mut rectangles);
assert_eq!(rectangles.len(), 6);
assert_eq!(rectangles[0], Rect::from_box([DVec2::new(-2., -2.), DVec2::new(3., 2.)]));
assert_eq!(rectangles[3], Rect::from_box([DVec2::new(8., -2.), DVec2::new(13., 2.)]));
}
#[test]
fn dist_simple_2() {
let rectangles = [10., 20.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.));
let source = Rect::from_square(DVec2::new(0.5, 0.), 2.);
let (offset, rectangles) = DistributionSnapper::top_level_matches(source, &rectangles, 1., dist_right);
assert_eq!(offset, Some(DistributionMatch { first: 5.5, equal: 6. }));
assert_eq!(rectangles.len(), 2);
}
#[test]
fn dist_simple_3() {
let rectangles = [10., 20., 30.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.));
let source = Rect::from_square(DVec2::new(0.5, 0.), 2.);
let (offset, rectangles) = DistributionSnapper::top_level_matches(source, &rectangles, 1., dist_right);
assert_eq!(offset, Some(DistributionMatch { first: 5.5, equal: 6. }));
assert_eq!(rectangles.len(), 3);
}
#[test]
fn dist_out_of_tolerance() {
let rectangles = [10., 20.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.));
let source = Rect::from_square(DVec2::new(0.5, 0.), 2.);
let (offset, rectangles) = DistributionSnapper::top_level_matches(source, &rectangles, 0.4, dist_right);
assert_eq!(offset, None);
assert_eq!(rectangles.len(), 1);
}
#[test]
fn dist_with_nonsense() {
let source = Rect::from_square(DVec2::new(0.5, 0.), 2.);
let rectangles = [2., 10., 15., 20.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.));
let (offset, rectangles) = DistributionSnapper::top_level_matches(source, &rectangles, 1., dist_right);
assert_eq!(offset, Some(DistributionMatch { first: 5.5, equal: 6. }));
assert_eq!(rectangles.len(), 2);
}
#[cfg(test)]
fn assert_boxes_in_order(rectangles: &VecDeque<Rect>, index: usize) {
for (&first, &second) in rectangles.iter().zip(rectangles.iter().skip(1)) {
assert!(first.max()[index] < second.min()[index], "{first:?} {second:?} {index}")
mod tests {
use super::*;
#[test]
fn merge_intersecting_test() {
let mut rectangles = vec![Rect::from_square(DVec2::ZERO, 2.), Rect::from_square(DVec2::new(10., 0.), 2.)];
DistributionSnapper::merge_intersecting(&mut rectangles);
assert_eq!(rectangles.len(), 2);
let mut rectangles = vec![
Rect::from_square(DVec2::ZERO, 2.),
Rect::from_square(DVec2::new(1., 0.), 2.),
Rect::from_square(DVec2::new(10., 0.), 2.),
Rect::from_square(DVec2::new(11., 0.), 2.),
];
DistributionSnapper::merge_intersecting(&mut rectangles);
assert_eq!(rectangles.len(), 6);
assert_eq!(rectangles[0], Rect::from_box([DVec2::new(-2., -2.), DVec2::new(3., 2.)]));
assert_eq!(rectangles[3], Rect::from_box([DVec2::new(8., -2.), DVec2::new(13., 2.)]));
}
#[test]
fn dist_simple_2() {
let rectangles = [10., 20.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.));
let source = Rect::from_square(DVec2::new(0.5, 0.), 2.);
let (offset, rectangles) = DistributionSnapper::top_level_matches(source, &rectangles, 1., dist_right);
assert_eq!(offset, Some(DistributionMatch { first: 5.5, equal: 6. }));
assert_eq!(rectangles.len(), 2);
}
#[test]
fn dist_simple_3() {
let rectangles = [10., 20., 30.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.));
let source = Rect::from_square(DVec2::new(0.5, 0.), 2.);
let (offset, rectangles) = DistributionSnapper::top_level_matches(source, &rectangles, 1., dist_right);
assert_eq!(offset, Some(DistributionMatch { first: 5.5, equal: 6. }));
assert_eq!(rectangles.len(), 3);
}
#[test]
fn dist_out_of_tolerance() {
let rectangles = [10., 20.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.));
let source = Rect::from_square(DVec2::new(0.5, 0.), 2.);
let (offset, rectangles) = DistributionSnapper::top_level_matches(source, &rectangles, 0.4, dist_right);
assert_eq!(offset, None);
assert_eq!(rectangles.len(), 1);
}
#[test]
fn dist_with_nonsense() {
let source = Rect::from_square(DVec2::new(0.5, 0.), 2.);
let rectangles = [2., 10., 15., 20.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.));
let (offset, rectangles) = DistributionSnapper::top_level_matches(source, &rectangles, 1., dist_right);
assert_eq!(offset, Some(DistributionMatch { first: 5.5, equal: 6. }));
assert_eq!(rectangles.len(), 2);
}
#[cfg(test)]
fn assert_boxes_in_order(rectangles: &VecDeque<Rect>, index: usize) {
for (&first, &second) in rectangles.iter().zip(rectangles.iter().skip(1)) {
assert!(first.max()[index] < second.min()[index], "{first:?} {second:?} {index}")
}
}
#[test]
fn dist_snap_point_right() {
let dist_snapper = DistributionSnapper {
right: [2., 10., 15., 20.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.)).to_vec(),
left: [-2.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.)).to_vec(),
..Default::default()
};
let source = Rect::from_square(DVec2::new(0.5, 0.), 2.);
let snap_results = &mut SnapResults::default();
dist_snapper.snap_bbox_points(1., &SnapCandidatePoint::default(), snap_results, SnapConstraint::None, source);
assert_eq!(snap_results.points.len(), 1);
assert_eq!(snap_results.points[0].distance, 0.5);
assert_eq!(snap_results.points[0].distribution_equal_distance_horizontal, Some(6.));
let mut expected_box = Rect::from_square(DVec2::new(0., 0.), 2.);
expected_box[0].y = expected_box[0].y.min(dist_snapper.left[0][1].y);
expected_box[1].y = expected_box[1].y.min(dist_snapper.left[0][1].y);
assert_eq!(snap_results.points[0].distribution_boxes_horizontal.len(), 3);
assert_eq!(snap_results.points[0].distribution_boxes_horizontal[0], expected_box);
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_horizontal, 0);
}
#[test]
fn dist_snap_point_right_left() {
let dist_snapper = DistributionSnapper {
right: [2., 10., 15., 20.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.)).to_vec(),
left: [-2., -10., -15., -20.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.)).to_vec(),
..Default::default()
};
let source = Rect::from_square(DVec2::new(0.5, 0.), 2.);
let snap_results = &mut SnapResults::default();
dist_snapper.snap_bbox_points(1., &SnapCandidatePoint::default(), snap_results, SnapConstraint::None, source);
assert_eq!(snap_results.points.len(), 1);
assert_eq!(snap_results.points[0].distance, 0.5);
assert_eq!(snap_results.points[0].distribution_equal_distance_horizontal, Some(6.));
assert_eq!(snap_results.points[0].distribution_boxes_horizontal.len(), 5);
let mut expected_left1 = dist_snapper.left[1];
let mut expected_center = Rect::from_square(DVec2::new(0., 0.), 2.);
expected_center[0].y = expected_center[0].y.min(dist_snapper.left[1][1].y).min(dist_snapper.right[0][1].y);
expected_center[1].y = expected_center[1].y.min(dist_snapper.left[1][1].y).min(dist_snapper.right[0][1].y);
expected_left1[0].y = expected_left1[0].y.min(dist_snapper.left[0][1].y).min(expected_center[1].y);
expected_left1[1].y = expected_left1[1].y.min(dist_snapper.left[0][1].y).min(expected_center[1].y);
assert_eq!(snap_results.points[0].distribution_boxes_horizontal[1], expected_left1);
assert_eq!(snap_results.points[0].distribution_boxes_horizontal[2], expected_center);
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_horizontal, 0);
}
#[test]
fn dist_snap_point_left() {
let dist_snapper = DistributionSnapper {
left: [-2., -10., -15., -20.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.)).to_vec(),
..Default::default()
};
let source = Rect::from_square(DVec2::new(0.5, 0.), 2.);
let snap_results = &mut SnapResults::default();
dist_snapper.snap_bbox_points(1., &SnapCandidatePoint::default(), snap_results, SnapConstraint::None, source);
assert_eq!(snap_results.points.len(), 1);
assert_eq!(snap_results.points[0].distance, 0.5);
assert_eq!(snap_results.points[0].distribution_equal_distance_horizontal, Some(6.));
assert_eq!(snap_results.points[0].distribution_boxes_horizontal.len(), 3);
assert_eq!(snap_results.points[0].distribution_boxes_horizontal[2], Rect::from_square(DVec2::new(0., 0.), 2.));
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_horizontal, 0);
}
#[test]
fn dist_snap_point_left_right() {
let dist_snapper = DistributionSnapper {
left: [-2., -10., -15., -20.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.)).to_vec(),
right: [2., 10., 15.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.)).to_vec(),
..Default::default()
};
let source = Rect::from_square(DVec2::new(0.5, 0.), 2.);
let snap_results = &mut SnapResults::default();
dist_snapper.snap_bbox_points(1., &SnapCandidatePoint::default(), snap_results, SnapConstraint::None, source);
assert_eq!(snap_results.points.len(), 1);
assert_eq!(snap_results.points[0].distance, 0.5);
assert_eq!(snap_results.points[0].distribution_equal_distance_horizontal, Some(6.));
assert_eq!(snap_results.points[0].distribution_boxes_horizontal.len(), 4);
assert_eq!(snap_results.points[0].distribution_boxes_horizontal[2], Rect::from_square(DVec2::new(0., 0.), 2.));
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_horizontal, 0);
}
#[test]
fn dist_snap_point_center_x() {
let dist_snapper = DistributionSnapper {
left: [-10., -15.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.)).to_vec(),
right: [10., 15.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.)).to_vec(),
..Default::default()
};
let source = Rect::from_square(DVec2::new(0.5, 0.), 2.);
let snap_results = &mut SnapResults::default();
dist_snapper.snap_bbox_points(1., &SnapCandidatePoint::default(), snap_results, SnapConstraint::None, source);
assert_eq!(snap_results.points.len(), 1);
assert_eq!(snap_results.points[0].distance, 0.5);
assert_eq!(snap_results.points[0].distribution_equal_distance_horizontal, Some(6.));
let mut expected_box = Rect::from_square(DVec2::new(0., 0.), 2.);
expected_box[0].y = expected_box[0].y.min(dist_snapper.left[0][1].y);
expected_box[1].y = expected_box[1].y.min(dist_snapper.left[0][1].y);
assert_eq!(snap_results.points[0].distribution_boxes_horizontal.len(), 3);
assert_eq!(snap_results.points[0].distribution_boxes_horizontal[1], expected_box);
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_horizontal, 0);
}
// ----------------------------------
#[test]
fn dist_snap_point_down() {
let dist_snapper = DistributionSnapper {
down: [2., 10., 15., 20.].map(|y| Rect::from_square(DVec2::new(0., y), 2.)).to_vec(),
up: [-2.].map(|y| Rect::from_square(DVec2::new(0., y), 2.)).to_vec(),
..Default::default()
};
let source = Rect::from_square(DVec2::new(0., 0.5), 2.);
let snap_results = &mut SnapResults::default();
dist_snapper.snap_bbox_points(1., &SnapCandidatePoint::default(), snap_results, SnapConstraint::None, source);
assert_eq!(snap_results.points.len(), 1);
assert_eq!(snap_results.points[0].distance, 0.5);
assert_eq!(snap_results.points[0].distribution_equal_distance_vertical, Some(6.));
let mut expected_box = Rect::from_square(DVec2::new(0., 0.), 2.);
expected_box[0].x = expected_box[0].x.min(dist_snapper.down[0][1].x);
expected_box[1].x = expected_box[1].x.min(dist_snapper.down[0][1].x);
assert_eq!(snap_results.points[0].distribution_boxes_vertical.len(), 3);
assert_eq!(snap_results.points[0].distribution_boxes_vertical[0], expected_box);
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_vertical, 1);
}
#[test]
fn dist_snap_point_down_up() {
let dist_snapper = DistributionSnapper {
down: [2., 10., 15., 20.].map(|y| Rect::from_square(DVec2::new(0., y), 2.)).to_vec(),
up: [-2., -10., -15., -20.].map(|y| Rect::from_square(DVec2::new(0., y), 2.)).to_vec(),
..Default::default()
};
let source = Rect::from_square(DVec2::new(0., 0.5), 2.);
let snap_results = &mut SnapResults::default();
dist_snapper.snap_bbox_points(1., &SnapCandidatePoint::default(), snap_results, SnapConstraint::None, source);
assert_eq!(snap_results.points.len(), 1);
assert_eq!(snap_results.points[0].distance, 0.5);
assert_eq!(snap_results.points[0].distribution_equal_distance_vertical, Some(6.));
assert_eq!(snap_results.points[0].distribution_boxes_vertical.len(), 5);
let mut expected_center = Rect::from_square(DVec2::new(0., 0.), 2.);
expected_center[0].x = expected_center[0].x.min(dist_snapper.up[1][1].x).min(dist_snapper.down[0][1].x);
expected_center[1].x = expected_center[1].x.min(dist_snapper.up[1][1].x).min(dist_snapper.down[0][1].x);
let mut expected_up = Rect::from_square(DVec2::new(0., -10.), 2.);
expected_up[0].x = expected_up[0].x.min(dist_snapper.up[0][1].x).min(expected_center[0].x);
expected_up[1].x = expected_up[1].x.min(dist_snapper.up[0][1].x).min(expected_center[1].x);
assert_eq!(snap_results.points[0].distribution_boxes_vertical[1], expected_up);
assert_eq!(snap_results.points[0].distribution_boxes_vertical[2], expected_center);
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_vertical, 1);
}
#[test]
fn dist_snap_point_up() {
let dist_snapper = DistributionSnapper {
up: [-2., -10., -15., -20.].map(|y| Rect::from_square(DVec2::new(0., y), 2.)).to_vec(),
..Default::default()
};
let source = Rect::from_square(DVec2::new(0., 0.5), 2.);
let snap_results = &mut SnapResults::default();
dist_snapper.snap_bbox_points(1., &SnapCandidatePoint::default(), snap_results, SnapConstraint::None, source);
assert_eq!(snap_results.points.len(), 1);
assert_eq!(snap_results.points[0].distance, 0.5);
assert_eq!(snap_results.points[0].distribution_equal_distance_vertical, Some(6.));
assert_eq!(snap_results.points[0].distribution_boxes_vertical.len(), 3);
assert_eq!(snap_results.points[0].distribution_boxes_vertical[2], Rect::from_square(DVec2::new(0., 0.), 2.));
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_vertical, 1);
}
#[test]
fn dist_snap_point_up_down() {
let dist_snapper = DistributionSnapper {
up: [-2., -10., -15., -20.].map(|y| Rect::from_square(DVec2::new(0., y), 2.)).to_vec(),
down: [2., 10., 15.].map(|y| Rect::from_square(DVec2::new(0., y), 2.)).to_vec(),
..Default::default()
};
let source = Rect::from_square(DVec2::new(0., 0.5), 2.);
let snap_results = &mut SnapResults::default();
dist_snapper.snap_bbox_points(1., &SnapCandidatePoint::default(), snap_results, SnapConstraint::None, source);
assert_eq!(snap_results.points.len(), 1);
assert_eq!(snap_results.points[0].distance, 0.5);
assert_eq!(snap_results.points[0].distribution_equal_distance_vertical, Some(6.));
assert_eq!(snap_results.points[0].distribution_boxes_vertical.len(), 4);
assert_eq!(snap_results.points[0].distribution_boxes_vertical[2], Rect::from_square(DVec2::new(0., 0.), 2.));
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_vertical, 1);
}
#[test]
fn dist_snap_point_center_y() {
let dist_snapper = DistributionSnapper {
up: [-10., -15.].map(|y| Rect::from_square(DVec2::new(0., y), 2.)).to_vec(),
down: [10., 15.].map(|y| Rect::from_square(DVec2::new(0., y), 2.)).to_vec(),
..Default::default()
};
let source = Rect::from_square(DVec2::new(0., 0.5), 2.);
let snap_results = &mut SnapResults::default();
dist_snapper.snap_bbox_points(1., &SnapCandidatePoint::default(), snap_results, SnapConstraint::None, source);
assert_eq!(snap_results.points.len(), 1);
assert_eq!(snap_results.points[0].distance, 0.5);
assert_eq!(snap_results.points[0].distribution_equal_distance_vertical, Some(6.));
assert_eq!(snap_results.points[0].distribution_boxes_vertical.len(), 3);
let mut expected_box = Rect::from_square(DVec2::new(0., 0.), 2.);
expected_box[0].x = expected_box[0].x.min(dist_snapper.up[0][1].x).min(dist_snapper.down[0][1].x);
expected_box[1].x = expected_box[1].x.min(dist_snapper.up[0][1].x).min(dist_snapper.down[0][1].x);
assert_eq!(snap_results.points[0].distribution_boxes_vertical[1], expected_box);
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_vertical, 1);
}
#[test]
fn dist_snap_point_center_xy() {
let dist_snapper = DistributionSnapper {
up: [-10., -15.].map(|y| Rect::from_square(DVec2::new(0., y), 2.)).to_vec(),
down: [10., 15.].map(|y| Rect::from_square(DVec2::new(0., y), 2.)).to_vec(),
left: [-12., -15.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.)).to_vec(),
right: [12., 15.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.)).to_vec(),
..Default::default()
};
let source = Rect::from_square(DVec2::new(0.3, 0.4), 2.);
let snap_results = &mut SnapResults::default();
dist_snapper.snap_bbox_points(1., &SnapCandidatePoint::default(), snap_results, SnapConstraint::None, source);
assert_eq!(snap_results.points.len(), 1);
assert_eq!(snap_results.points[0].distance, 0.5000000000000001);
assert_eq!(snap_results.points[0].distribution_equal_distance_horizontal, Some(8.));
assert_eq!(snap_results.points[0].distribution_equal_distance_vertical, Some(6.));
assert_eq!(snap_results.points[0].distribution_boxes_horizontal.len(), 3);
assert_eq!(snap_results.points[0].distribution_boxes_vertical.len(), 3);
assert!(snap_results.points[0].distribution_boxes_horizontal[1][0].y <= dist_snapper.left[0][1].y);
assert!(snap_results.points[0].distribution_boxes_horizontal[1][1].y <= dist_snapper.left[0][1].y);
assert!(snap_results.points[0].distribution_boxes_vertical[1][0].x <= dist_snapper.up[0][1].x);
assert!(snap_results.points[0].distribution_boxes_vertical[1][1].x <= dist_snapper.up[0][1].x);
assert_eq!(Rect::from_box(snap_results.points[0].source_bounds.unwrap().bounding_box()), Rect::from_square(DVec2::new(0., 0.), 2.));
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_horizontal, 0);
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_vertical, 1);
}
}
#[test]
fn dist_snap_point_right() {
let dist_snapper = DistributionSnapper {
right: [2., 10., 15., 20.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.)).to_vec(),
left: [-2.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.)).to_vec(),
..Default::default()
};
let source = Rect::from_square(DVec2::new(0.5, 0.), 2.);
let snap_results = &mut SnapResults::default();
dist_snapper.snap_bbox_points(1., &SnapCandidatePoint::default(), snap_results, SnapConstraint::None, source);
assert_eq!(snap_results.points.len(), 1);
assert_eq!(snap_results.points[0].distance, 0.5);
assert_eq!(snap_results.points[0].distribution_equal_distance_horizontal, Some(6.));
let mut expected_box = Rect::from_square(DVec2::new(0., 0.), 2.);
expected_box[0].y = expected_box[0].y.min(dist_snapper.left[0][1].y);
expected_box[1].y = expected_box[1].y.min(dist_snapper.left[0][1].y);
assert_eq!(snap_results.points[0].distribution_boxes_horizontal.len(), 3);
assert_eq!(snap_results.points[0].distribution_boxes_horizontal[0], expected_box);
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_horizontal, 0);
}
#[test]
fn dist_snap_point_right_left() {
let dist_snapper = DistributionSnapper {
right: [2., 10., 15., 20.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.)).to_vec(),
left: [-2., -10., -15., -20.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.)).to_vec(),
..Default::default()
};
let source = Rect::from_square(DVec2::new(0.5, 0.), 2.);
let snap_results = &mut SnapResults::default();
dist_snapper.snap_bbox_points(1., &SnapCandidatePoint::default(), snap_results, SnapConstraint::None, source);
assert_eq!(snap_results.points.len(), 1);
assert_eq!(snap_results.points[0].distance, 0.5);
assert_eq!(snap_results.points[0].distribution_equal_distance_horizontal, Some(6.));
assert_eq!(snap_results.points[0].distribution_boxes_horizontal.len(), 5);
let mut expected_left1 = dist_snapper.left[1];
let mut expected_center = Rect::from_square(DVec2::new(0., 0.), 2.);
expected_center[0].y = expected_center[0].y.min(dist_snapper.left[1][1].y).min(dist_snapper.right[0][1].y);
expected_center[1].y = expected_center[1].y.min(dist_snapper.left[1][1].y).min(dist_snapper.right[0][1].y);
expected_left1[0].y = expected_left1[0].y.min(dist_snapper.left[0][1].y).min(expected_center[1].y);
expected_left1[1].y = expected_left1[1].y.min(dist_snapper.left[0][1].y).min(expected_center[1].y);
assert_eq!(snap_results.points[0].distribution_boxes_horizontal[1], expected_left1);
assert_eq!(snap_results.points[0].distribution_boxes_horizontal[2], expected_center);
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_horizontal, 0);
}
#[test]
fn dist_snap_point_left() {
let dist_snapper = DistributionSnapper {
left: [-2., -10., -15., -20.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.)).to_vec(),
..Default::default()
};
let source = Rect::from_square(DVec2::new(0.5, 0.), 2.);
let snap_results = &mut SnapResults::default();
dist_snapper.snap_bbox_points(1., &SnapCandidatePoint::default(), snap_results, SnapConstraint::None, source);
assert_eq!(snap_results.points.len(), 1);
assert_eq!(snap_results.points[0].distance, 0.5);
assert_eq!(snap_results.points[0].distribution_equal_distance_horizontal, Some(6.));
assert_eq!(snap_results.points[0].distribution_boxes_horizontal.len(), 3);
assert_eq!(snap_results.points[0].distribution_boxes_horizontal[2], Rect::from_square(DVec2::new(0., 0.), 2.));
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_horizontal, 0);
}
#[test]
fn dist_snap_point_left_right() {
let dist_snapper = DistributionSnapper {
left: [-2., -10., -15., -20.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.)).to_vec(),
right: [2., 10., 15.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.)).to_vec(),
..Default::default()
};
let source = Rect::from_square(DVec2::new(0.5, 0.), 2.);
let snap_results = &mut SnapResults::default();
dist_snapper.snap_bbox_points(1., &SnapCandidatePoint::default(), snap_results, SnapConstraint::None, source);
assert_eq!(snap_results.points.len(), 1);
assert_eq!(snap_results.points[0].distance, 0.5);
assert_eq!(snap_results.points[0].distribution_equal_distance_horizontal, Some(6.));
assert_eq!(snap_results.points[0].distribution_boxes_horizontal.len(), 4);
assert_eq!(snap_results.points[0].distribution_boxes_horizontal[2], Rect::from_square(DVec2::new(0., 0.), 2.));
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_horizontal, 0);
}
#[test]
fn dist_snap_point_center_x() {
let dist_snapper = DistributionSnapper {
left: [-10., -15.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.)).to_vec(),
right: [10., 15.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.)).to_vec(),
..Default::default()
};
let source = Rect::from_square(DVec2::new(0.5, 0.), 2.);
let snap_results = &mut SnapResults::default();
dist_snapper.snap_bbox_points(1., &SnapCandidatePoint::default(), snap_results, SnapConstraint::None, source);
assert_eq!(snap_results.points.len(), 1);
assert_eq!(snap_results.points[0].distance, 0.5);
assert_eq!(snap_results.points[0].distribution_equal_distance_horizontal, Some(6.));
let mut expected_box = Rect::from_square(DVec2::new(0., 0.), 2.);
expected_box[0].y = expected_box[0].y.min(dist_snapper.left[0][1].y);
expected_box[1].y = expected_box[1].y.min(dist_snapper.left[0][1].y);
assert_eq!(snap_results.points[0].distribution_boxes_horizontal.len(), 3);
assert_eq!(snap_results.points[0].distribution_boxes_horizontal[1], expected_box);
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_horizontal, 0);
}
// ----------------------------------
#[test]
fn dist_snap_point_down() {
let dist_snapper = DistributionSnapper {
down: [2., 10., 15., 20.].map(|y| Rect::from_square(DVec2::new(0., y), 2.)).to_vec(),
up: [-2.].map(|y| Rect::from_square(DVec2::new(0., y), 2.)).to_vec(),
..Default::default()
};
let source = Rect::from_square(DVec2::new(0., 0.5), 2.);
let snap_results = &mut SnapResults::default();
dist_snapper.snap_bbox_points(1., &SnapCandidatePoint::default(), snap_results, SnapConstraint::None, source);
assert_eq!(snap_results.points.len(), 1);
assert_eq!(snap_results.points[0].distance, 0.5);
assert_eq!(snap_results.points[0].distribution_equal_distance_vertical, Some(6.));
let mut expected_box = Rect::from_square(DVec2::new(0., 0.), 2.);
expected_box[0].x = expected_box[0].x.min(dist_snapper.down[0][1].x);
expected_box[1].x = expected_box[1].x.min(dist_snapper.down[0][1].x);
assert_eq!(snap_results.points[0].distribution_boxes_vertical.len(), 3);
assert_eq!(snap_results.points[0].distribution_boxes_vertical[0], expected_box);
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_vertical, 1);
}
#[test]
fn dist_snap_point_down_up() {
let dist_snapper = DistributionSnapper {
down: [2., 10., 15., 20.].map(|y| Rect::from_square(DVec2::new(0., y), 2.)).to_vec(),
up: [-2., -10., -15., -20.].map(|y| Rect::from_square(DVec2::new(0., y), 2.)).to_vec(),
..Default::default()
};
let source = Rect::from_square(DVec2::new(0., 0.5), 2.);
let snap_results = &mut SnapResults::default();
dist_snapper.snap_bbox_points(1., &SnapCandidatePoint::default(), snap_results, SnapConstraint::None, source);
assert_eq!(snap_results.points.len(), 1);
assert_eq!(snap_results.points[0].distance, 0.5);
assert_eq!(snap_results.points[0].distribution_equal_distance_vertical, Some(6.));
assert_eq!(snap_results.points[0].distribution_boxes_vertical.len(), 5);
let mut expected_center = Rect::from_square(DVec2::new(0., 0.), 2.);
expected_center[0].x = expected_center[0].x.min(dist_snapper.up[1][1].x).min(dist_snapper.down[0][1].x);
expected_center[1].x = expected_center[1].x.min(dist_snapper.up[1][1].x).min(dist_snapper.down[0][1].x);
let mut expected_up = Rect::from_square(DVec2::new(0., -10.), 2.);
expected_up[0].x = expected_up[0].x.min(dist_snapper.up[0][1].x).min(expected_center[0].x);
expected_up[1].x = expected_up[1].x.min(dist_snapper.up[0][1].x).min(expected_center[1].x);
assert_eq!(snap_results.points[0].distribution_boxes_vertical[1], expected_up);
assert_eq!(snap_results.points[0].distribution_boxes_vertical[2], expected_center);
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_vertical, 1);
}
#[test]
fn dist_snap_point_up() {
let dist_snapper = DistributionSnapper {
up: [-2., -10., -15., -20.].map(|y| Rect::from_square(DVec2::new(0., y), 2.)).to_vec(),
..Default::default()
};
let source = Rect::from_square(DVec2::new(0., 0.5), 2.);
let snap_results = &mut SnapResults::default();
dist_snapper.snap_bbox_points(1., &SnapCandidatePoint::default(), snap_results, SnapConstraint::None, source);
assert_eq!(snap_results.points.len(), 1);
assert_eq!(snap_results.points[0].distance, 0.5);
assert_eq!(snap_results.points[0].distribution_equal_distance_vertical, Some(6.));
assert_eq!(snap_results.points[0].distribution_boxes_vertical.len(), 3);
assert_eq!(snap_results.points[0].distribution_boxes_vertical[2], Rect::from_square(DVec2::new(0., 0.), 2.));
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_vertical, 1);
}
#[test]
fn dist_snap_point_up_down() {
let dist_snapper = DistributionSnapper {
up: [-2., -10., -15., -20.].map(|y| Rect::from_square(DVec2::new(0., y), 2.)).to_vec(),
down: [2., 10., 15.].map(|y| Rect::from_square(DVec2::new(0., y), 2.)).to_vec(),
..Default::default()
};
let source = Rect::from_square(DVec2::new(0., 0.5), 2.);
let snap_results = &mut SnapResults::default();
dist_snapper.snap_bbox_points(1., &SnapCandidatePoint::default(), snap_results, SnapConstraint::None, source);
assert_eq!(snap_results.points.len(), 1);
assert_eq!(snap_results.points[0].distance, 0.5);
assert_eq!(snap_results.points[0].distribution_equal_distance_vertical, Some(6.));
assert_eq!(snap_results.points[0].distribution_boxes_vertical.len(), 4);
assert_eq!(snap_results.points[0].distribution_boxes_vertical[2], Rect::from_square(DVec2::new(0., 0.), 2.));
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_vertical, 1);
}
#[test]
fn dist_snap_point_center_y() {
let dist_snapper = DistributionSnapper {
up: [-10., -15.].map(|y| Rect::from_square(DVec2::new(0., y), 2.)).to_vec(),
down: [10., 15.].map(|y| Rect::from_square(DVec2::new(0., y), 2.)).to_vec(),
..Default::default()
};
let source = Rect::from_square(DVec2::new(0., 0.5), 2.);
let snap_results = &mut SnapResults::default();
dist_snapper.snap_bbox_points(1., &SnapCandidatePoint::default(), snap_results, SnapConstraint::None, source);
assert_eq!(snap_results.points.len(), 1);
assert_eq!(snap_results.points[0].distance, 0.5);
assert_eq!(snap_results.points[0].distribution_equal_distance_vertical, Some(6.));
assert_eq!(snap_results.points[0].distribution_boxes_vertical.len(), 3);
let mut expected_box = Rect::from_square(DVec2::new(0., 0.), 2.);
expected_box[0].x = expected_box[0].x.min(dist_snapper.up[0][1].x).min(dist_snapper.down[0][1].x);
expected_box[1].x = expected_box[1].x.min(dist_snapper.up[0][1].x).min(dist_snapper.down[0][1].x);
assert_eq!(snap_results.points[0].distribution_boxes_vertical[1], expected_box);
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_vertical, 1);
}
#[test]
fn dist_snap_point_center_xy() {
let dist_snapper = DistributionSnapper {
up: [-10., -15.].map(|y| Rect::from_square(DVec2::new(0., y), 2.)).to_vec(),
down: [10., 15.].map(|y| Rect::from_square(DVec2::new(0., y), 2.)).to_vec(),
left: [-12., -15.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.)).to_vec(),
right: [12., 15.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.)).to_vec(),
..Default::default()
};
let source = Rect::from_square(DVec2::new(0.3, 0.4), 2.);
let snap_results = &mut SnapResults::default();
dist_snapper.snap_bbox_points(1., &SnapCandidatePoint::default(), snap_results, SnapConstraint::None, source);
assert_eq!(snap_results.points.len(), 1);
assert_eq!(snap_results.points[0].distance, 0.5000000000000001);
assert_eq!(snap_results.points[0].distribution_equal_distance_horizontal, Some(8.));
assert_eq!(snap_results.points[0].distribution_equal_distance_vertical, Some(6.));
assert_eq!(snap_results.points[0].distribution_boxes_horizontal.len(), 3);
assert_eq!(snap_results.points[0].distribution_boxes_vertical.len(), 3);
assert!(snap_results.points[0].distribution_boxes_horizontal[1][0].y <= dist_snapper.left[0][1].y);
assert!(snap_results.points[0].distribution_boxes_horizontal[1][1].y <= dist_snapper.left[0][1].y);
assert!(snap_results.points[0].distribution_boxes_vertical[1][0].x <= dist_snapper.up[0][1].x);
assert!(snap_results.points[0].distribution_boxes_vertical[1][1].x <= dist_snapper.up[0][1].x);
assert_eq!(Rect::from_box(snap_results.points[0].source_bounds.unwrap().bounding_box()), Rect::from_square(DVec2::new(0., 0.), 2.));
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_horizontal, 0);
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_vertical, 1);
}
@@ -1,7 +1,7 @@
use super::*;
use crate::messages::portfolio::document::utility_types::misc::{GridSnapTarget, GridSnapping, GridType, SnapTarget};
use glam::DVec2;
use graphene_core::renderer::Quad;
use graphene_std::renderer::Quad;
struct Line {
pub point: DVec2,
@@ -5,8 +5,9 @@ use crate::messages::portfolio::document::utility_types::misc::*;
use crate::messages::prelude::*;
use bezier_rs::{Bezier, Identifier, Subpath, TValue};
use glam::{DAffine2, DVec2};
use graphene_core::renderer::Quad;
use graphene_core::vector::PointId;
use graphene_std::math::math_ext::QuadExt;
use graphene_std::renderer::Quad;
use graphene_std::vector::PointId;
#[derive(Clone, Debug, Default)]
pub struct LayerSnapper {
@@ -4,9 +4,9 @@ use crate::messages::portfolio::document::utility_types::misc::{DistributionSnap
use crate::messages::tool::common_functionality::snapping::SnapCandidatePoint;
use bezier_rs::Bezier;
use glam::DVec2;
use graphene_core::renderer::Quad;
use graphene_core::vector::PointId;
use graphene_std::renderer::Quad;
use graphene_std::renderer::Rect;
use graphene_std::vector::PointId;
use std::collections::VecDeque;
#[derive(Clone, Debug, Default)]
@@ -1,8 +1,8 @@
use super::snapping::{self, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnappedPoint};
use crate::consts::{
BOUNDS_ROTATE_THRESHOLD, BOUNDS_SELECT_THRESHOLD, COLOR_OVERLAY_WHITE, MAXIMUM_ALT_SCALE_FACTOR, MIN_LENGTH_FOR_CORNERS_VISIBILITY, MIN_LENGTH_FOR_EDGE_RESIZE_PRIORITY_OVER_CORNERS,
MIN_LENGTH_FOR_MIDPOINT_VISIBILITY, MIN_LENGTH_FOR_RESIZE_TO_INCLUDE_INTERIOR, MIN_LENGTH_FOR_SKEW_TRIANGLE_VISIBILITY, RESIZE_HANDLE_SIZE, SELECTION_DRAG_ANGLE, SKEW_TRIANGLE_OFFSET,
SKEW_TRIANGLE_SIZE,
BOUNDS_ROTATE_THRESHOLD, BOUNDS_SELECT_THRESHOLD, COLOR_OVERLAY_WHITE, MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT, MAXIMUM_ALT_SCALE_FACTOR, MIN_LENGTH_FOR_CORNERS_VISIBILITY,
MIN_LENGTH_FOR_EDGE_RESIZE_PRIORITY_OVER_CORNERS, MIN_LENGTH_FOR_MIDPOINT_VISIBILITY, MIN_LENGTH_FOR_RESIZE_TO_INCLUDE_INTERIOR, MIN_LENGTH_FOR_SKEW_TRIANGLE_VISIBILITY, RESIZE_HANDLE_SIZE,
SELECTION_DRAG_ANGLE, SKEW_TRIANGLE_OFFSET, SKEW_TRIANGLE_SIZE,
};
use crate::messages::frontend::utility_types::MouseCursorIcon;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
@@ -11,7 +11,7 @@ use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::compass_rose::Axis;
use crate::messages::tool::common_functionality::snapping::SnapTypeConfiguration;
use glam::{DAffine2, DMat2, DVec2};
use graphene_core::renderer::Quad;
use graphene_std::renderer::Quad;
use graphene_std::renderer::Rect;
/// (top, bottom, left, right)
@@ -52,6 +52,8 @@ enum TransformCageSizeCategory {
Narrow,
/// - ![Diagram](https://files.keavon.com/-/OpenPaleturquoiseArthropods/capture.png)
Flat,
/// A single point in space with no width or height.
Point,
}
impl SelectedEdges {
@@ -431,46 +433,24 @@ impl BoundingBoxManager {
}
pub fn check_skew_handle(&self, cursor: DVec2, edge: EdgeBool) -> bool {
if let Some([start, end]) = self.edge_endpoints_vector_from_edge_bool(edge) {
if (end - start).length() < MIN_LENGTH_FOR_SKEW_TRIANGLE_VISIBILITY {
return false;
}
let Some([start, end]) = self.edge_endpoints_vector_from_edge_bool(edge) else { return false };
if (end - start).length_squared() < MIN_LENGTH_FOR_SKEW_TRIANGLE_VISIBILITY.powi(2) {
return false;
};
let touches_triangle = |base: DVec2, direction: DVec2, cursor: DVec2| -> bool {
let normal = direction.perp();
let top = base + direction * SKEW_TRIANGLE_SIZE;
let edge1 = base + normal * SKEW_TRIANGLE_SIZE / 2.;
let edge2 = base - normal * SKEW_TRIANGLE_SIZE / 2.;
let edge_dir = (end - start).normalize();
let mid = start.midpoint(end);
let v0 = edge1 - top;
let v1 = edge2 - top;
let v2 = cursor - top;
for direction in [-edge_dir, edge_dir] {
let base = mid + direction * (3. + SKEW_TRIANGLE_OFFSET + SKEW_TRIANGLE_SIZE / 2.);
let extension = cursor - base;
let along_edge = extension.dot(edge_dir).abs();
let along_perp = extension.perp_dot(edge_dir).abs();
let d00 = v0.dot(v0);
let d01 = v0.dot(v1);
let d11 = v1.dot(v1);
let d20 = v2.dot(v0);
let d21 = v2.dot(v1);
let denom = d00 * d11 - d01 * d01;
let v = (d11 * d20 - d01 * d21) / denom;
let w = (d00 * d21 - d01 * d20) / denom;
let u = 1. - v - w;
u >= 0. && v >= 0. && w >= 0.
};
let edge_dir = (end - start).normalize();
let mid = end.midpoint(start);
for direction in [edge_dir, -edge_dir] {
let base = mid + direction * (3. + SKEW_TRIANGLE_OFFSET);
if touches_triangle(base, direction, cursor) {
return true;
}
if along_edge <= SKEW_TRIANGLE_SIZE / 2. && along_perp <= BOUNDS_SELECT_THRESHOLD {
return true;
}
}
false
}
@@ -598,7 +578,9 @@ impl BoundingBoxManager {
category,
TransformCageSizeCategory::Full | TransformCageSizeCategory::Narrow | TransformCageSizeCategory::ReducedLandscape
) {
horizontal_edges.map(|point| draw_handle(point, horizontal_angle));
for point in horizontal_edges {
draw_handle(point, horizontal_angle);
}
}
// Draw the vertical midpoint drag handles
@@ -606,7 +588,9 @@ impl BoundingBoxManager {
category,
TransformCageSizeCategory::Full | TransformCageSizeCategory::Narrow | TransformCageSizeCategory::ReducedPortrait
) {
vertical_edges.map(|point| draw_handle(point, vertical_angle));
for point in vertical_edges {
draw_handle(point, vertical_angle);
}
}
let angle = quad
@@ -621,7 +605,9 @@ impl BoundingBoxManager {
category,
TransformCageSizeCategory::Full | TransformCageSizeCategory::ReducedBoth | TransformCageSizeCategory::ReducedLandscape | TransformCageSizeCategory::ReducedPortrait
) {
quad.0.map(|point| draw_handle(point, angle));
for point in quad.0 {
draw_handle(point, angle);
}
}
// Draw the flat line endpoint drag handles
@@ -635,7 +621,12 @@ impl BoundingBoxManager {
fn overlay_display_category(&self) -> TransformCageSizeCategory {
let quad = self.transform * Quad::from_box(self.bounds);
// Check if the area is essentially zero because either the width or height is smaller than an epsilon
// Check if the bounds are essentially the same because the width and height are smaller than MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT
if self.is_bounds_point() {
return TransformCageSizeCategory::Point;
}
// Check if the area is essentially zero because either the width or height is smaller than MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT
if self.is_bounds_flat() {
return TransformCageSizeCategory::Flat;
}
@@ -661,7 +652,12 @@ impl BoundingBoxManager {
/// Determine if these bounds are flat ([`TransformCageSizeCategory::Flat`]), which means that the width and/or height is essentially zero and the bounds are a line with effectively no area. This can happen on actual lines (axis-aligned, i.e. drawn horizontally or vertically) or when an element is scaled to zero in X or Y. A flat transform cage can still be rotated by a transformation, but its local space remains flat.
fn is_bounds_flat(&self) -> bool {
(self.bounds[0] - self.bounds[1]).abs().cmple(DVec2::splat(1e-4)).any()
(self.bounds[0] - self.bounds[1]).abs().cmple(DVec2::splat(MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT)).any()
}
/// Determine if these bounds are point ([`TransformCageSizeCategory::Point`]), which means that the width and height are essentially zero and the bounds are a point with no area. This can happen on points when an element is scaled to zero in both X and Y, or if an element is just a single anchor point. A point transform cage cannot be rotated by a transformation, and its local space remains a point.
fn is_bounds_point(&self) -> bool {
(self.bounds[0] - self.bounds[1]).abs().cmple(DVec2::splat(MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT)).all()
}
/// Determine if the given point in viewport space falls within the bounds of `self`.
@@ -699,7 +695,7 @@ impl BoundingBoxManager {
let [edge_min_x, edge_min_y] = self.compute_viewport_threshold(MIN_LENGTH_FOR_RESIZE_TO_INCLUDE_INTERIOR);
let [midpoint_threshold_x, midpoint_threshold_y] = self.compute_viewport_threshold(MIN_LENGTH_FOR_EDGE_RESIZE_PRIORITY_OVER_CORNERS);
if min.x - cursor.x < threshold_x && min.y - cursor.y < threshold_y && cursor.x - max.x < threshold_x && cursor.y - max.y < threshold_y {
if (min.x - cursor.x < threshold_x && min.y - cursor.y < threshold_y) && (cursor.x - max.x < threshold_x && cursor.y - max.y < threshold_y) {
let mut top = (cursor.y - min.y).abs() < threshold_y;
let mut bottom = (max.y - cursor.y).abs() < threshold_y;
let mut left = (cursor.x - min.x).abs() < threshold_x;
@@ -741,11 +737,11 @@ impl BoundingBoxManager {
}
// On bounds with no width/height, disallow transformation in the relevant axis
if width < f64::EPSILON * 1000. {
if width < MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT {
left = false;
right = false;
}
if height < f64::EPSILON * 1000. {
if height < MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT {
top = false;
bottom = false;
}
@@ -767,9 +763,12 @@ impl BoundingBoxManager {
let [threshold_x, threshold_y] = self.compute_viewport_threshold(BOUNDS_ROTATE_THRESHOLD);
let cursor = self.transform.inverse().transform_point2(cursor);
let flat = (self.bounds[0] - self.bounds[1]).abs().cmple(DVec2::splat(1e-4)).any();
let flat = self.is_bounds_flat();
let point = self.is_bounds_point();
let within_square_bounds = |center: &DVec2| center.x - threshold_x < cursor.x && cursor.x < center.x + threshold_x && center.y - threshold_y < cursor.y && cursor.y < center.y + threshold_y;
if flat {
if point {
false
} else if flat {
[self.bounds[0], self.bounds[1]].iter().any(within_square_bounds)
} else {
self.evaluate_transform_handle_positions().iter().any(within_square_bounds)
@@ -807,59 +806,64 @@ impl BoundingBoxManager {
}
}
#[test]
fn skew_transform_singular() {
for edge in [
SelectedEdges::new(true, false, false, false, [DVec2::NEG_ONE, DVec2::ONE]),
SelectedEdges::new(false, true, false, false, [DVec2::NEG_ONE, DVec2::ONE]),
SelectedEdges::new(false, false, true, false, [DVec2::NEG_ONE, DVec2::ONE]),
SelectedEdges::new(false, false, false, true, [DVec2::NEG_ONE, DVec2::ONE]),
] {
// The determinant is 0.
let transform = DAffine2::from_cols_array(&[2.; 6]);
// This shouldn't panic. We don't really care about the behavior in this test.
let _ = edge.skew_transform(DVec2::new(1.5, 1.5), transform, false);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn skew_transform_singular() {
for edge in [
SelectedEdges::new(true, false, false, false, [DVec2::NEG_ONE, DVec2::ONE]),
SelectedEdges::new(false, true, false, false, [DVec2::NEG_ONE, DVec2::ONE]),
SelectedEdges::new(false, false, true, false, [DVec2::NEG_ONE, DVec2::ONE]),
SelectedEdges::new(false, false, false, true, [DVec2::NEG_ONE, DVec2::ONE]),
] {
// The determinant is 0.
let transform = DAffine2::from_cols_array(&[2.; 6]);
// This shouldn't panic. We don't really care about the behavior in this test.
let _ = edge.skew_transform(DVec2::new(1.5, 1.5), transform, false);
}
}
}
#[test]
fn skew_transform_correct() {
for edge in [
SelectedEdges::new(true, false, false, false, [DVec2::NEG_ONE, DVec2::ONE]),
SelectedEdges::new(false, true, false, false, [DVec2::NEG_ONE, DVec2::ONE]),
SelectedEdges::new(false, false, true, false, [DVec2::NEG_ONE, DVec2::ONE]),
SelectedEdges::new(false, false, false, true, [DVec2::NEG_ONE, DVec2::ONE]),
] {
// Random transform with det != 0.
let to_viewport_transform = DAffine2::from_cols_array(&[2., 1., 0., 1., 2., 3.]);
// Random mouse position.
let mouse = DVec2::new(1.5, 1.5);
let final_transform = edge.skew_transform(mouse, to_viewport_transform, false);
// This is the current handle that goes under the mouse.
let opposite = edge.pivot_from_bounds(edge.bounds[0], edge.bounds[1]);
let dragging_point = edge.pivot_from_bounds(edge.bounds[1], edge.bounds[0]);
let viewport_dragging_point = to_viewport_transform.transform_point2(dragging_point);
let parallel_to_x = edge.top || edge.bottom;
let parallel_to_y = !parallel_to_x && (edge.left || edge.right);
let drag_vector = mouse - viewport_dragging_point;
let document_drag_vector = to_viewport_transform.inverse().transform_vector2(drag_vector);
let sign = if edge.top || edge.left { -1. } else { 1. };
let scale_factor = (edge.bounds[1] - edge.bounds[0])[parallel_to_x as usize].abs().recip() * sign;
let scaled_document_drag = document_drag_vector * scale_factor;
let skew = DAffine2::from_mat2(DMat2::from_cols_array(&[
1.,
if parallel_to_y { scaled_document_drag.y } else { 0. },
if parallel_to_x { scaled_document_drag.x } else { 0. },
1.,
]));
let constructed_transform = DAffine2::from_translation(opposite) * skew * DAffine2::from_translation(-opposite);
assert_eq!(constructed_transform, final_transform);
#[test]
fn skew_transform_correct() {
for edge in [
SelectedEdges::new(true, false, false, false, [DVec2::NEG_ONE, DVec2::ONE]),
SelectedEdges::new(false, true, false, false, [DVec2::NEG_ONE, DVec2::ONE]),
SelectedEdges::new(false, false, true, false, [DVec2::NEG_ONE, DVec2::ONE]),
SelectedEdges::new(false, false, false, true, [DVec2::NEG_ONE, DVec2::ONE]),
] {
// Random transform with det != 0.
let to_viewport_transform = DAffine2::from_cols_array(&[2., 1., 0., 1., 2., 3.]);
// Random mouse position.
let mouse = DVec2::new(1.5, 1.5);
let final_transform = edge.skew_transform(mouse, to_viewport_transform, false);
// This is the current handle that goes under the mouse.
let opposite = edge.pivot_from_bounds(edge.bounds[0], edge.bounds[1]);
let dragging_point = edge.pivot_from_bounds(edge.bounds[1], edge.bounds[0]);
let viewport_dragging_point = to_viewport_transform.transform_point2(dragging_point);
let parallel_to_x = edge.top || edge.bottom;
let parallel_to_y = !parallel_to_x && (edge.left || edge.right);
let drag_vector = mouse - viewport_dragging_point;
let document_drag_vector = to_viewport_transform.inverse().transform_vector2(drag_vector);
let sign = if edge.top || edge.left { -1. } else { 1. };
let scale_factor = (edge.bounds[1] - edge.bounds[0])[parallel_to_x as usize].abs().recip() * sign;
let scaled_document_drag = document_drag_vector * scale_factor;
let skew = DAffine2::from_mat2(DMat2::from_cols_array(&[
1.,
if parallel_to_y { scaled_document_drag.y } else { 0. },
if parallel_to_x { scaled_document_drag.x } else { 0. },
1.,
]));
let constructed_transform = DAffine2::from_translation(opposite) * skew * DAffine2::from_translation(-opposite);
assert_eq!(constructed_transform, final_transform);
}
}
}
@@ -1,11 +1,19 @@
use super::snapping::{SnapCandidatePoint, SnapData, SnapManager};
use super::transformation_cage::{BoundingBoxManager, SizeSnapData};
use crate::consts::ROTATE_INCREMENT;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::transformation::Selected;
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils::get_text;
use crate::messages::tool::common_functionality::transformation_cage::SelectedEdges;
use crate::messages::tool::tool_messages::path_tool::PathOverlayMode;
use glam::DVec2;
use graphene_core::renderer::Quad;
use graphene_core::text::{FontCache, load_face};
use graphene_std::vector::{ManipulatorPointId, PointId, SegmentId, VectorData};
use crate::messages::tool::utility_types::ToolType;
use bezier_rs::{Bezier, BezierHandles};
use glam::{DAffine2, DVec2};
use graphene_std::renderer::Quad;
use graphene_std::text::{FontCache, load_face};
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(
@@ -63,7 +71,7 @@ pub fn text_bounding_box(layer: LayerNodeIdentifier, document: &DocumentMessageH
};
let buzz_face = font_cache.get(font).map(|data| load_face(data));
let far = graphene_core::text::bounding_box(text, buzz_face.as_ref(), typesetting, false);
let far = graphene_std::text::bounding_box(text, buzz_face.as_ref(), typesetting, false);
Quad::from_box([DVec2::ZERO, far])
}
@@ -95,6 +103,65 @@ pub fn calculate_segment_angle(anchor: PointId, segment: SegmentId, vector_data:
required_handle.map(|handle| -(handle - anchor_position).angle_to(DVec2::X))
}
pub fn adjust_handle_colinearity(handle: HandleId, anchor_position: DVec2, target_control_point: DVec2, vector_data: &VectorData, layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) {
let Some(other_handle) = vector_data.other_colinear_handle(handle) else { return };
let Some(handle_position) = other_handle.to_manipulator_point().get_position(vector_data) else {
return;
};
let Some(direction) = (anchor_position - target_control_point).try_normalize() else { return };
let new_relative_position = (handle_position - anchor_position).length() * direction;
let modification_type = other_handle.set_relative_position(new_relative_position);
responses.add(GraphOperationMessage::Vector { layer, modification_type });
}
pub fn restore_previous_handle_position(
handle: HandleId,
original_c: DVec2,
anchor_position: DVec2,
vector_data: &VectorData,
layer: LayerNodeIdentifier,
responses: &mut VecDeque<Message>,
) -> Option<HandleId> {
let other_handle = vector_data.other_colinear_handle(handle)?;
let handle_position = other_handle.to_manipulator_point().get_position(vector_data)?;
let direction = (anchor_position - original_c).try_normalize()?;
let old_relative_position = (handle_position - anchor_position).length() * direction;
let modification_type = other_handle.set_relative_position(old_relative_position);
responses.add(GraphOperationMessage::Vector { layer, modification_type });
let handles = [handle, other_handle];
let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false };
responses.add(GraphOperationMessage::Vector { layer, modification_type });
Some(other_handle)
}
pub fn restore_g1_continuity(
handle: HandleId,
other_handle: HandleId,
control_point: DVec2,
anchor_position: DVec2,
vector_data: &VectorData,
layer: LayerNodeIdentifier,
responses: &mut VecDeque<Message>,
) {
let Some(handle_position) = other_handle.to_manipulator_point().get_position(vector_data) else {
return;
};
let Some(direction) = (anchor_position - control_point).try_normalize() else { return };
let new_relative_position = (handle_position - anchor_position).length() * direction;
let modification_type = other_handle.set_relative_position(new_relative_position);
responses.add(GraphOperationMessage::Vector { layer, modification_type });
let handles = [handle, other_handle];
let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: true };
responses.add(GraphOperationMessage::Vector { layer, modification_type });
}
/// Check whether a point is visible in the current overlay mode.
pub fn is_visible_point(
manipulator_point_id: ManipulatorPointId,
@@ -137,3 +204,387 @@ 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>,
bounds: &mut BoundingBoxManager,
dragging_layers: &mut Vec<LayerNodeIdentifier>,
snap_manager: &mut SnapManager,
snap_candidates: &mut Vec<SnapCandidatePoint>,
input: &InputPreprocessorMessageHandler,
center: bool,
constrain: bool,
tool: ToolType,
) {
if let Some(movement) = &mut bounds.selected_edges {
let center = center.then_some(bounds.center_of_transformation);
let snap = Some(SizeSnapData {
manager: snap_manager,
points: snap_candidates,
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);
let pivot_transform = DAffine2::from_translation(pivot);
let transformation = pivot_transform * delta * pivot_transform.inverse();
dragging_layers.retain(|layer| {
if *layer != LayerNodeIdentifier::ROOT_PARENT {
document.network_interface.document_network().nodes.contains_key(&layer.to_node())
} else {
log::error!("ROOT_PARENT should not be part of layers_dragging");
false
}
});
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>,
bounds: &mut BoundingBoxManager,
dragging_layers: &mut Vec<LayerNodeIdentifier>,
drag_start: DVec2,
mouse_position: DVec2,
snap_angle: bool,
tool: ToolType,
) {
let angle = {
let start_offset = drag_start - bounds.center_of_transformation;
let end_offset = mouse_position - bounds.center_of_transformation;
start_offset.angle_to(end_offset)
};
let snapped_angle = if snap_angle {
let snap_resolution = ROTATE_INCREMENT.to_radians();
(angle / snap_resolution).round() * snap_resolution
} else {
angle
};
let delta = DAffine2::from_angle(snapped_angle);
dragging_layers.retain(|layer| {
if *layer != LayerNodeIdentifier::ROOT_PARENT {
document.network_interface.document_network().nodes.contains_key(&layer.to_node())
} else {
log::error!("ROOT_PARENT should not be part of replacement_selected_layers");
false
}
});
let mut selected = Selected::new(
&mut bounds.original_transforms,
&mut bounds.center_of_transformation,
dragging_layers,
responses,
&document.network_interface,
None,
&tool,
None,
);
selected.update_transforms(delta, None, None);
}
pub fn skew_bounds(
document: &DocumentMessageHandler,
responses: &mut VecDeque<Message>,
bounds: &mut BoundingBoxManager,
free_movement: bool,
layers: &mut Vec<LayerNodeIdentifier>,
mouse_position: DVec2,
tool: ToolType,
) {
if let Some(movement) = &mut bounds.selected_edges {
let mut pivot = DVec2::ZERO;
let transformation = movement.skew_transform(mouse_position, bounds.original_bound_transform, free_movement);
layers.retain(|layer| {
if *layer != LayerNodeIdentifier::ROOT_PARENT {
document.network_interface.document_network().nodes.contains_key(&layer.to_node())
} else {
log::error!("ROOT_PARENT should not be part of layers_dragging");
false
}
});
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);
}
}
// TODO: Replace returned tuple (where at most 1 element is true at a time) with an enum.
/// Returns the tuple (resize, rotate, skew).
pub fn transforming_transform_cage(
document: &DocumentMessageHandler,
mut bounding_box_manager: &mut Option<BoundingBoxManager>,
input: &InputPreprocessorMessageHandler,
responses: &mut VecDeque<Message>,
layers_dragging: &mut Vec<LayerNodeIdentifier>,
) -> (bool, bool, bool) {
let dragging_bounds = bounding_box_manager.as_mut().and_then(|bounding_box| {
let edges = bounding_box.check_selected_edges(input.mouse.position);
bounding_box.selected_edges = edges.map(|(top, bottom, left, right)| {
let selected_edges = SelectedEdges::new(top, bottom, left, right, bounding_box.bounds);
bounding_box.opposite_pivot = selected_edges.calculate_pivot();
selected_edges
});
edges
});
let rotating_bounds = bounding_box_manager.as_ref().map(|bounding_box| bounding_box.check_rotate(input.mouse.position)).unwrap_or_default();
let selected: Vec<_> = document.network_interface.selected_nodes().selected_visible_and_unlocked_layers(&document.network_interface).collect();
let is_flat_layer = bounding_box_manager.as_ref().map(|bounding_box_manager| bounding_box_manager.transform_tampered).unwrap_or(true);
if dragging_bounds.is_some() && !is_flat_layer {
responses.add(DocumentMessage::StartTransaction);
*layers_dragging = selected;
if let Some(bounds) = &mut bounding_box_manager {
bounds.original_bound_transform = bounds.transform;
layers_dragging.retain(|layer| {
if *layer != LayerNodeIdentifier::ROOT_PARENT {
document.network_interface.document_network().nodes.contains_key(&layer.to_node())
} else {
log::error!("ROOT_PARENT should not be part of layers_dragging");
false
}
});
let mut selected = Selected::new(
&mut bounds.original_transforms,
&mut bounds.center_of_transformation,
layers_dragging,
responses,
&document.network_interface,
None,
&ToolType::Select,
None,
);
bounds.center_of_transformation = selected.mean_average_of_pivots();
// Check if we're hovering over a skew triangle
let edges = bounds.check_selected_edges(input.mouse.position);
if let Some(edges) = edges {
let closest_edge = bounds.get_closest_edge(edges, input.mouse.position);
if bounds.check_skew_handle(input.mouse.position, closest_edge) {
// No resize or rotate, just skew
return (false, false, true);
}
}
}
// Just resize, no rotate or skew
return (true, false, false);
}
if rotating_bounds {
responses.add(DocumentMessage::StartTransaction);
if let Some(bounds) = &mut bounding_box_manager {
layers_dragging.retain(|layer| {
if *layer != LayerNodeIdentifier::ROOT_PARENT {
document.network_interface.document_network().nodes.contains_key(&layer.to_node())
} else {
log::error!("ROOT_PARENT should not be part of layers_dragging");
false
}
});
let mut selected = Selected::new(
&mut bounds.original_transforms,
&mut bounds.center_of_transformation,
&selected,
responses,
&document.network_interface,
None,
&ToolType::Select,
None,
);
bounds.center_of_transformation = selected.mean_average_of_pivots();
}
*layers_dragging = selected;
// No resize or skew, just rotate
return (false, true, false);
}
// No resize, rotate, or skew
(false, false, false)
}
/// Calculates similarity metric between new bezier curve and two old beziers by using sampled points.
#[allow(clippy::too_many_arguments)]
pub fn log_optimization(a: f64, b: f64, p1: DVec2, p3: DVec2, d1: DVec2, d2: DVec2, points1: &[DVec2], n: usize) -> f64 {
let start_handle_length = a.exp();
let end_handle_length = b.exp();
// Compute the handle positions of new bezier curve
let c1 = p1 + d1 * start_handle_length;
let c2 = p3 + d2 * end_handle_length;
let new_curve = Bezier::from_cubic_coordinates(p1.x, p1.y, c1.x, c1.y, c2.x, c2.y, p3.x, p3.y);
// Sample 2*n points from new curve and get the L2 metric between all of points
let points = new_curve.compute_lookup_table(Some(2 * n), None).collect::<Vec<_>>();
let dist = points1.iter().zip(points.iter()).map(|(p1, p2)| (p1.x - p2.x).powi(2) + (p1.y - p2.y).powi(2)).sum::<f64>();
dist / (2 * n) as f64
}
/// Calculates optimal handle lengths with adam optimization.
#[allow(clippy::too_many_arguments)]
pub fn find_two_param_best_approximate(p1: DVec2, p3: DVec2, d1: DVec2, d2: DVec2, min_len1: f64, min_len2: f64, farther_segment: Bezier, other_segment: Bezier) -> (DVec2, DVec2) {
let h = 1e-6;
let tol = 1e-6;
let max_iter = 200;
let mut a = (5_f64).ln();
let mut b = (5_f64).ln();
let mut m_a = 0.;
let mut v_a = 0.;
let mut m_b = 0.;
let mut v_b = 0.;
let initial_alpha = 0.05;
let decay_rate: f64 = 0.99;
let beta1 = 0.9;
let beta2 = 0.999;
let epsilon = 1e-8;
let n = 20;
let farther_segment = if farther_segment.start.distance(p1) >= f64::EPSILON {
farther_segment.reverse()
} else {
farther_segment
};
let other_segment = if other_segment.end.distance(p3) >= f64::EPSILON { other_segment.reverse() } else { other_segment };
// Now we sample points proportional to the lengths of the beziers
let l1 = farther_segment.length(None);
let l2 = other_segment.length(None);
let ratio = l1 / (l1 + l2);
let n_points1 = ((2 * n) as f64 * ratio).floor() as usize;
let mut points1 = farther_segment.compute_lookup_table(Some(n_points1), None).collect::<Vec<_>>();
let mut points2 = other_segment.compute_lookup_table(Some(n), None).collect::<Vec<_>>();
points1.append(&mut points2);
let f = |a: f64, b: f64| -> f64 { log_optimization(a, b, p1, p3, d1, d2, &points1, n) };
for t in 1..=max_iter {
let dfa = (f(a + h, b) - f(a - h, b)) / (2. * h);
let dfb = (f(a, b + h) - f(a, b - h)) / (2. * h);
m_a = beta1 * m_a + (1. - beta1) * dfa;
m_b = beta1 * m_b + (1. - beta1) * dfb;
v_a = beta2 * v_a + (1. - beta2) * dfa * dfa;
v_b = beta2 * v_b + (1. - beta2) * dfb * dfb;
let m_a_hat = m_a / (1. - beta1.powi(t));
let v_a_hat = v_a / (1. - beta2.powi(t));
let m_b_hat = m_b / (1. - beta1.powi(t));
let v_b_hat = v_b / (1. - beta2.powi(t));
let alpha_t = initial_alpha * decay_rate.powi(t);
// Update log-lengths
a -= alpha_t * m_a_hat / (v_a_hat.sqrt() + epsilon);
b -= alpha_t * m_b_hat / (v_b_hat.sqrt() + epsilon);
// Convergence check
if dfa.abs() < tol && dfb.abs() < tol {
break;
}
}
let len1 = a.exp().max(min_len1);
let len2 = b.exp().max(min_len2);
(d1 * len1, d2 * len2)
}
+12 -19
View File
@@ -1,7 +1,7 @@
use super::utility_types::ToolType;
use crate::messages::preferences::SelectionMode;
use crate::messages::prelude::*;
use graphene_core::raster::color::Color;
use graphene_std::raster::color::Color;
#[impl_message(Message, Tool)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
@@ -32,13 +32,7 @@ pub enum ToolMessage {
#[child]
Spline(SplineToolMessage),
#[child]
Line(LineToolMessage),
#[child]
Rectangle(RectangleToolMessage),
#[child]
Ellipse(EllipseToolMessage),
#[child]
Polygon(PolygonToolMessage),
Shape(ShapeToolMessage),
#[child]
Text(TextToolMessage),
@@ -54,30 +48,29 @@ pub enum ToolMessage {
// Relight(RelightToolMessage),
// // #[child]
// Detail(DetailToolMessage),
// #[child]
// Imaginate(ImaginateToolMessage),
// Messages
// General tools
ActivateToolSelect,
ActivateToolArtboard,
ActivateToolNavigate,
ActivateToolEyedropper,
ActivateToolText,
ActivateToolFill,
ActivateToolGradient,
// Vector tools
ActivateToolPath,
ActivateToolPen,
ActivateToolFreehand,
ActivateToolSpline,
ActivateToolLine,
ActivateToolRectangle,
ActivateToolEllipse,
ActivateToolPolygon,
ActivateToolShapeLine,
ActivateToolShapeRectangle,
ActivateToolShapeEllipse,
ActivateToolShape,
ActivateToolText,
// Raster tools
ActivateToolBrush,
// ActivateToolImaginate,
//
ActivateTool {
tool_type: ToolType,
},
@@ -1,4 +1,5 @@
use super::common_functionality::shape_editor::ShapeState;
use super::common_functionality::shapes::shape_utility::ShapeType::{self, Ellipse, Line, Rectangle};
use super::utility_types::{ToolActionHandlerData, ToolFsmState, tool_message_to_tool_type};
use crate::application::generate_uuid;
use crate::messages::layout::utility_types::widget_prelude::*;
@@ -7,7 +8,7 @@ use crate::messages::portfolio::utility_types::PersistentData;
use crate::messages::prelude::*;
use crate::messages::tool::utility_types::ToolType;
use crate::node_graph_executor::NodeGraphExecutor;
use graphene_core::raster::color::Color;
use graphene_std::raster::color::Color;
const ARTBOARD_OVERLAY_PROVIDER: OverlayProvider = |context| DocumentMessage::DrawArtboardOverlays(context).into();
@@ -58,21 +59,46 @@ impl MessageHandler<ToolMessage, ToolMessageData<'_>> for ToolMessageHandler {
ToolMessage::ActivateToolPen => responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Pen }),
ToolMessage::ActivateToolFreehand => responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Freehand }),
ToolMessage::ActivateToolSpline => responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Spline }),
ToolMessage::ActivateToolLine => responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Line }),
ToolMessage::ActivateToolRectangle => responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Rectangle }),
ToolMessage::ActivateToolEllipse => responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Ellipse }),
ToolMessage::ActivateToolPolygon => responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Polygon }),
ToolMessage::ActivateToolShape => {
if self.tool_state.tool_data.active_shape_type.is_some() {
self.tool_state.tool_data.active_shape_type = None;
self.tool_state.tool_data.active_tool_type = ToolType::Shape;
}
responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Shape });
responses.add(ShapeToolMessage::SetShape(ShapeType::Polygon));
responses.add(ShapeToolMessage::HideShapeTypeWidget(false))
}
ToolMessage::ActivateToolBrush => responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Brush }),
// ToolMessage::ActivateToolImaginate => responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Imaginate }),
ToolMessage::ActivateToolShapeLine | ToolMessage::ActivateToolShapeRectangle | ToolMessage::ActivateToolShapeEllipse => {
let shape = match message {
ToolMessage::ActivateToolShapeLine => Line,
ToolMessage::ActivateToolShapeRectangle => Rectangle,
ToolMessage::ActivateToolShapeEllipse => Ellipse,
_ => unreachable!(),
};
self.tool_state.tool_data.active_shape_type = Some(shape.tool_type());
responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Shape });
responses.add(ShapeToolMessage::HideShapeTypeWidget(true));
responses.add(ShapeToolMessage::SetShape(shape));
}
ToolMessage::ActivateTool { tool_type } => {
let tool_data = &mut self.tool_state.tool_data;
let old_tool = tool_data.active_tool_type;
let old_tool = tool_data.active_tool_type.get_tool();
let tool_type = tool_type.get_tool();
responses.add(ToolMessage::RefreshToolOptions);
tool_data.send_layout(responses, LayoutTarget::ToolShelf);
// Do nothing if switching to the same tool
if self.tool_is_active && tool_type == old_tool {
return;
}
if tool_type != ToolType::Shape {
tool_data.active_shape_type = None;
}
self.tool_is_active = true;
// Send the old and new tools a transition to their FSM Abort states
@@ -299,7 +325,6 @@ impl MessageHandler<ToolMessage, ToolMessageData<'_>> for ToolMessageHandler {
ActivateToolArtboard,
ActivateToolNavigate,
ActivateToolEyedropper,
ActivateToolText,
ActivateToolFill,
ActivateToolGradient,
@@ -307,13 +332,13 @@ impl MessageHandler<ToolMessage, ToolMessageData<'_>> for ToolMessageHandler {
ActivateToolPen,
ActivateToolFreehand,
ActivateToolSpline,
ActivateToolLine,
ActivateToolRectangle,
ActivateToolEllipse,
ActivateToolPolygon,
ActivateToolShapeLine,
ActivateToolShapeRectangle,
ActivateToolShapeEllipse,
ActivateToolShape,
ActivateToolText,
ActivateToolBrush,
// ActivateToolImaginate,
SelectRandomPrimaryColor,
ResetColors,
@@ -11,7 +11,7 @@ use crate::messages::tool::common_functionality::snapping::SnapData;
use crate::messages::tool::common_functionality::snapping::SnapManager;
use crate::messages::tool::common_functionality::transformation_cage::*;
use graph_craft::document::NodeId;
use graphene_core::renderer::Quad;
use graphene_std::renderer::Quad;
#[derive(Default)]
pub struct ArtboardTool {
@@ -335,12 +335,12 @@ impl Fsm for ArtboardToolFsmState {
responses.add(GraphOperationMessage::NewArtboard {
id,
artboard: graphene_core::Artboard {
graphic_group: graphene_core::GraphicGroupTable::default(),
artboard: graphene_std::Artboard {
graphic_group: graphene_std::GraphicGroupTable::default(),
label: String::from("Artboard"),
location: start.min(end).round().as_ivec2(),
dimensions: (start.round() - end.round()).abs().as_ivec2(),
background: graphene_core::Color::WHITE,
background: graphene_std::Color::WHITE,
clip: false,
},
})
@@ -562,9 +562,12 @@ impl Fsm for ArtboardToolFsmState {
mod test_artboard {
pub use crate::test_utils::test_prelude::*;
async fn get_artboards(editor: &mut EditorTestUtils) -> Vec<graphene_core::Artboard> {
let instrumented = editor.eval_graph().await;
instrumented.grab_all_input::<graphene_core::append_artboard::ArtboardInput>(&editor.runtime).collect()
async fn get_artboards(editor: &mut EditorTestUtils) -> Vec<graphene_std::Artboard> {
let instrumented = match editor.eval_graph().await {
Ok(instrumented) => instrumented,
Err(e) => panic!("Failed to evaluate graph: {}", e),
};
instrumented.grab_all_input::<graphene_std::append_artboard::ArtboardInput>(&editor.runtime).collect()
}
#[tokio::test]
@@ -7,9 +7,9 @@ use crate::messages::portfolio::document::utility_types::network_interface::Flow
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType};
use graph_craft::document::NodeId;
use graph_craft::document::value::TaggedValue;
use graphene_core::Color;
use graphene_core::raster::BlendMode;
use graphene_core::vector::brush_stroke::{BrushInputSample, BrushStroke, BrushStyle};
use graphene_std::Color;
use graphene_std::raster::BlendMode;
use graphene_std::vector::brush_stroke::{BrushInputSample, BrushStroke, BrushStyle};
const BRUSH_MAX_SIZE: f64 = 5000.;
@@ -276,15 +276,16 @@ impl BrushToolData {
let Some(reference) = document.network_interface.reference(&node_id, &[]) else {
continue;
};
if *reference == Some("Brush".to_string()) && node_id != layer.to_node() {
let points_input = node.inputs.get(2)?;
let Some(TaggedValue::BrushStrokes(strokes)) = points_input.as_value() else {
continue;
};
let points_input = node.inputs.get(1)?;
let Some(TaggedValue::BrushStrokes(strokes)) = points_input.as_value() else { continue };
self.strokes.clone_from(strokes);
return Some(layer);
} else if *reference == Some("Transform".to_string()) {
}
if *reference == Some("Transform".to_string()) {
let upstream = document.metadata().upstream_transform(node_id);
let pivot = DAffine2::from_translation(upstream.transform_point2(get_current_normalized_pivot(&node.inputs)));
self.transform = pivot * get_current_transform(&node.inputs) * pivot.inverse() * self.transform;
@@ -1,441 +0,0 @@
use super::tool_prelude::*;
use crate::consts::DEFAULT_STROKE_WIDTH;
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::network_interface::InputConnector;
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType};
use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::resize::Resize;
use crate::messages::tool::common_functionality::snapping::SnapData;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput};
use graphene_core::Color;
#[derive(Default)]
pub struct EllipseTool {
fsm_state: EllipseToolFsmState,
data: EllipseToolData,
options: EllipseToolOptions,
}
pub struct EllipseToolOptions {
line_weight: f64,
fill: ToolColorOptions,
stroke: ToolColorOptions,
}
impl Default for EllipseToolOptions {
fn default() -> Self {
Self {
line_weight: DEFAULT_STROKE_WIDTH,
fill: ToolColorOptions::new_secondary(),
stroke: ToolColorOptions::new_primary(),
}
}
}
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum EllipseOptionsUpdate {
FillColor(Option<Color>),
FillColorType(ToolColorType),
LineWeight(f64),
StrokeColor(Option<Color>),
StrokeColorType(ToolColorType),
WorkingColors(Option<Color>, Option<Color>),
}
#[impl_message(Message, ToolMessage, Ellipse)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum EllipseToolMessage {
// Standard messages
Overlays(OverlayContext),
Abort,
WorkingColorChanged,
// Tool-specific messages
DragStart,
DragStop,
PointerMove { center: Key, lock_ratio: Key },
PointerOutsideViewport { center: Key, lock_ratio: Key },
UpdateOptions(EllipseOptionsUpdate),
}
impl ToolMetadata for EllipseTool {
fn icon_name(&self) -> String {
"VectorEllipseTool".into()
}
fn tooltip(&self) -> String {
"Ellipse Tool".into()
}
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
ToolType::Ellipse
}
}
fn create_weight_widget(line_weight: f64) -> WidgetHolder {
NumberInput::new(Some(line_weight))
.unit(" px")
.label("Weight")
.min(0.)
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
.on_update(|number_input: &NumberInput| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::LineWeight(number_input.value.unwrap())).into())
.widget_holder()
}
impl LayoutHolder for EllipseTool {
fn layout(&self) -> Layout {
let mut widgets = self.options.fill.create_widgets(
"Fill",
true,
|_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::FillColor(None)).into(),
|color_type: ToolColorType| WidgetCallback::new(move |_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::FillColorType(color_type.clone())).into()),
|color: &ColorInput| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(),
);
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
widgets.append(&mut self.options.stroke.create_widgets(
"Stroke",
true,
|_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::StrokeColor(None)).into(),
|color_type: ToolColorType| WidgetCallback::new(move |_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|color: &ColorInput| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(),
));
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
widgets.push(create_weight_widget(self.options.line_weight));
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
}
}
impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for EllipseTool {
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, tool_data: &mut ToolActionHandlerData<'a>) {
let ToolMessage::Ellipse(EllipseToolMessage::UpdateOptions(action)) = message else {
self.fsm_state.process_event(message, &mut self.data, tool_data, &self.options, responses, true);
return;
};
match action {
EllipseOptionsUpdate::FillColor(color) => {
self.options.fill.custom_color = color;
self.options.fill.color_type = ToolColorType::Custom;
}
EllipseOptionsUpdate::FillColorType(color_type) => self.options.fill.color_type = color_type,
EllipseOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight,
EllipseOptionsUpdate::StrokeColor(color) => {
self.options.stroke.custom_color = color;
self.options.stroke.color_type = ToolColorType::Custom;
}
EllipseOptionsUpdate::StrokeColorType(color_type) => self.options.stroke.color_type = color_type,
EllipseOptionsUpdate::WorkingColors(primary, secondary) => {
self.options.stroke.primary_working_color = primary;
self.options.stroke.secondary_working_color = secondary;
self.options.fill.primary_working_color = primary;
self.options.fill.secondary_working_color = secondary;
}
}
self.send_layout(responses, LayoutTarget::ToolOptions);
}
fn actions(&self) -> ActionList {
match self.fsm_state {
EllipseToolFsmState::Ready => actions!(EllipseToolMessageDiscriminant;
DragStart,
PointerMove,
),
EllipseToolFsmState::Drawing => actions!(EllipseToolMessageDiscriminant;
DragStop,
Abort,
PointerMove,
),
}
}
}
impl ToolTransition for EllipseTool {
fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap {
overlay_provider: Some(|overlay_context| EllipseToolMessage::Overlays(overlay_context).into()),
tool_abort: Some(EllipseToolMessage::Abort.into()),
working_color_changed: Some(EllipseToolMessage::WorkingColorChanged.into()),
..Default::default()
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
enum EllipseToolFsmState {
#[default]
Ready,
Drawing,
}
#[derive(Clone, Debug, Default)]
struct EllipseToolData {
data: Resize,
auto_panning: AutoPanning,
}
impl Fsm for EllipseToolFsmState {
type ToolData = EllipseToolData;
type ToolOptions = EllipseToolOptions;
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, global_tool_data, input, ..
} = tool_action_data;
let shape_data = &mut tool_data.data;
let ToolMessage::Ellipse(event) = event else { return self };
match (self, event) {
(_, EllipseToolMessage::Overlays(mut overlay_context)) => {
shape_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
self
}
(EllipseToolFsmState::Ready, EllipseToolMessage::DragStart) => {
shape_data.start(document, input);
responses.add(DocumentMessage::StartTransaction);
// Create a new ellipse vector shape
let node_type = resolve_document_node_type("Ellipse").expect("Ellipse node does not exist");
let node = node_type.node_template_input_override([None, Some(NodeInput::value(TaggedValue::F64(0.5), false)), Some(NodeInput::value(TaggedValue::F64(0.5), false))]);
let nodes = vec![(NodeId(0), node)];
let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, document.new_layer_bounding_artboard(input), responses);
responses.add(Message::StartBuffer);
responses.add(GraphOperationMessage::TransformSet {
layer,
transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position),
transform_in: TransformIn::Viewport,
skip_rerender: false,
});
tool_options.fill.apply_fill(layer, responses);
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses);
shape_data.layer = Some(layer);
EllipseToolFsmState::Drawing
}
(EllipseToolFsmState::Drawing, EllipseToolMessage::PointerMove { center, lock_ratio }) => {
if let Some([start, end]) = shape_data.calculate_points(document, input, center, lock_ratio) {
if let Some(layer) = shape_data.layer {
let Some(node_id) = graph_modification_utils::get_ellipse_id(layer, &document.network_interface) else {
return self;
};
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 1),
input: NodeInput::value(TaggedValue::F64(((start.x - end.x) / 2.).abs()), false),
});
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 2),
input: NodeInput::value(TaggedValue::F64(((start.y - end.y) / 2.).abs()), false),
});
responses.add(GraphOperationMessage::TransformSet {
layer,
transform: DAffine2::from_translation((start + end) / 2.),
transform_in: TransformIn::Viewport,
skip_rerender: false,
});
}
}
// Auto-panning
let messages = [
EllipseToolMessage::PointerOutsideViewport { center, lock_ratio }.into(),
EllipseToolMessage::PointerMove { center, lock_ratio }.into(),
];
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses);
self
}
(_, EllipseToolMessage::PointerMove { .. }) => {
shape_data.snap_manager.preview_draw(&SnapData::new(document, input), input.mouse.position);
responses.add(OverlaysMessage::Draw);
self
}
(EllipseToolFsmState::Drawing, EllipseToolMessage::PointerOutsideViewport { .. }) => {
// Auto-panning
let _ = tool_data.auto_panning.shift_viewport(input, responses);
EllipseToolFsmState::Drawing
}
(state, EllipseToolMessage::PointerOutsideViewport { center, lock_ratio }) => {
// Auto-panning
let messages = [
EllipseToolMessage::PointerOutsideViewport { center, lock_ratio }.into(),
EllipseToolMessage::PointerMove { center, lock_ratio }.into(),
];
tool_data.auto_panning.stop(&messages, responses);
state
}
(EllipseToolFsmState::Drawing, EllipseToolMessage::DragStop) => {
input.mouse.finish_transaction(shape_data.viewport_drag_start(document), responses);
shape_data.cleanup(responses);
EllipseToolFsmState::Ready
}
(EllipseToolFsmState::Drawing, EllipseToolMessage::Abort) => {
responses.add(DocumentMessage::AbortTransaction);
shape_data.cleanup(responses);
EllipseToolFsmState::Ready
}
(_, EllipseToolMessage::WorkingColorChanged) => {
responses.add(EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::WorkingColors(
Some(global_tool_data.primary_color),
Some(global_tool_data.secondary_color),
)));
self
}
_ => self,
}
}
fn update_hints(&self, responses: &mut VecDeque<Message>) {
let hint_data = match self {
EllipseToolFsmState::Ready => HintData(vec![HintGroup(vec![
HintInfo::mouse(MouseMotion::LmbDrag, "Draw Ellipse"),
HintInfo::keys([Key::Shift], "Constrain Circular").prepend_plus(),
HintInfo::keys([Key::Alt], "From Center").prepend_plus(),
])]),
EllipseToolFsmState::Drawing => HintData(vec![
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Circular"), HintInfo::keys([Key::Alt], "From Center")]),
]),
};
responses.add(FrontendMessage::UpdateInputHints { hint_data });
}
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair });
}
}
#[cfg(test)]
mod test_ellipse {
pub use crate::test_utils::test_prelude::*;
use glam::DAffine2;
use graphene_core::vector::generator_nodes::ellipse;
#[derive(Debug, PartialEq)]
struct ResolvedEllipse {
radius_x: f64,
radius_y: f64,
transform: DAffine2,
}
async fn get_ellipse(editor: &mut EditorTestUtils) -> Vec<ResolvedEllipse> {
let instrumented = editor.eval_graph().await;
let document = editor.active_document();
let layers = document.metadata().all_layers();
layers
.filter_map(|layer| {
let node_graph_layer = NodeGraphLayer::new(layer, &document.network_interface);
let ellipse_node = node_graph_layer.upstream_node_id_from_protonode(ellipse::protonode_identifier())?;
Some(ResolvedEllipse {
radius_x: instrumented.grab_protonode_input::<ellipse::RadiusXInput>(&vec![ellipse_node], &editor.runtime).unwrap(),
radius_y: instrumented.grab_protonode_input::<ellipse::RadiusYInput>(&vec![ellipse_node], &editor.runtime).unwrap(),
transform: document.metadata().transform_to_document(layer),
})
})
.collect()
}
#[tokio::test]
async fn ellipse_draw_simple() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor.drag_tool(ToolType::Ellipse, 10., 10., 19., 0., ModifierKeys::empty()).await;
assert_eq!(editor.active_document().metadata().all_layers().count(), 1);
let ellipse = get_ellipse(&mut editor).await;
assert_eq!(ellipse.len(), 1);
assert_eq!(
ellipse[0],
ResolvedEllipse {
radius_x: 4.5,
radius_y: 5.,
transform: DAffine2::from_translation(DVec2::new(14.5, 5.)) // Uses center
}
);
}
#[tokio::test]
async fn ellipse_draw_circle() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor.drag_tool(ToolType::Ellipse, 10., 10., -10., 11., ModifierKeys::SHIFT).await;
let ellipse = get_ellipse(&mut editor).await;
assert_eq!(ellipse.len(), 1);
assert_eq!(
ellipse[0],
ResolvedEllipse {
radius_x: 10.,
radius_y: 10.,
transform: DAffine2::from_translation(DVec2::new(0., 20.)) // Uses center
}
);
}
#[tokio::test]
async fn ellipse_draw_square_rotated() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor
.handle_message(NavigationMessage::CanvasTiltSet {
// 45 degree rotation of content clockwise
angle_radians: f64::consts::FRAC_PI_4,
})
.await;
editor.drag_tool(ToolType::Ellipse, 0., 0., 1., 10., ModifierKeys::SHIFT).await; // Viewport coordinates
let ellipse = get_ellipse(&mut editor).await;
assert_eq!(ellipse.len(), 1);
println!("{ellipse:?}");
assert_eq!(ellipse[0].radius_x, 5.);
assert_eq!(ellipse[0].radius_y, 5.);
assert!(
ellipse[0]
.transform
.abs_diff_eq(DAffine2::from_angle_translation(-f64::consts::FRAC_PI_4, DVec2::X * f64::consts::FRAC_1_SQRT_2 * 10.), 0.001)
);
}
#[tokio::test]
async fn ellipse_draw_center_square_rotated() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor
.handle_message(NavigationMessage::CanvasTiltSet {
// 45 degree rotation of content clockwise
angle_radians: f64::consts::FRAC_PI_4,
})
.await;
editor.drag_tool(ToolType::Ellipse, 0., 0., 1., 10., ModifierKeys::SHIFT | ModifierKeys::ALT).await; // Viewport coordinates
let ellipse = get_ellipse(&mut editor).await;
assert_eq!(ellipse.len(), 1);
assert_eq!(ellipse[0].radius_x, 10.);
assert_eq!(ellipse[0].radius_y, 10.);
assert!(ellipse[0].transform.abs_diff_eq(DAffine2::from_angle(-f64::consts::FRAC_PI_4), 0.001));
}
#[tokio::test]
async fn ellipse_cancel() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor.drag_tool_cancel_rmb(ToolType::Ellipse).await;
let ellipse = get_ellipse(&mut editor).await;
assert_eq!(ellipse.len(), 0);
}
}
@@ -1,7 +1,7 @@
use super::tool_prelude::*;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer;
use graphene_core::vector::style::Fill;
use graphene_std::vector::style::Fill;
#[derive(Default)]
pub struct FillTool {
@@ -157,11 +157,14 @@ impl Fsm for FillToolFsmState {
#[cfg(test)]
mod test_fill {
pub use crate::test_utils::test_prelude::*;
use graphene_core::vector::fill;
use graphene_std::vector::fill;
use graphene_std::vector::style::Fill;
async fn get_fills(editor: &mut EditorTestUtils) -> Vec<Fill> {
let instrumented = editor.eval_graph().await;
let instrumented = match editor.eval_graph().await {
Ok(instrumented) => instrumented,
Err(e) => panic!("Failed to evaluate graph: {e}"),
};
instrumented.grab_all_input::<fill::FillInput<Fill>>(&editor.runtime).collect()
}
@@ -9,8 +9,8 @@ use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::utility_functions::should_extend;
use glam::DVec2;
use graph_craft::document::NodeId;
use graphene_core::Color;
use graphene_core::vector::VectorModificationType;
use graphene_std::Color;
use graphene_std::vector::VectorModificationType;
use graphene_std::vector::{PointId, SegmentId};
#[derive(Default)]
@@ -349,7 +349,7 @@ mod test_freehand {
use crate::messages::tool::tool_messages::freehand_tool::FreehandOptionsUpdate;
use crate::test_utils::test_prelude::*;
use glam::{DAffine2, DVec2};
use graphene_core::vector::VectorData;
use graphene_std::vector::VectorData;
async fn get_vector_data(editor: &mut EditorTestUtils) -> Vec<(VectorData, DAffine2)> {
let document = editor.active_document();
@@ -416,7 +416,7 @@ mod test_freehand {
editor
.handle_message(GraphOperationMessage::TransformSet {
layer: artboard,
transform: DAffine2::from_scale_angle_translation(DVec2::new(1.5, 0.8), 0.3, DVec2::new(10.0, -5.0)),
transform: DAffine2::from_scale_angle_translation(DVec2::new(1.5, 0.8), 0.3, DVec2::new(10., -5.)),
transform_in: TransformIn::Local,
skip_rerender: false,
})
@@ -424,14 +424,14 @@ mod test_freehand {
editor.select_tool(ToolType::Freehand).await;
let mouse_points = [DVec2::new(150.0, 100.0), DVec2::new(200.0, 150.0), DVec2::new(250.0, 130.0), DVec2::new(300.0, 170.0)];
let mouse_points = [DVec2::new(150., 100.), DVec2::new(200., 150.), DVec2::new(250., 130.), DVec2::new(300., 170.)];
// Expected points that will actually be captured by the tool
let expected_captured_points = &mouse_points[1..];
editor.drag_path(&mouse_points, ModifierKeys::empty()).await;
let vector_data_list = get_vector_data(&mut editor).await;
verify_path_points(&vector_data_list, expected_captured_points, 1.0).expect("Path points verification failed");
verify_path_points(&vector_data_list, expected_captured_points, 1.).expect("Path points verification failed");
}
#[tokio::test]
@@ -439,7 +439,7 @@ mod test_freehand {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
let initial_points = [DVec2::new(100.0, 100.0), DVec2::new(200.0, 200.0), DVec2::new(300.0, 100.0)];
let initial_points = [DVec2::new(100., 100.), DVec2::new(200., 200.), DVec2::new(300., 100.)];
editor.select_tool(ToolType::Freehand).await;
@@ -491,7 +491,7 @@ mod test_freehand {
assert!(endpoint_viewport_pos.is_finite(), "Endpoint position is not finite");
let extension_points = [DVec2::new(400.0, 200.0), DVec2::new(500.0, 100.0)];
let extension_points = [DVec2::new(400., 200.), DVec2::new(500., 100.)];
let layer_node_id = {
let document = editor.active_document();
@@ -558,7 +558,7 @@ mod test_freehand {
editor.select_tool(ToolType::Freehand).await;
let initial_points = [DVec2::new(100.0, 100.0), DVec2::new(200.0, 200.0), DVec2::new(300.0, 100.0)];
let initial_points = [DVec2::new(100., 100.), DVec2::new(200., 200.), DVec2::new(300., 100.)];
let first_point = initial_points[0];
editor.move_mouse(first_point.x, first_point.y, ModifierKeys::empty(), MouseKeys::empty()).await;
@@ -599,7 +599,7 @@ mod test_freehand {
})
.await;
let second_path_points = [DVec2::new(400.0, 100.0), DVec2::new(500.0, 200.0), DVec2::new(600.0, 100.0)];
let second_path_points = [DVec2::new(400., 100.), DVec2::new(500., 200.), DVec2::new(600., 100.)];
let first_second_point = second_path_points[0];
editor.move_mouse(first_second_point.x, first_second_point.y, ModifierKeys::SHIFT, MouseKeys::empty()).await;
@@ -677,12 +677,12 @@ mod test_freehand {
editor.select_tool(ToolType::Freehand).await;
let custom_line_weight = 5.0;
let custom_line_weight = 5.;
editor
.handle_message(ToolMessage::Freehand(FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::LineWeight(custom_line_weight))))
.await;
let points = [DVec2::new(100.0, 100.0), DVec2::new(200.0, 200.0), DVec2::new(300.0, 100.0)];
let points = [DVec2::new(100., 100.), DVec2::new(200., 200.), DVec2::new(300., 100.)];
let first_point = points[0];
editor.move_mouse(first_point.x, first_point.y, ModifierKeys::empty(), MouseKeys::empty()).await;
@@ -5,7 +5,7 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
use crate::messages::tool::common_functionality::graph_modification_utils::{NodeGraphLayer, get_gradient};
use crate::messages::tool::common_functionality::snapping::SnapManager;
use graphene_core::vector::style::{Fill, Gradient, GradientType};
use graphene_std::vector::style::{Fill, Gradient, GradientType};
#[derive(Default)]
pub struct GradientTool {
@@ -535,17 +535,21 @@ impl Fsm for GradientToolFsmState {
mod test_gradient {
use crate::messages::input_mapper::utility_types::input_mouse::EditorMouseState;
use crate::messages::input_mapper::utility_types::input_mouse::ScrollDelta;
use crate::messages::portfolio::document::{graph_operation::utility_types::TransformIn, utility_types::misc::GroupFolderType};
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
use crate::messages::portfolio::document::utility_types::misc::GroupFolderType;
pub use crate::test_utils::test_prelude::*;
use glam::DAffine2;
use graphene_core::vector::fill;
use graphene_core::vector::style::Gradient;
use graphene_std::vector::fill;
use graphene_std::vector::style::Fill;
use graphene_std::vector::style::Gradient;
use super::gradient_space_transform;
async fn get_fills(editor: &mut EditorTestUtils) -> Vec<(Fill, DAffine2)> {
let instrumented = editor.eval_graph().await;
let instrumented = match editor.eval_graph().await {
Ok(instrumented) => instrumented,
Err(e) => panic!("Failed to evaluate graph: {}", e),
};
let document = editor.active_document();
let layers = document.metadata().all_layers();
@@ -714,7 +718,7 @@ mod test_gradient {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor.handle_message(NavigationMessage::CanvasZoomSet { zoom_factor: 2.0 }).await;
editor.handle_message(NavigationMessage::CanvasZoomSet { zoom_factor: 2. }).await;
editor.drag_tool(ToolType::Rectangle, -5., -3., 100., 100., ModifierKeys::empty()).await;
@@ -723,7 +727,7 @@ mod test_gradient {
editor
.handle_message(GraphOperationMessage::TransformSet {
layer: selected_layer,
transform: DAffine2::from_scale_angle_translation(DVec2::new(1.5, 0.8), 0.3, DVec2::new(10.0, -5.0)),
transform: DAffine2::from_scale_angle_translation(DVec2::new(1.5, 0.8), 0.3, DVec2::new(10., -5.)),
transform_in: TransformIn::Local,
skip_rerender: false,
})
@@ -799,7 +803,7 @@ mod test_gradient {
stops.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
let positions: Vec<f64> = stops.iter().map(|(pos, _)| *pos).collect();
assert_stops_at_positions(&positions, &[0.0, 0.5, 1.0], 0.1);
assert_stops_at_positions(&positions, &[0., 0.5, 1.], 0.1);
let middle_color = stops[1].1.to_rgba8_srgb();
@@ -839,7 +843,7 @@ mod test_gradient {
// Check positions are now correctly ordered
let updated_positions: Vec<f64> = updated_stops.iter().map(|(pos, _)| *pos).collect();
assert_stops_at_positions(&updated_positions, &[0.0, 0.8, 1.0], 0.1);
assert_stops_at_positions(&updated_positions, &[0., 0.8, 1.], 0.1);
// Colors should maintain their associations with the stop points
assert_eq!(updated_stops[0].1.to_rgba8_srgb(), Color::BLUE.to_rgba8_srgb());
@@ -873,7 +877,7 @@ mod test_gradient {
let positions: Vec<f64> = updated_gradient.stops.iter().map(|(pos, _)| *pos).collect();
// Use helper function to verify positions
assert_stops_at_positions(&positions, &[0.0, 0.25, 0.75, 1.0], 0.05);
assert_stops_at_positions(&positions, &[0., 0.25, 0.75, 1.], 0.05);
// Select the stop at position 0.75 and delete it
let position2 = DVec2::new(75., 0.);
@@ -899,7 +903,7 @@ mod test_gradient {
let final_positions: Vec<f64> = final_gradient.stops.iter().map(|(pos, _)| *pos).collect();
// Verify final positions with helper function
assert_stops_at_positions(&final_positions, &[0.0, 0.25, 1.0], 0.05);
assert_stops_at_positions(&final_positions, &[0., 0.25, 1.], 0.05);
// Additional verification that 0.75 stop is gone
assert!(!final_positions.iter().any(|pos| (pos - 0.75).abs() < 0.05), "Stop at position 0.75 should have been deleted");
@@ -1,184 +0,0 @@
use super::tool_prelude::*;
use crate::messages::tool::common_functionality::resize::Resize;
#[derive(Default)]
pub struct ImaginateTool {
fsm_state: ImaginateToolFsmState,
tool_data: ImaginateToolData,
}
#[impl_message(Message, ToolMessage, Imaginate)]
#[derive(PartialEq, Eq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum ImaginateToolMessage {
// Standard messages
Abort,
// Tool-specific messages
DragStart,
DragStop,
Resize { center: Key, lock_ratio: Key },
}
impl LayoutHolder for ImaginateTool {
fn layout(&self) -> Layout {
Layout::WidgetLayout(WidgetLayout::default())
}
}
impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for ImaginateTool {
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, tool_data: &mut ToolActionHandlerData<'a>) {
self.fsm_state.process_event(message, &mut self.tool_data, tool_data, &(), responses, true);
}
fn actions(&self) -> ActionList {
match self.fsm_state {
ImaginateToolFsmState::Ready => actions!(ImaginateToolMessageDiscriminant;
DragStart,
),
ImaginateToolFsmState::Drawing => actions!(ImaginateToolMessageDiscriminant;
DragStop,
Abort,
Resize,
),
}
}
}
impl ToolMetadata for ImaginateTool {
fn icon_name(&self) -> String {
"RasterImaginateTool".into()
}
fn tooltip(&self) -> String {
"Imaginate Tool".into()
}
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
ToolType::Imaginate
}
}
impl ToolTransition for ImaginateTool {
fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap {
tool_abort: Some(ImaginateToolMessage::Abort.into()),
..Default::default()
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
enum ImaginateToolFsmState {
#[default]
Ready,
Drawing,
}
#[derive(Clone, Debug, Default)]
struct ImaginateToolData {
data: Resize,
}
impl Fsm for ImaginateToolFsmState {
type ToolData = ImaginateToolData;
type ToolOptions = ();
fn transition(
self,
event: ToolMessage,
tool_data: &mut Self::ToolData,
ToolActionHandlerData { document, input, .. }: &mut ToolActionHandlerData,
_tool_options: &Self::ToolOptions,
responses: &mut VecDeque<Message>,
) -> Self {
let shape_data = &mut tool_data.data;
let ToolMessage::Imaginate(event) = event else { return self };
match (self, event) {
(ImaginateToolFsmState::Ready, ImaginateToolMessage::DragStart) => {
shape_data.start(document, input);
// responses.add(DocumentMessage::AddTransaction);
//shape_data.layer = Some(LayerNodeIdentifier::new(NodeId::new(), &document.network_interface));
responses.add(DocumentMessage::DeselectAllLayers);
// // Utility function to offset the position of each consecutive node
// let mut pos = 8;
// let mut next_pos = || {
// pos += 8;
// DocumentNodeMetadata::position((pos, 4))
// };
// // Get the node type for the Transform and Imaginate nodes
// let Some(transform_node_type) = resolve_document_node_type("Transform") else {
// warn!("Transform node should be in registry");
// return ImaginateToolFsmState::Drawing;
// };
// let imaginate_node_type = &*IMAGINATE_NODE;
// // Give them a unique ID
// let transform_node_id = NodeId(100);
//let imaginate_node_id = NodeId(101);
// Create the network based on the Input -> Output passthrough default network
// let mut network = new_image_network(16, imaginate_node_id);
// // Insert the nodes into the default network
// network.insert_node(
// transform_node_id,
// transform_node_type.to_document_node_default_inputs([Some(NodeInput::node(NodeId(0), 0))], next_pos()),
// );
// network.insert_node(
// imaginate_node_id,
// imaginate_node_type.to_document_node_default_inputs([Some(NodeInput::node(transform_node_id, 0))], next_pos()),
// );
// responses.add(NodeGraphMessage::ShiftNode { node_id: imaginate_node_id });
// // Add a layer with a frame to the document
// responses.add(Operation::AddFrame {
// path: shape_data.layer.unwrap().to_path(),
// insert_index: -1,
// transform: DAffine2::ZERO.to_cols_array(),
// network,
// });
ImaginateToolFsmState::Drawing
}
(state, ImaginateToolMessage::Resize { center, lock_ratio }) => {
let message = shape_data.calculate_transform(document, input, center, lock_ratio, true);
responses.try_add(message);
state
}
(ImaginateToolFsmState::Drawing, ImaginateToolMessage::DragStop) => {
input.mouse.finish_transaction(shape_data.viewport_drag_start(document), responses);
shape_data.cleanup(responses);
ImaginateToolFsmState::Ready
}
(ImaginateToolFsmState::Drawing, ImaginateToolMessage::Abort) => {
responses.add(DocumentMessage::AbortTransaction);
shape_data.cleanup(responses);
ImaginateToolFsmState::Ready
}
(_, ImaginateToolMessage::Abort) => ImaginateToolFsmState::Ready,
_ => self,
}
}
fn update_hints(&self, responses: &mut VecDeque<Message>) {
let hint_data = match self {
ImaginateToolFsmState::Ready => HintData(vec![HintGroup(vec![
HintInfo::mouse(MouseMotion::LmbDrag, "Draw Repaint Frame"),
HintInfo::keys([Key::Shift], "Constrain Square").prepend_plus(),
HintInfo::keys([Key::Alt], "From Center").prepend_plus(),
])]),
ImaginateToolFsmState::Drawing => HintData(vec![HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Square"), HintInfo::keys([Key::Alt], "From Center")])]),
};
responses.add(FrontendMessage::UpdateInputHints { hint_data });
}
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair });
}
}
@@ -1,624 +0,0 @@
use super::tool_prelude::*;
use crate::consts::{BOUNDS_SELECT_THRESHOLD, DEFAULT_STROKE_WIDTH, LINE_ROTATE_SNAP_ANGLE};
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::InputConnector;
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType};
use crate::messages::tool::common_functionality::graph_modification_utils::{self, NodeGraphLayer};
use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration};
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput};
use graphene_core::Color;
#[derive(Default)]
pub struct LineTool {
fsm_state: LineToolFsmState,
tool_data: LineToolData,
options: LineOptions,
}
pub struct LineOptions {
line_weight: f64,
stroke: ToolColorOptions,
}
impl Default for LineOptions {
fn default() -> Self {
Self {
line_weight: DEFAULT_STROKE_WIDTH,
stroke: ToolColorOptions::new_primary(),
}
}
}
#[impl_message(Message, ToolMessage, Line)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum LineToolMessage {
// Standard messages
Overlays(OverlayContext),
Abort,
WorkingColorChanged,
// Tool-specific messages
DragStart,
DragStop,
PointerMove { center: Key, lock_angle: Key, snap_angle: Key },
PointerOutsideViewport { center: Key, lock_angle: Key, snap_angle: Key },
UpdateOptions(LineOptionsUpdate),
}
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum LineOptionsUpdate {
LineWeight(f64),
StrokeColor(Option<Color>),
StrokeColorType(ToolColorType),
WorkingColors(Option<Color>, Option<Color>),
}
impl ToolMetadata for LineTool {
fn icon_name(&self) -> String {
"VectorLineTool".into()
}
fn tooltip(&self) -> String {
"Line Tool".into()
}
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
ToolType::Line
}
}
fn create_weight_widget(line_weight: f64) -> WidgetHolder {
NumberInput::new(Some(line_weight))
.unit(" px")
.label("Weight")
.min(0.)
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
.on_update(|number_input: &NumberInput| LineToolMessage::UpdateOptions(LineOptionsUpdate::LineWeight(number_input.value.unwrap())).into())
.widget_holder()
}
impl LayoutHolder for LineTool {
fn layout(&self) -> Layout {
let mut widgets = self.options.stroke.create_widgets(
"Stroke",
true,
|_| LineToolMessage::UpdateOptions(LineOptionsUpdate::StrokeColor(None)).into(),
|color_type: ToolColorType| WidgetCallback::new(move |_| LineToolMessage::UpdateOptions(LineOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|color: &ColorInput| LineToolMessage::UpdateOptions(LineOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(),
);
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
widgets.push(create_weight_widget(self.options.line_weight));
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
}
}
impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for LineTool {
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, tool_data: &mut ToolActionHandlerData<'a>) {
let ToolMessage::Line(LineToolMessage::UpdateOptions(action)) = message else {
self.fsm_state.process_event(message, &mut self.tool_data, tool_data, &self.options, responses, true);
return;
};
match action {
LineOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight,
LineOptionsUpdate::StrokeColor(color) => {
self.options.stroke.custom_color = color;
self.options.stroke.color_type = ToolColorType::Custom;
}
LineOptionsUpdate::StrokeColorType(color_type) => self.options.stroke.color_type = color_type,
LineOptionsUpdate::WorkingColors(primary, secondary) => {
self.options.stroke.primary_working_color = primary;
self.options.stroke.secondary_working_color = secondary;
}
}
self.send_layout(responses, LayoutTarget::ToolOptions);
}
fn actions(&self) -> ActionList {
match self.fsm_state {
LineToolFsmState::Ready => actions!(LineToolMessageDiscriminant; DragStart, PointerMove),
LineToolFsmState::Drawing => actions!(LineToolMessageDiscriminant; DragStop, PointerMove, Abort),
}
}
}
impl ToolTransition for LineTool {
fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap {
overlay_provider: Some(|overlay_context| LineToolMessage::Overlays(overlay_context).into()),
tool_abort: Some(LineToolMessage::Abort.into()),
working_color_changed: Some(LineToolMessage::WorkingColorChanged.into()),
..Default::default()
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
enum LineToolFsmState {
#[default]
Ready,
Drawing,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum LineEnd {
Start,
End,
}
#[derive(Clone, Debug, Default)]
struct LineToolData {
drag_begin: DVec2,
drag_start_shifted: DVec2,
drag_current_shifted: DVec2,
drag_start: DVec2,
drag_current: DVec2,
angle: f64,
weight: f64,
selected_layers_with_position: HashMap<LayerNodeIdentifier, [DVec2; 2]>,
editing_layer: Option<LayerNodeIdentifier>,
snap_manager: SnapManager,
auto_panning: AutoPanning,
dragging_endpoint: Option<LineEnd>,
}
impl Fsm for LineToolFsmState {
type ToolData = LineToolData;
type ToolOptions = LineOptions;
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, global_tool_data, input, ..
} = tool_action_data;
let ToolMessage::Line(event) = event else { return self };
match (self, event) {
(_, LineToolMessage::Overlays(mut overlay_context)) => {
tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
tool_data.selected_layers_with_position = document
.network_interface
.selected_nodes()
.selected_visible_and_unlocked_layers(&document.network_interface)
.filter_map(|layer| {
let node_inputs = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Line")?;
let (Some(&TaggedValue::DVec2(start)), Some(&TaggedValue::DVec2(end))) = (node_inputs[1].as_value(), node_inputs[2].as_value()) else {
return None;
};
let [viewport_start, viewport_end] = [start, end].map(|point| document.metadata().transform_to_viewport(layer).transform_point2(point));
if !start.abs_diff_eq(end, f64::EPSILON * 1000.) {
overlay_context.line(viewport_start, viewport_end, None, None);
overlay_context.square(viewport_start, Some(6.), None, None);
overlay_context.square(viewport_end, Some(6.), None, None);
}
Some((layer, [start, end]))
})
.collect::<HashMap<LayerNodeIdentifier, [DVec2; 2]>>();
self
}
(LineToolFsmState::Ready, LineToolMessage::DragStart) => {
let point = SnapCandidatePoint::handle(document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position));
let snapped = tool_data.snap_manager.free_snap(&SnapData::new(document, input), &point, SnapTypeConfiguration::default());
tool_data.drag_start = snapped.snapped_point_document;
tool_data.drag_begin = document.metadata().document_to_viewport.transform_point2(tool_data.drag_start);
responses.add(DocumentMessage::StartTransaction);
for (layer, [document_start, document_end]) in tool_data.selected_layers_with_position.iter() {
let transform = document.metadata().transform_to_viewport(*layer);
let viewport_x = transform.transform_vector2(DVec2::X).normalize_or_zero() * BOUNDS_SELECT_THRESHOLD;
let viewport_y = transform.transform_vector2(DVec2::Y).normalize_or_zero() * BOUNDS_SELECT_THRESHOLD;
let threshold_x = transform.inverse().transform_vector2(viewport_x).length();
let threshold_y = transform.inverse().transform_vector2(viewport_y).length();
let drag_start = input.mouse.position;
let [start, end] = [document_start, document_end].map(|point| transform.transform_point2(*point));
let start_click = (drag_start.y - start.y).abs() < threshold_y && (drag_start.x - start.x).abs() < threshold_x;
let end_click = (drag_start.y - end.y).abs() < threshold_y && (drag_start.x - end.x).abs() < threshold_x;
if start_click || end_click {
tool_data.dragging_endpoint = Some(if end_click { LineEnd::End } else { LineEnd::Start });
tool_data.drag_start = if end_click { *document_start } else { *document_end };
tool_data.editing_layer = Some(*layer);
return LineToolFsmState::Drawing;
}
}
let node_type = resolve_document_node_type("Line").expect("Line node does not exist");
let node = node_type.node_template_input_override([
None,
Some(NodeInput::value(
TaggedValue::DVec2(document.metadata().document_to_viewport.transform_point2(tool_data.drag_start)),
false,
)),
Some(NodeInput::value(
TaggedValue::DVec2(document.metadata().document_to_viewport.transform_point2(tool_data.drag_start)),
false,
)),
]);
let nodes = vec![(NodeId(0), node)];
let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, document.new_layer_bounding_artboard(input), responses);
responses.add(Message::StartBuffer);
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses);
tool_data.editing_layer = Some(layer);
tool_data.angle = 0.;
tool_data.weight = tool_options.line_weight;
LineToolFsmState::Drawing
}
(LineToolFsmState::Drawing, LineToolMessage::PointerMove { center, snap_angle, lock_angle }) => {
let Some(layer) = tool_data.editing_layer else { return LineToolFsmState::Ready };
tool_data.drag_current_shifted = document.metadata().transform_to_viewport(layer).inverse().transform_point2(input.mouse.position);
tool_data.drag_current = document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position);
tool_data.drag_start_shifted = document.metadata().transform_to_viewport(layer).inverse().transform_point2(tool_data.drag_begin);
let keyboard = &input.keyboard;
let ignore = vec![layer];
let snap_data = SnapData::ignore(document, input, &ignore);
let mut document_points = generate_line(tool_data, snap_data, keyboard.key(lock_angle), keyboard.key(snap_angle), keyboard.key(center));
if tool_data.dragging_endpoint == Some(LineEnd::Start) {
document_points.swap(0, 1);
}
let Some(node_id) = graph_modification_utils::get_line_id(layer, &document.network_interface) else {
return LineToolFsmState::Ready;
};
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 1),
input: NodeInput::value(TaggedValue::DVec2(document_points[0]), false),
});
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 2),
input: NodeInput::value(TaggedValue::DVec2(document_points[1]), false),
});
responses.add(NodeGraphMessage::RunDocumentGraph);
// Auto-panning
let messages = [
LineToolMessage::PointerOutsideViewport { center, snap_angle, lock_angle }.into(),
LineToolMessage::PointerMove { center, snap_angle, lock_angle }.into(),
];
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses);
LineToolFsmState::Drawing
}
(_, LineToolMessage::PointerMove { .. }) => {
tool_data.snap_manager.preview_draw(&SnapData::new(document, input), input.mouse.position);
responses.add(OverlaysMessage::Draw);
self
}
(LineToolFsmState::Drawing, LineToolMessage::PointerOutsideViewport { .. }) => {
// Auto-panning
let _ = tool_data.auto_panning.shift_viewport(input, responses);
LineToolFsmState::Drawing
}
(state, LineToolMessage::PointerOutsideViewport { center, lock_angle, snap_angle }) => {
// Auto-panning
let messages = [
LineToolMessage::PointerOutsideViewport { center, lock_angle, snap_angle }.into(),
LineToolMessage::PointerMove { center, lock_angle, snap_angle }.into(),
];
tool_data.auto_panning.stop(&messages, responses);
state
}
(LineToolFsmState::Drawing, LineToolMessage::DragStop) => {
tool_data.snap_manager.cleanup(responses);
tool_data.editing_layer.take();
input.mouse.finish_transaction(tool_data.drag_start, responses);
LineToolFsmState::Ready
}
(LineToolFsmState::Drawing, LineToolMessage::Abort) => {
tool_data.snap_manager.cleanup(responses);
tool_data.editing_layer.take();
responses.add(DocumentMessage::AbortTransaction);
LineToolFsmState::Ready
}
(_, LineToolMessage::WorkingColorChanged) => {
responses.add(LineToolMessage::UpdateOptions(LineOptionsUpdate::WorkingColors(
Some(global_tool_data.primary_color),
Some(global_tool_data.secondary_color),
)));
self
}
_ => self,
}
}
fn update_hints(&self, responses: &mut VecDeque<Message>) {
let hint_data = match self {
LineToolFsmState::Ready => HintData(vec![HintGroup(vec![
HintInfo::mouse(MouseMotion::LmbDrag, "Draw Line"),
HintInfo::keys([Key::Shift], "15° Increments").prepend_plus(),
HintInfo::keys([Key::Alt], "From Center").prepend_plus(),
HintInfo::keys([Key::Control], "Lock Angle").prepend_plus(),
])]),
LineToolFsmState::Drawing => HintData(vec![
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
HintGroup(vec![
HintInfo::keys([Key::Shift], "15° Increments"),
HintInfo::keys([Key::Alt], "From Center"),
HintInfo::keys([Key::Control], "Lock Angle"),
]),
]),
};
responses.add(FrontendMessage::UpdateInputHints { hint_data });
}
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair });
}
}
fn generate_line(tool_data: &mut LineToolData, snap_data: SnapData, lock_angle: bool, snap_angle: bool, center: bool) -> [DVec2; 2] {
let shift = tool_data.drag_current_shifted - tool_data.drag_current;
let mut document_points = [tool_data.drag_start, tool_data.drag_current];
let mut angle = -(document_points[1] - document_points[0]).angle_to(DVec2::X);
let mut line_length = (document_points[1] - document_points[0]).length();
if lock_angle {
angle = tool_data.angle;
} else if snap_angle {
let snap_resolution = LINE_ROTATE_SNAP_ANGLE.to_radians();
angle = (angle / snap_resolution).round() * snap_resolution;
}
tool_data.angle = angle;
let angle_vec = DVec2::from_angle(angle);
if lock_angle {
line_length = (document_points[1] - document_points[0]).dot(angle_vec);
}
document_points[1] = document_points[0] + line_length * angle_vec;
let constrained = snap_angle || lock_angle;
let snap = &mut tool_data.snap_manager;
let near_point = SnapCandidatePoint::handle_neighbors(document_points[1], [tool_data.drag_start]);
let far_point = SnapCandidatePoint::handle_neighbors(2. * document_points[0] - document_points[1], [tool_data.drag_start]);
let mid_point = SnapCandidatePoint::handle_neighbors((tool_data.drag_start + document_points[1]) / 2., [tool_data.drag_start]);
let config = SnapTypeConfiguration::default();
if constrained {
let constraint = SnapConstraint::Line {
origin: document_points[0],
direction: document_points[1] - document_points[0],
};
if center {
let snapped = snap.constrained_snap(&snap_data, &near_point, constraint, config);
let snapped_far = snap.constrained_snap(&snap_data, &far_point, constraint, config);
let best = if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far };
document_points[1] = document_points[0] * 2. - best.snapped_point_document;
document_points[0] = best.snapped_point_document;
snap.update_indicator(best);
} else {
let snapped = snap.constrained_snap(&snap_data, &near_point, constraint, config);
let snapped_mid = snap.constrained_snap(&snap_data, &mid_point, constraint, config);
let best = if snap_data.document.snapping_state.path.line_midpoint && snapped_mid.other_snap_better(&snapped_mid) {
document_points[1] += (snapped_mid.snapped_point_document - mid_point.document_point) * 2.;
snapped_mid
} else {
document_points[1] = snapped.snapped_point_document;
snapped.clone()
};
snap.update_indicator(best);
}
} else if center {
let snapped = snap.free_snap(&snap_data, &near_point, config);
let snapped_far = snap.free_snap(&snap_data, &far_point, config);
let best = if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far };
document_points[1] = document_points[0] * 2. - best.snapped_point_document;
document_points[0] = best.snapped_point_document;
snap.update_indicator(best);
} else {
let snapped = snap.free_snap(&snap_data, &near_point, config);
let snapped_mid = snap.free_snap(&snap_data, &mid_point, config);
let best = if snap_data.document.snapping_state.path.line_midpoint && snapped_mid.other_snap_better(&snapped_mid) {
document_points[1] += (snapped_mid.snapped_point_document - mid_point.document_point) * 2.;
snapped_mid
} else {
document_points[1] = snapped.snapped_point_document;
snapped.clone()
};
snap.update_indicator(best);
}
// Snapping happens in other space, while document graph renders in another.
document_points.map(|vector| vector + shift)
}
#[cfg(test)]
mod test_line_tool {
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer;
use crate::test_utils::test_prelude::*;
use glam::DAffine2;
use graph_craft::document::value::TaggedValue;
async fn get_line_node_inputs(editor: &mut EditorTestUtils) -> Option<(DVec2, DVec2)> {
let document = editor.active_document();
let network_interface = &document.network_interface;
let node_id = network_interface
.selected_nodes()
.selected_visible_and_unlocked_layers(network_interface)
.filter_map(|layer| {
let node_inputs = NodeGraphLayer::new(layer, &network_interface).find_node_inputs("Line")?;
let (Some(&TaggedValue::DVec2(start)), Some(&TaggedValue::DVec2(end))) = (node_inputs[1].as_value(), node_inputs[2].as_value()) else {
return None;
};
Some((start, end))
})
.next();
node_id
}
#[tokio::test]
async fn test_line_tool_basicdraw() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor.drag_tool(ToolType::Line, 0., 0., 100., 100., ModifierKeys::empty()).await;
if let Some((start_input, end_input)) = get_line_node_inputs(&mut editor).await {
match (start_input, end_input) {
(start_input, end_input) => {
assert!((start_input - DVec2::ZERO).length() < 1., "Start point should be near (0,0)");
assert!((end_input - DVec2::new(100., 100.)).length() < 1., "End point should be near (100,100)");
}
}
}
}
#[tokio::test]
async fn test_line_tool_with_transformed_viewport() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor.handle_message(NavigationMessage::CanvasZoomSet { zoom_factor: 2. }).await;
editor.handle_message(NavigationMessage::CanvasPan { delta: DVec2::new(100., 50.) }).await;
editor
.handle_message(NavigationMessage::CanvasTiltSet {
angle_radians: (30. as f64).to_radians(),
})
.await;
editor.drag_tool(ToolType::Line, 0., 0., 100., 100., ModifierKeys::empty()).await;
if let Some((start_input, end_input)) = get_line_node_inputs(&mut editor).await {
let document = editor.active_document();
let document_to_viewport = document.metadata().document_to_viewport;
let viewport_to_document = document_to_viewport.inverse();
let expected_start = viewport_to_document.transform_point2(DVec2::ZERO);
let expected_end = viewport_to_document.transform_point2(DVec2::new(100., 100.));
assert!(
(start_input - expected_start).length() < 1.,
"Start point should match expected document coordinates. Got {:?}, expected {:?}",
start_input,
expected_start
);
assert!(
(end_input - expected_end).length() < 1.,
"End point should match expected document coordinates. Got {:?}, expected {:?}",
end_input,
expected_end
);
} else {
panic!("Line was not created successfully with transformed viewport");
}
}
#[tokio::test]
async fn test_line_tool_ctrl_anglelock() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor.drag_tool(ToolType::Line, 0., 0., 100., 100., ModifierKeys::CONTROL).await;
if let Some((start_input, end_input)) = get_line_node_inputs(&mut editor).await {
match (start_input, end_input) {
(start_input, end_input) => {
let line_vec = end_input - start_input;
let original_angle = line_vec.angle_to(DVec2::X);
editor.drag_tool(ToolType::Line, 0., 0., 200., 50., ModifierKeys::CONTROL).await;
if let Some((updated_start, updated_end)) = get_line_node_inputs(&mut editor).await {
match (updated_start, updated_end) {
(updated_start, updated_end) => {
let updated_line_vec = updated_end - updated_start;
let updated_angle = updated_line_vec.angle_to(DVec2::X);
assert!((original_angle - updated_angle).abs() < 0.1, "Line angle should be locked when Ctrl is kept pressed");
assert!((updated_start - updated_end).length() > 1., "Line should be able to change length when Ctrl is kept pressed");
}
}
}
}
}
}
}
#[tokio::test]
async fn test_line_tool_alt() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor.drag_tool(ToolType::Line, 100., 100., 200., 100., ModifierKeys::ALT).await;
if let Some((start_input, end_input)) = get_line_node_inputs(&mut editor).await {
match (start_input, end_input) {
(start_input, end_input) => {
let expected_start = DVec2::new(0., 100.);
let expected_end = DVec2::new(200., 100.);
assert!((start_input - expected_start).length() < 1., "start point should be near (0,100)");
assert!((end_input - expected_end).length() < 1., "end point should be near (200,100)");
}
}
}
}
#[tokio::test]
async fn test_line_tool_alt_shift_drag() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor.drag_tool(ToolType::Line, 100., 100., 150., 120., ModifierKeys::ALT | ModifierKeys::SHIFT).await;
if let Some((start_input, end_input)) = get_line_node_inputs(&mut editor).await {
match (start_input, end_input) {
(start_input, end_input) => {
let line_vec = end_input - start_input;
let angle_radians = line_vec.angle_to(DVec2::X);
let angle_degrees = angle_radians.to_degrees();
let nearest_angle = (angle_degrees / 15.).round() * 15.;
assert!((angle_degrees - nearest_angle).abs() < 1., "Angle should snap to the nearest 15 degrees");
}
}
}
}
#[tokio::test]
async fn test_line_tool_with_transformed_artboard() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor.drag_tool(ToolType::Artboard, 0., 0., 200., 200., ModifierKeys::empty()).await;
let artboard_id = editor.get_selected_layer().await.expect("Should have selected the artboard");
editor
.handle_message(GraphOperationMessage::TransformChange {
layer: artboard_id,
transform: DAffine2::from_angle(45.0_f64.to_radians()),
transform_in: TransformIn::Local,
skip_rerender: false,
})
.await;
editor.drag_tool(ToolType::Line, 50., 50., 150., 150., ModifierKeys::empty()).await;
let (start_input, end_input) = get_line_node_inputs(&mut editor).await.expect("Line was not created successfully within transformed artboard");
// The line should still be diagonal with equal change in x and y
let line_vector = end_input - start_input;
// Verifying the line is approximately 100*sqrt(2) units in length (diagonal of 100x100 square)
let line_length = line_vector.length();
assert!(
(line_length - 141.42).abs() < 1.0, // 100 * sqrt(2) ~= 141.42
"Line length should be approximately 141.42 units. Got: {line_length}"
);
assert!((line_vector.x - 100.0).abs() < 1.0, "X-component of line vector should be approximately 100. Got: {}", line_vector.x);
assert!(
(line_vector.y.abs() - 100.0).abs() < 1.0,
"Absolute Y-component of line vector should be approximately 100. Got: {}",
line_vector.y.abs()
);
let angle_degrees = line_vector.angle_to(DVec2::X).to_degrees();
assert!((angle_degrees - (-45.0)).abs() < 1.0, "Line angle should be close to -45 degrees. Got: {angle_degrees}");
}
}
@@ -1,18 +1,14 @@
pub mod artboard_tool;
pub mod brush_tool;
pub mod ellipse_tool;
pub mod eyedropper_tool;
pub mod fill_tool;
pub mod freehand_tool;
pub mod gradient_tool;
// pub mod imaginate_tool;
pub mod line_tool;
pub mod navigate_tool;
pub mod path_tool;
pub mod pen_tool;
pub mod polygon_tool;
pub mod rectangle_tool;
pub mod select_tool;
pub mod shape_tool;
pub mod spline_tool;
pub mod text_tool;
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,5 @@
use super::tool_prelude::*;
use crate::consts::{COLOR_OVERLAY_BLUE, DEFAULT_STROKE_WIDTH, HIDE_HANDLE_DISTANCE, LINE_ROTATE_SNAP_ANGLE};
use crate::consts::{COLOR_OVERLAY_BLUE, DEFAULT_STROKE_WIDTH, HIDE_HANDLE_DISTANCE, LINE_ROTATE_SNAP_ANGLE, SEGMENT_OVERLAY_SIZE};
use crate::messages::input_mapper::utility_types::input_mouse::MouseKeys;
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
use crate::messages::portfolio::document::overlays::utility_functions::path_overlays;
@@ -13,9 +13,9 @@ use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandi
use crate::messages::tool::common_functionality::utility_functions::{calculate_segment_angle, closest_point, should_extend};
use bezier_rs::{Bezier, BezierHandles};
use graph_craft::document::NodeId;
use graphene_core::Color;
use graphene_core::vector::{PointId, VectorModificationType};
use graphene_std::Color;
use graphene_std::vector::{HandleId, ManipulatorPointId, NoHashBuilder, SegmentId, StrokeId, VectorData};
use graphene_std::vector::{PointId, VectorModificationType};
#[derive(Default)]
pub struct PenTool {
@@ -361,6 +361,9 @@ struct PenToolData {
current_layer: Option<LayerNodeIdentifier>,
prior_segment_endpoint: Option<PointId>,
prior_segment: Option<SegmentId>,
/// For vector meshes, storing all the previous segments the last anchor point was connected to
prior_segments: Option<Vec<SegmentId>>,
handle_type: TargetHandle,
handle_start_offset: Option<DVec2>,
handle_end_offset: Option<DVec2>,
@@ -533,7 +536,15 @@ impl PenToolData {
}
/// If the user places the anchor on top of the previous anchor, it becomes sharp and the outgoing handle may be dragged.
fn bend_from_previous_point(&mut self, snap_data: SnapData, transform: DAffine2, layer: LayerNodeIdentifier, preferences: &PreferencesMessageHandler) {
fn bend_from_previous_point(
&mut self,
snap_data: SnapData,
transform: DAffine2,
layer: LayerNodeIdentifier,
preferences: &PreferencesMessageHandler,
shape_editor: &mut ShapeState,
responses: &mut VecDeque<Message>,
) {
self.g1_continuous = true;
let document = snap_data.document;
self.next_handle_start = self.next_point;
@@ -567,6 +578,43 @@ impl PenToolData {
}
// Closing path
let closing_path_on_point = self.close_path_on_point(snap_data, &vector_data, document, preferences, id, &transform);
if !closing_path_on_point && preferences.vector_meshes {
// Attempt to find nearest segment and close path on segment by creating an anchor point on it
let tolerance = crate::consts::SNAP_POINT_TOLERANCE;
if let Some(closest_segment) = shape_editor.upper_closest_segment(&document.network_interface, transform.transform_point2(self.next_point), tolerance) {
let (point, _) = closest_segment.adjusted_insert(responses);
self.update_handle_type(TargetHandle::PreviewInHandle);
self.handle_end_offset = None;
self.path_closed = true;
self.next_handle_start = self.next_point;
self.prior_segment_endpoint = Some(point);
self.prior_segment_layer = Some(closest_segment.layer());
self.prior_segments = None;
self.prior_segment = None;
// Should also update the SnapCache here?
self.handle_mode = HandleMode::Free;
if let (true, Some(prior_endpoint)) = (self.modifiers.lock_angle, self.prior_segment_endpoint) {
self.set_lock_angle(&vector_data, prior_endpoint, self.prior_segment);
self.switch_to_free_on_ctrl_release = true;
}
}
}
}
fn close_path_on_point(
&mut self,
snap_data: SnapData,
vector_data: &VectorData,
document: &DocumentMessageHandler,
preferences: &PreferencesMessageHandler,
id: PointId,
transform: &DAffine2,
) -> bool {
for id in vector_data.extendable_points(preferences.vector_meshes).filter(|&point| point != id) {
let Some(pos) = vector_data.point_domain.position_from_id(id) else { continue };
let transformed_distance_between_squared = transform.transform_point2(pos).distance_squared(transform.transform_point2(self.next_point));
@@ -577,14 +625,16 @@ impl PenToolData {
self.handle_end_offset = None;
self.path_closed = true;
self.next_handle_start = self.next_point;
self.store_clicked_endpoint(document, &transform, snap_data.input, preferences);
self.store_clicked_endpoint(document, transform, snap_data.input, preferences);
self.handle_mode = HandleMode::Free;
if let (true, Some(prior_endpoint)) = (self.modifiers.lock_angle, self.prior_segment_endpoint) {
self.set_lock_angle(&vector_data, prior_endpoint, self.prior_segment);
self.set_lock_angle(vector_data, prior_endpoint, self.prior_segment);
self.switch_to_free_on_ctrl_release = true;
}
return true;
}
}
false
}
fn finish_placing_handle(&mut self, snap_data: SnapData, transform: DAffine2, preferences: &PreferencesMessageHandler, responses: &mut VecDeque<Message>) -> Option<PenToolFsmState> {
@@ -656,6 +706,7 @@ impl PenToolData {
layer,
modification_type: VectorModificationType::SetG1Continuous { handles, enabled: colinear },
});
self.cleanup(responses);
}
}
@@ -1122,6 +1173,7 @@ impl PenToolData {
transform.inverse().transform_point2(document_pos)
}
#[allow(clippy::too_many_arguments)]
fn create_initial_point(
&mut self,
document: &DocumentMessageHandler,
@@ -1130,6 +1182,7 @@ impl PenToolData {
tool_options: &PenOptions,
append: bool,
preferences: &PreferencesMessageHandler,
shape_editor: &mut ShapeState,
) {
let point = SnapCandidatePoint::handle(document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position));
let snapped = self.snap_manager.free_snap(&SnapData::new(document, input), &point, SnapTypeConfiguration::default());
@@ -1145,6 +1198,20 @@ impl PenToolData {
self.current_layer = Some(layer);
self.extend_existing_path(document, layer, point, position);
return;
} else if preferences.vector_meshes {
if let Some(closest_segment) = shape_editor.upper_closest_segment(&document.network_interface, viewport, tolerance) {
let (point, segments) = closest_segment.adjusted_insert(responses);
let layer = closest_segment.layer();
let position = closest_segment.closest_point_document();
// Setting any one of the new segments created as the previous segment
self.prior_segment_endpoint = Some(point);
self.prior_segment_layer = Some(layer);
self.prior_segments = Some(segments.to_vec());
self.extend_existing_path(document, layer, point, position);
return;
}
}
if append {
@@ -1186,6 +1253,7 @@ impl PenToolData {
tool_options.fill.apply_fill(layer, responses);
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses);
self.prior_segment = None;
self.prior_segments = None;
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] });
// This causes the following message to be run only after the next graph evaluation runs and the transforms are updated
@@ -1266,6 +1334,7 @@ impl PenToolData {
self.prior_segment = None;
self.prior_segment_endpoint = None;
self.prior_segment_layer = None;
self.prior_segments = None;
if let Some((layer, point, _position)) = closest_point(document, viewport, tolerance, document.metadata().all_layers(), |_| false, preferences) {
self.prior_segment_endpoint = Some(point);
@@ -1493,6 +1562,22 @@ impl Fsm for PenToolFsmState {
path_overlays(document, DrawHandles::None, shape_editor, &mut overlay_context);
}
}
// Check if there is an anchor within threshold
// If not check if there is a closest segment within threshold, if yes then draw overlay
let tolerance = crate::consts::SNAP_POINT_TOLERANCE;
let point = SnapCandidatePoint::handle(document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position));
let snapped = tool_data.snap_manager.free_snap(&SnapData::new(document, input), &point, SnapTypeConfiguration::default());
let viewport = document.metadata().document_to_viewport.transform_point2(snapped.snapped_point_document);
let close_to_point = closest_point(document, viewport, tolerance, document.metadata().all_layers(), |_| false, preferences).is_some();
if preferences.vector_meshes && !close_to_point {
if let Some(closest_segment) = shape_editor.upper_closest_segment(&document.network_interface, viewport, tolerance) {
let pos = closest_segment.closest_point_to_viewport();
let perp = closest_segment.calculate_perp(document);
overlay_context.manipulator_anchor(pos, true, None);
overlay_context.line(pos - perp * SEGMENT_OVERLAY_SIZE, pos + perp * SEGMENT_OVERLAY_SIZE, Some(COLOR_OVERLAY_BLUE), None);
}
}
tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
self
}
@@ -1530,6 +1615,7 @@ impl Fsm for PenToolFsmState {
// Draw the line between the currently-being-placed anchor and its currently-being-dragged-out outgoing handle (opposite the one currently being dragged out)
overlay_context.line(next_anchor, next_handle_start, None, None);
}
match tool_options.pen_overlay_mode {
PenOverlayMode::AllHandles => {
path_overlays(document, DrawHandles::All, shape_editor, &mut overlay_context);
@@ -1537,6 +1623,12 @@ impl Fsm for PenToolFsmState {
PenOverlayMode::FrontierHandles => {
if let Some(latest_segment) = tool_data.prior_segment {
path_overlays(document, DrawHandles::SelectedAnchors(vec![latest_segment]), shape_editor, &mut overlay_context);
}
// If a vector mesh then there can be more than one prior segments
else if let Some(segments) = tool_data.prior_segments.clone() {
if preferences.vector_meshes {
path_overlays(document, DrawHandles::SelectedAnchors(segments), shape_editor, &mut overlay_context);
}
} else {
path_overlays(document, DrawHandles::None, shape_editor, &mut overlay_context);
};
@@ -1598,6 +1690,22 @@ impl Fsm for PenToolFsmState {
overlay_context.manipulator_anchor(next_anchor, false, None);
}
if self == PenToolFsmState::PlacingAnchor && preferences.vector_meshes {
let tolerance = crate::consts::SNAP_POINT_TOLERANCE;
let point = SnapCandidatePoint::handle(document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position));
let snapped = tool_data.snap_manager.free_snap(&SnapData::new(document, input), &point, SnapTypeConfiguration::default());
let viewport = document.metadata().document_to_viewport.transform_point2(snapped.snapped_point_document);
let close_to_point = closest_point(document, viewport, tolerance, document.metadata().all_layers(), |_| false, preferences).is_some();
if !close_to_point {
if let Some(closest_segment) = shape_editor.upper_closest_segment(&document.network_interface, viewport, tolerance) {
let pos = closest_segment.closest_point_to_viewport();
let perp = closest_segment.calculate_perp(document);
overlay_context.manipulator_anchor(pos, true, None);
overlay_context.line(pos - perp * SEGMENT_OVERLAY_SIZE, pos + perp * SEGMENT_OVERLAY_SIZE, Some(COLOR_OVERLAY_BLUE), None);
}
}
}
// Display a filled overlay of the shape if the new point closes the path
if let Some(latest_point) = tool_data.latest_point() {
let handle_start = latest_point.handle_start;
@@ -1663,8 +1771,10 @@ impl Fsm for PenToolFsmState {
tool_data.handle_mode = HandleMode::Free;
// Get the closest point and the segment it is on
let append = input.keyboard.key(append_to_selected);
tool_data.store_clicked_endpoint(document, &transform, input, preferences);
tool_data.create_initial_point(document, input, responses, tool_options, input.keyboard.key(append_to_selected), preferences);
tool_data.create_initial_point(document, input, responses, tool_options, append, preferences, shape_editor);
// Enter the dragging handle state while the mouse is held down, allowing the user to move the mouse and position the handle
PenToolFsmState::DraggingHandle(tool_data.handle_mode)
@@ -1688,7 +1798,7 @@ impl Fsm for PenToolFsmState {
if let Some(layer) = layer {
tool_data.buffering_merged_vector = false;
tool_data.handle_mode = HandleMode::ColinearLocked;
tool_data.bend_from_previous_point(SnapData::new(document, input), transform, layer, preferences);
tool_data.bend_from_previous_point(SnapData::new(document, input), transform, layer, preferences, shape_editor, responses);
tool_data.place_anchor(SnapData::new(document, input), transform, input.mouse.position, preferences, responses);
}
tool_data.buffering_merged_vector = false;
@@ -1,436 +0,0 @@
use super::tool_prelude::*;
use crate::consts::DEFAULT_STROKE_WIDTH;
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::InputConnector;
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType};
use crate::messages::tool::common_functionality::graph_modification_utils::{self, NodeGraphLayer};
use crate::messages::tool::common_functionality::resize::Resize;
use crate::messages::tool::common_functionality::snapping::SnapData;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput};
use graphene_core::Color;
#[derive(Default)]
pub struct PolygonTool {
fsm_state: PolygonToolFsmState,
tool_data: PolygonToolData,
options: PolygonOptions,
}
pub struct PolygonOptions {
line_weight: f64,
fill: ToolColorOptions,
stroke: ToolColorOptions,
vertices: u32,
polygon_type: PolygonType,
}
impl Default for PolygonOptions {
fn default() -> Self {
Self {
vertices: 5,
line_weight: DEFAULT_STROKE_WIDTH,
fill: ToolColorOptions::new_secondary(),
stroke: ToolColorOptions::new_primary(),
polygon_type: PolygonType::Convex,
}
}
}
#[impl_message(Message, ToolMessage, Polygon)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum PolygonToolMessage {
// Standard messages
Overlays(OverlayContext),
Abort,
WorkingColorChanged,
// Tool-specific messages
DragStart,
DragStop,
PointerMove { center: Key, lock_ratio: Key },
PointerOutsideViewport { center: Key, lock_ratio: Key },
UpdateOptions(PolygonOptionsUpdate),
}
#[derive(PartialEq, Copy, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum PolygonType {
Convex = 0,
Star = 1,
}
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum PolygonOptionsUpdate {
FillColor(Option<Color>),
FillColorType(ToolColorType),
LineWeight(f64),
PolygonType(PolygonType),
StrokeColor(Option<Color>),
StrokeColorType(ToolColorType),
Vertices(u32),
WorkingColors(Option<Color>, Option<Color>),
}
impl ToolMetadata for PolygonTool {
fn icon_name(&self) -> String {
"VectorPolygonTool".into()
}
fn tooltip(&self) -> String {
"Polygon Tool".into()
}
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
ToolType::Polygon
}
}
fn create_sides_widget(vertices: u32) -> WidgetHolder {
NumberInput::new(Some(vertices as f64))
.label("Sides")
.int()
.min(3.)
.max(1000.)
.mode(NumberInputMode::Increment)
.on_update(|number_input: &NumberInput| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::Vertices(number_input.value.unwrap() as u32)).into())
.widget_holder()
}
fn create_star_option_widget(polygon_type: PolygonType) -> WidgetHolder {
let entries = vec![
RadioEntryData::new("convex")
.label("Convex")
.on_update(move |_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::PolygonType(PolygonType::Convex)).into()),
RadioEntryData::new("star")
.label("Star")
.on_update(move |_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::PolygonType(PolygonType::Star)).into()),
];
RadioInput::new(entries).selected_index(Some(polygon_type as u32)).widget_holder()
}
fn create_weight_widget(line_weight: f64) -> WidgetHolder {
NumberInput::new(Some(line_weight))
.unit(" px")
.label("Weight")
.min(0.)
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
.on_update(|number_input: &NumberInput| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::LineWeight(number_input.value.unwrap())).into())
.widget_holder()
}
impl LayoutHolder for PolygonTool {
fn layout(&self) -> Layout {
let mut widgets = vec![
create_star_option_widget(self.options.polygon_type),
Separator::new(SeparatorType::Related).widget_holder(),
create_sides_widget(self.options.vertices),
];
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
widgets.append(&mut self.options.fill.create_widgets(
"Fill",
true,
|_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::FillColor(None)).into(),
|color_type: ToolColorType| WidgetCallback::new(move |_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::FillColorType(color_type.clone())).into()),
|color: &ColorInput| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(),
));
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
widgets.append(&mut self.options.stroke.create_widgets(
"Stroke",
true,
|_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::StrokeColor(None)).into(),
|color_type: ToolColorType| WidgetCallback::new(move |_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|color: &ColorInput| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(),
));
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
widgets.push(create_weight_widget(self.options.line_weight));
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
}
}
impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PolygonTool {
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, tool_data: &mut ToolActionHandlerData<'a>) {
let ToolMessage::Polygon(PolygonToolMessage::UpdateOptions(action)) = message else {
self.fsm_state.process_event(message, &mut self.tool_data, tool_data, &self.options, responses, true);
return;
};
match action {
PolygonOptionsUpdate::Vertices(vertices) => self.options.vertices = vertices,
PolygonOptionsUpdate::PolygonType(polygon_type) => self.options.polygon_type = polygon_type,
PolygonOptionsUpdate::FillColor(color) => {
self.options.fill.custom_color = color;
self.options.fill.color_type = ToolColorType::Custom;
}
PolygonOptionsUpdate::FillColorType(color_type) => self.options.fill.color_type = color_type,
PolygonOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight,
PolygonOptionsUpdate::StrokeColor(color) => {
self.options.stroke.custom_color = color;
self.options.stroke.color_type = ToolColorType::Custom;
}
PolygonOptionsUpdate::StrokeColorType(color_type) => self.options.stroke.color_type = color_type,
PolygonOptionsUpdate::WorkingColors(primary, secondary) => {
self.options.stroke.primary_working_color = primary;
self.options.stroke.secondary_working_color = secondary;
self.options.fill.primary_working_color = primary;
self.options.fill.secondary_working_color = secondary;
}
}
self.send_layout(responses, LayoutTarget::ToolOptions);
}
fn actions(&self) -> ActionList {
match self.fsm_state {
PolygonToolFsmState::Ready => actions!(PolygonToolMessageDiscriminant;
DragStart,
PointerMove,
),
PolygonToolFsmState::Drawing => actions!(PolygonToolMessageDiscriminant;
DragStop,
Abort,
PointerMove,
),
}
}
}
impl ToolTransition for PolygonTool {
fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap {
overlay_provider: Some(|overlay_context| PolygonToolMessage::Overlays(overlay_context).into()),
tool_abort: Some(PolygonToolMessage::Abort.into()),
working_color_changed: Some(PolygonToolMessage::WorkingColorChanged.into()),
..Default::default()
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
enum PolygonToolFsmState {
#[default]
Ready,
Drawing,
}
#[derive(Clone, Debug, Default)]
struct PolygonToolData {
data: Resize,
auto_panning: AutoPanning,
}
impl Fsm for PolygonToolFsmState {
type ToolData = PolygonToolData;
type ToolOptions = PolygonOptions;
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, global_tool_data, input, ..
} = tool_action_data;
let polygon_data = &mut tool_data.data;
let ToolMessage::Polygon(event) = event else { return self };
match (self, event) {
(_, PolygonToolMessage::Overlays(mut overlay_context)) => {
polygon_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
self
}
(PolygonToolFsmState::Ready, PolygonToolMessage::DragStart) => {
polygon_data.start(document, input);
responses.add(DocumentMessage::StartTransaction);
let node = match tool_options.polygon_type {
PolygonType::Convex => resolve_document_node_type("Regular Polygon")
.expect("Regular Polygon node does not exist")
.node_template_input_override([
None,
Some(NodeInput::value(TaggedValue::U32(tool_options.vertices), false)),
Some(NodeInput::value(TaggedValue::F64(0.5), false)),
]),
PolygonType::Star => resolve_document_node_type("Star").expect("Star node does not exist").node_template_input_override([
None,
Some(NodeInput::value(TaggedValue::U32(tool_options.vertices), false)),
Some(NodeInput::value(TaggedValue::F64(0.5), false)),
Some(NodeInput::value(TaggedValue::F64(0.25), false)),
]),
};
let nodes = vec![(NodeId(0), node)];
let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, document.new_layer_bounding_artboard(input), responses);
responses.add(Message::StartBuffer);
responses.add(GraphOperationMessage::TransformSet {
layer,
transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position),
transform_in: TransformIn::Viewport,
skip_rerender: false,
});
tool_options.fill.apply_fill(layer, responses);
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses);
polygon_data.layer = Some(layer);
PolygonToolFsmState::Drawing
}
(PolygonToolFsmState::Drawing, PolygonToolMessage::PointerMove { center, lock_ratio }) => {
if let Some([start, end]) = tool_data.data.calculate_points(document, input, center, lock_ratio) {
if let Some(layer) = tool_data.data.layer {
// TODO: We need to determine how to allow the polygon node to make irregular shapes
update_radius_sign(end, start, layer, document, responses);
let dimensions = (start - end).abs();
let mut scale = DVec2::ONE;
let radius: f64;
// We keep the smaller dimension's scale at 1 and scale the other dimension accordingly
if dimensions.x > dimensions.y {
scale.x = dimensions.x / dimensions.y;
radius = dimensions.y / 2.;
} else {
scale.y = dimensions.y / dimensions.x;
radius = dimensions.x / 2.;
}
match tool_options.polygon_type {
PolygonType::Convex => {
let Some(node_id) = graph_modification_utils::get_polygon_id(layer, &document.network_interface) else {
return self;
};
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 2),
input: NodeInput::value(TaggedValue::F64(radius), false),
});
}
PolygonType::Star => {
let Some(node_id) = graph_modification_utils::get_star_id(layer, &document.network_interface) else {
return self;
};
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 2),
input: NodeInput::value(TaggedValue::F64(radius), false),
});
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 3),
input: NodeInput::value(TaggedValue::F64(radius / 2.), false),
});
}
}
responses.add(GraphOperationMessage::TransformSet {
layer,
transform: DAffine2::from_scale_angle_translation(scale, 0., (start + end) / 2.),
transform_in: TransformIn::Viewport,
skip_rerender: false,
});
}
}
// Auto-panning
let messages = [
PolygonToolMessage::PointerOutsideViewport { center, lock_ratio }.into(),
PolygonToolMessage::PointerMove { center, lock_ratio }.into(),
];
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses);
self
}
(_, PolygonToolMessage::PointerMove { .. }) => {
polygon_data.snap_manager.preview_draw(&SnapData::new(document, input), input.mouse.position);
responses.add(OverlaysMessage::Draw);
self
}
(PolygonToolFsmState::Drawing, PolygonToolMessage::PointerOutsideViewport { .. }) => {
// Auto-panning
let _ = tool_data.auto_panning.shift_viewport(input, responses);
PolygonToolFsmState::Drawing
}
(state, PolygonToolMessage::PointerOutsideViewport { center, lock_ratio }) => {
// Auto-panning
let messages = [
PolygonToolMessage::PointerOutsideViewport { center, lock_ratio }.into(),
PolygonToolMessage::PointerMove { center, lock_ratio }.into(),
];
tool_data.auto_panning.stop(&messages, responses);
state
}
(PolygonToolFsmState::Drawing, PolygonToolMessage::DragStop) => {
input.mouse.finish_transaction(polygon_data.viewport_drag_start(document), responses);
polygon_data.cleanup(responses);
PolygonToolFsmState::Ready
}
(PolygonToolFsmState::Drawing, PolygonToolMessage::Abort) => {
responses.add(DocumentMessage::AbortTransaction);
polygon_data.cleanup(responses);
PolygonToolFsmState::Ready
}
(_, PolygonToolMessage::WorkingColorChanged) => {
responses.add(PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::WorkingColors(
Some(global_tool_data.primary_color),
Some(global_tool_data.secondary_color),
)));
self
}
_ => self,
}
}
fn update_hints(&self, responses: &mut VecDeque<Message>) {
let hint_data = match self {
PolygonToolFsmState::Ready => HintData(vec![HintGroup(vec![
HintInfo::mouse(MouseMotion::LmbDrag, "Draw Polygon"),
HintInfo::keys([Key::Shift], "Constrain Regular").prepend_plus(),
HintInfo::keys([Key::Alt], "From Center").prepend_plus(),
])]),
PolygonToolFsmState::Drawing => HintData(vec![
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Regular"), HintInfo::keys([Key::Alt], "From Center")]),
]),
};
responses.add(FrontendMessage::UpdateInputHints { hint_data });
}
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair });
}
}
/// In the case where the polygon/star is upside down and the number of sides is odd, we negate the radius instead of using a negative scale.
fn update_radius_sign(end: DVec2, start: DVec2, layer: LayerNodeIdentifier, document: &mut DocumentMessageHandler, responses: &mut VecDeque<Message>) {
let sign_num = if end[1] > start[1] { 1. } else { -1. };
let new_layer = NodeGraphLayer::new(layer, &document.network_interface);
if new_layer.find_input("Regular Polygon", 1).unwrap_or(&TaggedValue::U32(0)).to_u32() % 2 == 1 {
let Some(polygon_node_id) = new_layer.upstream_node_id_from_name("Regular Polygon") else { return };
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(polygon_node_id, 2),
input: NodeInput::value(TaggedValue::F64(sign_num * 0.5), false),
});
return;
}
if new_layer.find_input("Star", 1).unwrap_or(&TaggedValue::U32(0)).to_u32() % 2 == 1 {
let Some(star_node_id) = new_layer.upstream_node_id_from_name("Star") else { return };
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(star_node_id, 2),
input: NodeInput::value(TaggedValue::F64(sign_num * 0.5), false),
});
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(star_node_id, 3),
input: NodeInput::value(TaggedValue::F64(sign_num * 0.25), false),
});
}
}

Some files were not shown because too many files have changed in this diff Show More