Merge remote-tracking branch 'mohsin/master' into hierarchical-tree

This commit is contained in:
Mohd Mohsin 2025-07-06 09:43:51 +05:30
commit 22d3ea15b2
324 changed files with 20669 additions and 31314 deletions

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

View file

@ -2,7 +2,6 @@ name: Clippy Check
on:
pull_request:
branches: [master]
types: [opened, reopened, synchronize, ready_for_review]
jobs:

View file

@ -2,7 +2,6 @@ name: Profiling Changes
on:
pull_request:
branches: [master]
env:
CARGO_TERM_COLOR: always

View file

@ -7,8 +7,6 @@ on:
paths:
- "libraries/rawkit/**"
pull_request:
branches:
- master
paths:
- "libraries/rawkit/**"

View file

@ -7,8 +7,6 @@ on:
paths:
- website/**
pull_request:
branches:
- master
paths:
- website/**
env:

3
.gitignore vendored
View file

@ -4,4 +4,5 @@ target/
perf.data*
profile.json
flamegraph.svg
.idea/
.direnv

18
.nix/flake.lock generated
View file

@ -20,11 +20,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1743583204,
"narHash": "sha256-F7n4+KOIfWrwoQjXrL2wD9RhFYLs2/GGe/MQY1sSdlE=",
"lastModified": 1748190013,
"narHash": "sha256-R5HJFflOfsP5FBtk+zE8FpL8uqE7n62jqOsADvVshhE=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "5135c59491985879812717f4c9fea69604e7f26f",
"rev": "62b852f6c6742134ade1abdd2a21685fd617a291",
"type": "github"
},
"original": {
@ -36,11 +36,11 @@
},
"nixpkgs-unstable": {
"locked": {
"lastModified": 1739214665,
"narHash": "sha256-26L8VAu3/1YRxS8MHgBOyOM8xALdo6N0I04PgorE7UM=",
"lastModified": 1748190013,
"narHash": "sha256-R5HJFflOfsP5FBtk+zE8FpL8uqE7n62jqOsADvVshhE=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "2c8d3f48d33929642c1c12cd243df4cc7d2ce434",
"rev": "62b852f6c6742134ade1abdd2a21685fd617a291",
"type": "github"
},
"original": {
@ -65,11 +65,11 @@
]
},
"locked": {
"lastModified": 1743682350,
"narHash": "sha256-S/MyKOFajCiBm5H5laoE59wB6w0NJ4wJG53iAPfYW3k=",
"lastModified": 1748399823,
"narHash": "sha256-kahD8D5hOXOsGbNdoLLnqCL887cjHkx98Izc37nDjlA=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "c4a8327b0f25d1d81edecbb6105f74d7cf9d7382",
"rev": "d68a69dc71bc19beb3479800392112c2f6218159",
"type": "github"
},
"original": {

View file

@ -51,7 +51,7 @@
libraw
# Tauri dependencies: keep in sync with https://v2.tauri.app/start/prerequisites/
# Tauri dependencies: keep in sync with https://v2.tauri.app/start/prerequisites/#system-dependencies (under the NixOS tab)
at-spi2-atk
atkmm
cairo
@ -78,6 +78,7 @@
pkgs.git
pkgs.gobject-introspection
pkgs-unstable.cargo-tauri
pkgs-unstable.cargo-about
# Linker
pkgs.mold

679
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -4,31 +4,37 @@ members = [
"proc-macros",
"frontend/wasm",
"frontend/src-tauri",
"node-graph/gapplication-io",
"node-graph/gbrush",
"node-graph/gcore",
"node-graph/gstd",
"node-graph/gmath-nodes",
"node-graph/gpath-bool",
"node-graph/graph-craft",
"node-graph/graphene-cli",
"node-graph/graster-nodes",
"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/gbrush",
"node-graph/gcore",
"node-graph/gstd",
"node-graph/gmath-nodes",
"node-graph/gpath-bool",
"node-graph/graph-craft",
"node-graph/graphene-cli",
"node-graph/graster-nodes",
"node-graph/gsvg-renderer",
"node-graph/interpreted-executor",
"node-graph/node-macro",
]
@ -36,19 +42,28 @@ 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-brush = { path = "node-graph/gbrush" }
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-raster-nodes = { path = "node-graph/graster-nodes" }
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 +75,55 @@ 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"] }
rustybuzz = "0.20"
spirv = "0.3"
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp"] }
parley = "0.5.0"
skrifa = "0.32.0"
pretty_assertions = "1.4.1"
fern = { version = "0.7", features = ["colored"] }
num_enum = "0.7"
@ -103,11 +137,23 @@ 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", features = ["std"] }
criterion = { version = "0.5", features = ["html_reports"] }
iai-callgrind = { version = "0.12.3" }
ndarray = "0.16.1"
[profile.dev]
opt-level = 1

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

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:#?}");
// }
// }

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";
@ -136,3 +146,6 @@ pub const DEFAULT_DOCUMENT_NAME: &str = "Untitled Document";
pub const FILE_SAVE_SUFFIX: &str = ".graphite";
pub const MAX_UNDO_HISTORY_LEN: usize = 100; // TODO: Add this to user preferences
pub const AUTO_SAVE_TIMEOUT_SECONDS: u64 = 15;
// INPUT
pub const DOUBLE_CLICK_MILLISECONDS: u64 = 500;

View file

@ -40,7 +40,6 @@ impl DispatcherMessageHandlers {
/// The last occurrence of the message in the message queue is sufficient to ensure correct behavior.
/// In addition, these messages do not change any state in the backend (aside from caches).
const SIDE_EFFECT_FREE_MESSAGES: &[MessageDiscriminant] = &[
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::NodeGraph(NodeGraphMessageDiscriminant::SendGraph))),
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::PropertiesPanel(
PropertiesPanelMessageDiscriminant::Refresh,
))),
@ -319,12 +318,11 @@ impl Dispatcher {
}))
}
/// Logs a message that is about to be executed,
/// either as a tree with a discriminant or the entire payload (depending on settings)
/// Logs a message that is about to be executed, either as a tree
/// with a discriminant or the entire payload (depending on settings)
fn log_message(&self, message: &Message, queues: &[VecDeque<Message>], message_logging_verbosity: MessageLoggingVerbosity) {
let discriminant = MessageDiscriminant::from(message);
let is_blocked = DEBUG_MESSAGE_BLOCK_LIST.iter().any(|&blocked_discriminant| discriminant == blocked_discriminant)
|| DEBUG_MESSAGE_ENDING_BLOCK_LIST.iter().any(|blocked_name| discriminant.local_name().ends_with(blocked_name));
let is_blocked = DEBUG_MESSAGE_BLOCK_LIST.contains(&discriminant) || DEBUG_MESSAGE_ENDING_BLOCK_LIST.iter().any(|blocked_name| discriminant.local_name().ends_with(blocked_name));
if !is_blocked {
match message_logging_verbosity {
@ -498,9 +496,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 +541,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

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;

View file

@ -145,12 +145,14 @@ impl LayoutHolder for ExportDialogMessageHandler {
DropdownInput::new(entries).selected_index(Some(index as u32)).widget_holder(),
];
let mut checkbox_id = CheckboxId::default();
let transparent_background = vec![
TextLabel::new("Transparency").table_align(true).min_width(100).widget_holder(),
TextLabel::new("Transparency").table_align(true).min_width(100).for_checkbox(&mut checkbox_id).widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
CheckboxInput::new(self.transparent_background)
.disabled(self.file_type == FileType::Jpg)
.on_update(move |value: &CheckboxInput| ExportDialogMessage::TransparentBackground(value.checked).into())
.for_label(checkbox_id.clone())
.widget_holder(),
];

View file

@ -27,7 +27,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()),
});
}
@ -78,11 +78,13 @@ impl LayoutHolder for NewDocumentDialogMessageHandler {
.widget_holder(),
];
let mut checkbox_id = CheckboxId::default();
let infinite = vec![
TextLabel::new("Infinite Canvas").table_align(true).min_width(90).widget_holder(),
TextLabel::new("Infinite Canvas").table_align(true).min_width(90).for_checkbox(&mut checkbox_id).widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
CheckboxInput::new(self.infinite)
.on_update(|checkbox_input: &CheckboxInput| NewDocumentDialogMessage::Infinite(checkbox_input.checked).into())
.for_label(checkbox_id.clone())
.widget_holder(),
];

View file

@ -1,6 +1,6 @@
use crate::consts::{VIEWPORT_ZOOM_WHEEL_RATE, VIEWPORT_ZOOM_WHEEL_RATE_CHANGE};
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::node_graph::utility_types::GraphWireStyle;
use crate::messages::portfolio::document::utility_types::wires::GraphWireStyle;
use crate::messages::preferences::SelectionMode;
use crate::messages::prelude::*;
@ -68,6 +68,7 @@ impl PreferencesDialogMessageHandler {
.widget_holder(),
];
let mut checkbox_id = CheckboxId::default();
let zoom_with_scroll_tooltip = "Use the scroll wheel for zooming instead of vertically panning (not recommended for trackpads)";
let zoom_with_scroll = vec![
Separator::new(SeparatorType::Unrelated).widget_holder(),
@ -80,8 +81,13 @@ impl PreferencesDialogMessageHandler {
}
.into()
})
.for_label(checkbox_id.clone())
.widget_holder(),
TextLabel::new("Zoom with Scroll")
.table_align(true)
.tooltip(zoom_with_scroll_tooltip)
.for_checkbox(&mut checkbox_id)
.widget_holder(),
TextLabel::new("Zoom with Scroll").table_align(true).tooltip(zoom_with_scroll_tooltip).widget_holder(),
];
// =======
@ -163,6 +169,7 @@ impl PreferencesDialogMessageHandler {
graph_wire_style,
];
let mut checkbox_id = CheckboxId::default();
let vello_tooltip = "Use the experimental Vello renderer (your browser must support WebGPU)";
let use_vello = vec![
Separator::new(SeparatorType::Unrelated).widget_holder(),
@ -171,46 +178,34 @@ impl PreferencesDialogMessageHandler {
.tooltip(vello_tooltip)
.disabled(!preferences.supports_wgpu())
.on_update(|checkbox_input: &CheckboxInput| PreferencesMessage::UseVello { use_vello: checkbox_input.checked }.into())
.for_label(checkbox_id.clone())
.widget_holder(),
TextLabel::new("Vello Renderer")
.table_align(true)
.tooltip(vello_tooltip)
.disabled(!preferences.supports_wgpu())
.for_checkbox(&mut checkbox_id)
.widget_holder(),
];
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 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 stroke joins and fills.";
let vector_meshes = vec![
Separator::new(SeparatorType::Unrelated).widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
CheckboxInput::new(preferences.vector_meshes)
.tooltip(vector_mesh_tooltip)
.on_update(|checkbox_input: &CheckboxInput| PreferencesMessage::VectorMeshes { enabled: checkbox_input.checked }.into())
.for_label(checkbox_id.clone())
.widget_holder(),
TextLabel::new("Vector Meshes")
.table_align(true)
.tooltip(vector_mesh_tooltip)
.for_checkbox(&mut checkbox_id)
.widget_holder(),
TextLabel::new("Vector Meshes").table_align(true).tooltip(vector_mesh_tooltip).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 },
@ -224,8 +219,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 },
]))
}

View file

@ -1,14 +1,15 @@
use super::utility_types::{FrontendDocumentDetails, MouseCursorIcon};
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::node_graph::utility_types::{
BoxSelection, ContextMenuInformation, FrontendClickTargets, FrontendGraphInput, FrontendGraphOutput, FrontendNode, FrontendNodeType, FrontendNodeWire, Transform, WirePath,
BoxSelection, ContextMenuInformation, FrontendClickTargets, FrontendGraphInput, FrontendGraphOutput, FrontendNode, FrontendNodeType, Transform,
};
use crate::messages::portfolio::document::utility_types::nodes::{JsRawBuffer, LayerPanelEntry, RawBuffer};
use crate::messages::portfolio::document::utility_types::wires::{WirePath, WirePathUpdate};
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 +100,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,
},
@ -240,7 +228,17 @@ pub enum FrontendMessage {
#[serde(rename = "hintData")]
hint_data: HintData,
},
UpdateLayersPanelControlBarLayout {
UpdateLayersPanelControlBarLeftLayout {
#[serde(rename = "layoutTarget")]
layout_target: LayoutTarget,
diff: Vec<WidgetDiff>,
},
UpdateLayersPanelControlBarRightLayout {
#[serde(rename = "layoutTarget")]
layout_target: LayoutTarget,
diff: Vec<WidgetDiff>,
},
UpdateLayersPanelBottomBarLayout {
#[serde(rename = "layoutTarget")]
layout_target: LayoutTarget,
diff: Vec<WidgetDiff>,
@ -253,12 +251,16 @@ pub enum FrontendMessage {
UpdateMouseCursor {
cursor: MouseCursorIcon,
},
UpdateNodeGraph {
UpdateNodeGraphNodes {
nodes: Vec<FrontendNode>,
wires: Vec<FrontendNodeWire>,
#[serde(rename = "wiresDirectNotGridAligned")]
wires_direct_not_grid_aligned: bool,
},
UpdateVisibleNodes {
nodes: Vec<NodeId>,
},
UpdateNodeGraphWires {
wires: Vec<WirePathUpdate>,
},
ClearAllNodeGraphWires,
UpdateNodeGraphControlBarLayout {
#[serde(rename = "layoutTarget")]
layout_target: LayoutTarget,

View file

@ -1,6 +1,7 @@
use super::utility_types::input_keyboard::KeysGroup;
use super::utility_types::misc::Mapping;
use crate::messages::input_mapper::utility_types::input_keyboard::{self, Key};
use crate::messages::input_mapper::utility_types::misc::MappingEntry;
use crate::messages::portfolio::utility_types::KeyboardPlatformLayout;
use crate::messages::prelude::*;
use std::fmt::Write;
@ -49,12 +50,12 @@ impl InputMapperMessageHandler {
ma.map(|a| ((i as u8).try_into().unwrap(), a))
})
.for_each(|(k, a): (Key, _)| {
let _ = write!(output, "{}: {}, ", k.to_discriminant().local_name(), a.local_name().split('.').last().unwrap());
let _ = write!(output, "{}: {}, ", k.to_discriminant().local_name(), a.local_name().split('.').next_back().unwrap());
});
output.replace("Key", "")
}
pub fn action_input_mapping(&self, action_to_find: &MessageDiscriminant) -> Vec<KeysGroup> {
pub fn action_input_mapping(&self, action_to_find: &MessageDiscriminant) -> Option<KeysGroup> {
let all_key_mapping_entries = std::iter::empty()
.chain(self.mapping.key_up.iter())
.chain(self.mapping.key_down.iter())
@ -68,55 +69,65 @@ impl InputMapperMessageHandler {
// Filter for the desired message
let found_actions = all_mapping_entries.filter(|entry| entry.action.to_discriminant() == *action_to_find);
// Get the `Key` for this platform's accelerator key
let keyboard_layout = || GLOBAL_PLATFORM.get().copied().unwrap_or_default().as_keyboard_platform_layout();
let platform_accel_key = match keyboard_layout() {
KeyboardPlatformLayout::Standard => Key::Control,
KeyboardPlatformLayout::Mac => Key::Command,
};
let entry_to_key = |entry: &MappingEntry| {
// Get the modifier keys for the entry (and convert them to Key)
let mut keys = entry
.modifiers
.iter()
.map(|i| {
// TODO: Use a safe solution eventually
assert!(
i < input_keyboard::NUMBER_OF_KEYS,
"Attempting to convert a Key with enum index {i}, which is larger than the number of Key enums",
);
(i as u8).try_into().unwrap()
})
.collect::<Vec<_>>();
// Append the key button for the entry
use InputMapperMessage as IMM;
match entry.input {
IMM::KeyDown(key) | IMM::KeyUp(key) | IMM::KeyDownNoRepeat(key) | IMM::KeyUpNoRepeat(key) => keys.push(key),
_ => (),
}
keys.sort_by(|&a, &b| {
// Order according to platform guidelines mentioned at https://ux.stackexchange.com/questions/58185/normative-ordering-for-modifier-key-combinations
const ORDER: [Key; 4] = [Key::Control, Key::Alt, Key::Shift, Key::Command];
// Treat the `Accel` virtual key as the platform's accel key for sorting comparison purposes
let a = if a == Key::Accel { platform_accel_key } else { a };
let b = if b == Key::Accel { platform_accel_key } else { b };
// Find where the keys are in the order, or put them at the end if they're not found
let a = ORDER.iter().position(|&key| key == a).unwrap_or(ORDER.len());
let b = ORDER.iter().position(|&key| key == b).unwrap_or(ORDER.len());
// Compare the positions of both keys
a.cmp(&b)
});
KeysGroup(keys)
};
// If a canonical key combination is found, return it
if let Some(canonical) = found_actions.clone().find(|entry| entry.canonical).map(entry_to_key) {
return Some(canonical);
}
// Find the key combinations for all keymaps matching the desired action
assert!(std::mem::size_of::<usize>() >= std::mem::size_of::<Key>());
found_actions
.map(|entry| {
// Get the modifier keys for the entry (and convert them to Key)
let mut keys = entry
.modifiers
.iter()
.map(|i| {
// TODO: Use a safe solution eventually
assert!(
i < input_keyboard::NUMBER_OF_KEYS,
"Attempting to convert a Key with enum index {i}, which is larger than the number of Key enums",
);
(i as u8).try_into().unwrap()
})
.collect::<Vec<_>>();
let mut key_sequences = found_actions.map(entry_to_key).collect::<Vec<_>>();
// Append the key button for the entry
use InputMapperMessage as IMM;
match entry.input {
IMM::KeyDown(key) | IMM::KeyUp(key) | IMM::KeyDownNoRepeat(key) | IMM::KeyUpNoRepeat(key) => keys.push(key),
_ => (),
}
keys.sort_by(|&a, &b| {
// Order according to platform guidelines mentioned at https://ux.stackexchange.com/questions/58185/normative-ordering-for-modifier-key-combinations
const ORDER: [Key; 4] = [Key::Control, Key::Alt, Key::Shift, Key::Command];
// Treat the `Accel` virtual key as the platform's accel key for sorting comparison purposes
let a = if a == Key::Accel { platform_accel_key } else { a };
let b = if b == Key::Accel { platform_accel_key } else { b };
// Find where the keys are in the order, or put them at the end if they're not found
let a = ORDER.iter().position(|&key| key == a).unwrap_or(ORDER.len());
let b = ORDER.iter().position(|&key| key == b).unwrap_or(ORDER.len());
// Compare the positions of both keys
a.cmp(&b)
});
KeysGroup(keys)
})
.collect::<Vec<_>>()
// Return the shortest key sequence, if any
key_sequences.sort_by_key(|keys| keys.0.len());
key_sequences.first().cloned()
}
}

View file

@ -8,7 +8,6 @@ use crate::messages::input_mapper::utility_types::misc::{KeyMappingEntries, Mapp
use crate::messages::portfolio::document::node_graph::utility_types::Direction;
use crate::messages::portfolio::document::utility_types::clipboards::Clipboard;
use crate::messages::portfolio::document::utility_types::misc::GroupFolderType;
use crate::messages::portfolio::document::utility_types::transformation::TransformType;
use crate::messages::prelude::*;
use crate::messages::tool::tool_messages::brush_tool::BrushToolMessageOptionsUpdate;
use crate::messages::tool::tool_messages::select_tool::SelectToolPointerKeys;
@ -171,40 +170,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,20 +211,21 @@ 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),
entry!(KeyDown(KeyA); modifiers=[Accel, Shift], canonical, action_dispatch=PathToolMessage::DeselectAllPoints),
entry!(KeyDown(KeyA); modifiers=[Alt], action_dispatch=PathToolMessage::DeselectAllPoints),
entry!(KeyDown(Backspace); action_dispatch=PathToolMessage::Delete),
entry!(KeyUp(MouseLeft); action_dispatch=PathToolMessage::DragStop { extend_selection: Shift, shrink_selection: Alt }),
entry!(KeyDown(Enter); action_dispatch=PathToolMessage::Enter { extend_selection: Shift, shrink_selection: Alt }),
entry!(DoubleClick(MouseButton::Left); action_dispatch=PathToolMessage::FlipSmoothSharp),
entry!(DoubleClick(MouseButton::Left); action_dispatch=PathToolMessage::DoubleClick { extend_selection: Shift, shrink_selection: Alt }),
entry!(KeyDown(ArrowRight); action_dispatch=PathToolMessage::NudgeSelectedPoints { delta_x: NUDGE_AMOUNT, delta_y: 0. }),
entry!(KeyDown(ArrowRight); modifiers=[Shift], action_dispatch=PathToolMessage::NudgeSelectedPoints { delta_x: BIG_NUDGE_AMOUNT, delta_y: 0. }),
entry!(KeyDown(ArrowRight); modifiers=[ArrowUp], action_dispatch=PathToolMessage::NudgeSelectedPoints { delta_x: NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT }),
@ -308,14 +308,15 @@ 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(KeyD); action_dispatch=ToolMessage::ResetColors),
entry!(KeyDown(KeyX); modifiers=[Shift], action_dispatch=ToolMessage::SwapColors),
entry!(KeyDown(KeyC); modifiers=[Alt], action_dispatch=ToolMessage::SelectRandomPrimaryColor),
entry!(KeyDown(KeyC); modifiers=[Alt], action_dispatch=ToolMessage::SelectRandomWorkingColor { primary: true }),
entry!(KeyDown(KeyC); modifiers=[Alt, Shift], action_dispatch=ToolMessage::SelectRandomWorkingColor { primary: false }),
//
// DocumentMessage
entry!(KeyDown(Space); modifiers=[Control], action_dispatch=DocumentMessage::GraphViewOverlayToggle),
@ -327,20 +328,21 @@ pub fn input_mappings() -> Mapping {
entry!(KeyDown(KeyH); modifiers=[Accel], action_dispatch=DocumentMessage::ToggleSelectedVisibility),
entry!(KeyDown(KeyL); modifiers=[Accel], action_dispatch=DocumentMessage::ToggleSelectedLocked),
entry!(KeyDown(KeyG); modifiers=[Alt], action_dispatch=DocumentMessage::ToggleGridVisibility),
entry!(KeyDown(KeyZ); modifiers=[Accel, Shift], action_dispatch=DocumentMessage::Redo),
entry!(KeyDown(KeyZ); modifiers=[Accel, Shift], canonical, action_dispatch=DocumentMessage::Redo),
entry!(KeyDown(KeyY); modifiers=[Accel], action_dispatch=DocumentMessage::Redo),
entry!(KeyDown(KeyZ); modifiers=[Accel], action_dispatch=DocumentMessage::Undo),
entry!(KeyDown(KeyA); modifiers=[Accel], action_dispatch=DocumentMessage::SelectAllLayers),
entry!(KeyDown(KeyA); modifiers=[Accel, Shift], action_dispatch=DocumentMessage::DeselectAllLayers),
entry!(KeyDown(KeyA); modifiers=[Accel, Shift], canonical, action_dispatch=DocumentMessage::DeselectAllLayers),
entry!(KeyDown(KeyA); modifiers=[Alt], action_dispatch=DocumentMessage::DeselectAllLayers),
entry!(KeyDown(KeyS); modifiers=[Accel], action_dispatch=DocumentMessage::SaveDocument),
entry!(KeyDown(KeyD); modifiers=[Accel], action_dispatch=DocumentMessage::DuplicateSelectedLayers),
entry!(KeyDown(KeyD); modifiers=[Accel], canonical, action_dispatch=DocumentMessage::DuplicateSelectedLayers),
entry!(KeyDown(KeyJ); modifiers=[Accel], action_dispatch=DocumentMessage::DuplicateSelectedLayers),
entry!(KeyDown(KeyG); modifiers=[Accel], action_dispatch=DocumentMessage::GroupSelectedLayers { group_folder_type: GroupFolderType::Layer }),
entry!(KeyDown(KeyG); modifiers=[Accel, Shift], action_dispatch=DocumentMessage::UngroupSelectedLayers),
entry!(KeyDown(KeyN); modifiers=[Accel, Shift], action_dispatch=DocumentMessage::CreateEmptyFolder),
entry!(KeyDown(Backslash); modifiers=[Alt], action_dispatch=DocumentMessage::SelectParentLayer),
entry!(KeyDown(BracketLeft); modifiers=[Alt], action_dispatch=DocumentMessage::SelectionStepBack),
entry!(KeyDown(BracketRight); modifiers=[Alt], action_dispatch=DocumentMessage::SelectionStepForward),
entry!(KeyDown(Escape); modifiers=[Shift], action_dispatch=DocumentMessage::SelectParentLayer),
entry!(KeyDown(BracketLeft); modifiers=[Alt], canonical, action_dispatch=DocumentMessage::SelectionStepBack),
entry!(KeyDown(BracketRight); modifiers=[Alt], canonical, action_dispatch=DocumentMessage::SelectionStepForward),
entry!(KeyDown(MouseBack); action_dispatch=DocumentMessage::SelectionStepBack),
entry!(KeyDown(MouseForward); action_dispatch=DocumentMessage::SelectionStepForward),
entry!(KeyDown(Digit0); modifiers=[Accel], action_dispatch=DocumentMessage::ZoomCanvasToFitAll),
@ -376,9 +378,9 @@ pub fn input_mappings() -> Mapping {
entry!(KeyDown(ArrowRight); action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: 0., resize: Alt, resize_opposite_corner: Control }),
//
// TransformLayerMessage
entry!(KeyDown(KeyG); action_dispatch=TransformLayerMessage::BeginGRS { transform_type: TransformType::Grab }),
entry!(KeyDown(KeyR); action_dispatch=TransformLayerMessage::BeginGRS { transform_type: TransformType::Rotate }),
entry!(KeyDown(KeyS); action_dispatch=TransformLayerMessage::BeginGRS { transform_type: TransformType::Scale }),
entry!(KeyDown(KeyG); action_dispatch=TransformLayerMessage::BeginGrab),
entry!(KeyDown(KeyR); action_dispatch=TransformLayerMessage::BeginRotate),
entry!(KeyDown(KeyS); action_dispatch=TransformLayerMessage::BeginScale),
entry!(KeyDown(Digit0); action_dispatch=TransformLayerMessage::TypeDigit { digit: 0 }),
entry!(KeyDown(Digit1); action_dispatch=TransformLayerMessage::TypeDigit { digit: 1 }),
entry!(KeyDown(Digit2); action_dispatch=TransformLayerMessage::TypeDigit { digit: 2 }),

View file

@ -27,7 +27,7 @@ impl MessageHandler<KeyMappingMessage, KeyMappingMessageData<'_>> for KeyMapping
}
impl KeyMappingMessageHandler {
pub fn action_input_mapping(&self, action_to_find: &MessageDiscriminant) -> Vec<KeysGroup> {
pub fn action_input_mapping(&self, action_to_find: &MessageDiscriminant) -> Option<KeysGroup> {
self.mapping_handler.action_input_mapping(action_to_find)
}
}

View file

@ -24,44 +24,54 @@ macro_rules! modifiers {
/// Each handler adds or removes actions in the form of message discriminants. Here, we tie an input condition (such as a hotkey) to an action's full message.
/// When an action is currently available, and the user enters that input, the action's message is dispatched on the message bus.
macro_rules! entry {
// Pattern with canonical parameter
($input:expr_2021; $(modifiers=[$($modifier:ident),*],)? $(refresh_keys=[$($refresh:ident),* $(,)?],)? canonical, action_dispatch=$action_dispatch:expr_2021$(,)?) => {
entry!($input; $($($modifier),*)?; $($($refresh),*)?; $action_dispatch; true)
};
// Pattern without canonical parameter
($input:expr_2021; $(modifiers=[$($modifier:ident),*],)? $(refresh_keys=[$($refresh:ident),* $(,)?],)? action_dispatch=$action_dispatch:expr_2021$(,)?) => {
entry!($input; $($($modifier),*)?; $($($refresh),*)?; $action_dispatch; false)
};
// Implementation macro to avoid code duplication
($input:expr; $($modifier:ident),*; $($refresh:ident),*; $action_dispatch:expr; $canonical:expr) => {
&[&[
// Cause the `action_dispatch` message to be sent when the specified input occurs.
MappingEntry {
action: $action_dispatch.into(),
input: $input,
modifiers: modifiers!($($($modifier),*)?),
modifiers: modifiers!($($modifier),*),
canonical: $canonical,
},
// Also cause the `action_dispatch` message to be sent when any of the specified refresh keys change.
//
// For example, a snapping state bound to the Shift key may change if the user presses or releases that key.
// In that case, we want to dispatch the action's message even though the pointer didn't necessarily move so
// the input handler can update the snapping state without making the user move the mouse to see the change.
$(
$(
MappingEntry {
action: $action_dispatch.into(),
input: InputMapperMessage::KeyDown(Key::$refresh),
modifiers: modifiers!(),
canonical: $canonical,
},
MappingEntry {
action: $action_dispatch.into(),
input: InputMapperMessage::KeyUp(Key::$refresh),
modifiers: modifiers!(),
canonical: $canonical,
},
MappingEntry {
action: $action_dispatch.into(),
input: InputMapperMessage::KeyDownNoRepeat(Key::$refresh),
modifiers: modifiers!(),
canonical: $canonical,
},
MappingEntry {
action: $action_dispatch.into(),
input: InputMapperMessage::KeyUpNoRepeat(Key::$refresh),
modifiers: modifiers!(),
canonical: $canonical,
},
)*
)*
]]
};
}

View file

@ -1,4 +1,4 @@
use super::input_keyboard::{Key, KeysGroup, LayoutKeysGroup, all_required_modifiers_pressed};
use super::input_keyboard::{KeysGroup, LayoutKeysGroup, all_required_modifiers_pressed};
use crate::messages::input_mapper::key_mapping::MappingVariant;
use crate::messages::input_mapper::utility_types::input_keyboard::{KeyStates, NUMBER_OF_KEYS};
use crate::messages::input_mapper::utility_types::input_mouse::NUMBER_OF_MOUSE_BUTTONS;
@ -120,6 +120,8 @@ pub struct MappingEntry {
pub input: InputMapperMessage,
/// Any additional keys that must be also pressed for this input mapping to match
pub modifiers: KeyStates,
/// True indicates that this takes priority as the labeled hotkey shown in UI menus and tooltips instead of an alternate binding for the same action
pub canonical: bool,
}
#[derive(Debug, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
@ -130,36 +132,12 @@ pub enum ActionKeys {
}
impl ActionKeys {
pub fn to_keys(&mut self, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Vec<KeysGroup>) -> String {
pub fn to_keys(&mut self, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Option<KeysGroup>) -> String {
match self {
Self::Action(action) => {
// Take the shortest sequence of keys
let mut key_sequences = action_input_mapping(action);
key_sequences.sort_by_key(|keys| keys.0.len());
let mut secondary_key_sequence = key_sequences.get(1).cloned();
let mut key_sequence = key_sequences.get_mut(0);
// TODO: Replace this exception with a per-action choice of canonical hotkey
if let Some(key_sequence) = &mut key_sequence {
if key_sequence.0.as_slice() == [Key::MouseBack] {
if let Some(replacement) = &mut secondary_key_sequence {
std::mem::swap(*key_sequence, replacement);
}
}
}
if let Some(key_sequence) = &mut key_sequence {
if key_sequence.0.as_slice() == [Key::MouseForward] {
if let Some(replacement) = &mut secondary_key_sequence {
std::mem::swap(*key_sequence, replacement);
}
}
}
if let Some(keys) = key_sequence {
let mut taken_keys = KeysGroup::default();
std::mem::swap(keys, &mut taken_keys);
let description = taken_keys.to_string();
*self = Self::Keys(taken_keys.into());
if let Some(keys) = action_input_mapping(action) {
let description = keys.to_string();
*self = Self::Keys(keys.into());
description
} else {
*self = Self::Keys(KeysGroup::default().into());

View file

@ -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
})()
}
};
@ -386,7 +390,7 @@ impl LayoutMessageHandler {
layout_target: LayoutTarget,
new_layout: Layout,
responses: &mut VecDeque<Message>,
action_input_mapping: &impl Fn(&MessageDiscriminant) -> Vec<KeysGroup>,
action_input_mapping: &impl Fn(&MessageDiscriminant) -> Option<KeysGroup>,
) {
match new_layout {
Layout::WidgetLayout(_) => {
@ -420,7 +424,7 @@ impl LayoutMessageHandler {
}
/// Send a diff to the frontend based on the layout target.
fn send_diff(&self, mut diff: Vec<WidgetDiff>, layout_target: LayoutTarget, responses: &mut VecDeque<Message>, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Vec<KeysGroup>) {
fn send_diff(&self, mut diff: Vec<WidgetDiff>, layout_target: LayoutTarget, responses: &mut VecDeque<Message>, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Option<KeysGroup>) {
diff.iter_mut().for_each(|diff| diff.new_value.apply_keyboard_shortcut(action_input_mapping));
let message = match layout_target {
@ -429,7 +433,9 @@ impl LayoutMessageHandler {
LayoutTarget::DialogColumn2 => FrontendMessage::UpdateDialogColumn2 { layout_target, diff },
LayoutTarget::DocumentBar => FrontendMessage::UpdateDocumentBarLayout { layout_target, diff },
LayoutTarget::DocumentMode => FrontendMessage::UpdateDocumentModeLayout { layout_target, diff },
LayoutTarget::LayersPanelControlBar => FrontendMessage::UpdateLayersPanelControlBarLayout { layout_target, diff },
LayoutTarget::LayersPanelControlLeftBar => FrontendMessage::UpdateLayersPanelControlBarLeftLayout { layout_target, diff },
LayoutTarget::LayersPanelControlRightBar => FrontendMessage::UpdateLayersPanelControlBarRightLayout { layout_target, diff },
LayoutTarget::LayersPanelBottomBar => FrontendMessage::UpdateLayersPanelBottomBarLayout { layout_target, diff },
LayoutTarget::MenuBar => unreachable!("Menu bar is not diffed"),
LayoutTarget::NodeGraphControlBar => FrontendMessage::UpdateNodeGraphControlBarLayout { layout_target, diff },
LayoutTarget::PropertiesSections => FrontendMessage::UpdatePropertyPanelSectionsLayout { layout_target, diff },

View file

@ -31,8 +31,12 @@ pub enum LayoutTarget {
DocumentBar,
/// Contains the dropdown for design / select / guide mode found on the top left of the canvas.
DocumentMode,
/// Options for opacity seen at the top of the Layers panel.
LayersPanelControlBar,
/// Blending options at the top of the Layers panel.
LayersPanelControlLeftBar,
/// Selected layer status (locked/hidden) at the top of the Layers panel.
LayersPanelControlRightBar,
/// Controls for adding, grouping, and deleting layers at the bottom of the Layers panel.
LayersPanelBottomBar,
/// The dropdown menu at the very top of the application: File, Edit, etc.
MenuBar,
/// Bar at the top of the node graph containing the location and the "Preview" and "Hide" buttons.
@ -105,7 +109,7 @@ pub enum Layout {
}
impl Layout {
pub fn unwrap_menu_layout(self, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Vec<KeysGroup>) -> MenuLayout {
pub fn unwrap_menu_layout(self, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Option<KeysGroup>) -> MenuLayout {
if let Self::MenuLayout(mut menu) = self {
menu.layout
.iter_mut()
@ -585,7 +589,7 @@ pub enum DiffUpdate {
impl DiffUpdate {
/// Append the keyboard shortcut to the tooltip where applicable
pub fn apply_keyboard_shortcut(&mut self, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Vec<KeysGroup>) {
pub fn apply_keyboard_shortcut(&mut self, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Option<KeysGroup>) {
// Function used multiple times later in this code block to convert `ActionKeys::Action` to `ActionKeys::Keys` and append its shortcut to the tooltip
let apply_shortcut_to_tooltip = |tooltip_shortcut: &mut ActionKeys, tooltip: &mut String| {
let shortcut_text = tooltip_shortcut.to_keys(action_input_mapping);

View file

@ -42,6 +42,9 @@ pub struct IconButton {
pub struct PopoverButton {
pub style: Option<String>,
#[serde(rename = "menuDirection")]
pub menu_direction: Option<MenuDirection>,
pub icon: Option<String>,
pub disabled: bool,
@ -58,6 +61,20 @@ pub struct PopoverButton {
pub popover_min_width: Option<u32>,
}
#[derive(Clone, Default, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum MenuDirection {
Top,
#[default]
Bottom,
Left,
Right,
TopLeft,
TopRight,
BottomLeft,
BottomRight,
Center,
}
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Default, WidgetBuilder, specta::Type)]
#[derivative(Debug, PartialEq)]
pub struct ParameterExposeButton {

View file

@ -1,10 +1,12 @@
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;
use std::sync::Arc;
#[derive(Clone, Derivative, serde::Serialize, serde::Deserialize, WidgetBuilder, specta::Type)]
#[derivative(Debug, PartialEq)]
@ -18,6 +20,9 @@ pub struct CheckboxInput {
pub tooltip: String,
#[serde(rename = "forLabel", skip_serializing_if = "checkbox_id_is_empty")]
pub for_label: CheckboxId,
#[serde(skip)]
pub tooltip_shortcut: Option<ActionKeys>,
@ -39,12 +44,51 @@ impl Default for CheckboxInput {
icon: "Checkmark".into(),
tooltip: Default::default(),
tooltip_shortcut: Default::default(),
for_label: CheckboxId::default(),
on_update: Default::default(),
on_commit: Default::default(),
}
}
}
#[derive(Clone, Default, Debug, Eq, PartialEq)]
pub struct CheckboxId(Arc<OnceCell<u64>>);
impl CheckboxId {
pub fn fill(&mut self) {
let _ = self.0.set(graphene_std::uuid::generate_uuid());
}
}
impl specta::Type for CheckboxId {
fn inline(_type_map: &mut specta::TypeCollection, _generics: specta::Generics) -> specta::datatype::DataType {
// TODO: This might not be right, but it works for now. We just need the type `bigint | undefined`.
specta::datatype::DataType::Primitive(specta::datatype::PrimitiveType::u64)
}
}
impl serde::Serialize for CheckboxId {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.0.get().copied().serialize(serializer)
}
}
impl<'a> serde::Deserialize<'a> for CheckboxId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'a>,
{
let id = u64::deserialize(deserializer)?;
let checkbox_id = CheckboxId(OnceCell::new().into());
checkbox_id.0.set(id).map_err(serde::de::Error::custom)?;
Ok(checkbox_id)
}
}
fn checkbox_id_is_empty(id: &CheckboxId) -> bool {
id.0.get().is_none()
}
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder, specta::Type)]
#[derivative(Debug, PartialEq, Default)]
pub struct DropdownInput {
@ -67,6 +111,13 @@ pub struct DropdownInput {
#[serde(skip)]
pub tooltip_shortcut: Option<ActionKeys>,
// Styling
#[serde(rename = "minWidth")]
pub min_width: u32,
#[serde(rename = "maxWidth")]
pub max_width: u32,
//
// Callbacks
// `on_update` exists on the `MenuListEntry`, not this parent `DropdownInput`
@ -208,6 +259,9 @@ pub struct NumberInput {
#[serde(rename = "minWidth")]
pub min_width: u32,
#[serde(rename = "maxWidth")]
pub max_width: u32,
// Callbacks
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]

View file

@ -1,3 +1,4 @@
use super::input_widgets::CheckboxId;
use derivative::*;
use graphite_proc_macros::WidgetBuilder;
@ -56,9 +57,21 @@ pub struct TextLabel {
pub tooltip: String,
#[serde(rename = "checkboxId")]
#[widget_builder(skip)]
pub checkbox_id: CheckboxId,
// Body
#[widget_builder(constructor)]
pub value: String,
}
impl TextLabel {
pub fn for_checkbox(mut self, id: &mut CheckboxId) -> Self {
id.fill();
self.checkbox_id = id.clone();
self
}
}
// TODO: Add UserInputLabel

View file

@ -12,7 +12,7 @@ impl MenuBarEntryChildren {
Self(Vec::new())
}
pub fn fill_in_shortcut_actions_with_keys(&mut self, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Vec<KeysGroup>) {
pub fn fill_in_shortcut_actions_with_keys(&mut self, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Option<KeysGroup>) {
let entries = self.0.iter_mut().flatten();
for entry in entries {

View file

@ -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,
},

View file

@ -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;
#[derive(ExtractField)]
@ -301,7 +303,17 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
// Clear the control bar
responses.add(LayoutMessage::SendLayout {
layout: Layout::WidgetLayout(Default::default()),
layout_target: LayoutTarget::LayersPanelControlBar,
layout_target: LayoutTarget::LayersPanelControlLeftBar,
});
responses.add(LayoutMessage::SendLayout {
layout: Layout::WidgetLayout(Default::default()),
layout_target: LayoutTarget::LayersPanelControlRightBar,
});
// Clear the bottom bar
responses.add(LayoutMessage::SendLayout {
layout: Layout::WidgetLayout(Default::default()),
layout_target: LayoutTarget::LayersPanelBottomBar,
});
}
DocumentMessage::CreateEmptyFolder => {
@ -346,6 +358,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
DocumentMessage::DocumentHistoryForward => self.redo_with_history(ipp, responses),
DocumentMessage::DocumentStructureChanged => {
self.update_layers_panel_control_bar_widgets(responses);
self.update_layers_panel_bottom_bar_widgets(responses);
self.network_interface.load_structure();
let data_buffer: RawBuffer = self.serialize_root();
@ -433,6 +446,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
DocumentMessage::EnterNestedNetwork { node_id } => {
self.breadcrumb_network_path.push(node_id);
self.selection_network_path.clone_from(&self.breadcrumb_network_path);
responses.add(NodeGraphMessage::UnloadWires);
responses.add(NodeGraphMessage::SendGraph);
responses.add(DocumentMessage::ZoomCanvasToFitAll);
responses.add(NodeGraphMessage::SetGridAlignedEdges);
@ -462,9 +476,10 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
self.breadcrumb_network_path.pop();
self.selection_network_path.clone_from(&self.breadcrumb_network_path);
}
responses.add(NodeGraphMessage::UnloadWires);
responses.add(NodeGraphMessage::SendGraph);
responses.add(DocumentMessage::PTZUpdate);
responses.add(NodeGraphMessage::SetGridAlignedEdges);
responses.add(NodeGraphMessage::SendGraph);
}
DocumentMessage::FlipSelectedLayers { flip_axis } => {
let scale = match flip_axis {
@ -514,6 +529,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
}
}
DocumentMessage::GraphViewOverlay { open } => {
let opened = !self.graph_view_overlay_open && open;
self.graph_view_overlay_open = open;
responses.add(FrontendMessage::UpdateGraphViewOverlay { open });
@ -526,6 +542,9 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
responses.add(DocumentMessage::RenderRulers);
responses.add(DocumentMessage::RenderScrollbars);
if opened {
responses.add(NodeGraphMessage::UnloadWires);
}
if open {
responses.add(ToolMessage::DeactivateTools);
responses.add(OverlaysMessage::Draw); // Clear the overlays
@ -604,37 +623,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");
@ -681,38 +669,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,
@ -852,7 +843,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 {
@ -1070,6 +1061,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, &[]);
@ -1164,6 +1161,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 {
@ -1568,7 +1571,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;
@ -1576,7 +1579,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(), &[]))
}
@ -1593,7 +1596,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 };
@ -1624,10 +1627,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)
}
})
})
}
@ -1674,13 +1684,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]> {
@ -1688,7 +1713,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 {
@ -1861,6 +1886,7 @@ impl DocumentMessageHandler {
responses.add(NodeGraphMessage::SelectedNodesUpdated);
responses.add(NodeGraphMessage::ForceRunDocumentGraph);
// TODO: Remove once the footprint is used to load the imports/export distances from the edge
responses.add(NodeGraphMessage::UnloadWires);
responses.add(NodeGraphMessage::SetGridAlignedEdges);
responses.add(Message::StartBuffer);
Some(previous_network)
@ -1892,7 +1918,8 @@ impl DocumentMessageHandler {
responses.add(PortfolioMessage::UpdateOpenDocumentsList);
responses.add(NodeGraphMessage::SelectedNodesUpdated);
responses.add(NodeGraphMessage::ForceRunDocumentGraph);
responses.add(NodeGraphMessage::UnloadWires);
responses.add(NodeGraphMessage::SendWires);
Some(previous_network)
}
@ -2049,7 +2076,7 @@ impl DocumentMessageHandler {
/// Loads all of the fonts in the document.
pub fn load_layer_resources(&self, responses: &mut VecDeque<Message>) {
let mut fonts = HashSet::new();
for (_node_id, node) in self.document_network().recursive_nodes() {
for (_node_id, node, _) in self.document_network().recursive_nodes() {
for input in &node.inputs {
if let Some(TaggedValue::Font(font)) = input.as_value() {
fonts.insert(font.clone());
@ -2132,165 +2159,212 @@ impl DocumentMessageHandler {
widgets: vec![TextLabel::new("General").widget_holder()],
},
LayoutGroup::Row {
widgets: vec![
CheckboxInput::new(self.overlays_visibility_settings.artboard_name)
.on_update(|optional_input: &CheckboxInput| {
DocumentMessage::SetOverlaysVisibility {
visible: optional_input.checked,
overlays_type: Some(OverlaysType::ArtboardName),
}
.into()
})
.widget_holder(),
TextLabel::new("Artboard Name".to_string()).widget_holder(),
],
widgets: {
let mut checkbox_id = CheckboxId::default();
vec![
CheckboxInput::new(self.overlays_visibility_settings.artboard_name)
.on_update(|optional_input: &CheckboxInput| {
DocumentMessage::SetOverlaysVisibility {
visible: optional_input.checked,
overlays_type: Some(OverlaysType::ArtboardName),
}
.into()
})
.for_label(checkbox_id.clone())
.widget_holder(),
TextLabel::new("Artboard Name".to_string()).for_checkbox(&mut checkbox_id).widget_holder(),
]
},
},
LayoutGroup::Row {
widgets: vec![
CheckboxInput::new(self.overlays_visibility_settings.transform_measurement)
.on_update(|optional_input: &CheckboxInput| {
DocumentMessage::SetOverlaysVisibility {
visible: optional_input.checked,
overlays_type: Some(OverlaysType::TransformMeasurement),
}
.into()
})
.widget_holder(),
TextLabel::new("G/R/S Measurement".to_string()).widget_holder(),
],
widgets: {
let mut checkbox_id = CheckboxId::default();
vec![
CheckboxInput::new(self.overlays_visibility_settings.transform_measurement)
.on_update(|optional_input: &CheckboxInput| {
DocumentMessage::SetOverlaysVisibility {
visible: optional_input.checked,
overlays_type: Some(OverlaysType::TransformMeasurement),
}
.into()
})
.for_label(checkbox_id.clone())
.widget_holder(),
TextLabel::new("G/R/S Measurement".to_string()).for_checkbox(&mut checkbox_id).widget_holder(),
]
},
},
LayoutGroup::Row {
widgets: vec![TextLabel::new("Select Tool").widget_holder()],
},
LayoutGroup::Row {
widgets: vec![
CheckboxInput::new(self.overlays_visibility_settings.quick_measurement)
.on_update(|optional_input: &CheckboxInput| {
DocumentMessage::SetOverlaysVisibility {
visible: optional_input.checked,
overlays_type: Some(OverlaysType::QuickMeasurement),
}
.into()
})
.widget_holder(),
TextLabel::new("Quick Measurement".to_string()).widget_holder(),
],
widgets: {
let mut checkbox_id = CheckboxId::default();
vec![
CheckboxInput::new(self.overlays_visibility_settings.quick_measurement)
.on_update(|optional_input: &CheckboxInput| {
DocumentMessage::SetOverlaysVisibility {
visible: optional_input.checked,
overlays_type: Some(OverlaysType::QuickMeasurement),
}
.into()
})
.for_label(checkbox_id.clone())
.widget_holder(),
TextLabel::new("Quick Measurement".to_string()).for_checkbox(&mut checkbox_id).widget_holder(),
]
},
},
LayoutGroup::Row {
widgets: vec![
CheckboxInput::new(self.overlays_visibility_settings.transform_cage)
.on_update(|optional_input: &CheckboxInput| {
DocumentMessage::SetOverlaysVisibility {
visible: optional_input.checked,
overlays_type: Some(OverlaysType::TransformCage),
}
.into()
})
.widget_holder(),
TextLabel::new("Transform Cage".to_string()).widget_holder(),
],
widgets: {
let mut checkbox_id = CheckboxId::default();
vec![
CheckboxInput::new(self.overlays_visibility_settings.transform_cage)
.on_update(|optional_input: &CheckboxInput| {
DocumentMessage::SetOverlaysVisibility {
visible: optional_input.checked,
overlays_type: Some(OverlaysType::TransformCage),
}
.into()
})
.for_label(checkbox_id.clone())
.widget_holder(),
TextLabel::new("Transform Cage".to_string()).for_checkbox(&mut checkbox_id).widget_holder(),
]
},
},
LayoutGroup::Row {
widgets: vec![
CheckboxInput::new(self.overlays_visibility_settings.compass_rose)
.on_update(|optional_input: &CheckboxInput| {
DocumentMessage::SetOverlaysVisibility {
visible: optional_input.checked,
overlays_type: Some(OverlaysType::CompassRose),
}
.into()
})
.widget_holder(),
TextLabel::new("Transform Dial".to_string()).widget_holder(),
],
widgets: {
let mut checkbox_id = CheckboxId::default();
vec![
CheckboxInput::new(self.overlays_visibility_settings.compass_rose)
.on_update(|optional_input: &CheckboxInput| {
DocumentMessage::SetOverlaysVisibility {
visible: optional_input.checked,
overlays_type: Some(OverlaysType::CompassRose),
}
.into()
})
.for_label(checkbox_id.clone())
.widget_holder(),
TextLabel::new("Transform Dial".to_string()).for_checkbox(&mut checkbox_id).widget_holder(),
]
},
},
LayoutGroup::Row {
widgets: vec![
CheckboxInput::new(self.overlays_visibility_settings.pivot)
.on_update(|optional_input: &CheckboxInput| {
DocumentMessage::SetOverlaysVisibility {
visible: optional_input.checked,
overlays_type: Some(OverlaysType::Pivot),
}
.into()
})
.widget_holder(),
TextLabel::new("Transform Pivot".to_string()).widget_holder(),
],
widgets: {
let mut checkbox_id = CheckboxId::default();
vec![
CheckboxInput::new(self.overlays_visibility_settings.pivot)
.on_update(|optional_input: &CheckboxInput| {
DocumentMessage::SetOverlaysVisibility {
visible: optional_input.checked,
overlays_type: Some(OverlaysType::Pivot),
}
.into()
})
.for_label(checkbox_id.clone())
.widget_holder(),
TextLabel::new("Transform Pivot".to_string()).for_checkbox(&mut checkbox_id).widget_holder(),
]
},
},
LayoutGroup::Row {
widgets: vec![
CheckboxInput::new(self.overlays_visibility_settings.hover_outline)
.on_update(|optional_input: &CheckboxInput| {
DocumentMessage::SetOverlaysVisibility {
visible: optional_input.checked,
overlays_type: Some(OverlaysType::HoverOutline),
}
.into()
})
.widget_holder(),
TextLabel::new("Hover Outline".to_string()).widget_holder(),
],
widgets: {
let mut checkbox_id = CheckboxId::default();
vec![
CheckboxInput::new(self.overlays_visibility_settings.hover_outline)
.on_update(|optional_input: &CheckboxInput| {
DocumentMessage::SetOverlaysVisibility {
visible: optional_input.checked,
overlays_type: Some(OverlaysType::HoverOutline),
}
.into()
})
.for_label(checkbox_id.clone())
.widget_holder(),
TextLabel::new("Hover Outline".to_string()).for_checkbox(&mut checkbox_id).widget_holder(),
]
},
},
LayoutGroup::Row {
widgets: vec![
CheckboxInput::new(self.overlays_visibility_settings.selection_outline)
.on_update(|optional_input: &CheckboxInput| {
DocumentMessage::SetOverlaysVisibility {
visible: optional_input.checked,
overlays_type: Some(OverlaysType::SelectionOutline),
}
.into()
})
.widget_holder(),
TextLabel::new("Selection Outline".to_string()).widget_holder(),
],
widgets: {
let mut checkbox_id = CheckboxId::default();
vec![
CheckboxInput::new(self.overlays_visibility_settings.selection_outline)
.on_update(|optional_input: &CheckboxInput| {
DocumentMessage::SetOverlaysVisibility {
visible: optional_input.checked,
overlays_type: Some(OverlaysType::SelectionOutline),
}
.into()
})
.for_label(checkbox_id.clone())
.widget_holder(),
TextLabel::new("Selection Outline".to_string()).for_checkbox(&mut checkbox_id).widget_holder(),
]
},
},
LayoutGroup::Row {
widgets: vec![TextLabel::new("Pen & Path Tools").widget_holder()],
},
LayoutGroup::Row {
widgets: vec![
CheckboxInput::new(self.overlays_visibility_settings.path)
.on_update(|optional_input: &CheckboxInput| {
DocumentMessage::SetOverlaysVisibility {
visible: optional_input.checked,
overlays_type: Some(OverlaysType::Path),
}
.into()
})
.widget_holder(),
TextLabel::new("Path".to_string()).widget_holder(),
],
widgets: {
let mut checkbox_id = CheckboxId::default();
vec![
CheckboxInput::new(self.overlays_visibility_settings.path)
.on_update(|optional_input: &CheckboxInput| {
DocumentMessage::SetOverlaysVisibility {
visible: optional_input.checked,
overlays_type: Some(OverlaysType::Path),
}
.into()
})
.for_label(checkbox_id.clone())
.widget_holder(),
TextLabel::new("Path".to_string()).for_checkbox(&mut checkbox_id).widget_holder(),
]
},
},
LayoutGroup::Row {
widgets: vec![
CheckboxInput::new(self.overlays_visibility_settings.anchors)
.on_update(|optional_input: &CheckboxInput| {
DocumentMessage::SetOverlaysVisibility {
visible: optional_input.checked,
overlays_type: Some(OverlaysType::Anchors),
}
.into()
})
.widget_holder(),
TextLabel::new("Anchors".to_string()).widget_holder(),
],
widgets: {
let mut checkbox_id = CheckboxId::default();
vec![
CheckboxInput::new(self.overlays_visibility_settings.anchors)
.on_update(|optional_input: &CheckboxInput| {
DocumentMessage::SetOverlaysVisibility {
visible: optional_input.checked,
overlays_type: Some(OverlaysType::Anchors),
}
.into()
})
.for_label(checkbox_id.clone())
.widget_holder(),
TextLabel::new("Anchors".to_string()).for_checkbox(&mut checkbox_id).widget_holder(),
]
},
},
LayoutGroup::Row {
widgets: vec![
CheckboxInput::new(self.overlays_visibility_settings.handles)
.disabled(!self.overlays_visibility_settings.anchors)
.on_update(|optional_input: &CheckboxInput| {
DocumentMessage::SetOverlaysVisibility {
visible: optional_input.checked,
overlays_type: Some(OverlaysType::Handles),
}
.into()
})
.widget_holder(),
TextLabel::new("Handles".to_string()).disabled(!self.overlays_visibility_settings.anchors).widget_holder(),
],
widgets: {
let mut checkbox_id = CheckboxId::default();
vec![
CheckboxInput::new(self.overlays_visibility_settings.handles)
.disabled(!self.overlays_visibility_settings.anchors)
.on_update(|optional_input: &CheckboxInput| {
DocumentMessage::SetOverlaysVisibility {
visible: optional_input.checked,
overlays_type: Some(OverlaysType::Handles),
}
.into()
})
.for_label(checkbox_id.clone())
.widget_holder(),
TextLabel::new("Handles".to_string())
.disabled(!self.overlays_visibility_settings.anchors)
.for_checkbox(&mut checkbox_id)
.widget_holder(),
]
},
},
])
.widget_holder(),
@ -2319,25 +2393,45 @@ impl DocumentMessageHandler {
]
.into_iter()
.chain(SNAP_FUNCTIONS_FOR_BOUNDING_BOXES.into_iter().map(|(name, closure, tooltip)| LayoutGroup::Row {
widgets: vec![
CheckboxInput::new(*closure(&mut snapping_state))
.on_update(move |input: &CheckboxInput| DocumentMessage::SetSnapping { closure: Some(closure), snapping_state: input.checked }.into())
.tooltip(tooltip)
.widget_holder(),
TextLabel::new(name).tooltip(tooltip).widget_holder(),
],
widgets: {
let mut checkbox_id = CheckboxId::default();
vec![
CheckboxInput::new(*closure(&mut snapping_state))
.on_update(move |input: &CheckboxInput| {
DocumentMessage::SetSnapping {
closure: Some(closure),
snapping_state: input.checked,
}
.into()
})
.tooltip(tooltip)
.for_label(checkbox_id.clone())
.widget_holder(),
TextLabel::new(name).tooltip(tooltip).for_checkbox(&mut checkbox_id).widget_holder(),
]
},
}))
.chain([LayoutGroup::Row {
widgets: vec![TextLabel::new(SnappingOptions::Paths.to_string()).widget_holder()],
}])
.chain(SNAP_FUNCTIONS_FOR_PATHS.into_iter().map(|(name, closure, tooltip)| LayoutGroup::Row {
widgets: vec![
CheckboxInput::new(*closure(&mut snapping_state2))
.on_update(move |input: &CheckboxInput| DocumentMessage::SetSnapping { closure: Some(closure), snapping_state: input.checked }.into())
.tooltip(tooltip)
.widget_holder(),
TextLabel::new(name).tooltip(tooltip).widget_holder(),
],
widgets: {
let mut checkbox_id = CheckboxId::default();
vec![
CheckboxInput::new(*closure(&mut snapping_state2))
.on_update(move |input: &CheckboxInput| {
DocumentMessage::SetSnapping {
closure: Some(closure),
snapping_state: input.checked,
}
.into()
})
.tooltip(tooltip)
.for_label(checkbox_id.clone())
.widget_holder(),
TextLabel::new(name).tooltip(tooltip).for_checkbox(&mut checkbox_id).widget_holder(),
]
},
}))
.collect(),
)
@ -2446,38 +2540,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()
@ -2511,12 +2614,14 @@ impl DocumentMessageHandler {
.selected_index(blend_mode.and_then(|blend_mode| blend_mode.index_in_list_svg_subset()).map(|index| index as u32))
.disabled(disabled)
.draw_icon(false)
.max_width(100)
.tooltip("Blend Mode")
.widget_holder(),
Separator::new(SeparatorType::Related).widget_holder(),
NumberInput::new(opacity)
.label("Opacity")
.unit("%")
.display_decimal_places(2)
.display_decimal_places(0)
.disabled(disabled)
.min(0.)
.max(100.)
@ -2531,33 +2636,35 @@ impl DocumentMessageHandler {
}
})
.on_commit(|_| DocumentMessage::AddTransaction.into())
.max_width(100)
.tooltip("Opacity")
.widget_holder(),
//
Separator::new(SeparatorType::Unrelated).widget_holder(),
//
IconButton::new("NewLayer", 24)
.tooltip("New Layer")
.tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::CreateEmptyFolder))
.on_update(|_| DocumentMessage::CreateEmptyFolder.into())
.widget_holder(),
IconButton::new("Folder", 24)
.tooltip("Group Selected")
.tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::GroupSelectedLayers))
.on_update(|_| {
let group_folder_type = GroupFolderType::Layer;
DocumentMessage::GroupSelectedLayers { group_folder_type }.into()
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
}
})
.disabled(!has_selection)
.on_commit(|_| DocumentMessage::AddTransaction.into())
.max_width(100)
.tooltip("Fill")
.widget_holder(),
IconButton::new("Trash", 24)
.tooltip("Delete Selected")
.tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::DeleteSelectedLayers))
.on_update(|_| DocumentMessage::DeleteSelectedLayers.into())
.disabled(!has_selection)
.widget_holder(),
//
Separator::new(SeparatorType::Unrelated).widget_holder(),
//
];
let layers_panel_control_bar_left = WidgetLayout::new(vec![LayoutGroup::Row { widgets }]);
let widgets = vec![
IconButton::new(if selection_all_locked { "PadlockLocked" } else { "PadlockUnlocked" }, 24)
.hover_icon(Some((if selection_all_locked { "PadlockUnlocked" } else { "PadlockLocked" }).into()))
.tooltip(if selection_all_locked { "Unlock Selected" } else { "Lock Selected" })
@ -2573,11 +2680,75 @@ impl DocumentMessageHandler {
.disabled(!has_selection)
.widget_holder(),
];
let layers_panel_control_bar = WidgetLayout::new(vec![LayoutGroup::Row { widgets }]);
let layers_panel_control_bar_right = WidgetLayout::new(vec![LayoutGroup::Row { widgets }]);
responses.add(LayoutMessage::SendLayout {
layout: Layout::WidgetLayout(layers_panel_control_bar),
layout_target: LayoutTarget::LayersPanelControlBar,
layout: Layout::WidgetLayout(layers_panel_control_bar_left),
layout_target: LayoutTarget::LayersPanelControlLeftBar,
});
responses.add(LayoutMessage::SendLayout {
layout: Layout::WidgetLayout(layers_panel_control_bar_right),
layout_target: LayoutTarget::LayersPanelControlRightBar,
});
}
pub fn update_layers_panel_bottom_bar_widgets(&self, responses: &mut VecDeque<Message>) {
let selected_nodes = self.network_interface.selected_nodes();
let mut selected_layers = selected_nodes.selected_layers(self.metadata());
let selected_layer = selected_layers.next();
let has_selection = selected_layer.is_some();
let has_multiple_selection = selected_layers.next().is_some();
let widgets = vec![
PopoverButton::new()
.icon(Some("Node".to_string()))
.menu_direction(Some(MenuDirection::Top))
.tooltip("Add an operation to the end of this layer's chain of nodes")
.disabled(!has_selection || has_multiple_selection)
.popover_layout({
let node_chooser = NodeCatalog::new()
.on_update(move |node_type| {
if let Some(layer) = selected_layer {
NodeGraphMessage::CreateNodeInLayerWithTransaction {
node_type: node_type.clone(),
layer: LayerNodeIdentifier::new_unchecked(layer.to_node()),
}
.into()
} else {
Message::NoOp
}
})
.widget_holder();
vec![LayoutGroup::Row { widgets: vec![node_chooser] }]
})
.widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
IconButton::new("Folder", 24)
.tooltip("Group Selected")
.tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::GroupSelectedLayers))
.on_update(|_| {
let group_folder_type = GroupFolderType::Layer;
DocumentMessage::GroupSelectedLayers { group_folder_type }.into()
})
.disabled(!has_selection)
.widget_holder(),
IconButton::new("NewLayer", 24)
.tooltip("New Layer")
.tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::CreateEmptyFolder))
.on_update(|_| DocumentMessage::CreateEmptyFolder.into())
.widget_holder(),
IconButton::new("Trash", 24)
.tooltip("Delete Selected")
.tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::DeleteSelectedLayers))
.on_update(|_| DocumentMessage::DeleteSelectedLayers.into())
.disabled(!has_selection)
.widget_holder(),
];
let layers_panel_bottom_bar = WidgetLayout::new(vec![LayoutGroup::Row { widgets }]);
responses.add(LayoutMessage::SendLayout {
layout: Layout::WidgetLayout(layers_panel_bottom_bar),
layout_target: LayoutTarget::LayersPanelBottomBar,
});
}
@ -2716,7 +2887,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
}
@ -2754,7 +2925,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()
}
@ -2795,7 +2973,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 {
@ -3112,6 +3290,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();
@ -3170,7 +3350,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
@ -3178,10 +3358,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)"
);
}
}

View file

@ -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::brush::brush_stroke::BrushStroke;
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::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,
},

View file

@ -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 {
@ -43,6 +44,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);
@ -53,6 +59,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);
@ -364,7 +376,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));
}
@ -378,18 +390,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,
})

View file

@ -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}",
);
}
}
}
}

View file

@ -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::brush::brush_stroke::BrushStroke;
use graphene_std::raster::BlendMode;
use graphene_std::raster_types::{CPU, RasterDataTable};
use graphene_std::text::{Font, TypesettingConfig};
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)),
@ -190,6 +190,7 @@ impl<'a> ModifyInputsContext<'a> {
Some(NodeInput::value(TaggedValue::F64(typesetting.character_spacing), false)),
Some(NodeInput::value(TaggedValue::OptionalF64(typesetting.max_width), false)),
Some(NodeInput::value(TaggedValue::OptionalF64(typesetting.max_height), false)),
Some(NodeInput::value(TaggedValue::F64(typesetting.tilt), false)),
]);
let text_id = NodeId::new();
@ -209,11 +210,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 +290,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 +334,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 +429,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 +466,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) {

View file

@ -0,0 +1,94 @@
use super::DocumentNodeDefinition;
use crate::messages::portfolio::document::utility_types::network_interface::{DocumentNodePersistentMetadata, InputMetadata, NodeTemplate, 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_metadata: fields
.iter()
.map(|f| match f.widget_override {
RegistryWidgetOverride::None => (f.name, f.description).into(),
RegistryWidgetOverride::Hidden => InputMetadata::with_name_description_override(f.name, f.description, WidgetOverride::Hidden),
RegistryWidgetOverride::String(str) => InputMetadata::with_name_description_override(f.name, f.description, WidgetOverride::String(str.to_string())),
RegistryWidgetOverride::Custom(str) => InputMetadata::with_name_description_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
}

View file

@ -33,6 +33,7 @@ pub enum NodeGraphMessage {
node_id: Option<NodeId>,
node_type: String,
xy: Option<(i32, i32)>,
add_transaction: bool,
},
CreateWire {
output_connector: OutputConnector,
@ -123,6 +124,9 @@ pub enum NodeGraphMessage {
},
SendClickTargets,
EndSendClickTargets,
UnloadWires,
SendWires,
UpdateVisibleNodes,
SendGraph,
SetGridAlignedEdges,
SetInputValue {

View file

@ -1,4 +1,4 @@
use super::utility_types::{BoxSelection, ContextMenuInformation, DragStart, FrontendGraphInput, FrontendGraphOutput, FrontendNode, FrontendNodeWire, WirePath};
use super::utility_types::{BoxSelection, ContextMenuInformation, DragStart, FrontendGraphInput, FrontendGraphOutput, FrontendNode};
use super::{document_node_definitions, node_properties};
use crate::consts::GRID_SIZE;
use crate::messages::input_mapper::utility_types::macros::action_keys;
@ -13,14 +13,17 @@ use crate::messages::portfolio::document::utility_types::network_interface::{
self, InputConnector, NodeNetworkInterface, NodeTemplate, NodeTypePersistentMetadata, OutputConnector, Previewing, TypeSource,
};
use crate::messages::portfolio::document::utility_types::nodes::{CollapsedLayers, LayerPanelEntry};
use crate::messages::portfolio::document::utility_types::wires::{GraphWireStyle, WirePath, WirePathUpdate, build_vector_wire};
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;
@ -65,6 +68,7 @@ pub struct NodeGraphMessageHandler {
select_if_not_dragged: Option<NodeId>,
/// The start of the dragged line (cannot be moved), stored in node graph coordinates
pub wire_in_progress_from_connector: Option<DVec2>,
wire_in_progress_type: FrontendGraphDataType,
/// The end point of the dragged line (cannot be moved), stored in node graph coordinates
pub wire_in_progress_to_connector: Option<DVec2>,
/// State for the context menu popups.
@ -75,12 +79,16 @@ pub struct NodeGraphMessageHandler {
auto_panning: AutoPanning,
/// The node to preview on mouse up if alt-clicked
preview_on_mouse_up: Option<NodeId>,
// The index of the import that is being moved
/// The index of the import that is being moved
reordering_import: Option<usize>,
// The index of the export that is being moved
/// The index of the export that is being moved
reordering_export: Option<usize>,
// The end index of the moved port
/// The end index of the moved port
end_index: Option<usize>,
/// Used to keep track of what nodes are sent to the front end so that only visible ones are sent to the frontend
frontend_nodes: Vec<NodeId>,
/// Used to keep track of what wires are sent to the front end so the old ones can be removed
frontend_wires: HashSet<(NodeId, usize)>,
}
/// NodeGraphMessageHandler always modifies the network which the selected nodes are in. No GraphOperationMessages should be added here, since those messages will always affect the document network.
@ -174,7 +182,12 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
responses.add(PropertiesPanelMessage::Refresh);
responses.add(NodeGraphMessage::RunDocumentGraph);
}
NodeGraphMessage::CreateNodeFromContextMenu { node_id, node_type, xy } => {
NodeGraphMessage::CreateNodeFromContextMenu {
node_id,
node_type,
xy,
add_transaction,
} => {
let (x, y) = if let Some((x, y)) = xy {
(x, y)
} else if let Some(node_graph_ptz) = network_interface.node_graph_ptz(breadcrumb_network_path) {
@ -196,7 +209,10 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
let node_template = document_node_type.default_node_template();
self.context_menu = None;
responses.add(DocumentMessage::AddTransaction);
if add_transaction {
responses.add(DocumentMessage::AddTransaction);
}
responses.add(NodeGraphMessage::InsertNode {
node_id,
node_template: node_template.clone(),
@ -219,13 +235,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
};
// Ensure connection is to correct input of new node. If it does not have an input then do not connect
if let Some((input_index, _)) = node_template
.document_node
.inputs
.iter()
.enumerate()
.find(|(_, input)| input.is_exposed_to_frontend(selection_network_path.is_empty()))
{
if let Some((input_index, _)) = node_template.document_node.inputs.iter().enumerate().find(|(_, input)| input.is_exposed()) {
responses.add(NodeGraphMessage::CreateWire {
output_connector: *output_connector,
input_connector: InputConnector::node(node_id, input_index),
@ -235,6 +245,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
}
self.wire_in_progress_from_connector = None;
self.wire_in_progress_type = FrontendGraphDataType::General;
self.wire_in_progress_to_connector = None;
}
responses.add(FrontendMessage::UpdateWirePathInProgress { wire_path: None });
@ -366,9 +377,14 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
responses.add(DocumentMessage::CommitTransaction);
// Update the graph UI and re-render
responses.add(PropertiesPanelMessage::Refresh);
responses.add(NodeGraphMessage::SendGraph);
responses.add(NodeGraphMessage::RunDocumentGraph);
if graph_view_overlay_open {
responses.add(PropertiesPanelMessage::Refresh);
responses.add(NodeGraphMessage::SendGraph);
} else {
responses.add(DocumentMessage::GraphViewOverlay { open: true });
responses.add(NavigationMessage::FitViewportToSelection);
responses.add(DocumentMessage::ZoomCanvasTo100Percent);
}
}
NodeGraphMessage::InsertNode { node_id, node_template } => {
network_interface.insert_node(node_id, node_template, selection_network_path);
@ -567,10 +583,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,
@ -626,6 +644,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
// Abort dragging a wire
if self.wire_in_progress_from_connector.is_some() {
self.wire_in_progress_from_connector = None;
self.wire_in_progress_type = FrontendGraphDataType::General;
self.wire_in_progress_to_connector = None;
responses.add(DocumentMessage::AbortTransaction);
responses.add(FrontendMessage::UpdateWirePathInProgress { wire_path: None });
@ -704,6 +723,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
if self.context_menu.is_some() {
self.context_menu = None;
self.wire_in_progress_from_connector = None;
self.wire_in_progress_type = FrontendGraphDataType::General;
self.wire_in_progress_to_connector = None;
responses.add(FrontendMessage::UpdateContextMenuInformation {
context_menu_information: self.context_menu.clone(),
@ -737,6 +757,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
};
let Some(output_connector) = output_connector else { return };
self.wire_in_progress_from_connector = network_interface.output_position(&output_connector, selection_network_path);
self.wire_in_progress_type = FrontendGraphDataType::from_type(&network_interface.input_type(clicked_input, breadcrumb_network_path).0);
return;
}
@ -746,6 +767,15 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
self.initial_disconnecting = false;
self.wire_in_progress_from_connector = network_interface.output_position(&clicked_output, selection_network_path);
if let Some((output_type, source)) = clicked_output
.node_id()
.map(|node_id| network_interface.output_type(&node_id, clicked_output.index(), breadcrumb_network_path))
{
self.wire_in_progress_type = FrontendGraphDataType::displayed_type(&output_type, &source);
} else {
self.wire_in_progress_type = FrontendGraphDataType::General;
}
self.update_node_graph_hints(responses);
return;
}
@ -892,9 +922,18 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
false
}
});
let vector_wire = build_vector_wire(
wire_in_progress_from_connector,
wire_in_progress_to_connector,
from_connector_is_layer,
to_connector_is_layer,
GraphWireStyle::Direct,
);
let mut path_string = String::new();
let _ = vector_wire.subpath_to_svg(&mut path_string, DAffine2::IDENTITY);
let wire_path = WirePath {
path_string: Self::build_wire_path_string(wire_in_progress_from_connector, wire_in_progress_to_connector, from_connector_is_layer, to_connector_is_layer),
data_type: FrontendGraphDataType::General,
path_string,
data_type: self.wire_in_progress_type,
thick: false,
dashed: false,
};
@ -938,7 +977,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
self.update_node_graph_hints(responses);
} else if self.reordering_import.is_some() {
let Some(modify_import_export) = network_interface.modify_import_export(selection_network_path) else {
log::error!("Could not get modify import export in PointerUp");
log::error!("Could not get modify import export in PointerMove");
return;
};
// Find the first import that is below the mouse position
@ -958,7 +997,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
responses.add(FrontendMessage::UpdateImportReorderIndex { index: self.end_index });
} else if self.reordering_export.is_some() {
let Some(modify_import_export) = network_interface.modify_import_export(selection_network_path) else {
log::error!("Could not get modify import export in PointerUp");
log::error!("Could not get modify import export in PointerMove");
return;
};
// Find the first export that is below the mouse position
@ -996,11 +1035,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
@ -1038,15 +1079,13 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
// Get the compatible type from the output connector
let compatible_type = output_connector.and_then(|output_connector| {
output_connector.node_id().and_then(|node_id| {
let output_index = output_connector.index();
// Get the output types from the network interface
let output_types = network_interface.output_types(&node_id, selection_network_path);
let (output_type, type_source) = network_interface.output_type(&node_id, output_connector.index(), selection_network_path);
// Extract the type if available
output_types.get(output_index).and_then(|type_option| type_option.as_ref()).map(|(output_type, _)| {
// Create a search term based on the type
format!("type:{}", output_type.clone().nested_type())
})
match type_source {
TypeSource::RandomProtonodeImplementation | TypeSource::Error(_) => None,
_ => Some(format!("type:{}", output_type.nested_type())),
}
})
});
let appear_right_of_mouse = if ipp.mouse.position.x > ipp.viewport_bounds.size().x - 173. { -173. } else { 0. };
@ -1112,112 +1151,56 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
let has_primary_output_connection = network_interface
.outward_wires(selection_network_path)
.is_some_and(|outward_wires| outward_wires.get(&OutputConnector::node(selected_node_id, 0)).is_some_and(|outward_wires| !outward_wires.is_empty()));
let Some(network) = network_interface.nested_network(selection_network_path) else {
return;
};
if let Some(selected_node) = network.nodes.get(&selected_node_id) {
// Check if any downstream node has any input that feeds into the primary export of the selected node
let primary_input_is_value = selected_node.inputs.first().is_some_and(|first_input| first_input.as_value().is_some());
// Check that neither the primary input or output of the selected node are already connected.
if !has_primary_output_connection && primary_input_is_value {
if !has_primary_output_connection {
let Some(network) = network_interface.nested_network(selection_network_path) else {
return;
};
let Some(selected_node) = network.nodes.get(&selected_node_id) else {
return;
};
// Check that the first visible input is disconnected
let selected_node_input_connect_index = selected_node
.inputs
.iter()
.enumerate()
.find(|input| input.1.is_exposed())
.filter(|input| input.1.as_value().is_some())
.map(|input| input.0);
if let Some(selected_node_input_connect_index) = selected_node_input_connect_index {
let Some(bounding_box) = network_interface.node_bounding_box(&selected_node_id, selection_network_path) else {
log::error!("Could not get bounding box for node: {selected_node_id}");
return;
};
// TODO: Cache all wire locations if this is a performance issue
let overlapping_wires = Self::collect_wires(network_interface, selection_network_path)
.into_iter()
.filter(|frontend_wire| {
// Prevent inserting on a link that is connected upstream to the selected node
if network_interface
.upstream_flow_back_from_nodes(vec![selected_node_id], selection_network_path, network_interface::FlowType::UpstreamFlow)
.any(|upstream_id| {
frontend_wire.wire_end.node_id().is_some_and(|wire_end_id| wire_end_id == upstream_id)
|| frontend_wire.wire_start.node_id().is_some_and(|wire_start_id| wire_start_id == upstream_id)
}) {
return false;
}
let mut wires_to_check = network_interface.node_graph_input_connectors(selection_network_path).into_iter().collect::<HashSet<_>>();
// Prevent inserting on a link that is connected upstream to the selected node
for upstream_node in network_interface.upstream_flow_back_from_nodes(vec![selected_node_id], selection_network_path, network_interface::FlowType::UpstreamFlow) {
for input_index in 0..network_interface.number_of_inputs(&upstream_node, selection_network_path) {
wires_to_check.remove(&InputConnector::node(upstream_node, input_index));
}
}
let overlapping_wires = wires_to_check
.into_iter()
.filter_map(|input| {
// Prevent inserting a layer into a chain
if network_interface.is_layer(&selected_node_id, selection_network_path)
&& frontend_wire
.wire_start
.node_id()
.is_some_and(|wire_start_id| network_interface.is_chain(&wire_start_id, selection_network_path))
&& input.node_id().is_some_and(|input_node_id| network_interface.is_chain(&input_node_id, selection_network_path))
{
return false;
return None;
}
let Some(input_position) = network_interface.input_position(&frontend_wire.wire_end, selection_network_path) else {
log::error!("Could not get input port position for {:?}", frontend_wire.wire_end);
return false;
};
let Some(output_position) = network_interface.output_position(&frontend_wire.wire_start, selection_network_path) else {
log::error!("Could not get output port position for {:?}", frontend_wire.wire_start);
return false;
};
let start_node_is_layer = frontend_wire
.wire_end
.node_id()
.is_some_and(|wire_start_id| network_interface.is_layer(&wire_start_id, selection_network_path));
let end_node_is_layer = frontend_wire
.wire_end
.node_id()
.is_some_and(|wire_end_id| network_interface.is_layer(&wire_end_id, selection_network_path));
let locations = Self::build_wire_path_locations(output_position, input_position, start_node_is_layer, end_node_is_layer);
let bezier = bezier_rs::Bezier::from_cubic_dvec2(
(locations[0].x, locations[0].y).into(),
(locations[1].x, locations[1].y).into(),
(locations[2].x, locations[2].y).into(),
(locations[3].x, locations[3].y).into(),
);
!bezier.rectangle_intersections(bounding_box[0], bounding_box[1]).is_empty() || bezier.is_contained_within(bounding_box[0], bounding_box[1])
})
.collect::<Vec<_>>()
.into_iter()
.filter_map(|mut wire| {
if let Some(end_node_id) = wire.wire_end.node_id() {
let Some(actual_index_from_exposed) = (0..network_interface.number_of_inputs(&end_node_id, selection_network_path))
.filter(|&input_index| {
network_interface
.input_from_connector(&InputConnector::Node { node_id: end_node_id, input_index }, selection_network_path)
.is_some_and(|input| input.is_exposed_to_frontend(selection_network_path.is_empty()))
})
.nth(wire.wire_end.input_index())
else {
log::error!("Could not get exposed input index for {:?}", wire.wire_end);
return None;
};
wire.wire_end = InputConnector::Node {
node_id: end_node_id,
input_index: actual_index_from_exposed,
};
}
Some(wire)
let (wire, is_stack) = network_interface.vector_wire_from_input(&input, preferences.graph_wire_style, selection_network_path)?;
wire.rectangle_intersections_exist(bounding_box[0], bounding_box[1]).then_some((input, is_stack))
})
.collect::<Vec<_>>();
let is_stack_wire = |wire: &FrontendNodeWire| match (wire.wire_start.node_id(), wire.wire_end.node_id(), wire.wire_end.input_index()) {
(Some(start_id), Some(end_id), input_index) => {
input_index == 0 && network_interface.is_layer(&start_id, selection_network_path) && network_interface.is_layer(&end_id, selection_network_path)
}
_ => false,
};
// Prioritize vertical thick lines and cancel if there are multiple potential wires
let mut node_wires = Vec::new();
let mut stack_wires = Vec::new();
for wire in overlapping_wires {
if is_stack_wire(&wire) { stack_wires.push(wire) } else { node_wires.push(wire) }
}
// Auto convert node to layer when inserting on a single stack wire
if stack_wires.len() == 1 && node_wires.is_empty() {
network_interface.set_to_node_or_layer(&selected_node_id, selection_network_path, true)
for (overlapping_wire_input, is_stack) in overlapping_wires {
if is_stack {
stack_wires.push(overlapping_wire_input)
} else {
node_wires.push(overlapping_wire_input)
}
}
let overlapping_wire = if network_interface.is_layer(&selected_node_id, selection_network_path) {
@ -1234,29 +1217,13 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
None
};
if let Some(overlapping_wire) = overlapping_wire {
let Some(network) = network_interface.nested_network(selection_network_path) else {
return;
};
// Ensure connection is to first visible input of selected node. If it does not have an input then do not connect
if let Some((selected_node_input_index, _)) = network
.nodes
.get(&selected_node_id)
.unwrap()
.inputs
.iter()
.enumerate()
.find(|(_, input)| input.is_exposed_to_frontend(selection_network_path.is_empty()))
{
responses.add(NodeGraphMessage::InsertNodeBetween {
node_id: selected_node_id,
input_connector: overlapping_wire.wire_end,
insert_node_input_index: selected_node_input_index,
});
responses.add(NodeGraphMessage::RunDocumentGraph);
responses.add(NodeGraphMessage::SendGraph);
}
responses.add(NodeGraphMessage::InsertNodeBetween {
node_id: selected_node_id,
input_connector: *overlapping_wire,
insert_node_input_index: selected_node_input_connect_index,
});
responses.add(NodeGraphMessage::RunDocumentGraph);
responses.add(NodeGraphMessage::SendGraph);
}
}
}
@ -1283,6 +1250,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
self.begin_dragging = false;
self.box_selection_start = None;
self.wire_in_progress_from_connector = None;
self.wire_in_progress_type = FrontendGraphDataType::General;
self.wire_in_progress_to_connector = None;
self.reordering_export = None;
self.reordering_import = None;
@ -1357,23 +1325,52 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
click_targets: Some(network_interface.collect_frontend_click_targets(breadcrumb_network_path)),
}),
NodeGraphMessage::EndSendClickTargets => responses.add(FrontendMessage::UpdateClickTargets { click_targets: None }),
NodeGraphMessage::UnloadWires => {
for input in network_interface.node_graph_input_connectors(breadcrumb_network_path) {
network_interface.unload_wire(&input, breadcrumb_network_path);
}
responses.add(FrontendMessage::ClearAllNodeGraphWires);
}
NodeGraphMessage::SendWires => {
let wires = self.collect_wires(network_interface, preferences.graph_wire_style, breadcrumb_network_path);
responses.add(FrontendMessage::UpdateNodeGraphWires { wires });
}
NodeGraphMessage::UpdateVisibleNodes => {
let Some(network_metadata) = network_interface.network_metadata(breadcrumb_network_path) else {
return;
};
let viewport_bbox = ipp.document_bounds();
let document_bbox: [DVec2; 2] = viewport_bbox.map(|p| network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport.inverse().transform_point2(p));
let mut nodes = Vec::new();
for node_id in &self.frontend_nodes {
let Some(node_bbox) = network_interface.node_bounding_box(node_id, breadcrumb_network_path) else {
log::error!("Could not get bbox for node: {:?}", node_id);
continue;
};
if node_bbox[1].x >= document_bbox[0].x && node_bbox[0].x <= document_bbox[1].x && node_bbox[1].y >= document_bbox[0].y && node_bbox[0].y <= document_bbox[1].y {
nodes.push(*node_id);
}
}
responses.add(FrontendMessage::UpdateVisibleNodes { nodes });
}
NodeGraphMessage::SendGraph => {
responses.add(NodeGraphMessage::UpdateLayerPanel);
responses.add(DocumentMessage::DocumentStructureChanged);
responses.add(PropertiesPanelMessage::Refresh);
if breadcrumb_network_path == selection_network_path && graph_view_overlay_open {
// TODO: Implement culling of nodes and wires whose bounding boxes are outside of the viewport
let wires = Self::collect_wires(network_interface, breadcrumb_network_path);
let nodes = self.collect_nodes(network_interface, breadcrumb_network_path);
self.frontend_nodes = nodes.iter().map(|node| node.id).collect();
responses.add(FrontendMessage::UpdateNodeGraphNodes { nodes });
responses.add(NodeGraphMessage::UpdateVisibleNodes);
let (layer_widths, chain_widths, has_left_input_wire) = network_interface.collect_layer_widths(breadcrumb_network_path);
let wires_direct_not_grid_aligned = preferences.graph_wire_style.is_direct();
responses.add(NodeGraphMessage::UpdateImportsExports);
responses.add(FrontendMessage::UpdateNodeGraph {
nodes,
wires,
wires_direct_not_grid_aligned,
});
responses.add(FrontendMessage::UpdateLayerWidths {
layer_widths,
chain_widths,
@ -1397,12 +1394,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);
}
}
@ -1460,6 +1452,8 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
Ordering::Equal => {}
}
}
responses.add(NodeGraphMessage::SendWires);
}
NodeGraphMessage::ToggleSelectedAsLayersOrNodes => {
let Some(selected_nodes) = network_interface.selected_nodes_in_nested_network(selection_network_path) else {
@ -1479,6 +1473,8 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
}
NodeGraphMessage::ShiftNodePosition { node_id, x, y } => {
network_interface.shift_absolute_node_position(&node_id, IVec2::new(x, y), selection_network_path);
responses.add(NodeGraphMessage::SendWires);
}
NodeGraphMessage::SetToNodeOrLayer { node_id, is_layer } => {
if is_layer && !network_interface.is_eligible_to_be_layer(&node_id, selection_network_path) {
@ -1492,6 +1488,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
});
responses.add(NodeGraphMessage::RunDocumentGraph);
responses.add(NodeGraphMessage::SendGraph);
responses.add(NodeGraphMessage::SendWires);
}
NodeGraphMessage::SetDisplayName {
node_id,
@ -1628,7 +1625,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
// }
let Some(network_metadata) = network_interface.network_metadata(selection_network_path) else {
log::error!("Could not get network metadata in PointerMove");
log::error!("Could not get network metadata in UpdateBoxSelection");
return;
};
@ -1694,7 +1691,8 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
)
.into_iter()
.next();
responses.add(NodeGraphMessage::UpdateVisibleNodes);
responses.add(NodeGraphMessage::SendWires);
responses.add(FrontendMessage::UpdateImportsExports {
imports,
exports,
@ -1840,6 +1838,7 @@ impl NodeGraphMessageHandler {
node_id: Some(node_id),
node_type: node_type.clone(),
xy: None,
add_transaction: true,
}
.into(),
NodeGraphMessage::SelectedNodesSet { nodes: vec![node_id] }.into(),
@ -1852,11 +1851,6 @@ impl NodeGraphMessageHandler {
//
Separator::new(SeparatorType::Unrelated).widget_holder(),
//
IconButton::new("NewLayer", 24)
.tooltip("New Layer")
.tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::CreateEmptyFolder))
.on_update(|_| DocumentMessage::CreateEmptyFolder.into())
.widget_holder(),
IconButton::new("Folder", 24)
.tooltip("Group Selected")
.tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::GroupSelectedLayers))
@ -1866,6 +1860,11 @@ impl NodeGraphMessageHandler {
})
.disabled(!has_selection)
.widget_holder(),
IconButton::new("NewLayer", 24)
.tooltip("New Layer")
.tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::CreateEmptyFolder))
.on_update(|_| DocumentMessage::CreateEmptyFolder.into())
.widget_holder(),
IconButton::new("Trash", 24)
.tooltip("Delete Selected")
.tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::DeleteSelectedLayers))
@ -2010,12 +2009,38 @@ impl NodeGraphMessageHandler {
}
// Next, we decide what to display based on the number of layers and nodes selected
match layers.len() {
match *layers.as_slice() {
// If no layers are selected, show properties for all selected nodes
0 => {
[] => {
let selected_nodes = nodes.iter().map(|node_id| node_properties::generate_node_properties(*node_id, context)).collect::<Vec<_>>();
if !selected_nodes.is_empty() {
return selected_nodes;
let mut properties = Vec::new();
if let [node_id] = *nodes.as_slice() {
properties.push(LayoutGroup::Row {
widgets: vec![
Separator::new(SeparatorType::Related).widget_holder(),
IconLabel::new("Node").tooltip("Name of the selected node").widget_holder(),
Separator::new(SeparatorType::Related).widget_holder(),
TextInput::new(context.network_interface.display_name(&node_id, context.selection_network_path))
.tooltip("Name of the selected node")
.on_update(move |text_input| {
NodeGraphMessage::SetDisplayName {
node_id,
alias: text_input.value.clone(),
skip_adding_history_step: false,
}
.into()
})
.widget_holder(),
Separator::new(SeparatorType::Related).widget_holder(),
],
});
}
properties.extend(selected_nodes);
return properties;
}
// TODO: Display properties for encapsulating node when no nodes are selected in a nested network
@ -2057,8 +2082,7 @@ impl NodeGraphMessageHandler {
properties
}
// If one layer is selected, filter out all selected nodes that are not upstream of it. If there are no nodes left, show properties for the layer. Otherwise, show nothing.
1 => {
let layer = layers[0];
[layer] => {
let nodes_not_upstream_of_layer = nodes.into_iter().filter(|&selected_node_id| {
!context
.network_interface
@ -2131,69 +2155,39 @@ impl NodeGraphMessageHandler {
}
}
fn collect_wires(network_interface: &NodeNetworkInterface, breadcrumb_network_path: &[NodeId]) -> Vec<FrontendNodeWire> {
let Some(network) = network_interface.nested_network(breadcrumb_network_path) else {
log::error!("Could not get network when collecting wires");
return Vec::new();
};
let mut wires = network
.nodes
fn collect_wires(&mut self, network_interface: &mut NodeNetworkInterface, graph_wire_style: GraphWireStyle, breadcrumb_network_path: &[NodeId]) -> Vec<WirePathUpdate> {
let mut added_wires = network_interface
.node_graph_input_connectors(breadcrumb_network_path)
.iter()
.flat_map(|(wire_end, node)| node.inputs.iter().filter(|input| input.is_exposed()).enumerate().map(move |(index, input)| (input, wire_end, index)))
.filter_map(|(input, &wire_end, wire_end_input_index)| {
match *input {
NodeInput::Node {
node_id: wire_start,
output_index: wire_start_output_index,
// TODO: add ui for lambdas
lambda: _,
} => Some(FrontendNodeWire {
wire_start: OutputConnector::node(wire_start, wire_start_output_index),
wire_end: InputConnector::node(wire_end, wire_end_input_index),
dashed: false,
}),
NodeInput::Network { import_index, .. } => Some(FrontendNodeWire {
wire_start: OutputConnector::Import(import_index),
wire_end: InputConnector::node(wire_end, wire_end_input_index),
dashed: false,
}),
_ => None,
}
})
.filter_map(|connector| network_interface.newly_loaded_input_wire(connector, graph_wire_style, breadcrumb_network_path))
.collect::<Vec<_>>();
// Connect primary export to root node, since previewing a node will change the primary export
if let Some(root_node) = network_interface.root_node(breadcrumb_network_path) {
wires.push(FrontendNodeWire {
wire_start: OutputConnector::node(root_node.node_id, root_node.output_index),
wire_end: InputConnector::Export(0),
dashed: false,
});
let changed_wire_inputs = added_wires.iter().map(|update| (update.id, update.input_index)).collect::<Vec<_>>();
self.frontend_wires.extend(changed_wire_inputs);
let mut orphaned_wire_inputs = self.frontend_wires.clone();
self.frontend_wires = network_interface
.node_graph_wire_inputs(breadcrumb_network_path)
.iter()
.filter_map(|visible_wire_input| orphaned_wire_inputs.take(visible_wire_input))
.collect::<HashSet<_>>();
added_wires.extend(orphaned_wire_inputs.into_iter().map(|(id, input_index)| WirePathUpdate {
id,
input_index,
wire_path_update: None,
}));
if let Some(wire_to_root) = network_interface.wire_to_root(graph_wire_style, breadcrumb_network_path) {
added_wires.push(wire_to_root);
} else {
added_wires.push(WirePathUpdate {
id: NodeId(u64::MAX),
input_index: usize::MAX,
wire_path_update: None,
})
}
// Connect rest of exports to their actual export field since they are not affected by previewing. Only connect the primary export if it is dashed
for (i, export) in network.exports.iter().enumerate() {
let dashed = matches!(network_interface.previewing(breadcrumb_network_path), Previewing::Yes { .. }) && i == 0;
if dashed || i != 0 {
if let NodeInput::Node { node_id, output_index, .. } = export {
wires.push(FrontendNodeWire {
wire_start: OutputConnector::Node {
node_id: *node_id,
output_index: *output_index,
},
wire_end: InputConnector::Export(i),
dashed,
});
} else if let NodeInput::Network { import_index, .. } = *export {
wires.push(FrontendNodeWire {
wire_start: OutputConnector::Import(import_index),
wire_end: InputConnector::Export(i),
dashed,
})
}
}
}
wires
added_wires
}
fn collect_nodes(&self, network_interface: &mut NodeNetworkInterface, breadcrumb_network_path: &[NodeId]) -> Vec<FrontendNode> {
@ -2217,6 +2211,7 @@ impl NodeGraphMessageHandler {
log::error!("Could not get position for node {node_id}");
}
}
let mut frontend_inputs_lookup = frontend_inputs_lookup(breadcrumb_network_path, network_interface);
let Some(network) = network_interface.nested_network(breadcrumb_network_path) else {
log::error!("Could not get nested network when collecting nodes");
@ -2232,13 +2227,14 @@ impl NodeGraphMessageHandler {
let node_id_path = [breadcrumb_network_path, (&[node_id])].concat();
let inputs = frontend_inputs_lookup.remove(&node_id).unwrap_or_default();
let mut inputs = inputs.into_iter().map(|input| {
input.map(|input| FrontendGraphInput {
data_type: FrontendGraphDataType::displayed_type(&input.ty, &input.type_source),
resolved_type: Some(format!("{:?}", &input.ty)),
resolved_type: format!("{:?}", &input.ty),
valid_types: input.valid_types.iter().map(|ty| ty.to_string()).collect(),
name: input.input_name.unwrap_or_else(|| input.ty.nested_type().to_string()),
description: input.input_description.unwrap_or_default(),
name: input.input_name,
description: input.input_description,
connected_to: input.output_connector,
})
});
@ -2246,20 +2242,16 @@ impl NodeGraphMessageHandler {
let primary_input = inputs.next().flatten();
let exposed_inputs = inputs.flatten().collect();
let output_types = network_interface.output_types(&node_id, breadcrumb_network_path);
let primary_output_type = output_types.first().cloned().flatten();
let frontend_data_type = if let Some((output_type, type_source)) = &primary_output_type {
FrontendGraphDataType::displayed_type(output_type, type_source)
} else {
FrontendGraphDataType::General
};
let (output_type, type_source) = network_interface.output_type(&node_id, 0, breadcrumb_network_path);
let frontend_data_type = FrontendGraphDataType::displayed_type(&output_type, &type_source);
let connected_to = outward_wires.get(&OutputConnector::node(node_id, 0)).cloned().unwrap_or_default();
let primary_output = if network_interface.has_primary_output(&node_id, breadcrumb_network_path) && !output_types.is_empty() {
let primary_output = if network_interface.has_primary_output(&node_id, breadcrumb_network_path) {
Some(FrontendGraphOutput {
data_type: frontend_data_type,
name: "Output 1".to_string(),
description: String::new(),
resolved_type: primary_output_type.map(|(input, _)| format!("{input:?}")),
resolved_type: format!("{:?}", output_type),
connected_to,
})
} else {
@ -2267,15 +2259,13 @@ impl NodeGraphMessageHandler {
};
let mut exposed_outputs = Vec::new();
for (index, exposed_output) in output_types.iter().enumerate() {
if index == 0 && network_interface.has_primary_output(&node_id, breadcrumb_network_path) {
for output_index in 0..network_interface.number_of_outputs(&node_id, breadcrumb_network_path) {
if output_index == 0 && network_interface.has_primary_output(&node_id, breadcrumb_network_path) {
continue;
}
let frontend_data_type = if let Some((output_type, type_source)) = &exposed_output {
FrontendGraphDataType::displayed_type(output_type, type_source)
} else {
FrontendGraphDataType::General
};
let (output_type, type_source) = network_interface.output_type(&node_id, 0, breadcrumb_network_path);
let data_type = FrontendGraphDataType::displayed_type(&output_type, &type_source);
let Some(node_metadata) = network_metadata.persistent_metadata.node_metadata.get(&node_id) else {
log::error!("Could not get node_metadata when getting output for {node_id}");
continue;
@ -2283,17 +2273,17 @@ impl NodeGraphMessageHandler {
let output_name = node_metadata
.persistent_metadata
.output_names
.get(index)
.map(|output_name| output_name.to_string())
.get(output_index)
.cloned()
.filter(|output_name| !output_name.is_empty())
.unwrap_or_else(|| exposed_output.clone().map(|(output_type, _)| output_type.nested_type().to_string()).unwrap_or_default());
.unwrap_or_else(|| output_type.nested_type().to_string());
let connected_to = outward_wires.get(&OutputConnector::node(node_id, index)).cloned().unwrap_or_default();
let connected_to = outward_wires.get(&OutputConnector::node(node_id, output_index)).cloned().unwrap_or_default();
exposed_outputs.push(FrontendGraphOutput {
data_type: frontend_data_type,
data_type,
name: output_name,
description: String::new(),
resolved_type: exposed_output.clone().map(|(input, _)| format!("{input:?}")),
resolved_type: format!("{:?}", output_type),
connected_to,
});
}
@ -2396,9 +2386,9 @@ impl NodeGraphMessageHandler {
network_interface.upstream_flow_back_from_nodes(vec![node_id], &[], network_interface::FlowType::HorizontalFlow).last().is_some_and(|node_id|
network_interface.document_node(&node_id, &[]).map_or_else(||{log::error!("Could not get node {node_id} in update_layer_panel"); false}, |node| {
if network_interface.is_layer(&node_id, &[]) {
node.inputs.iter().filter(|input| input.is_exposed_to_frontend(true)).nth(1).is_some_and(|input| input.as_value().is_some())
node.inputs.iter().filter(|input| input.is_exposed()).nth(1).is_some_and(|input| input.as_value().is_some())
} else {
node.inputs.iter().filter(|input| input.is_exposed_to_frontend(true)).nth(0).is_some_and(|input| input.as_value().is_some())
node.inputs.iter().filter(|input| input.is_exposed()).nth(0).is_some_and(|input| input.as_value().is_some())
}
}))
);
@ -2419,6 +2409,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, &[]),
@ -2438,72 +2429,14 @@ 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 });
}
}
}
fn build_wire_path_string(output_position: DVec2, input_position: DVec2, vertical_out: bool, vertical_in: bool) -> String {
let locations = Self::build_wire_path_locations(output_position, input_position, vertical_out, vertical_in);
let smoothing = 0.5;
let delta01 = DVec2::new((locations[1].x - locations[0].x) * smoothing, (locations[1].y - locations[0].y) * smoothing);
let delta23 = DVec2::new((locations[3].x - locations[2].x) * smoothing, (locations[3].y - locations[2].y) * smoothing);
format!(
"M{},{} L{},{} C{},{} {},{} {},{} L{},{}",
locations[0].x,
locations[0].y,
locations[1].x,
locations[1].y,
locations[1].x + delta01.x,
locations[1].y + delta01.y,
locations[2].x - delta23.x,
locations[2].y - delta23.y,
locations[2].x,
locations[2].y,
locations[3].x,
locations[3].y
)
}
fn build_wire_path_locations(output_position: DVec2, input_position: DVec2, vertical_out: bool, vertical_in: bool) -> Vec<DVec2> {
let horizontal_gap = (output_position.x - input_position.x).abs();
let vertical_gap = (output_position.y - input_position.y).abs();
// TODO: Finish this commented out code replacement for the code below it based on this diagram: <https://files.keavon.com/-/SuperbWideFoxterrier/capture.png>
// // Straight: stacking lines which are always straight, or a straight horizontal wire between two aligned nodes
// if ((verticalOut && vertical_in) || (!verticalOut && !vertical_in && vertical_gap === 0)) {
// return [
// { x: output_position.x, y: output_position.y },
// { x: input_position.x, y: input_position.y },
// ];
// }
// // L-shape bend
// if (verticalOut !== vertical_in) {
// }
let curve_length = 24.;
let curve_falloff_rate = curve_length * std::f64::consts::PI * 2.;
let horizontal_curve_amount = -(2_f64.powf((-10. * horizontal_gap) / curve_falloff_rate)) + 1.;
let vertical_curve_amount = -(2_f64.powf((-10. * vertical_gap) / curve_falloff_rate)) + 1.;
let horizontal_curve = horizontal_curve_amount * curve_length;
let vertical_curve = vertical_curve_amount * curve_length;
vec![
output_position,
DVec2::new(
if vertical_out { output_position.x } else { output_position.x + horizontal_curve },
if vertical_out { output_position.y - vertical_curve } else { output_position.y },
),
DVec2::new(
if vertical_in { input_position.x } else { input_position.x - horizontal_curve },
if vertical_in { input_position.y + vertical_curve } else { input_position.y },
),
DVec2::new(input_position.x, input_position.y),
]
}
pub fn update_node_graph_hints(&self, responses: &mut VecDeque<Message>) {
// A wire is in progress and its start and end connectors are set
let wiring = self.wire_in_progress_from_connector.is_some();
@ -2547,8 +2480,8 @@ impl NodeGraphMessageHandler {
#[derive(Default)]
struct InputLookup {
input_name: Option<String>,
input_description: Option<String>,
input_name: String,
input_description: String,
ty: Type,
type_source: TypeSource,
valid_types: Vec<Type>,
@ -2563,34 +2496,31 @@ fn frontend_inputs_lookup(breadcrumb_network_path: &[NodeId], network_interface:
return Default::default();
};
let mut frontend_inputs_lookup = HashMap::new();
for (&node_id, node) in network.nodes.iter() {
let mut inputs = Vec::with_capacity(node.inputs.len());
for (index, input) in node.inputs.iter().enumerate() {
let is_exposed = input.is_exposed_to_frontend(breadcrumb_network_path.is_empty());
// Skip not exposed inputs (they still get an entry to help with finding the primary input)
if !is_exposed {
inputs.push(None);
continue;
}
for (node_id, index, output_connector, is_exposed) in network
.nodes
.iter()
.flat_map(|(node_id, node)| {
node.inputs
.iter()
.enumerate()
.map(|(index, input)| (*node_id, index, OutputConnector::from_input(input), input.is_exposed()))
})
.collect::<Vec<_>>()
{
// Skip not exposed inputs (they still get an entry to help with finding the primary input)
let lookup = if !is_exposed {
None
} else {
// Get the name from the metadata here (since it also requires a reference to the `network_interface`)
let input_name = network_interface
.input_name(node_id, index, breadcrumb_network_path)
.filter(|s| !s.is_empty())
.map(|name| name.to_string());
let input_description = network_interface.input_description(node_id, index, breadcrumb_network_path).map(|description| description.to_string());
// Get the output connector that feeds into this input (done here as well for simplicity)
let connector = OutputConnector::from_input(input);
inputs.push(Some(InputLookup {
let (input_name, input_description) = network_interface.displayed_input_name_and_description(&node_id, index, breadcrumb_network_path);
Some(InputLookup {
input_name,
input_description,
output_connector: connector,
output_connector,
..Default::default()
}));
}
frontend_inputs_lookup.insert(node_id, inputs);
})
};
frontend_inputs_lookup.entry(node_id).or_insert_with(Vec::new).push(lookup);
}
for (&node_id, value) in frontend_inputs_lookup.iter_mut() {
@ -2633,6 +2563,7 @@ impl Default for NodeGraphMessageHandler {
select_if_not_dragged: None,
wire_in_progress_from_connector: None,
wire_in_progress_to_connector: None,
wire_in_progress_type: FrontendGraphDataType::General,
context_menu: None,
deselect_on_pointer_up: None,
auto_panning: Default::default(),
@ -2640,6 +2571,8 @@ impl Default for NodeGraphMessageHandler {
reordering_export: None,
reordering_import: None,
end_index: None,
frontend_nodes: Vec::new(),
frontend_wires: HashSet::new(),
}
}
}

View file

@ -1,546 +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)
// .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(), 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(), 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(), 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
// }

View file

@ -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 {
@ -15,9 +15,9 @@ pub enum FrontendGraphDataType {
}
impl FrontendGraphDataType {
fn with_type(input: &Type) -> Self {
pub fn from_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(_)
@ -38,7 +38,7 @@ impl FrontendGraphDataType {
pub fn displayed_type(input: &Type, type_source: &TypeSource) -> Self {
match type_source {
TypeSource::Error(_) | TypeSource::RandomProtonodeImplementation => Self::General,
_ => Self::with_type(input),
_ => Self::from_type(input),
}
}
}
@ -50,7 +50,7 @@ pub struct FrontendGraphInput {
pub name: String,
pub description: String,
#[serde(rename = "resolvedType")]
pub resolved_type: Option<String>,
pub resolved_type: String,
#[serde(rename = "validTypes")]
pub valid_types: Vec<String>,
#[serde(rename = "connectedTo")]
@ -64,7 +64,7 @@ pub struct FrontendGraphOutput {
pub name: String,
pub description: String,
#[serde(rename = "resolvedType")]
pub resolved_type: Option<String>,
pub resolved_type: String,
#[serde(rename = "connectedTo")]
pub connected_to: Vec<InputConnector>,
}
@ -96,15 +96,6 @@ pub struct FrontendNode {
pub ui_only: bool,
}
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct FrontendNodeWire {
#[serde(rename = "wireStart")]
pub wire_start: OutputConnector,
#[serde(rename = "wireEnd")]
pub wire_end: InputConnector,
pub dashed: bool,
}
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct FrontendNodeType {
pub name: String,
@ -153,16 +144,6 @@ pub struct Transform {
pub y: f64,
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct WirePath {
#[serde(rename = "pathString")]
pub path_string: String,
#[serde(rename = "dataType")]
pub data_type: FrontendGraphDataType,
pub thick: bool,
pub dashed: bool,
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct BoxSelection {
#[serde(rename = "startX")]
@ -224,32 +205,3 @@ pub enum Direction {
Left,
Right,
}
#[derive(Copy, Clone, Debug, PartialEq, Default, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum GraphWireStyle {
#[default]
Direct = 0,
GridAligned = 1,
}
impl std::fmt::Display for GraphWireStyle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
GraphWireStyle::GridAligned => write!(f, "Grid-Aligned"),
GraphWireStyle::Direct => write!(f, "Direct"),
}
}
}
impl GraphWireStyle {
pub fn tooltip_description(&self) -> &'static str {
match self {
GraphWireStyle::GridAligned => "Wires follow the grid, running in straight lines between nodes",
GraphWireStyle::Direct => "Wires bend to run at an angle directly between nodes",
}
}
pub fn is_direct(&self) -> bool {
*self == GraphWireStyle::Direct
}
}

View file

@ -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 {

View file

@ -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,
),
}

View file

@ -1,10 +1,11 @@
use super::utility_types::{DrawHandles, OverlayContext};
use crate::consts::HIDE_HANDLE_DISTANCE;
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
use crate::messages::tool::common_functionality::shape_editor::{SelectedLayerState, ShapeState};
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;
@ -23,7 +24,7 @@ pub fn overlay_canvas_context() -> web_sys::CanvasRenderingContext2d {
create_context().expect("Failed to get canvas context")
}
pub fn selected_segments(document: &DocumentMessageHandler, shape_editor: &mut ShapeState) -> Vec<SegmentId> {
pub fn selected_segments(network_interface: &NodeNetworkInterface, shape_editor: &ShapeState) -> Vec<SegmentId> {
let selected_points = shape_editor.selected_points();
let selected_anchors = selected_points
.filter_map(|point_id| if let ManipulatorPointId::Anchor(p) = point_id { Some(*p) } else { None })
@ -40,8 +41,8 @@ pub fn selected_segments(document: &DocumentMessageHandler, shape_editor: &mut S
// TODO: Currently if there are two duplicate layers, both of their segments get overlays
// Adding segments which are are connected to selected anchors
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 };
for layer in network_interface.selected_nodes().selected_layers(network_interface.document_metadata()) {
let Some(vector_data) = network_interface.compute_modified_vector(layer) else { continue };
for (segment_id, _bezier, start, end) in vector_data.segment_bezier_iter() {
if selected_anchors.contains(&start) || selected_anchors.contains(&end) {
@ -123,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();
@ -186,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 };

View file

@ -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.

View file

@ -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)]
);
}
}

View file

@ -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.

View file

@ -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,
}
@ -404,8 +406,7 @@ pub const SNAP_FUNCTIONS_FOR_BOUNDING_BOXES: [(&str, GetSnapState, &str); 5] = [
(
"Distribute Evenly",
(|snapping_state| &mut snapping_state.bounding_box.distribute_evenly) as GetSnapState,
// TODO: Fix the bug/limitation that requires 'Center Points' and 'Corner Points' to be enabled
"Snaps to a consistent distance offset established by the bounding boxes of nearby layers\n(due to a bug, 'Center Points' and 'Corner Points' must be enabled)",
"Snaps to a consistent distance offset established by the bounding boxes of nearby layers",
),
];
pub const SNAP_FUNCTIONS_FOR_PATHS: [(&str, GetSnapState, &str); 7] = [
@ -692,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),
}

View file

@ -5,3 +5,4 @@ pub mod misc;
pub mod network_interface;
pub mod nodes;
pub mod transformation;
pub mod wires;

View file

@ -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)]

View file

@ -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)
}
}
}

View file

@ -0,0 +1,589 @@
use crate::messages::portfolio::document::node_graph::utility_types::FrontendGraphDataType;
use bezier_rs::{ManipulatorGroup, Subpath};
use glam::{DVec2, IVec2};
use graphene_std::uuid::NodeId;
use graphene_std::vector::PointId;
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct WirePath {
#[serde(rename = "pathString")]
pub path_string: String,
#[serde(rename = "dataType")]
pub data_type: FrontendGraphDataType,
pub thick: bool,
pub dashed: bool,
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct WirePathUpdate {
pub id: NodeId,
#[serde(rename = "inputIndex")]
pub input_index: usize,
// If none, then remove the wire from the map
#[serde(rename = "wirePathUpdate")]
pub wire_path_update: Option<WirePath>,
}
#[derive(Copy, Clone, Debug, PartialEq, Default, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum GraphWireStyle {
#[default]
Direct = 0,
GridAligned = 1,
}
impl std::fmt::Display for GraphWireStyle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
GraphWireStyle::GridAligned => write!(f, "Grid-Aligned"),
GraphWireStyle::Direct => write!(f, "Direct"),
}
}
}
impl GraphWireStyle {
pub fn tooltip_description(&self) -> &'static str {
match self {
GraphWireStyle::GridAligned => "Wires follow the grid, running in straight lines between nodes",
GraphWireStyle::Direct => "Wires bend to run at an angle directly between nodes",
}
}
pub fn is_direct(&self) -> bool {
*self == GraphWireStyle::Direct
}
}
pub fn build_vector_wire(output_position: DVec2, input_position: DVec2, vertical_out: bool, vertical_in: bool, graph_wire_style: GraphWireStyle) -> Subpath<PointId> {
let grid_spacing = 24.;
match graph_wire_style {
GraphWireStyle::Direct => {
let horizontal_gap = (output_position.x - input_position.x).abs();
let vertical_gap = (output_position.y - input_position.y).abs();
let curve_length = grid_spacing;
let curve_falloff_rate = curve_length * std::f64::consts::TAU;
let horizontal_curve_amount = -(2_f64.powf((-10. * horizontal_gap) / curve_falloff_rate)) + 1.;
let vertical_curve_amount = -(2_f64.powf((-10. * vertical_gap) / curve_falloff_rate)) + 1.;
let horizontal_curve = horizontal_curve_amount * curve_length;
let vertical_curve = vertical_curve_amount * curve_length;
let locations = [
output_position,
DVec2::new(
if vertical_out { output_position.x } else { output_position.x + horizontal_curve },
if vertical_out { output_position.y - vertical_curve } else { output_position.y },
),
DVec2::new(
if vertical_in { input_position.x } else { input_position.x - horizontal_curve },
if vertical_in { input_position.y + vertical_curve } else { input_position.y },
),
DVec2::new(input_position.x, input_position.y),
];
let smoothing = 0.5;
let delta01 = DVec2::new((locations[1].x - locations[0].x) * smoothing, (locations[1].y - locations[0].y) * smoothing);
let delta23 = DVec2::new((locations[3].x - locations[2].x) * smoothing, (locations[3].y - locations[2].y) * smoothing);
Subpath::new(
vec![
ManipulatorGroup {
anchor: locations[0],
in_handle: None,
out_handle: None,
id: PointId::generate(),
},
ManipulatorGroup {
anchor: locations[1],
in_handle: None,
out_handle: Some(locations[1] + delta01),
id: PointId::generate(),
},
ManipulatorGroup {
anchor: locations[2],
in_handle: Some(locations[2] - delta23),
out_handle: None,
id: PointId::generate(),
},
ManipulatorGroup {
anchor: locations[3],
in_handle: None,
out_handle: None,
id: PointId::generate(),
},
],
false,
)
}
GraphWireStyle::GridAligned => {
let locations = straight_wire_paths(output_position, input_position, vertical_out, vertical_in);
straight_wire_subpath(locations)
}
}
}
fn straight_wire_paths(output_position: DVec2, input_position: DVec2, vertical_out: bool, vertical_in: bool) -> Vec<IVec2> {
let grid_spacing = 24;
let line_width = 2;
let in_x = input_position.x as i32;
let in_y = input_position.y as i32;
let out_x = output_position.x as i32;
let out_y = output_position.y as i32;
let mid_x = (in_x + out_x) / 2 + (((in_x + out_x) / 2) % grid_spacing);
let mid_y = (in_y + out_y) / 2 + (((in_y + out_y) / 2) % grid_spacing);
let mid_y_alternate = (in_y + in_y) / 2 - (((in_y + in_y) / 2) % grid_spacing);
let x1 = out_x;
let x2 = out_x + grid_spacing;
let x3 = in_x - 2 * grid_spacing;
let x4 = in_x;
let x5 = in_x - 2 * grid_spacing + line_width;
let x6 = out_x + grid_spacing + line_width;
let x7 = out_x + 2 * grid_spacing + line_width;
let x8 = in_x + line_width;
let x9 = out_x + 2 * grid_spacing;
let x10 = mid_x + line_width;
let x11 = out_x - grid_spacing;
let x12 = out_x - 4 * grid_spacing;
let x13 = mid_x;
let x14 = in_x + grid_spacing;
let x15 = in_x - 4 * grid_spacing;
let x16 = in_x + 8 * grid_spacing;
let x17 = mid_x - 2 * line_width;
let x18 = out_x + grid_spacing - 2 * line_width;
let x19 = out_x - 2 * line_width;
let x20 = mid_x - line_width;
let y1 = out_y;
let y2 = out_y - grid_spacing;
let y3 = in_y;
let y4 = out_y - grid_spacing + 5 * line_width + 1;
let y5 = in_y - 2 * grid_spacing;
let y6 = out_y + 4 * line_width;
let y7 = out_y + 5 * line_width;
let y8 = out_y - 2 * grid_spacing + 5 * line_width + 1;
let y9 = out_y + 6 * line_width;
let y10 = in_y + 2 * grid_spacing;
let y111 = in_y + grid_spacing + 6 * line_width + 1;
let y12 = in_y + grid_spacing - 5 * line_width + 1;
let y13 = in_y - grid_spacing;
let y14 = in_y + grid_spacing;
let y15 = mid_y;
let y16 = mid_y_alternate;
let wire1 = vec![IVec2::new(x1, y1), IVec2::new(x1, y4), IVec2::new(x5, y4), IVec2::new(x5, y3), IVec2::new(x4, y3)];
let wire2 = vec![IVec2::new(x1, y1), IVec2::new(x1, y16), IVec2::new(x3, y16), IVec2::new(x3, y3), IVec2::new(x4, y3)];
let wire3 = vec![
IVec2::new(x1, y1),
IVec2::new(x1, y4),
IVec2::new(x12, y4),
IVec2::new(x12, y10),
IVec2::new(x3, y10),
IVec2::new(x3, y3),
IVec2::new(x4, y3),
];
let wire4 = vec![
IVec2::new(x1, y1),
IVec2::new(x1, y4),
IVec2::new(x13, y4),
IVec2::new(x13, y10),
IVec2::new(x3, y10),
IVec2::new(x3, y3),
IVec2::new(x4, y3),
];
if out_y == in_y && out_x > in_x && (vertical_out || !vertical_in) {
return vec![IVec2::new(x1, y1), IVec2::new(x2, y1), IVec2::new(x2, y2), IVec2::new(x3, y2), IVec2::new(x3, y3), IVec2::new(x4, y3)];
}
// `outConnector` point and `inConnector` point lying on the same horizontal grid line and `outConnector` point lies to the right of `inConnector` point
if out_y == in_y && out_x > in_x && (vertical_out || !vertical_in) {
return vec![IVec2::new(x1, y1), IVec2::new(x2, y1), IVec2::new(x2, y2), IVec2::new(x3, y2), IVec2::new(x3, y3), IVec2::new(x4, y3)];
};
// Handle straight lines
if out_y == in_y || (out_x == in_x && vertical_out) {
return vec![IVec2::new(x1, y1), IVec2::new(x4, y3)];
};
// Handle standard right-angle paths
// Start vertical, then horizontal
// `outConnector` point lies to the left of `inConnector` point
if vertical_out && in_x > out_x {
// `outConnector` point lies above `inConnector` point
if out_y < in_y {
// `outConnector` point lies on the vertical grid line 4 units to the left of `inConnector` point point
if -4 * grid_spacing <= out_x - in_x && out_x - in_x < -3 * grid_spacing {
return wire1;
};
// `outConnector` point lying on vertical grid lines 3 and 2 units to the left of `inConnector` point
if -3 * grid_spacing <= out_x - in_x && out_x - in_x <= -grid_spacing {
if -2 * grid_spacing <= out_y - in_y && out_y - in_y <= -grid_spacing {
return vec![IVec2::new(x1, y1), IVec2::new(x1, y2), IVec2::new(x2, y2), IVec2::new(x2, y3), IVec2::new(x4, y3)];
};
if -grid_spacing <= out_y - in_y && out_y - in_y <= 0 {
return vec![IVec2::new(x1, y1), IVec2::new(x1, y4), IVec2::new(x6, y4), IVec2::new(x6, y3), IVec2::new(x4, y3)];
};
return vec![
IVec2::new(x1, y1),
IVec2::new(x1, y4),
IVec2::new(x7, y4),
IVec2::new(x7, y5),
IVec2::new(x3, y5),
IVec2::new(x3, y3),
IVec2::new(x4, y3),
];
}
// `outConnector` point lying on vertical grid line 1 units to the left of `inConnector` point
if -grid_spacing < out_x - in_x && out_x - in_x <= 0 {
// `outConnector` point lying on horizontal grid line 1 unit above `inConnector` point
if -2 * grid_spacing <= out_y - in_y && out_y - in_y <= -grid_spacing {
return vec![IVec2::new(x1, y6), IVec2::new(x2, y6), IVec2::new(x8, y3)];
};
// `outConnector` point lying on the same horizontal grid line as `inConnector` point
if -grid_spacing <= out_y - in_y && out_y - in_y <= 0 {
return vec![IVec2::new(x1, y7), IVec2::new(x4, y3)];
};
return vec![
IVec2::new(x1, y1),
IVec2::new(x1, y2),
IVec2::new(x9, y2),
IVec2::new(x9, y5),
IVec2::new(x3, y5),
IVec2::new(x3, y3),
IVec2::new(x4, y3),
];
}
return vec![IVec2::new(x1, y1), IVec2::new(x1, y4), IVec2::new(x10, y4), IVec2::new(x10, y3), IVec2::new(x4, y3)];
}
// `outConnector` point lies below `inConnector` point
// `outConnector` point lying on vertical grid line 1 unit to the left of `inConnector` point
if -grid_spacing <= out_x - in_x && out_x - in_x <= 0 {
// `outConnector` point lying on the horizontal grid lines 1 and 2 units below the `inConnector` point
if 0 <= out_y - in_y && out_y - in_y <= 2 * grid_spacing {
return vec![IVec2::new(x1, y6), IVec2::new(x11, y6), IVec2::new(x11, y3), IVec2::new(x4, y3)];
};
return wire2;
}
return vec![IVec2::new(x1, y1), IVec2::new(x1, y3), IVec2::new(x4, y3)];
}
// `outConnector` point lies to the right of `inConnector` point
if vertical_out && in_x <= out_x {
// `outConnector` point lying on any horizontal grid line above `inConnector` point
if out_y < in_y {
// `outConnector` point lying on horizontal grid line 1 unit above `inConnector` point
if -2 * grid_spacing < out_y - in_y && out_y - in_y <= -grid_spacing {
return wire1;
};
// `outConnector` point lying on the same horizontal grid line as `inConnector` point
if -grid_spacing < out_y - in_y && out_y - in_y <= 0 {
return vec![IVec2::new(x1, y1), IVec2::new(x1, y8), IVec2::new(x5, y8), IVec2::new(x5, y3), IVec2::new(x4, y3)];
};
// `outConnector` point lying on vertical grid lines 1 and 2 units to the right of `inConnector` point
if grid_spacing <= out_x - in_x && out_x - in_x <= 3 * grid_spacing {
return vec![
IVec2::new(x1, y1),
IVec2::new(x1, y4),
IVec2::new(x9, y4),
IVec2::new(x9, y5),
IVec2::new(x3, y5),
IVec2::new(x3, y3),
IVec2::new(x4, y3),
];
}
return vec![
IVec2::new(x1, y1),
IVec2::new(x1, y4),
IVec2::new(x10, y4),
IVec2::new(x10, y5),
IVec2::new(x5, y5),
IVec2::new(x5, y3),
IVec2::new(x4, y3),
];
}
// `outConnector` point lies below `inConnector` point
if out_y - in_y <= grid_spacing {
// `outConnector` point lies on the horizontal grid line 1 unit below the `inConnector` Point
if 0 <= out_x - in_x && out_x - in_x <= 13 * grid_spacing {
return vec![IVec2::new(x1, y9), IVec2::new(x3, y9), IVec2::new(x3, y3), IVec2::new(x4, y3)];
};
if 13 < out_x - in_x && out_x - in_x <= 18 * grid_spacing {
return wire3;
};
return wire4;
}
// `outConnector` point lies on the horizontal grid line 2 units below `outConnector` point
if grid_spacing <= out_y - in_y && out_y - in_y <= 2 * grid_spacing {
if 0 <= out_x - in_x && out_x - in_x <= 13 * grid_spacing {
return vec![IVec2::new(x1, y7), IVec2::new(x5, y7), IVec2::new(x5, y3), IVec2::new(x4, y3)];
};
if 13 < out_x - in_x && out_x - in_x <= 18 * grid_spacing {
return wire3;
};
return wire4;
}
// 0 to 4 units below the `outConnector` Point
if out_y - in_y <= 4 * grid_spacing {
return wire1;
};
return wire2;
}
// Start horizontal, then vertical
if vertical_in {
// when `outConnector` lies below `inConnector`
if out_y > in_y {
// `out_x` lies to the left of `in_x`
if out_x < in_x {
return vec![IVec2::new(x1, y1), IVec2::new(x4, y1), IVec2::new(x4, y3)];
};
// `out_x` lies to the right of `in_x`
if out_y - in_y <= grid_spacing {
// `outConnector` point directly below `inConnector` point
if 0 <= out_x - in_x && out_x - in_x <= grid_spacing {
return vec![IVec2::new(x1, y1), IVec2::new(x14, y1), IVec2::new(x14, y2), IVec2::new(x4, y2), IVec2::new(x4, y3)];
};
// `outConnector` point lies below `inConnector` point and strictly to the right of `inConnector` point
return vec![IVec2::new(x1, y1), IVec2::new(x2, y1), IVec2::new(x2, y111), IVec2::new(x4, y111), IVec2::new(x4, y3)];
}
return vec![IVec2::new(x1, y1), IVec2::new(x2, y1), IVec2::new(x2, y2), IVec2::new(x4, y2), IVec2::new(x4, y3)];
}
// `out_y` lies on or above the `in_y` point
if -6 * grid_spacing < in_x - out_x && in_x - out_x < 4 * grid_spacing {
// edge case: `outConnector` point lying on vertical grid lines ranging from 4 units to left to 5 units to right of `inConnector` point
if -grid_spacing < in_x - out_x && in_x - out_x < 4 * grid_spacing {
return vec![
IVec2::new(x1, y1),
IVec2::new(x2, y1),
IVec2::new(x2, y2),
IVec2::new(x15, y2),
IVec2::new(x15, y12),
IVec2::new(x4, y12),
IVec2::new(x4, y3),
];
}
return vec![IVec2::new(x1, y1), IVec2::new(x16, y1), IVec2::new(x16, y12), IVec2::new(x4, y12), IVec2::new(x4, y3)];
}
// left of edge case: `outConnector` point lying on vertical grid lines more than 4 units to left of `inConnector` point
if 4 * grid_spacing < in_x - out_x {
return vec![IVec2::new(x1, y1), IVec2::new(x17, y1), IVec2::new(x17, y12), IVec2::new(x4, y12), IVec2::new(x4, y3)];
};
// right of edge case: `outConnector` point lying on the vertical grid lines more than 5 units to right of `inConnector` point
if 6 * grid_spacing > in_x - out_x {
return vec![IVec2::new(x1, y1), IVec2::new(x18, y1), IVec2::new(x18, y12), IVec2::new(x4, y12), IVec2::new(x4, y3)];
};
}
// Both horizontal - use horizontal middle point
// When `inConnector` point is one of the two closest diagonally opposite points
if 0 <= in_x - out_x && in_x - out_x <= grid_spacing && in_y - out_y >= -grid_spacing && in_y - out_y <= grid_spacing {
return vec![IVec2::new(x19, y1), IVec2::new(x19, y3), IVec2::new(x4, y3)];
}
// When `inConnector` point lies on the horizontal line 1 unit above and below the `outConnector` point
if -grid_spacing <= out_y - in_y && out_y - in_y <= grid_spacing && out_x > in_x {
// Horizontal line above `out_y`
if in_y < out_y {
return vec![IVec2::new(x1, y1), IVec2::new(x2, y1), IVec2::new(x2, y13), IVec2::new(x3, y13), IVec2::new(x3, y3), IVec2::new(x4, y3)];
};
// Horizontal line below `out_y`
return vec![IVec2::new(x1, y1), IVec2::new(x2, y1), IVec2::new(x2, y14), IVec2::new(x3, y14), IVec2::new(x3, y3), IVec2::new(x4, y3)];
}
// `outConnector` point to the right of `inConnector` point
if out_x > in_x - grid_spacing {
return vec![
IVec2::new(x1, y1),
IVec2::new(x18, y1),
IVec2::new(x18, y15),
IVec2::new(x5, y15),
IVec2::new(x5, y3),
IVec2::new(x4, y3),
];
};
// When `inConnector` point lies on the vertical grid line two units to the right of `outConnector` point
if grid_spacing <= in_x - out_x && in_x - out_x <= 2 * grid_spacing {
return vec![IVec2::new(x1, y1), IVec2::new(x18, y1), IVec2::new(x18, y3), IVec2::new(x4, y3)];
};
vec![IVec2::new(x1, y1), IVec2::new(x20, y1), IVec2::new(x20, y3), IVec2::new(x4, y3)]
}
fn straight_wire_subpath(locations: Vec<IVec2>) -> Subpath<PointId> {
if locations.is_empty() {
return Subpath::new(Vec::new(), false);
}
if locations.len() == 2 {
return Subpath::new(
vec![
ManipulatorGroup {
anchor: locations[0].into(),
in_handle: None,
out_handle: None,
id: PointId::generate(),
},
ManipulatorGroup {
anchor: locations[1].into(),
in_handle: None,
out_handle: None,
id: PointId::generate(),
},
],
false,
);
}
let corner_radius = 10;
// Create path with rounded corners
let mut path = vec![ManipulatorGroup {
anchor: locations[0].into(),
in_handle: None,
out_handle: None,
id: PointId::generate(),
}];
for i in 1..(locations.len() - 1) {
let prev = locations[i - 1];
let curr = locations[i];
let next = locations[i + 1];
let corner_start = IVec2::new(
curr.x
+ if curr.x == prev.x {
0
} else if prev.x > curr.x {
corner_radius
} else {
-corner_radius
},
curr.y
+ if curr.y == prev.y {
0
} else if prev.y > curr.y {
corner_radius
} else {
-corner_radius
},
);
let corner_start_mid = IVec2::new(
curr.x
+ if curr.x == prev.x {
0
} else if prev.x > curr.x {
corner_radius / 2
} else {
-corner_radius / 2
},
curr.y
+ if curr.y == prev.y {
0
} else {
match prev.y > curr.y {
true => corner_radius / 2,
false => -corner_radius / 2,
}
},
);
let corner_end = IVec2::new(
curr.x
+ if curr.x == next.x {
0
} else if next.x > curr.x {
corner_radius
} else {
-corner_radius
},
curr.y
+ if curr.y == next.y {
0
} else if next.y > curr.y {
corner_radius
} else {
-corner_radius
},
);
let corner_end_mid = IVec2::new(
curr.x
+ if curr.x == next.x {
0
} else if next.x > curr.x {
corner_radius / 2
} else {
-corner_radius / 2
},
curr.y
+ if curr.y == next.y {
0
} else if next.y > curr.y {
10 / 2
} else {
-corner_radius / 2
},
);
path.extend(vec![
ManipulatorGroup {
anchor: corner_start.into(),
in_handle: None,
out_handle: Some(corner_start_mid.into()),
id: PointId::generate(),
},
ManipulatorGroup {
anchor: corner_end.into(),
in_handle: Some(corner_end_mid.into()),
out_handle: None,
id: PointId::generate(),
},
])
}
path.push(ManipulatorGroup {
anchor: (*locations.last().unwrap()).into(),
in_handle: None,
out_handle: None,
id: PointId::generate(),
});
Subpath::new(path, false)
}

View file

@ -0,0 +1,682 @@
// 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, NodeTemplate, 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::{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"),
// math_nodes
("graphene_core::ops::MathNode", "graphene_math_nodes::MathNode"),
("graphene_core::ops::AddNode", "graphene_math_nodes::AddNode"),
("graphene_core::ops::SubtractNode", "graphene_math_nodes::SubtractNode"),
("graphene_core::ops::MultiplyNode", "graphene_math_nodes::MultiplyNode"),
("graphene_core::ops::DivideNode", "graphene_math_nodes::DivideNode"),
("graphene_core::ops::ModuloNode", "graphene_math_nodes::ModuloNode"),
("graphene_core::ops::ExponentNode", "graphene_math_nodes::ExponentNode"),
("graphene_core::ops::RootNode", "graphene_math_nodes::RootNode"),
("graphene_core::ops::LogarithmNode", "graphene_math_nodes::LogarithmNode"),
("graphene_core::ops::SineNode", "graphene_math_nodes::SineNode"),
("graphene_core::ops::CosineNode", "graphene_math_nodes::CosineNode"),
("graphene_core::ops::TangentNode", "graphene_math_nodes::TangentNode"),
("graphene_core::ops::SineInverseNode", "graphene_math_nodes::SineInverseNode"),
("graphene_core::ops::CosineInverseNode", "graphene_math_nodes::CosineInverseNode"),
("graphene_core::ops::TangentInverseNode", "graphene_math_nodes::TangentInverseNode"),
("graphene_core::ops::RandomNode", "graphene_math_nodes::RandomNode"),
("graphene_core::ops::ToU32Node", "graphene_math_nodes::ToU32Node"),
("graphene_core::ops::ToU64Node", "graphene_math_nodes::ToU64Node"),
("graphene_core::ops::ToF64Node", "graphene_math_nodes::ToF64Node"),
("graphene_core::ops::RoundNode", "graphene_math_nodes::RoundNode"),
("graphene_core::ops::FloorNode", "graphene_math_nodes::FloorNode"),
("graphene_core::ops::CeilingNode", "graphene_math_nodes::CeilingNode"),
("graphene_core::ops::MinNode", "graphene_math_nodes::MinNode"),
("graphene_core::ops::MaxNode", "graphene_math_nodes::MaxNode"),
("graphene_core::ops::ClampNode", "graphene_math_nodes::ClampNode"),
("graphene_core::ops::EqualsNode", "graphene_math_nodes::EqualsNode"),
("graphene_core::ops::NotEqualsNode", "graphene_math_nodes::NotEqualsNode"),
("graphene_core::ops::LessThanNode", "graphene_math_nodes::LessThanNode"),
("graphene_core::ops::GreaterThanNode", "graphene_math_nodes::GreaterThanNode"),
("graphene_core::ops::LogicalOrNode", "graphene_math_nodes::LogicalOrNode"),
("graphene_core::ops::LogicalAndNode", "graphene_math_nodes::LogicalAndNode"),
("graphene_core::ops::LogicalNotNode", "graphene_math_nodes::LogicalNotNode"),
("graphene_core::ops::BoolValueNode", "graphene_math_nodes::BoolValueNode"),
("graphene_core::ops::NumberValueNode", "graphene_math_nodes::NumberValueNode"),
("graphene_core::ops::PercentageValueNode", "graphene_math_nodes::PercentageValueNode"),
("graphene_core::ops::CoordinateValueNode", "graphene_math_nodes::CoordinateValueNode"),
("graphene_core::ops::ConstructVector2", "graphene_math_nodes::CoordinateValueNode"),
("graphene_core::ops::Vector2ValueNode", "graphene_math_nodes::CoordinateValueNode"),
("graphene_core::ops::ColorValueNode", "graphene_math_nodes::ColorValueNode"),
("graphene_core::ops::GradientValueNode", "graphene_math_nodes::GradientValueNode"),
("graphene_core::ops::StringValueNode", "graphene_math_nodes::StringValueNode"),
("graphene_core::ops::DotProductNode", "graphene_math_nodes::DotProductNode"),
// debug
("graphene_core::ops::SizeOfNode", "graphene_core::debug::SizeOfNode"),
("graphene_core::ops::SomeNode", "graphene_core::debug::SomeNode"),
("graphene_core::ops::UnwrapNode", "graphene_core::debug::UnwrapNode"),
("graphene_core::ops::CloneNode", "graphene_core::debug::CloneNode"),
// ???
("graphene_core::ops::ExtractXyNode", "graphene_core::extract_xy::ExtractXyNode"),
("graphene_core::logic::LogicAndNode", "graphene_core::ops::LogicAndNode"),
("graphene_core::logic::LogicNotNode", "graphene_core::ops::LogicNotNode"),
("graphene_core::logic::LogicOrNode", "graphene_core::ops::LogicOrNode"),
("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::vector::GenerateHandlesNode", "graphene_core::vector::AutoTangentsNode"),
("graphene_core::vector::RemoveHandlesNode", "graphene_core::vector::AutoTangentsNode"),
// raster::adjustments
("graphene_core::raster::adjustments::LuminanceNode", "graphene_raster_nodes::adjustments::LuminanceNode"),
("graphene_core::raster::LuminanceNode", "graphene_raster_nodes::adjustments::LuminanceNode"),
("graphene_core::raster::adjustments::ExtractChannelNode", "graphene_raster_nodes::adjustments::ExtractChannelNode"),
("graphene_core::raster::ExtractChannelNode", "graphene_raster_nodes::adjustments::ExtractChannelNode"),
("graphene_core::raster::adjustments::MakeOpaqueNode", "graphene_raster_nodes::adjustments::MakeOpaqueNode"),
("graphene_core::raster::ExtractOpaqueNode", "graphene_raster_nodes::adjustments::MakeOpaqueNode"),
(
"graphene_core::raster::adjustments::BrightnessContrastNode",
"graphene_raster_nodes::adjustments::BrightnessContrastNode",
),
("graphene_core::raster::adjustments::LevelsNode", "graphene_raster_nodes::adjustments::LevelsNode"),
("graphene_core::raster::LevelsNode", "graphene_raster_nodes::adjustments::LevelsNode"),
("graphene_core::raster::adjustments::BlackAndWhiteNode", "graphene_raster_nodes::adjustments::BlackAndWhiteNode"),
("graphene_core::raster::BlackAndWhiteNode", "graphene_raster_nodes::adjustments::BlackAndWhiteNode"),
("graphene_core::raster::adjustments::HueSaturationNode", "graphene_raster_nodes::adjustments::HueSaturationNode"),
("graphene_core::raster::HueSaturationNode", "graphene_raster_nodes::adjustments::HueSaturationNode"),
("graphene_core::raster::adjustments::InvertNode", "graphene_raster_nodes::adjustments::InvertNode"),
("graphene_core::raster::InvertNode", "graphene_raster_nodes::adjustments::InvertNode"),
("graphene_core::raster::InvertRGBNode", "graphene_raster_nodes::adjustments::InvertNode"),
("graphene_core::raster::adjustments::ThresholdNode", "graphene_raster_nodes::adjustments::ThresholdNode"),
("graphene_core::raster::ThresholdNode", "graphene_raster_nodes::adjustments::ThresholdNode"),
("graphene_core::raster::adjustments::BlendNode", "graphene_raster_nodes::adjustments::BlendNode"),
("graphene_core::raster::BlendNode", "graphene_raster_nodes::adjustments::BlendNode"),
("graphene_core::raster::BlendColorPairNode", "graphene_raster_nodes::adjustments::BlendColorPairNode"),
("graphene_core::raster::adjustments::BlendColorsNode", "graphene_raster_nodes::adjustments::BlendColorsNode"),
("graphene_core::raster::BlendColorsNode", "graphene_raster_nodes::adjustments::BlendColorsNode"),
("graphene_core::raster::adjustments::GradientMapNode", "graphene_raster_nodes::adjustments::GradientMapNode"),
("graphene_core::raster::GradientMapNode", "graphene_raster_nodes::adjustments::GradientMapNode"),
("graphene_core::raster::adjustments::VibranceNode", "graphene_raster_nodes::adjustments::VibranceNode"),
("graphene_core::raster::VibranceNode", "graphene_raster_nodes::adjustments::VibranceNode"),
("graphene_core::raster::adjustments::ChannelMixerNode", "graphene_raster_nodes::adjustments::ChannelMixerNode"),
("graphene_core::raster::ChannelMixerNode", "graphene_raster_nodes::adjustments::ChannelMixerNode"),
("graphene_core::raster::adjustments::SelectiveColorNode", "graphene_raster_nodes::adjustments::SelectiveColorNode"),
("graphene_core::raster::adjustments::PosterizeNode", "graphene_raster_nodes::adjustments::PosterizeNode"),
("graphene_core::raster::PosterizeNode", "graphene_raster_nodes::adjustments::PosterizeNode"),
("graphene_core::raster::adjustments::ExposureNode", "graphene_raster_nodes::adjustments::ExposureNode"),
("graphene_core::raster::ExposureNode", "graphene_raster_nodes::adjustments::ExposureNode"),
("graphene_core::raster::adjustments::ColorOverlayNode", "graphene_raster_nodes::adjustments::ColorOverlayNode"),
("graphene_raster_nodes::generate_curves::ColorOverlayNode", "graphene_raster_nodes::adjustments::ColorOverlayNode"),
// raster
("graphene_core::raster::adjustments::GenerateCurvesNode", "graphene_raster_nodes::generate_curves::GenerateCurvesNode"),
("graphene_std::dehaze::DehazeNode", "graphene_raster_nodes::dehaze::DehazeNode"),
("graphene_std::filter::BlurNode", "graphene_raster_nodes::filter::BlurNode"),
(
"graphene_std::image_color_palette::ImageColorPaletteNode",
"graphene_raster_nodes::image_color_palette::ImageColorPaletteNode",
),
("graphene_std::raster::SampleImageNode", "graphene_raster_nodes::std_nodes::SampleImageNode"),
("graphene_std::raster::CombineChannelsNode", "graphene_raster_nodes::std_nodes::CombineChannelsNode"),
("graphene_std::raster::MaskNode", "graphene_raster_nodes::std_nodes::MaskNode"),
("graphene_std::raster::ExtendImageToBoundsNode", "graphene_raster_nodes::std_nodes::ExtendImageToBoundsNode"),
("graphene_std::raster::EmptyImageNode", "graphene_raster_nodes::std_nodes::EmptyImageNode"),
("graphene_std::raster::ImageValueNode", "graphene_raster_nodes::std_nodes::ImageValueNode"),
("graphene_std::raster::NoisePatternNode", "graphene_raster_nodes::std_nodes::NoisePatternNode"),
("graphene_std::raster::MandelbrotNode", "graphene_raster_nodes::std_nodes::MandelbrotNode"),
// text
("graphene_core::text::TextGeneratorNode", "graphene_core::text::TextNode"),
// transform
("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"),
// brush
("graphene_std::brush::BrushStampGeneratorNode", "graphene_brush::brush::BrushStampGeneratorNode"),
("graphene_std::brush::BlitNode", "graphene_brush::brush::BlitNode"),
("graphene_std::brush::BrushNode", "graphene_brush::brush::BrushNode"),
];
pub fn document_migration_string_preprocessing(document_serialized_content: String) -> String {
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 network = document.network_interface.document_network().clone();
// Apply string replacements to each node
for (node_id, node, network_path) in network.recursive_nodes() {
if let DocumentNodeImplementation::ProtoNode(protonode_id) = &node.implementation {
for (old, new) in REPLACEMENTS {
let node_path_without_type_args = protonode_id.name.split('<').next();
let mut default_template = NodeTemplate::default();
default_template.document_node.implementation = DocumentNodeImplementation::ProtoNode(new.to_string().into());
if node_path_without_type_args == Some(old) {
document.network_interface.replace_implementation(node_id, &network_path, &mut default_template);
document.network_interface.set_manual_compostion(node_id, &network_path, Some(graph_craft::Type::Generic("T".into())));
}
}
}
}
// Apply upgrades to each unmodified node.
let nodes = document
.network_interface
.document_network()
.recursive_nodes()
.map(|(node_id, node, path)| (*node_id, node.clone(), path))
.collect::<Vec<(NodeId, graph_craft::document::DocumentNode, Vec<NodeId>)>>();
for (node_id, node, network_path) in &nodes {
if reset_node_definitions_on_open {
if let Some(Some(reference)) = document.network_interface.reference(node_id, network_path) {
let Some(node_definition) = resolve_document_node_type(reference) else { continue };
document.network_interface.replace_implementation(node_id, network_path, &mut node_definition.default_node_template());
}
}
// 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(Some(reference)) = document.network_interface.reference(node_id, network_path).cloned() else {
// Only nodes that have not been modified and still refer to a definition can be updated
continue;
};
let reference = &reference;
let inputs_count = node.inputs.len();
// Upgrade Stroke node to reorder parameters and add "Align" and "Paint Order" (#2644)
if reference == "Stroke" && inputs_count == 8 {
let mut node_template = resolve_document_node_type(reference).unwrap().default_node_template();
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template).unwrap();
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 != 9 {
let mut template = resolve_document_node_type(reference).unwrap().default_node_template();
document.network_interface.replace_implementation(node_id, network_path, &mut template);
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut template).unwrap();
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),
if inputs_count >= 7 {
old_inputs[6].clone()
} else {
NodeInput::value(TaggedValue::OptionalF64(TypesettingConfig::default().max_width), false)
},
network_path,
);
document.network_interface.set_input(
&InputConnector::node(*node_id, 7),
if inputs_count >= 8 {
old_inputs[7].clone()
} else {
NodeInput::value(TaggedValue::OptionalF64(TypesettingConfig::default().max_height), false)
},
network_path,
);
document.network_interface.set_input(
&InputConnector::node(*node_id, 8),
if inputs_count >= 9 {
old_inputs[8].clone()
} else {
NodeInput::value(TaggedValue::F64(TypesettingConfig::default().tilt), false)
},
network_path,
);
}
// 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 mut node_template = resolve_document_node_type(reference).unwrap().default_node_template();
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template).unwrap();
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 mut node_template = resolve_document_node_type(reference).unwrap().default_node_template();
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template).unwrap();
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 mut node_template = resolve_document_node_type(reference).unwrap().default_node_template();
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template).unwrap();
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 mut node_template = resolve_document_node_type(reference).unwrap().default_node_template();
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template).unwrap();
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 mut node_template = resolve_document_node_type(reference).unwrap().default_node_template();
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
// 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 mut node_template = resolve_document_node_type(reference).unwrap().default_node_template();
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template).unwrap();
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 mut node_template = resolve_document_node_type(reference).unwrap().default_node_template();
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template).unwrap();
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 mut node_template = resolve_document_node_type(reference).unwrap().default_node_template();
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template).unwrap();
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 mut node_template = resolve_document_node_type(reference).unwrap().default_node_template();
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template).unwrap();
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 mut node_template = resolve_document_node_type(reference).unwrap().default_node_template();
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template).unwrap();
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 mut node_template = resolve_document_node_type(reference).unwrap().default_node_template();
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template).unwrap();
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 mut node_template = resolve_document_node_type("Auto-Tangents").unwrap().default_node_template();
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template).unwrap();
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 mut node_template = resolve_document_node_type(reference).unwrap().default_node_template();
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template).unwrap();
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 mut node_template = resolve_document_node_type("Merge by Distance").unwrap().default_node_template();
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template).unwrap();
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 mut node_template = resolve_document_node_type("Sample Polyline").unwrap().default_node_template();
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template).unwrap();
let new_spacing_value = NodeInput::value(TaggedValue::PointSpacingType(graphene_std::vector::misc::PointSpacingType::Separation), false);
let new_quantity_value = NodeInput::value(TaggedValue::U32(100), false);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 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), new_quantity_value, network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 4), old_inputs[2].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 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());
}
// Make the "Quantity" parameter a u32 instead of f64
if reference == "Sample Polyline" {
let node_definition = resolve_document_node_type("Sample Polyline").unwrap();
let mut new_node_template = node_definition.default_node_template();
// Get the inputs, obtain the quantity value, and put the inputs back
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut new_node_template).unwrap();
let quantity_value = old_inputs.get(3).cloned();
if let Some(NodeInput::Value { tagged_value, exposed }) = quantity_value {
if let TaggedValue::F64(value) = *tagged_value {
let new_quantity_value = NodeInput::value(TaggedValue::U32(value as u32), exposed);
document.network_interface.set_input(&InputConnector::node(*node_id, 3), new_quantity_value, network_path);
}
}
}
// Make the "Grid" node, if its input of index 3 is a DVec2 for "angles" instead of a u32 for the "columns" input that now succeeds "angles", move the angle to index 5 (after "columns" and "rows")
if reference == "Grid" && inputs_count == 6 {
let node_definition = resolve_document_node_type(reference).unwrap();
let mut new_node_template = node_definition.default_node_template();
let mut current_node_template = document.network_interface.create_node_template(node_id, network_path).unwrap();
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut new_node_template).unwrap();
let index_3_value = old_inputs.get(3).cloned();
if let Some(NodeInput::Value { tagged_value, exposed: _ }) = index_3_value {
if matches!(*tagged_value, TaggedValue::DVec2(_)) {
// Move index 3 to the end
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 3), old_inputs[4].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 4), old_inputs[5].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 5), old_inputs[3].clone(), network_path);
} else {
// Swap it back if we're not changing anything
let _ = document.network_interface.replace_inputs(node_id, network_path, &mut current_node_template);
}
}
}
// 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, &[]);
}
}
}
}
}

View file

@ -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, ExtractField)]
pub struct MenuBarMessageHandler {
@ -417,7 +417,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()

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;

View file

@ -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,
@ -90,6 +87,9 @@ pub enum PortfolioMessage {
PasteSerializedData {
data: String,
},
CenterPastedLayers {
layers: Vec<LayerNodeIdentifier>,
},
PasteImage {
name: Option<String>,
image: Image<Color>,

View file

@ -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;
@ -10,20 +10,19 @@ use crate::messages::dialog::simple_dialogs;
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::node_graph::document_node_definitions::resolve_document_node_type;
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
use crate::messages::portfolio::document::utility_types::clipboards::{Clipboard, CopyBufferEntry, INTERNAL_CLIPBOARD_COUNT};
use crate::messages::portfolio::document::utility_types::network_interface::OutputConnector;
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::IVec2;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNodeImplementation, NodeId, NodeInput};
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;
#[derive(ExtractField)]
@ -332,35 +331,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 +400,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,486 +426,16 @@ 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(&[]);
// Upgrade the document's nodes to be compatible with the latest version
document_migration_upgrades(&mut document, reset_node_definitions_on_open);
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())));
}
}
}
// Ensure each node has the metadata for its inputs
for (node_id, node, path) in document.network_interface.document_network().clone().recursive_nodes() {
document.network_interface.validate_input_metadata(node_id, node, &path);
document.network_interface.validate_display_name_metadata(node_id, &path);
}
// 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
// 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 {
@ -959,6 +448,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
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
@ -968,15 +458,18 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
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, &[]);
}
}
}
// 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 } => {
@ -1003,8 +496,10 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
}
PortfolioMessage::PasteSerializedData { data } => {
if let Some(document) = self.active_document() {
let mut all_new_ids = Vec::new();
if let Ok(data) = serde_json::from_str::<Vec<CopyBufferEntry>>(&data) {
let parent = document.new_layer_parent(false);
let mut layers = Vec::new();
let mut added_nodes = false;
@ -1014,16 +509,129 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
responses.add(DocumentMessage::AddTransaction);
added_nodes = true;
}
document.load_layer_resources(responses);
let new_ids: HashMap<_, _> = entry.nodes.iter().map(|(id, _)| (*id, NodeId::new())).collect();
let layer = LayerNodeIdentifier::new_unchecked(new_ids[&NodeId(0)]);
all_new_ids.extend(new_ids.values().cloned());
responses.add(NodeGraphMessage::AddNodes { nodes: entry.nodes, new_ids });
responses.add(NodeGraphMessage::MoveLayerToStack { layer, parent, insert_index: 0 });
layers.push(layer);
}
responses.add(NodeGraphMessage::RunDocumentGraph);
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: all_new_ids });
responses.add(Message::StartBuffer);
responses.add(PortfolioMessage::CenterPastedLayers { layers });
}
}
}
PortfolioMessage::CenterPastedLayers { layers } => {
if let Some(document) = self.active_document_mut() {
let viewport_bounds_quad_pixels = Quad::from_box([DVec2::ZERO, ipp.viewport_bounds.size()]);
let viewport_center_pixels = viewport_bounds_quad_pixels.center(); // In viewport pixel coordinates
let doc_to_viewport_transform = document.metadata().document_to_viewport;
let viewport_to_doc_transform = doc_to_viewport_transform.inverse();
let viewport_quad_doc_space = viewport_to_doc_transform * viewport_bounds_quad_pixels;
let mut top_level_items_to_center: Vec<LayerNodeIdentifier> = Vec::new();
let mut artboards_in_selection: Vec<LayerNodeIdentifier> = Vec::new();
for &layer_id in &layers {
if document.network_interface.is_artboard(&layer_id.to_node(), &document.node_graph_handler.network) {
artboards_in_selection.push(layer_id);
}
}
for &layer_id in &layers {
let is_child_of_selected_artboard = artboards_in_selection.iter().any(|&artboard_id| {
if layer_id == artboard_id {
return false;
}
layer_id.ancestors(document.metadata()).any(|ancestor| ancestor == artboard_id)
});
if !is_child_of_selected_artboard {
top_level_items_to_center.push(layer_id);
}
}
if top_level_items_to_center.is_empty() {
return;
}
let mut combined_min_doc = DVec2::MAX;
let mut combined_max_doc = DVec2::MIN;
let mut has_any_bounds = false;
for &item_id in &top_level_items_to_center {
if let Some(bounds_doc) = document.metadata().bounding_box_document(item_id) {
combined_min_doc = combined_min_doc.min(bounds_doc[0]);
combined_max_doc = combined_max_doc.max(bounds_doc[1]);
has_any_bounds = true;
}
}
if !has_any_bounds {
return;
}
let combined_bounds_doc_quad = Quad::from_box([combined_min_doc, combined_max_doc]);
if combined_bounds_doc_quad.intersects(viewport_quad_doc_space) {
return;
}
let combined_center_doc = combined_bounds_doc_quad.center();
let combined_center_viewport_pixels = doc_to_viewport_transform.transform_point2(combined_center_doc);
let translation_viewport_pixels_rounded = (viewport_center_pixels - combined_center_viewport_pixels).round();
let final_translation_offset_doc = viewport_to_doc_transform.transform_vector2(translation_viewport_pixels_rounded);
if final_translation_offset_doc.abs_diff_eq(glam::DVec2::ZERO, 1e-9) {
return;
}
responses.add(DocumentMessage::AddTransaction);
for &item_id in &top_level_items_to_center {
if document.network_interface.is_artboard(&item_id.to_node(), &document.node_graph_handler.network) {
if let Some(bounds_doc) = document.metadata().bounding_box_document(item_id) {
let current_artboard_origin_doc = bounds_doc[0];
let dimensions_doc = bounds_doc[1] - bounds_doc[0];
let new_artboard_origin_doc = current_artboard_origin_doc + final_translation_offset_doc;
responses.add(GraphOperationMessage::ResizeArtboard {
layer: item_id,
location: new_artboard_origin_doc.round().as_ivec2(),
dimensions: dimensions_doc.round().as_ivec2(),
});
}
} else {
let current_abs_doc_transform = document.metadata().transform_to_document(item_id);
let new_abs_doc_transform = DAffine2 {
matrix2: current_abs_doc_transform.matrix2,
translation: current_abs_doc_transform.translation + final_translation_offset_doc,
};
let transform = doc_to_viewport_transform * new_abs_doc_transform;
responses.add(GraphOperationMessage::TransformSet {
layer: item_id,
transform,
transform_in: TransformIn::Viewport,
skip_rerender: false,
});
}
}
responses.add(NodeGraphMessage::RunDocumentGraph);
}
}
PortfolioMessage::PasteImage {
name,
image,
@ -1140,7 +748,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
}
let Some(document) = self.documents.get_mut(&document_id) else {
warn!("Tried to read non existant document");
warn!("Tried to read non existent document");
return;
};
if !document.is_loaded {
@ -1320,6 +928,7 @@ impl PortfolioMessageHandler {
self.document_ids.push_back(document_id);
}
new_document.update_layers_panel_control_bar_widgets(responses);
new_document.update_layers_panel_bottom_bar_widgets(responses);
self.documents.insert(document_id, new_document);
@ -1328,7 +937,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 });
}
@ -1354,11 +963,11 @@ impl PortfolioMessageHandler {
let result = self.executor.poll_node_graph_evaluation(active_document, responses);
if result.is_err() {
let error = r#"
<rect x="50%" y="50%" width="480" height="100" transform="translate(-240 -50)" rx="4" fill="var(--color-error-red)" />
<rect x="50%" y="50%" width="460" height="100" transform="translate(-230 -50)" rx="4" fill="var(--color-warning-yellow)" />
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-size="18" fill="var(--color-2-mildblack)">
<tspan x="50%" dy="-24" font-weight="bold">The document cannot be rendered in its current state.</tspan>
<tspan x="50%" dy="24">Check for error details in the node graph, which can be</tspan>
<tspan x="50%" dy="24">opened with the viewport's top right <tspan font-style="italic">Node Graph</tspan> button.</tspan>
<tspan x="50%" dy="-24" font-weight="bold">The document cannot render in its current state.</tspan>
<tspan x="50%" dy="24">Undo to go back, if available, or check for error details</tspan>
<tspan x="50%" dy="24">by clicking the <tspan font-style="italic">Node Graph</tspan> button up at the top right.</tspan>
/text>"#
// It's a mystery why the `/text>` tag above needs to be missing its `<`, but when it exists it prints the `<` character in the text. However this works with it removed.
.to_string();

View file

@ -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;
@ -155,7 +157,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
@ -166,7 +169,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"),
}
}
}
@ -179,19 +183,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(),
@ -201,8 +228,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(),
@ -212,17 +239,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 }]
}
}
@ -262,13 +292,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(),
]
})

View file

@ -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)]

View file

@ -1,4 +1,4 @@
use crate::messages::portfolio::document::node_graph::utility_types::GraphWireStyle;
use crate::messages::portfolio::document::utility_types::wires::GraphWireStyle;
use crate::messages::preferences::SelectionMode;
use crate::messages::prelude::*;
@ -16,6 +16,4 @@ pub enum PreferencesMessage {
ModifyLayout { zoom_with_scroll: bool },
GraphWireStyle { style: GraphWireStyle },
ViewportZoomWheelRate { rate: f64 },
// ImaginateRefreshFrequency { seconds: f64 },
// ImaginateServerHostname { hostname: String },
}

View file

@ -1,14 +1,12 @@
use crate::consts::VIEWPORT_ZOOM_WHEEL_RATE;
use crate::messages::input_mapper::key_mapping::MappingVariant;
use crate::messages::portfolio::document::node_graph::utility_types::GraphWireStyle;
use crate::messages::portfolio::document::utility_types::wires::GraphWireStyle;
use crate::messages::preferences::SelectionMode;
use crate::messages::prelude::*;
use graph_craft::wasm_application_io::EditorPreferences;
#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize, specta::Type, ExtractField)]
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,
@ -58,10 +53,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 {
@ -96,33 +87,13 @@ impl MessageHandler<PreferencesMessage, ()> for PreferencesMessageHandler {
}
PreferencesMessage::GraphWireStyle { style } => {
self.graph_wire_style = style;
responses.add(NodeGraphMessage::SendGraph);
responses.add(NodeGraphMessage::UnloadWires);
responses.add(NodeGraphMessage::SendWires);
}
PreferencesMessage::ViewportZoomWheelRate { rate } => {
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() });
}

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};

View file

@ -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 });
}
}

View file

@ -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);
}
}
}
}

View file

@ -0,0 +1,2 @@
pub mod gizmo_manager;
pub mod shape_gizmos;

View file

@ -0,0 +1,2 @@
pub mod number_of_points_dial;
pub mod point_radius_handle;

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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")
}
@ -342,6 +361,7 @@ pub fn get_text(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInter
let Some(&TaggedValue::F64(character_spacing)) = inputs[5].as_value() else { return None };
let Some(&TaggedValue::OptionalF64(max_width)) = inputs[6].as_value() else { return None };
let Some(&TaggedValue::OptionalF64(max_height)) = inputs[7].as_value() else { return None };
let Some(&TaggedValue::F64(tilt)) = inputs[8].as_value() else { return None };
let typesetting = TypesettingConfig {
font_size,
@ -349,12 +369,13 @@ pub fn get_text(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInter
max_width,
character_spacing,
max_height,
tilt,
};
Some((text, font, typesetting))
}
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 +445,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>)
}
}

View file

@ -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;

View file

@ -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,
}

View file

@ -1,18 +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::tool_messages::path_tool::PointSelectState;
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 {
@ -44,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,
@ -53,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)
}
@ -65,12 +84,36 @@ 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 deselect_all_points_in_layer(&mut self) {
self.selected_points.clear();
}
pub fn deselect_all_segments_in_layer(&mut self) {
self.selected_segments.clear();
}
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 {
if self.ignore_handles != status {
return;
}
@ -86,7 +129,7 @@ impl SelectedLayerState {
}
pub fn ignore_anchors(&mut self, status: bool) {
if self.ignore_anchors == !status {
if self.ignore_anchors != status {
return;
}
@ -100,14 +143,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>;
@ -127,6 +162,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,
@ -135,6 +176,7 @@ pub struct ManipulatorPointInfo {
pub type OpposingHandleLengths = HashMap<LayerNodeIdentifier, HashMap<HandleId, f64>>;
#[derive(Clone)]
pub struct ClosestSegment {
layer: LayerNodeIdentifier,
segment: SegmentId,
@ -143,7 +185,6 @@ pub struct ClosestSegment {
colinear: [Option<HandleId>; 2],
t: f64,
bezier_point_to_viewport: DVec2,
stroke_width: f64,
}
impl ClosestSegment {
@ -159,10 +200,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);
@ -180,10 +235,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>) {
@ -200,7 +253,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));
@ -245,11 +298,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)
}
@ -274,10 +327,79 @@ 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
impl ShapeState {
pub fn is_selected_layer(&self, layer: LayerNodeIdentifier) -> bool {
self.selected_shape_state.contains_key(&layer)
}
pub fn is_point_ignored(&self, point: &ManipulatorPointId) -> bool {
(point.as_handle().is_some() && self.ignore_handles) || (point.as_anchor().is_some() && self.ignore_anchors)
}
@ -400,7 +522,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 };
@ -421,63 +543,82 @@ impl ShapeState {
/// Select/deselect the first point within the selection threshold.
/// Returns a tuple of the points if found and the offset, or `None` otherwise.
pub fn change_point_selection(&mut self, network_interface: &NodeNetworkInterface, mouse_position: DVec2, select_threshold: f64, extend_selection: bool) -> Option<Option<SelectedPointsInfo>> {
pub fn change_point_selection(
&mut self,
network_interface: &NodeNetworkInterface,
mouse_position: DVec2,
select_threshold: f64,
extend_selection: bool,
path_overlay_mode: PathOverlayMode,
frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>,
) -> Option<Option<SelectedPointsInfo>> {
if self.selected_shape_state.is_empty() {
return None;
}
if let Some((layer, manipulator_point_id)) = self.find_nearest_point_indices(network_interface, mouse_position, select_threshold) {
if let Some((layer, manipulator_point_id)) = self.find_nearest_visible_point_indices(network_interface, mouse_position, select_threshold, path_overlay_mode, frontier_handles_info) {
let vector_data = network_interface.compute_modified_vector(layer)?;
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
}
pub fn get_point_selection_state(&mut self, network_interface: &NodeNetworkInterface, mouse_position: DVec2, select_threshold: f64) -> Option<(bool, Option<SelectedPointsInfo>)> {
pub fn get_point_selection_state(
&mut self,
network_interface: &NodeNetworkInterface,
mouse_position: DVec2,
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)?;
// Check if point is visible under current overlay mode or not
let selected_segments = selected_segments(network_interface, self);
let selected_points = self.selected_points().cloned().collect::<HashSet<_>>();
if !is_visible_point(manipulator_point_id, &vector_data, path_overlay_mode, frontier_handles_info, selected_segments, &selected_points) {
return None;
}
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);
@ -508,7 +649,7 @@ impl ShapeState {
}
/// Selects all anchors connected to the selected subpath, and deselects all handles, for the given layer.
pub fn select_connected_anchors(&mut self, document: &DocumentMessageHandler, layer: LayerNodeIdentifier, mouse: DVec2) {
pub fn select_connected(&mut self, document: &DocumentMessageHandler, layer: LayerNodeIdentifier, mouse: DVec2, points: bool, segments: bool) {
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else {
return;
};
@ -526,18 +667,39 @@ impl ShapeState {
}
}
state.clear_points();
if selected_stack.is_empty() {
// Fall back on just selecting all points in the layer
for &point in vector_data.point_domain.ids() {
state.select_point(ManipulatorPointId::Anchor(point))
// Fall back on just selecting all points/segments in the layer
if points {
for &point in vector_data.point_domain.ids() {
state.select_point(ManipulatorPointId::Anchor(point));
}
}
} else {
// Select all connected points
while let Some(point) = selected_stack.pop() {
let anchor_point = ManipulatorPointId::Anchor(point);
if !state.is_selected(anchor_point) {
state.select_point(anchor_point);
selected_stack.extend(vector_data.connected_points(point));
if segments {
for &segment in vector_data.segment_domain.ids() {
state.select_segment(segment);
}
}
return;
}
let mut connected_points = HashSet::new();
while let Some(point) = selected_stack.pop() {
if !connected_points.contains(&point) {
connected_points.insert(point);
selected_stack.extend(vector_data.connected_points(point));
}
}
if points {
connected_points.iter().for_each(|point| state.select_point(ManipulatorPointId::Anchor(*point)));
}
if segments {
for (id, _, start, end) in vector_data.segment_bezier_iter() {
if connected_points.contains(&start) || connected_points.contains(&end) {
state.select_segment(id);
}
}
}
@ -576,6 +738,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;
@ -641,10 +810,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,
@ -671,7 +848,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;
}
@ -755,6 +932,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
@ -774,7 +954,7 @@ impl ShapeState {
// For a non-endpoint anchor, handles are perpendicular to the average tangent of adjacent segments.(Refer:https://github.com/GraphiteEditor/Graphite/pull/2620#issuecomment-2881501494)
let mut handle_direction = if segment_count > 1. {
segment_angle = segment_angle / segment_count;
segment_angle /= segment_count;
segment_angle += std::f64::consts::FRAC_PI_2;
DVec2::new(segment_angle.cos(), segment_angle.sin())
} else {
@ -796,12 +976,12 @@ 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
.to_manipulator_point()
.get_position(&vector_data)
.get_position(vector_data)
.and_then(|position| (position - anchor_position).try_normalize())
else {
return;
@ -917,9 +1097,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,
@ -945,7 +1125,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;
}
@ -960,7 +1150,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;
}
@ -979,7 +1169,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 };
@ -1030,12 +1220,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],
@ -1113,11 +1303,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);
}
@ -1162,6 +1356,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();
@ -1209,6 +1405,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 };
@ -1320,6 +1530,42 @@ impl ShapeState {
None
}
pub fn find_nearest_visible_point_indices(
&mut self,
network_interface: &NodeNetworkInterface,
mouse_position: DVec2,
select_threshold: f64,
path_overlay_mode: PathOverlayMode,
frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>,
) -> Option<(LayerNodeIdentifier, ManipulatorPointId)> {
if self.selected_shape_state.is_empty() {
return None;
}
let select_threshold_squared = select_threshold.powi(2);
// Find the closest control point among all elements of shapes_to_modify
for &layer in self.selected_shape_state.keys() {
if let Some((manipulator_point_id, distance_squared)) = Self::closest_point_in_layer(network_interface, layer, mouse_position) {
// Choose the first point under the threshold
if distance_squared < select_threshold_squared {
// Check if point is visible in current PathOverlayMode
let vector_data = network_interface.compute_modified_vector(layer)?;
let selected_segments = selected_segments(network_interface, self);
let selected_points = self.selected_points().cloned().collect::<HashSet<_>>();
if !is_visible_point(manipulator_point_id, &vector_data, path_overlay_mode, frontier_handles_info, selected_segments, &selected_points) {
return None;
}
return Some((layer, manipulator_point_id));
}
}
}
None
}
// TODO Use quadtree or some equivalent spatial acceleration structure to improve this to O(log(n))
/// Find the closest manipulator, manipulator point, and distance so we can select path elements.
/// Brute force comparison to determine which manipulator (handle or anchor) we want to select taking O(n) time.
@ -1385,11 +1631,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.) {
@ -1410,7 +1651,6 @@ impl ShapeState {
t,
bezier_point_to_viewport: screenspace,
layer,
stroke_width,
});
}
}
@ -1538,6 +1778,7 @@ impl ShapeState {
}
}
}
/// Converts a nearby clicked anchor point's handles between sharp (zero-length handles) and smooth (pulled-apart handle(s)).
/// If both handles aren't zero-length, they are set that. If both are zero-length, they are stretched apart by a reasonable amount.
/// This can can be activated by double clicking on an anchor with the Path tool.
@ -1567,45 +1808,51 @@ 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
if positions != 0 {
self.convert_manipulator_handles_to_colinear(&vector_data, id, responses, layer);
} else {
for handle in vector_data.all_connected(id) {
let Some(bezier) = vector_data.segment_from_id(handle.segment) else { continue };
for point in self.selected_points() {
let Some(point_id) = point.as_anchor() else { continue };
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) {
let Some(bezier) = vector_data.segment_from_id(handle.segment) else { continue };
match bezier.handles {
BezierHandles::Linear => {}
BezierHandles::Quadratic { .. } => {
let segment = handle.segment;
// Convert to linear
let modification_type = VectorModificationType::SetHandles { segment, handles: [None; 2] };
responses.add(GraphOperationMessage::Vector { layer, modification_type });
match bezier.handles {
BezierHandles::Linear => {}
BezierHandles::Quadratic { .. } => {
let segment = handle.segment;
// Convert to linear
let modification_type = VectorModificationType::SetHandles { segment, handles: [None; 2] };
responses.add(GraphOperationMessage::Vector { layer, modification_type });
// Set the manipulator to have non-colinear handles
for &handles in &vector_data.colinear_manipulators {
if handles.contains(&HandleId::primary(segment)) {
let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false };
responses.add(GraphOperationMessage::Vector { layer, modification_type });
// Set the manipulator to have non-colinear handles
for &handles in &vector_data.colinear_manipulators {
if handles.contains(&HandleId::primary(segment)) {
let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false };
responses.add(GraphOperationMessage::Vector { layer, modification_type });
}
}
}
}
BezierHandles::Cubic { .. } => {
// Set handle position to anchor position
let modification_type = handle.set_relative_position(DVec2::ZERO);
responses.add(GraphOperationMessage::Vector { layer, modification_type });
BezierHandles::Cubic { .. } => {
// Set handle position to anchor position
let modification_type = handle.set_relative_position(DVec2::ZERO);
responses.add(GraphOperationMessage::Vector { layer, modification_type });
// Set the manipulator to have non-colinear handles
for &handles in &vector_data.colinear_manipulators {
if handles.contains(&handle) {
let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false };
responses.add(GraphOperationMessage::Vector { layer, modification_type });
// Set the manipulator to have non-colinear handles
for &handles in &vector_data.colinear_manipulators {
if handles.contains(&handle) {
let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false };
responses.add(GraphOperationMessage::Vector { layer, modification_type });
}
}
}
}
}
}
};
};
}
Some(true)
};
@ -1619,10 +1866,25 @@ impl ShapeState {
false
}
pub fn select_all_in_shape(&mut self, network_interface: &NodeNetworkInterface, selection_shape: SelectionShape, selection_change: SelectionChange) {
#[allow(clippy::too_many_arguments)]
pub fn select_all_in_shape(
&mut self,
network_interface: &NodeNetworkInterface,
selection_shape: SelectionShape,
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);
@ -1648,7 +1910,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);
@ -1662,13 +1963,17 @@ impl ShapeState {
};
if select {
match selection_change {
SelectionChange::Shrink => state.deselect_point(id),
_ => {
// Select only the handles which are of nonzero length
if let Some(handle) = id.as_handle() {
if handle.length(&vector_data) > 0. {
state.select_point(id)
let is_visible_handle = is_visible_point(id, &vector_data, path_overlay_mode, frontier_handles_info.clone(), selected_segments.clone(), &selected_points);
if is_visible_handle {
match selection_change {
SelectionChange::Shrink => state.deselect_point(id),
_ => {
// Select only the handles which are of nonzero length
if let Some(handle) = id.as_handle() {
if handle.length(&vector_data) > 0. {
state.select_point(id)
}
}
}
}
@ -1677,6 +1982,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);

View file

@ -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);
}
}

View file

@ -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}");
}
}

View file

@ -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;

View file

@ -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,
});
}
}
}

View file

@ -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,
});
}
}
}

View file

@ -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.));
}
}

View file

@ -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,
});
}
}
}

View file

@ -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::*;
@ -114,13 +114,13 @@ fn get_closest_point(points: Vec<SnappedPoint>) -> Option<SnappedPoint> {
(Some(result), None) | (None, Some(result)) => Some(result),
(Some(mut result), Some(align)) => {
let SnapTarget::DistributeEvenly(distribution) = result.target else { return Some(result) };
if distribution.is_x() && align.alignment_target_x.is_some() {
if distribution.is_x() && align.alignment_target_horizontal.is_some() {
result.snapped_point_document.y = align.snapped_point_document.y;
result.alignment_target_x = align.alignment_target_x;
result.alignment_target_horizontal = align.alignment_target_horizontal;
}
if distribution.is_y() && align.alignment_target_y.is_some() {
if distribution.is_y() && align.alignment_target_vertical.is_some() {
result.snapped_point_document.x = align.snapped_point_document.x;
result.alignment_target_y = align.alignment_target_y;
result.alignment_target_vertical = align.alignment_target_vertical;
}
Some(result)
@ -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,17 +453,21 @@ 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);
}
let viewport = to_viewport.transform_point2(ind.snapped_point_document);
Self::alignment_x_overlay(&ind.distribution_boxes_x, to_viewport, overlay_context);
Self::alignment_y_overlay(&ind.distribution_boxes_y, to_viewport, overlay_context);
Self::alignment_x_overlay(&ind.distribution_boxes_horizontal, to_viewport, overlay_context);
Self::alignment_y_overlay(&ind.distribution_boxes_vertical, to_viewport, overlay_context);
let align = [ind.alignment_target_x, ind.alignment_target_y].map(|target| target.map(|target| to_viewport.transform_point2(target)));
let align = [ind.alignment_target_horizontal, ind.alignment_target_vertical].map(|target| target.map(|target| to_viewport.transform_point2(target)));
let any_align = align.iter().flatten().next().is_some();
for &target in align.iter().flatten() {
overlay_context.line(viewport, target, None, None);
@ -471,7 +479,7 @@ impl SnapManager {
overlay_context.manipulator_handle(viewport, false, None);
}
if !any_align && ind.distribution_equal_distance_x.is_none() && ind.distribution_equal_distance_y.is_none() {
if !any_align && ind.distribution_equal_distance_horizontal.is_none() && ind.distribution_equal_distance_vertical.is_none() {
let text = format!("[{}] from [{}]", ind.target, ind.source);
let transform = DAffine2::from_translation(viewport - DVec2::new(0., 4.));
overlay_context.text(&text, COLOR_OVERLAY_WHITE, Some(COLOR_OVERLAY_LABEL_BACKGROUND), transform, 4., [Pivot::Start, Pivot::End]);

View file

@ -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] {
@ -90,7 +90,7 @@ impl AlignmentSnapper {
distance_to_align_target,
fully_constrained: false,
at_intersection: true,
alignment_target_x: Some(endpoint),
alignment_target_horizontal: Some(endpoint),
..Default::default()
};
snap_results.points.push(snap_point);
@ -129,7 +129,7 @@ impl AlignmentSnapper {
distance: distance_to_snapped,
tolerance,
distance_to_align_target,
alignment_target_x: Some(target_position),
alignment_target_horizontal: Some(target_position),
fully_constrained: true,
at_intersection: matches!(constraint, SnapConstraint::Line { .. }),
..Default::default()
@ -148,7 +148,7 @@ impl AlignmentSnapper {
distance: distance_to_snapped,
tolerance,
distance_to_align_target,
alignment_target_y: Some(target_position),
alignment_target_vertical: Some(target_position),
fully_constrained: true,
at_intersection: matches!(constraint, SnapConstraint::Line { .. }),
..Default::default()
@ -174,8 +174,8 @@ impl AlignmentSnapper {
target_bounds: snap_x.target_bounds,
distance,
tolerance,
alignment_target_x: snap_x.alignment_target_x,
alignment_target_y: snap_y.alignment_target_y,
alignment_target_horizontal: snap_x.alignment_target_horizontal,
alignment_target_vertical: snap_y.alignment_target_vertical,
constrained: true,
at_intersection: true,
..Default::default()

View file

@ -1,9 +1,9 @@
use super::*;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::misc::*;
use crate::messages::prelude::*;
use glam::DVec2;
use graphene_core::renderer::Quad;
use graphene_std::renderer::Quad;
use std::collections::VecDeque;
#[derive(Clone, Debug, Default)]
pub struct DistributionSnapper {
@ -79,18 +79,21 @@ impl DistributionSnapper {
let screen_bounds = (document.metadata().document_to_viewport.inverse() * Quad::from_box([DVec2::ZERO, snap_data.input.viewport_bounds.size()])).bounding_box();
let max_extent = (screen_bounds[1] - screen_bounds[0]).abs().max_element();
// Collect artboard bounds
for layer in document.metadata().all_layers() {
if document.network_interface.is_artboard(&layer.to_node(), &[]) && !snap_data.ignore.contains(&layer) {
self.add_bounds(layer, snap_data, bbox_to_snap, max_extent);
}
}
// Collect alignment candidate bounds
for &layer in snap_data.alignment_candidates.map_or([].as_slice(), |candidates| candidates.as_slice()) {
if !snap_data.ignore_bounds(layer) {
self.add_bounds(layer, snap_data, bbox_to_snap, max_extent);
}
}
// Sort and merge intersecting rectangles
self.right.sort_unstable_by(|a, b| a.center().x.total_cmp(&b.center().x));
self.left.sort_unstable_by(|a, b| b.center().x.total_cmp(&a.center().x));
self.down.sort_unstable_by(|a, b| a.center().y.total_cmp(&b.center().y));
@ -184,20 +187,18 @@ impl DistributionSnapper {
fn snap_bbox_points(&self, tolerance: f64, point: &SnapCandidatePoint, snap_results: &mut SnapResults, constraint: SnapConstraint, bounds: Rect) {
let mut consider_x = true;
let mut consider_y = true;
if let SnapConstraint::Line { direction, .. } = constraint {
let direction = direction.normalize_or_zero();
if direction.x == 0. {
consider_x = false;
} else if direction.y == 0. {
consider_y = false;
}
consider_x = direction.x != 0.;
consider_y = direction.y != 0.;
}
let mut snap_x: Option<SnappedPoint> = None;
let mut snap_y: Option<SnappedPoint> = None;
self.x(consider_x, bounds, tolerance, &mut snap_x, point);
self.y(consider_y, bounds, tolerance, &mut snap_y, point);
self.horizontal_snap(consider_x, bounds, tolerance, &mut snap_x, point);
self.vertical_snap(consider_y, bounds, tolerance, &mut snap_y, point);
match (snap_x, snap_y) {
(Some(x), Some(y)) => {
@ -209,8 +210,8 @@ impl DistributionSnapper {
final_point.snapped_point_document += y.snapped_point_document - point.document_point;
final_point.source_bounds = Some(final_bounds.into());
final_point.target = SnapTarget::DistributeEvenly(DistributionSnapTarget::XY);
final_point.distribution_boxes_y = y.distribution_boxes_y;
final_point.distribution_equal_distance_y = y.distribution_equal_distance_y;
final_point.distribution_boxes_vertical = y.distribution_boxes_vertical;
final_point.distribution_equal_distance_vertical = y.distribution_equal_distance_vertical;
final_point.distance = (final_point.distance * final_point.distance + y.distance * y.distance).sqrt();
snap_results.points.push(final_point);
}
@ -220,42 +221,55 @@ impl DistributionSnapper {
}
}
fn x(&self, consider_x: bool, bounds: Rect, tolerance: f64, snap_x: &mut Option<SnappedPoint>, point: &SnapCandidatePoint) {
// Right
if consider_x && !self.right.is_empty() {
fn horizontal_snap(&self, consider_x: bool, bounds: Rect, tolerance: f64, snap_x: &mut Option<SnappedPoint>, point: &SnapCandidatePoint) {
if !consider_x {
return;
}
// Try right distribution first
if !self.right.is_empty() {
let (equal_dist, mut vec_right) = Self::top_level_matches(bounds, &self.right, tolerance, dist_right);
if let Some(distances) = equal_dist {
let translation = DVec2::X * (distances.first - distances.equal);
vec_right.push_front(bounds.translate(translation));
// Find matching left distribution
for &left in Self::exact_further_matches(bounds.translate(translation), &self.left, dist_left, distances.equal, 2).iter().skip(1) {
vec_right.push_front(left);
}
*snap_x = Some(SnappedPoint::distribute(point, DistributionSnapTarget::Right, vec_right, distances, bounds, translation, tolerance))
// Adjust bounds to maintain alignment
if vec_right.len() > 1 {
vec_right[0][0].y = vec_right[0][0].y.min(vec_right[1][1].y);
vec_right[0][1].y = vec_right[0][1].y.min(vec_right[1][1].y);
}
*snap_x = Some(SnappedPoint::distribute(point, DistributionSnapTarget::Right, vec_right, distances, bounds, translation, tolerance));
return;
}
}
// Left
if consider_x && !self.left.is_empty() && snap_x.is_none() {
// Try left distribution if right didn't work
if !self.left.is_empty() {
let (equal_dist, mut vec_left) = Self::top_level_matches(bounds, &self.left, tolerance, dist_left);
if let Some(distances) = equal_dist {
let translation = -DVec2::X * (distances.first - distances.equal);
vec_left.make_contiguous().reverse();
vec_left.push_back(bounds.translate(translation));
// Find matching right distribution
for &right in Self::exact_further_matches(bounds.translate(translation), &self.right, dist_right, distances.equal, 2).iter().skip(1) {
vec_left.push_back(right);
}
*snap_x = Some(SnappedPoint::distribute(point, DistributionSnapTarget::Left, vec_left, distances, bounds, translation, tolerance))
*snap_x = Some(SnappedPoint::distribute(point, DistributionSnapTarget::Left, vec_left, distances, bounds, translation, tolerance));
return;
}
}
// Center X
if consider_x && !self.left.is_empty() && !self.right.is_empty() && snap_x.is_none() {
// Try center distribution if both sides exist
if !self.left.is_empty() && !self.right.is_empty() {
let target_x = (self.right[0].min() + self.left[0].max()).x / 2.;
let offset = target_x - bounds.center().x;
if offset.abs() < tolerance {
@ -263,67 +277,93 @@ impl DistributionSnapper {
let equal = bounds.translate(translation).min().x - self.left[0].max().x;
let first = equal + offset;
let distances = DistributionMatch { first, equal };
let boxes = VecDeque::from([self.left[0], bounds.translate(translation), self.right[0]]);
*snap_x = Some(SnappedPoint::distribute(point, DistributionSnapTarget::X, boxes, distances, bounds, translation, tolerance))
let mut boxes = VecDeque::from([self.left[0], bounds.translate(translation), self.right[0]]);
// Adjust bounds to maintain alignment
if boxes.len() > 1 {
boxes[1][0].y = boxes[1][0].y.min(boxes[0][1].y);
boxes[1][1].y = boxes[1][1].y.min(boxes[0][1].y);
}
*snap_x = Some(SnappedPoint::distribute(point, DistributionSnapTarget::X, boxes, distances, bounds, translation, tolerance));
}
}
}
fn y(&self, consider_y: bool, bounds: Rect, tolerance: f64, snap_y: &mut Option<SnappedPoint>, point: &SnapCandidatePoint) {
// Down
if consider_y && !self.down.is_empty() {
fn vertical_snap(&self, consider_y: bool, bounds: Rect, tolerance: f64, snap_y: &mut Option<SnappedPoint>, point: &SnapCandidatePoint) {
if !consider_y {
return;
}
// Try down distribution first
if !self.down.is_empty() {
let (equal_dist, mut vec_down) = Self::top_level_matches(bounds, &self.down, tolerance, dist_down);
if let Some(distances) = equal_dist {
let translation = DVec2::Y * (distances.first - distances.equal);
vec_down.push_front(bounds.translate(translation));
// Find matching up distribution
for &up in Self::exact_further_matches(bounds.translate(translation), &self.up, dist_up, distances.equal, 2).iter().skip(1) {
vec_down.push_front(up);
}
*snap_y = Some(SnappedPoint::distribute(point, DistributionSnapTarget::Down, vec_down, distances, bounds, translation, tolerance))
// Adjust bounds to maintain alignment
if vec_down.len() > 1 {
vec_down[0][0].x = vec_down[0][0].x.min(vec_down[1][1].x);
vec_down[0][1].x = vec_down[0][1].x.min(vec_down[1][1].x);
}
*snap_y = Some(SnappedPoint::distribute(point, DistributionSnapTarget::Down, vec_down, distances, bounds, translation, tolerance));
return;
}
}
// Up
if consider_y && !self.up.is_empty() && snap_y.is_none() {
// Try up distribution if down didn't work
if !self.up.is_empty() {
let (equal_dist, mut vec_up) = Self::top_level_matches(bounds, &self.up, tolerance, dist_up);
if let Some(distances) = equal_dist {
let translation = -DVec2::Y * (distances.first - distances.equal);
vec_up.make_contiguous().reverse();
vec_up.push_back(bounds.translate(translation));
// Find matching down distribution
for &down in Self::exact_further_matches(bounds.translate(translation), &self.down, dist_down, distances.equal, 2).iter().skip(1) {
vec_up.push_back(down);
}
*snap_y = Some(SnappedPoint::distribute(point, DistributionSnapTarget::Up, vec_up, distances, bounds, translation, tolerance))
*snap_y = Some(SnappedPoint::distribute(point, DistributionSnapTarget::Up, vec_up, distances, bounds, translation, tolerance));
return;
}
}
// Center Y
if consider_y && !self.up.is_empty() && !self.down.is_empty() && snap_y.is_none() {
// Try center distribution if both sides exist
if !self.up.is_empty() && !self.down.is_empty() {
let target_y = (self.down[0].min() + self.up[0].max()).y / 2.;
let offset = target_y - bounds.center().y;
if offset.abs() < tolerance {
let translation = DVec2::Y * offset;
let equal = bounds.translate(translation).min().y - self.up[0].max().y;
let first = equal + offset;
let distances = DistributionMatch { first, equal };
let boxes = VecDeque::from([self.up[0], bounds.translate(translation), self.down[0]]);
let mut boxes = VecDeque::from([self.up[0], bounds.translate(translation), self.down[0]]);
*snap_y = Some(SnappedPoint::distribute(point, DistributionSnapTarget::Y, boxes, distances, bounds, translation, tolerance))
// Adjust bounds to maintain alignment
if boxes.len() > 1 {
boxes[1][0].x = boxes[1][0].x.min(boxes[0][1].x);
boxes[1][1].x = boxes[1][1].x.min(boxes[0][1].x);
}
*snap_y = Some(SnappedPoint::distribute(point, DistributionSnapTarget::Y, boxes, distances, bounds, translation, tolerance));
}
}
}
pub fn free_snap(&mut self, snap_data: &mut SnapData, point: &SnapCandidatePoint, snap_results: &mut SnapResults, config: SnapTypeConfiguration) {
let Some(bounds) = config.bbox else { return };
if point.source != SnapSource::BoundingBox(BoundingBoxSnapSource::CenterPoint) || !snap_data.document.snapping_state.bounding_box.distribute_evenly {
if !snap_data.document.snapping_state.bounding_box.distribute_evenly {
return;
}
@ -333,7 +373,7 @@ impl DistributionSnapper {
pub fn constrained_snap(&mut self, snap_data: &mut SnapData, point: &SnapCandidatePoint, snap_results: &mut SnapResults, constraint: SnapConstraint, config: SnapTypeConfiguration) {
let Some(bounds) = config.bbox else { return };
if point.source != SnapSource::BoundingBox(BoundingBoxSnapSource::CenterPoint) || !snap_data.document.snapping_state.bounding_box.distribute_evenly {
if !snap_data.document.snapping_state.bounding_box.distribute_evenly {
return;
}
@ -342,267 +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_x, Some(6.));
assert_eq!(snap_results.points[0].distribution_boxes_x.len(), 3);
assert_eq!(snap_results.points[0].distribution_boxes_x[0], Rect::from_square(DVec2::new(0., 0.), 2.));
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_x, 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_x, Some(6.));
assert_eq!(snap_results.points[0].distribution_boxes_x.len(), 5);
assert_eq!(snap_results.points[0].distribution_boxes_x[1], Rect::from_square(DVec2::new(-10., 0.), 2.));
assert_eq!(snap_results.points[0].distribution_boxes_x[2], Rect::from_square(DVec2::new(0., 0.), 2.));
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_x, 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_x, Some(6.));
assert_eq!(snap_results.points[0].distribution_boxes_x.len(), 3);
assert_eq!(snap_results.points[0].distribution_boxes_x[2], Rect::from_square(DVec2::new(0., 0.), 2.));
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_x, 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_x, Some(6.));
assert_eq!(snap_results.points[0].distribution_boxes_x.len(), 4);
assert_eq!(snap_results.points[0].distribution_boxes_x[2], Rect::from_square(DVec2::new(0., 0.), 2.));
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_x, 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_x, Some(6.));
assert_eq!(snap_results.points[0].distribution_boxes_x.len(), 3);
assert_eq!(snap_results.points[0].distribution_boxes_x[1], Rect::from_square(DVec2::new(0., 0.), 2.));
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_x, 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_y, Some(6.));
assert_eq!(snap_results.points[0].distribution_boxes_y.len(), 3);
assert_eq!(snap_results.points[0].distribution_boxes_y[0], Rect::from_square(DVec2::new(0., 0.), 2.));
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_y, 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_y, Some(6.));
assert_eq!(snap_results.points[0].distribution_boxes_y.len(), 5);
assert_eq!(snap_results.points[0].distribution_boxes_y[1], Rect::from_square(DVec2::new(0., -10.), 2.));
assert_eq!(snap_results.points[0].distribution_boxes_y[2], Rect::from_square(DVec2::new(0., 0.), 2.));
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_y, 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_y, Some(6.));
assert_eq!(snap_results.points[0].distribution_boxes_y.len(), 3);
assert_eq!(snap_results.points[0].distribution_boxes_y[2], Rect::from_square(DVec2::new(0., 0.), 2.));
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_y, 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_y, Some(6.));
assert_eq!(snap_results.points[0].distribution_boxes_y.len(), 4);
assert_eq!(snap_results.points[0].distribution_boxes_y[2], Rect::from_square(DVec2::new(0., 0.), 2.));
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_y, 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_y, Some(6.));
assert_eq!(snap_results.points[0].distribution_boxes_y.len(), 3);
assert_eq!(snap_results.points[0].distribution_boxes_y[1], Rect::from_square(DVec2::new(0., 0.), 2.));
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_y, 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(),
};
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_x, Some(8.));
assert_eq!(snap_results.points[0].distribution_equal_distance_y, Some(6.));
assert_eq!(snap_results.points[0].distribution_boxes_x.len(), 3);
assert_eq!(snap_results.points[0].distribution_boxes_y.len(), 3);
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_x, 0);
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_y, 1);
}

View file

@ -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,

View file

@ -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 {

View file

@ -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)]
@ -29,17 +29,17 @@ pub struct SnappedPoint {
pub outline_layers: [Option<LayerNodeIdentifier>; 2],
pub distance: f64,
pub tolerance: f64,
pub distribution_boxes_x: VecDeque<Rect>,
pub distribution_equal_distance_x: Option<f64>,
pub distribution_boxes_y: VecDeque<Rect>,
pub distribution_equal_distance_y: Option<f64>,
pub distribution_boxes_horizontal: VecDeque<Rect>,
pub distribution_equal_distance_horizontal: Option<f64>,
pub distribution_boxes_vertical: VecDeque<Rect>,
pub distribution_equal_distance_vertical: Option<f64>,
pub distance_to_align_target: f64, // If aligning so that the top is aligned but the X pos is 200 from the target, this is 200.
pub alignment_target_x: Option<DVec2>,
pub alignment_target_y: Option<DVec2>,
pub alignment_target_horizontal: Option<DVec2>,
pub alignment_target_vertical: Option<DVec2>,
}
impl SnappedPoint {
pub fn align(&self) -> bool {
self.alignment_target_x.is_some() || self.alignment_target_y.is_some()
self.alignment_target_horizontal.is_some() || self.alignment_target_vertical.is_some()
}
pub fn infinite_snap(snapped_point_document: DVec2) -> Self {
Self {
@ -58,15 +58,15 @@ impl SnappedPoint {
pub fn distribute(point: &SnapCandidatePoint, target: DistributionSnapTarget, boxes: VecDeque<Rect>, distances: DistributionMatch, bounds: Rect, translation: DVec2, tolerance: f64) -> Self {
let is_x = target.is_x();
let [distribution_boxes_x, distribution_boxes_y] = if is_x { [boxes, Default::default()] } else { [Default::default(), boxes] };
let [distribution_boxes_horizontal, distribution_boxes_vertical] = if is_x { [boxes, Default::default()] } else { [Default::default(), boxes] };
Self {
snapped_point_document: point.document_point + translation,
source: point.source,
target: SnapTarget::DistributeEvenly(target),
distribution_boxes_x,
distribution_equal_distance_x: is_x.then_some(distances.equal),
distribution_boxes_y,
distribution_equal_distance_y: (!is_x).then_some(distances.equal),
distribution_boxes_horizontal,
distribution_equal_distance_horizontal: is_x.then_some(distances.equal),
distribution_boxes_vertical,
distribution_equal_distance_vertical: (!is_x).then_some(distances.equal),
distance: (distances.first - distances.equal).abs(),
constrained: true,
source_bounds: Some(bounds.translate(translation).into()),

View file

@ -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);
}
}
#[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);
#[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);
}
}
}

View file

@ -1,10 +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 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::common_functionality::transformation_cage::SelectedEdges;
use crate::messages::tool::tool_messages::path_tool::PathOverlayMode;
use crate::messages::tool::utility_types::ToolType;
use bezier_rs::{Bezier, BezierHandles};
use glam::{DAffine2, DVec2};
use graphene_std::renderer::Quad;
use graphene_std::text::{FontCache, load_font};
use graphene_std::vector::{HandleExt, HandleId, ManipulatorPointId, PointId, SegmentId, VectorData, VectorModificationType};
use kurbo::{CubicBez, Line, ParamCurveExtrema, PathSeg, Point, QuadBez};
/// Determines if a path should be extended. Goal in viewport space. Returns the path and if it is extending from the start, if applicable.
pub fn should_extend(
@ -61,13 +70,13 @@ pub fn text_bounding_box(layer: LayerNodeIdentifier, document: &DocumentMessageH
return Quad::from_box([DVec2::ZERO, DVec2::ZERO]);
};
let buzz_face = font_cache.get(font).map(|data| load_face(data));
let far = graphene_core::text::bounding_box(text, buzz_face.as_ref(), typesetting, false);
let font_data = font_cache.get(font).map(|data| load_font(data));
let far = graphene_std::text::bounding_box(text, font_data, typesetting, false);
Quad::from_box([DVec2::ZERO, far])
}
pub fn calculate_segment_angle(anchor: PointId, segment: SegmentId, vector_data: &VectorData, pen_tool: bool) -> Option<f64> {
pub fn calculate_segment_angle(anchor: PointId, segment: SegmentId, vector_data: &VectorData, prefer_handle_direction: bool) -> Option<f64> {
let is_start = |point: PointId, segment: SegmentId| vector_data.segment_start_from_id(segment) == Some(point);
let anchor_position = vector_data.point_domain.position_from_id(anchor)?;
let end_handle = ManipulatorPointId::EndHandle(segment).get_position(vector_data);
@ -81,15 +90,501 @@ pub fn calculate_segment_angle(anchor: PointId, segment: SegmentId, vector_data:
let required_handle = if is_start(anchor, segment) {
start_handle
.filter(|&handle| pen_tool && handle != anchor_position)
.filter(|&handle| prefer_handle_direction && handle != anchor_position)
.or(end_handle.filter(|&handle| Some(handle) != start_point))
.or(start_point)
} else {
end_handle
.filter(|&handle| pen_tool && handle != anchor_position)
.filter(|&handle| prefer_handle_direction && handle != anchor_position)
.or(start_handle.filter(|&handle| Some(handle) != start_point))
.or(start_point)
};
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,
vector_data: &VectorData,
path_overlay_mode: PathOverlayMode,
frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>,
selected_segments: Vec<SegmentId>,
selected_points: &HashSet<ManipulatorPointId>,
) -> bool {
match manipulator_point_id {
ManipulatorPointId::Anchor(_) => true,
ManipulatorPointId::EndHandle(segment_id) | ManipulatorPointId::PrimaryHandle(segment_id) => {
match (path_overlay_mode, selected_points.len() == 1) {
(PathOverlayMode::AllHandles, _) => true,
(PathOverlayMode::SelectedPointHandles, _) | (PathOverlayMode::FrontierHandles, true) => {
if selected_segments.contains(&segment_id) {
return true;
}
// Either the segment is a part of selected segments or the opposite handle is a part of existing selection
let Some(handle_pair) = manipulator_point_id.get_handle_pair(vector_data) else { return false };
let other_handle = handle_pair[1].to_manipulator_point();
// Return whether the list of selected points contain the other handle
selected_points.contains(&other_handle)
}
(PathOverlayMode::FrontierHandles, false) => {
let Some(anchor) = manipulator_point_id.get_anchor(vector_data) else {
warn!("No anchor for selected handle");
return false;
};
let Some(frontier_handles) = &frontier_handles_info else {
warn!("No frontier handles info provided");
return false;
};
frontier_handles.get(&segment_id).map(|anchors| anchors.contains(&anchor)).unwrap_or_default()
}
}
}
}
}
/// 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)
}

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