resolved conflicts

This commit is contained in:
0SlowPoke0 2025-08-20 12:45:53 +05:30
parent 3603426aee
commit 1338310d9f
256 changed files with 5951 additions and 20321 deletions

View file

@ -63,14 +63,14 @@ jobs:
mkdir artifacts mkdir artifacts
mv hierarchical_message_system_tree.txt artifacts/hierarchical_message_system_tree.txt mv hierarchical_message_system_tree.txt artifacts/hierarchical_message_system_tree.txt
- name: 🚚 Move `artifacts` contents to `website/other/editor-structure` - name: 🚚 Move `artifacts` contents to the project root
run: | run: |
mv artifacts/* website/other/editor-structure mv artifacts/* .
- name: 🔧 Build auto-generated code docs artifacts into HTML - name: 🔧 Build auto-generated code docs artifacts into HTML
run: | run: |
cd website/other/editor-structure cd website
node generate.js hierarchical_message_system_tree.txt replacement.html npm run generate-editor-structure
- name: 🌐 Build Graphite website with Zola - name: 🌐 Build Graphite website with Zola
env: env:
@ -80,38 +80,6 @@ jobs:
npm run install-fonts npm run install-fonts
zola --config config.toml build --minify zola --config config.toml build --minify
- name: 💿 Restore cache of `website/other/dist` directory, if available and `website/other` didn't change
if: steps.changes.outputs.website-other != 'true'
id: cache-website-other-dist
uses: actions/cache/restore@v3
with:
path: website/other/dist
key: website-other-dist-${{ runner.os }}
- name: 🟢 Set up Node only if we are going to build in the next step
if: steps.cache-website-other-dist.outputs.cache-hit != 'true'
uses: actions/setup-node@v4
with:
node-version: "latest"
- name: 📁 Build `website/other` directory only if changed or not cached
if: steps.cache-website-other-dist.outputs.cache-hit != 'true'
id: build-website-other
run: |
sh website/other/build.sh
- name: 💾 Save cache of `website/other/dist` directory if it was built above
if: steps.cache-website-other-dist.outputs.cache-hit != 'true'
uses: actions/cache/save@v3
with:
path: website/other/dist
key: ${{ steps.cache-website-other-dist.outputs.cache-primary-key }}
- name: 🚚 Move `website/other/dist` contents to `website/public`
run: |
mkdir -p website/public
mv website/other/dist/* website/public
- name: 📤 Publish to Cloudflare Pages - name: 📤 Publish to Cloudflare Pages
id: cloudflare id: cloudflare
uses: cloudflare/pages-action@1 uses: cloudflare/pages-action@1

1
.gitignore vendored
View file

@ -8,3 +8,4 @@ flamegraph.svg
.idea/ .idea/
.direnv .direnv
hierarchical_message_system_tree.txt hierarchical_message_system_tree.txt
hierarchical_message_system_tree.html

View file

@ -33,17 +33,17 @@
pkgs = import nixpkgs { pkgs = import nixpkgs {
inherit system overlays; inherit system overlays;
}; };
rustc-wasm = pkgs.rust-bin.stable.latest.default.override { rustc-wasm = pkgs.rust-bin.stable.latest.default.override {
targets = [ "wasm32-unknown-unknown" ]; targets = [ "wasm32-unknown-unknown" ];
extensions = [ "rust-src" "rust-analyzer" "clippy" "cargo" ]; extensions = [ "rust-src" "rust-analyzer" "clippy" "cargo" ];
}; };
libcef = pkgs.libcef.overrideAttrs (finalAttrs: previousAttrs: { libcef = pkgs.libcef.overrideAttrs (finalAttrs: previousAttrs: {
version = "138.0.26"; version = "139.0.17";
gitRevision = "84f2d27"; gitRevision = "6c347eb";
chromiumVersion = "138.0.7204.158"; chromiumVersion = "139.0.7258.31";
srcHash = "sha256-d9jQJX7rgdoHfROD3zmOdMSesRdKE3slB5ZV+U2wlbQ="; srcHash = "sha256-kRMO8DP4El1qytDsAZBdHvR9AAHXce90nPdyfJailBg=";
__intentionallyOverridingVersion = true; __intentionallyOverridingVersion = true;
@ -75,6 +75,12 @@
vulkan-loader vulkan-loader
libraw libraw
libGL libGL
# X11 libraries, not needed on wayland! Remove when x11 is finally dead
libxkbcommon
xorg.libXcursor
xorg.libxcb
xorg.libX11
]; ];
# Development tools that don't need to be in LD_LIBRARY_PATH # Development tools that don't need to be in LD_LIBRARY_PATH

View file

@ -35,7 +35,7 @@
"rust-analyzer.cargo.allTargets": false, "rust-analyzer.cargo.allTargets": false,
// ESLint config // ESLint config
"eslint.format.enable": true, "eslint.format.enable": true,
"eslint.workingDirectories": ["./frontend", "./website/other/bezier-rs-demos", "./website"], "eslint.workingDirectories": ["./frontend", "./website"],
"eslint.validate": ["javascript", "typescript", "svelte"], "eslint.validate": ["javascript", "typescript", "svelte"],
// Svelte config // Svelte config
"svelte.plugin.svelte.compilerWarnings": { "svelte.plugin.svelte.compilerWarnings": {

60
Cargo.lock generated
View file

@ -461,27 +461,6 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bezier-rs"
version = "0.4.0"
dependencies = [
"dyn-any",
"glam",
"kurbo",
"serde",
]
[[package]]
name = "bezier-rs-wasm"
version = "0.0.0"
dependencies = [
"bezier-rs",
"glam",
"js-sys",
"log",
"wasm-bindgen",
]
[[package]] [[package]]
name = "bincode" name = "bincode"
version = "1.3.3" version = "1.3.3"
@ -683,19 +662,20 @@ dependencies = [
[[package]] [[package]]
name = "cef" name = "cef"
version = "138.5.0+138.0.26" version = "139.0.1+139.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bfa138b538b29584b02e990a631ef44261d9a70d35a77ce4ef624410046fe91" checksum = "39b749cfc4124f9505b3fbe32279c0e93f30831f1ecf3c2cf85863179319cd7b"
dependencies = [ dependencies = [
"cef-dll-sys", "cef-dll-sys",
"libloading",
"windows-sys 0.60.2", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
name = "cef-dll-sys" name = "cef-dll-sys"
version = "138.5.0+138.0.26" version = "139.0.1+139.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "658450855d0ef25af50420b95fbdfacb5df17c7eb20a1615ec995470c26ed643" checksum = "fc42adb0adc477860b705e967d9f899be05ba3775fe4d6dc4d844a1391ffa3dd"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"cmake", "cmake",
@ -1883,7 +1863,6 @@ dependencies = [
name = "graph-craft" name = "graph-craft"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"bezier-rs",
"criterion", "criterion",
"dyn-any", "dyn-any",
"glam", "glam",
@ -1961,7 +1940,6 @@ name = "graphene-core"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"bezier-rs",
"bytemuck", "bytemuck",
"ctor", "ctor",
"dyn-any", "dyn-any",
@ -1974,6 +1952,7 @@ dependencies = [
"num-traits", "num-traits",
"parley", "parley",
"petgraph 0.7.1", "petgraph 0.7.1",
"poly-cool",
"rand 0.9.1", "rand 0.9.1",
"rand_chacha 0.9.0", "rand_chacha 0.9.0",
"rustc-hash 2.1.1", "rustc-hash 2.1.1",
@ -2018,7 +1997,6 @@ dependencies = [
name = "graphene-path-bool" name = "graphene-path-bool"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"bezier-rs",
"dyn-any", "dyn-any",
"glam", "glam",
"graphene-core", "graphene-core",
@ -2033,7 +2011,6 @@ dependencies = [
name = "graphene-raster-nodes" name = "graphene-raster-nodes"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"bezier-rs",
"bytemuck", "bytemuck",
"dyn-any", "dyn-any",
"fastnoise-lite", "fastnoise-lite",
@ -2042,6 +2019,7 @@ dependencies = [
"graphene-core", "graphene-core",
"graphene-core-shaders", "graphene-core-shaders",
"image", "image",
"kurbo",
"ndarray", "ndarray",
"node-macro", "node-macro",
"rand 0.9.1", "rand 0.9.1",
@ -2056,10 +2034,7 @@ name = "graphene-std"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"bytemuck",
"dyn-any", "dyn-any",
"fastnoise-lite",
"futures",
"glam", "glam",
"graph-craft", "graph-craft",
"graphene-application-io", "graphene-application-io",
@ -2071,10 +2046,7 @@ dependencies = [
"graphene-svg-renderer", "graphene-svg-renderer",
"image", "image",
"log", "log",
"ndarray",
"node-macro", "node-macro",
"rand 0.9.1",
"rand_chacha 0.9.0",
"reqwest", "reqwest",
"tokio", "tokio",
"vello", "vello",
@ -2089,10 +2061,10 @@ name = "graphene-svg-renderer"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"bezier-rs",
"dyn-any", "dyn-any",
"glam", "glam",
"graphene-core", "graphene-core",
"kurbo",
"log", "log",
"num-traits", "num-traits",
"serde", "serde",
@ -2113,6 +2085,7 @@ dependencies = [
"graph-craft", "graph-craft",
"graphene-std", "graphene-std",
"graphite-editor", "graphite-editor",
"image",
"include_dir", "include_dir",
"open", "open",
"rfd", "rfd",
@ -2130,9 +2103,8 @@ dependencies = [
name = "graphite-editor" name = "graphite-editor"
version = "0.0.0" version = "0.0.0"
dependencies = [ dependencies = [
"bezier-rs", "base64 0.22.1",
"bitflags 2.9.1", "bitflags 2.9.1",
"bytemuck",
"derivative", "derivative",
"dyn-any", "dyn-any",
"env_logger", "env_logger",
@ -2148,18 +2120,15 @@ dependencies = [
"num_enum", "num_enum",
"once_cell", "once_cell",
"preprocessor", "preprocessor",
"ron",
"serde", "serde",
"serde_json", "serde_json",
"specta", "specta",
"spin", "spin",
"thiserror 2.0.12", "thiserror 2.0.12",
"tokio", "tokio",
"tracing",
"usvg", "usvg",
"vello", "vello",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures",
"web-sys", "web-sys",
"wgpu-executor", "wgpu-executor",
] ]
@ -4040,6 +4009,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
[[package]]
name = "poly-cool"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6113ca52ade3ae52044cf0519e2c78ffa82120f6fa82f5099c8a4fd3ec8de43"
dependencies = [
"arrayvec",
]
[[package]] [[package]]
name = "portable-atomic" name = "portable-atomic"
version = "1.11.1" version = "1.11.1"

View file

@ -20,9 +20,7 @@ members = [
"node-graph/preprocessor", "node-graph/preprocessor",
"libraries/dyn-any", "libraries/dyn-any",
"libraries/path-bool", "libraries/path-bool",
"libraries/bezier-rs",
"libraries/math-parser", "libraries/math-parser",
"website/other/bezier-rs-demos/wasm",
] ]
default-members = [ default-members = [
"editor", "editor",
@ -44,9 +42,14 @@ resolver = "2"
[workspace.dependencies] [workspace.dependencies]
# Local dependencies # Local dependencies
bezier-rs = { path = "libraries/bezier-rs", features = ["dyn-any", "serde"] } dyn-any = { path = "libraries/dyn-any", features = [
dyn-any = { path = "libraries/dyn-any", features = ["derive", "glam", "reqwest", "log-bad-types", "rc"] } "derive",
preprocessor = { path = "node-graph/preprocessor"} "glam",
"reqwest",
"log-bad-types",
"rc",
] }
preprocessor = { path = "node-graph/preprocessor" }
math-parser = { path = "libraries/math-parser" } math-parser = { path = "libraries/math-parser" }
path-bool = { path = "libraries/path-bool" } path-bool = { path = "libraries/path-bool" }
graphene-application-io = { path = "node-graph/gapplication-io" } graphene-application-io = { path = "node-graph/gapplication-io" }
@ -80,9 +83,8 @@ convert_case = "0.7"
derivative = "2.2" derivative = "2.2"
thiserror = "2" thiserror = "2"
anyhow = "1.0" anyhow = "1.0"
proc-macro2 = { version = "1", features = [ "span-locations" ] } proc-macro2 = { version = "1", features = ["span-locations"] }
quote = "1.0" quote = "1.0"
axum = "0.8"
chrono = "0.4" chrono = "0.4"
ron = "0.8" ron = "0.8"
fastnoise-lite = "1.1" fastnoise-lite = "1.1"
@ -122,9 +124,17 @@ resvg = "0.44"
usvg = "0.44" usvg = "0.44"
rand = { version = "0.9", default-features = false, features = ["std_rng"] } rand = { version = "0.9", default-features = false, features = ["std_rng"] }
rand_chacha = "0.9" rand_chacha = "0.9"
glam = { version = "0.29", default-features = false, features = ["serde", "scalar-math", "debug-glam-assert"] } glam = { version = "0.29", default-features = false, features = [
"serde",
"scalar-math",
"debug-glam-assert",
] }
base64 = "0.22" base64 = "0.22"
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp"] } image = { version = "0.25", default-features = false, features = [
"png",
"jpeg",
"bmp",
] }
parley = "0.5.0" parley = "0.5.0"
skrifa = "0.32.0" skrifa = "0.32.0"
pretty_assertions = "1.4.1" pretty_assertions = "1.4.1"
@ -159,12 +169,13 @@ iai-callgrind = { version = "0.12.3" }
ndarray = "0.16.1" ndarray = "0.16.1"
strum = { version = "0.26.3", features = ["derive"] } strum = { version = "0.26.3", features = ["derive"] }
dirs = "6.0" dirs = "6.0"
cef = "138.5.0" cef = "139.0.1"
include_dir = "0.7.4" include_dir = "0.7.4"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
tracing = "0.1.41" tracing = "0.1.41"
rfd = "0.15.4" rfd = "0.15.4"
open = "5.3.2" open = "5.3.2"
poly-cool = "0.2.0"
[profile.dev] [profile.dev]
opt-level = 1 opt-level = 1

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

@ -39,3 +39,4 @@ vello = { workspace = true }
derivative = { workspace = true } derivative = { workspace = true }
rfd = { workspace = true } rfd = { workspace = true }
open = { workspace = true } open = { workspace = true }
image = { workspace = true }

View file

@ -7,8 +7,12 @@ use crate::dialogs::dialog_save_graphite_file;
use crate::render::GraphicsState; use crate::render::GraphicsState;
use crate::render::WgpuContext; use crate::render::WgpuContext;
use graph_craft::wasm_application_io::WasmApplicationIo; use graph_craft::wasm_application_io::WasmApplicationIo;
use graphene_std::Color;
use graphene_std::raster::Image;
use graphite_editor::application::Editor; use graphite_editor::application::Editor;
use graphite_editor::consts::DEFAULT_DOCUMENT_NAME;
use graphite_editor::messages::prelude::*; use graphite_editor::messages::prelude::*;
use std::fs;
use std::sync::Arc; use std::sync::Arc;
use std::sync::mpsc::Sender; use std::sync::mpsc::Sender;
use std::thread; use std::thread;
@ -57,8 +61,8 @@ impl WinitApp {
} }
fn send_messages_to_editor(&mut self, mut responses: Vec<FrontendMessage>) { fn send_messages_to_editor(&mut self, mut responses: Vec<FrontendMessage>) {
for message in responses.extract_if(.., |m| matches!(m, FrontendMessage::RenderOverlays(_))) { for message in responses.extract_if(.., |m| matches!(m, FrontendMessage::RenderOverlays { .. })) {
let FrontendMessage::RenderOverlays(overlay_context) = message else { unreachable!() }; let FrontendMessage::RenderOverlays { context: overlay_context } = message else { unreachable!() };
if let Some(graphics_state) = &mut self.graphics_state { if let Some(graphics_state) = &mut self.graphics_state {
let scene = overlay_context.take_scene(); let scene = overlay_context.take_scene();
graphics_state.set_overlays_scene(scene); graphics_state.set_overlays_scene(scene);
@ -75,7 +79,7 @@ impl WinitApp {
String::new() String::new()
}); });
let message = PortfolioMessage::OpenDocumentFile { let message = PortfolioMessage::OpenDocumentFile {
document_name: path.file_name().and_then(|s| s.to_str()).unwrap_or("unknown").to_string(), document_name: path.file_stem().and_then(|s| s.to_str()).unwrap_or("unknown").to_string(),
document_serialized_content: content, document_serialized_content: content,
}; };
let _ = event_loop_proxy.send_event(CustomEvent::DispatchMessage(message.into())); let _ = event_loop_proxy.send_event(CustomEvent::DispatchMessage(message.into()));
@ -264,6 +268,75 @@ impl ApplicationHandler<CustomEvent> for WinitApp {
let Some(event) = self.cef_context.handle_window_event(event) else { return }; let Some(event) = self.cef_context.handle_window_event(event) else { return };
match event { match event {
// Currently not supported on wayland see https://github.com/rust-windowing/winit/issues/1881
WindowEvent::DroppedFile(path) => {
let name = path.file_stem().and_then(|s| s.to_str()).map(|s| s.to_string());
let Some(extension) = path.extension().and_then(|s| s.to_str()) else {
tracing::warn!("Unsupported file dropped: {}", path.display());
// Fine to early return since we don't need to do cef work in this case
return;
};
let load_string = |path: &std::path::PathBuf| {
let Ok(content) = fs::read_to_string(path) else {
tracing::error!("Failed to read file: {}", path.display());
return None;
};
if content.is_empty() {
tracing::warn!("Dropped file is empty: {}", path.display());
return None;
}
Some(content)
};
// TODO: Consider moving this logic to the editor so we have one message to load data which is then demultiplexed in the portfolio message handler
match extension {
"graphite" => {
let Some(content) = load_string(&path) else { return };
let message = PortfolioMessage::OpenDocumentFile {
document_name: name.unwrap_or(DEFAULT_DOCUMENT_NAME.to_string()),
document_serialized_content: content,
};
self.dispatch_message(message.into());
}
"svg" => {
let Some(content) = load_string(&path) else { return };
let message = PortfolioMessage::PasteSvg {
name: path.file_stem().map(|s| s.to_string_lossy().to_string()),
svg: content,
mouse: None,
parent_and_insert_index: None,
};
self.dispatch_message(message.into());
}
_ => match image::ImageReader::open(&path) {
Ok(reader) => match reader.decode() {
Ok(image) => {
let width = image.width();
let height = image.height();
// TODO: support loading images with more than 8 bits per channel
let image_data = image.to_rgba8();
let image = Image::<Color>::from_image_data(image_data.as_raw(), width, height);
let message = PortfolioMessage::PasteImage {
name,
image,
mouse: None,
parent_and_insert_index: None,
};
self.dispatch_message(message.into());
}
Err(e) => {
tracing::error!("Failed to decode image: {}: {}", path.display(), e);
}
},
Err(e) => {
tracing::error!("Failed to open image file: {}: {}", path.display(), e);
}
},
}
}
WindowEvent::CloseRequested => { WindowEvent::CloseRequested => {
tracing::info!("The close button was pressed; stopping"); tracing::info!("The close button was pressed; stopping");
event_loop.exit(); event_loop.exit();

View file

@ -44,34 +44,34 @@ var s_diffuse: sampler;
@fragment @fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> { fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let ui = textureSample(t_ui, s_diffuse, in.tex_coords); let ui_linear = textureSample(t_ui, s_diffuse, in.tex_coords);
if (ui.a >= 0.999) { if (ui_linear.a >= 0.999) {
return ui; return ui_linear;
} }
let viewport_coordinate = (in.tex_coords - constants.viewport_offset) * constants.viewport_scale; let viewport_coordinate = (in.tex_coords - constants.viewport_offset) * constants.viewport_scale;
// Vello renders its values to an `RgbaUnorm` texture, but if we try to use this in the main rendering pipeline let overlay_srgb = textureSample(t_overlays, s_diffuse, viewport_coordinate);
// which renders to an `Srgb` surface, gamma mapping is applied twice. This converts back to linear to compensate. let viewport_srgb = textureSample(t_viewport, s_diffuse, viewport_coordinate);
let overlay_raw = textureSample(t_overlays, s_diffuse, viewport_coordinate);
let overlay = vec4<f32>(srgb_to_linear(overlay_raw.rgb), overlay_raw.a);
let viewport_raw = textureSample(t_viewport, s_diffuse, viewport_coordinate);
let viewport = vec4<f32>(srgb_to_linear(viewport_raw.rgb), viewport_raw.a);
if (overlay.a < 0.001) { // UI texture is premultiplied, we need to unpremultiply before blending
return blend(ui, viewport); let ui_srgb = linear_to_srgb(unpremultiply(ui_linear));
if (overlay_srgb.a < 0.001) {
if (ui_srgb.a < 0.001) {
return srgb_to_linear(viewport_srgb);
} else {
return srgb_to_linear(blend(ui_srgb, viewport_srgb));
}
} }
let composite = blend(overlay, viewport); let composite_linear = blend(srgb_to_linear(overlay_srgb), srgb_to_linear(viewport_srgb));
return blend(ui, composite);
}
fn srgb_to_linear(srgb: vec3<f32>) -> vec3<f32> { if (ui_srgb.a < 0.001) {
return select( return composite_linear;
pow((srgb + 0.055) / 1.055, vec3<f32>(2.4)), }
srgb / 12.92,
srgb <= vec3<f32>(0.04045) return srgb_to_linear(blend(ui_srgb, linear_to_srgb(composite_linear)));
);
} }
fn blend(fg: vec4<f32>, bg: vec4<f32>) -> vec4<f32> { fn blend(fg: vec4<f32>, bg: vec4<f32>) -> vec4<f32> {
@ -79,3 +79,25 @@ fn blend(fg: vec4<f32>, bg: vec4<f32>) -> vec4<f32> {
let rgb = fg.rgb * fg.a + bg.rgb * bg.a * (1.0 - fg.a); let rgb = fg.rgb * fg.a + bg.rgb * bg.a * (1.0 - fg.a);
return vec4<f32>(rgb, a); return vec4<f32>(rgb, a);
} }
fn linear_to_srgb(in: vec4<f32>) -> vec4<f32> {
let cutoff = vec3<f32>(0.0031308);
let lo = in.rgb * 12.92;
let hi = 1.055 * pow(max(in.rgb, vec3<f32>(0.0)), vec3<f32>(1.0/2.4)) - 0.055;
return vec4<f32>(select(lo, hi, in.rgb > cutoff), in.a);
}
fn srgb_to_linear(in: vec4<f32>) -> vec4<f32> {
let cutoff = vec3<f32>(0.04045);
let lo = in.rgb / 12.92;
let hi = pow((in.rgb + 0.055) / 1.055, vec3<f32>(2.4));
return vec4<f32>(select(lo, hi, in.rgb > cutoff), in.a);
}
fn unpremultiply(in: vec4<f32>) -> vec4<f32> {
if (in.a > 0.0) {
return vec4<f32>((in.rgb / in.a), in.a);
} else {
return vec4<f32>(0.0);
}
}

View file

@ -12,18 +12,18 @@ license = "Apache-2.0"
[features] [features]
default = ["wasm"] default = ["wasm"]
wasm = ["wasm-bindgen", "graphene-std/wasm", "wasm-bindgen-futures"] wasm = ["wasm-bindgen", "graphene-std/wasm"]
gpu = ["interpreted-executor/gpu", "wgpu-executor"] gpu = ["interpreted-executor/gpu", "wgpu-executor"]
resvg = ["graphene-std/resvg"] resvg = ["graphene-std/resvg"]
vello = ["graphene-std/vello", "resvg"] vello = ["graphene-std/vello", "resvg"]
ron = ["dep:ron"] ron = []
[dependencies] [dependencies]
# Local dependencies # Local dependencies
graphite-proc-macros = { workspace = true } graphite-proc-macros = { workspace = true }
graph-craft = { workspace = true } graph-craft = { workspace = true }
interpreted-executor = { workspace = true } interpreted-executor = { workspace = true }
graphene-std = { workspace = true } graphene-std = { workspace = true } # NOTE: `graphene-core` should not be added here because `graphene-std` re-exports its contents
preprocessor = { workspace = true } preprocessor = { workspace = true }
# Workspace dependencies # Workspace dependencies
@ -33,7 +33,6 @@ bitflags = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
bezier-rs = { workspace = true }
kurbo = { workspace = true } kurbo = { workspace = true }
futures = { workspace = true } futures = { workspace = true }
glam = { workspace = true } glam = { workspace = true }
@ -44,9 +43,8 @@ num_enum = { workspace = true }
usvg = { workspace = true } usvg = { workspace = true }
once_cell = { workspace = true } once_cell = { workspace = true }
web-sys = { workspace = true } web-sys = { workspace = true }
bytemuck = { workspace = true }
vello = { workspace = true } vello = { workspace = true }
tracing = { workspace = true } base64 = { workspace = true }
# Required dependencies # Required dependencies
spin = "0.9.8" spin = "0.9.8"
@ -56,8 +54,6 @@ wgpu-executor = { workspace = true, optional = true }
# Optional workspace dependencies # Optional workspace dependencies
wasm-bindgen = { workspace = true, optional = true } wasm-bindgen = { workspace = true, optional = true }
wasm-bindgen-futures = { workspace = true, optional = true }
ron = { workspace = true, optional = true }
[dev-dependencies] [dev-dependencies]
# Workspace dependencies # Workspace dependencies

View file

@ -26,7 +26,6 @@ pub struct DispatcherMessageHandlers {
pub portfolio_message_handler: PortfolioMessageHandler, pub portfolio_message_handler: PortfolioMessageHandler,
preferences_message_handler: PreferencesMessageHandler, preferences_message_handler: PreferencesMessageHandler,
tool_message_handler: ToolMessageHandler, tool_message_handler: ToolMessageHandler,
workspace_message_handler: WorkspaceMessageHandler,
} }
impl DispatcherMessageHandlers { impl DispatcherMessageHandlers {
@ -53,7 +52,7 @@ const SIDE_EFFECT_FREE_MESSAGES: &[MessageDiscriminant] = &[
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::TriggerFontLoad), MessageDiscriminant::Frontend(FrontendMessageDiscriminant::TriggerFontLoad),
]; ];
const DEBUG_MESSAGE_BLOCK_LIST: &[MessageDiscriminant] = &[ const DEBUG_MESSAGE_BLOCK_LIST: &[MessageDiscriminant] = &[
MessageDiscriminant::Broadcast(BroadcastMessageDiscriminant::TriggerEvent(BroadcastEventDiscriminant::AnimationFrame)), MessageDiscriminant::Broadcast(BroadcastMessageDiscriminant::TriggerEvent(EventMessageDiscriminant::AnimationFrame)),
MessageDiscriminant::Animation(AnimationMessageDiscriminant::IncrementFrameCounter), MessageDiscriminant::Animation(AnimationMessageDiscriminant::IncrementFrameCounter),
]; ];
// TODO: Find a way to combine these with the list above. We use strings for now since these are the standard variant names used by multiple messages. But having these also type-checked would be best. // TODO: Find a way to combine these with the list above. We use strings for now since these are the standard variant names used by multiple messages. But having these also type-checked would be best.
@ -231,9 +230,6 @@ impl Dispatcher {
self.message_handlers.tool_message_handler.process_message(message, &mut queue, context); self.message_handlers.tool_message_handler.process_message(message, &mut queue, context);
} }
Message::Workspace(message) => {
self.message_handlers.workspace_message_handler.process_message(message, &mut queue, ());
}
Message::NoOp => {} Message::NoOp => {}
Message::Batched { messages } => { Message::Batched { messages } => {
messages.iter().for_each(|message| self.handle_message(message.to_owned(), false)); messages.iter().for_each(|message| self.handle_message(message.to_owned(), false));

View file

@ -1,6 +1,5 @@
use crate::messages::prelude::*;
use super::animation_message_handler::AnimationTimeMode; use super::animation_message_handler::AnimationTimeMode;
use crate::messages::prelude::*;
#[impl_message(Message, Animation)] #[impl_message(Message, Animation)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]

View file

@ -5,15 +5,15 @@ use crate::messages::prelude::*;
pub enum BroadcastMessage { pub enum BroadcastMessage {
// Sub-messages // Sub-messages
#[child] #[child]
TriggerEvent(BroadcastEvent), TriggerEvent(EventMessage),
// Messages // Messages
SubscribeEvent { SubscribeEvent {
on: BroadcastEvent, on: EventMessage,
send: Box<Message>, send: Box<Message>,
}, },
UnsubscribeEvent { UnsubscribeEvent {
on: BroadcastEvent, on: EventMessage,
message: Box<Message>, send: Box<Message>,
}, },
} }

View file

@ -2,7 +2,8 @@ use crate::messages::prelude::*;
#[derive(Debug, Clone, Default, ExtractField)] #[derive(Debug, Clone, Default, ExtractField)]
pub struct BroadcastMessageHandler { pub struct BroadcastMessageHandler {
listeners: HashMap<BroadcastEvent, Vec<Message>>, event: EventMessageHandler,
listeners: HashMap<EventMessage, Vec<Message>>,
} }
#[message_handler_data] #[message_handler_data]
@ -10,19 +11,15 @@ impl MessageHandler<BroadcastMessage, ()> for BroadcastMessageHandler {
fn process_message(&mut self, message: BroadcastMessage, responses: &mut VecDeque<Message>, _: ()) { fn process_message(&mut self, message: BroadcastMessage, responses: &mut VecDeque<Message>, _: ()) {
match message { match message {
// Sub-messages // Sub-messages
BroadcastMessage::TriggerEvent(event) => { BroadcastMessage::TriggerEvent(message) => self.event.process_message(message, responses, EventMessageContext { listeners: &mut self.listeners }),
for message in self.listeners.entry(event).or_default() {
responses.add_front(message.clone())
}
}
// Messages // Messages
BroadcastMessage::SubscribeEvent { on, send } => self.listeners.entry(on).or_default().push(*send), BroadcastMessage::SubscribeEvent { on, send } => self.listeners.entry(on).or_default().push(*send),
BroadcastMessage::UnsubscribeEvent { on, message } => self.listeners.entry(on).or_default().retain(|msg| *msg != *message), BroadcastMessage::UnsubscribeEvent { on, send } => self.listeners.entry(on).or_default().retain(|msg| *msg != *send),
} }
} }
fn actions(&self) -> ActionList { fn actions(&self) -> ActionList {
actions!(BroadcastEventDiscriminant;) actions!(EventMessageDiscriminant;)
} }
} }

View file

@ -1,8 +1,8 @@
use crate::messages::prelude::*; use crate::messages::prelude::*;
#[derive(PartialEq, Eq, Clone, Debug, serde::Serialize, serde::Deserialize, Hash)]
#[impl_message(Message, BroadcastMessage, TriggerEvent)] #[impl_message(Message, BroadcastMessage, TriggerEvent)]
pub enum BroadcastEvent { #[derive(PartialEq, Eq, Clone, Debug, serde::Serialize, serde::Deserialize, Hash)]
pub enum EventMessage {
/// Triggered by requestAnimationFrame in JS /// Triggered by requestAnimationFrame in JS
AnimationFrame, AnimationFrame,
CanvasTransformed, CanvasTransformed,

View file

@ -0,0 +1,22 @@
use crate::messages::prelude::*;
#[derive(ExtractField)]
pub struct EventMessageContext<'a> {
pub listeners: &'a mut HashMap<EventMessage, Vec<Message>>,
}
#[derive(Debug, Clone, Default, ExtractField)]
pub struct EventMessageHandler {}
#[message_handler_data]
impl MessageHandler<EventMessage, EventMessageContext<'_>> for EventMessageHandler {
fn process_message(&mut self, message: EventMessage, responses: &mut VecDeque<Message>, context: EventMessageContext) {
for message in context.listeners.entry(message).or_default() {
responses.add_front(message.clone())
}
}
fn actions(&self) -> ActionList {
actions!(EventMessageDiscriminant;)
}
}

View file

@ -0,0 +1,7 @@
mod event_message;
mod event_message_handler;
#[doc(inline)]
pub use event_message::{EventMessage, EventMessageDiscriminant};
#[doc(inline)]
pub use event_message_handler::{EventMessageContext, EventMessageHandler};

View file

@ -1,7 +1,7 @@
mod broadcast_message; mod broadcast_message;
mod broadcast_message_handler; mod broadcast_message_handler;
pub mod broadcast_event; pub mod event;
#[doc(inline)] #[doc(inline)]
pub use broadcast_message::{BroadcastMessage, BroadcastMessageDiscriminant}; pub use broadcast_message::{BroadcastMessage, BroadcastMessageDiscriminant};

View file

@ -3,8 +3,8 @@ use crate::messages::prelude::*;
#[impl_message(Message, Defer)] #[impl_message(Message, Defer)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum DeferMessage { pub enum DeferMessage {
SetGraphSubmissionIndex(u64), SetGraphSubmissionIndex { execution_id: u64 },
TriggerGraphRun(u64, DocumentId), TriggerGraphRun { execution_id: u64, document_id: DocumentId },
AfterGraphRun { messages: Vec<Message> }, AfterGraphRun { messages: Vec<Message> },
TriggerNavigationReady, TriggerNavigationReady,
AfterNavigationReady { messages: Vec<Message> }, AfterNavigationReady { messages: Vec<Message> },

View file

@ -24,10 +24,10 @@ impl MessageHandler<DeferMessage, DeferMessageContext<'_>> for DeferMessageHandl
DeferMessage::AfterNavigationReady { messages } => { DeferMessage::AfterNavigationReady { messages } => {
self.after_viewport_resize.extend_from_slice(&messages); self.after_viewport_resize.extend_from_slice(&messages);
} }
DeferMessage::SetGraphSubmissionIndex(execution_id) => { DeferMessage::SetGraphSubmissionIndex { execution_id } => {
self.current_graph_submission_id = execution_id + 1; self.current_graph_submission_id = execution_id + 1;
} }
DeferMessage::TriggerGraphRun(execution_id, document_id) => { DeferMessage::TriggerGraphRun { execution_id, document_id } => {
let after_graph_run = self.after_graph_run.entry(document_id).or_default(); let after_graph_run = self.after_graph_run.entry(document_id).or_default();
if after_graph_run.is_empty() { if after_graph_run.is_empty() {
return; return;
@ -38,9 +38,9 @@ impl MessageHandler<DeferMessage, DeferMessageContext<'_>> for DeferMessageHandl
for (_, message) in elements.rev() { for (_, message) in elements.rev() {
responses.add_front(message); responses.add_front(message);
} }
for (id, messages) in self.after_graph_run.iter() { for (&document_id, messages) in self.after_graph_run.iter() {
if !messages.is_empty() { if !messages.is_empty() {
responses.add(PortfolioMessage::SubmitGraphRender { document_id: *id, ignore_hash: false }); responses.add(PortfolioMessage::SubmitGraphRender { document_id, ignore_hash: false });
} }
} }
} }

View file

@ -4,10 +4,10 @@ use crate::messages::prelude::*;
#[impl_message(Message, DialogMessage, ExportDialog)] #[impl_message(Message, DialogMessage, ExportDialog)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum ExportDialogMessage { pub enum ExportDialogMessage {
FileType(FileType), FileType { file_type: FileType },
ScaleFactor(f64), ScaleFactor { factor: f64 },
TransparentBackground(bool), TransparentBackground { transparent: bool },
ExportBounds(ExportBounds), ExportBounds { bounds: ExportBounds },
Submit, Submit,
} }

View file

@ -38,10 +38,10 @@ impl MessageHandler<ExportDialogMessage, ExportDialogMessageContext<'_>> for Exp
let ExportDialogMessageContext { portfolio } = context; let ExportDialogMessageContext { portfolio } = context;
match message { match message {
ExportDialogMessage::FileType(export_type) => self.file_type = export_type, ExportDialogMessage::FileType { file_type } => self.file_type = file_type,
ExportDialogMessage::ScaleFactor(factor) => self.scale_factor = factor, ExportDialogMessage::ScaleFactor { factor } => self.scale_factor = factor,
ExportDialogMessage::TransparentBackground(transparent_background) => self.transparent_background = transparent_background, ExportDialogMessage::TransparentBackground { transparent } => self.transparent_background = transparent,
ExportDialogMessage::ExportBounds(export_area) => self.bounds = export_area, ExportDialogMessage::ExportBounds { bounds } => self.bounds = bounds,
ExportDialogMessage::Submit => responses.add_front(PortfolioMessage::SubmitDocumentExport { ExportDialogMessage::Submit => responses.add_front(PortfolioMessage::SubmitDocumentExport {
file_name: portfolio.active_document().map(|document| document.name.clone()).unwrap_or_default(), file_name: portfolio.active_document().map(|document| document.name.clone()).unwrap_or_default(),
@ -84,7 +84,11 @@ impl LayoutHolder for ExportDialogMessageHandler {
fn layout(&self) -> Layout { fn layout(&self) -> Layout {
let entries = [(FileType::Png, "PNG"), (FileType::Jpg, "JPG"), (FileType::Svg, "SVG")] let entries = [(FileType::Png, "PNG"), (FileType::Jpg, "JPG"), (FileType::Svg, "SVG")]
.into_iter() .into_iter()
.map(|(val, name)| RadioEntryData::new(format!("{val:?}")).label(name).on_update(move |_| ExportDialogMessage::FileType(val).into())) .map(|(file_type, name)| {
RadioEntryData::new(format!("{file_type:?}"))
.label(name)
.on_update(move |_| ExportDialogMessage::FileType { file_type }.into())
})
.collect(); .collect();
let export_type = vec![ let export_type = vec![
@ -101,7 +105,7 @@ impl LayoutHolder for ExportDialogMessageHandler {
.min(0.) .min(0.)
.max((1_u64 << f64::MANTISSA_DIGITS) as f64) .max((1_u64 << f64::MANTISSA_DIGITS) as f64)
.disabled(self.file_type == FileType::Svg) .disabled(self.file_type == FileType::Svg)
.on_update(|number_input: &NumberInput| ExportDialogMessage::ScaleFactor(number_input.value.unwrap()).into()) .on_update(|number_input: &NumberInput| ExportDialogMessage::ScaleFactor { factor: number_input.value.unwrap() }.into())
.min_width(200) .min_width(200)
.widget_holder(), .widget_holder(),
]; ];
@ -125,10 +129,10 @@ impl LayoutHolder for ExportDialogMessageHandler {
.map(|choice| { .map(|choice| {
choice choice
.into_iter() .into_iter()
.map(|(val, name, disabled)| { .map(|(bounds, name, disabled)| {
MenuListEntry::new(format!("{val:?}")) MenuListEntry::new(format!("{bounds:?}"))
.label(name) .label(name)
.on_commit(move |_| ExportDialogMessage::ExportBounds(val).into()) .on_commit(move |_| ExportDialogMessage::ExportBounds { bounds }.into())
.disabled(disabled) .disabled(disabled)
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
@ -151,7 +155,7 @@ impl LayoutHolder for ExportDialogMessageHandler {
Separator::new(SeparatorType::Unrelated).widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder(),
CheckboxInput::new(self.transparent_background) CheckboxInput::new(self.transparent_background)
.disabled(self.file_type == FileType::Jpg) .disabled(self.file_type == FileType::Jpg)
.on_update(move |value: &CheckboxInput| ExportDialogMessage::TransparentBackground(value.checked).into()) .on_update(move |value: &CheckboxInput| ExportDialogMessage::TransparentBackground { transparent: value.checked }.into())
.for_label(checkbox_id) .for_label(checkbox_id)
.widget_holder(), .widget_holder(),
]; ];

View file

@ -3,10 +3,10 @@ use crate::messages::prelude::*;
#[impl_message(Message, DialogMessage, NewDocumentDialog)] #[impl_message(Message, DialogMessage, NewDocumentDialog)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum NewDocumentDialogMessage { pub enum NewDocumentDialogMessage {
Name(String), Name { name: String },
Infinite(bool), Infinite { infinite: bool },
DimensionsX(f64), DimensionsX { width: f64 },
DimensionsY(f64), DimensionsY { height: f64 },
Submit, Submit,
} }

View file

@ -20,10 +20,10 @@ pub struct NewDocumentDialogMessageHandler {
impl<'a> MessageHandler<NewDocumentDialogMessage, NewDocumentDialogMessageContext<'a>> for NewDocumentDialogMessageHandler { impl<'a> MessageHandler<NewDocumentDialogMessage, NewDocumentDialogMessageContext<'a>> for NewDocumentDialogMessageHandler {
fn process_message(&mut self, message: NewDocumentDialogMessage, responses: &mut VecDeque<Message>, context: NewDocumentDialogMessageContext<'a>) { fn process_message(&mut self, message: NewDocumentDialogMessage, responses: &mut VecDeque<Message>, context: NewDocumentDialogMessageContext<'a>) {
match message { match message {
NewDocumentDialogMessage::Name(name) => self.name = name, NewDocumentDialogMessage::Name { name } => self.name = name,
NewDocumentDialogMessage::Infinite(infinite) => self.infinite = infinite, NewDocumentDialogMessage::Infinite { infinite } => self.infinite = infinite,
NewDocumentDialogMessage::DimensionsX(x) => self.dimensions.x = x as u32, NewDocumentDialogMessage::DimensionsX { width } => self.dimensions.x = width as u32,
NewDocumentDialogMessage::DimensionsY(y) => self.dimensions.y = y as u32, NewDocumentDialogMessage::DimensionsY { height } => self.dimensions.y = height as u32,
NewDocumentDialogMessage::Submit => { NewDocumentDialogMessage::Submit => {
responses.add(PortfolioMessage::NewDocumentWithName { name: self.name.clone() }); responses.add(PortfolioMessage::NewDocumentWithName { name: self.name.clone() });
@ -82,7 +82,7 @@ impl LayoutHolder for NewDocumentDialogMessageHandler {
TextLabel::new("Name").table_align(true).min_width(90).widget_holder(), TextLabel::new("Name").table_align(true).min_width(90).widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder(),
TextInput::new(&self.name) TextInput::new(&self.name)
.on_update(|text_input: &TextInput| NewDocumentDialogMessage::Name(text_input.value.clone()).into()) .on_update(|text_input: &TextInput| NewDocumentDialogMessage::Name { name: text_input.value.clone() }.into())
.min_width(204) // Matches the 100px of both NumberInputs below + the 4px of the Unrelated-type separator .min_width(204) // Matches the 100px of both NumberInputs below + the 4px of the Unrelated-type separator
.widget_holder(), .widget_holder(),
]; ];
@ -92,7 +92,7 @@ impl LayoutHolder for NewDocumentDialogMessageHandler {
TextLabel::new("Infinite Canvas").table_align(true).min_width(90).for_checkbox(checkbox_id).widget_holder(), TextLabel::new("Infinite Canvas").table_align(true).min_width(90).for_checkbox(checkbox_id).widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder(),
CheckboxInput::new(self.infinite) CheckboxInput::new(self.infinite)
.on_update(|checkbox_input: &CheckboxInput| NewDocumentDialogMessage::Infinite(checkbox_input.checked).into()) .on_update(|checkbox_input: &CheckboxInput| NewDocumentDialogMessage::Infinite { infinite: checkbox_input.checked }.into())
.for_label(checkbox_id) .for_label(checkbox_id)
.widget_holder(), .widget_holder(),
]; ];
@ -108,7 +108,7 @@ impl LayoutHolder for NewDocumentDialogMessageHandler {
.is_integer(true) .is_integer(true)
.disabled(self.infinite) .disabled(self.infinite)
.min_width(100) .min_width(100)
.on_update(|number_input: &NumberInput| NewDocumentDialogMessage::DimensionsX(number_input.value.unwrap()).into()) .on_update(|number_input: &NumberInput| NewDocumentDialogMessage::DimensionsX { width: number_input.value.unwrap() }.into())
.widget_holder(), .widget_holder(),
Separator::new(SeparatorType::Related).widget_holder(), Separator::new(SeparatorType::Related).widget_holder(),
NumberInput::new(Some(self.dimensions.y as f64)) NumberInput::new(Some(self.dimensions.y as f64))
@ -119,7 +119,7 @@ impl LayoutHolder for NewDocumentDialogMessageHandler {
.is_integer(true) .is_integer(true)
.disabled(self.infinite) .disabled(self.infinite)
.min_width(100) .min_width(100)
.on_update(|number_input: &NumberInput| NewDocumentDialogMessage::DimensionsY(number_input.value.unwrap()).into()) .on_update(|number_input: &NumberInput| NewDocumentDialogMessage::DimensionsY { height: number_input.value.unwrap() }.into())
.widget_holder(), .widget_holder(),
]; ];

View file

@ -1,4 +1,4 @@
use crate::messages::broadcast::broadcast_event::BroadcastEvent; use crate::messages::broadcast::event::EventMessage;
use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::prelude::*; use crate::messages::prelude::*;
@ -27,7 +27,7 @@ impl DialogLayoutHolder for CloseDocumentDialog {
TextButton::new("Discard") TextButton::new("Discard")
.on_update(move |_| { .on_update(move |_| {
DialogMessage::CloseDialogAndThen { DialogMessage::CloseDialogAndThen {
followups: vec![BroadcastEvent::ToolAbort.into(), PortfolioMessage::CloseDocument { document_id }.into()], followups: vec![EventMessage::ToolAbort.into(), PortfolioMessage::CloseDocument { document_id }.into()],
} }
.into() .into()
}) })

View file

@ -149,11 +149,16 @@ pub enum FrontendMessage {
UpdateGraphViewOverlay { UpdateGraphViewOverlay {
open: bool, open: bool,
}, },
UpdateSpreadsheetState { UpdateDataPanelState {
open: bool, open: bool,
node: Option<NodeId>,
}, },
UpdateSpreadsheetLayout { UpdatePropertiesPanelState {
open: bool,
},
UpdateLayersPanelState {
open: bool,
},
UpdateDataPanelLayout {
#[serde(rename = "layoutTarget")] #[serde(rename = "layoutTarget")]
layout_target: LayoutTarget, layout_target: LayoutTarget,
diff: Vec<WidgetDiff>, diff: Vec<WidgetDiff>,
@ -296,7 +301,7 @@ pub enum FrontendMessage {
#[serde(rename = "openDocuments")] #[serde(rename = "openDocuments")]
open_documents: Vec<FrontendDocumentDetails>, open_documents: Vec<FrontendDocumentDetails>,
}, },
UpdatePropertyPanelSectionsLayout { UpdatePropertiesPanelLayout {
#[serde(rename = "layoutTarget")] #[serde(rename = "layoutTarget")]
layout_target: LayoutTarget, layout_target: LayoutTarget,
diff: Vec<WidgetDiff>, diff: Vec<WidgetDiff>,
@ -330,9 +335,9 @@ pub enum FrontendMessage {
active: bool, active: bool,
}, },
#[cfg(not(target_family = "wasm"))] #[cfg(not(target_family = "wasm"))]
RenderOverlays( RenderOverlays {
#[serde(skip, default = "OverlayContext::default")] #[serde(skip, default = "OverlayContext::default")]
#[derivative(Debug = "ignore", PartialEq = "ignore")] #[derivative(Debug = "ignore", PartialEq = "ignore")]
OverlayContext, context: OverlayContext,
), },
} }

View file

@ -96,7 +96,7 @@ pub fn input_mappings() -> Mapping {
entry!(PointerMove; refresh_keys=[Control, Shift], action_dispatch=TransformLayerMessage::PointerMove { slow_key: Shift, increments_key: Control }), entry!(PointerMove; refresh_keys=[Control, Shift], action_dispatch=TransformLayerMessage::PointerMove { slow_key: Shift, increments_key: Control }),
// //
// SelectToolMessage // SelectToolMessage
entry!(PointerMove; refresh_keys=[Control, Alt, Shift], action_dispatch=SelectToolMessage::PointerMove(SelectToolPointerKeys { axis_align: Shift, snap_angle: Shift, center: Alt, duplicate: Alt })), entry!(PointerMove; refresh_keys=[Control, Alt, Shift], action_dispatch=SelectToolMessage::PointerMove { modifier_keys: SelectToolPointerKeys { axis_align: Shift, snap_angle: Shift, center: Alt, duplicate: Alt } }),
entry!(KeyDown(MouseLeft); action_dispatch=SelectToolMessage::DragStart { extend_selection: Shift, remove_from_selection: Alt, select_deepest: Accel, lasso_select: Control, skew: Control }), entry!(KeyDown(MouseLeft); action_dispatch=SelectToolMessage::DragStart { extend_selection: Shift, remove_from_selection: Alt, select_deepest: Accel, lasso_select: Control, skew: Control }),
entry!(KeyUp(MouseLeft); action_dispatch=SelectToolMessage::DragStop { remove_from_selection: Alt }), entry!(KeyUp(MouseLeft); action_dispatch=SelectToolMessage::DragStop { remove_from_selection: Alt }),
entry!(KeyDown(Enter); action_dispatch=SelectToolMessage::Enter), entry!(KeyDown(Enter); action_dispatch=SelectToolMessage::Enter),
@ -178,7 +178,7 @@ pub fn input_mappings() -> Mapping {
entry!(KeyDown(Escape); action_dispatch=ShapeToolMessage::Abort), entry!(KeyDown(Escape); action_dispatch=ShapeToolMessage::Abort),
entry!(KeyDown(BracketLeft); action_dispatch=ShapeToolMessage::DecreaseSides), entry!(KeyDown(BracketLeft); action_dispatch=ShapeToolMessage::DecreaseSides),
entry!(KeyDown(BracketRight); action_dispatch=ShapeToolMessage::IncreaseSides), entry!(KeyDown(BracketRight); action_dispatch=ShapeToolMessage::IncreaseSides),
entry!(PointerMove; refresh_keys=[Alt, Shift, Control], action_dispatch=ShapeToolMessage::PointerMove([Alt, Shift, Control])), entry!(PointerMove; refresh_keys=[Alt, Shift, Control], action_dispatch=ShapeToolMessage::PointerMove { modifier: [Alt, Shift, Control] }),
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, 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, 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(ArrowUp); modifiers=[Shift], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: 0., delta_y: -BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
@ -297,8 +297,8 @@ pub fn input_mappings() -> Mapping {
entry!(PointerMove; action_dispatch=BrushToolMessage::PointerMove), entry!(PointerMove; action_dispatch=BrushToolMessage::PointerMove),
entry!(KeyDown(MouseLeft); action_dispatch=BrushToolMessage::DragStart), entry!(KeyDown(MouseLeft); action_dispatch=BrushToolMessage::DragStart),
entry!(KeyUp(MouseLeft); action_dispatch=BrushToolMessage::DragStop), entry!(KeyUp(MouseLeft); action_dispatch=BrushToolMessage::DragStop),
entry!(KeyDown(BracketLeft); action_dispatch=BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::ChangeDiameter(-BRUSH_SIZE_CHANGE_KEYBOARD))), entry!(KeyDown(BracketLeft); action_dispatch=BrushToolMessage::UpdateOptions { options: BrushToolMessageOptionsUpdate::ChangeDiameter(-BRUSH_SIZE_CHANGE_KEYBOARD) }),
entry!(KeyDown(BracketRight); action_dispatch=BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::ChangeDiameter(BRUSH_SIZE_CHANGE_KEYBOARD))), entry!(KeyDown(BracketRight); action_dispatch=BrushToolMessage::UpdateOptions { options: BrushToolMessageOptionsUpdate::ChangeDiameter(BRUSH_SIZE_CHANGE_KEYBOARD) }),
entry!(KeyDown(MouseRight); action_dispatch=BrushToolMessage::Abort), entry!(KeyDown(MouseRight); action_dispatch=BrushToolMessage::Abort),
entry!(KeyDown(Escape); action_dispatch=BrushToolMessage::Abort), entry!(KeyDown(Escape); action_dispatch=BrushToolMessage::Abort),
// //
@ -427,6 +427,7 @@ pub fn input_mappings() -> Mapping {
entry!(KeyDown(KeyX); modifiers=[Accel], action_dispatch=PortfolioMessage::Cut { clipboard: Clipboard::Device }), entry!(KeyDown(KeyX); modifiers=[Accel], action_dispatch=PortfolioMessage::Cut { clipboard: Clipboard::Device }),
entry!(KeyDown(KeyC); modifiers=[Accel], action_dispatch=PortfolioMessage::Copy { clipboard: Clipboard::Device }), entry!(KeyDown(KeyC); modifiers=[Accel], action_dispatch=PortfolioMessage::Copy { clipboard: Clipboard::Device }),
entry!(KeyDown(KeyR); modifiers=[Alt], action_dispatch=PortfolioMessage::ToggleRulers), entry!(KeyDown(KeyR); modifiers=[Alt], action_dispatch=PortfolioMessage::ToggleRulers),
entry!(KeyDown(KeyD); modifiers=[Alt], action_dispatch=PortfolioMessage::ToggleDataPanelOpen),
// //
// FrontendMessage // FrontendMessage
entry!(KeyDown(KeyV); modifiers=[Accel], action_dispatch=FrontendMessage::TriggerPaste), entry!(KeyDown(KeyV); modifiers=[Accel], action_dispatch=FrontendMessage::TriggerPaste),

View file

@ -3,13 +3,16 @@ use crate::messages::prelude::*;
#[impl_message(Message, KeyMapping)] #[impl_message(Message, KeyMapping)]
#[derive(PartialEq, Eq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize)] #[derive(PartialEq, Eq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize)]
pub enum KeyMappingMessage { pub enum KeyMappingMessage {
// Sub-messages
#[child] #[child]
Lookup(InputMapperMessage), Lookup(InputMapperMessage),
#[child]
ModifyMapping(MappingVariant), // Messages
ModifyMapping {
mapping: MappingVariant,
},
} }
#[impl_message(Message, KeyMappingMessage, ModifyMapping)]
#[derive(PartialEq, Eq, Clone, Debug, Default, Hash, serde::Serialize, serde::Deserialize)] #[derive(PartialEq, Eq, Clone, Debug, Default, Hash, serde::Serialize, serde::Deserialize)]
pub enum MappingVariant { pub enum MappingVariant {
#[default] #[default]

View file

@ -19,8 +19,11 @@ impl MessageHandler<KeyMappingMessage, KeyMappingMessageContext<'_>> for KeyMapp
let KeyMappingMessageContext { input, actions } = context; let KeyMappingMessageContext { input, actions } = context;
match message { match message {
// Sub-messages
KeyMappingMessage::Lookup(input_message) => self.mapping_handler.process_message(input_message, responses, InputMapperMessageContext { input, actions }), KeyMappingMessage::Lookup(input_message) => self.mapping_handler.process_message(input_message, responses, InputMapperMessageContext { input, actions }),
KeyMappingMessage::ModifyMapping(new_layout) => self.mapping_handler.set_mapping(new_layout.into()),
// Messages
KeyMappingMessage::ModifyMapping { mapping } => self.mapping_handler.set_mapping(mapping.into()),
} }
} }
advertise_actions!(); advertise_actions!();

View file

@ -2,6 +2,6 @@ mod key_mapping_message;
mod key_mapping_message_handler; mod key_mapping_message_handler;
#[doc(inline)] #[doc(inline)]
pub use key_mapping_message::{KeyMappingMessage, KeyMappingMessageDiscriminant, MappingVariant, MappingVariantDiscriminant}; pub use key_mapping_message::{KeyMappingMessage, KeyMappingMessageDiscriminant, MappingVariant};
#[doc(inline)] #[doc(inline)]
pub use key_mapping_message_handler::{KeyMappingMessageContext, KeyMappingMessageHandler}; pub use key_mapping_message_handler::{KeyMappingMessageContext, KeyMappingMessageHandler};

View file

@ -308,6 +308,7 @@ impl LayoutMessageHandler {
responses.add(callback_message); responses.add(callback_message);
} }
Widget::ImageLabel(_) => {}
Widget::IconLabel(_) => {} Widget::IconLabel(_) => {}
Widget::InvisibleStandinInput(invisible) => { Widget::InvisibleStandinInput(invisible) => {
let callback_message = match action { let callback_message = match action {
@ -481,18 +482,18 @@ impl LayoutMessageHandler {
diff.iter_mut().for_each(|diff| diff.new_value.apply_keyboard_shortcut(action_input_mapping)); diff.iter_mut().for_each(|diff| diff.new_value.apply_keyboard_shortcut(action_input_mapping));
let message = match layout_target { let message = match layout_target {
LayoutTarget::MenuBar => unreachable!("Menu bar is not diffed"),
LayoutTarget::DialogButtons => FrontendMessage::UpdateDialogButtons { layout_target, diff }, LayoutTarget::DialogButtons => FrontendMessage::UpdateDialogButtons { layout_target, diff },
LayoutTarget::DialogColumn1 => FrontendMessage::UpdateDialogColumn1 { layout_target, diff }, LayoutTarget::DialogColumn1 => FrontendMessage::UpdateDialogColumn1 { layout_target, diff },
LayoutTarget::DialogColumn2 => FrontendMessage::UpdateDialogColumn2 { layout_target, diff }, LayoutTarget::DialogColumn2 => FrontendMessage::UpdateDialogColumn2 { layout_target, diff },
LayoutTarget::DocumentBar => FrontendMessage::UpdateDocumentBarLayout { layout_target, diff }, LayoutTarget::DocumentBar => FrontendMessage::UpdateDocumentBarLayout { layout_target, diff },
LayoutTarget::DocumentMode => FrontendMessage::UpdateDocumentModeLayout { layout_target, diff }, LayoutTarget::DocumentMode => FrontendMessage::UpdateDocumentModeLayout { layout_target, diff },
LayoutTarget::DataPanel => FrontendMessage::UpdateDataPanelLayout { layout_target, diff },
LayoutTarget::LayersPanelControlLeftBar => FrontendMessage::UpdateLayersPanelControlBarLeftLayout { layout_target, diff }, LayoutTarget::LayersPanelControlLeftBar => FrontendMessage::UpdateLayersPanelControlBarLeftLayout { layout_target, diff },
LayoutTarget::LayersPanelControlRightBar => FrontendMessage::UpdateLayersPanelControlBarRightLayout { layout_target, diff }, LayoutTarget::LayersPanelControlRightBar => FrontendMessage::UpdateLayersPanelControlBarRightLayout { layout_target, diff },
LayoutTarget::LayersPanelBottomBar => FrontendMessage::UpdateLayersPanelBottomBarLayout { layout_target, diff }, LayoutTarget::LayersPanelBottomBar => FrontendMessage::UpdateLayersPanelBottomBarLayout { layout_target, diff },
LayoutTarget::MenuBar => unreachable!("Menu bar is not diffed"), LayoutTarget::PropertiesPanel => FrontendMessage::UpdatePropertiesPanelLayout { layout_target, diff },
LayoutTarget::NodeGraphControlBar => FrontendMessage::UpdateNodeGraphControlBarLayout { layout_target, diff }, LayoutTarget::NodeGraphControlBar => FrontendMessage::UpdateNodeGraphControlBarLayout { layout_target, diff },
LayoutTarget::PropertiesSections => FrontendMessage::UpdatePropertyPanelSectionsLayout { layout_target, diff },
LayoutTarget::Spreadsheet => FrontendMessage::UpdateSpreadsheetLayout { layout_target, diff },
LayoutTarget::ToolOptions => FrontendMessage::UpdateToolOptionsLayout { layout_target, diff }, LayoutTarget::ToolOptions => FrontendMessage::UpdateToolOptionsLayout { layout_target, diff },
LayoutTarget::ToolShelf => FrontendMessage::UpdateToolShelfLayout { layout_target, diff }, LayoutTarget::ToolShelf => FrontendMessage::UpdateToolShelfLayout { layout_target, diff },
LayoutTarget::WorkingColors => FrontendMessage::UpdateWorkingColorsLayout { layout_target, diff }, LayoutTarget::WorkingColors => FrontendMessage::UpdateWorkingColorsLayout { layout_target, diff },

View file

@ -42,9 +42,9 @@ pub enum LayoutTarget {
/// Bar at the top of the node graph containing the location and the "Preview" and "Hide" buttons. /// Bar at the top of the node graph containing the location and the "Preview" and "Hide" buttons.
NodeGraphControlBar, NodeGraphControlBar,
/// The body of the Properties panel containing many collapsable sections. /// The body of the Properties panel containing many collapsable sections.
PropertiesSections, PropertiesPanel,
/// The spredsheet panel allows for the visualisation of data in the graph. /// The spredsheet panel allows for the visualisation of data in the graph.
Spreadsheet, DataPanel,
/// The bar directly above the canvas, left-aligned and to the right of the document mode dropdown. /// The bar directly above the canvas, left-aligned and to the right of the document mode dropdown.
ToolOptions, ToolOptions,
/// The vertical buttons for all of the tools on the left of the canvas. /// The vertical buttons for all of the tools on the left of the canvas.
@ -369,6 +369,7 @@ impl LayoutGroup {
Widget::IconButton(x) => &mut x.tooltip, Widget::IconButton(x) => &mut x.tooltip,
Widget::IconLabel(x) => &mut x.tooltip, Widget::IconLabel(x) => &mut x.tooltip,
Widget::ImageButton(x) => &mut x.tooltip, Widget::ImageButton(x) => &mut x.tooltip,
Widget::ImageLabel(x) => &mut x.tooltip,
Widget::NumberInput(x) => &mut x.tooltip, Widget::NumberInput(x) => &mut x.tooltip,
Widget::ParameterExposeButton(x) => &mut x.tooltip, Widget::ParameterExposeButton(x) => &mut x.tooltip,
Widget::PopoverButton(x) => &mut x.tooltip, Widget::PopoverButton(x) => &mut x.tooltip,
@ -546,6 +547,7 @@ pub enum Widget {
IconButton(IconButton), IconButton(IconButton),
IconLabel(IconLabel), IconLabel(IconLabel),
ImageButton(ImageButton), ImageButton(ImageButton),
ImageLabel(ImageLabel),
InvisibleStandinInput(InvisibleStandinInput), InvisibleStandinInput(InvisibleStandinInput),
NodeCatalog(NodeCatalog), NodeCatalog(NodeCatalog),
NumberInput(NumberInput), NumberInput(NumberInput),
@ -622,6 +624,7 @@ impl DiffUpdate {
Widget::TextButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), Widget::TextButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::ImageButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), Widget::ImageButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::IconLabel(_) Widget::IconLabel(_)
| Widget::ImageLabel(_)
| Widget::CurveInput(_) | Widget::CurveInput(_)
| Widget::InvisibleStandinInput(_) | Widget::InvisibleStandinInput(_)
| Widget::NodeCatalog(_) | Widget::NodeCatalog(_)

View file

@ -168,8 +168,6 @@ pub struct ColorInput {
#[widget_builder(constructor)] #[widget_builder(constructor)]
pub value: FillChoice, pub value: FillChoice,
pub disabled: bool,
// TODO: Implement // TODO: Implement
// #[serde(rename = "allowTransparency")] // #[serde(rename = "allowTransparency")]
// #[derivative(Default(value = "false"))] // #[derivative(Default(value = "false"))]
@ -179,9 +177,11 @@ pub struct ColorInput {
#[derivative(Default(value = "true"))] #[derivative(Default(value = "true"))]
pub allow_none: bool, pub allow_none: bool,
// TODO: Implement pub disabled: bool,
// pub disabled: bool,
// #[serde(rename = "menuDirection")]
pub menu_direction: Option<MenuDirection>,
pub tooltip: String, pub tooltip: String,
#[serde(skip)] #[serde(skip)]

View file

@ -416,6 +416,9 @@ pub struct TextInput {
#[serde(rename = "minWidth")] #[serde(rename = "minWidth")]
pub min_width: u32, pub min_width: u32,
#[serde(rename = "maxWidth")]
pub max_width: u32,
// Callbacks // Callbacks
#[serde(skip)] #[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")] #[derivative(Debug = "ignore", PartialEq = "ignore")]

View file

@ -65,4 +65,17 @@ pub struct TextLabel {
pub value: String, pub value: String,
} }
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Default, WidgetBuilder, specta::Type)]
#[derivative(Debug, PartialEq)]
pub struct ImageLabel {
#[widget_builder(constructor)]
pub url: String,
pub width: Option<String>,
pub height: Option<String>,
pub tooltip: String,
}
// TODO: Add UserInputLabel // TODO: Add UserInputLabel

View file

@ -33,14 +33,12 @@ pub enum Message {
Preferences(PreferencesMessage), Preferences(PreferencesMessage),
#[child] #[child]
Tool(ToolMessage), Tool(ToolMessage),
#[child]
Workspace(WorkspaceMessage),
// Messages // Messages
NoOp,
Batched { Batched {
messages: Box<[Message]>, messages: Box<[Message]>,
}, },
NoOp,
} }
/// Provides an impl of `specta::Type` for `MessageDiscriminant`, the struct created by `impl_message`. /// Provides an impl of `specta::Type` for `MessageDiscriminant`, the struct created by `impl_message`.
@ -71,7 +69,7 @@ mod test {
fn print_tree_node(tree: &DebugMessageTree, prefix: &str, is_last: bool, file: &mut std::fs::File) { fn print_tree_node(tree: &DebugMessageTree, prefix: &str, is_last: bool, file: &mut std::fs::File) {
// Print the current node // Print the current node
let (branch, child_prefix) = if tree.has_message_handler_data_fields() || tree.has_message_handler_fields() { let (branch, child_prefix) = if tree.message_handler_data_fields().is_some() || tree.message_handler_fields().is_some() {
("├── ", format!("{}", prefix)) ("├── ", format!("{}", prefix))
} else { } else {
if is_last { if is_last {
@ -96,24 +94,38 @@ mod test {
} }
} }
// Print message field if any
if let Some(fields) = tree.fields() {
let len = fields.len();
for (i, field) in fields.iter().enumerate() {
let is_last_field = i == len - 1;
let branch = if is_last_field { "└── " } else { "├── " };
file.write_all(format!("{}{}{}\n", child_prefix, branch, field).as_bytes()).unwrap();
}
}
// Print handler field if any // Print handler field if any
if let Some(data) = tree.message_handler_fields() { if let Some(data) = tree.message_handler_fields() {
let len = data.fields().len(); let len = data.fields().len();
let (branch, child_prefix) = if tree.has_message_handler_data_fields() { let (branch, child_prefix) = if tree.message_handler_data_fields().is_some() {
("├── ", format!("{}", prefix)) ("├── ", format!("{}", prefix))
} else { } else {
("└── ", format!("{} ", prefix)) ("└── ", format!("{} ", prefix))
}; };
if data.path().is_empty() {
file.write_all(format!("{}{}{}\n", prefix, branch, data.name()).as_bytes()).unwrap();
} else {
file.write_all(format!("{}{}{} `{}`\n", prefix, branch, data.name(), data.path()).as_bytes()).unwrap();
}
for (i, field) in data.fields().iter().enumerate() {
let is_last_field = i == len - 1;
let branch = if is_last_field { "└── " } else { "├── " };
file.write_all(format!("{}{}{}\n", child_prefix, branch, field.0).as_bytes()).unwrap(); const FRONTEND_MESSAGE_STR: &str = "FrontendMessage";
if data.name().is_empty() && tree.name() != FRONTEND_MESSAGE_STR {
panic!("{}'s MessageHandler is missing #[message_handler_data]", tree.name());
} else if tree.name() != FRONTEND_MESSAGE_STR {
file.write_all(format!("{}{}{} `{}`\n", prefix, branch, data.name(), data.path()).as_bytes()).unwrap();
for (i, field) in data.fields().iter().enumerate() {
let is_last_field = i == len - 1;
let branch = if is_last_field { "└── " } else { "├── " };
file.write_all(format!("{}{}{}\n", child_prefix, branch, field.0).as_bytes()).unwrap();
}
} }
} }

View file

@ -16,4 +16,3 @@ pub mod portfolio;
pub mod preferences; pub mod preferences;
pub mod prelude; pub mod prelude;
pub mod tool; pub mod tool;
pub mod workspace;

View file

@ -1,16 +1,15 @@
use crate::messages::prelude::*; use crate::messages::prelude::*;
use crate::node_graph_executor::InspectResult; use crate::node_graph_executor::InspectResult;
/// The spreadsheet UI allows for graph data to be previewed. /// The Data panel UI allows the user to visualize the output data of the selected node.
#[impl_message(Message, PortfolioMessage, Spreadsheet)] #[impl_message(Message, DocumentMessage, DataPanel)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum SpreadsheetMessage { pub enum DataPanelMessage {
ToggleOpen,
UpdateLayout { UpdateLayout {
#[serde(skip)] #[serde(skip)]
inspect_result: InspectResult, inspect_result: InspectResult,
}, },
ClearLayout,
PushToElementPath { PushToElementPath {
index: usize, index: usize,
@ -19,14 +18,15 @@ pub enum SpreadsheetMessage {
len: usize, len: usize,
}, },
ViewVectorDomain { ViewVectorTableTab {
domain: VectorDomain, tab: VectorTableTab,
}, },
} }
#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, serde::Serialize, serde::Deserialize)] #[derive(PartialEq, Eq, Clone, Copy, Default, Debug, serde::Serialize, serde::Deserialize)]
pub enum VectorDomain { pub enum VectorTableTab {
#[default] #[default]
Properties,
Points, Points,
Segments, Segments,
Regions, Regions,

View file

@ -0,0 +1,627 @@
use super::VectorTableTab;
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, LayoutTarget, WidgetLayout};
use crate::messages::portfolio::document::data_panel::DataPanelMessage;
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
use crate::messages::prelude::*;
use crate::messages::tool::tool_messages::tool_prelude::*;
use graph_craft::document::NodeId;
use graphene_std::Color;
use graphene_std::Context;
use graphene_std::gradient::GradientStops;
use graphene_std::memo::IORecord;
use graphene_std::raster_types::{CPU, GPU, Raster};
use graphene_std::table::Table;
use graphene_std::vector::Vector;
use graphene_std::vector::style::{Fill, FillChoice};
use graphene_std::{Artboard, Graphic};
use std::any::Any;
use std::sync::Arc;
#[derive(ExtractField)]
pub struct DataPanelMessageContext<'a> {
pub network_interface: &'a mut NodeNetworkInterface,
pub data_panel_open: bool,
}
/// The data panel allows for graph data to be previewed.
#[derive(Default, Debug, Clone, ExtractField)]
pub struct DataPanelMessageHandler {
introspected_node: Option<NodeId>,
introspected_data: Option<Arc<dyn Any + Send + Sync>>,
element_path: Vec<usize>,
active_vector_table_tab: VectorTableTab,
}
#[message_handler_data]
impl MessageHandler<DataPanelMessage, DataPanelMessageContext<'_>> for DataPanelMessageHandler {
fn process_message(&mut self, message: DataPanelMessage, responses: &mut VecDeque<Message>, context: DataPanelMessageContext) {
match message {
DataPanelMessage::UpdateLayout { mut inspect_result } => {
self.introspected_node = Some(inspect_result.inspect_node);
self.introspected_data = inspect_result.take_data();
self.update_layout(responses, context);
}
DataPanelMessage::ClearLayout => {
self.introspected_node = None;
self.introspected_data = None;
self.element_path.clear();
self.active_vector_table_tab = VectorTableTab::default();
self.update_layout(responses, context);
}
DataPanelMessage::PushToElementPath { index } => {
self.element_path.push(index);
self.update_layout(responses, context);
}
DataPanelMessage::TruncateElementPath { len } => {
self.element_path.truncate(len);
self.update_layout(responses, context);
}
DataPanelMessage::ViewVectorTableTab { tab } => {
self.active_vector_table_tab = tab;
self.update_layout(responses, context);
}
}
}
fn actions(&self) -> ActionList {
actions!(DataPanelMessage;)
}
}
impl DataPanelMessageHandler {
fn update_layout(&mut self, responses: &mut VecDeque<Message>, context: DataPanelMessageContext<'_>) {
let DataPanelMessageContext { network_interface, .. } = context;
let mut layout_data = LayoutData {
current_depth: 0,
desired_path: &mut self.element_path,
breadcrumbs: Vec::new(),
vector_table_tab: self.active_vector_table_tab,
};
// Main data visualization
let mut layout = self
.introspected_data
.as_ref()
.map(|instrospected_data| generate_layout(instrospected_data, &mut layout_data).unwrap_or_else(|| label("Visualization of this data type is not yet supported")))
.unwrap_or_default();
let mut widgets = Vec::new();
// Selected layer/node name
if let Some(node_id) = self.introspected_node {
let is_layer = network_interface.is_layer(&node_id, &[]);
widgets.extend([
if is_layer {
IconLabel::new("Layer").tooltip("Name of the selected layer").widget_holder()
} else {
IconLabel::new("Node").tooltip("Name of the selected node").widget_holder()
},
Separator::new(SeparatorType::Related).widget_holder(),
TextInput::new(network_interface.display_name(&node_id, &[]))
.tooltip(if is_layer { "Name of the selected layer" } else { "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()
})
.max_width(200)
.widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
]);
}
// Element path breadcrumbs
if !layout_data.breadcrumbs.is_empty() {
let breadcrumb = BreadcrumbTrailButtons::new(layout_data.breadcrumbs)
.on_update(|&len| DataPanelMessage::TruncateElementPath { len: len as usize }.into())
.widget_holder();
widgets.push(breadcrumb);
}
if !widgets.is_empty() {
layout.insert(0, LayoutGroup::Row { widgets });
}
responses.add(LayoutMessage::SendLayout {
layout: Layout::WidgetLayout(WidgetLayout { layout }),
layout_target: LayoutTarget::DataPanel,
});
}
}
struct LayoutData<'a> {
current_depth: usize,
desired_path: &'a mut Vec<usize>,
breadcrumbs: Vec<String>,
vector_table_tab: VectorTableTab,
}
macro_rules! generate_layout_downcast {
($introspected_data:expr, $data:expr, [ $($ty:ty),* $(,)? ]) => {
if false { None }
$(
else if let Some(io) = $introspected_data.downcast_ref::<IORecord<Context, $ty>>() {
Some(io.output.layout_with_breadcrumb($data))
}
)*
else { None }
}
}
// TODO: We simply try all these types sequentially. Find a better strategy.
fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'static>, data: &mut LayoutData) -> Option<Vec<LayoutGroup>> {
generate_layout_downcast!(introspected_data, data, [
Table<Artboard>,
Table<Graphic>,
Table<Vector>,
Table<Raster<CPU>>,
Table<Raster<GPU>>,
Table<Color>,
Table<GradientStops>,
f64,
u32,
u64,
bool,
String,
Option<f64>,
DVec2,
DAffine2,
])
}
fn column_headings(value: &[&str]) -> Vec<WidgetHolder> {
value.iter().map(|text| TextLabel::new(*text).widget_holder()).collect()
}
fn label(x: impl Into<String>) -> Vec<LayoutGroup> {
let error = vec![TextLabel::new(x).widget_holder()];
vec![LayoutGroup::Row { widgets: error }]
}
trait TableRowLayout {
fn type_name() -> &'static str;
fn identifier(&self) -> String;
fn layout_with_breadcrumb(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
data.breadcrumbs.push(self.identifier());
self.element_page(data)
}
fn element_widget(&self, index: usize) -> WidgetHolder {
TextButton::new(self.identifier())
.on_update(move |_| DataPanelMessage::PushToElementPath { index }.into())
.widget_holder()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
vec![]
}
}
impl<T: TableRowLayout> TableRowLayout for Table<T> {
fn type_name() -> &'static str {
"Table"
}
fn identifier(&self) -> String {
format!("Table<{}> ({} row{})", T::type_name(), self.len(), if self.len() == 1 { "" } else { "s" })
}
fn element_page(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
if let Some(index) = data.desired_path.get(data.current_depth).copied() {
if let Some(row) = self.get(index) {
data.current_depth += 1;
let result = row.element.layout_with_breadcrumb(data);
data.current_depth -= 1;
return result;
} else {
warn!("Desired path truncated");
data.desired_path.truncate(data.current_depth);
}
}
let mut rows = self
.iter()
.enumerate()
.map(|(index, row)| {
vec![
TextLabel::new(format!("{index}")).widget_holder(),
row.element.element_widget(index),
TextLabel::new(format_transform_matrix(row.transform)).widget_holder(),
TextLabel::new(format!("{}", row.alpha_blending)).widget_holder(),
TextLabel::new(row.source_node_id.map_or_else(|| "-".to_string(), |id| format!("{}", id.0))).widget_holder(),
]
})
.collect::<Vec<_>>();
rows.insert(0, column_headings(&["", "element", "transform", "alpha_blending", "source_node_id"]));
vec![LayoutGroup::Table { rows }]
}
}
impl TableRowLayout for Artboard {
fn type_name() -> &'static str {
"Artboard"
}
fn identifier(&self) -> String {
self.label.clone()
}
fn element_page(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
self.content.element_page(data)
}
}
impl TableRowLayout for Graphic {
fn type_name() -> &'static str {
"Graphic"
}
fn identifier(&self) -> String {
match self {
Self::Graphic(table) => table.identifier(),
Self::Vector(table) => table.identifier(),
Self::RasterCPU(table) => table.identifier(),
Self::RasterGPU(table) => table.identifier(),
Self::Color(table) => table.identifier(),
Self::Gradient(table) => table.identifier(),
}
}
// Don't put a breadcrumb for Graphic
fn layout_with_breadcrumb(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
self.element_page(data)
}
fn element_page(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
match self {
Self::Graphic(table) => table.layout_with_breadcrumb(data),
Self::Vector(table) => table.layout_with_breadcrumb(data),
Self::RasterCPU(table) => table.layout_with_breadcrumb(data),
Self::RasterGPU(table) => table.layout_with_breadcrumb(data),
Self::Color(table) => table.layout_with_breadcrumb(data),
Self::Gradient(table) => table.layout_with_breadcrumb(data),
}
}
}
impl TableRowLayout for Vector {
fn type_name() -> &'static str {
"Vector"
}
fn identifier(&self) -> String {
format!(
"Vector ({} point{}, {} segment{})",
self.point_domain.ids().len(),
if self.point_domain.ids().len() == 1 { "" } else { "s" },
self.segment_domain.ids().len(),
if self.segment_domain.ids().len() == 1 { "" } else { "s" }
)
}
fn element_page(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
let table_tab_entries = [VectorTableTab::Properties, VectorTableTab::Points, VectorTableTab::Segments, VectorTableTab::Regions]
.into_iter()
.map(|tab| {
RadioEntryData::new(format!("{tab:?}"))
.label(format!("{tab:?}"))
.on_update(move |_| DataPanelMessage::ViewVectorTableTab { tab }.into())
})
.collect();
let table_tabs = vec![RadioInput::new(table_tab_entries).selected_index(Some(data.vector_table_tab as u32)).widget_holder()];
let mut table_rows = Vec::new();
match data.vector_table_tab {
VectorTableTab::Properties => {
table_rows.push(column_headings(&["property", "value"]));
match self.style.fill.clone() {
Fill::None => table_rows.push(vec![
TextLabel::new("Fill").widget_holder(),
ColorInput::new(FillChoice::None).disabled(true).menu_direction(Some(MenuDirection::Top)).widget_holder(),
]),
Fill::Solid(color) => table_rows.push(vec![
TextLabel::new("Fill").widget_holder(),
ColorInput::new(FillChoice::Solid(color)).disabled(true).menu_direction(Some(MenuDirection::Top)).widget_holder(),
]),
Fill::Gradient(gradient) => {
table_rows.push(vec![
TextLabel::new("Fill").widget_holder(),
ColorInput::new(FillChoice::Gradient(gradient.stops))
.disabled(true)
.menu_direction(Some(MenuDirection::Top))
.widget_holder(),
]);
table_rows.push(vec![
TextLabel::new("Fill Gradient Type").widget_holder(),
TextLabel::new(gradient.gradient_type.to_string()).widget_holder(),
]);
table_rows.push(vec![
TextLabel::new("Fill Gradient Start").widget_holder(),
TextLabel::new(format_dvec2(gradient.start)).widget_holder(),
]);
table_rows.push(vec![TextLabel::new("Fill Gradient End").widget_holder(), TextLabel::new(format_dvec2(gradient.end)).widget_holder()]);
}
}
if let Some(stroke) = self.style.stroke.clone() {
let color = if let Some(color) = stroke.color { FillChoice::Solid(color) } else { FillChoice::None };
table_rows.push(vec![
TextLabel::new("Stroke").widget_holder(),
ColorInput::new(color).disabled(true).menu_direction(Some(MenuDirection::Top)).widget_holder(),
]);
table_rows.push(vec![TextLabel::new("Stroke Weight").widget_holder(), TextLabel::new(format!("{} px", stroke.weight)).widget_holder()]);
table_rows.push(vec![
TextLabel::new("Stroke Dash Lengths").widget_holder(),
TextLabel::new(if stroke.dash_lengths.is_empty() {
"-".to_string()
} else {
format!("[{}]", stroke.dash_lengths.iter().map(|x| format!("{x} px")).collect::<Vec<_>>().join(", "))
})
.widget_holder(),
]);
table_rows.push(vec![
TextLabel::new("Stroke Dash Offset").widget_holder(),
TextLabel::new(format!("{}", stroke.dash_offset)).widget_holder(),
]);
table_rows.push(vec![TextLabel::new("Stroke Cap").widget_holder(), TextLabel::new(stroke.cap.to_string()).widget_holder()]);
table_rows.push(vec![TextLabel::new("Stroke Join").widget_holder(), TextLabel::new(stroke.join.to_string()).widget_holder()]);
table_rows.push(vec![
TextLabel::new("Stroke Join Miter Limit").widget_holder(),
TextLabel::new(format!("{}", stroke.join_miter_limit)).widget_holder(),
]);
table_rows.push(vec![TextLabel::new("Stroke Align").widget_holder(), TextLabel::new(stroke.align.to_string()).widget_holder()]);
table_rows.push(vec![
TextLabel::new("Stroke Transform").widget_holder(),
TextLabel::new(format_transform_matrix(&stroke.transform)).widget_holder(),
]);
table_rows.push(vec![
TextLabel::new("Stroke Non-Scaling").widget_holder(),
TextLabel::new((if stroke.non_scaling { "Yes" } else { "No" }).to_string()).widget_holder(),
]);
table_rows.push(vec![
TextLabel::new("Stroke Paint Order").widget_holder(),
TextLabel::new(stroke.paint_order.to_string()).widget_holder(),
]);
}
let colinear = self.colinear_manipulators.iter().map(|[a, b]| format!("[{a} / {b}]")).collect::<Vec<_>>().join(", ");
let colinear = if colinear.is_empty() { "-".to_string() } else { colinear };
table_rows.push(vec![TextLabel::new("Colinear Handle IDs").widget_holder(), TextLabel::new(colinear).widget_holder()]);
table_rows.push(vec![
TextLabel::new("Upstream Nested Layers").widget_holder(),
TextLabel::new(if self.upstream_nested_layers.is_some() {
"Yes (this preserves references to its upstream nested layers for editing by tools)"
} else {
"No (this doesn't preserve references to its upstream nested layers for editing by tools)"
})
.widget_holder(),
]);
}
VectorTableTab::Points => {
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()]),
);
}
VectorTableTab::Segments => {
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(),
TextLabel::new(format!("{end}")).widget_holder(),
TextLabel::new(format!("{handles:?}")).widget_holder(),
]
}));
}
VectorTableTab::Regions => {
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(),
TextLabel::new(format!("{}", fill.inner())).widget_holder(),
]
}));
}
}
vec![LayoutGroup::Row { widgets: table_tabs }, LayoutGroup::Table { rows: table_rows }]
}
}
impl TableRowLayout for Raster<CPU> {
fn type_name() -> &'static str {
"Raster"
}
fn identifier(&self) -> String {
format!("Raster ({}x{})", self.width, self.height)
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let base64_string = self.data().base64_string.clone().unwrap_or_else(|| {
use base64::Engine;
let output = self.data().to_png();
let preamble = "data:image/png;base64,";
let mut base64_string = String::with_capacity(preamble.len() + output.len() * 4);
base64_string.push_str(preamble);
base64::engine::general_purpose::STANDARD.encode_string(output, &mut base64_string);
base64_string
});
let widgets = vec![ImageLabel::new(base64_string).widget_holder()];
vec![LayoutGroup::Row { widgets }]
}
}
impl TableRowLayout for Raster<GPU> {
fn type_name() -> &'static str {
"Raster"
}
fn identifier(&self) -> String {
format!("Raster ({}x{})", self.data().width(), self.data().height())
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![TextLabel::new("Raster is a texture on the GPU and cannot currently be displayed here").widget_holder()];
vec![LayoutGroup::Row { widgets }]
}
}
impl TableRowLayout for Color {
fn type_name() -> &'static str {
"Color"
}
fn identifier(&self) -> String {
format!("Color (#{})", self.to_gamma_srgb().to_rgba_hex_srgb())
}
fn element_widget(&self, _index: usize) -> WidgetHolder {
ColorInput::new(FillChoice::Solid(*self)).disabled(true).menu_direction(Some(MenuDirection::Top)).widget_holder()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![self.element_widget(0)];
vec![LayoutGroup::Row { widgets }]
}
}
impl TableRowLayout for GradientStops {
fn type_name() -> &'static str {
"Gradient"
}
fn identifier(&self) -> String {
format!("Gradient ({} stops)", self.0.len())
}
fn element_widget(&self, _index: usize) -> WidgetHolder {
ColorInput::new(FillChoice::Gradient(self.clone()))
.disabled(true)
.menu_direction(Some(MenuDirection::Top))
.widget_holder()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![self.element_widget(0)];
vec![LayoutGroup::Row { widgets }]
}
}
impl TableRowLayout for f64 {
fn type_name() -> &'static str {
"Number (f64)"
}
fn identifier(&self) -> String {
"Number (f64)".to_string()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![TextLabel::new(self.to_string()).widget_holder()];
vec![LayoutGroup::Row { widgets }]
}
}
impl TableRowLayout for u32 {
fn type_name() -> &'static str {
"Number (u32)"
}
fn identifier(&self) -> String {
"Number (u32)".to_string()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![TextLabel::new(self.to_string()).widget_holder()];
vec![LayoutGroup::Row { widgets }]
}
}
impl TableRowLayout for u64 {
fn type_name() -> &'static str {
"Number (u64)"
}
fn identifier(&self) -> String {
"Number (u64)".to_string()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![TextLabel::new(self.to_string()).widget_holder()];
vec![LayoutGroup::Row { widgets }]
}
}
impl TableRowLayout for bool {
fn type_name() -> &'static str {
"Bool"
}
fn identifier(&self) -> String {
"Bool".to_string()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![TextLabel::new(self.to_string()).widget_holder()];
vec![LayoutGroup::Row { widgets }]
}
}
impl TableRowLayout for String {
fn type_name() -> &'static str {
"String"
}
fn identifier(&self) -> String {
"String".to_string()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![TextAreaInput::new(self.to_string()).disabled(true).widget_holder()];
vec![LayoutGroup::Row { widgets }]
}
}
impl TableRowLayout for Option<f64> {
fn type_name() -> &'static str {
"Option<f64>"
}
fn identifier(&self) -> String {
"Option<f64>".to_string()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![TextLabel::new(format!("{self:?}")).widget_holder()];
vec![LayoutGroup::Row { widgets }]
}
}
impl TableRowLayout for DVec2 {
fn type_name() -> &'static str {
"Vec2"
}
fn identifier(&self) -> String {
"Vec2".to_string()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![TextLabel::new(format!("({}, {})", self.x, self.y)).widget_holder()];
vec![LayoutGroup::Row { widgets }]
}
}
impl TableRowLayout for DAffine2 {
fn type_name() -> &'static str {
"Transform"
}
fn identifier(&self) -> String {
"Transform".to_string()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![TextLabel::new(format_transform_matrix(self)).widget_holder()];
vec![LayoutGroup::Row { widgets }]
}
}
fn format_transform_matrix(transform: &DAffine2) -> String {
let (scale, angle, translation) = transform.to_scale_angle_translation();
let rotation = if angle == -0. { 0. } else { angle.to_degrees() };
let round = |x: f64| (x * 1e3).round() / 1e3;
format!(
"Location: ({} px, {} px) — Rotation: {rotation:2}° — Scale: ({}x, {}x)",
round(translation.x),
round(translation.y),
round(scale.x),
round(scale.y)
)
}
fn format_dvec2(value: DVec2) -> String {
let round = |x: f64| (x * 1e3).round() / 1e3;
format!("({} px, {} px)", round(value.x), round(value.y))
}

View file

@ -0,0 +1,7 @@
mod data_panel_message;
mod data_panel_message_handler;
#[doc(inline)]
pub use data_panel_message::*;
#[doc(inline)]
pub use data_panel_message_handler::*;

View file

@ -2,6 +2,7 @@ use std::path::PathBuf;
use super::utility_types::misc::{GroupFolderType, SnappingState}; use super::utility_types::misc::{GroupFolderType, SnappingState};
use crate::messages::input_mapper::utility_types::input_keyboard::Key; use crate::messages::input_mapper::utility_types::input_keyboard::Key;
use crate::messages::portfolio::document::data_panel::DataPanelMessage;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::overlays::utility_types::OverlaysType; use crate::messages::portfolio::document::overlays::utility_types::OverlaysType;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
@ -33,6 +34,8 @@ pub enum DocumentMessage {
Overlays(OverlaysMessage), Overlays(OverlaysMessage),
#[child] #[child]
PropertiesPanel(PropertiesPanelMessage), PropertiesPanel(PropertiesPanelMessage),
#[child]
DataPanel(DataPanelMessage),
// Messages // Messages
AlignSelectedLayers { AlignSelectedLayers {
@ -50,7 +53,9 @@ pub enum DocumentMessage {
DocumentHistoryBackward, DocumentHistoryBackward,
DocumentHistoryForward, DocumentHistoryForward,
DocumentStructureChanged, DocumentStructureChanged,
DrawArtboardOverlays(OverlayContext), DrawArtboardOverlays {
context: OverlayContext,
},
DuplicateSelectedLayers, DuplicateSelectedLayers,
EnterNestedNetwork { EnterNestedNetwork {
node_id: NodeId, node_id: NodeId,
@ -69,9 +74,15 @@ pub enum DocumentMessage {
open: bool, open: bool,
}, },
GraphViewOverlayToggle, GraphViewOverlayToggle,
GridOptions(GridSnapping), GridOptions {
GridOverlays(OverlayContext), options: GridSnapping,
GridVisibility(bool), },
GridOverlays {
context: OverlayContext,
},
GridVisibility {
visible: bool,
},
GroupSelectedLayers { GroupSelectedLayers {
group_folder_type: GroupFolderType, group_folder_type: GroupFolderType,
}, },

View file

@ -9,6 +9,7 @@ use crate::application::{GRAPHITE_GIT_COMMIT_HASH, generate_uuid};
use crate::consts::{ASYMPTOTIC_EFFECT, COLOR_OVERLAY_GRAY, DEFAULT_DOCUMENT_NAME, FILE_SAVE_SUFFIX, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ROTATE_SNAP_INTERVAL}; use crate::consts::{ASYMPTOTIC_EFFECT, COLOR_OVERLAY_GRAY, DEFAULT_DOCUMENT_NAME, FILE_SAVE_SUFFIX, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ROTATE_SNAP_INTERVAL};
use crate::messages::input_mapper::utility_types::macros::action_keys; use crate::messages::input_mapper::utility_types::macros::action_keys;
use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::data_panel::{DataPanelMessageContext, DataPanelMessageHandler};
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
use crate::messages::portfolio::document::node_graph::NodeGraphMessageContext; use crate::messages::portfolio::document::node_graph::NodeGraphMessageContext;
use crate::messages::portfolio::document::overlays::grid_overlays::{grid_overlay, overlay_options}; use crate::messages::portfolio::document::overlays::grid_overlays::{grid_overlay, overlay_options};
@ -18,6 +19,7 @@ use crate::messages::portfolio::document::utility_types::document_metadata::{Doc
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, DocumentMode, FlipAxis, PTZ}; use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, DocumentMode, FlipAxis, PTZ};
use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, InputConnector, NodeTemplate}; use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, InputConnector, NodeTemplate};
use crate::messages::portfolio::document::utility_types::nodes::RawBuffer; use crate::messages::portfolio::document::utility_types::nodes::RawBuffer;
use crate::messages::portfolio::utility_types::PanelType;
use crate::messages::portfolio::utility_types::PersistentData; use crate::messages::portfolio::utility_types::PersistentData;
use crate::messages::prelude::*; use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils::{self, get_blend_mode, get_fill, get_opacity}; use crate::messages::tool::common_functionality::graph_modification_utils::{self, get_blend_mode, get_fill, get_opacity};
@ -25,7 +27,6 @@ use crate::messages::tool::tool_messages::select_tool::SelectToolPointerKeys;
use crate::messages::tool::tool_messages::tool_prelude::Key; use crate::messages::tool::tool_messages::tool_prelude::Key;
use crate::messages::tool::utility_types::ToolType; use crate::messages::tool::utility_types::ToolType;
use crate::node_graph_executor::NodeGraphExecutor; use crate::node_graph_executor::NodeGraphExecutor;
use bezier_rs::Subpath;
use glam::{DAffine2, DVec2, IVec2}; use glam::{DAffine2, DVec2, IVec2};
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput, NodeNetwork, OldNodeNetwork}; use graph_craft::document::{NodeId, NodeInput, NodeNetwork, OldNodeNetwork};
@ -33,10 +34,13 @@ use graphene_std::math::quad::Quad;
use graphene_std::path_bool::{boolean_intersect, path_bool_lib}; use graphene_std::path_bool::{boolean_intersect, path_bool_lib};
use graphene_std::raster::BlendMode; use graphene_std::raster::BlendMode;
use graphene_std::raster_types::Raster; use graphene_std::raster_types::Raster;
use graphene_std::subpath::Subpath;
use graphene_std::table::Table; use graphene_std::table::Table;
use graphene_std::vector::PointId; use graphene_std::vector::PointId;
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType}; use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
use graphene_std::vector::misc::{dvec2_to_point, point_to_dvec2};
use graphene_std::vector::style::ViewMode; use graphene_std::vector::style::ViewMode;
use kurbo::{Affine, CubicBez, Line, ParamCurve, PathSeg, QuadBez};
use std::path::PathBuf; use std::path::PathBuf;
use std::time::Duration; use std::time::Duration;
@ -49,6 +53,9 @@ pub struct DocumentMessageContext<'a> {
pub current_tool: &'a ToolType, pub current_tool: &'a ToolType,
pub preferences: &'a PreferencesMessageHandler, pub preferences: &'a PreferencesMessageHandler,
pub device_pixel_ratio: f64, pub device_pixel_ratio: f64,
pub data_panel_open: bool,
pub layers_panel_open: bool,
pub properties_panel_open: bool,
} }
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, ExtractField)] #[derive(Clone, Debug, serde::Serialize, serde::Deserialize, ExtractField)]
@ -63,9 +70,11 @@ pub struct DocumentMessageHandler {
#[serde(skip)] #[serde(skip)]
pub node_graph_handler: NodeGraphMessageHandler, pub node_graph_handler: NodeGraphMessageHandler,
#[serde(skip)] #[serde(skip)]
overlays_message_handler: OverlaysMessageHandler, pub overlays_message_handler: OverlaysMessageHandler,
#[serde(skip)] #[serde(skip)]
properties_panel_message_handler: PropertiesPanelMessageHandler, pub properties_panel_message_handler: PropertiesPanelMessageHandler,
#[serde(skip)]
pub data_panel_message_handler: DataPanelMessageHandler,
// ============================================ // ============================================
// Fields that are saved in the document format // Fields that are saved in the document format
@ -144,6 +153,7 @@ impl Default for DocumentMessageHandler {
node_graph_handler: NodeGraphMessageHandler::default(), node_graph_handler: NodeGraphMessageHandler::default(),
overlays_message_handler: OverlaysMessageHandler::default(), overlays_message_handler: OverlaysMessageHandler::default(),
properties_panel_message_handler: PropertiesPanelMessageHandler::default(), properties_panel_message_handler: PropertiesPanelMessageHandler::default(),
data_panel_message_handler: DataPanelMessageHandler::default(),
// ============================================ // ============================================
// Fields that are saved in the document format // Fields that are saved in the document format
// ============================================ // ============================================
@ -186,6 +196,9 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
current_tool, current_tool,
preferences, preferences,
device_pixel_ratio, device_pixel_ratio,
data_panel_open,
layers_panel_open,
properties_panel_open,
} = context; } = context;
match message { match message {
@ -223,9 +236,20 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
document_name: self.name.as_str(), document_name: self.name.as_str(),
executor, executor,
persistent_data, persistent_data,
properties_panel_open,
}; };
self.properties_panel_message_handler.process_message(message, responses, context); self.properties_panel_message_handler.process_message(message, responses, context);
} }
DocumentMessage::DataPanel(message) => {
self.data_panel_message_handler.process_message(
message,
responses,
DataPanelMessageContext {
network_interface: &mut self.network_interface,
data_panel_open,
},
);
}
DocumentMessage::NodeGraph(message) => { DocumentMessage::NodeGraph(message) => {
self.node_graph_handler.process_message( self.node_graph_handler.process_message(
message, message,
@ -241,6 +265,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
graph_fade_artwork_percentage: self.graph_fade_artwork_percentage, graph_fade_artwork_percentage: self.graph_fade_artwork_percentage,
navigation_handler: &self.navigation_handler, navigation_handler: &self.navigation_handler,
preferences, preferences,
layers_panel_open,
}, },
); );
} }
@ -356,14 +381,17 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
DocumentMessage::DocumentHistoryBackward => self.undo_with_history(ipp, responses), DocumentMessage::DocumentHistoryBackward => self.undo_with_history(ipp, responses),
DocumentMessage::DocumentHistoryForward => self.redo_with_history(ipp, responses), DocumentMessage::DocumentHistoryForward => self.redo_with_history(ipp, responses),
DocumentMessage::DocumentStructureChanged => { DocumentMessage::DocumentStructureChanged => {
self.update_layers_panel_control_bar_widgets(responses); if layers_panel_open {
self.update_layers_panel_bottom_bar_widgets(responses); self.network_interface.load_structure();
let data_buffer: RawBuffer = self.serialize_root();
self.network_interface.load_structure(); self.update_layers_panel_control_bar_widgets(layers_panel_open, responses);
let data_buffer: RawBuffer = self.serialize_root(); self.update_layers_panel_bottom_bar_widgets(layers_panel_open, responses);
responses.add(FrontendMessage::UpdateDocumentLayerStructure { data_buffer });
responses.add(FrontendMessage::UpdateDocumentLayerStructure { data_buffer });
}
} }
DocumentMessage::DrawArtboardOverlays(overlay_context) => { DocumentMessage::DrawArtboardOverlays { context: overlay_context } => {
if !overlay_context.visibility_settings.artboard_name() { if !overlay_context.visibility_settings.artboard_name() {
return; return;
} }
@ -554,24 +582,25 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
responses.add(NodeGraphMessage::UpdateHints); responses.add(NodeGraphMessage::UpdateHints);
} else { } else {
responses.add(ToolMessage::ActivateTool { tool_type: *current_tool }); responses.add(ToolMessage::ActivateTool { tool_type: *current_tool });
responses.add(OverlaysMessage::Draw); // Redraw overlays when graph is closed
} }
} }
DocumentMessage::GraphViewOverlayToggle => { DocumentMessage::GraphViewOverlayToggle => {
responses.add(DocumentMessage::GraphViewOverlay { open: !self.graph_view_overlay_open }); responses.add(DocumentMessage::GraphViewOverlay { open: !self.graph_view_overlay_open });
} }
DocumentMessage::GridOptions(grid) => { DocumentMessage::GridOptions { options } => {
self.snapping_state.grid = grid; self.snapping_state.grid = options;
self.snapping_state.grid_snapping = true; self.snapping_state.grid_snapping = true;
responses.add(OverlaysMessage::Draw); responses.add(OverlaysMessage::Draw);
responses.add(PortfolioMessage::UpdateDocumentWidgets); responses.add(PortfolioMessage::UpdateDocumentWidgets);
} }
DocumentMessage::GridOverlays(mut overlay_context) => { DocumentMessage::GridOverlays { context: mut overlay_context } => {
if self.snapping_state.grid_snapping { if self.snapping_state.grid_snapping {
grid_overlay(self, &mut overlay_context) grid_overlay(self, &mut overlay_context)
} }
} }
DocumentMessage::GridVisibility(enabled) => { DocumentMessage::GridVisibility { visible } => {
self.snapping_state.grid_snapping = enabled; self.snapping_state.grid_snapping = visible;
responses.add(OverlaysMessage::Draw); responses.add(OverlaysMessage::Draw);
} }
DocumentMessage::GroupSelectedLayers { group_folder_type } => { DocumentMessage::GroupSelectedLayers { group_folder_type } => {
@ -1033,7 +1062,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
if !parent_layers.is_empty() { if !parent_layers.is_empty() {
let nodes = parent_layers.into_iter().collect(); let nodes = parent_layers.into_iter().collect();
responses.add(NodeGraphMessage::SelectedNodesSet { nodes }); responses.add(NodeGraphMessage::SelectedNodesSet { nodes });
responses.add(BroadcastEvent::SelectionChanged); responses.add(EventMessage::SelectionChanged);
} }
} }
DocumentMessage::SelectAllLayers => { DocumentMessage::SelectAllLayers => {
@ -1108,7 +1137,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
} else { } else {
responses.add_front(NodeGraphMessage::SelectedNodesAdd { nodes: vec![id] }); responses.add_front(NodeGraphMessage::SelectedNodesAdd { nodes: vec![id] });
} }
responses.add(BroadcastEvent::SelectionChanged); responses.add(EventMessage::SelectionChanged);
} else { } else {
nodes.push(id); nodes.push(id);
} }
@ -1128,7 +1157,6 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
} }
} }
DocumentMessage::SetActivePanel { active_panel: panel } => { DocumentMessage::SetActivePanel { active_panel: panel } => {
use crate::messages::portfolio::utility_types::PanelType;
match panel { match panel {
PanelType::Document => { PanelType::Document => {
if self.graph_view_overlay_open { if self.graph_view_overlay_open {
@ -1178,7 +1206,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
Some(overlays_type) => overlays_type, Some(overlays_type) => overlays_type,
None => { None => {
visibility_settings.all = visible; visibility_settings.all = visible;
responses.add(BroadcastEvent::ToolAbort); responses.add(EventMessage::ToolAbort);
responses.add(OverlaysMessage::Draw); responses.add(OverlaysMessage::Draw);
return; return;
} }
@ -1201,7 +1229,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
OverlaysType::Handles => visibility_settings.handles = visible, OverlaysType::Handles => visibility_settings.handles = visible,
} }
responses.add(BroadcastEvent::ToolAbort); responses.add(EventMessage::ToolAbort);
responses.add(OverlaysMessage::Draw); responses.add(OverlaysMessage::Draw);
} }
DocumentMessage::SetRangeSelectionLayer { new_layer } => { DocumentMessage::SetRangeSelectionLayer { new_layer } => {
@ -1415,12 +1443,14 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
let transform = self.navigation_handler.calculate_offset_transform(ipp.viewport_bounds.center(), &self.document_ptz); let transform = self.navigation_handler.calculate_offset_transform(ipp.viewport_bounds.center(), &self.document_ptz);
self.network_interface.set_document_to_viewport_transform(transform); self.network_interface.set_document_to_viewport_transform(transform);
// Ensure selection box is kept in sync with the pointer when the PTZ changes // Ensure selection box is kept in sync with the pointer when the PTZ changes
responses.add(SelectToolMessage::PointerMove(SelectToolPointerKeys { responses.add(SelectToolMessage::PointerMove {
axis_align: Key::Shift, modifier_keys: SelectToolPointerKeys {
snap_angle: Key::Shift, axis_align: Key::Shift,
center: Key::Alt, snap_angle: Key::Shift,
duplicate: Key::Alt, center: Key::Alt,
})); duplicate: Key::Alt,
},
});
responses.add(NodeGraphMessage::RunDocumentGraph); responses.add(NodeGraphMessage::RunDocumentGraph);
} else { } else {
let Some(network_metadata) = self.network_interface.network_metadata(&self.breadcrumb_network_path) else { let Some(network_metadata) = self.network_interface.network_metadata(&self.breadcrumb_network_path) else {
@ -1449,11 +1479,11 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
} }
DocumentMessage::SelectionStepBack => { DocumentMessage::SelectionStepBack => {
self.network_interface.selection_step_back(&self.selection_network_path); self.network_interface.selection_step_back(&self.selection_network_path);
responses.add(BroadcastEvent::SelectionChanged); responses.add(EventMessage::SelectionChanged);
} }
DocumentMessage::SelectionStepForward => { DocumentMessage::SelectionStepForward => {
self.network_interface.selection_step_forward(&self.selection_network_path); self.network_interface.selection_step_forward(&self.selection_network_path);
responses.add(BroadcastEvent::SelectionChanged); responses.add(EventMessage::SelectionChanged);
} }
DocumentMessage::WrapContentInArtboard { place_artboard_at_origin } => { DocumentMessage::WrapContentInArtboard { place_artboard_at_origin } => {
// Get bounding box of all layers // Get bounding box of all layers
@ -2456,7 +2486,7 @@ impl DocumentMessageHandler {
.icon("Grid") .icon("Grid")
.tooltip("Grid") .tooltip("Grid")
.tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::ToggleGridVisibility)) .tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::ToggleGridVisibility))
.on_update(|optional_input: &CheckboxInput| DocumentMessage::GridVisibility(optional_input.checked).into()) .on_update(|optional_input: &CheckboxInput| DocumentMessage::GridVisibility { visible: optional_input.checked }.into())
.widget_holder(), .widget_holder(),
PopoverButton::new() PopoverButton::new()
.popover_layout(overlay_options(&self.snapping_state.grid)) .popover_layout(overlay_options(&self.snapping_state.grid))
@ -2549,7 +2579,11 @@ impl DocumentMessageHandler {
responses.add(NodeGraphMessage::ForceRunDocumentGraph); responses.add(NodeGraphMessage::ForceRunDocumentGraph);
} }
pub fn update_layers_panel_control_bar_widgets(&self, responses: &mut VecDeque<Message>) { pub fn update_layers_panel_control_bar_widgets(&self, layers_panel_open: bool, responses: &mut VecDeque<Message>) {
if !layers_panel_open {
return;
}
// Get an iterator over the selected layers (excluding artboards which don't have an opacity or blend mode). // Get an iterator over the selected layers (excluding artboards which don't have an opacity or blend mode).
let selected_nodes = self.network_interface.selected_nodes(); let selected_nodes = self.network_interface.selected_nodes();
let selected_layers_except_artboards = selected_nodes.selected_layers_except_artboards(&self.network_interface); let selected_layers_except_artboards = selected_nodes.selected_layers_except_artboards(&self.network_interface);
@ -2707,7 +2741,11 @@ impl DocumentMessageHandler {
}); });
} }
pub fn update_layers_panel_bottom_bar_widgets(&self, responses: &mut VecDeque<Message>) { pub fn update_layers_panel_bottom_bar_widgets(&self, layers_panel_open: bool, responses: &mut VecDeque<Message>) {
if !layers_panel_open {
return;
}
let selected_nodes = self.network_interface.selected_nodes(); let selected_nodes = self.network_interface.selected_nodes();
let mut selected_layers = selected_nodes.selected_layers(self.metadata()); let mut selected_layers = selected_nodes.selected_layers(self.metadata());
let selected_layer = selected_layers.next(); let selected_layer = selected_layers.next();
@ -2949,10 +2987,10 @@ fn quad_to_path_lib_segments(quad: Quad) -> Vec<path_bool_lib::PathSegment> {
} }
fn click_targets_to_path_lib_segments<'a>(click_targets: impl Iterator<Item = &'a ClickTarget>, transform: DAffine2) -> Vec<path_bool_lib::PathSegment> { fn click_targets_to_path_lib_segments<'a>(click_targets: impl Iterator<Item = &'a ClickTarget>, transform: DAffine2) -> Vec<path_bool_lib::PathSegment> {
let segment = |bezier: bezier_rs::Bezier| match bezier.handles { let segment = |bezier: PathSeg| match bezier {
bezier_rs::BezierHandles::Linear => path_bool_lib::PathSegment::Line(bezier.start, bezier.end), PathSeg::Line(line) => path_bool_lib::PathSegment::Line(point_to_dvec2(line.p0), point_to_dvec2(line.p1)),
bezier_rs::BezierHandles::Quadratic { handle } => path_bool_lib::PathSegment::Quadratic(bezier.start, handle, bezier.end), PathSeg::Quad(quad_bez) => path_bool_lib::PathSegment::Quadratic(point_to_dvec2(quad_bez.p0), point_to_dvec2(quad_bez.p1), point_to_dvec2(quad_bez.p2)),
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => path_bool_lib::PathSegment::Cubic(bezier.start, handle_start, handle_end, bezier.end), PathSeg::Cubic(cubic_bez) => path_bool_lib::PathSegment::Cubic(point_to_dvec2(cubic_bez.p0), point_to_dvec2(cubic_bez.p1), point_to_dvec2(cubic_bez.p2), point_to_dvec2(cubic_bez.p3)),
}; };
click_targets click_targets
.filter_map(|target| { .filter_map(|target| {
@ -2963,7 +3001,7 @@ fn click_targets_to_path_lib_segments<'a>(click_targets: impl Iterator<Item = &'
} }
}) })
.flatten() .flatten()
.map(|bezier| segment(bezier.apply_transformation(|x| transform.transform_point2(x)))) .map(|bezier| segment(Affine::new(transform.to_cols_array()) * bezier))
.collect() .collect()
} }
@ -2986,11 +3024,11 @@ impl<'a> ClickXRayIter<'a> {
/// Handles the checking of the layer where the target is a rect or path /// Handles the checking of the layer where the target is a rect or path
fn check_layer_area_target(&mut self, click_targets: Option<&Vec<ClickTarget>>, clip: bool, layer: LayerNodeIdentifier, path: Vec<path_bool_lib::PathSegment>, transform: DAffine2) -> XRayResult { fn check_layer_area_target(&mut self, click_targets: Option<&Vec<ClickTarget>>, clip: bool, layer: LayerNodeIdentifier, path: Vec<path_bool_lib::PathSegment>, transform: DAffine2) -> XRayResult {
// Convert back to Bezier-rs types for intersections // Convert back to Kurbo types for intersections
let segment = |bezier: &path_bool_lib::PathSegment| match *bezier { let segment = |bezier: &path_bool_lib::PathSegment| match *bezier {
path_bool_lib::PathSegment::Line(start, end) => bezier_rs::Bezier::from_linear_dvec2(start, end), path_bool_lib::PathSegment::Line(start, end) => PathSeg::Line(Line::new(dvec2_to_point(start), dvec2_to_point(end))),
path_bool_lib::PathSegment::Cubic(start, h1, h2, end) => bezier_rs::Bezier::from_cubic_dvec2(start, h1, h2, end), path_bool_lib::PathSegment::Cubic(start, h1, h2, end) => PathSeg::Cubic(CubicBez::new(dvec2_to_point(start), dvec2_to_point(h1), dvec2_to_point(h2), dvec2_to_point(end))),
path_bool_lib::PathSegment::Quadratic(start, h1, end) => bezier_rs::Bezier::from_quadratic_dvec2(start, h1, end), path_bool_lib::PathSegment::Quadratic(start, h1, end) => PathSeg::Quad(QuadBez::new(dvec2_to_point(start), dvec2_to_point(h1), dvec2_to_point(end))),
path_bool_lib::PathSegment::Arc(_, _, _, _, _, _, _) => unimplemented!(), path_bool_lib::PathSegment::Arc(_, _, _, _, _, _, _) => unimplemented!(),
}; };
let get_clip = || path.iter().map(segment); let get_clip = || path.iter().map(segment);
@ -3039,7 +3077,10 @@ impl<'a> ClickXRayIter<'a> {
XRayTarget::Quad(quad) => self.check_layer_area_target(click_targets, clip, layer, quad_to_path_lib_segments(*quad), transform), XRayTarget::Quad(quad) => self.check_layer_area_target(click_targets, clip, layer, quad_to_path_lib_segments(*quad), transform),
XRayTarget::Path(path) => self.check_layer_area_target(click_targets, clip, layer, path.clone(), transform), XRayTarget::Path(path) => self.check_layer_area_target(click_targets, clip, layer, path.clone(), transform),
XRayTarget::Polygon(polygon) => { XRayTarget::Polygon(polygon) => {
let polygon = polygon.iter_closed().map(|line| path_bool_lib::PathSegment::Line(line.start, line.end)).collect(); let polygon = polygon
.iter_closed()
.map(|line| path_bool_lib::PathSegment::Line(point_to_dvec2(line.start()), point_to_dvec2(line.end())))
.collect();
self.check_layer_area_target(click_targets, clip, layer, polygon, transform) self.check_layer_area_target(click_targets, clip, layer, polygon, transform)
} }
} }

View file

@ -2,13 +2,13 @@ use super::utility_types::TransformIn;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::NodeTemplate; use crate::messages::portfolio::document::utility_types::network_interface::NodeTemplate;
use crate::messages::prelude::*; use crate::messages::prelude::*;
use bezier_rs::Subpath;
use glam::{DAffine2, IVec2}; use glam::{DAffine2, IVec2};
use graph_craft::document::NodeId; use graph_craft::document::NodeId;
use graphene_std::Artboard; use graphene_std::Artboard;
use graphene_std::brush::brush_stroke::BrushStroke; use graphene_std::brush::brush_stroke::BrushStroke;
use graphene_std::raster::BlendMode; use graphene_std::raster::BlendMode;
use graphene_std::raster_types::{CPU, Raster}; use graphene_std::raster_types::{CPU, Raster};
use graphene_std::subpath::Subpath;
use graphene_std::table::Table; use graphene_std::table::Table;
use graphene_std::text::{Font, TypesettingConfig}; use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::PointId; use graphene_std::vector::PointId;

View file

@ -426,7 +426,6 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, t
Fill::Gradient(Gradient { Fill::Gradient(Gradient {
start, start,
end, end,
transform: DAffine2::IDENTITY,
gradient_type: GradientType::Linear, gradient_type: GradientType::Linear,
stops, stops,
}) })
@ -453,7 +452,6 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, t
Fill::Gradient(Gradient { Fill::Gradient(Gradient {
start, start,
end, end,
transform: DAffine2::IDENTITY,
gradient_type: GradientType::Radial, gradient_type: GradientType::Radial,
stops, stops,
}) })

View file

@ -1,8 +1,8 @@
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeNetworkInterface}; use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeNetworkInterface};
use bezier_rs::Subpath;
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput}; use graph_craft::document::{NodeId, NodeInput};
use graphene_std::subpath::Subpath;
use graphene_std::vector::PointId; use graphene_std::vector::PointId;
/// Convert an affine transform into the tuple `(scale, angle, translation, shear)` assuming `shear.y = 0`. /// Convert an affine transform into the tuple `(scale, angle, translation, shear)` assuming `shear.y = 0`.

View file

@ -3,7 +3,6 @@ use crate::messages::portfolio::document::node_graph::document_node_definitions:
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{self, InputConnector, NodeNetworkInterface, OutputConnector}; use crate::messages::portfolio::document::utility_types::network_interface::{self, InputConnector, NodeNetworkInterface, OutputConnector};
use crate::messages::prelude::*; use crate::messages::prelude::*;
use bezier_rs::Subpath;
use glam::{DAffine2, IVec2}; use glam::{DAffine2, IVec2};
use graph_craft::concrete; use graph_craft::concrete;
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
@ -12,6 +11,7 @@ use graphene_std::Artboard;
use graphene_std::brush::brush_stroke::BrushStroke; use graphene_std::brush::brush_stroke::BrushStroke;
use graphene_std::raster::BlendMode; use graphene_std::raster::BlendMode;
use graphene_std::raster_types::{CPU, Raster}; use graphene_std::raster_types::{CPU, Raster};
use graphene_std::subpath::Subpath;
use graphene_std::table::Table; use graphene_std::table::Table;
use graphene_std::text::{Font, TypesettingConfig}; use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::Vector; use graphene_std::vector::Vector;
@ -135,7 +135,7 @@ impl<'a> ModifyInputsContext<'a> {
Some(NodeInput::value(TaggedValue::Graphic(Default::default()), true)), Some(NodeInput::value(TaggedValue::Graphic(Default::default()), true)),
Some(NodeInput::value(TaggedValue::DVec2(artboard.location.into()), false)), Some(NodeInput::value(TaggedValue::DVec2(artboard.location.into()), false)),
Some(NodeInput::value(TaggedValue::DVec2(artboard.dimensions.into()), false)), Some(NodeInput::value(TaggedValue::DVec2(artboard.dimensions.into()), false)),
Some(NodeInput::value(TaggedValue::Color(artboard.background), false)), Some(NodeInput::value(TaggedValue::Color(Table::new_from_element(artboard.background)), false)),
Some(NodeInput::value(TaggedValue::Bool(artboard.clip), false)), Some(NodeInput::value(TaggedValue::Bool(artboard.clip), false)),
]); ]);
self.network_interface.insert_node(new_id, artboard_node_template, &[]); self.network_interface.insert_node(new_id, artboard_node_template, &[]);
@ -329,11 +329,11 @@ impl<'a> ModifyInputsContext<'a> {
match &fill { match &fill {
Fill::None => { Fill::None => {
let input_connector = InputConnector::node(fill_node_id, backup_color_index); let input_connector = InputConnector::node(fill_node_id, backup_color_index);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::OptionalColor(None), false), true); self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(Table::new()), false), true);
} }
Fill::Solid(color) => { Fill::Solid(color) => {
let input_connector = InputConnector::node(fill_node_id, backup_color_index); let input_connector = InputConnector::node(fill_node_id, backup_color_index);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::OptionalColor(Some(*color)), false), true); self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(Table::new_from_element(*color)), false), true);
} }
Fill::Gradient(gradient) => { Fill::Gradient(gradient) => {
let input_connector = InputConnector::node(fill_node_id, backup_gradient_index); let input_connector = InputConnector::node(fill_node_id, backup_gradient_index);
@ -372,8 +372,10 @@ impl<'a> ModifyInputsContext<'a> {
pub fn stroke_set(&mut self, stroke: Stroke) { pub fn stroke_set(&mut self, stroke: Stroke) {
let Some(stroke_node_id) = self.existing_node_id("Stroke", true) else { return }; let Some(stroke_node_id) = self.existing_node_id("Stroke", true) else { return };
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::ColorInput::<Option<graphene_std::Color>>::INDEX); let stroke_color = if let Some(color) = stroke.color { Table::new_from_element(color) } else { Table::new() };
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::OptionalColor(stroke.color), false), true);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::ColorInput::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(stroke_color), false), true);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::WeightInput::INDEX); 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); self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.weight), false), true);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::AlignInput::INDEX); let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::AlignInput::INDEX);

View file

@ -1,6 +1,7 @@
mod document_message; mod document_message;
mod document_message_handler; mod document_message_handler;
pub mod data_panel;
pub mod graph_operation; pub mod graph_operation;
pub mod navigation; pub mod navigation;
pub mod node_graph; pub mod node_graph;

View file

@ -139,7 +139,7 @@ impl MessageHandler<NavigationMessage, NavigationMessageContext<'_>> for Navigat
let transformed_delta = document_to_viewport.inverse().transform_vector2(delta); let transformed_delta = document_to_viewport.inverse().transform_vector2(delta);
ptz.pan += transformed_delta; ptz.pan += transformed_delta;
responses.add(BroadcastEvent::CanvasTransformed); responses.add(EventMessage::CanvasTransformed);
responses.add(DocumentMessage::PTZUpdate); responses.add(DocumentMessage::PTZUpdate);
} }
NavigationMessage::CanvasPanAbortPrepare { x_not_y_axis } => { NavigationMessage::CanvasPanAbortPrepare { x_not_y_axis } => {
@ -286,7 +286,7 @@ impl MessageHandler<NavigationMessage, NavigationMessageContext<'_>> for Navigat
ptz.flip = !ptz.flip; ptz.flip = !ptz.flip;
responses.add(DocumentMessage::PTZUpdate); responses.add(DocumentMessage::PTZUpdate);
responses.add(BroadcastEvent::CanvasTransformed); responses.add(EventMessage::CanvasTransformed);
responses.add(MenuBarMessage::SendLayout); responses.add(MenuBarMessage::SendLayout);
responses.add(PortfolioMessage::UpdateDocumentWidgets); responses.add(PortfolioMessage::UpdateDocumentWidgets);
} }
@ -325,7 +325,7 @@ impl MessageHandler<NavigationMessage, NavigationMessageContext<'_>> for Navigat
self.navigation_operation = NavigationOperation::None; self.navigation_operation = NavigationOperation::None;
// Send the final messages to close out the operation // Send the final messages to close out the operation
responses.add(BroadcastEvent::CanvasTransformed); responses.add(EventMessage::CanvasTransformed);
responses.add(ToolMessage::UpdateCursor); responses.add(ToolMessage::UpdateCursor);
responses.add(ToolMessage::UpdateHints); responses.add(ToolMessage::UpdateHints);
responses.add(NavigateToolMessage::End); responses.add(NavigateToolMessage::End);

View file

@ -399,7 +399,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
NodeInput::value(TaggedValue::Graphic(Default::default()), true), NodeInput::value(TaggedValue::Graphic(Default::default()), true),
NodeInput::value(TaggedValue::DVec2(DVec2::ZERO), false), NodeInput::value(TaggedValue::DVec2(DVec2::ZERO), false),
NodeInput::value(TaggedValue::DVec2(DVec2::new(1920., 1080.)), false), NodeInput::value(TaggedValue::DVec2(DVec2::new(1920., 1080.)), false),
NodeInput::value(TaggedValue::Color(Color::WHITE), false), NodeInput::value(TaggedValue::Color(Table::new_from_element(Color::WHITE)), false),
NodeInput::value(TaggedValue::Bool(false), false), NodeInput::value(TaggedValue::Bool(false), false),
], ],
..Default::default() ..Default::default()

View file

@ -42,6 +42,7 @@ pub struct NodeGraphMessageContext<'a> {
pub graph_fade_artwork_percentage: f64, pub graph_fade_artwork_percentage: f64,
pub navigation_handler: &'a NavigationMessageHandler, pub navigation_handler: &'a NavigationMessageHandler,
pub preferences: &'a PreferencesMessageHandler, pub preferences: &'a PreferencesMessageHandler,
pub layers_panel_open: bool,
} }
#[derive(Debug, Clone, ExtractField)] #[derive(Debug, Clone, ExtractField)]
@ -88,7 +89,7 @@ pub struct NodeGraphMessageHandler {
reordering_import: Option<usize>, 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>, reordering_export: Option<usize>,
/// The end index of the moved port /// The end index of the moved connector
end_index: Option<usize>, 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 /// 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>, frontend_nodes: Vec<NodeId>,
@ -111,6 +112,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
graph_fade_artwork_percentage, graph_fade_artwork_percentage,
navigation_handler, navigation_handler,
preferences, preferences,
layers_panel_open,
} = context; } = context;
match message { match message {
@ -127,7 +129,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
NodeGraphMessage::AddPathNode => { NodeGraphMessage::AddPathNode => {
if let Some(layer) = make_path_editable_is_allowed(network_interface, network_interface.document_metadata()) { if let Some(layer) = make_path_editable_is_allowed(network_interface, network_interface.document_metadata()) {
responses.add(NodeGraphMessage::CreateNodeInLayerWithTransaction { node_type: "Path".to_string(), layer }); responses.add(NodeGraphMessage::CreateNodeInLayerWithTransaction { node_type: "Path".to_string(), layer });
responses.add(BroadcastEvent::SelectionChanged); responses.add(EventMessage::SelectionChanged);
} }
} }
NodeGraphMessage::AddImport => { NodeGraphMessage::AddImport => {
@ -140,7 +142,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
} }
NodeGraphMessage::Init => { NodeGraphMessage::Init => {
responses.add(BroadcastMessage::SubscribeEvent { responses.add(BroadcastMessage::SubscribeEvent {
on: BroadcastEvent::SelectionChanged, on: EventMessage::SelectionChanged,
send: Box::new(NodeGraphMessage::SelectedNodesUpdated.into()), send: Box::new(NodeGraphMessage::SelectedNodesUpdated.into()),
}); });
network_interface.load_structure(); network_interface.load_structure();
@ -155,11 +157,13 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
} }
responses.add(MenuBarMessage::SendLayout); responses.add(MenuBarMessage::SendLayout);
responses.add(NodeGraphMessage::UpdateLayerPanel); responses.add(NodeGraphMessage::UpdateLayerPanel);
responses.add(PropertiesPanelMessage::Refresh);
responses.add(NodeGraphMessage::SendSelectedNodes); responses.add(NodeGraphMessage::SendSelectedNodes);
responses.add(ArtboardToolMessage::UpdateSelectedArtboard); responses.add(ArtboardToolMessage::UpdateSelectedArtboard);
responses.add(DocumentMessage::DocumentStructureChanged); responses.add(DocumentMessage::DocumentStructureChanged);
responses.add(OverlaysMessage::Draw); responses.add(OverlaysMessage::Draw);
responses.add(NodeGraphMessage::SendGraph); responses.add(NodeGraphMessage::SendGraph);
responses.add(PortfolioMessage::SubmitActiveGraphRender);
} }
NodeGraphMessage::CreateWire { output_connector, input_connector } => { NodeGraphMessage::CreateWire { output_connector, input_connector } => {
// TODO: Add support for flattening NodeInput::Network exports in flatten_with_fns https://github.com/GraphiteEditor/Graphite/issues/1762 // TODO: Add support for flattening NodeInput::Network exports in flatten_with_fns https://github.com/GraphiteEditor/Graphite/issues/1762
@ -1215,7 +1219,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
{ {
return None; return None;
} }
log::debug!("preferences.graph_wire_style: {:?}", preferences.graph_wire_style);
let (wire, is_stack) = network_interface.vector_wire_from_input(&input, preferences.graph_wire_style, selection_network_path)?; let (wire, is_stack) = network_interface.vector_wire_from_input(&input, preferences.graph_wire_style, selection_network_path)?;
let node_bbox = kurbo::Rect::new(node_bbox[0].x, node_bbox[0].y, node_bbox[1].x, node_bbox[1].y).to_path(DEFAULT_ACCURACY); let node_bbox = kurbo::Rect::new(node_bbox[0].x, node_bbox[0].y, node_bbox[1].x, node_bbox[1].y).to_path(DEFAULT_ACCURACY);
@ -1468,7 +1472,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
return; return;
}; };
selected_nodes.add_selected_nodes(nodes); selected_nodes.add_selected_nodes(nodes);
responses.add(BroadcastEvent::SelectionChanged); responses.add(EventMessage::SelectionChanged);
} }
NodeGraphMessage::SelectedNodesRemove { nodes } => { NodeGraphMessage::SelectedNodesRemove { nodes } => {
let Some(selected_nodes) = network_interface.selected_nodes_mut(selection_network_path) else { let Some(selected_nodes) = network_interface.selected_nodes_mut(selection_network_path) else {
@ -1476,7 +1480,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
return; return;
}; };
selected_nodes.retain_selected_nodes(|node| !nodes.contains(node)); selected_nodes.retain_selected_nodes(|node| !nodes.contains(node));
responses.add(BroadcastEvent::SelectionChanged); responses.add(EventMessage::SelectionChanged);
} }
NodeGraphMessage::SelectedNodesSet { nodes } => { NodeGraphMessage::SelectedNodesSet { nodes } => {
let Some(selected_nodes) = network_interface.selected_nodes_mut(selection_network_path) else { let Some(selected_nodes) = network_interface.selected_nodes_mut(selection_network_path) else {
@ -1484,8 +1488,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
return; return;
}; };
selected_nodes.set_selected_nodes(nodes); selected_nodes.set_selected_nodes(nodes);
responses.add(BroadcastEvent::SelectionChanged); responses.add(EventMessage::SelectionChanged);
responses.add(PropertiesPanelMessage::Refresh);
} }
NodeGraphMessage::SendClickTargets => responses.add(FrontendMessage::UpdateClickTargets { NodeGraphMessage::SendClickTargets => responses.add(FrontendMessage::UpdateClickTargets {
click_targets: Some(network_interface.collect_frontend_click_targets(breadcrumb_network_path)), click_targets: Some(network_interface.collect_frontend_click_targets(breadcrumb_network_path)),
@ -1677,6 +1680,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
responses.add(DocumentMessage::RenderRulers); responses.add(DocumentMessage::RenderRulers);
responses.add(DocumentMessage::RenderScrollbars); responses.add(DocumentMessage::RenderScrollbars);
responses.add(NodeGraphMessage::SendGraph); responses.add(NodeGraphMessage::SendGraph);
responses.add(OverlaysMessage::Draw); // Redraw overlays to update artboard names
} }
NodeGraphMessage::SetDisplayNameImpl { node_id, alias } => { NodeGraphMessage::SetDisplayNameImpl { node_id, alias } => {
network_interface.set_display_name(&node_id, alias, selection_network_path); network_interface.set_display_name(&node_id, alias, selection_network_path);
@ -1833,7 +1837,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
continue; continue;
}; };
let quad = Quad::from_box([box_selection_start, box_selection_end_graph]); let quad = Quad::from_box([box_selection_start, box_selection_end_graph]);
if click_targets.node_click_target.intersect_path(|| quad.bezier_lines(), DAffine2::IDENTITY) { if click_targets.node_click_target.intersect_path(|| quad.to_lines(), DAffine2::IDENTITY) {
nodes.insert(node_id); nodes.insert(node_id);
} }
} }
@ -1873,7 +1877,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
} }
NodeGraphMessage::UpdateLayerPanel => { NodeGraphMessage::UpdateLayerPanel => {
Self::update_layer_panel(network_interface, selection_network_path, collapsed, responses); Self::update_layer_panel(network_interface, selection_network_path, collapsed, layers_panel_open, responses);
} }
NodeGraphMessage::UpdateEdges => { NodeGraphMessage::UpdateEdges => {
// Update the import/export UI edges whenever the PTZ changes or the bounding box of all nodes changes // Update the import/export UI edges whenever the PTZ changes or the bounding box of all nodes changes
@ -1884,7 +1888,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
return; return;
}; };
selected_nodes.clear_selected_nodes(); selected_nodes.clear_selected_nodes();
responses.add(BroadcastEvent::SelectionChanged); responses.add(EventMessage::SelectionChanged);
responses.add(NodeGraphMessage::SendGraph); responses.add(NodeGraphMessage::SendGraph);
} }
@ -2329,9 +2333,9 @@ impl NodeGraphMessageHandler {
.icon(Some("Node".to_string())) .icon(Some("Node".to_string()))
.tooltip("Add an operation to the end of this layer's chain of nodes") .tooltip("Add an operation to the end of this layer's chain of nodes")
.popover_layout({ .popover_layout({
let layer_identifier = LayerNodeIdentifier::new(layer, &context.network_interface); let layer_identifier = LayerNodeIdentifier::new(layer, context.network_interface);
let compatible_type = { let compatible_type = {
let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer_identifier, &context.network_interface); let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer_identifier, context.network_interface);
let node_type = graph_layer.horizontal_layer_flow().nth(1); let node_type = graph_layer.horizontal_layer_flow().nth(1);
if let Some(node_id) = node_type { if let Some(node_id) = node_type {
let (output_type, _) = context.network_interface.output_type(&node_id, 0, &[]); let (output_type, _) = context.network_interface.output_type(&node_id, 0, &[]);
@ -2413,7 +2417,7 @@ impl NodeGraphMessageHandler {
} else { } else {
added_wires.push(WirePathUpdate { added_wires.push(WirePathUpdate {
id: NodeId(u64::MAX), id: NodeId(u64::MAX),
input_index: usize::MAX, input_index: u32::MAX as usize,
wire_path_update: None, wire_path_update: None,
}) })
} }
@ -2585,7 +2589,11 @@ impl NodeGraphMessageHandler {
Some(subgraph_names) Some(subgraph_names)
} }
fn update_layer_panel(network_interface: &NodeNetworkInterface, selection_network_path: &[NodeId], collapsed: &CollapsedLayers, responses: &mut VecDeque<Message>) { fn update_layer_panel(network_interface: &NodeNetworkInterface, selection_network_path: &[NodeId], collapsed: &CollapsedLayers, layers_panel_open: bool, responses: &mut VecDeque<Message>) {
if !layers_panel_open {
return;
}
let selected_layers = network_interface let selected_layers = network_interface
.selected_nodes() .selected_nodes()
.selected_layers(network_interface.document_metadata()) .selected_layers(network_interface.document_metadata())
@ -2668,7 +2676,7 @@ impl NodeGraphMessageHandler {
} }
} }
pub fn update_node_graph_hints(&self, responses: &mut VecDeque<Message>) { fn update_node_graph_hints(&self, responses: &mut VecDeque<Message>) {
// A wire is in progress and its start and end connectors are set // A wire is in progress and its start and end connectors are set
let wiring = self.wire_in_progress_from_connector.is_some(); let wiring = self.wire_in_progress_from_connector.is_some();

View file

@ -20,6 +20,7 @@ use graphene_std::raster::{
BlendMode, CellularDistanceFunction, CellularReturnType, Color, DomainWarpType, FractalType, LuminanceCalculation, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute, BlendMode, CellularDistanceFunction, CellularReturnType, Color, DomainWarpType, FractalType, LuminanceCalculation, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute,
SelectiveColorChoice, SelectiveColorChoice,
}; };
use graphene_std::table::{Table, TableRow};
use graphene_std::text::{Font, TextAlign}; use graphene_std::text::{Font, TextAlign};
use graphene_std::transform::{Footprint, ReferencePoint, Transform}; use graphene_std::transform::{Footprint, ReferencePoint, Transform};
use graphene_std::vector::misc::{ArcType, CentroidType, GridType, MergeByDistanceAlgorithm, PointSpacingType}; use graphene_std::vector::misc::{ArcType, CentroidType, GridType, MergeByDistanceAlgorithm, PointSpacingType};
@ -180,8 +181,8 @@ pub(crate) fn property_from_type(
// ============ // ============
// STRUCT TYPES // STRUCT TYPES
// ============ // ============
Some(x) if x == TypeId::of::<Color>() => color_widget(default_info, ColorInput::default().allow_none(false)), Some(x) if x == TypeId::of::<Table<Color>>() => color_widget(default_info, ColorInput::default().allow_none(true)),
Some(x) if x == TypeId::of::<Option<Color>>() => color_widget(default_info, ColorInput::default().allow_none(true)), Some(x) if x == TypeId::of::<Table<GradientStops>>() => color_widget(default_info, ColorInput::default().allow_none(false)),
Some(x) if x == TypeId::of::<GradientStops>() => color_widget(default_info, ColorInput::default().allow_none(false)), Some(x) if x == TypeId::of::<GradientStops>() => color_widget(default_info, ColorInput::default().allow_none(false)),
Some(x) if x == TypeId::of::<Font>() => font_widget(default_info), Some(x) if x == TypeId::of::<Font>() => font_widget(default_info),
Some(x) if x == TypeId::of::<Curve>() => curve_widget(default_info), Some(x) if x == TypeId::of::<Curve>() => curve_widget(default_info),
@ -908,28 +909,39 @@ pub fn color_widget(parameter_widgets_info: ParameterWidgetsInfo, color_button:
// Add the color input // Add the color input
match &**tagged_value { match &**tagged_value {
TaggedValue::Color(color) => widgets.push( TaggedValue::Color(color_table) => widgets.push(
color_button color_button
.value(FillChoice::Solid(*color)) .value(match color_table.iter().next() {
.on_update(update_value(|x: &ColorInput| TaggedValue::Color(x.value.as_solid().unwrap_or_default()), node_id, index)) Some(color) => FillChoice::Solid(*color.element),
.on_commit(commit_value)
.widget_holder(),
),
TaggedValue::OptionalColor(color) => widgets.push(
color_button
.value(match color {
Some(color) => FillChoice::Solid(*color),
None => FillChoice::None, None => FillChoice::None,
}) })
.on_update(update_value(|x: &ColorInput| TaggedValue::OptionalColor(x.value.as_solid()), node_id, index)) .on_update(update_value(
|input: &ColorInput| TaggedValue::Color(input.value.as_solid().iter().map(|&color| TableRow::new_from_element(color)).collect()),
node_id,
index,
))
.on_commit(commit_value) .on_commit(commit_value)
.widget_holder(), .widget_holder(),
), ),
TaggedValue::GradientStops(x) => widgets.push( TaggedValue::GradientTable(gradient_table) => widgets.push(
color_button color_button
.value(FillChoice::Gradient(x.clone())) .value(match gradient_table.iter().next() {
Some(row) => FillChoice::Gradient(row.element.clone()),
None => FillChoice::None,
})
.on_update(update_value( .on_update(update_value(
|x: &ColorInput| TaggedValue::GradientStops(x.value.as_gradient().cloned().unwrap_or_default()), |input: &ColorInput| TaggedValue::GradientTable(input.value.as_gradient().iter().map(|&gradient| TableRow::new_from_element(gradient.clone())).collect()),
node_id,
index,
))
.on_commit(commit_value)
.widget_holder(),
),
TaggedValue::GradientStops(gradient_stops) => widgets.push(
color_button
.value(FillChoice::Gradient(gradient_stops.clone()))
.on_update(update_value(
|input: &ColorInput| TaggedValue::GradientStops(input.value.as_gradient().cloned().unwrap_or_default()),
node_id, node_id,
index, index,
)) ))
@ -1559,7 +1571,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
} }
}; };
let (fill, backup_color, backup_gradient) = if let (Some(TaggedValue::Fill(fill)), &Some(&TaggedValue::OptionalColor(backup_color)), Some(TaggedValue::Gradient(backup_gradient))) = ( let (fill, backup_color, backup_gradient) = if let (Some(TaggedValue::Fill(fill)), Some(TaggedValue::Color(backup_color)), Some(TaggedValue::Gradient(backup_gradient))) = (
&document_node.inputs[FillInput::<Color>::INDEX].as_value(), &document_node.inputs[FillInput::<Color>::INDEX].as_value(),
&document_node.inputs[BackupColorInput::INDEX].as_value(), &document_node.inputs[BackupColorInput::INDEX].as_value(),
&document_node.inputs[BackupGradientInput::INDEX].as_value(), &document_node.inputs[BackupGradientInput::INDEX].as_value(),
@ -1569,7 +1581,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
return vec![LayoutGroup::Row { widgets: widgets_first_row }]; return vec![LayoutGroup::Row { widgets: widgets_first_row }];
}; };
let fill2 = fill.clone(); let fill2 = fill.clone();
let backup_color_fill: Fill = backup_color.into(); let backup_color_fill: Fill = backup_color.clone().into();
let backup_gradient_fill: Fill = backup_gradient.clone().into(); let backup_gradient_fill: Fill = backup_gradient.clone().into();
widgets_first_row.push(Separator::new(SeparatorType::Unrelated).widget_holder()); widgets_first_row.push(Separator::new(SeparatorType::Unrelated).widget_holder());
@ -1582,13 +1594,13 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
Fill::None => NodeGraphMessage::SetInputValue { Fill::None => NodeGraphMessage::SetInputValue {
node_id, node_id,
input_index: BackupColorInput::INDEX, input_index: BackupColorInput::INDEX,
value: TaggedValue::OptionalColor(None), value: TaggedValue::Color(Table::new()),
} }
.into(), .into(),
Fill::Solid(color) => NodeGraphMessage::SetInputValue { Fill::Solid(color) => NodeGraphMessage::SetInputValue {
node_id, node_id,
input_index: BackupColorInput::INDEX, input_index: BackupColorInput::INDEX,
value: TaggedValue::OptionalColor(Some(*color)), value: TaggedValue::Color(Table::new_from_element(*color)),
} }
.into(), .into(),
Fill::Gradient(gradient) => NodeGraphMessage::SetInputValue { Fill::Gradient(gradient) => NodeGraphMessage::SetInputValue {
@ -1750,7 +1762,7 @@ pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) -
let miter_limit_disabled = join_value != &StrokeJoin::Miter; let miter_limit_disabled = join_value != &StrokeJoin::Miter;
let color = color_widget( let color = color_widget(
ParameterWidgetsInfo::new(node_id, ColorInput::<Option<Color>>::INDEX, true, context), ParameterWidgetsInfo::new(node_id, ColorInput::INDEX, true, context),
crate::messages::layout::utility_types::widgets::button_widgets::ColorInput::default(), crate::messages::layout::utility_types::widgets::button_widgets::ColorInput::default(),
); );
let weight = number_widget(ParameterWidgetsInfo::new(node_id, WeightInput::INDEX, true, context), NumberInput::default().unit(" px").min(0.)); let weight = number_widget(ParameterWidgetsInfo::new(node_id, WeightInput::INDEX, true, context), NumberInput::default().unit(" px").min(0.));

View file

@ -8,18 +8,19 @@ use std::borrow::Cow;
pub enum FrontendGraphDataType { pub enum FrontendGraphDataType {
#[default] #[default]
General, General,
Number,
Artboard,
Graphic,
Raster, Raster,
Vector, Vector,
Number, Color,
Graphic, Gradient,
Artboard, Typography,
} }
impl FrontendGraphDataType { impl FrontendGraphDataType {
pub fn from_type(input: &Type) -> Self { pub fn from_type(input: &Type) -> Self {
match TaggedValue::from_type_or_none(input) { match TaggedValue::from_type_or_none(input) {
TaggedValue::Raster(_) => Self::Raster,
TaggedValue::Vector(_) => Self::Vector,
TaggedValue::U32(_) TaggedValue::U32(_)
| TaggedValue::U64(_) | TaggedValue::U64(_)
| TaggedValue::F64(_) | TaggedValue::F64(_)
@ -28,8 +29,13 @@ impl FrontendGraphDataType {
| TaggedValue::VecF64(_) | TaggedValue::VecF64(_)
| TaggedValue::VecDVec2(_) | TaggedValue::VecDVec2(_)
| TaggedValue::DAffine2(_) => Self::Number, | TaggedValue::DAffine2(_) => Self::Number,
TaggedValue::Graphic(_) => Self::Graphic,
TaggedValue::Artboard(_) => Self::Artboard, TaggedValue::Artboard(_) => Self::Artboard,
TaggedValue::Graphic(_) => Self::Graphic,
TaggedValue::Raster(_) => Self::Raster,
TaggedValue::Vector(_) => Self::Vector,
TaggedValue::Color(_) => Self::Color,
TaggedValue::Gradient(_) | TaggedValue::GradientStops(_) | TaggedValue::GradientTable(_) => Self::Gradient,
TaggedValue::String(_) => Self::Typography,
_ => Self::General, _ => Self::General,
} }
} }
@ -177,8 +183,8 @@ pub struct FrontendClickTargets {
pub node_click_targets: Vec<String>, pub node_click_targets: Vec<String>,
#[serde(rename = "layerClickTargets")] #[serde(rename = "layerClickTargets")]
pub layer_click_targets: Vec<String>, pub layer_click_targets: Vec<String>,
#[serde(rename = "portClickTargets")] #[serde(rename = "connectorClickTargets")]
pub port_click_targets: Vec<String>, pub connector_click_targets: Vec<String>,
#[serde(rename = "iconClickTargets")] #[serde(rename = "iconClickTargets")]
pub icon_click_targets: Vec<String>, pub icon_click_targets: Vec<String>,
#[serde(rename = "allNodesBoundingBox")] #[serde(rename = "allNodesBoundingBox")]

View file

@ -200,7 +200,7 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
move |input: &I| { move |input: &I| {
let mut grid = grid.clone(); let mut grid = grid.clone();
update(&mut grid, input); update(&mut grid, input);
DocumentMessage::GridOptions(grid).into() DocumentMessage::GridOptions { options: grid }.into()
} }
} }
let update_origin = |grid, update: fn(&mut GridSnapping) -> Option<&mut f64>| { let update_origin = |grid, update: fn(&mut GridSnapping) -> Option<&mut f64>| {

View file

@ -7,14 +7,14 @@ use crate::messages::prelude::*;
pub enum OverlaysMessage { pub enum OverlaysMessage {
Draw, Draw,
// Serde functionality isn't used but is required by the message system macros // Serde functionality isn't used but is required by the message system macros
AddProvider( AddProvider {
#[serde(skip, default = "empty_provider")] #[serde(skip, default = "empty_provider")]
#[derivative(Debug = "ignore", PartialEq = "ignore")] #[derivative(Debug = "ignore", PartialEq = "ignore")]
OverlayProvider, provider: OverlayProvider,
), },
RemoveProvider( RemoveProvider {
#[serde(skip, default = "empty_provider")] #[serde(skip, default = "empty_provider")]
#[derivative(Debug = "ignore", PartialEq = "ignore")] #[derivative(Debug = "ignore", PartialEq = "ignore")]
OverlayProvider, provider: OverlayProvider,
), },
} }

View file

@ -56,12 +56,14 @@ impl MessageHandler<OverlaysMessage, OverlaysMessageContext<'_>> for OverlaysMes
let _ = canvas_context.reset_transform(); let _ = canvas_context.reset_transform();
if visibility_settings.all() { if visibility_settings.all() {
responses.add(DocumentMessage::GridOverlays(OverlayContext { responses.add(DocumentMessage::GridOverlays {
render_context: canvas_context.clone(), context: OverlayContext {
size: size.as_dvec2(), render_context: canvas_context.clone(),
device_pixel_ratio, size: size.as_dvec2(),
visibility_settings: visibility_settings.clone(), device_pixel_ratio,
})); visibility_settings: visibility_settings.clone(),
},
});
for provider in &self.overlay_providers { for provider in &self.overlay_providers {
responses.add(provider(OverlayContext { responses.add(provider(OverlayContext {
render_context: canvas_context.clone(), render_context: canvas_context.clone(),
@ -81,22 +83,22 @@ impl MessageHandler<OverlaysMessage, OverlaysMessageContext<'_>> for OverlaysMes
let overlay_context = OverlayContext::new(size, device_pixel_ratio, visibility_settings); let overlay_context = OverlayContext::new(size, device_pixel_ratio, visibility_settings);
if visibility_settings.all() { if visibility_settings.all() {
responses.add(DocumentMessage::GridOverlays(overlay_context.clone())); responses.add(DocumentMessage::GridOverlays { context: overlay_context.clone() });
for provider in &self.overlay_providers { for provider in &self.overlay_providers {
responses.add(provider(overlay_context.clone())); responses.add(provider(overlay_context.clone()));
} }
} }
responses.add(FrontendMessage::RenderOverlays(overlay_context)); responses.add(FrontendMessage::RenderOverlays { context: overlay_context });
} }
#[cfg(all(not(target_family = "wasm"), test))] #[cfg(all(not(target_family = "wasm"), test))]
OverlaysMessage::Draw => { OverlaysMessage::Draw => {
let _ = (responses, visibility_settings, ipp, device_pixel_ratio); let _ = (responses, visibility_settings, ipp, device_pixel_ratio);
} }
OverlaysMessage::AddProvider(message) => { OverlaysMessage::AddProvider { provider: message } => {
self.overlay_providers.insert(message); self.overlay_providers.insert(message);
} }
OverlaysMessage::RemoveProvider(message) => { OverlaysMessage::RemoveProvider { provider: message } => {
self.overlay_providers.remove(&message); self.overlay_providers.remove(&message);
} }
} }

View file

@ -3,8 +3,8 @@ use crate::consts::HIDE_HANDLE_DISTANCE;
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface; use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
use crate::messages::tool::common_functionality::shape_editor::{SelectedLayerState, ShapeState}; use crate::messages::tool::common_functionality::shape_editor::{SelectedLayerState, ShapeState};
use crate::messages::tool::tool_messages::tool_prelude::{DocumentMessageHandler, PreferencesMessageHandler}; use crate::messages::tool::tool_messages::tool_prelude::{DocumentMessageHandler, PreferencesMessageHandler};
use bezier_rs::{Bezier, BezierHandles};
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use graphene_std::subpath::{Bezier, BezierHandles};
use graphene_std::vector::misc::ManipulatorPointId; use graphene_std::vector::misc::ManipulatorPointId;
use graphene_std::vector::{PointId, SegmentId}; use graphene_std::vector::{PointId, SegmentId};
use wasm_bindgen::JsCast; use wasm_bindgen::JsCast;
@ -125,7 +125,7 @@ pub fn path_overlays(document: &DocumentMessageHandler, draw_handles: DrawHandle
} }
// Get the selected segments and then add a bold line overlay on them // Get the selected segments and then add a bold line overlay on them
for (segment_id, bezier, _, _) in vector.segment_bezier_iter() { for (segment_id, bezier, _, _) in vector.segment_iter() {
let Some(selected_shape_state) = shape_editor.selected_shape_state.get_mut(&layer) else { let Some(selected_shape_state) = shape_editor.selected_shape_state.get_mut(&layer) else {
continue; continue;
}; };

View file

@ -5,14 +5,16 @@ use crate::consts::{
PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, SEGMENT_SELECTED_THICKNESS, PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, SEGMENT_SELECTED_THICKNESS,
}; };
use crate::messages::prelude::Message; use crate::messages::prelude::Message;
use bezier_rs::{Bezier, Subpath};
use core::borrow::Borrow; use core::borrow::Borrow;
use core::f64::consts::{FRAC_PI_2, PI, TAU}; use core::f64::consts::{FRAC_PI_2, PI, TAU};
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use graphene_std::Color; use graphene_std::Color;
use graphene_std::math::quad::Quad; use graphene_std::math::quad::Quad;
use graphene_std::subpath::Subpath;
use graphene_std::vector::click_target::ClickTargetType; use graphene_std::vector::click_target::ClickTargetType;
use graphene_std::vector::misc::{dvec2_to_point, point_to_dvec2};
use graphene_std::vector::{PointId, SegmentId, Vector}; use graphene_std::vector::{PointId, SegmentId, Vector};
use kurbo::{self, Affine, CubicBez, ParamCurve, PathSeg};
use std::collections::HashMap; use std::collections::HashMap;
use wasm_bindgen::{JsCast, JsValue}; use wasm_bindgen::{JsCast, JsValue};
use web_sys::{OffscreenCanvas, OffscreenCanvasRenderingContext2d}; use web_sys::{OffscreenCanvas, OffscreenCanvasRenderingContext2d};
@ -571,11 +573,7 @@ impl OverlayContext {
let handle_start = start + start_vec.perp() * radius * factor; let handle_start = start + start_vec.perp() * radius * factor;
let handle_end = end - end_vec.perp() * radius * factor; let handle_end = end - end_vec.perp() * radius * factor;
let bezier = Bezier { let bezier = PathSeg::Cubic(CubicBez::new(dvec2_to_point(start), dvec2_to_point(handle_start), dvec2_to_point(handle_end), dvec2_to_point(end)));
start,
end,
handles: bezier_rs::BezierHandles::Cubic { handle_start, handle_end },
};
self.bezier_command(bezier, DAffine2::IDENTITY, i == 0); self.bezier_command(bezier, DAffine2::IDENTITY, i == 0);
} }
@ -762,7 +760,7 @@ impl OverlayContext {
self.render_context.begin_path(); self.render_context.begin_path();
let mut last_point = None; let mut last_point = None;
for (_, bezier, start_id, end_id) in vector.segment_bezier_iter() { for (_, bezier, start_id, end_id) in vector.segment_iter() {
let move_to = last_point != Some(start_id); let move_to = last_point != Some(start_id);
last_point = Some(end_id); last_point = Some(end_id);
@ -776,7 +774,7 @@ impl OverlayContext {
} }
/// Used by the Pen tool in order to show how the bezier curve would look like. /// Used by the Pen tool in order to show how the bezier curve would look like.
pub fn outline_bezier(&mut self, bezier: Bezier, transform: DAffine2) { pub fn outline_bezier(&mut self, bezier: PathSeg, transform: DAffine2) {
self.start_dpi_aware_transform(); self.start_dpi_aware_transform();
self.render_context.begin_path(); self.render_context.begin_path();
@ -788,7 +786,7 @@ impl OverlayContext {
} }
/// Used by the path tool segment mode in order to show the selected segments. /// 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) { pub fn outline_select_bezier(&mut self, bezier: PathSeg, transform: DAffine2) {
self.start_dpi_aware_transform(); self.start_dpi_aware_transform();
self.render_context.begin_path(); self.render_context.begin_path();
@ -802,7 +800,7 @@ impl OverlayContext {
self.end_dpi_aware_transform(); self.end_dpi_aware_transform();
} }
pub fn outline_overlay_bezier(&mut self, bezier: Bezier, transform: DAffine2) { pub fn outline_overlay_bezier(&mut self, bezier: PathSeg, transform: DAffine2) {
self.start_dpi_aware_transform(); self.start_dpi_aware_transform();
self.render_context.begin_path(); self.render_context.begin_path();
@ -816,18 +814,18 @@ impl OverlayContext {
self.end_dpi_aware_transform(); self.end_dpi_aware_transform();
} }
fn bezier_command(&self, bezier: Bezier, transform: DAffine2, move_to: bool) { fn bezier_command(&self, bezier: PathSeg, transform: DAffine2, move_to: bool) {
self.start_dpi_aware_transform(); self.start_dpi_aware_transform();
let Bezier { start, end, handles } = bezier.apply_transformation(|point| transform.transform_point2(point)); let bezier = Affine::new(transform.to_cols_array()) * bezier;
if move_to { if move_to {
self.render_context.move_to(start.x, start.y); self.render_context.move_to(bezier.start().x, bezier.start().y);
} }
match bezier.as_path_el() {
match handles { kurbo::PathEl::LineTo(point) => self.render_context.line_to(point.x, point.y),
bezier_rs::BezierHandles::Linear => self.render_context.line_to(end.x, end.y), kurbo::PathEl::QuadTo(point, point1) => self.render_context.quadratic_curve_to(point.x, point.y, point1.x, point1.y),
bezier_rs::BezierHandles::Quadratic { handle } => self.render_context.quadratic_curve_to(handle.x, handle.y, end.x, end.y), kurbo::PathEl::CurveTo(point, point1, point2) => self.render_context.bezier_curve_to(point.x, point.y, point1.x, point1.y, point2.x, point2.y),
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => self.render_context.bezier_curve_to(handle_start.x, handle_start.y, handle_end.x, handle_end.y, end.x, end.y), _ => unreachable!(),
} }
self.end_dpi_aware_transform(); self.end_dpi_aware_transform();
@ -841,36 +839,35 @@ impl OverlayContext {
let subpath = subpath.borrow(); let subpath = subpath.borrow();
let mut curves = subpath.iter().peekable(); let mut curves = subpath.iter().peekable();
let Some(first) = curves.peek() else { let Some(&first) = curves.peek() else {
continue; continue;
}; };
self.render_context.move_to(transform.transform_point2(first.start()).x, transform.transform_point2(first.start()).y); let start_point = transform.transform_point2(point_to_dvec2(first.start()));
for curve in curves { self.render_context.move_to(start_point.x, start_point.y);
match curve.handles {
bezier_rs::BezierHandles::Linear => {
let a = transform.transform_point2(curve.end());
let a = a.round() - DVec2::splat(0.5);
self.render_context.line_to(a.x, a.y) for curve in curves {
match curve {
PathSeg::Line(line) => {
let a = transform.transform_point2(point_to_dvec2(line.p1));
let a = a.round() - DVec2::splat(0.5);
self.render_context.line_to(a.x, a.y);
} }
bezier_rs::BezierHandles::Quadratic { handle } => { PathSeg::Quad(quad_bez) => {
let a = transform.transform_point2(handle); let a = transform.transform_point2(point_to_dvec2(quad_bez.p1));
let b = transform.transform_point2(curve.end()); let b = transform.transform_point2(point_to_dvec2(quad_bez.p2));
let a = a.round() - DVec2::splat(0.5); let a = a.round() - DVec2::splat(0.5);
let b = b.round() - DVec2::splat(0.5); let b = b.round() - DVec2::splat(0.5);
self.render_context.quadratic_curve_to(a.x, a.y, b.x, b.y);
self.render_context.quadratic_curve_to(a.x, a.y, b.x, b.y)
} }
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => { PathSeg::Cubic(cubic_bez) => {
let a = transform.transform_point2(handle_start); let a = transform.transform_point2(point_to_dvec2(cubic_bez.p1));
let b = transform.transform_point2(handle_end); let b = transform.transform_point2(point_to_dvec2(cubic_bez.p2));
let c = transform.transform_point2(curve.end()); let c = transform.transform_point2(point_to_dvec2(cubic_bez.p3));
let a = a.round() - DVec2::splat(0.5); let a = a.round() - DVec2::splat(0.5);
let b = b.round() - DVec2::splat(0.5); let b = b.round() - DVec2::splat(0.5);
let c = c.round() - DVec2::splat(0.5); let c = c.round() - DVec2::splat(0.5);
self.render_context.bezier_curve_to(a.x, a.y, b.x, b.y, c.x, c.y);
self.render_context.bezier_curve_to(a.x, a.y, b.x, b.y, c.x, c.y)
} }
} }
} }
@ -885,7 +882,7 @@ impl OverlayContext {
/// Used by the Select tool to outline a path or a free point when selected or hovered. /// 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>) { 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 mut subpaths: Vec<Subpath<PointId>> = vec![];
target_types.for_each(|target_type| match target_type.borrow() { target_types.for_each(|target_type| match target_type.borrow() {
ClickTargetType::FreePoint(point) => { ClickTargetType::FreePoint(point) => {

View file

@ -4,20 +4,22 @@ use crate::consts::{
PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER,
}; };
use crate::messages::prelude::Message; use crate::messages::prelude::Message;
use bezier_rs::{Bezier, Subpath};
use core::borrow::Borrow; use core::borrow::Borrow;
use core::f64::consts::{FRAC_PI_2, PI, TAU}; use core::f64::consts::{FRAC_PI_2, PI, TAU};
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use graphene_std::Color; use graphene_std::Color;
use graphene_std::math::quad::Quad; use graphene_std::math::quad::Quad;
use graphene_std::subpath::{self, Subpath};
use graphene_std::table::Table; use graphene_std::table::Table;
use graphene_std::text::{TextAlign, TypesettingConfig, load_font, to_path}; use graphene_std::text::{TextAlign, TypesettingConfig, load_font, to_path};
use graphene_std::vector::click_target::ClickTargetType; use graphene_std::vector::click_target::ClickTargetType;
use graphene_std::vector::misc::point_to_dvec2;
use graphene_std::vector::{PointId, SegmentId, Vector}; use graphene_std::vector::{PointId, SegmentId, Vector};
use kurbo::{self, BezPath, ParamCurve};
use kurbo::{Affine, PathSeg};
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::{Arc, Mutex, MutexGuard}; use std::sync::{Arc, Mutex, MutexGuard};
use vello::Scene; use vello::Scene;
use vello::kurbo::{self, BezPath};
use vello::peniko; use vello::peniko;
pub type OverlayProvider = fn(OverlayContext) -> Message; pub type OverlayProvider = fn(OverlayContext) -> Message;
@ -200,6 +202,7 @@ impl core::hash::Hash for OverlayContext {
} }
impl OverlayContext { impl OverlayContext {
#[allow(dead_code)]
pub(super) fn new(size: DVec2, device_pixel_ratio: f64, visibility_settings: OverlaysVisibilitySettings) -> Self { pub(super) fn new(size: DVec2, device_pixel_ratio: f64, visibility_settings: OverlaysVisibilitySettings) -> Self {
Self { Self {
internal: Arc::new(Mutex::new(OverlayContextInternal::new(size, device_pixel_ratio, visibility_settings))), internal: Arc::new(Mutex::new(OverlayContextInternal::new(size, device_pixel_ratio, visibility_settings))),
@ -345,16 +348,16 @@ impl OverlayContext {
} }
/// Used by the Pen tool in order to show how the bezier curve would look like. /// Used by the Pen tool in order to show how the bezier curve would look like.
pub fn outline_bezier(&mut self, bezier: Bezier, transform: DAffine2) { pub fn outline_bezier(&mut self, bezier: PathSeg, transform: DAffine2) {
self.internal().outline_bezier(bezier, transform); self.internal().outline_bezier(bezier, transform);
} }
/// Used by the path tool segment mode in order to show the selected segments. /// 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) { pub fn outline_select_bezier(&mut self, bezier: PathSeg, transform: DAffine2) {
self.internal().outline_select_bezier(bezier, transform); self.internal().outline_select_bezier(bezier, transform);
} }
pub fn outline_overlay_bezier(&mut self, bezier: Bezier, transform: DAffine2) { pub fn outline_overlay_bezier(&mut self, bezier: PathSeg, transform: DAffine2) {
self.internal().outline_overlay_bezier(bezier, transform); self.internal().outline_overlay_bezier(bezier, transform);
} }
@ -842,7 +845,7 @@ impl OverlayContextInternal {
let mut path = BezPath::new(); let mut path = BezPath::new();
let mut last_point = None; let mut last_point = None;
for (_, bezier, start_id, end_id) in vector.segment_bezier_iter() { for (_, bezier, start_id, end_id) in vector.segment_iter() {
let move_to = last_point != Some(start_id); let move_to = last_point != Some(start_id);
last_point = Some(end_id); last_point = Some(end_id);
@ -853,7 +856,7 @@ impl OverlayContextInternal {
} }
/// Used by the Pen tool in order to show how the bezier curve would look like. /// Used by the Pen tool in order to show how the bezier curve would look like.
fn outline_bezier(&mut self, bezier: Bezier, transform: DAffine2) { fn outline_bezier(&mut self, bezier: PathSeg, transform: DAffine2) {
let vello_transform = self.get_transform(); let vello_transform = self.get_transform();
let mut path = BezPath::new(); let mut path = BezPath::new();
self.bezier_to_path(bezier, transform, true, &mut path); self.bezier_to_path(bezier, transform, true, &mut path);
@ -862,7 +865,7 @@ impl OverlayContextInternal {
} }
/// Used by the path tool segment mode in order to show the selected segments. /// Used by the path tool segment mode in order to show the selected segments.
fn outline_select_bezier(&mut self, bezier: Bezier, transform: DAffine2) { fn outline_select_bezier(&mut self, bezier: PathSeg, transform: DAffine2) {
let vello_transform = self.get_transform(); let vello_transform = self.get_transform();
let mut path = BezPath::new(); let mut path = BezPath::new();
self.bezier_to_path(bezier, transform, true, &mut path); self.bezier_to_path(bezier, transform, true, &mut path);
@ -870,7 +873,7 @@ impl OverlayContextInternal {
self.scene.stroke(&kurbo::Stroke::new(4.0), vello_transform, Self::parse_color(COLOR_OVERLAY_BLUE), None, &path); self.scene.stroke(&kurbo::Stroke::new(4.0), vello_transform, Self::parse_color(COLOR_OVERLAY_BLUE), None, &path);
} }
fn outline_overlay_bezier(&mut self, bezier: Bezier, transform: DAffine2) { fn outline_overlay_bezier(&mut self, bezier: PathSeg, transform: DAffine2) {
let vello_transform = self.get_transform(); let vello_transform = self.get_transform();
let mut path = BezPath::new(); let mut path = BezPath::new();
self.bezier_to_path(bezier, transform, true, &mut path); self.bezier_to_path(bezier, transform, true, &mut path);
@ -878,21 +881,12 @@ impl OverlayContextInternal {
self.scene.stroke(&kurbo::Stroke::new(4.0), vello_transform, Self::parse_color(COLOR_OVERLAY_BLUE_50), None, &path); self.scene.stroke(&kurbo::Stroke::new(4.0), vello_transform, Self::parse_color(COLOR_OVERLAY_BLUE_50), None, &path);
} }
fn bezier_to_path(&self, bezier: Bezier, transform: DAffine2, move_to: bool, path: &mut BezPath) { fn bezier_to_path(&self, bezier: PathSeg, transform: DAffine2, move_to: bool, path: &mut BezPath) {
let Bezier { start, end, handles } = bezier.apply_transformation(|point| transform.transform_point2(point)); let bezier = Affine::new(transform.to_cols_array()) * bezier;
if move_to { if move_to {
path.move_to(kurbo::Point::new(start.x, start.y)); path.move_to(bezier.start());
}
match handles {
bezier_rs::BezierHandles::Linear => path.line_to(kurbo::Point::new(end.x, end.y)),
bezier_rs::BezierHandles::Quadratic { handle } => path.quad_to(kurbo::Point::new(handle.x, handle.y), kurbo::Point::new(end.x, end.y)),
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => path.curve_to(
kurbo::Point::new(handle_start.x, handle_start.y),
kurbo::Point::new(handle_end.x, handle_end.y),
kurbo::Point::new(end.x, end.y),
),
} }
path.push(bezier.as_path_el());
} }
fn push_path(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2) -> BezPath { fn push_path(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2) -> BezPath {
@ -906,27 +900,27 @@ impl OverlayContextInternal {
continue; continue;
}; };
let start_point = transform.transform_point2(first.start()); let start_point = transform.transform_point2(point_to_dvec2(first.start()));
path.move_to(kurbo::Point::new(start_point.x, start_point.y)); path.move_to(kurbo::Point::new(start_point.x, start_point.y));
for curve in curves { for curve in curves {
match curve.handles { match curve {
bezier_rs::BezierHandles::Linear => { PathSeg::Line(line) => {
let a = transform.transform_point2(curve.end()); let a = transform.transform_point2(point_to_dvec2(line.p1));
let a = a.round() - DVec2::splat(0.5); let a = a.round() - DVec2::splat(0.5);
path.line_to(kurbo::Point::new(a.x, a.y)); path.line_to(kurbo::Point::new(a.x, a.y));
} }
bezier_rs::BezierHandles::Quadratic { handle } => { PathSeg::Quad(quad_bez) => {
let a = transform.transform_point2(handle); let a = transform.transform_point2(point_to_dvec2(quad_bez.p1));
let b = transform.transform_point2(curve.end()); let b = transform.transform_point2(point_to_dvec2(quad_bez.p2));
let a = a.round() - DVec2::splat(0.5); let a = a.round() - DVec2::splat(0.5);
let b = b.round() - DVec2::splat(0.5); let b = b.round() - DVec2::splat(0.5);
path.quad_to(kurbo::Point::new(a.x, a.y), kurbo::Point::new(b.x, b.y)); path.quad_to(kurbo::Point::new(a.x, a.y), kurbo::Point::new(b.x, b.y));
} }
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => { PathSeg::Cubic(cubic_bez) => {
let a = transform.transform_point2(handle_start); let a = transform.transform_point2(point_to_dvec2(cubic_bez.p1));
let b = transform.transform_point2(handle_end); let b = transform.transform_point2(point_to_dvec2(cubic_bez.p2));
let c = transform.transform_point2(curve.end()); let c = transform.transform_point2(point_to_dvec2(cubic_bez.p3));
let a = a.round() - DVec2::splat(0.5); let a = a.round() - DVec2::splat(0.5);
let b = b.round() - DVec2::splat(0.5); let b = b.round() - DVec2::splat(0.5);
let c = c.round() - DVec2::splat(0.5); let c = c.round() - DVec2::splat(0.5);
@ -945,7 +939,7 @@ impl OverlayContextInternal {
/// Used by the Select tool to outline a path or a free point when selected or hovered. /// Used by the Select tool to outline a path or a free point when selected or hovered.
fn outline(&mut self, target_types: impl Iterator<Item = impl Borrow<ClickTargetType>>, transform: DAffine2, color: Option<&str>) { 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 mut subpaths: Vec<subpath::Subpath<PointId>> = vec![];
for target_type in target_types { for target_type in target_types {
match target_type.borrow() { match target_type.borrow() {
@ -1118,13 +1112,13 @@ impl OverlayContextInternal {
// Add handle points if they exist // Add handle points if they exist
match transformed_bezier.handles { match transformed_bezier.handles {
bezier_rs::BezierHandles::Quadratic { handle } => { subpath::BezierHandles::Quadratic { handle } => {
min_x = min_x.min(handle.x); min_x = min_x.min(handle.x);
min_y = min_y.min(handle.y); min_y = min_y.min(handle.y);
max_x = max_x.max(handle.x); max_x = max_x.max(handle.x);
max_y = max_y.max(handle.y); max_y = max_y.max(handle.y);
} }
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => { subpath::BezierHandles::Cubic { handle_start, handle_end } => {
for handle in [handle_start, handle_end] { for handle in [handle_start, handle_end] {
min_x = min_x.min(handle.x); min_x = min_x.min(handle.x);
min_y = min_y.min(handle.y); min_y = min_y.min(handle.y);
@ -1154,7 +1148,7 @@ impl OverlayContextInternal {
let mut path = BezPath::new(); let mut path = BezPath::new();
let mut last_point = None; let mut last_point = None;
for (_, bezier, start_id, end_id) in row.element.segment_bezier_iter() { for (_, bezier, start_id, end_id) in row.element.segment_iter() {
let move_to = last_point != Some(start_id); let move_to = last_point != Some(start_id);
last_point = Some(end_id); last_point = Some(end_id);

View file

@ -14,6 +14,7 @@ pub struct PropertiesPanelMessageContext<'a> {
pub document_name: &'a str, pub document_name: &'a str,
pub executor: &'a mut NodeGraphExecutor, pub executor: &'a mut NodeGraphExecutor,
pub persistent_data: &'a PersistentData, pub persistent_data: &'a PersistentData,
pub properties_panel_open: bool,
} }
#[derive(Debug, Clone, Default, ExtractField)] #[derive(Debug, Clone, Default, ExtractField)]
@ -28,16 +29,22 @@ impl MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageContext<'_>> f
document_name, document_name,
executor, executor,
persistent_data, persistent_data,
properties_panel_open,
} = context; } = context;
match message { match message {
PropertiesPanelMessage::Clear => { PropertiesPanelMessage::Clear => {
responses.add(LayoutMessage::SendLayout { responses.add(LayoutMessage::SendLayout {
layout: Layout::WidgetLayout(WidgetLayout::new(vec![])), layout: Layout::WidgetLayout(WidgetLayout::new(vec![])),
layout_target: LayoutTarget::PropertiesSections, layout_target: LayoutTarget::PropertiesPanel,
}); });
} }
PropertiesPanelMessage::Refresh => { PropertiesPanelMessage::Refresh => {
if !properties_panel_open {
responses.add(PropertiesPanelMessage::Clear);
return;
}
let mut node_properties_context = NodePropertiesContext { let mut node_properties_context = NodePropertiesContext {
persistent_data, persistent_data,
responses, responses,
@ -50,7 +57,7 @@ impl MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageContext<'_>> f
node_properties_context.responses.add(LayoutMessage::SendLayout { node_properties_context.responses.add(LayoutMessage::SendLayout {
layout: Layout::WidgetLayout(WidgetLayout::new(properties_sections)), layout: Layout::WidgetLayout(WidgetLayout::new(properties_sections)),
layout_target: LayoutTarget::PropertiesSections, layout_target: LayoutTarget::PropertiesPanel,
}); });
} }
} }

View file

@ -6,6 +6,7 @@ use crate::messages::tool::common_functionality::graph_modification_utils;
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use graph_craft::document::NodeId; use graph_craft::document::NodeId;
use graphene_std::math::quad::Quad; use graphene_std::math::quad::Quad;
use graphene_std::subpath;
use graphene_std::transform::Footprint; use graphene_std::transform::Footprint;
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType}; use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
use graphene_std::vector::{PointId, Vector}; use graphene_std::vector::{PointId, Vector};
@ -196,7 +197,7 @@ impl DocumentMetadata {
self.all_layers().filter_map(|layer| self.bounding_box_viewport(layer)).reduce(Quad::combine_bounds) self.all_layers().filter_map(|layer| self.bounding_box_viewport(layer)).reduce(Quad::combine_bounds)
} }
pub fn layer_outline(&self, layer: LayerNodeIdentifier) -> impl Iterator<Item = &bezier_rs::Subpath<PointId>> { pub fn layer_outline(&self, layer: LayerNodeIdentifier) -> impl Iterator<Item = &subpath::Subpath<PointId>> {
static EMPTY: Vec<ClickTarget> = Vec::new(); static EMPTY: Vec<ClickTarget> = Vec::new();
let click_targets = self.click_targets.get(&layer).unwrap_or(&EMPTY); let click_targets = self.click_targets.get(&layer).unwrap_or(&EMPTY);
click_targets.iter().filter_map(|target| match target.target_type() { click_targets.iter().filter_map(|target| match target.target_type() {

View file

@ -8,13 +8,13 @@ use crate::messages::portfolio::document::node_graph::utility_types::{Direction,
use crate::messages::portfolio::document::utility_types::wires::{GraphWireStyle, WirePath, WirePathUpdate, build_vector_wire}; use crate::messages::portfolio::document::utility_types::wires::{GraphWireStyle, WirePath, WirePathUpdate, build_vector_wire};
use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::tool_messages::tool_prelude::NumberInputMode; use crate::messages::tool::tool_messages::tool_prelude::NumberInputMode;
use bezier_rs::Subpath;
use glam::{DAffine2, DVec2, IVec2}; use glam::{DAffine2, DVec2, IVec2};
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork, OldDocumentNodeImplementation, OldNodeNetwork}; use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork, OldDocumentNodeImplementation, OldNodeNetwork};
use graph_craft::{Type, concrete}; use graph_craft::{Type, concrete};
use graphene_std::Artboard; use graphene_std::Artboard;
use graphene_std::math::quad::Quad; use graphene_std::math::quad::Quad;
use graphene_std::subpath::Subpath;
use graphene_std::table::Table; use graphene_std::table::Table;
use graphene_std::transform::Footprint; use graphene_std::transform::Footprint;
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType}; use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
@ -1287,7 +1287,7 @@ impl NodeNetworkInterface {
let artboard = self.document_node(&artboard_node_identifier.to_node(), &[]); let artboard = self.document_node(&artboard_node_identifier.to_node(), &[]);
let clip_input = artboard.unwrap().inputs.get(5).unwrap(); let clip_input = artboard.unwrap().inputs.get(5).unwrap();
if let NodeInput::Value { tagged_value, .. } = clip_input { if let NodeInput::Value { tagged_value, .. } = clip_input {
if tagged_value.to_primitive_string() == "true" { if tagged_value.clone().into_inner() == TaggedValue::Bool(true) {
return Some(Quad::clip( return Some(Quad::clip(
self.document_metadata.bounding_box_document(layer).unwrap_or_default(), self.document_metadata.bounding_box_document(layer).unwrap_or_default(),
self.document_metadata.bounding_box_document(artboard_node_identifier).unwrap_or_default(), self.document_metadata.bounding_box_document(artboard_node_identifier).unwrap_or_default(),
@ -2635,7 +2635,7 @@ impl NodeNetworkInterface {
InputConnector::Node { node_id, input_index } => (*node_id, *input_index), InputConnector::Node { node_id, input_index } => (*node_id, *input_index),
InputConnector::Export(export_index) => (NodeId(u64::MAX), *export_index), InputConnector::Export(export_index) => (NodeId(u64::MAX), *export_index),
}) })
.chain(std::iter::once((NodeId(u64::MAX), usize::MAX))) .chain(std::iter::once((NodeId(u64::MAX), u32::MAX as usize)))
.collect() .collect()
} }
@ -2725,7 +2725,7 @@ impl NodeNetworkInterface {
Some(WirePathUpdate { Some(WirePathUpdate {
id: NodeId(u64::MAX), id: NodeId(u64::MAX),
input_index: usize::MAX, input_index: u32::MAX as usize,
wire_path_update, wire_path_update,
}) })
} }
@ -2832,7 +2832,7 @@ impl NodeNetworkInterface {
let node_click_target_bottom_right = node_click_target_top_left + DVec2::new(width as f64, height as f64); let node_click_target_bottom_right = node_click_target_top_left + DVec2::new(width as f64, height as f64);
let radius = 3.; let radius = 3.;
let subpath = bezier_rs::Subpath::new_rounded_rect(node_click_target_top_left, node_click_target_bottom_right, [radius; 4]); let subpath = Subpath::new_rounded_rect(node_click_target_top_left, node_click_target_bottom_right, [radius; 4]);
let node_click_target = ClickTarget::new_with_subpath(subpath, 0.); let node_click_target = ClickTarget::new_with_subpath(subpath, 0.);
DocumentNodeClickTargets { DocumentNodeClickTargets {
@ -2871,7 +2871,7 @@ impl NodeNetworkInterface {
let node_bottom_right = node_top_left + DVec2::new(width as f64, height as f64); let node_bottom_right = node_top_left + DVec2::new(width as f64, height as f64);
let chain_top_left = node_top_left - DVec2::new((chain_width_grid_spaces * crate::consts::GRID_SIZE) as f64, 0.); let chain_top_left = node_top_left - DVec2::new((chain_width_grid_spaces * crate::consts::GRID_SIZE) as f64, 0.);
let radius = 10.; let radius = 10.;
let subpath = bezier_rs::Subpath::new_rounded_rect(chain_top_left, node_bottom_right, [radius; 4]); let subpath = Subpath::new_rounded_rect(chain_top_left, node_bottom_right, [radius; 4]);
let node_click_target = ClickTarget::new_with_subpath(subpath, 0.); let node_click_target = ClickTarget::new_with_subpath(subpath, 0.);
DocumentNodeClickTargets { DocumentNodeClickTargets {
@ -3048,7 +3048,7 @@ impl NodeNetworkInterface {
pub fn collect_frontend_click_targets(&mut self, network_path: &[NodeId]) -> FrontendClickTargets { pub fn collect_frontend_click_targets(&mut self, network_path: &[NodeId]) -> FrontendClickTargets {
let mut all_node_click_targets = Vec::new(); let mut all_node_click_targets = Vec::new();
let mut port_click_targets = Vec::new(); let mut connector_click_targets = Vec::new();
let mut icon_click_targets = Vec::new(); let mut icon_click_targets = Vec::new();
let Some(network_metadata) = self.network_metadata(network_path) else { let Some(network_metadata) = self.network_metadata(network_path) else {
log::error!("Could not get nested network_metadata in collect_frontend_click_targets"); log::error!("Could not get nested network_metadata in collect_frontend_click_targets");
@ -3059,27 +3059,21 @@ impl NodeNetworkInterface {
let mut node_path = String::new(); let mut node_path = String::new();
if let ClickTargetType::Subpath(subpath) = node_click_targets.node_click_target.target_type() { if let ClickTargetType::Subpath(subpath) = node_click_targets.node_click_target.target_type() {
let _ = subpath.subpath_to_svg(&mut node_path, DAffine2::IDENTITY); node_path.push_str(subpath.to_bezpath().to_svg().as_str())
} }
all_node_click_targets.push((node_id, node_path)); all_node_click_targets.push((node_id, node_path));
for port in node_click_targets.port_click_targets.click_targets().chain(import_export_click_targets.click_targets()) { for port in node_click_targets.port_click_targets.click_targets().chain(import_export_click_targets.click_targets()) {
if let ClickTargetType::Subpath(subpath) = port.target_type() { if let ClickTargetType::Subpath(subpath) = port.target_type() {
let mut port_path = String::new(); connector_click_targets.push(subpath.to_bezpath().to_svg());
let _ = subpath.subpath_to_svg(&mut port_path, DAffine2::IDENTITY);
port_click_targets.push(port_path);
} }
} }
if let NodeTypeClickTargets::Layer(layer_metadata) = &node_click_targets.node_type_metadata { if let NodeTypeClickTargets::Layer(layer_metadata) = &node_click_targets.node_type_metadata {
if let ClickTargetType::Subpath(subpath) = layer_metadata.visibility_click_target.target_type() { if let ClickTargetType::Subpath(subpath) = layer_metadata.visibility_click_target.target_type() {
let mut port_path = String::new(); icon_click_targets.push(subpath.to_bezpath().to_svg());
let _ = subpath.subpath_to_svg(&mut port_path, DAffine2::IDENTITY);
icon_click_targets.push(port_path);
} }
if let ClickTargetType::Subpath(subpath) = layer_metadata.grip_click_target.target_type() { if let ClickTargetType::Subpath(subpath) = layer_metadata.grip_click_target.target_type() {
let mut port_path = String::new(); icon_click_targets.push(subpath.to_bezpath().to_svg());
let _ = subpath.subpath_to_svg(&mut port_path, DAffine2::IDENTITY);
icon_click_targets.push(port_path);
} }
} }
} }
@ -3095,9 +3089,8 @@ impl NodeNetworkInterface {
}); });
let bounds = self.all_nodes_bounding_box(network_path).cloned().unwrap_or([DVec2::ZERO, DVec2::ZERO]); let bounds = self.all_nodes_bounding_box(network_path).cloned().unwrap_or([DVec2::ZERO, DVec2::ZERO]);
let rect = bezier_rs::Subpath::<PointId>::new_rect(bounds[0], bounds[1]); let rect = Subpath::<PointId>::new_rect(bounds[0], bounds[1]);
let mut all_nodes_bounding_box = String::new(); let all_nodes_bounding_box = rect.to_bezpath().to_svg();
let _ = rect.subpath_to_svg(&mut all_nodes_bounding_box, DAffine2::IDENTITY);
let Some(rounded_network_edge_distance) = self.rounded_network_edge_distance(network_path).cloned() else { let Some(rounded_network_edge_distance) = self.rounded_network_edge_distance(network_path).cloned() else {
log::error!("Could not get rounded_network_edge_distance in collect_frontend_click_targets"); log::error!("Could not get rounded_network_edge_distance in collect_frontend_click_targets");
@ -3123,9 +3116,8 @@ impl NodeNetworkInterface {
.inverse() .inverse()
.transform_point2(import_exports_viewport_bottom_right); .transform_point2(import_exports_viewport_bottom_right);
let import_exports_target = bezier_rs::Subpath::<PointId>::new_rect(node_graph_top_left, node_graph_bottom_right); let import_exports_target = Subpath::<PointId>::new_rect(node_graph_top_left, node_graph_bottom_right);
let mut import_exports_bounding_box = String::new(); let import_exports_bounding_box = import_exports_target.to_bezpath().to_svg();
let _ = import_exports_target.subpath_to_svg(&mut import_exports_bounding_box, DAffine2::IDENTITY);
let mut modify_import_export = Vec::new(); let mut modify_import_export = Vec::new();
if let Some(modify_import_export_click_targets) = self.modify_import_export(network_path) { if let Some(modify_import_export_click_targets) = self.modify_import_export(network_path) {
@ -3136,16 +3128,14 @@ impl NodeNetworkInterface {
.chain(modify_import_export_click_targets.reorder_imports_exports.click_targets()) .chain(modify_import_export_click_targets.reorder_imports_exports.click_targets())
{ {
if let ClickTargetType::Subpath(subpath) = click_target.target_type() { if let ClickTargetType::Subpath(subpath) = click_target.target_type() {
let mut remove_string = String::new(); modify_import_export.push(subpath.to_bezpath().to_svg());
let _ = subpath.subpath_to_svg(&mut remove_string, DAffine2::IDENTITY);
modify_import_export.push(remove_string);
} }
} }
} }
FrontendClickTargets { FrontendClickTargets {
node_click_targets, node_click_targets,
layer_click_targets, layer_click_targets,
port_click_targets, connector_click_targets,
icon_click_targets, icon_click_targets,
all_nodes_bounding_box, all_nodes_bounding_box,
import_exports_bounding_box, import_exports_bounding_box,
@ -3407,7 +3397,7 @@ impl NodeNetworkInterface {
return None; return None;
}; };
let bounding_box_subpath = bezier_rs::Subpath::<PointId>::new_rect(bounds[0], bounds[1]); let bounding_box_subpath = Subpath::<PointId>::new_rect(bounds[0], bounds[1]);
bounding_box_subpath.bounding_box_with_transform(network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport) bounding_box_subpath.bounding_box_with_transform(network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport)
} }
@ -6047,7 +6037,7 @@ impl Iterator for FlowIter<'_> {
} else { } else {
0 0
}; };
let take = if self.flow_type == FlowType::UpstreamFlow { usize::MAX } else { 1 }; let take = if self.flow_type == FlowType::UpstreamFlow { u32::MAX as usize } else { 1 };
let inputs = document_node.inputs.iter().skip(skip).take(take); let inputs = document_node.inputs.iter().skip(skip).take(take);
let node_ids = inputs.filter_map(|input| match input { let node_ids = inputs.filter_map(|input| match input {

View file

@ -5,11 +5,11 @@ use crate::messages::portfolio::document::node_graph::document_node_definitions:
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate, OutputConnector}; use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate, OutputConnector};
use crate::messages::prelude::DocumentMessageHandler; use crate::messages::prelude::DocumentMessageHandler;
use bezier_rs::Subpath;
use glam::IVec2; use glam::IVec2;
use graph_craft::document::DocumentNode; use graph_craft::document::DocumentNode;
use graph_craft::document::{DocumentNodeImplementation, NodeInput, value::TaggedValue}; use graph_craft::document::{DocumentNodeImplementation, NodeInput, value::TaggedValue};
use graphene_std::ProtoNodeIdentifier; use graphene_std::ProtoNodeIdentifier;
use graphene_std::subpath::Subpath;
use graphene_std::table::Table; use graphene_std::table::Table;
use graphene_std::text::{TextAlign, TypesettingConfig}; use graphene_std::text::{TextAlign, TypesettingConfig};
use graphene_std::uuid::NodeId; use graphene_std::uuid::NodeId;
@ -300,10 +300,6 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
"graphene_core::raster::BlendNode", "graphene_core::raster::BlendNode",
], ],
}, },
NodeReplacement {
node: graphene_std::raster_nodes::blending_nodes::blend_color_pair::IDENTIFIER,
aliases: &["graphene_raster_nodes::adjustments::BlendColorPairNode", "graphene_core::raster::BlendColorPairNode"],
},
NodeReplacement { NodeReplacement {
node: graphene_std::raster_nodes::blending_nodes::color_overlay::IDENTIFIER, node: graphene_std::raster_nodes::blending_nodes::color_overlay::IDENTIFIER,
aliases: &[ aliases: &[
@ -637,9 +633,9 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
}; };
let vector = Vector::from_subpath(Subpath::from_anchors_linear(points.to_vec(), false)); let vector = Vector::from_subpath(Subpath::from_anchors_linear(points.to_vec(), false));
// Retrieve the output connectors linked to the "Spline" node's output port // Retrieve the output connectors linked to the "Spline" node's output connector
let Some(spline_outputs) = document.network_interface.outward_wires(network_path)?.get(&OutputConnector::node(*node_id, 0)).cloned() else { let Some(spline_outputs) = document.network_interface.outward_wires(network_path)?.get(&OutputConnector::node(*node_id, 0)).cloned() else {
log::error!("Vec of InputConnector Spline node is connected to its output port 0."); log::error!("Vec of InputConnector Spline node is connected to its output connector 0.");
return None; return None;
}; };
@ -684,7 +680,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
// Reposition the new "Path" node with an offset relative to the original "Spline" node's position // 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); 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 // Redirect each output connection from the old node to the new "Spline" node's output connector
for input_connector in spline_outputs { for input_connector in spline_outputs {
document.network_interface.set_input(&input_connector, NodeInput::node(new_spline_id, 0), network_path); document.network_interface.set_input(&input_connector, NodeInput::node(new_spline_id, 0), network_path);
} }

View file

@ -16,10 +16,12 @@ pub struct MenuBarMessageHandler {
pub has_selected_nodes: bool, pub has_selected_nodes: bool,
pub has_selected_layers: bool, pub has_selected_layers: bool,
pub has_selection_history: (bool, bool), pub has_selection_history: (bool, bool),
pub spreadsheet_view_open: bool,
pub message_logging_verbosity: MessageLoggingVerbosity, pub message_logging_verbosity: MessageLoggingVerbosity,
pub reset_node_definitions_on_open: bool, pub reset_node_definitions_on_open: bool,
pub make_path_editable_is_allowed: bool, pub make_path_editable_is_allowed: bool,
pub data_panel_open: bool,
pub layers_panel_open: bool,
pub properties_panel_open: bool,
} }
#[message_handler_data] #[message_handler_data]
@ -585,18 +587,40 @@ impl LayoutHolder for MenuBarMessageHandler {
disabled: no_active_document, disabled: no_active_document,
..MenuBarEntry::default() ..MenuBarEntry::default()
}], }],
]),
),
MenuBarEntry::new_root(
"Window".into(),
false,
MenuBarEntryChildren(vec![
vec![
MenuBarEntry {
label: "Properties".into(),
icon: Some(if self.properties_panel_open { "CheckboxChecked" } else { "CheckboxUnchecked" }.into()),
shortcut: action_keys!(PortfolioMessageDiscriminant::TogglePropertiesPanelOpen),
action: MenuBarEntry::create_action(|_| PortfolioMessage::TogglePropertiesPanelOpen.into()),
..MenuBarEntry::default()
},
MenuBarEntry {
label: "Layers".into(),
icon: Some(if self.layers_panel_open { "CheckboxChecked" } else { "CheckboxUnchecked" }.into()),
shortcut: action_keys!(PortfolioMessageDiscriminant::ToggleLayersPanelOpen),
action: MenuBarEntry::create_action(|_| PortfolioMessage::ToggleLayersPanelOpen.into()),
..MenuBarEntry::default()
},
],
vec![MenuBarEntry { vec![MenuBarEntry {
label: "Window: Spreadsheet".into(), label: "Data".into(),
icon: Some(if self.spreadsheet_view_open { "CheckboxChecked" } else { "CheckboxUnchecked" }.into()), icon: Some(if self.data_panel_open { "CheckboxChecked" } else { "CheckboxUnchecked" }.into()),
action: MenuBarEntry::create_action(|_| SpreadsheetMessage::ToggleOpen.into()), shortcut: action_keys!(PortfolioMessageDiscriminant::ToggleDataPanelOpen),
disabled: no_active_document, action: MenuBarEntry::create_action(|_| PortfolioMessage::ToggleDataPanelOpen.into()),
..MenuBarEntry::default() ..MenuBarEntry::default()
}], }],
]), ]),
), ),
MenuBarEntry::new_root( MenuBarEntry::new_root(
"Help".into(), "Help".into(),
true, false,
MenuBarEntryChildren(vec![ MenuBarEntryChildren(vec![
vec![MenuBarEntry { vec![MenuBarEntry {
label: "About Graphite…".into(), label: "About Graphite…".into(),

View file

@ -4,7 +4,6 @@ mod portfolio_message_handler;
pub mod document; pub mod document;
pub mod document_migration; pub mod document_migration;
pub mod menu_bar; pub mod menu_bar;
pub mod spreadsheet;
pub mod utility_types; pub mod utility_types;
#[doc(inline)] #[doc(inline)]

View file

@ -15,8 +15,6 @@ pub enum PortfolioMessage {
MenuBar(MenuBarMessage), MenuBar(MenuBarMessage),
#[child] #[child]
Document(DocumentMessage), Document(DocumentMessage),
#[child]
Spreadsheet(SpreadsheetMessage),
// Messages // Messages
Init, Init,
@ -128,6 +126,9 @@ pub enum PortfolioMessage {
document_id: DocumentId, document_id: DocumentId,
ignore_hash: bool, ignore_hash: bool,
}, },
ToggleDataPanelOpen,
TogglePropertiesPanelOpen,
ToggleLayersPanelOpen,
ToggleRulers, ToggleRulers,
UpdateDocumentWidgets, UpdateDocumentWidgets,
UpdateOpenDocumentsList, UpdateOpenDocumentsList,

View file

@ -1,6 +1,5 @@
use super::document::utility_types::document_metadata::LayerNodeIdentifier; use super::document::utility_types::document_metadata::LayerNodeIdentifier;
use super::document::utility_types::network_interface; use super::document::utility_types::network_interface;
use super::spreadsheet::SpreadsheetMessageHandler;
use super::utility_types::{PanelType, PersistentData}; use super::utility_types::{PanelType, PersistentData};
use crate::application::generate_uuid; use crate::application::generate_uuid;
use crate::consts::{DEFAULT_DOCUMENT_NAME, DEFAULT_STROKE_WIDTH}; use crate::consts::{DEFAULT_DOCUMENT_NAME, DEFAULT_STROKE_WIDTH};
@ -23,11 +22,12 @@ use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::utility_functions::make_path_editable_is_allowed; use crate::messages::tool::common_functionality::utility_functions::make_path_editable_is_allowed;
use crate::messages::tool::utility_types::{HintData, HintGroup, ToolType}; use crate::messages::tool::utility_types::{HintData, HintGroup, ToolType};
use crate::node_graph_executor::{ExportConfig, NodeGraphExecutor}; use crate::node_graph_executor::{ExportConfig, NodeGraphExecutor};
use bezier_rs::BezierHandles; use derivative::*;
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use graph_craft::document::NodeId; use graph_craft::document::NodeId;
use graphene_std::Color; use graphene_std::Color;
use graphene_std::renderer::Quad; use graphene_std::renderer::Quad;
use graphene_std::subpath::BezierHandles;
use graphene_std::text::Font; use graphene_std::text::Font;
use graphene_std::vector::misc::HandleId; use graphene_std::vector::misc::HandleId;
use graphene_std::vector::{PointId, SegmentId, Vector, VectorModificationType}; use graphene_std::vector::{PointId, SegmentId, Vector, VectorModificationType};
@ -37,14 +37,15 @@ use std::vec;
pub struct PortfolioMessageContext<'a> { pub struct PortfolioMessageContext<'a> {
pub ipp: &'a InputPreprocessorMessageHandler, pub ipp: &'a InputPreprocessorMessageHandler,
pub preferences: &'a PreferencesMessageHandler, pub preferences: &'a PreferencesMessageHandler,
pub animation: &'a AnimationMessageHandler,
pub current_tool: &'a ToolType, pub current_tool: &'a ToolType,
pub message_logging_verbosity: MessageLoggingVerbosity, pub message_logging_verbosity: MessageLoggingVerbosity,
pub reset_node_definitions_on_open: bool, pub reset_node_definitions_on_open: bool,
pub timing_information: TimingInformation, pub timing_information: TimingInformation,
pub animation: &'a AnimationMessageHandler,
} }
#[derive(Debug, Default, ExtractField)] #[derive(Debug, Derivative, ExtractField)]
#[derivative(Default)]
pub struct PortfolioMessageHandler { pub struct PortfolioMessageHandler {
menu_bar_message_handler: MenuBarMessageHandler, menu_bar_message_handler: MenuBarMessageHandler,
pub documents: HashMap<DocumentId, DocumentMessageHandler>, pub documents: HashMap<DocumentId, DocumentMessageHandler>,
@ -55,10 +56,13 @@ pub struct PortfolioMessageHandler {
pub persistent_data: PersistentData, pub persistent_data: PersistentData,
pub executor: NodeGraphExecutor, pub executor: NodeGraphExecutor,
pub selection_mode: SelectionMode, pub selection_mode: SelectionMode,
/// The spreadsheet UI allows for graph data to be previewed.
pub spreadsheet: SpreadsheetMessageHandler,
device_pixel_ratio: Option<f64>, device_pixel_ratio: Option<f64>,
pub reset_node_definitions_on_open: bool, pub reset_node_definitions_on_open: bool,
pub data_panel_open: bool,
#[derivative(Default(value = "true"))]
pub layers_panel_open: bool,
#[derivative(Default(value = "true"))]
pub properties_panel_open: bool,
} }
#[message_handler_data] #[message_handler_data]
@ -67,11 +71,11 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
let PortfolioMessageContext { let PortfolioMessageContext {
ipp, ipp,
preferences, preferences,
animation,
current_tool, current_tool,
message_logging_verbosity, message_logging_verbosity,
reset_node_definitions_on_open, reset_node_definitions_on_open,
timing_information, timing_information,
animation,
} = context; } = context;
match message { match message {
@ -86,7 +90,9 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
self.menu_bar_message_handler.has_selected_layers = false; self.menu_bar_message_handler.has_selected_layers = false;
self.menu_bar_message_handler.has_selection_history = (false, false); self.menu_bar_message_handler.has_selection_history = (false, false);
self.menu_bar_message_handler.make_path_editable_is_allowed = false; self.menu_bar_message_handler.make_path_editable_is_allowed = false;
self.menu_bar_message_handler.spreadsheet_view_open = self.spreadsheet.spreadsheet_view_open; self.menu_bar_message_handler.data_panel_open = self.data_panel_open;
self.menu_bar_message_handler.layers_panel_open = self.layers_panel_open;
self.menu_bar_message_handler.properties_panel_open = self.properties_panel_open;
self.menu_bar_message_handler.message_logging_verbosity = message_logging_verbosity; self.menu_bar_message_handler.message_logging_verbosity = message_logging_verbosity;
self.menu_bar_message_handler.reset_node_definitions_on_open = reset_node_definitions_on_open; self.menu_bar_message_handler.reset_node_definitions_on_open = reset_node_definitions_on_open;
@ -108,9 +114,6 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
self.menu_bar_message_handler.process_message(message, responses, ()); self.menu_bar_message_handler.process_message(message, responses, ());
} }
PortfolioMessage::Spreadsheet(message) => {
self.spreadsheet.process_message(message, responses, ());
}
PortfolioMessage::Document(message) => { PortfolioMessage::Document(message) => {
if let Some(document_id) = self.active_document_id { if let Some(document_id) = self.active_document_id {
if let Some(document) = self.documents.get_mut(&document_id) { if let Some(document) = self.documents.get_mut(&document_id) {
@ -122,6 +125,9 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
current_tool, current_tool,
preferences, preferences,
device_pixel_ratio: self.device_pixel_ratio.unwrap_or(1.), device_pixel_ratio: self.device_pixel_ratio.unwrap_or(1.),
data_panel_open: self.data_panel_open,
layers_panel_open: self.layers_panel_open,
properties_panel_open: self.properties_panel_open,
}; };
document.process_message(message, responses, document_inputs) document.process_message(message, responses, document_inputs)
} }
@ -156,6 +162,9 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
current_tool, current_tool,
preferences, preferences,
device_pixel_ratio: self.device_pixel_ratio.unwrap_or(1.), device_pixel_ratio: self.device_pixel_ratio.unwrap_or(1.),
data_panel_open: self.data_panel_open,
layers_panel_open: self.layers_panel_open,
properties_panel_open: self.properties_panel_open,
}; };
document.process_message(message, responses, document_inputs) document.process_message(message, responses, document_inputs)
} }
@ -195,12 +204,13 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
} }
PortfolioMessage::CloseAllDocuments => { PortfolioMessage::CloseAllDocuments => {
if self.active_document_id.is_some() { if self.active_document_id.is_some() {
responses.add(BroadcastEvent::ToolAbort); responses.add(EventMessage::ToolAbort);
responses.add(ToolMessage::DeactivateTools); responses.add(ToolMessage::DeactivateTools);
// Clear relevant UI layouts if there are no documents // Clear relevant UI layouts if there are no documents
responses.add(PropertiesPanelMessage::Clear); responses.add(PropertiesPanelMessage::Clear);
responses.add(DocumentMessage::ClearLayersPanel); responses.add(DocumentMessage::ClearLayersPanel);
responses.add(DataPanelMessage::ClearLayout);
let hint_data = HintData(vec![HintGroup(vec![])]); let hint_data = HintData(vec![HintGroup(vec![])]);
responses.add(FrontendMessage::UpdateInputHints { hint_data }); responses.add(FrontendMessage::UpdateInputHints { hint_data });
} }
@ -225,6 +235,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
// Clear UI layouts that assume the existence of a document // Clear UI layouts that assume the existence of a document
responses.add(PropertiesPanelMessage::Clear); responses.add(PropertiesPanelMessage::Clear);
responses.add(DocumentMessage::ClearLayersPanel); responses.add(DocumentMessage::ClearLayersPanel);
responses.add(DataPanelMessage::ClearLayout);
let hint_data = HintData(vec![HintGroup(vec![])]); let hint_data = HintData(vec![HintGroup(vec![])]);
responses.add(FrontendMessage::UpdateInputHints { hint_data }); responses.add(FrontendMessage::UpdateInputHints { hint_data });
} }
@ -239,7 +250,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
PortfolioMessage::CloseDocumentWithConfirmation { document_id } => { PortfolioMessage::CloseDocumentWithConfirmation { document_id } => {
let target_document = self.documents.get(&document_id).unwrap(); let target_document = self.documents.get(&document_id).unwrap();
if target_document.is_saved() { if target_document.is_saved() {
responses.add(BroadcastEvent::ToolAbort); responses.add(EventMessage::ToolAbort);
responses.add(PortfolioMessage::CloseDocument { document_id }); responses.add(PortfolioMessage::CloseDocument { document_id });
} else { } else {
let dialog = simple_dialogs::CloseDocumentDialog { let dialog = simple_dialogs::CloseDocumentDialog {
@ -344,13 +355,13 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
self.persistent_data.font_cache.insert(font, preview_url, data); self.persistent_data.font_cache.insert(font, preview_url, data);
self.executor.update_font_cache(self.persistent_data.font_cache.clone()); self.executor.update_font_cache(self.persistent_data.font_cache.clone());
for document_id in self.document_ids.iter() { for document_id in self.document_ids.iter() {
let inspect_node = self.inspect_node_id(); let node_to_inspect = self.node_to_inspect();
if let Ok(message) = self.executor.submit_node_graph_evaluation( if let Ok(message) = self.executor.submit_node_graph_evaluation(
self.documents.get_mut(document_id).expect("Tried to render non-existent document"), self.documents.get_mut(document_id).expect("Tried to render non-existent document"),
*document_id, *document_id,
ipp.viewport_bounds.size().as_uvec2(), ipp.viewport_bounds.size().as_uvec2(),
timing_information, timing_information,
inspect_node, node_to_inspect,
true, true,
) { ) {
responses.add_front(message); responses.add_front(message);
@ -384,11 +395,11 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
let document_id = DocumentId(generate_uuid()); let document_id = DocumentId(generate_uuid());
if self.active_document().is_some() { if self.active_document().is_some() {
new_responses.add(BroadcastEvent::ToolAbort); new_responses.add(EventMessage::ToolAbort);
new_responses.add(NavigationMessage::CanvasPan { delta: (0., 0.).into() }); new_responses.add(NavigationMessage::CanvasPan { delta: (0., 0.).into() });
} }
self.load_document(new_document, document_id, &mut new_responses, false); self.load_document(new_document, document_id, self.layers_panel_open, &mut new_responses, false);
new_responses.add(PortfolioMessage::SelectDocument { document_id }); new_responses.add(PortfolioMessage::SelectDocument { document_id });
new_responses.extend(responses.drain(..)); new_responses.extend(responses.drain(..));
*responses = new_responses; *responses = new_responses;
@ -504,7 +515,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
document.set_save_state(document_is_saved); document.set_save_state(document_is_saved);
// Load the document into the portfolio so it opens in the editor // Load the document into the portfolio so it opens in the editor
self.load_document(document, document_id, responses, to_front); self.load_document(document, document_id, self.layers_panel_open, responses, to_front);
} }
PortfolioMessage::PasteIntoFolder { clipboard, parent, insert_index } => { PortfolioMessage::PasteIntoFolder { clipboard, parent, insert_index } => {
let mut all_new_ids = Vec::new(); let mut all_new_ids = Vec::new();
@ -770,7 +781,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
if create_document { if create_document {
responses.add(PortfolioMessage::NewDocumentWithName { responses.add(PortfolioMessage::NewDocumentWithName {
name: name.clone().unwrap_or("Untitled Document".into()), name: name.clone().unwrap_or(DEFAULT_DOCUMENT_NAME.into()),
}); });
} }
@ -801,7 +812,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
if create_document { if create_document {
responses.add(PortfolioMessage::NewDocumentWithName { responses.add(PortfolioMessage::NewDocumentWithName {
name: name.clone().unwrap_or("Untitled Document".into()), name: name.clone().unwrap_or(DEFAULT_DOCUMENT_NAME.into()),
}); });
} }
@ -863,8 +874,8 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
responses.add(ToolMessage::InitTools); responses.add(ToolMessage::InitTools);
responses.add(NodeGraphMessage::Init); responses.add(NodeGraphMessage::Init);
responses.add(OverlaysMessage::Draw); responses.add(OverlaysMessage::Draw);
responses.add(BroadcastEvent::ToolAbort); responses.add(EventMessage::ToolAbort);
responses.add(BroadcastEvent::SelectionChanged); responses.add(EventMessage::SelectionChanged);
responses.add(NavigationMessage::CanvasPan { delta: (0., 0.).into() }); responses.add(NavigationMessage::CanvasPan { delta: (0., 0.).into() });
responses.add(NodeGraphMessage::RunDocumentGraph); responses.add(NodeGraphMessage::RunDocumentGraph);
responses.add(DocumentMessage::GraphViewOverlay { open: node_graph_open }); responses.add(DocumentMessage::GraphViewOverlay { open: node_graph_open });
@ -918,13 +929,13 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
} }
} }
PortfolioMessage::SubmitGraphRender { document_id, ignore_hash } => { PortfolioMessage::SubmitGraphRender { document_id, ignore_hash } => {
let inspect_node = self.inspect_node_id(); let node_to_inspect = self.node_to_inspect();
let result = self.executor.submit_node_graph_evaluation( let result = self.executor.submit_node_graph_evaluation(
self.documents.get_mut(&document_id).expect("Tried to render non-existent document"), self.documents.get_mut(&document_id).expect("Tried to render non-existent document"),
document_id, document_id,
ipp.viewport_bounds.size().as_uvec2(), ipp.viewport_bounds.size().as_uvec2(),
timing_information, timing_information,
inspect_node, node_to_inspect,
ignore_hash, ignore_hash,
); );
@ -938,6 +949,58 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
Ok(message) => responses.add_front(message), Ok(message) => responses.add_front(message),
} }
} }
PortfolioMessage::ToggleDataPanelOpen => {
self.data_panel_open = !self.data_panel_open;
responses.add(MenuBarMessage::SendLayout);
// Run the graph to grab the data
if self.data_panel_open {
// When opening, we make the frontend show the panel first so it can start receiving its message subscriptions for the data it will display
responses.add(FrontendMessage::UpdateDataPanelState { open: self.data_panel_open });
responses.add(NodeGraphMessage::RunDocumentGraph);
} else {
// If we don't clear the panel, the layout diffing system will assume widgets still exist when it attempts to update the data panel next time it is opened
responses.add(DataPanelMessage::ClearLayout);
// When closing, we make the frontend hide the panel last so it can finish receiving its message subscriptions before it is destroyed
responses.add(FrontendMessage::UpdateDataPanelState { open: self.data_panel_open });
}
}
PortfolioMessage::TogglePropertiesPanelOpen => {
self.properties_panel_open = !self.properties_panel_open;
responses.add(MenuBarMessage::SendLayout);
responses.add(FrontendMessage::UpdatePropertiesPanelState { open: self.properties_panel_open });
// Run the graph to grab the data
if self.properties_panel_open {
responses.add(NodeGraphMessage::RunDocumentGraph);
}
responses.add(PropertiesPanelMessage::Refresh);
}
PortfolioMessage::ToggleLayersPanelOpen => {
self.layers_panel_open = !self.layers_panel_open;
responses.add(MenuBarMessage::SendLayout);
// Run the graph to grab the data
if self.layers_panel_open {
// When opening, we make the frontend show the panel first so it can start receiving its message subscriptions for the data it will display
responses.add(FrontendMessage::UpdateLayersPanelState { open: self.layers_panel_open });
responses.add(NodeGraphMessage::RunDocumentGraph);
responses.add(DeferMessage::AfterGraphRun {
messages: vec![NodeGraphMessage::UpdateLayerPanel.into(), DocumentMessage::DocumentStructureChanged.into()],
});
} else {
// If we don't clear the panel, the layout diffing system will assume widgets still exist when it attempts to update the layers panel next time it is opened
responses.add(DocumentMessage::ClearLayersPanel);
// When closing, we make the frontend hide the panel last so it can finish receiving its message subscriptions before it is destroyed
responses.add(FrontendMessage::UpdateLayersPanelState { open: self.layers_panel_open });
}
}
PortfolioMessage::ToggleRulers => { PortfolioMessage::ToggleRulers => {
if let Some(document) = self.active_document_mut() { if let Some(document) = self.active_document_mut() {
document.rulers_visible = !document.rulers_visible; document.rulers_visible = !document.rulers_visible;
@ -987,6 +1050,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
PasteIntoFolder, PasteIntoFolder,
PrevDocument, PrevDocument,
ToggleRulers, ToggleRulers,
ToggleDataPanelOpen,
); );
// Extend with actions that require an active document // Extend with actions that require an active document
@ -1056,19 +1120,19 @@ impl PortfolioMessageHandler {
} }
} }
fn load_document(&mut self, new_document: DocumentMessageHandler, document_id: DocumentId, responses: &mut VecDeque<Message>, to_front: bool) { fn load_document(&mut self, new_document: DocumentMessageHandler, document_id: DocumentId, layers_panel_open: bool, responses: &mut VecDeque<Message>, to_front: bool) {
if to_front { if to_front {
self.document_ids.push_front(document_id); self.document_ids.push_front(document_id);
} else { } else {
self.document_ids.push_back(document_id); self.document_ids.push_back(document_id);
} }
new_document.update_layers_panel_control_bar_widgets(responses); new_document.update_layers_panel_control_bar_widgets(layers_panel_open, responses);
new_document.update_layers_panel_bottom_bar_widgets(responses); new_document.update_layers_panel_bottom_bar_widgets(layers_panel_open, responses);
self.documents.insert(document_id, new_document); self.documents.insert(document_id, new_document);
if self.active_document().is_some() { if self.active_document().is_some() {
responses.add(BroadcastEvent::ToolAbort); responses.add(EventMessage::ToolAbort);
responses.add(ToolMessage::DeactivateTools); responses.add(ToolMessage::DeactivateTools);
} else { } else {
// Load the default font upon creating the first document // Load the default font upon creating the first document
@ -1111,17 +1175,17 @@ impl PortfolioMessageHandler {
result result
} }
/// Get the id of the node that should be used as the target for the spreadsheet /// Get the ID of the selected node that should be used as the current source for the Data panel.
pub fn inspect_node_id(&self) -> Option<NodeId> { pub fn node_to_inspect(&self) -> Option<NodeId> {
// Spreadsheet not open, skipping // Skip if the Data panel is not open
if !self.spreadsheet.spreadsheet_view_open { if !self.data_panel_open {
return None; return None;
} }
let document = self.documents.get(&self.active_document_id?)?; let document = self.documents.get(&self.active_document_id?)?;
let selected_nodes = document.network_interface.selected_nodes().0; let selected_nodes = document.network_interface.selected_nodes().0;
// Selected nodes != 1, skipping // Skip if there is not exactly one selected node
if selected_nodes.len() != 1 { if selected_nodes.len() != 1 {
return None; return None;
} }

View file

@ -1,7 +0,0 @@
mod spreadsheet_message;
mod spreadsheet_message_handler;
#[doc(inline)]
pub use spreadsheet_message::*;
#[doc(inline)]
pub use spreadsheet_message_handler::*;

View file

@ -1,326 +0,0 @@
use super::VectorDomain;
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, LayoutTarget, WidgetLayout};
use crate::messages::prelude::*;
use crate::messages::tool::tool_messages::tool_prelude::*;
use graph_craft::document::NodeId;
use graphene_std::Color;
use graphene_std::Context;
use graphene_std::memo::IORecord;
use graphene_std::raster::Image;
use graphene_std::table::Table;
use graphene_std::vector::Vector;
use graphene_std::{Artboard, Graphic};
use std::any::Any;
use std::sync::Arc;
/// The spreadsheet UI allows for graph data to be previewed.
#[derive(Default, Debug, Clone, ExtractField)]
pub struct SpreadsheetMessageHandler {
/// Sets whether or not the spreadsheet is drawn.
pub spreadsheet_view_open: bool,
inspect_node: Option<NodeId>,
introspected_data: Option<Arc<dyn Any + Send + Sync>>,
element_path: Vec<usize>,
viewing_vector_domain: VectorDomain,
}
#[message_handler_data]
impl MessageHandler<SpreadsheetMessage, ()> for SpreadsheetMessageHandler {
fn process_message(&mut self, message: SpreadsheetMessage, responses: &mut VecDeque<Message>, _: ()) {
match message {
SpreadsheetMessage::ToggleOpen => {
self.spreadsheet_view_open = !self.spreadsheet_view_open;
// Run the graph to grab the data
if self.spreadsheet_view_open {
responses.add(NodeGraphMessage::RunDocumentGraph);
}
// Update checked UI state for open
responses.add(MenuBarMessage::SendLayout);
self.update_layout(responses);
}
SpreadsheetMessage::UpdateLayout { mut inspect_result } => {
self.inspect_node = Some(inspect_result.inspect_node);
self.introspected_data = inspect_result.take_data();
self.update_layout(responses)
}
SpreadsheetMessage::PushToElementPath { index } => {
self.element_path.push(index);
self.update_layout(responses);
}
SpreadsheetMessage::TruncateElementPath { len } => {
self.element_path.truncate(len);
self.update_layout(responses);
}
SpreadsheetMessage::ViewVectorDomain { domain } => {
self.viewing_vector_domain = domain;
self.update_layout(responses);
}
}
}
fn actions(&self) -> ActionList {
actions!(SpreadsheetMessage;)
}
}
impl SpreadsheetMessageHandler {
fn update_layout(&mut self, responses: &mut VecDeque<Message>) {
responses.add(FrontendMessage::UpdateSpreadsheetState {
node: self.inspect_node,
open: self.spreadsheet_view_open,
});
if !self.spreadsheet_view_open {
return;
}
let mut layout_data = LayoutData {
current_depth: 0,
desired_path: &mut self.element_path,
breadcrumbs: Vec::new(),
vector_domain: self.viewing_vector_domain,
};
let mut layout = self
.introspected_data
.as_ref()
.map(|instrospected_data| generate_layout(instrospected_data, &mut layout_data))
.unwrap_or_else(|| Some(label("No data")))
.unwrap_or_else(|| label("Failed to downcast data"));
if layout_data.breadcrumbs.len() > 1 {
let breadcrumb = BreadcrumbTrailButtons::new(layout_data.breadcrumbs)
.on_update(|&len| SpreadsheetMessage::TruncateElementPath { len: len as usize }.into())
.widget_holder();
layout.insert(0, LayoutGroup::Row { widgets: vec![breadcrumb] });
}
responses.add(LayoutMessage::SendLayout {
layout: Layout::WidgetLayout(WidgetLayout { layout }),
layout_target: LayoutTarget::Spreadsheet,
});
}
}
struct LayoutData<'a> {
current_depth: usize,
desired_path: &'a mut Vec<usize>,
breadcrumbs: Vec<String>,
vector_domain: VectorDomain,
}
fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'static>, data: &mut LayoutData) -> Option<Vec<LayoutGroup>> {
// We simply try random types. TODO: better strategy.
#[allow(clippy::manual_map)]
if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, Table<Artboard>>>() {
Some(io.output.layout_with_breadcrumb(data))
} else if let Some(io) = introspected_data.downcast_ref::<IORecord<(), Table<Artboard>>>() {
Some(io.output.layout_with_breadcrumb(data))
} else if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, Table<Vector>>>() {
Some(io.output.layout_with_breadcrumb(data))
} else if let Some(io) = introspected_data.downcast_ref::<IORecord<(), Table<Vector>>>() {
Some(io.output.layout_with_breadcrumb(data))
} else if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, Table<Graphic>>>() {
Some(io.output.layout_with_breadcrumb(data))
} else if let Some(io) = introspected_data.downcast_ref::<IORecord<(), Table<Graphic>>>() {
Some(io.output.layout_with_breadcrumb(data))
} else {
None
}
}
fn column_headings(value: &[&str]) -> Vec<WidgetHolder> {
value.iter().map(|text| TextLabel::new(*text).widget_holder()).collect()
}
fn label(x: impl Into<String>) -> Vec<LayoutGroup> {
let error = vec![TextLabel::new(x).widget_holder()];
vec![LayoutGroup::Row { widgets: error }]
}
trait TableRowLayout {
fn type_name() -> &'static str;
fn identifier(&self) -> String;
fn layout_with_breadcrumb(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
data.breadcrumbs.push(self.identifier());
self.compute_layout(data)
}
fn compute_layout(&self, data: &mut LayoutData) -> Vec<LayoutGroup>;
}
impl TableRowLayout for Graphic {
fn type_name() -> &'static str {
"Graphic"
}
fn identifier(&self) -> String {
match self {
Self::Graphic(graphic) => graphic.identifier(),
Self::Vector(vector) => vector.identifier(),
Self::RasterCPU(_) => "Raster (on CPU)".to_string(),
Self::RasterGPU(_) => "Raster (on GPU)".to_string(),
}
}
// Don't put a breadcrumb for Graphic
fn layout_with_breadcrumb(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
self.compute_layout(data)
}
fn compute_layout(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
match self {
Self::Graphic(table) => table.layout_with_breadcrumb(data),
Self::Vector(table) => table.layout_with_breadcrumb(data),
Self::RasterCPU(_) => label("Raster is not supported"),
Self::RasterGPU(_) => label("Raster is not supported"),
}
}
}
impl TableRowLayout for Vector {
fn type_name() -> &'static str {
"Vector"
}
fn identifier(&self) -> String {
format!(
"Vector ({} point{}, {} segment{})",
self.point_domain.ids().len(),
if self.point_domain.ids().len() == 1 { "" } else { "s" },
self.segment_domain.ids().len(),
if self.segment_domain.ids().len() == 1 { "" } else { "s" }
)
}
fn compute_layout(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
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: {}\nPreserves Reference to Upstream Nested Layers for Editing by Tools: {}",
self.style,
colinear,
if self.upstream_nested_layers.is_some() { "Yes" } else { "No" }
))
.multiline(true)
.widget_holder(),
];
let domain_entries = [VectorDomain::Points, VectorDomain::Segments, VectorDomain::Regions]
.into_iter()
.map(|domain| {
RadioEntryData::new(format!("{domain:?}"))
.label(format!("{domain:?}"))
.on_update(move |_| SpreadsheetMessage::ViewVectorDomain { domain }.into())
})
.collect();
let domain = vec![RadioInput::new(domain_entries).selected_index(Some(data.vector_domain as u32)).widget_holder()];
let mut table_rows = Vec::new();
match data.vector_domain {
VectorDomain::Points => {
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()]),
);
}
VectorDomain::Segments => {
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(),
TextLabel::new(format!("{}", end)).widget_holder(),
TextLabel::new(format!("{:?}", handles)).widget_holder(),
]
}));
}
VectorDomain::Regions => {
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(),
TextLabel::new(format!("{}", fill.inner())).widget_holder(),
]
}));
}
}
vec![LayoutGroup::Row { widgets: style }, LayoutGroup::Row { widgets: domain }, LayoutGroup::Table { rows: table_rows }]
}
}
impl TableRowLayout for Image<Color> {
fn type_name() -> &'static str {
"Image"
}
fn identifier(&self) -> String {
format!("Image ({}x{})", self.width, self.height)
}
fn compute_layout(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let rows = vec![vec![TextLabel::new(format!("Image ({}x{})", self.width, self.height)).widget_holder()]];
vec![LayoutGroup::Table { rows }]
}
}
impl TableRowLayout for Artboard {
fn type_name() -> &'static str {
"Artboard"
}
fn identifier(&self) -> String {
self.label.clone()
}
fn compute_layout(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
self.content.compute_layout(data)
}
}
impl<T: TableRowLayout> TableRowLayout for Table<T> {
fn type_name() -> &'static str {
"Table"
}
fn identifier(&self) -> String {
format!("Table<{}> ({} row{})", T::type_name(), self.len(), if self.len() == 1 { "" } else { "s" })
}
fn compute_layout(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
if let Some(index) = data.desired_path.get(data.current_depth).copied() {
if let Some(row) = self.get(index) {
data.current_depth += 1;
let result = row.element.layout_with_breadcrumb(data);
data.current_depth -= 1;
return result;
} else {
warn!("Desired path truncated");
data.desired_path.truncate(data.current_depth);
}
}
let mut rows = self
.iter()
.enumerate()
.map(|(index, row)| {
let (scale, angle, translation) = row.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(row.element.identifier())
.on_update(move |_| SpreadsheetMessage::PushToElementPath { index }.into())
.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!("{}", row.alpha_blending)).widget_holder(),
TextLabel::new(row.source_node_id.map_or_else(|| "-".to_string(), |id| format!("{}", id.0))).widget_holder(),
]
})
.collect::<Vec<_>>();
rows.insert(0, column_headings(&["", "element", "transform", "alpha_blending", "source_node_id"]));
vec![LayoutGroup::Table { rows }]
}
}

View file

@ -43,7 +43,7 @@ pub enum PanelType {
Document, Document,
Layers, Layers,
Properties, Properties,
Spreadsheet, DataPanel,
} }
impl From<String> for PanelType { impl From<String> for PanelType {
@ -52,7 +52,7 @@ impl From<String> for PanelType {
"Document" => PanelType::Document, "Document" => PanelType::Document,
"Layers" => PanelType::Layers, "Layers" => PanelType::Layers,
"Properties" => PanelType::Properties, "Properties" => PanelType::Properties,
"Spreadsheet" => PanelType::Spreadsheet, "Data" => PanelType::DataPanel,
_ => panic!("Unknown panel type: {value}"), _ => panic!("Unknown panel type: {value}"),
} }
} }

View file

@ -62,7 +62,7 @@ impl MessageHandler<PreferencesMessage, ()> for PreferencesMessageHandler {
} }
PreferencesMessage::ResetToDefaults => { PreferencesMessage::ResetToDefaults => {
refresh_dialog(responses); refresh_dialog(responses);
responses.add(KeyMappingMessage::ModifyMapping(MappingVariant::Default)); responses.add(KeyMappingMessage::ModifyMapping { mapping: MappingVariant::Default });
*self = Self::default() *self = Self::default()
} }
@ -80,7 +80,7 @@ impl MessageHandler<PreferencesMessage, ()> for PreferencesMessageHandler {
self.zoom_with_scroll = zoom_with_scroll; self.zoom_with_scroll = zoom_with_scroll;
let variant = if zoom_with_scroll { MappingVariant::ZoomWithScroll } else { MappingVariant::Default }; let variant = if zoom_with_scroll { MappingVariant::ZoomWithScroll } else { MappingVariant::Default };
responses.add(KeyMappingMessage::ModifyMapping(variant)); responses.add(KeyMappingMessage::ModifyMapping { mapping: variant });
} }
PreferencesMessage::SelectionMode { selection_mode } => { PreferencesMessage::SelectionMode { selection_mode } => {
self.selection_mode = selection_mode; self.selection_mode = selection_mode;

View file

@ -1,9 +1,11 @@
// Root // Message-related
pub use crate::utility_traits::{ActionList, AsMessage, HierarchicalTree, MessageHandler, ToDiscriminant, TransitiveChild}; pub use crate::utility_traits::{ActionList, AsMessage, ExtractField, HierarchicalTree, MessageHandler, ToDiscriminant, TransitiveChild};
pub use crate::utility_types::{DebugMessageTree, MessageData}; pub use crate::utility_types::{DebugMessageTree, MessageData};
// Message, MessageData, MessageDiscriminant, MessageHandler // Message, MessageData, MessageDiscriminant, MessageHandler
pub use crate::messages::animation::{AnimationMessage, AnimationMessageDiscriminant, AnimationMessageHandler}; pub use crate::messages::animation::{AnimationMessage, AnimationMessageDiscriminant, AnimationMessageHandler};
pub use crate::messages::app_window::{AppWindowMessage, AppWindowMessageDiscriminant, AppWindowMessageHandler}; pub use crate::messages::app_window::{AppWindowMessage, AppWindowMessageDiscriminant, AppWindowMessageHandler};
pub use crate::messages::broadcast::event::{EventMessage, EventMessageContext, EventMessageDiscriminant, EventMessageHandler};
pub use crate::messages::broadcast::{BroadcastMessage, BroadcastMessageDiscriminant, BroadcastMessageHandler}; pub use crate::messages::broadcast::{BroadcastMessage, BroadcastMessageDiscriminant, BroadcastMessageHandler};
pub use crate::messages::debug::{DebugMessage, DebugMessageDiscriminant, DebugMessageHandler}; pub use crate::messages::debug::{DebugMessage, DebugMessageDiscriminant, DebugMessageHandler};
pub use crate::messages::defer::{DeferMessage, DeferMessageDiscriminant, DeferMessageHandler}; pub use crate::messages::defer::{DeferMessage, DeferMessageDiscriminant, DeferMessageHandler};
@ -17,6 +19,7 @@ pub use crate::messages::input_mapper::key_mapping::{KeyMappingMessage, KeyMappi
pub use crate::messages::input_mapper::{InputMapperMessage, InputMapperMessageContext, InputMapperMessageDiscriminant, InputMapperMessageHandler}; pub use crate::messages::input_mapper::{InputMapperMessage, InputMapperMessageContext, InputMapperMessageDiscriminant, InputMapperMessageHandler};
pub use crate::messages::input_preprocessor::{InputPreprocessorMessage, InputPreprocessorMessageContext, InputPreprocessorMessageDiscriminant, InputPreprocessorMessageHandler}; pub use crate::messages::input_preprocessor::{InputPreprocessorMessage, InputPreprocessorMessageContext, InputPreprocessorMessageDiscriminant, InputPreprocessorMessageHandler};
pub use crate::messages::layout::{LayoutMessage, LayoutMessageDiscriminant, LayoutMessageHandler}; pub use crate::messages::layout::{LayoutMessage, LayoutMessageDiscriminant, LayoutMessageHandler};
pub use crate::messages::portfolio::document::data_panel::{DataPanelMessage, DataPanelMessageDiscriminant};
pub use crate::messages::portfolio::document::graph_operation::{GraphOperationMessage, GraphOperationMessageContext, GraphOperationMessageDiscriminant, GraphOperationMessageHandler}; pub use crate::messages::portfolio::document::graph_operation::{GraphOperationMessage, GraphOperationMessageContext, GraphOperationMessageDiscriminant, GraphOperationMessageHandler};
pub use crate::messages::portfolio::document::navigation::{NavigationMessage, NavigationMessageContext, NavigationMessageDiscriminant, NavigationMessageHandler}; pub use crate::messages::portfolio::document::navigation::{NavigationMessage, NavigationMessageContext, NavigationMessageDiscriminant, NavigationMessageHandler};
pub use crate::messages::portfolio::document::node_graph::{NodeGraphMessage, NodeGraphMessageDiscriminant, NodeGraphMessageHandler}; pub use crate::messages::portfolio::document::node_graph::{NodeGraphMessage, NodeGraphMessageDiscriminant, NodeGraphMessageHandler};
@ -24,15 +27,12 @@ pub use crate::messages::portfolio::document::overlays::{OverlaysMessage, Overla
pub use crate::messages::portfolio::document::properties_panel::{PropertiesPanelMessage, PropertiesPanelMessageDiscriminant, PropertiesPanelMessageHandler}; pub use crate::messages::portfolio::document::properties_panel::{PropertiesPanelMessage, PropertiesPanelMessageDiscriminant, PropertiesPanelMessageHandler};
pub use crate::messages::portfolio::document::{DocumentMessage, DocumentMessageContext, DocumentMessageDiscriminant, DocumentMessageHandler}; pub use crate::messages::portfolio::document::{DocumentMessage, DocumentMessageContext, DocumentMessageDiscriminant, DocumentMessageHandler};
pub use crate::messages::portfolio::menu_bar::{MenuBarMessage, MenuBarMessageDiscriminant, MenuBarMessageHandler}; pub use crate::messages::portfolio::menu_bar::{MenuBarMessage, MenuBarMessageDiscriminant, MenuBarMessageHandler};
pub use crate::messages::portfolio::spreadsheet::{SpreadsheetMessage, SpreadsheetMessageDiscriminant};
pub use crate::messages::portfolio::{PortfolioMessage, PortfolioMessageContext, PortfolioMessageDiscriminant, PortfolioMessageHandler}; pub use crate::messages::portfolio::{PortfolioMessage, PortfolioMessageContext, PortfolioMessageDiscriminant, PortfolioMessageHandler};
pub use crate::messages::preferences::{PreferencesMessage, PreferencesMessageDiscriminant, PreferencesMessageHandler}; pub use crate::messages::preferences::{PreferencesMessage, PreferencesMessageDiscriminant, PreferencesMessageHandler};
pub use crate::messages::tool::transform_layer::{TransformLayerMessage, TransformLayerMessageDiscriminant, TransformLayerMessageHandler}; pub use crate::messages::tool::transform_layer::{TransformLayerMessage, TransformLayerMessageDiscriminant, TransformLayerMessageHandler};
pub use crate::messages::tool::{ToolMessage, ToolMessageContext, ToolMessageDiscriminant, ToolMessageHandler}; pub use crate::messages::tool::{ToolMessage, ToolMessageContext, ToolMessageDiscriminant, ToolMessageHandler};
pub use crate::messages::workspace::{WorkspaceMessage, WorkspaceMessageDiscriminant, WorkspaceMessageHandler};
// Message, MessageDiscriminant // Message, MessageDiscriminant
pub use crate::messages::broadcast::broadcast_event::{BroadcastEvent, BroadcastEventDiscriminant};
pub use crate::messages::message::{Message, MessageDiscriminant}; 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::artboard_tool::{ArtboardToolMessage, ArtboardToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::brush_tool::{BrushToolMessage, BrushToolMessageDiscriminant}; pub use crate::messages::tool::tool_messages::brush_tool::{BrushToolMessage, BrushToolMessageDiscriminant};
@ -48,7 +48,7 @@ pub use crate::messages::tool::tool_messages::shape_tool::{ShapeToolMessage, Sha
pub use crate::messages::tool::tool_messages::spline_tool::{SplineToolMessage, SplineToolMessageDiscriminant}; pub use crate::messages::tool::tool_messages::spline_tool::{SplineToolMessage, SplineToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::text_tool::{TextToolMessage, TextToolMessageDiscriminant}; pub use crate::messages::tool::tool_messages::text_tool::{TextToolMessage, TextToolMessageDiscriminant};
// Helper // Helper/miscellaneous
pub use crate::messages::globals::global_variables::*; pub use crate::messages::globals::global_variables::*;
pub use crate::messages::portfolio::document::utility_types::misc::DocumentId; pub use crate::messages::portfolio::document::utility_types::misc::DocumentId;
pub use graphite_proc_macros::*; pub use graphite_proc_macros::*;

View file

@ -14,7 +14,7 @@ impl AutoPanning {
for message in messages { for message in messages {
responses.add(BroadcastMessage::SubscribeEvent { responses.add(BroadcastMessage::SubscribeEvent {
on: BroadcastEvent::AnimationFrame, on: EventMessage::AnimationFrame,
send: Box::new(message.clone()), send: Box::new(message.clone()),
}); });
} }
@ -27,8 +27,8 @@ impl AutoPanning {
for message in messages { for message in messages {
responses.add(BroadcastMessage::UnsubscribeEvent { responses.add(BroadcastMessage::UnsubscribeEvent {
on: BroadcastEvent::AnimationFrame, on: EventMessage::AnimationFrame,
message: Box::new(message.clone()), send: Box::new(message.clone()),
}); });
} }
} }

View file

@ -3,7 +3,6 @@ use crate::messages::portfolio::document::node_graph::document_node_definitions;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, InputConnector, NodeNetworkInterface, NodeTemplate}; use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, InputConnector, NodeNetworkInterface, NodeTemplate};
use crate::messages::prelude::*; use crate::messages::prelude::*;
use bezier_rs::Subpath;
use glam::DVec2; use glam::DVec2;
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput}; use graph_craft::document::{NodeId, NodeInput};
@ -12,10 +11,11 @@ use graphene_std::Color;
use graphene_std::NodeInputDecleration; use graphene_std::NodeInputDecleration;
use graphene_std::raster::BlendMode; use graphene_std::raster::BlendMode;
use graphene_std::raster_types::{CPU, GPU, Raster}; use graphene_std::raster_types::{CPU, GPU, Raster};
use graphene_std::subpath::Subpath;
use graphene_std::table::Table; use graphene_std::table::Table;
use graphene_std::text::{Font, TypesettingConfig}; use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::misc::ManipulatorPointId; use graphene_std::vector::misc::ManipulatorPointId;
use graphene_std::vector::style::Gradient; use graphene_std::vector::style::{Fill, Gradient};
use graphene_std::vector::{PointId, SegmentId, VectorModificationType}; use graphene_std::vector::{PointId, SegmentId, VectorModificationType};
use std::collections::VecDeque; use std::collections::VecDeque;
@ -273,7 +273,7 @@ pub fn get_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkI
let fill_index = 1; let fill_index = 1;
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Fill")?; let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Fill")?;
let TaggedValue::Fill(graphene_std::vector::style::Fill::Gradient(gradient)) = inputs.get(fill_index)?.as_value()? else { let TaggedValue::Fill(Fill::Gradient(gradient)) = inputs.get(fill_index)?.as_value()? else {
return None; return None;
}; };
Some(gradient.clone()) Some(gradient.clone())
@ -284,7 +284,7 @@ pub fn get_fill_color(layer: LayerNodeIdentifier, network_interface: &NodeNetwor
let fill_index = 1; let fill_index = 1;
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Fill")?; let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Fill")?;
let TaggedValue::Fill(graphene_std::vector::style::Fill::Solid(color)) = inputs.get(fill_index)?.as_value()? else { let TaggedValue::Fill(Fill::Solid(color)) = inputs.get(fill_index)?.as_value()? else {
return None; return None;
}; };
Some(color.to_linear_srgb()) Some(color.to_linear_srgb())

View file

@ -43,18 +43,19 @@ fn draw_line_with_length(line_start: DVec2, line_end: DVec2, transform: DAffine2
} }
} }
/// Draws a dashed outline around a rectangle to visualize the AABB /// Draws a dashed outline around the given rectangle (assumed to be in document space).
/// The provided transform is applied to convert coordinates (e.g., to viewport space) during rendering.
fn draw_dashed_rect_outline(rect: Rect, transform: DAffine2, overlay_context: &mut OverlayContext) { fn draw_dashed_rect_outline(rect: Rect, transform: DAffine2, overlay_context: &mut OverlayContext) {
let min = rect.min(); let min = rect.min();
let max = rect.max(); let max = rect.max();
// Create the four corners of the rectangle // Define corners in document space
let top_left = transform.transform_point2(DVec2::new(min.x, min.y)); let top_left = DVec2::new(min.x, min.y);
let top_right = transform.transform_point2(DVec2::new(max.x, min.y)); let top_right = DVec2::new(max.x, min.y);
let bottom_right = transform.transform_point2(DVec2::new(max.x, max.y)); let bottom_right = DVec2::new(max.x, max.y);
let bottom_left = transform.transform_point2(DVec2::new(min.x, max.y)); let bottom_left = DVec2::new(min.x, max.y);
// Draw the four sides as dashed lines // Draw each edge using document-space coordinates; transform is applied inside draw_dashed_line
draw_dashed_line(top_left, top_right, transform, overlay_context); draw_dashed_line(top_left, top_right, transform, overlay_context);
draw_dashed_line(top_right, bottom_right, transform, overlay_context); draw_dashed_line(top_right, bottom_right, transform, overlay_context);
draw_dashed_line(bottom_right, bottom_left, transform, overlay_context); draw_dashed_line(bottom_right, bottom_left, transform, overlay_context);

View file

@ -17,8 +17,14 @@ pub fn pin_pivot_widget(active: bool, enabled: bool, source: PivotToolSource) ->
.tooltip(String::from(if active { "Unpin Custom Pivot" } else { "Pin Custom Pivot" }) + "\n\nUnless pinned, the pivot will return to its prior reference point when a new selection is made.") .tooltip(String::from(if active { "Unpin Custom Pivot" } else { "Pin Custom Pivot" }) + "\n\nUnless pinned, the pivot will return to its prior reference point when a new selection is made.")
.disabled(!enabled) .disabled(!enabled)
.on_update(move |_| match source { .on_update(move |_| match source {
PivotToolSource::Select => SelectToolMessage::SelectOptions(SelectOptionsUpdate::TogglePivotPinned).into(), PivotToolSource::Select => SelectToolMessage::SelectOptions {
PivotToolSource::Path => PathToolMessage::UpdateOptions(PathOptionsUpdate::TogglePivotPinned).into(), options: SelectOptionsUpdate::TogglePivotPinned,
}
.into(),
PivotToolSource::Path => PathToolMessage::UpdateOptions {
options: PathOptionsUpdate::TogglePivotPinned,
}
.into(),
}) })
.widget_holder() .widget_holder()
} }
@ -41,8 +47,14 @@ pub fn pivot_gizmo_type_widget(state: PivotGizmoState, source: PivotToolSource)
MenuListEntry::new(format!("{gizmo_type:?}")).label(gizmo_type.to_string()).on_commit({ MenuListEntry::new(format!("{gizmo_type:?}")).label(gizmo_type.to_string()).on_commit({
let value = source.clone(); let value = source.clone();
move |_| match value { move |_| match value {
PivotToolSource::Select => SelectToolMessage::SelectOptions(SelectOptionsUpdate::PivotGizmoType(*gizmo_type)).into(), PivotToolSource::Select => SelectToolMessage::SelectOptions {
PivotToolSource::Path => PathToolMessage::UpdateOptions(PathOptionsUpdate::PivotGizmoType(*gizmo_type)).into(), options: SelectOptionsUpdate::PivotGizmoType(*gizmo_type),
}
.into(),
PivotToolSource::Path => PathToolMessage::UpdateOptions {
options: PathOptionsUpdate::PivotGizmoType(*gizmo_type),
}
.into(),
} }
}) })
}) })
@ -57,8 +69,14 @@ pub fn pivot_gizmo_type_widget(state: PivotGizmoState, source: PivotToolSource)
Disabled: rotation and scaling occurs about the center of the selection bounds.", Disabled: rotation and scaling occurs about the center of the selection bounds.",
) )
.on_update(move |optional_input: &CheckboxInput| match source { .on_update(move |optional_input: &CheckboxInput| match source {
PivotToolSource::Select => SelectToolMessage::SelectOptions(SelectOptionsUpdate::TogglePivotGizmoType(optional_input.checked)).into(), PivotToolSource::Select => SelectToolMessage::SelectOptions {
PivotToolSource::Path => PathToolMessage::UpdateOptions(PathOptionsUpdate::TogglePivotGizmoType(optional_input.checked)).into(), options: SelectOptionsUpdate::TogglePivotGizmoType(optional_input.checked),
}
.into(),
PivotToolSource::Path => PathToolMessage::UpdateOptions {
options: PathOptionsUpdate::TogglePivotGizmoType(optional_input.checked),
}
.into(),
}) })
.widget_holder(), .widget_holder(),
Separator::new(SeparatorType::Related).widget_holder(), Separator::new(SeparatorType::Related).widget_holder(),

View file

@ -1,6 +1,6 @@
use super::graph_modification_utils::merge_layers; use super::graph_modification_utils::merge_layers;
use super::snapping::{SnapCache, SnapCandidatePoint, SnapData, SnapManager, SnappedPoint}; use super::snapping::{SnapCache, SnapCandidatePoint, SnapData, SnapManager, SnappedPoint};
use super::utility_functions::{adjust_handle_colinearity, calculate_bezier_bbox, calculate_segment_angle, restore_g1_continuity, restore_previous_handle_position}; use super::utility_functions::{adjust_handle_colinearity, calculate_segment_angle, restore_g1_continuity, restore_previous_handle_position};
use crate::consts::HANDLE_LENGTH_FACTOR; use crate::consts::HANDLE_LENGTH_FACTOR;
use crate::messages::portfolio::document::overlays::utility_functions::selected_segments; use crate::messages::portfolio::document::overlays::utility_functions::selected_segments;
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier}; use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
@ -9,12 +9,15 @@ use crate::messages::portfolio::document::utility_types::network_interface::Node
use crate::messages::preferences::SelectionMode; use crate::messages::preferences::SelectionMode;
use crate::messages::prelude::*; use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::snapping::SnapTypeConfiguration; use crate::messages::tool::common_functionality::snapping::SnapTypeConfiguration;
use crate::messages::tool::common_functionality::utility_functions::{is_intersecting, is_visible_point}; use crate::messages::tool::common_functionality::utility_functions::is_visible_point;
use crate::messages::tool::tool_messages::path_tool::{PathOverlayMode, PointSelectState}; use crate::messages::tool::tool_messages::path_tool::{PathOverlayMode, PointSelectState};
use bezier_rs::{Bezier, BezierHandles, Subpath, TValue};
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use graphene_std::vector::misc::{HandleId, ManipulatorPointId}; use graphene_std::subpath::{BezierHandles, Subpath};
use graphene_std::subpath::{PathSegPoints, pathseg_points};
use graphene_std::vector::algorithms::bezpath_algorithms::pathseg_compute_lookup_table;
use graphene_std::vector::misc::{HandleId, ManipulatorPointId, dvec2_to_point, point_to_dvec2};
use graphene_std::vector::{HandleExt, PointId, SegmentId, Vector, VectorModificationType}; use graphene_std::vector::{HandleExt, PointId, SegmentId, Vector, VectorModificationType};
use kurbo::{Affine, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveNearest, PathSeg, Rect, Shape};
use std::f64::consts::TAU; use std::f64::consts::TAU;
#[derive(Debug, Copy, Clone, PartialEq, Eq)] #[derive(Debug, Copy, Clone, PartialEq, Eq)]
@ -26,7 +29,7 @@ pub enum SelectionChange {
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub enum SelectionShape<'a> { pub enum SelectionShape<'a> {
Box([DVec2; 2]), Box(Rect),
Lasso(&'a Vec<DVec2>), Lasso(&'a Vec<DVec2>),
} }
@ -185,7 +188,7 @@ pub type OpposingHandleLengths = HashMap<LayerNodeIdentifier, HashMap<HandleId,
pub struct ClosestSegment { pub struct ClosestSegment {
layer: LayerNodeIdentifier, layer: LayerNodeIdentifier,
segment: SegmentId, segment: SegmentId,
bezier: Bezier, bezier: PathSeg,
points: [PointId; 2], points: [PointId; 2],
colinear: [Option<HandleId>; 2], colinear: [Option<HandleId>; 2],
t: f64, t: f64,
@ -205,12 +208,12 @@ impl ClosestSegment {
self.points self.points
} }
pub fn bezier(&self) -> Bezier { pub fn pathseg(&self) -> PathSeg {
self.bezier self.bezier
} }
pub fn closest_point_document(&self) -> DVec2 { pub fn closest_point_document(&self) -> DVec2 {
self.bezier.evaluate(TValue::Parametric(self.t)) point_to_dvec2(self.bezier.eval(self.t))
} }
pub fn closest_point_to_viewport(&self) -> DVec2 { pub fn closest_point_to_viewport(&self) -> DVec2 {
@ -219,7 +222,7 @@ impl ClosestSegment {
pub fn closest_point(&self, document_metadata: &DocumentMetadata, network_interface: &NodeNetworkInterface) -> DVec2 { pub fn closest_point(&self, document_metadata: &DocumentMetadata, network_interface: &NodeNetworkInterface) -> DVec2 {
let transform = document_metadata.transform_to_viewport_if_feeds(self.layer, network_interface); let transform = document_metadata.transform_to_viewport_if_feeds(self.layer, network_interface);
let bezier_point = self.bezier.evaluate(TValue::Parametric(self.t)); let bezier_point = point_to_dvec2(self.bezier.eval(self.t));
transform.transform_point2(bezier_point) transform.transform_point2(bezier_point)
} }
@ -228,10 +231,10 @@ impl ClosestSegment {
let transform = document_metadata.transform_to_viewport_if_feeds(self.layer, network_interface); let transform = document_metadata.transform_to_viewport_if_feeds(self.layer, network_interface);
let layer_mouse_pos = transform.inverse().transform_point2(mouse_position); let layer_mouse_pos = transform.inverse().transform_point2(mouse_position);
let t = self.bezier.project(layer_mouse_pos).clamp(0., 1.); let t = self.bezier.nearest(dvec2_to_point(layer_mouse_pos), DEFAULT_ACCURACY).t.clamp(0., 1.);
self.t = t; self.t = t;
let bezier_point = self.bezier.evaluate(TValue::Parametric(t)); let bezier_point = point_to_dvec2(self.bezier.eval(t));
let bezier_point = transform.transform_point2(bezier_point); let bezier_point = transform.transform_point2(bezier_point);
self.bezier_point_to_viewport = bezier_point; self.bezier_point_to_viewport = bezier_point;
} }
@ -249,22 +252,24 @@ impl ClosestSegment {
let transform = document_metadata.transform_to_viewport_if_feeds(self.layer, network_interface); let transform = document_metadata.transform_to_viewport_if_feeds(self.layer, network_interface);
// Split the Bezier at the parameter `t` // Split the Bezier at the parameter `t`
let [first, second] = self.bezier.split(TValue::Parametric(self.t)); let first = self.bezier.subsegment(0_f64..self.t);
let second = self.bezier.subsegment(self.t..1.);
// Transform the handle positions to viewport space // Transform the handle positions to viewport space
let first_handle = first.handle_end().map(|handle| transform.transform_point2(handle)); let first_handle = pathseg_points(first).p2.map(|handle| transform.transform_point2(handle));
let second_handle = second.handle_start().map(|handle| transform.transform_point2(handle)); let second_handle = pathseg_points(second).p1.map(|handle| transform.transform_point2(handle));
(first_handle, second_handle) (first_handle, second_handle)
} }
pub fn adjusted_insert(&self, responses: &mut VecDeque<Message>) -> (PointId, [SegmentId; 2]) { pub fn adjusted_insert(&self, responses: &mut VecDeque<Message>) -> (PointId, [SegmentId; 2]) {
let layer = self.layer; let layer = self.layer;
let [first, second] = self.bezier.split(TValue::Parametric(self.t)); let first = pathseg_points(self.bezier.subsegment(0_f64..self.t));
let second = pathseg_points(self.bezier.subsegment(self.t..1.));
// Point // Point
let midpoint = PointId::generate(); let midpoint = PointId::generate();
let modification_type = VectorModificationType::InsertPoint { id: midpoint, position: first.end }; let modification_type = VectorModificationType::InsertPoint { id: midpoint, position: first.p3 };
responses.add(GraphOperationMessage::Vector { layer, modification_type }); responses.add(GraphOperationMessage::Vector { layer, modification_type });
// First segment // First segment
@ -272,7 +277,7 @@ impl ClosestSegment {
let modification_type = VectorModificationType::InsertSegment { let modification_type = VectorModificationType::InsertSegment {
id: segment_ids[0], id: segment_ids[0],
points: [self.points[0], midpoint], points: [self.points[0], midpoint],
handles: [first.handle_start().map(|handle| handle - first.start), first.handle_end().map(|handle| handle - first.end)], handles: [first.p1.map(|handle| handle - first.p0), first.p2.map(|handle| handle - first.p3)],
}; };
responses.add(GraphOperationMessage::Vector { layer, modification_type }); responses.add(GraphOperationMessage::Vector { layer, modification_type });
@ -280,12 +285,12 @@ impl ClosestSegment {
let modification_type = VectorModificationType::InsertSegment { let modification_type = VectorModificationType::InsertSegment {
id: segment_ids[1], id: segment_ids[1],
points: [midpoint, self.points[1]], points: [midpoint, self.points[1]],
handles: [second.handle_start().map(|handle| handle - second.start), second.handle_end().map(|handle| handle - second.end)], handles: [second.p1.map(|handle| handle - second.p0), second.p2.map(|handle| handle - second.p3)],
}; };
responses.add(GraphOperationMessage::Vector { layer, modification_type }); responses.add(GraphOperationMessage::Vector { layer, modification_type });
// G1 continuous on new handles // G1 continuous on new handles
if self.bezier.handle_end().is_some() { if pathseg_points(self.bezier).p2.is_some() {
let handles = [HandleId::end(segment_ids[0]), HandleId::primary(segment_ids[1])]; let handles = [HandleId::end(segment_ids[0]), HandleId::primary(segment_ids[1])];
let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: true }; let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: true };
responses.add(GraphOperationMessage::Vector { layer, modification_type }); responses.add(GraphOperationMessage::Vector { layer, modification_type });
@ -353,8 +358,8 @@ impl ClosestSegment {
) -> Option<[Option<HandleId>; 2]> { ) -> Option<[Option<HandleId>; 2]> {
let transform = document.metadata().transform_to_viewport_if_feeds(self.layer, &document.network_interface); let transform = document.metadata().transform_to_viewport_if_feeds(self.layer, &document.network_interface);
let start = self.bezier.start; let start = point_to_dvec2(self.bezier.start());
let end = self.bezier.end; let end = point_to_dvec2(self.bezier.end());
// Apply the drag delta to the segment's handles // Apply the drag delta to the segment's handles
let b = self.bezier_point_to_viewport; let b = self.bezier_point_to_viewport;
@ -1686,9 +1691,9 @@ impl ShapeState {
let vector = network_interface.compute_modified_vector(layer)?; let vector = network_interface.compute_modified_vector(layer)?;
for (segment, mut bezier, start, end) in vector.segment_bezier_iter() { for (segment_id, mut segment, start, end) in vector.segment_iter() {
let t = bezier.project(layer_pos); let t = segment.nearest(dvec2_to_point(layer_pos), DEFAULT_ACCURACY).t;
let layerspace = bezier.evaluate(TValue::Parametric(t)); let layerspace = point_to_dvec2(segment.eval(t));
let screenspace = transform.transform_point2(layerspace); let screenspace = transform.transform_point2(layerspace);
let distance_squared = screenspace.distance_squared(position); let distance_squared = screenspace.distance_squared(position);
@ -1697,20 +1702,22 @@ impl ShapeState {
closest_distance_squared = distance_squared; closest_distance_squared = distance_squared;
// Convert to linear if handes are on top of control points // Convert to linear if handes are on top of control points
if let bezier_rs::BezierHandles::Cubic { handle_start, handle_end } = bezier.handles { let PathSegPoints { p0: _, p1, p2, p3: _ } = pathseg_points(segment);
if handle_start.abs_diff_eq(bezier.start(), f64::EPSILON * 100.) && handle_end.abs_diff_eq(bezier.end(), f64::EPSILON * 100.) { if let (Some(p1), Some(p2)) = (p1, p2) {
bezier = Bezier::from_linear_dvec2(bezier.start, bezier.end); let segment_points = pathseg_points(segment);
if p1.abs_diff_eq(segment_points.p0, f64::EPSILON * 100.) && p2.abs_diff_eq(segment_points.p3, f64::EPSILON * 100.) {
segment = PathSeg::Line(Line::new(segment.start(), segment.end()));
} }
} }
let primary_handle = vector.colinear_manipulators.iter().find(|handles| handles.contains(&HandleId::primary(segment))); let primary_handle = vector.colinear_manipulators.iter().find(|handles| handles.contains(&HandleId::primary(segment_id)));
let end_handle = vector.colinear_manipulators.iter().find(|handles| handles.contains(&HandleId::end(segment))); let end_handle = vector.colinear_manipulators.iter().find(|handles| handles.contains(&HandleId::end(segment_id)));
let primary_handle = primary_handle.and_then(|&handles| handles.into_iter().find(|handle| handle.segment != segment)); let primary_handle = primary_handle.and_then(|&handles| handles.into_iter().find(|handle| handle.segment != segment_id));
let end_handle = end_handle.and_then(|&handles| handles.into_iter().find(|handle| handle.segment != segment)); let end_handle = end_handle.and_then(|&handles| handles.into_iter().find(|handle| handle.segment != segment_id));
closest = Some(ClosestSegment { closest = Some(ClosestSegment {
segment, segment: segment_id,
bezier, bezier: segment,
points: [start, end], points: [start, end],
colinear: [primary_handle, end_handle], colinear: [primary_handle, end_handle],
t, t,
@ -2076,21 +2083,24 @@ impl ShapeState {
}; };
// Selection segments // Selection segments
for (id, bezier, _, _) in vector.segment_bezier_iter() { for (id, segment, _, _) in vector.segment_iter() {
if select_segments { if select_segments {
// Select segments if they lie inside the bounding box or lasso polygon // Select segments if they lie inside the bounding box or lasso polygon
let segment_bbox = calculate_bezier_bbox(bezier); let transformed_segment = Affine::new(transform.to_cols_array()) * segment;
let bottom_left = transform.transform_point2(segment_bbox[0]); let segment_bbox = transformed_segment.bounding_box();
let top_right = transform.transform_point2(segment_bbox[1]);
let select = match selection_shape { let select = match selection_shape {
SelectionShape::Box(quad) => { SelectionShape::Box(rect) => {
let enclosed = quad[0].min(quad[1]).cmple(bottom_left).all() && quad[0].max(quad[1]).cmpge(top_right).all(); let enclosed = segment_bbox.contains_rect(rect);
match selection_mode { match selection_mode {
SelectionMode::Enclosed => enclosed, SelectionMode::Enclosed => enclosed,
_ => { _ => {
// Check for intersection with the segment // Check for intersection with the segment
enclosed || is_intersecting(bezier, quad, transform) enclosed
|| rect
.path_segments(DEFAULT_ACCURACY)
.map(|seg| seg.as_line().unwrap())
.any(|line| !transformed_segment.intersect_line(line).is_empty())
} }
} }
} }
@ -2098,7 +2108,7 @@ impl ShapeState {
let polygon = polygon_subpath.as_ref().expect("If `selection_shape` is a polygon then subpath is constructed beforehand."); 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 // 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); let points = pathseg_compute_lookup_table(segment, Some(10), false);
match selection_mode { match selection_mode {
SelectionMode::Enclosed => points.map(|p| transform.transform_point2(p)).all(|p| polygon.contains_point(p)), 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)), _ => points.map(|p| transform.transform_point2(p)).any(|p| polygon.contains_point(p)),
@ -2111,13 +2121,15 @@ impl ShapeState {
} }
} }
let segment_points = pathseg_points(segment);
// Selecting handles // Selecting handles
for (position, id) in [(bezier.handle_start(), ManipulatorPointId::PrimaryHandle(id)), (bezier.handle_end(), ManipulatorPointId::EndHandle(id))] { for (position, id) in [(segment_points.p1, ManipulatorPointId::PrimaryHandle(id)), (segment_points.p2, ManipulatorPointId::EndHandle(id))] {
let Some(position) = position else { continue }; let Some(position) = position else { continue };
let transformed_position = transform.transform_point2(position); let transformed_position = transform.transform_point2(position);
let select = match selection_shape { let select = match selection_shape {
SelectionShape::Box(quad) => quad[0].min(quad[1]).cmple(transformed_position).all() && quad[0].max(quad[1]).cmpge(transformed_position).all(), SelectionShape::Box(rect) => rect.contains(dvec2_to_point(transformed_position)),
SelectionShape::Lasso(_) => polygon_subpath SelectionShape::Lasso(_) => polygon_subpath
.as_ref() .as_ref()
.expect("If `selection_shape` is a polygon then subpath is constructed beforehand.") .expect("If `selection_shape` is a polygon then subpath is constructed beforehand.")
@ -2139,7 +2151,7 @@ impl ShapeState {
let transformed_position = transform.transform_point2(position); let transformed_position = transform.transform_point2(position);
let select = match selection_shape { let select = match selection_shape {
SelectionShape::Box(quad) => quad[0].min(quad[1]).cmple(transformed_position).all() && quad[0].max(quad[1]).cmpge(transformed_position).all(), SelectionShape::Box(rect) => rect.contains(dvec2_to_point(transformed_position)),
SelectionShape::Lasso(_) => polygon_subpath SelectionShape::Lasso(_) => polygon_subpath
.as_ref() .as_ref()
.expect("If `selection_shape` is a polygon then subpath is constructed beforehand.") .expect("If `selection_shape` is a polygon then subpath is constructed beforehand.")

View file

@ -182,7 +182,9 @@ impl Polygon {
let new_dimension = if increase { n + 1 } else { (n - 1).max(3) }; let new_dimension = if increase { n + 1 } else { (n - 1).max(3) };
responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices(new_dimension))); responses.add(ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::Vertices(new_dimension),
});
responses.add(NodeGraphMessage::SetInput { responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 1), input_connector: InputConnector::node(node_id, 1),

View file

@ -12,10 +12,10 @@ use crate::messages::tool::common_functionality::shape_editor::ShapeState;
use crate::messages::tool::common_functionality::transformation_cage::BoundingBoxManager; use crate::messages::tool::common_functionality::transformation_cage::BoundingBoxManager;
use crate::messages::tool::tool_messages::tool_prelude::Key; use crate::messages::tool::tool_messages::tool_prelude::Key;
use crate::messages::tool::utility_types::*; use crate::messages::tool::utility_types::*;
use bezier_rs::Subpath;
use glam::{DAffine2, DMat2, DVec2}; use glam::{DAffine2, DMat2, DVec2};
use graph_craft::document::NodeInput; use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
use graphene_std::subpath::{self, Subpath};
use graphene_std::vector::click_target::ClickTargetType; use graphene_std::vector::click_target::ClickTargetType;
use graphene_std::vector::misc::{ use graphene_std::vector::misc::{
ArcType, {GridType, dvec2_to_point}, ArcType, {GridType, dvec2_to_point},
@ -368,9 +368,9 @@ pub fn arc_outline(layer: Option<LayerNodeIdentifier>, document: &DocumentMessag
start_angle / 360. * std::f64::consts::TAU, start_angle / 360. * std::f64::consts::TAU,
sweep_angle / 360. * std::f64::consts::TAU, sweep_angle / 360. * std::f64::consts::TAU,
match arc_type { match arc_type {
ArcType::Open => bezier_rs::ArcType::Open, ArcType::Open => subpath::ArcType::Open,
ArcType::Closed => bezier_rs::ArcType::Closed, ArcType::Closed => subpath::ArcType::Closed,
ArcType::PieSlice => bezier_rs::ArcType::PieSlice, ArcType::PieSlice => subpath::ArcType::PieSlice,
}, },
))]; ))];
let viewport = document.metadata().transform_to_viewport(layer); let viewport = document.metadata().transform_to_viewport(layer);

View file

@ -10,14 +10,16 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye
use crate::messages::portfolio::document::utility_types::misc::{GridSnapTarget, PathSnapTarget, SnapTarget}; use crate::messages::portfolio::document::utility_types::misc::{GridSnapTarget, PathSnapTarget, SnapTarget};
use crate::messages::prelude::*; use crate::messages::prelude::*;
pub use alignment_snapper::*; pub use alignment_snapper::*;
use bezier_rs::TValue;
pub use distribution_snapper::*; pub use distribution_snapper::*;
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use graphene_std::renderer::Quad; use graphene_std::renderer::Quad;
use graphene_std::renderer::Rect; use graphene_std::renderer::Rect;
use graphene_std::vector::NoHashBuilder; use graphene_std::vector::NoHashBuilder;
use graphene_std::vector::PointId; use graphene_std::vector::PointId;
use graphene_std::vector::algorithms::intersection::filtered_segment_intersections;
use graphene_std::vector::misc::point_to_dvec2;
pub use grid_snapper::*; pub use grid_snapper::*;
use kurbo::ParamCurve;
pub use layer_snapper::*; pub use layer_snapper::*;
pub use snap_results::*; pub use snap_results::*;
use std::cmp::Ordering; use std::cmp::Ordering;
@ -81,6 +83,7 @@ impl SnapConstraint {
} }
} }
} }
pub fn snap_tolerance(document: &DocumentMessageHandler) -> f64 { pub fn snap_tolerance(document: &DocumentMessageHandler) -> f64 {
document.snapping_state.tolerance / document.document_ptz.zoom() document.snapping_state.tolerance / document.document_ptz.zoom()
} }
@ -127,13 +130,16 @@ fn get_closest_point(points: Vec<SnappedPoint>) -> Option<SnappedPoint> {
} }
} }
} }
fn get_closest_curve(curves: &[SnappedCurve], exclude_paths: bool) -> Option<&SnappedPoint> { fn get_closest_curve(curves: &[SnappedCurve], exclude_paths: bool) -> Option<&SnappedPoint> {
let keep_curve = |curve: &&SnappedCurve| !exclude_paths || curve.point.target != SnapTarget::Path(PathSnapTarget::AlongPath); let keep_curve = |curve: &&SnappedCurve| !exclude_paths || curve.point.target != SnapTarget::Path(PathSnapTarget::AlongPath);
curves.iter().filter(keep_curve).map(|curve| &curve.point).min_by(compare_points) curves.iter().filter(keep_curve).map(|curve| &curve.point).min_by(compare_points)
} }
fn get_closest_line(lines: &[SnappedLine]) -> Option<&SnappedPoint> { fn get_closest_line(lines: &[SnappedLine]) -> Option<&SnappedPoint> {
lines.iter().map(|curve| &curve.point).min_by(compare_points) lines.iter().map(|curve| &curve.point).min_by(compare_points)
} }
fn get_closest_intersection(snap_to: DVec2, curves: &[SnappedCurve]) -> Option<SnappedPoint> { fn get_closest_intersection(snap_to: DVec2, curves: &[SnappedCurve]) -> Option<SnappedPoint> {
let mut best = None; let mut best = None;
for curve_i in curves { for curve_i in curves {
@ -141,8 +147,8 @@ fn get_closest_intersection(snap_to: DVec2, curves: &[SnappedCurve]) -> Option<S
if curve_i.start == curve_j.start && curve_i.layer == curve_j.layer { if curve_i.start == curve_j.start && curve_i.layer == curve_j.layer {
continue; continue;
} }
for curve_i_t in curve_i.document_curve.intersections(&curve_j.document_curve, None, None) { for curve_i_t in filtered_segment_intersections(curve_i.document_curve, curve_j.document_curve, None, None) {
let snapped_point_document = curve_i.document_curve.evaluate(TValue::Parametric(curve_i_t)); let snapped_point_document = point_to_dvec2(curve_i.document_curve.eval(curve_i_t));
let distance = snap_to.distance(snapped_point_document); let distance = snap_to.distance(snapped_point_document);
let i_closer = curve_i.point.distance < curve_j.point.distance; let i_closer = curve_i.point.distance < curve_j.point.distance;
let close = if i_closer { curve_i } else { curve_j }; let close = if i_closer { curve_i } else { curve_j };
@ -165,6 +171,7 @@ fn get_closest_intersection(snap_to: DVec2, curves: &[SnappedCurve]) -> Option<S
} }
best best
} }
fn get_grid_intersection(snap_to: DVec2, lines: &[SnappedLine]) -> Option<SnappedPoint> { fn get_grid_intersection(snap_to: DVec2, lines: &[SnappedLine]) -> Option<SnappedPoint> {
let mut best = None; let mut best = None;
for line_i in lines { for line_i in lines {
@ -237,6 +244,7 @@ impl<'a> SnapData<'a> {
self.node_snap_cache.is_some_and(|cache| !cache.manipulators.is_empty()) self.node_snap_cache.is_some_and(|cache| !cache.manipulators.is_empty())
} }
} }
impl SnapManager { impl SnapManager {
pub fn update_indicator(&mut self, snapped_point: SnappedPoint) { pub fn update_indicator(&mut self, snapped_point: SnappedPoint) {
self.indicator = snapped_point.is_snapped().then_some(snapped_point); self.indicator = snapped_point.is_snapped().then_some(snapped_point);

View file

@ -3,11 +3,17 @@ use crate::consts::HIDE_HANDLE_DISTANCE;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::misc::*; use crate::messages::portfolio::document::utility_types::misc::*;
use crate::messages::prelude::*; use crate::messages::prelude::*;
use bezier_rs::{Bezier, Identifier, Subpath, TValue};
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use graphene_std::math::math_ext::QuadExt; use graphene_std::math::math_ext::QuadExt;
use graphene_std::renderer::Quad; use graphene_std::renderer::Quad;
use graphene_std::subpath::pathseg_points;
use graphene_std::subpath::{Identifier, ManipulatorGroup, Subpath};
use graphene_std::vector::PointId; use graphene_std::vector::PointId;
use graphene_std::vector::algorithms::bezpath_algorithms::{pathseg_normals_to_point, pathseg_tangents_to_point};
use graphene_std::vector::algorithms::intersection::filtered_segment_intersections;
use graphene_std::vector::misc::dvec2_to_point;
use graphene_std::vector::misc::point_to_dvec2;
use kurbo::{Affine, DEFAULT_ACCURACY, Nearest, ParamCurve, ParamCurveNearest, PathSeg};
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct LayerSnapper { pub struct LayerSnapper {
@ -37,7 +43,7 @@ impl LayerSnapper {
return; return;
} }
for document_curve in bounds.bezier_lines() { for document_curve in bounds.to_lines() {
self.paths_to_snap.push(SnapCandidatePath { self.paths_to_snap.push(SnapCandidatePath {
document_curve, document_curve,
layer, layer,
@ -70,7 +76,7 @@ impl LayerSnapper {
if document.snapping_state.target_enabled(SnapTarget::Path(PathSnapTarget::IntersectionPoint)) || document.snapping_state.target_enabled(SnapTarget::Path(PathSnapTarget::AlongPath)) { if document.snapping_state.target_enabled(SnapTarget::Path(PathSnapTarget::IntersectionPoint)) || document.snapping_state.target_enabled(SnapTarget::Path(PathSnapTarget::AlongPath)) {
for subpath in document.metadata().layer_outline(layer) { for subpath in document.metadata().layer_outline(layer) {
for (start_index, curve) in subpath.iter().enumerate() { for (start_index, curve) in subpath.iter().enumerate() {
let document_curve = curve.apply_transformation(|p| transform.transform_point2(p)); let document_curve = Affine::new(transform.to_cols_array()) * curve;
let start = subpath.manipulator_groups()[start_index].id; let start = subpath.manipulator_groups()[start_index].id;
if snap_data.ignore_manipulator(layer, start) || snap_data.ignore_manipulator(layer, subpath.manipulator_groups()[(start_index + 1) % subpath.len()].id) { if snap_data.ignore_manipulator(layer, start) || snap_data.ignore_manipulator(layer, subpath.manipulator_groups()[(start_index + 1) % subpath.len()].id) {
continue; continue;
@ -98,13 +104,12 @@ impl LayerSnapper {
for path in &self.paths_to_snap { for path in &self.paths_to_snap {
// Skip very short paths // Skip very short paths
if path.document_curve.start.distance_squared(path.document_curve.end) < tolerance * tolerance * 2. { if path.document_curve.start().distance_squared(path.document_curve.end()) < tolerance * tolerance * 2. {
continue; continue;
} }
let time = path.document_curve.project(point.document_point); let Nearest { distance_sq, t } = path.document_curve.nearest(dvec2_to_point(point.document_point), DEFAULT_ACCURACY);
let snapped_point_document = path.document_curve.evaluate(bezier_rs::TValue::Parametric(time)); let snapped_point_document = point_to_dvec2(path.document_curve.eval(t));
let distance = distance_sq.sqrt();
let distance = snapped_point_document.distance(point.document_point);
if distance < tolerance { if distance < tolerance {
snap_results.curves.push(SnappedCurve { snap_results.curves.push(SnappedCurve {
@ -144,8 +149,8 @@ impl LayerSnapper {
for path in &self.paths_to_snap { for path in &self.paths_to_snap {
for constraint_path in constraint_path.iter() { for constraint_path in constraint_path.iter() {
for time in path.document_curve.intersections(&constraint_path, None, None) { for time in filtered_segment_intersections(path.document_curve, constraint_path, None, None) {
let snapped_point_document = path.document_curve.evaluate(bezier_rs::TValue::Parametric(time)); let snapped_point_document = point_to_dvec2(path.document_curve.eval(time));
let distance = snapped_point_document.distance(point.document_point); let distance = snapped_point_document.distance(point.document_point);
@ -266,8 +271,8 @@ impl LayerSnapper {
fn normals_and_tangents(path: &SnapCandidatePath, normals: bool, tangents: bool, point: &SnapCandidatePoint, tolerance: f64, snap_results: &mut SnapResults) { fn normals_and_tangents(path: &SnapCandidatePath, normals: bool, tangents: bool, point: &SnapCandidatePoint, tolerance: f64, snap_results: &mut SnapResults) {
if normals && path.bounds.is_none() { if normals && path.bounds.is_none() {
for &neighbor in &point.neighbors { for &neighbor in &point.neighbors {
for t in path.document_curve.normals_to_point(neighbor) { for t in pathseg_normals_to_point(path.document_curve, dvec2_to_point(neighbor)) {
let normal_point = path.document_curve.evaluate(TValue::Parametric(t)); let normal_point = point_to_dvec2(path.document_curve.eval(t));
let distance = normal_point.distance(point.document_point); let distance = normal_point.distance(point.document_point);
if distance > tolerance { if distance > tolerance {
continue; continue;
@ -287,8 +292,8 @@ fn normals_and_tangents(path: &SnapCandidatePath, normals: bool, tangents: bool,
} }
if tangents && path.bounds.is_none() { if tangents && path.bounds.is_none() {
for &neighbor in &point.neighbors { for &neighbor in &point.neighbors {
for t in path.document_curve.tangents_to_point(neighbor) { for t in pathseg_tangents_to_point(path.document_curve, dvec2_to_point(neighbor)) {
let tangent_point = path.document_curve.evaluate(TValue::Parametric(t)); let tangent_point = point_to_dvec2(path.document_curve.eval(t));
let distance = tangent_point.distance(point.document_point); let distance = tangent_point.distance(point.document_point);
if distance > tolerance { if distance > tolerance {
continue; continue;
@ -310,7 +315,7 @@ fn normals_and_tangents(path: &SnapCandidatePath, normals: bool, tangents: bool,
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
struct SnapCandidatePath { struct SnapCandidatePath {
document_curve: Bezier, document_curve: PathSeg,
layer: LayerNodeIdentifier, layer: LayerNodeIdentifier,
start: PointId, start: PointId,
target: SnapTarget, target: SnapTarget,
@ -440,12 +445,13 @@ fn subpath_anchor_snap_points(layer: LayerNodeIdentifier, subpath: &Subpath<Poin
if points.len() >= crate::consts::MAX_LAYER_SNAP_POINTS { if points.len() >= crate::consts::MAX_LAYER_SNAP_POINTS {
return; return;
} }
let curve = pathseg_points(curve);
let in_handle = curve.handle_start().map(|handle| handle - curve.start).filter(handle_not_under(to_document)); let in_handle = curve.p1.map(|handle| handle - curve.p0).filter(handle_not_under(to_document));
let out_handle = curve.handle_end().map(|handle| handle - curve.end).filter(handle_not_under(to_document)); let out_handle = curve.p2.map(|handle| handle - curve.p3).filter(handle_not_under(to_document));
if in_handle.is_none() && out_handle.is_none() { if in_handle.is_none() && out_handle.is_none() {
points.push(SnapCandidatePoint::new( points.push(SnapCandidatePoint::new(
to_document.transform_point2(curve.start() * 0.5 + curve.end * 0.5), to_document.transform_point2(curve.p0 * 0.5 + curve.p3 * 0.5),
SnapSource::Path(PathSnapSource::LineMidpoint), SnapSource::Path(PathSnapSource::LineMidpoint),
SnapTarget::Path(PathSnapTarget::LineMidpoint), SnapTarget::Path(PathSnapTarget::LineMidpoint),
Some(layer), Some(layer),
@ -487,7 +493,7 @@ fn subpath_anchor_snap_points(layer: LayerNodeIdentifier, subpath: &Subpath<Poin
} }
} }
pub fn are_manipulator_handles_colinear(manipulators: &bezier_rs::ManipulatorGroup<PointId>, to_document: DAffine2, subpath: &Subpath<PointId>, index: usize) -> bool { pub fn are_manipulator_handles_colinear(manipulators: &ManipulatorGroup<PointId>, to_document: DAffine2, subpath: &Subpath<PointId>, index: usize) -> bool {
let anchor = manipulators.anchor; let anchor = manipulators.anchor;
let handle_in = manipulators.in_handle.map(|handle| anchor - handle).filter(handle_not_under(to_document)); let handle_in = manipulators.in_handle.map(|handle| anchor - handle).filter(handle_not_under(to_document));
let handle_out = manipulators.out_handle.map(|handle| handle - anchor).filter(handle_not_under(to_document)); let handle_out = manipulators.out_handle.map(|handle| handle - anchor).filter(handle_not_under(to_document));

View file

@ -2,11 +2,11 @@ use super::DistributionMatch;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::misc::{DistributionSnapTarget, SnapSource, SnapTarget}; use crate::messages::portfolio::document::utility_types::misc::{DistributionSnapTarget, SnapSource, SnapTarget};
use crate::messages::tool::common_functionality::snapping::SnapCandidatePoint; use crate::messages::tool::common_functionality::snapping::SnapCandidatePoint;
use bezier_rs::Bezier;
use glam::DVec2; use glam::DVec2;
use graphene_std::renderer::Quad; use graphene_std::renderer::Quad;
use graphene_std::renderer::Rect; use graphene_std::renderer::Rect;
use graphene_std::vector::PointId; use graphene_std::vector::PointId;
use kurbo::PathSeg;
use std::collections::VecDeque; use std::collections::VecDeque;
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
@ -120,5 +120,5 @@ pub struct SnappedCurve {
pub layer: LayerNodeIdentifier, pub layer: LayerNodeIdentifier,
pub start: PointId, pub start: PointId,
pub point: SnappedPoint, pub point: SnappedPoint,
pub document_curve: Bezier, pub document_curve: PathSeg,
} }

View file

@ -9,16 +9,17 @@ use crate::messages::tool::common_functionality::graph_modification_utils::{Node
use crate::messages::tool::common_functionality::transformation_cage::SelectedEdges; use crate::messages::tool::common_functionality::transformation_cage::SelectedEdges;
use crate::messages::tool::tool_messages::path_tool::PathOverlayMode; use crate::messages::tool::tool_messages::path_tool::PathOverlayMode;
use crate::messages::tool::utility_types::ToolType; use crate::messages::tool::utility_types::ToolType;
use bezier_rs::{Bezier, BezierHandles};
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use graph_craft::concrete; use graph_craft::concrete;
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
use graphene_std::renderer::Quad; use graphene_std::renderer::Quad;
use graphene_std::subpath::{Bezier, BezierHandles};
use graphene_std::table::Table; use graphene_std::table::Table;
use graphene_std::text::{FontCache, load_font}; use graphene_std::text::{FontCache, load_font};
use graphene_std::vector::misc::{HandleId, ManipulatorPointId}; use graphene_std::vector::algorithms::bezpath_algorithms::pathseg_compute_lookup_table;
use graphene_std::vector::misc::{HandleId, ManipulatorPointId, dvec2_to_point};
use graphene_std::vector::{HandleExt, PointId, SegmentId, Vector, VectorModification, VectorModificationType}; use graphene_std::vector::{HandleExt, PointId, SegmentId, Vector, VectorModification, VectorModificationType};
use kurbo::{CubicBez, Line, ParamCurveExtrema, PathSeg, Point, QuadBez}; use kurbo::{CubicBez, DEFAULT_ACCURACY, Line, ParamCurve, PathSeg, Point, QuadBez, Shape};
/// Determines if a path should be extended. Goal in viewport space. Returns the path and if it is extending from the start, if applicable. /// Determines if a path should be extended. Goal in viewport space. Returns the path and if it is extending from the start, if applicable.
pub fn should_extend( pub fn should_extend(
@ -208,25 +209,6 @@ pub fn is_visible_point(
} }
} }
/// Function to find the bounding box of bezier (uses method from kurbo)
pub fn calculate_bezier_bbox(bezier: Bezier) -> [DVec2; 2] {
let start = Point::new(bezier.start.x, bezier.start.y);
let end = Point::new(bezier.end.x, bezier.end.y);
let bbox = match bezier.handles {
BezierHandles::Cubic { handle_start, handle_end } => {
let p1 = Point::new(handle_start.x, handle_start.y);
let p2 = Point::new(handle_end.x, handle_end.y);
CubicBez::new(start, p1, p2, end).bounding_box()
}
BezierHandles::Quadratic { handle } => {
let p1 = Point::new(handle.x, handle.y);
QuadBez::new(start, p1, end).bounding_box()
}
BezierHandles::Linear => Line::new(start, end).bounding_box(),
};
[DVec2::new(bbox.x0, bbox.y0), DVec2::new(bbox.x1, bbox.y1)]
}
pub fn is_intersecting(bezier: Bezier, quad: [DVec2; 2], transform: DAffine2) -> bool { pub fn is_intersecting(bezier: Bezier, quad: [DVec2; 2], transform: DAffine2) -> bool {
let to_layerspace = transform.inverse(); let to_layerspace = transform.inverse();
let quad = [to_layerspace.transform_point2(quad[0]), to_layerspace.transform_point2(quad[1])]; let quad = [to_layerspace.transform_point2(quad[0]), to_layerspace.transform_point2(quad[1])];
@ -496,19 +478,19 @@ pub fn log_optimization(a: f64, b: f64, p1: DVec2, p3: DVec2, d1: DVec2, d2: DVe
let c1 = p1 + d1 * start_handle_length; let c1 = p1 + d1 * start_handle_length;
let c2 = p3 + d2 * end_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); let new_curve = PathSeg::Cubic(CubicBez::new(Point::new(p1.x, p1.y), Point::new(c1.x, c1.y), Point::new(c2.x, c2.y), Point::new(p3.x, p3.y)));
// Sample 2*n points from new curve and get the L2 metric between all of points // 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 points = pathseg_compute_lookup_table(new_curve, Some(2 * n), false);
let dist = points1.iter().zip(points.iter()).map(|(p1, p2)| (p1.x - p2.x).powi(2) + (p1.y - p2.y).powi(2)).sum::<f64>(); let dist = points1.iter().zip(points).map(|(p1, p2)| (p1.x - p2.x).powi(2) + (p1.y - p2.y).powi(2)).sum::<f64>();
dist / (2 * n) as f64 dist / (2 * n) as f64
} }
/// Calculates optimal handle lengths with adam optimization. /// Calculates optimal handle lengths with adam optimization.
#[allow(clippy::too_many_arguments)] #[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) { pub fn find_two_param_best_approximate(p1: DVec2, p3: DVec2, d1: DVec2, d2: DVec2, min_len1: f64, min_len2: f64, further_segment: PathSeg, other_segment: PathSeg) -> (DVec2, DVec2) {
let h = 1e-6; let h = 1e-6;
let tol = 1e-6; let tol = 1e-6;
let max_iter = 200; let max_iter = 200;
@ -530,21 +512,25 @@ pub fn find_two_param_best_approximate(p1: DVec2, p3: DVec2, d1: DVec2, d2: DVec
let n = 20; let n = 20;
let farther_segment = if farther_segment.start.distance(p1) >= f64::EPSILON { let further_segment = if further_segment.start().distance(dvec2_to_point(p1)) >= f64::EPSILON {
farther_segment.reverse() further_segment.reverse()
} else { } else {
farther_segment further_segment
}; };
let other_segment = if other_segment.end.distance(p3) >= f64::EPSILON { other_segment.reverse() } else { other_segment }; let other_segment = if other_segment.end().distance(dvec2_to_point(p3)) >= f64::EPSILON {
other_segment.reverse()
} else {
other_segment
};
// Now we sample points proportional to the lengths of the beziers // Now we sample points proportional to the lengths of the beziers
let l1 = farther_segment.length(None); let l1 = further_segment.perimeter(DEFAULT_ACCURACY);
let l2 = other_segment.length(None); let l2 = other_segment.perimeter(DEFAULT_ACCURACY);
let ratio = l1 / (l1 + l2); let ratio = l1 / (l1 + l2);
let n_points1 = ((2 * n) as f64 * ratio).floor() as usize; 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 points1 = pathseg_compute_lookup_table(further_segment, Some(n_points1), false).collect::<Vec<_>>();
let mut points2 = other_segment.compute_lookup_table(Some(n), None).collect::<Vec<_>>(); let mut points2 = pathseg_compute_lookup_table(other_segment, Some(n), false).collect::<Vec<_>>();
points1.append(&mut points2); points1.append(&mut points2);
let f = |a: f64, b: f64| -> f64 { log_optimization(a, b, p1, p3, d1, d2, &points1, n) }; let f = |a: f64, b: f64| -> f64 { log_optimization(a, b, p1, p3, d1, d2, &points1, n) };

View file

@ -11,7 +11,7 @@ use crate::messages::tool::utility_types::ToolType;
use crate::node_graph_executor::NodeGraphExecutor; use crate::node_graph_executor::NodeGraphExecutor;
use graphene_std::raster::color::Color; use graphene_std::raster::color::Color;
const ARTBOARD_OVERLAY_PROVIDER: OverlayProvider = |overlay_context| DocumentMessage::DrawArtboardOverlays(overlay_context).into(); const ARTBOARD_OVERLAY_PROVIDER: OverlayProvider = |context| DocumentMessage::DrawArtboardOverlays { context }.into();
#[derive(ExtractField)] #[derive(ExtractField)]
pub struct ToolMessageContext<'a> { pub struct ToolMessageContext<'a> {
@ -75,8 +75,8 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
self.tool_state.tool_data.active_tool_type = ToolType::Shape; self.tool_state.tool_data.active_tool_type = ToolType::Shape;
} }
responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Shape }); responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Shape });
responses.add(ShapeToolMessage::SetShape(ShapeType::Polygon)); responses.add(ShapeToolMessage::SetShape { shape: ShapeType::Polygon });
responses.add(ShapeToolMessage::HideShapeTypeWidget(false)) responses.add(ShapeToolMessage::HideShapeTypeWidget { hide: false })
} }
ToolMessage::ActivateToolBrush => responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Brush }), ToolMessage::ActivateToolBrush => responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Brush }),
ToolMessage::ActivateToolShapeLine | ToolMessage::ActivateToolShapeRectangle | ToolMessage::ActivateToolShapeEllipse => { ToolMessage::ActivateToolShapeLine | ToolMessage::ActivateToolShapeRectangle | ToolMessage::ActivateToolShapeEllipse => {
@ -89,8 +89,8 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
self.tool_state.tool_data.active_shape_type = Some(shape.tool_type()); self.tool_state.tool_data.active_shape_type = Some(shape.tool_type());
responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Shape }); responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Shape });
responses.add(ShapeToolMessage::HideShapeTypeWidget(true)); responses.add(ShapeToolMessage::HideShapeTypeWidget { hide: true });
responses.add(ShapeToolMessage::SetShape(shape)); responses.add(ShapeToolMessage::SetShape { shape });
} }
ToolMessage::ActivateTool { tool_type } => { ToolMessage::ActivateTool { tool_type } => {
let tool_data = &mut self.tool_state.tool_data; let tool_data = &mut self.tool_state.tool_data;
@ -156,11 +156,14 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
// Subscribe new tool // Subscribe new tool
tool_data.tools.get(&tool_type).unwrap().activate(responses); tool_data.tools.get(&tool_type).unwrap().activate(responses);
// Re-add the artboard overlay provider when tools are reactivated
responses.add(OverlaysMessage::AddProvider { provider: ARTBOARD_OVERLAY_PROVIDER });
// Send the SelectionChanged message to the active tool, this will ensure the selection is updated // Send the SelectionChanged message to the active tool, this will ensure the selection is updated
responses.add(BroadcastEvent::SelectionChanged); responses.add(EventMessage::SelectionChanged);
// Update the working colors for the active tool // Update the working colors for the active tool
responses.add(BroadcastEvent::WorkingColorChanged); responses.add(EventMessage::WorkingColorChanged);
// Send tool options to the frontend // Send tool options to the frontend
responses.add(ToolMessage::RefreshToolOptions); responses.add(ToolMessage::RefreshToolOptions);
@ -173,11 +176,12 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
tool_data.tools.get(&tool_data.active_tool_type).unwrap().deactivate(responses); tool_data.tools.get(&tool_data.active_tool_type).unwrap().deactivate(responses);
// Unsubscribe the transform layer to selection change events // Unsubscribe the transform layer to selection change events
let message = Box::new(TransformLayerMessage::SelectionChanged.into()); responses.add(BroadcastMessage::UnsubscribeEvent {
let on = BroadcastEvent::SelectionChanged; on: EventMessage::SelectionChanged,
responses.add(BroadcastMessage::UnsubscribeEvent { message, on }); send: Box::new(TransformLayerMessage::SelectionChanged.into()),
});
responses.add(OverlaysMessage::RemoveProvider(ARTBOARD_OVERLAY_PROVIDER)); responses.add(OverlaysMessage::RemoveProvider { provider: ARTBOARD_OVERLAY_PROVIDER });
responses.add(FrontendMessage::UpdateInputHints { hint_data: Default::default() }); responses.add(FrontendMessage::UpdateInputHints { hint_data: Default::default() });
responses.add(FrontendMessage::UpdateMouseCursor { cursor: Default::default() }); responses.add(FrontendMessage::UpdateMouseCursor { cursor: Default::default() });
@ -187,12 +191,12 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
ToolMessage::InitTools => { ToolMessage::InitTools => {
// Subscribe the transform layer to selection change events // Subscribe the transform layer to selection change events
responses.add(BroadcastMessage::SubscribeEvent { responses.add(BroadcastMessage::SubscribeEvent {
on: BroadcastEvent::SelectionChanged, on: EventMessage::SelectionChanged,
send: Box::new(TransformLayerMessage::SelectionChanged.into()), send: Box::new(TransformLayerMessage::SelectionChanged.into()),
}); });
responses.add(BroadcastMessage::SubscribeEvent { responses.add(BroadcastMessage::SubscribeEvent {
on: BroadcastEvent::SelectionChanged, on: EventMessage::SelectionChanged,
send: Box::new(SelectToolMessage::SyncHistory.into()), send: Box::new(SelectToolMessage::SyncHistory.into()),
}); });
@ -229,12 +233,12 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
tool_data.active_tool_mut().process_message(ToolMessage::UpdateHints, responses, &mut data); tool_data.active_tool_mut().process_message(ToolMessage::UpdateHints, responses, &mut data);
tool_data.active_tool_mut().process_message(ToolMessage::UpdateCursor, responses, &mut data); tool_data.active_tool_mut().process_message(ToolMessage::UpdateCursor, responses, &mut data);
responses.add(OverlaysMessage::AddProvider(ARTBOARD_OVERLAY_PROVIDER)); responses.add(OverlaysMessage::AddProvider { provider: ARTBOARD_OVERLAY_PROVIDER });
} }
ToolMessage::PreUndo => { ToolMessage::PreUndo => {
let tool_data = &mut self.tool_state.tool_data; let tool_data = &mut self.tool_state.tool_data;
if tool_data.active_tool_type != ToolType::Pen { if tool_data.active_tool_type != ToolType::Pen {
responses.add(BroadcastEvent::ToolAbort); responses.add(EventMessage::ToolAbort);
} }
} }
ToolMessage::Redo => { ToolMessage::Redo => {

View file

@ -26,7 +26,7 @@ pub struct ArtboardTool {
pub enum ArtboardToolMessage { pub enum ArtboardToolMessage {
// Standard messages // Standard messages
Abort, Abort,
Overlays(OverlayContext), Overlays { context: OverlayContext },
// Tool-specific messages // Tool-specific messages
UpdateSelectedArtboard, UpdateSelectedArtboard,
@ -83,7 +83,7 @@ impl ToolTransition for ArtboardTool {
fn event_to_message_map(&self) -> EventToMessageMap { fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap { EventToMessageMap {
tool_abort: Some(ArtboardToolMessage::Abort.into()), tool_abort: Some(ArtboardToolMessage::Abort.into()),
overlay_provider: Some(|overlay_context| ArtboardToolMessage::Overlays(overlay_context).into()), overlay_provider: Some(|context| ArtboardToolMessage::Overlays { context }.into()),
..Default::default() ..Default::default()
} }
} }
@ -227,7 +227,7 @@ impl Fsm for ArtboardToolFsmState {
let ToolMessage::Artboard(event) = event else { return self }; let ToolMessage::Artboard(event) = event else { return self };
match (self, event) { match (self, event) {
(state, ArtboardToolMessage::Overlays(mut overlay_context)) => { (state, ArtboardToolMessage::Overlays { context: mut overlay_context }) => {
let display_transform_cage = overlay_context.visibility_settings.transform_cage(); let display_transform_cage = overlay_context.visibility_settings.transform_cage();
if display_transform_cage && state != ArtboardToolFsmState::Drawing { if display_transform_cage && state != ArtboardToolFsmState::Drawing {
if let Some(bounds) = tool_data.selected_artboard.and_then(|layer| document.metadata().bounding_box_document(layer)) { if let Some(bounds) = tool_data.selected_artboard.and_then(|layer| document.metadata().bounding_box_document(layer)) {

View file

@ -62,7 +62,7 @@ pub enum BrushToolMessage {
DragStart, DragStart,
DragStop, DragStop,
PointerMove, PointerMove,
UpdateOptions(BrushToolMessageOptionsUpdate), UpdateOptions { options: BrushToolMessageOptionsUpdate },
} }
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
@ -106,7 +106,7 @@ impl LayoutHolder for BrushTool {
.min(1.) .min(1.)
.max(BRUSH_MAX_SIZE) /* Anything bigger would cause the application to be unresponsive and eventually die */ .max(BRUSH_MAX_SIZE) /* Anything bigger would cause the application to be unresponsive and eventually die */
.unit(" px") .unit(" px")
.on_update(|number_input: &NumberInput| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Diameter(number_input.value.unwrap())).into()) .on_update(|number_input: &NumberInput| BrushToolMessage::UpdateOptions { options: BrushToolMessageOptionsUpdate::Diameter(number_input.value.unwrap()) }.into())
.widget_holder(), .widget_holder(),
Separator::new(SeparatorType::Related).widget_holder(), Separator::new(SeparatorType::Related).widget_holder(),
NumberInput::new(Some(self.options.hardness)) NumberInput::new(Some(self.options.hardness))
@ -115,7 +115,12 @@ impl LayoutHolder for BrushTool {
.max(100.) .max(100.)
.mode_range() .mode_range()
.unit("%") .unit("%")
.on_update(|number_input: &NumberInput| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Hardness(number_input.value.unwrap())).into()) .on_update(|number_input: &NumberInput| {
BrushToolMessage::UpdateOptions {
options: BrushToolMessageOptionsUpdate::Hardness(number_input.value.unwrap()),
}
.into()
})
.widget_holder(), .widget_holder(),
Separator::new(SeparatorType::Related).widget_holder(), Separator::new(SeparatorType::Related).widget_holder(),
NumberInput::new(Some(self.options.flow)) NumberInput::new(Some(self.options.flow))
@ -124,7 +129,12 @@ impl LayoutHolder for BrushTool {
.max(100.) .max(100.)
.mode_range() .mode_range()
.unit("%") .unit("%")
.on_update(|number_input: &NumberInput| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Flow(number_input.value.unwrap())).into()) .on_update(|number_input: &NumberInput| {
BrushToolMessage::UpdateOptions {
options: BrushToolMessageOptionsUpdate::Flow(number_input.value.unwrap()),
}
.into()
})
.widget_holder(), .widget_holder(),
Separator::new(SeparatorType::Related).widget_holder(), Separator::new(SeparatorType::Related).widget_holder(),
NumberInput::new(Some(self.options.spacing)) NumberInput::new(Some(self.options.spacing))
@ -133,7 +143,12 @@ impl LayoutHolder for BrushTool {
.max(100.) .max(100.)
.mode_range() .mode_range()
.unit("%") .unit("%")
.on_update(|number_input: &NumberInput| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Spacing(number_input.value.unwrap())).into()) .on_update(|number_input: &NumberInput| {
BrushToolMessage::UpdateOptions {
options: BrushToolMessageOptionsUpdate::Spacing(number_input.value.unwrap()),
}
.into()
})
.widget_holder(), .widget_holder(),
]; ];
@ -142,9 +157,12 @@ impl LayoutHolder for BrushTool {
let draw_mode_entries: Vec<_> = [DrawMode::Draw, DrawMode::Erase, DrawMode::Restore] let draw_mode_entries: Vec<_> = [DrawMode::Draw, DrawMode::Erase, DrawMode::Restore]
.into_iter() .into_iter()
.map(|draw_mode| { .map(|draw_mode| {
RadioEntryData::new(format!("{draw_mode:?}")) RadioEntryData::new(format!("{draw_mode:?}")).label(format!("{draw_mode:?}")).on_update(move |_| {
.label(format!("{draw_mode:?}")) BrushToolMessage::UpdateOptions {
.on_update(move |_| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::DrawMode(draw_mode)).into()) options: BrushToolMessageOptionsUpdate::DrawMode(draw_mode),
}
.into()
})
}) })
.collect(); .collect();
widgets.push(RadioInput::new(draw_mode_entries).selected_index(Some(self.options.draw_mode as u32)).widget_holder()); widgets.push(RadioInput::new(draw_mode_entries).selected_index(Some(self.options.draw_mode as u32)).widget_holder());
@ -154,9 +172,26 @@ impl LayoutHolder for BrushTool {
widgets.append(&mut self.options.color.create_widgets( widgets.append(&mut self.options.color.create_widgets(
"Color", "Color",
false, false,
|_| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Color(None)).into(), |_| {
|color_type: ToolColorType| WidgetCallback::new(move |_| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::ColorType(color_type.clone())).into()), BrushToolMessage::UpdateOptions {
|color: &ColorInput| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Color(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(), options: BrushToolMessageOptionsUpdate::Color(None),
}
.into()
},
|color_type: ToolColorType| {
WidgetCallback::new(move |_| {
BrushToolMessage::UpdateOptions {
options: BrushToolMessageOptionsUpdate::ColorType(color_type.clone()),
}
.into()
})
},
|color: &ColorInput| {
BrushToolMessage::UpdateOptions {
options: BrushToolMessageOptionsUpdate::Color(color.value.as_solid().map(|color| color.to_linear_srgb())),
}
.into()
},
)); ));
widgets.push(Separator::new(SeparatorType::Related).widget_holder()); widgets.push(Separator::new(SeparatorType::Related).widget_holder());
@ -167,9 +202,12 @@ impl LayoutHolder for BrushTool {
section section
.iter() .iter()
.map(|blend_mode| { .map(|blend_mode| {
MenuListEntry::new(format!("{blend_mode:?}")) MenuListEntry::new(format!("{blend_mode:?}")).label(blend_mode.to_string()).on_commit(|_| {
.label(blend_mode.to_string()) BrushToolMessage::UpdateOptions {
.on_commit(|_| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::BlendMode(*blend_mode)).into()) options: BrushToolMessageOptionsUpdate::BlendMode(*blend_mode),
}
.into()
})
}) })
.collect() .collect()
}) })
@ -189,11 +227,11 @@ impl LayoutHolder for BrushTool {
#[message_handler_data] #[message_handler_data]
impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for BrushTool { impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for BrushTool {
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) { fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) {
let ToolMessage::Brush(BrushToolMessage::UpdateOptions(action)) = message else { let ToolMessage::Brush(BrushToolMessage::UpdateOptions { options }) = message else {
self.fsm_state.process_event(message, &mut self.data, context, &self.options, responses, true); self.fsm_state.process_event(message, &mut self.data, context, &self.options, responses, true);
return; return;
}; };
match action { match options {
BrushToolMessageOptionsUpdate::BlendMode(blend_mode) => self.options.blend_mode = blend_mode, BrushToolMessageOptionsUpdate::BlendMode(blend_mode) => self.options.blend_mode = blend_mode,
BrushToolMessageOptionsUpdate::ChangeDiameter(change) => { BrushToolMessageOptionsUpdate::ChangeDiameter(change) => {
let needs_rounding = ((self.options.diameter + change.abs() / 2.) % change.abs() - change.abs() / 2.).abs() > 0.5; let needs_rounding = ((self.options.diameter + change.abs() / 2.) % change.abs() - change.abs() / 2.).abs() > 0.5;
@ -408,10 +446,9 @@ impl Fsm for BrushToolFsmState {
BrushToolFsmState::Ready BrushToolFsmState::Ready
} }
(_, BrushToolMessage::WorkingColorChanged) => { (_, BrushToolMessage::WorkingColorChanged) => {
responses.add(BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::WorkingColors( responses.add(BrushToolMessage::UpdateOptions {
Some(global_tool_data.primary_color), options: BrushToolMessageOptionsUpdate::WorkingColors(Some(global_tool_data.primary_color), Some(global_tool_data.secondary_color)),
Some(global_tool_data.secondary_color), });
)));
self self
} }
_ => self, _ => self,

View file

@ -14,7 +14,7 @@ pub enum FillToolMessage {
// Standard messages // Standard messages
Abort, Abort,
WorkingColorChanged, WorkingColorChanged,
Overlays(OverlayContext), Overlays { context: OverlayContext },
// Tool-specific messages // Tool-specific messages
PointerMove, PointerMove,
@ -67,7 +67,7 @@ impl ToolTransition for FillTool {
EventToMessageMap { EventToMessageMap {
tool_abort: Some(FillToolMessage::Abort.into()), tool_abort: Some(FillToolMessage::Abort.into()),
working_color_changed: Some(FillToolMessage::WorkingColorChanged.into()), working_color_changed: Some(FillToolMessage::WorkingColorChanged.into()),
overlay_provider: Some(|overlay_context| FillToolMessage::Overlays(overlay_context).into()), overlay_provider: Some(|context| FillToolMessage::Overlays { context }.into()),
..Default::default() ..Default::default()
} }
} }
@ -99,7 +99,7 @@ impl Fsm for FillToolFsmState {
let ToolMessage::Fill(event) = event else { return self }; let ToolMessage::Fill(event) = event else { return self };
match (self, event) { match (self, event) {
(_, FillToolMessage::Overlays(mut overlay_context)) => { (_, FillToolMessage::Overlays { context: mut overlay_context }) => {
// Choose the working color to preview // Choose the working color to preview
let use_secondary = input.keyboard.get(Key::Shift as usize); let use_secondary = input.keyboard.get(Key::Shift as usize);
let preview_color = if use_secondary { global_tool_data.secondary_color } else { global_tool_data.primary_color }; let preview_color = if use_secondary { global_tool_data.secondary_color } else { global_tool_data.primary_color };

View file

@ -40,7 +40,7 @@ impl Default for FreehandOptions {
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum FreehandToolMessage { pub enum FreehandToolMessage {
// Standard messages // Standard messages
Overlays(OverlayContext), Overlays { context: OverlayContext },
Abort, Abort,
WorkingColorChanged, WorkingColorChanged,
@ -48,7 +48,7 @@ pub enum FreehandToolMessage {
DragStart { append_to_selected: Key }, DragStart { append_to_selected: Key },
DragStop, DragStop,
PointerMove, PointerMove,
UpdateOptions(FreehandOptionsUpdate), UpdateOptions { options: FreehandOptionsUpdate },
} }
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
@ -86,7 +86,12 @@ fn create_weight_widget(line_weight: f64) -> WidgetHolder {
.label("Weight") .label("Weight")
.min(1.) .min(1.)
.max((1_u64 << f64::MANTISSA_DIGITS) as f64) .max((1_u64 << f64::MANTISSA_DIGITS) as f64)
.on_update(|number_input: &NumberInput| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::LineWeight(number_input.value.unwrap())).into()) .on_update(|number_input: &NumberInput| {
FreehandToolMessage::UpdateOptions {
options: FreehandOptionsUpdate::LineWeight(number_input.value.unwrap()),
}
.into()
})
.widget_holder() .widget_holder()
} }
@ -95,9 +100,26 @@ impl LayoutHolder for FreehandTool {
let mut widgets = self.options.fill.create_widgets( let mut widgets = self.options.fill.create_widgets(
"Fill", "Fill",
true, true,
|_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::FillColor(None)).into(), |_| {
|color_type: ToolColorType| WidgetCallback::new(move |_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::FillColorType(color_type.clone())).into()), FreehandToolMessage::UpdateOptions {
|color: &ColorInput| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(), options: FreehandOptionsUpdate::FillColor(None),
}
.into()
},
|color_type: ToolColorType| {
WidgetCallback::new(move |_| {
FreehandToolMessage::UpdateOptions {
options: FreehandOptionsUpdate::FillColorType(color_type.clone()),
}
.into()
})
},
|color: &ColorInput| {
FreehandToolMessage::UpdateOptions {
options: FreehandOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb())),
}
.into()
},
); );
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
@ -105,9 +127,26 @@ impl LayoutHolder for FreehandTool {
widgets.append(&mut self.options.stroke.create_widgets( widgets.append(&mut self.options.stroke.create_widgets(
"Stroke", "Stroke",
true, true,
|_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::StrokeColor(None)).into(), |_| {
|color_type: ToolColorType| WidgetCallback::new(move |_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::StrokeColorType(color_type.clone())).into()), FreehandToolMessage::UpdateOptions {
|color: &ColorInput| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(), options: FreehandOptionsUpdate::StrokeColor(None),
}
.into()
},
|color_type: ToolColorType| {
WidgetCallback::new(move |_| {
FreehandToolMessage::UpdateOptions {
options: FreehandOptionsUpdate::StrokeColorType(color_type.clone()),
}
.into()
})
},
|color: &ColorInput| {
FreehandToolMessage::UpdateOptions {
options: FreehandOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb())),
}
.into()
},
)); ));
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
widgets.push(create_weight_widget(self.options.line_weight)); widgets.push(create_weight_widget(self.options.line_weight));
@ -119,11 +158,11 @@ impl LayoutHolder for FreehandTool {
#[message_handler_data] #[message_handler_data]
impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for FreehandTool { impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for FreehandTool {
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) { fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) {
let ToolMessage::Freehand(FreehandToolMessage::UpdateOptions(action)) = message else { let ToolMessage::Freehand(FreehandToolMessage::UpdateOptions { options }) = message else {
self.fsm_state.process_event(message, &mut self.data, context, &self.options, responses, true); self.fsm_state.process_event(message, &mut self.data, context, &self.options, responses, true);
return; return;
}; };
match action { match options {
FreehandOptionsUpdate::FillColor(color) => { FreehandOptionsUpdate::FillColor(color) => {
self.options.fill.custom_color = color; self.options.fill.custom_color = color;
self.options.fill.color_type = ToolColorType::Custom; self.options.fill.color_type = ToolColorType::Custom;
@ -164,7 +203,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Free
impl ToolTransition for FreehandTool { impl ToolTransition for FreehandTool {
fn event_to_message_map(&self) -> EventToMessageMap { fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap { EventToMessageMap {
overlay_provider: Some(|overlay_context: OverlayContext| FreehandToolMessage::Overlays(overlay_context).into()), overlay_provider: Some(|context: OverlayContext| FreehandToolMessage::Overlays { context }.into()),
tool_abort: Some(FreehandToolMessage::Abort.into()), tool_abort: Some(FreehandToolMessage::Abort.into()),
working_color_changed: Some(FreehandToolMessage::WorkingColorChanged.into()), working_color_changed: Some(FreehandToolMessage::WorkingColorChanged.into()),
..Default::default() ..Default::default()
@ -203,7 +242,7 @@ impl Fsm for FreehandToolFsmState {
let ToolMessage::Freehand(event) = event else { return self }; let ToolMessage::Freehand(event) = event else { return self };
match (self, event) { match (self, event) {
(_, FreehandToolMessage::Overlays(mut overlay_context)) => { (_, FreehandToolMessage::Overlays { context: mut overlay_context }) => {
path_endpoint_overlays(document, shape_editor, &mut overlay_context, tool_action_data.preferences); path_endpoint_overlays(document, shape_editor, &mut overlay_context, tool_action_data.preferences);
self self
@ -287,10 +326,9 @@ impl Fsm for FreehandToolFsmState {
FreehandToolFsmState::Ready FreehandToolFsmState::Ready
} }
(_, FreehandToolMessage::WorkingColorChanged) => { (_, FreehandToolMessage::WorkingColorChanged) => {
responses.add(FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::WorkingColors( responses.add(FreehandToolMessage::UpdateOptions {
Some(global_tool_data.primary_color), options: FreehandOptionsUpdate::WorkingColors(Some(global_tool_data.primary_color), Some(global_tool_data.secondary_color)),
Some(global_tool_data.secondary_color), });
)));
self self
} }
_ => self, _ => self,
@ -679,7 +717,9 @@ mod test_freehand {
let custom_line_weight = 5.; let custom_line_weight = 5.;
editor editor
.handle_message(ToolMessage::Freehand(FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::LineWeight(custom_line_weight)))) .handle_message(ToolMessage::Freehand(FreehandToolMessage::UpdateOptions {
options: FreehandOptionsUpdate::LineWeight(custom_line_weight),
}))
.await; .await;
let points = [DVec2::new(100., 100.), DVec2::new(200., 200.), DVec2::new(300., 100.)]; let points = [DVec2::new(100., 100.), DVec2::new(200., 200.), DVec2::new(300., 100.)];

View file

@ -24,7 +24,7 @@ pub struct GradientOptions {
pub enum GradientToolMessage { pub enum GradientToolMessage {
// Standard messages // Standard messages
Abort, Abort,
Overlays(OverlayContext), Overlays { context: OverlayContext },
// Tool-specific messages // Tool-specific messages
DeleteStop, DeleteStop,
@ -33,7 +33,7 @@ pub enum GradientToolMessage {
PointerMove { constrain_axis: Key }, PointerMove { constrain_axis: Key },
PointerOutsideViewport { constrain_axis: Key }, PointerOutsideViewport { constrain_axis: Key },
PointerUp, PointerUp,
UpdateOptions(GradientOptionsUpdate), UpdateOptions { options: GradientOptionsUpdate },
} }
#[derive(PartialEq, Eq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)] #[derive(PartialEq, Eq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)]
@ -56,11 +56,11 @@ impl ToolMetadata for GradientTool {
#[message_handler_data] #[message_handler_data]
impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for GradientTool { impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for GradientTool {
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) { fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) {
let ToolMessage::Gradient(GradientToolMessage::UpdateOptions(action)) = message else { let ToolMessage::Gradient(GradientToolMessage::UpdateOptions { options }) = message else {
self.fsm_state.process_event(message, &mut self.data, context, &self.options, responses, false); self.fsm_state.process_event(message, &mut self.data, context, &self.options, responses, false);
return; return;
}; };
match action { match options {
GradientOptionsUpdate::Type(gradient_type) => { GradientOptionsUpdate::Type(gradient_type) => {
self.options.gradient_type = gradient_type; self.options.gradient_type = gradient_type;
// Update the selected gradient if it exists // Update the selected gradient if it exists
@ -91,14 +91,18 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Grad
impl LayoutHolder for GradientTool { impl LayoutHolder for GradientTool {
fn layout(&self) -> Layout { fn layout(&self) -> Layout {
let gradient_type = RadioInput::new(vec![ let gradient_type = RadioInput::new(vec![
RadioEntryData::new("Linear") RadioEntryData::new("Linear").label("Linear").tooltip("Linear gradient").on_update(move |_| {
.label("Linear") GradientToolMessage::UpdateOptions {
.tooltip("Linear gradient") options: GradientOptionsUpdate::Type(GradientType::Linear),
.on_update(move |_| GradientToolMessage::UpdateOptions(GradientOptionsUpdate::Type(GradientType::Linear)).into()), }
RadioEntryData::new("Radial") .into()
.label("Radial") }),
.tooltip("Radial gradient") RadioEntryData::new("Radial").label("Radial").tooltip("Radial gradient").on_update(move |_| {
.on_update(move |_| GradientToolMessage::UpdateOptions(GradientOptionsUpdate::Type(GradientType::Radial)).into()), GradientToolMessage::UpdateOptions {
options: GradientOptionsUpdate::Type(GradientType::Radial),
}
.into()
}),
]) ])
.selected_index(Some((self.selected_gradient().unwrap_or(self.options.gradient_type) == GradientType::Radial) as u32)) .selected_index(Some((self.selected_gradient().unwrap_or(self.options.gradient_type) == GradientType::Radial) as u32))
.widget_holder(); .widget_holder();
@ -204,7 +208,6 @@ impl SelectedGradient {
/// Update the layer fill to the current gradient /// Update the layer fill to the current gradient
pub fn render_gradient(&mut self, responses: &mut VecDeque<Message>) { pub fn render_gradient(&mut self, responses: &mut VecDeque<Message>) {
self.gradient.transform = self.transform;
if let Some(layer) = self.layer { if let Some(layer) = self.layer {
responses.add(GraphOperationMessage::FillSet { responses.add(GraphOperationMessage::FillSet {
layer, layer,
@ -225,7 +228,7 @@ impl ToolTransition for GradientTool {
fn event_to_message_map(&self) -> EventToMessageMap { fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap { EventToMessageMap {
tool_abort: Some(GradientToolMessage::Abort.into()), tool_abort: Some(GradientToolMessage::Abort.into()),
overlay_provider: Some(|overlay_context| GradientToolMessage::Overlays(overlay_context).into()), overlay_provider: Some(|context| GradientToolMessage::Overlays { context }.into()),
..Default::default() ..Default::default()
} }
} }
@ -257,7 +260,7 @@ impl Fsm for GradientToolFsmState {
let ToolMessage::Gradient(event) = event else { return self }; let ToolMessage::Gradient(event) = event else { return self };
match (self, event) { match (self, event) {
(_, GradientToolMessage::Overlays(mut overlay_context)) => { (_, GradientToolMessage::Overlays { context: mut overlay_context }) => {
let selected = tool_data.selected_gradient.as_ref(); let selected = tool_data.selected_gradient.as_ref();
for layer in document.network_interface.selected_nodes().selected_visible_layers(&document.network_interface) { for layer in document.network_interface.selected_nodes().selected_visible_layers(&document.network_interface) {
@ -436,14 +439,7 @@ impl Fsm for GradientToolFsmState {
gradient.clone() gradient.clone()
} else { } else {
// Generate a new gradient // Generate a new gradient
Gradient::new( Gradient::new(DVec2::ZERO, global_tool_data.secondary_color, DVec2::ONE, global_tool_data.primary_color, tool_options.gradient_type)
DVec2::ZERO,
global_tool_data.secondary_color,
DVec2::ONE,
global_tool_data.primary_color,
DAffine2::IDENTITY,
tool_options.gradient_type,
)
}; };
let selected_gradient = SelectedGradient::new(gradient, layer, document).with_gradient_start(input.mouse.position); let selected_gradient = SelectedGradient::new(gradient, layer, document).with_gradient_start(input.mouse.position);

View file

@ -21,14 +21,16 @@ use crate::messages::tool::common_functionality::shape_editor::{
}; };
use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager}; use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager};
use crate::messages::tool::common_functionality::utility_functions::{calculate_segment_angle, find_two_param_best_approximate, make_path_editable_is_allowed}; use crate::messages::tool::common_functionality::utility_functions::{calculate_segment_angle, find_two_param_best_approximate, make_path_editable_is_allowed};
use bezier_rs::{Bezier, BezierHandles, TValue};
use graphene_std::Color; use graphene_std::Color;
use graphene_std::renderer::Quad; use graphene_std::renderer::Quad;
use graphene_std::subpath::pathseg_points;
use graphene_std::transform::ReferencePoint; use graphene_std::transform::ReferencePoint;
use graphene_std::uuid::NodeId; use graphene_std::uuid::NodeId;
use graphene_std::vector::algorithms::util::pathseg_tangent;
use graphene_std::vector::click_target::ClickTargetType; use graphene_std::vector::click_target::ClickTargetType;
use graphene_std::vector::misc::{HandleId, ManipulatorPointId}; use graphene_std::vector::misc::{HandleId, ManipulatorPointId, dvec2_to_point, point_to_dvec2};
use graphene_std::vector::{HandleExt, NoHashBuilder, PointId, SegmentId, Vector, VectorModificationType}; use graphene_std::vector::{HandleExt, NoHashBuilder, PointId, SegmentId, Vector, VectorModificationType};
use kurbo::{DEFAULT_ACCURACY, ParamCurve, ParamCurveNearest, PathSeg, Rect};
use std::vec; use std::vec;
#[derive(Default, ExtractField)] #[derive(Default, ExtractField)]
@ -49,8 +51,10 @@ pub struct PathToolOptions {
pub enum PathToolMessage { pub enum PathToolMessage {
// Standard messages // Standard messages
Abort, Abort,
Overlays(OverlayContext),
SelectionChanged, SelectionChanged,
Overlays {
context: OverlayContext,
},
// Tool-specific messages // Tool-specific messages
BreakPath, BreakPath,
@ -121,10 +125,13 @@ pub enum PathToolMessage {
position: ReferencePoint, position: ReferencePoint,
}, },
SwapSelectedHandles, SwapSelectedHandles,
UpdateOptions(PathOptionsUpdate), UpdateOptions {
options: PathOptionsUpdate,
},
UpdateSelectedPointsStatus { UpdateSelectedPointsStatus {
overlay_context: OverlayContext, overlay_context: OverlayContext,
}, },
StartSlidingPoint,
Copy { Copy {
clipboard: Clipboard, clipboard: Clipboard,
}, },
@ -272,15 +279,30 @@ impl LayoutHolder for PathTool {
RadioEntryData::new("all") RadioEntryData::new("all")
.icon("HandleVisibilityAll") .icon("HandleVisibilityAll")
.tooltip("Show all handles regardless of selection") .tooltip("Show all handles regardless of selection")
.on_update(move |_| PathToolMessage::UpdateOptions(PathOptionsUpdate::OverlayModeType(PathOverlayMode::AllHandles)).into()), .on_update(move |_| {
PathToolMessage::UpdateOptions {
options: PathOptionsUpdate::OverlayModeType(PathOverlayMode::AllHandles),
}
.into()
}),
RadioEntryData::new("selected") RadioEntryData::new("selected")
.icon("HandleVisibilitySelected") .icon("HandleVisibilitySelected")
.tooltip("Show only handles of the segments connected to selected points") .tooltip("Show only handles of the segments connected to selected points")
.on_update(move |_| PathToolMessage::UpdateOptions(PathOptionsUpdate::OverlayModeType(PathOverlayMode::SelectedPointHandles)).into()), .on_update(move |_| {
PathToolMessage::UpdateOptions {
options: PathOptionsUpdate::OverlayModeType(PathOverlayMode::SelectedPointHandles),
}
.into()
}),
RadioEntryData::new("frontier") RadioEntryData::new("frontier")
.icon("HandleVisibilityFrontier") .icon("HandleVisibilityFrontier")
.tooltip("Show only handles at the frontiers of the segments connected to selected points") .tooltip("Show only handles at the frontiers of the segments connected to selected points")
.on_update(move |_| PathToolMessage::UpdateOptions(PathOptionsUpdate::OverlayModeType(PathOverlayMode::FrontierHandles)).into()), .on_update(move |_| {
PathToolMessage::UpdateOptions {
options: PathOptionsUpdate::OverlayModeType(PathOverlayMode::FrontierHandles),
}
.into()
}),
]) ])
.selected_index(Some(self.options.path_overlay_mode as u32)) .selected_index(Some(self.options.path_overlay_mode as u32))
.widget_holder(); .widget_holder();
@ -342,7 +364,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Path
let updating_point = message == ToolMessage::Path(PathToolMessage::SelectedPointUpdated); let updating_point = message == ToolMessage::Path(PathToolMessage::SelectedPointUpdated);
match message { match message {
ToolMessage::Path(PathToolMessage::UpdateOptions(action)) => match action { ToolMessage::Path(PathToolMessage::UpdateOptions { options }) => match options {
PathOptionsUpdate::OverlayModeType(overlay_mode_type) => { PathOptionsUpdate::OverlayModeType(overlay_mode_type) => {
self.options.path_overlay_mode = overlay_mode_type; self.options.path_overlay_mode = overlay_mode_type;
responses.add(OverlaysMessage::Draw); responses.add(OverlaysMessage::Draw);
@ -418,6 +440,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Path
DeleteAndBreakPath, DeleteAndBreakPath,
ClosePath, ClosePath,
PointerMove, PointerMove,
StartSlidingPoint,
Copy, Copy,
Cut, Cut,
DeleteSelected, DeleteSelected,
@ -436,6 +459,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Path
BreakPath, BreakPath,
DeleteAndBreakPath, DeleteAndBreakPath,
SwapSelectedHandles, SwapSelectedHandles,
StartSlidingPoint,
Copy, Copy,
Cut, Cut,
DeleteSelected, DeleteSelected,
@ -454,6 +478,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Path
DeleteAndBreakPath, DeleteAndBreakPath,
Escape, Escape,
RightClick, RightClick,
StartSlidingPoint,
TogglePointEditing, TogglePointEditing,
ToggleSegmentEditing ToggleSegmentEditing
), ),
@ -472,7 +497,7 @@ impl ToolTransition for PathTool {
EventToMessageMap { EventToMessageMap {
tool_abort: Some(PathToolMessage::Abort.into()), tool_abort: Some(PathToolMessage::Abort.into()),
selection_changed: Some(PathToolMessage::SelectionChanged.into()), selection_changed: Some(PathToolMessage::SelectionChanged.into()),
overlay_provider: Some(|overlay_context| PathToolMessage::Overlays(overlay_context).into()), overlay_provider: Some(|context| PathToolMessage::Overlays { context }.into()),
..Default::default() ..Default::default()
} }
} }
@ -494,7 +519,7 @@ pub enum PointSelectState {
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub struct SlidingSegmentData { pub struct SlidingSegmentData {
segment_id: SegmentId, segment_id: SegmentId,
bezier: Bezier, bezier: PathSeg,
start: PointId, start: PointId,
} }
@ -840,13 +865,12 @@ impl PathToolData {
responses.add(OverlaysMessage::Draw); responses.add(OverlaysMessage::Draw);
PathToolFsmState::Dragging(self.dragging_state) PathToolFsmState::Dragging(self.dragging_state)
} else { } else {
let start_pos = segment.bezier().start; let points = pathseg_points(segment.pathseg());
let end_pos = segment.bezier().end;
let [pos1, pos2] = match segment.bezier().handles { let [pos1, pos2] = match (points.p1, points.p2) {
BezierHandles::Cubic { handle_start, handle_end } => [handle_start, handle_end], (Some(p1), Some(p2)) => [p1, p2],
BezierHandles::Quadratic { handle } => [handle, end_pos], (Some(p1), None) | (None, Some(p1)) => [p1, points.p3],
BezierHandles::Linear => [start_pos + (end_pos - start_pos) / 3., end_pos + (start_pos - end_pos) / 3.], (None, None) => [points.p0 + (points.p3 - points.p0) / 3., points.p3 + (points.p0 - points.p3) / 3.],
}; };
self.molding_info = Some((pos1, pos2)); self.molding_info = Some((pos1, pos2));
PathToolFsmState::Dragging(self.dragging_state) PathToolFsmState::Dragging(self.dragging_state)
@ -1207,15 +1231,10 @@ impl PathToolData {
}; };
let Some(vector) = document.network_interface.compute_modified_vector(layer) else { return false }; let Some(vector) = document.network_interface.compute_modified_vector(layer) else { return false };
// Check that the handles of anchor point are also colinear
if !vector.colinear(*anchor) {
return false;
};
let Some(point_id) = anchor.as_anchor() else { return false }; let Some(point_id) = anchor.as_anchor() else { return false };
let mut connected_segments = [None, None]; let mut connected_segments = [None, None];
for (segment, bezier, start, end) in vector.segment_bezier_iter() { for (segment, bezier, start, end) in vector.segment_iter() {
if start == point_id || end == point_id { if start == point_id || end == point_id {
match (connected_segments[0], connected_segments[1]) { match (connected_segments[0], connected_segments[1]) {
(None, None) => connected_segments[0] = Some(SlidingSegmentData { segment_id: segment, bezier, start }), (None, None) => connected_segments[0] = Some(SlidingSegmentData { segment_id: segment, bezier, start }),
@ -1255,13 +1274,13 @@ impl PathToolData {
let segments = sliding_point_info.connected_segments; let segments = sliding_point_info.connected_segments;
let t1 = segments[0].bezier.project(layer_pos); let t1 = segments[0].bezier.nearest(dvec2_to_point(layer_pos), DEFAULT_ACCURACY).t;
let position1 = segments[0].bezier.evaluate(TValue::Parametric(t1)); let position1 = point_to_dvec2(segments[0].bezier.eval(t1));
let t2 = segments[1].bezier.project(layer_pos); let t2 = segments[1].bezier.nearest(dvec2_to_point(layer_pos), DEFAULT_ACCURACY).t;
let position2 = segments[1].bezier.evaluate(TValue::Parametric(t2)); let position2 = point_to_dvec2(segments[1].bezier.eval(t2));
let (closer_segment, farther_segment, t_value, new_position) = if position2.distance(layer_pos) < position1.distance(layer_pos) { let (closer_segment, further_segment, t_value, new_position) = if position2.distance(layer_pos) < position1.distance(layer_pos) {
(segments[1], segments[0], t2, position2) (segments[1], segments[0], t2, position2)
} else { } else {
(segments[0], segments[1], t1, position1) (segments[0], segments[1], t1, position1)
@ -1276,50 +1295,59 @@ impl PathToolData {
shape_editor.move_anchor(anchor, &vector, delta, layer, None, responses); shape_editor.move_anchor(anchor, &vector, delta, layer, None, responses);
// Make a split at the t_value // Make a split at the t_value
let [first, second] = closer_segment.bezier.split(TValue::Parametric(t_value)); let first = closer_segment.bezier.subsegment(0_f64..t_value);
let closer_segment_other_point = if anchor == closer_segment.start { closer_segment.bezier.end } else { closer_segment.bezier.start }; let second = closer_segment.bezier.subsegment(t_value..1.);
let (split_segment, other_segment) = if first.start == closer_segment_other_point { (first, second) } else { (second, first) }; let closer_segment_other_point = if anchor == closer_segment.start {
closer_segment.bezier.end()
} else {
closer_segment.bezier.start()
};
let (split_segment, other_segment) = if first.start() == closer_segment_other_point { (first, second) } else { (second, first) };
let split_segment_points = pathseg_points(split_segment);
// Primary handle maps to primary handle and secondary maps to secondary // Primary handle maps to primary handle and secondary maps to secondary
let closer_primary_handle = HandleId::primary(closer_segment.segment_id); let closer_primary_handle = HandleId::primary(closer_segment.segment_id);
let Some(handle_position) = split_segment.handle_start() else { return }; let Some(handle_position) = split_segment_points.p1 else { return };
let relative_position1 = handle_position - split_segment.start; let relative_position1 = handle_position - split_segment_points.p0;
let modification_type = closer_primary_handle.set_relative_position(relative_position1); let modification_type = closer_primary_handle.set_relative_position(relative_position1);
responses.add(GraphOperationMessage::Vector { layer, modification_type }); responses.add(GraphOperationMessage::Vector { layer, modification_type });
let closer_secondary_handle = HandleId::end(closer_segment.segment_id); let closer_secondary_handle = HandleId::end(closer_segment.segment_id);
let Some(handle_position) = split_segment.handle_end() else { return }; let Some(handle_position) = split_segment_points.p2 else { return };
let relative_position2 = handle_position - split_segment.end; let relative_position2 = handle_position - split_segment_points.p3;
let modification_type = closer_secondary_handle.set_relative_position(relative_position2); let modification_type = closer_secondary_handle.set_relative_position(relative_position2);
responses.add(GraphOperationMessage::Vector { layer, modification_type }); responses.add(GraphOperationMessage::Vector { layer, modification_type });
let end_handle_direction = if anchor == closer_segment.start { -relative_position1 } else { -relative_position2 }; let end_handle_direction = if anchor == closer_segment.start { -relative_position1 } else { -relative_position2 };
let (farther_other_point, start_handle, end_handle, start_handle_pos) = if anchor == farther_segment.start { let further_segment_points = pathseg_points(further_segment.bezier);
let (further_other_point, start_handle, end_handle, start_handle_pos) = if anchor == further_segment.start {
( (
farther_segment.bezier.end, further_segment_points.p3,
HandleId::end(farther_segment.segment_id), HandleId::end(further_segment.segment_id),
HandleId::primary(farther_segment.segment_id), HandleId::primary(further_segment.segment_id),
farther_segment.bezier.handle_end(), further_segment_points.p2,
) )
} else { } else {
( (
farther_segment.bezier.start, further_segment_points.p0,
HandleId::primary(farther_segment.segment_id), HandleId::primary(further_segment.segment_id),
HandleId::end(farther_segment.segment_id), HandleId::end(further_segment.segment_id),
farther_segment.bezier.handle_start(), further_segment_points.p1,
) )
}; };
let Some(start_handle_position) = start_handle_pos else { return }; let Some(start_handle_position) = start_handle_pos else { return };
let start_handle_direction = start_handle_position - farther_other_point; let start_handle_direction = start_handle_position - further_other_point;
// Get normalized direction vectors, if cubic handle is zero then we consider corresponding tangent // Get normalized direction vectors, if cubic handle is zero then we consider corresponding tangent
let d1 = start_handle_direction.try_normalize().unwrap_or({ let d1 = start_handle_direction.try_normalize().unwrap_or({
if anchor == farther_segment.start { if anchor == further_segment.start {
-farther_segment.bezier.tangent(TValue::Parametric(0.99)) -pathseg_tangent(further_segment.bezier, 1.)
} else { } else {
farther_segment.bezier.tangent(TValue::Parametric(0.01)) pathseg_tangent(further_segment.bezier, 0.)
} }
}); });
@ -1328,7 +1356,7 @@ impl PathToolData {
let min_len1 = start_handle_direction.length() * 0.4; let min_len1 = start_handle_direction.length() * 0.4;
let min_len2 = end_handle_direction.length() * 0.4; let min_len2 = end_handle_direction.length() * 0.4;
let (relative_pos1, relative_pos2) = find_two_param_best_approximate(farther_other_point, new_position, d1, d2, min_len1, min_len2, farther_segment.bezier, other_segment); let (relative_pos1, relative_pos2) = find_two_param_best_approximate(further_other_point, new_position, d1, d2, min_len1, min_len2, further_segment.bezier, other_segment);
// Now set those handles to these handle lengths keeping the directions d1, d2 // Now set those handles to these handle lengths keeping the directions d1, d2
let modification_type = start_handle.set_relative_position(relative_pos1); let modification_type = start_handle.set_relative_position(relative_pos1);
@ -1548,14 +1576,22 @@ impl Fsm for PathToolFsmState {
match (multiple_toggle, point_edit) { match (multiple_toggle, point_edit) {
(true, true) => { (true, true) => {
responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::PointEditingMode { enabled: false })); responses.add(PathToolMessage::UpdateOptions {
options: PathOptionsUpdate::PointEditingMode { enabled: false },
});
} }
(true, false) => { (true, false) => {
responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::PointEditingMode { enabled: true })); responses.add(PathToolMessage::UpdateOptions {
options: PathOptionsUpdate::PointEditingMode { enabled: true },
});
} }
(_, _) => { (_, _) => {
responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::PointEditingMode { enabled: true })); responses.add(PathToolMessage::UpdateOptions {
responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::SegmentEditingMode { enabled: false })); options: PathOptionsUpdate::PointEditingMode { enabled: true },
});
responses.add(PathToolMessage::UpdateOptions {
options: PathOptionsUpdate::SegmentEditingMode { enabled: false },
});
// Select all of the end points of selected segments // Select all of the end points of selected segments
let selected_layers = shape_editor.selected_layers().cloned().collect::<Vec<_>>(); let selected_layers = shape_editor.selected_layers().cloned().collect::<Vec<_>>();
@ -1593,14 +1629,22 @@ impl Fsm for PathToolFsmState {
match (multiple_toggle, segment_edit) { match (multiple_toggle, segment_edit) {
(true, true) => { (true, true) => {
responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::SegmentEditingMode { enabled: false })); responses.add(PathToolMessage::UpdateOptions {
options: PathOptionsUpdate::SegmentEditingMode { enabled: false },
});
} }
(true, false) => { (true, false) => {
responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::SegmentEditingMode { enabled: true })); responses.add(PathToolMessage::UpdateOptions {
options: PathOptionsUpdate::SegmentEditingMode { enabled: true },
});
} }
(_, _) => { (_, _) => {
responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::PointEditingMode { enabled: false })); responses.add(PathToolMessage::UpdateOptions {
responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::SegmentEditingMode { enabled: true })); options: PathOptionsUpdate::PointEditingMode { enabled: false },
});
responses.add(PathToolMessage::UpdateOptions {
options: PathOptionsUpdate::SegmentEditingMode { enabled: true },
});
// Select all the segments which have both of the ends selected // Select all the segments which have both of the ends selected
let selected_layers = shape_editor.selected_layers().cloned().collect::<Vec<_>>(); let selected_layers = shape_editor.selected_layers().cloned().collect::<Vec<_>>();
@ -1623,7 +1667,7 @@ impl Fsm for PathToolFsmState {
self self
} }
(_, PathToolMessage::Overlays(mut overlay_context)) => { (_, PathToolMessage::Overlays { context: mut overlay_context }) => {
// Set this to show ghost line only if drag actually happened // Set this to show ghost line only if drag actually happened
if matches!(self, Self::Dragging(_)) && tool_data.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD { if matches!(self, Self::Dragging(_)) && tool_data.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD {
for (outline, layer) in &tool_data.ghost_outline { for (outline, layer) in &tool_data.ghost_outline {
@ -1737,13 +1781,13 @@ impl Fsm for PathToolFsmState {
if tool_options.path_editing_mode.segment_editing_mode && !tool_data.segment_editing_modifier { if tool_options.path_editing_mode.segment_editing_mode && !tool_data.segment_editing_modifier {
let transform = document.metadata().transform_to_viewport_if_feeds(closest_segment.layer(), &document.network_interface); let transform = document.metadata().transform_to_viewport_if_feeds(closest_segment.layer(), &document.network_interface);
overlay_context.outline_overlay_bezier(closest_segment.bezier(), transform); overlay_context.outline_overlay_bezier(closest_segment.pathseg(), transform);
// Draw the anchors again // Draw the anchors again
let display_anchors = overlay_context.visibility_settings.anchors(); let display_anchors = overlay_context.visibility_settings.anchors();
if display_anchors { if display_anchors {
let start_pos = transform.transform_point2(closest_segment.bezier().start); let start_pos = transform.transform_point2(point_to_dvec2(closest_segment.pathseg().start()));
let end_pos = transform.transform_point2(closest_segment.bezier().end); let end_pos = transform.transform_point2(point_to_dvec2(closest_segment.pathseg().end()));
let start_id = closest_segment.points()[0]; let start_id = closest_segment.points()[0];
let end_id = closest_segment.points()[1]; let end_id = closest_segment.points()[1];
if let Some(shape_state) = shape_editor.selected_shape_state.get_mut(&closest_segment.layer()) { if let Some(shape_state) = shape_editor.selected_shape_state.get_mut(&closest_segment.layer()) {
@ -1820,7 +1864,7 @@ impl Fsm for PathToolFsmState {
let (points_inside, segments_inside) = match selection_shape { let (points_inside, segments_inside) = match selection_shape {
SelectionShapeType::Box => { SelectionShapeType::Box => {
let previous_mouse = document.metadata().document_to_viewport.transform_point2(tool_data.previous_mouse_position); let previous_mouse = document.metadata().document_to_viewport.transform_point2(tool_data.previous_mouse_position);
let bbox = [tool_data.drag_start_pos, previous_mouse]; let bbox = Rect::new(tool_data.drag_start_pos.x, tool_data.drag_start_pos.y, previous_mouse.x, previous_mouse.y).abs();
shape_editor.get_inside_points_and_segments( shape_editor.get_inside_points_and_segments(
&document.network_interface, &document.network_interface,
SelectionShape::Box(bbox), SelectionShape::Box(bbox),
@ -1865,7 +1909,7 @@ impl Fsm for PathToolFsmState {
let transform = document.metadata().transform_to_viewport_if_feeds(layer, &document.network_interface); let transform = document.metadata().transform_to_viewport_if_feeds(layer, &document.network_interface);
for (segment, bezier, _, _) in vector.segment_bezier_iter() { for (segment, bezier, _, _) in vector.segment_iter() {
if segments.contains(&segment) { if segments.contains(&segment) {
overlay_context.outline_overlay_bezier(bezier, transform); overlay_context.outline_overlay_bezier(bezier, transform);
} }
@ -2073,10 +2117,6 @@ impl Fsm for PathToolFsmState {
} }
if !tool_data.update_colinear(equidistant_state, toggle_colinear_state, tool_action_data.shape_editor, tool_action_data.document, responses) { if !tool_data.update_colinear(equidistant_state, toggle_colinear_state, tool_action_data.shape_editor, tool_action_data.document, responses) {
if snap_angle_state && lock_angle_state && tool_data.start_sliding_point(tool_action_data.shape_editor, tool_action_data.document) {
return PathToolFsmState::SlidingPoint;
}
tool_data.drag( tool_data.drag(
equidistant_state, equidistant_state,
lock_angle_state, lock_angle_state,
@ -2241,7 +2281,8 @@ impl Fsm for PathToolFsmState {
match selection_shape { match selection_shape {
SelectionShapeType::Box => { SelectionShapeType::Box => {
let bbox = [tool_data.drag_start_pos, previous_mouse]; let bbox = Rect::new(tool_data.drag_start_pos.x, tool_data.drag_start_pos.y, previous_mouse.x, previous_mouse.y).abs();
shape_editor.select_all_in_shape( shape_editor.select_all_in_shape(
&document.network_interface, &document.network_interface,
SelectionShape::Box(bbox), SelectionShape::Box(bbox),
@ -2337,7 +2378,8 @@ impl Fsm for PathToolFsmState {
} else { } else {
match selection_shape { match selection_shape {
SelectionShapeType::Box => { SelectionShapeType::Box => {
let bbox = [tool_data.drag_start_pos, previous_mouse]; let bbox = Rect::new(tool_data.drag_start_pos.x, tool_data.drag_start_pos.y, previous_mouse.x, previous_mouse.y).abs();
shape_editor.select_all_in_shape( shape_editor.select_all_in_shape(
&document.network_interface, &document.network_interface,
SelectionShape::Box(bbox), SelectionShape::Box(bbox),
@ -2370,6 +2412,8 @@ impl Fsm for PathToolFsmState {
tool_data.ghost_outline.clear(); tool_data.ghost_outline.clear();
let extend_selection = input.keyboard.get(extend_selection as usize); let extend_selection = input.keyboard.get(extend_selection as usize);
let drag_occurred = tool_data.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD; let drag_occurred = tool_data.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD;
let mut segment_dissolved = false;
let mut point_inserted = false;
let nearest_point = shape_editor.find_nearest_visible_point_indices( let nearest_point = shape_editor.find_nearest_visible_point_indices(
&document.network_interface, &document.network_interface,
@ -2390,6 +2434,7 @@ impl Fsm for PathToolFsmState {
if tool_data.delete_segment_pressed { if tool_data.delete_segment_pressed {
if let Some(vector) = document.network_interface.compute_modified_vector(segment.layer()) { if let Some(vector) = document.network_interface.compute_modified_vector(segment.layer()) {
shape_editor.dissolve_segment(responses, segment.layer(), &vector, segment.segment(), segment.points()); shape_editor.dissolve_segment(responses, segment.layer(), &vector, segment.segment(), segment.points());
segment_dissolved = true;
} }
} else { } else {
let is_segment_selected = shape_editor let is_segment_selected = shape_editor
@ -2398,11 +2443,7 @@ impl Fsm for PathToolFsmState {
.is_some_and(|state| state.is_segment_selected(segment.segment())); .is_some_and(|state| state.is_segment_selected(segment.segment()));
segment.adjusted_insert_and_select(shape_editor, responses, extend_selection, point_mode, is_segment_selected); segment.adjusted_insert_and_select(shape_editor, responses, extend_selection, point_mode, is_segment_selected);
tool_data.segment = None; point_inserted = true;
tool_data.molding_info = None;
tool_data.molding_segment = false;
tool_data.temporary_adjacent_handles_while_molding = None;
return PathToolFsmState::Ready;
} }
} }
@ -2410,6 +2451,11 @@ impl Fsm for PathToolFsmState {
tool_data.molding_info = None; tool_data.molding_info = None;
tool_data.molding_segment = false; tool_data.molding_segment = false;
tool_data.temporary_adjacent_handles_while_molding = None; tool_data.temporary_adjacent_handles_while_molding = None;
if segment_dissolved || point_inserted {
responses.add(DocumentMessage::EndTransaction);
return PathToolFsmState::Ready;
}
} }
let segment_mode = tool_options.path_editing_mode.segment_editing_mode; let segment_mode = tool_options.path_editing_mode.segment_editing_mode;
@ -2566,6 +2612,14 @@ impl Fsm for PathToolFsmState {
shape_editor.delete_point_and_break_path(document, responses); shape_editor.delete_point_and_break_path(document, responses);
PathToolFsmState::Ready PathToolFsmState::Ready
} }
(_, PathToolMessage::StartSlidingPoint) => {
responses.add(DocumentMessage::StartTransaction);
if tool_data.start_sliding_point(shape_editor, document) {
PathToolFsmState::SlidingPoint
} else {
PathToolFsmState::Ready
}
}
(_, PathToolMessage::Copy { clipboard }) => { (_, PathToolMessage::Copy { clipboard }) => {
// TODO: Add support for selected segments // TODO: Add support for selected segments
@ -2697,16 +2751,13 @@ impl Fsm for PathToolFsmState {
// Create new segment ids and add the segments into the existing vector content // Create new segment ids and add the segments into the existing vector content
let mut segments_map = HashMap::new(); let mut segments_map = HashMap::new();
for (segment_id, bezier, start, end) in new_vector.segment_bezier_iter() { for (segment_id, bezier, start, end) in new_vector.segment_iter() {
let new_segment_id = SegmentId::generate(); let new_segment_id = SegmentId::generate();
segments_map.insert(segment_id, new_segment_id); segments_map.insert(segment_id, new_segment_id);
let handles = match bezier.handles { let points = pathseg_points(bezier);
BezierHandles::Linear => [None, None], let handles = [points.p1, points.p2];
BezierHandles::Quadratic { handle } => [Some(handle - bezier.start), None],
BezierHandles::Cubic { handle_start, handle_end } => [Some(handle_start - bezier.start), Some(handle_end - bezier.end)],
};
let points = [points_map[&start], points_map[&end]]; let points = [points_map[&start], points_map[&end]];
let modification_type = VectorModificationType::InsertSegment { id: new_segment_id, points, handles }; let modification_type = VectorModificationType::InsertSegment { id: new_segment_id, points, handles };
@ -2802,7 +2853,7 @@ impl Fsm for PathToolFsmState {
let mut segments_map = HashMap::new(); let mut segments_map = HashMap::new();
for (segment_id, bezier, start, end) in old_vector.segment_bezier_iter() { for (segment_id, bezier, start, end) in old_vector.segment_iter() {
let both_ends_selected = layer_selection_state.is_point_selected(ManipulatorPointId::Anchor(start)) && layer_selection_state.is_point_selected(ManipulatorPointId::Anchor(end)); let both_ends_selected = layer_selection_state.is_point_selected(ManipulatorPointId::Anchor(start)) && layer_selection_state.is_point_selected(ManipulatorPointId::Anchor(end));
let segment_selected = layer_selection_state.is_segment_selected(segment_id); let segment_selected = layer_selection_state.is_segment_selected(segment_id);
@ -2811,11 +2862,8 @@ impl Fsm for PathToolFsmState {
let new_id = SegmentId::generate(); let new_id = SegmentId::generate();
segments_map.insert(segment_id, new_id); segments_map.insert(segment_id, new_id);
let handles = match bezier.handles { let points = pathseg_points(bezier);
BezierHandles::Linear => [None, None], let handles = [points.p1, points.p2];
BezierHandles::Quadratic { handle } => [Some(handle - bezier.start), None],
BezierHandles::Cubic { handle_start, handle_end } => [Some(handle_start - bezier.start), Some(handle_end - bezier.end)],
};
let points = [points_map[&start], points_map[&end]]; let points = [points_map[&start], points_map[&end]];
let modification_type = VectorModificationType::InsertSegment { id: new_id, points, handles }; let modification_type = VectorModificationType::InsertSegment { id: new_id, points, handles };
@ -3291,7 +3339,7 @@ fn update_dynamic_hints(
} }
} }
let mut drag_selected_hints = vec![HintInfo::mouse(MouseMotion::LmbDrag, "Drag Selected")]; let drag_selected_hints = vec![HintInfo::mouse(MouseMotion::LmbDrag, "Drag Selected")];
let mut delete_selected_hints = vec![HintInfo::keys([Key::Delete], "Delete Selected")]; let mut delete_selected_hints = vec![HintInfo::keys([Key::Delete], "Delete Selected")];
if at_least_one_anchor_selected { if at_least_one_anchor_selected {
@ -3299,10 +3347,6 @@ fn update_dynamic_hints(
delete_selected_hints.push(HintInfo::keys([Key::Shift], "Cut Anchor").prepend_plus()); delete_selected_hints.push(HintInfo::keys([Key::Shift], "Cut Anchor").prepend_plus());
} }
if single_colinear_anchor_selected {
drag_selected_hints.push(HintInfo::multi_keys([[Key::Control], [Key::Shift]], "Slide").prepend_plus());
}
let segment_edit = tool_options.path_editing_mode.segment_editing_mode; let segment_edit = tool_options.path_editing_mode.segment_editing_mode;
let point_edit = tool_options.path_editing_mode.point_editing_mode; let point_edit = tool_options.path_editing_mode.point_editing_mode;
@ -3367,9 +3411,15 @@ fn update_dynamic_hints(
let mut groups = vec![ let mut groups = vec![
HintGroup(drag_selected_hints), HintGroup(drag_selected_hints),
HintGroup(vec![HintInfo::multi_keys([[Key::KeyG], [Key::KeyR], [Key::KeyS]], "Grab/Rotate/Scale Selected")]), HintGroup(vec![HintInfo::multi_keys([[Key::KeyG], [Key::KeyR], [Key::KeyS]], "Grab/Rotate/Scale Selected")]),
HintGroup(vec![HintInfo::arrow_keys("Nudge Selected"), HintInfo::keys([Key::Shift], "10x").prepend_plus()]),
HintGroup(delete_selected_hints),
]; ];
if single_colinear_anchor_selected {
groups.push(HintGroup(vec![HintInfo::multi_keys([[Key::KeyG], [Key::KeyG]], "Slide")]));
}
groups.push(HintGroup(vec![HintInfo::arrow_keys("Nudge Selected"), HintInfo::keys([Key::Shift], "10x").prepend_plus()]));
groups.push(HintGroup(delete_selected_hints));
hint_data.append(&mut groups); hint_data.append(&mut groups);
} }

View file

@ -11,11 +11,12 @@ use crate::messages::tool::common_functionality::graph_modification_utils::{self
use crate::messages::tool::common_functionality::shape_editor::ShapeState; use crate::messages::tool::common_functionality::shape_editor::ShapeState;
use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration}; use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration};
use crate::messages::tool::common_functionality::utility_functions::{calculate_segment_angle, closest_point, should_extend}; use crate::messages::tool::common_functionality::utility_functions::{calculate_segment_angle, closest_point, should_extend};
use bezier_rs::{Bezier, BezierHandles};
use graph_craft::document::NodeId; use graph_craft::document::NodeId;
use graphene_std::Color; use graphene_std::Color;
use graphene_std::vector::misc::{HandleId, ManipulatorPointId}; use graphene_std::subpath::pathseg_points;
use graphene_std::vector::misc::{HandleId, ManipulatorPointId, dvec2_to_point};
use graphene_std::vector::{NoHashBuilder, PointId, SegmentId, StrokeId, Vector, VectorModificationType}; use graphene_std::vector::{NoHashBuilder, PointId, SegmentId, StrokeId, Vector, VectorModificationType};
use kurbo::{CubicBez, PathSeg};
#[derive(Default, ExtractField)] #[derive(Default, ExtractField)]
pub struct PenTool { pub struct PenTool {
@ -49,7 +50,9 @@ pub enum PenToolMessage {
Abort, Abort,
SelectionChanged, SelectionChanged,
WorkingColorChanged, WorkingColorChanged,
Overlays(OverlayContext), Overlays {
context: OverlayContext,
},
// Tool-specific messages // Tool-specific messages
@ -79,7 +82,9 @@ pub enum PenToolMessage {
}, },
Redo, Redo,
Undo, Undo,
UpdateOptions(PenOptionsUpdate), UpdateOptions {
options: PenOptionsUpdate,
},
RecalculateLatestPointsPosition, RecalculateLatestPointsPosition,
RemovePreviousHandle, RemovePreviousHandle,
GRS { GRS {
@ -137,7 +142,12 @@ fn create_weight_widget(line_weight: f64) -> WidgetHolder {
.label("Weight") .label("Weight")
.min(0.) .min(0.)
.max((1_u64 << f64::MANTISSA_DIGITS) as f64) .max((1_u64 << f64::MANTISSA_DIGITS) as f64)
.on_update(|number_input: &NumberInput| PenToolMessage::UpdateOptions(PenOptionsUpdate::LineWeight(number_input.value.unwrap())).into()) .on_update(|number_input: &NumberInput| {
PenToolMessage::UpdateOptions {
options: PenOptionsUpdate::LineWeight(number_input.value.unwrap()),
}
.into()
})
.widget_holder() .widget_holder()
} }
@ -146,9 +156,26 @@ impl LayoutHolder for PenTool {
let mut widgets = self.options.fill.create_widgets( let mut widgets = self.options.fill.create_widgets(
"Fill", "Fill",
true, true,
|_| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColor(None)).into(), |_| {
|color_type: ToolColorType| WidgetCallback::new(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColorType(color_type.clone())).into()), PenToolMessage::UpdateOptions {
|color: &ColorInput| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(), options: PenOptionsUpdate::FillColor(None),
}
.into()
},
|color_type: ToolColorType| {
WidgetCallback::new(move |_| {
PenToolMessage::UpdateOptions {
options: PenOptionsUpdate::FillColorType(color_type.clone()),
}
.into()
})
},
|color: &ColorInput| {
PenToolMessage::UpdateOptions {
options: PenOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb())),
}
.into()
},
); );
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
@ -156,9 +183,26 @@ impl LayoutHolder for PenTool {
widgets.append(&mut self.options.stroke.create_widgets( widgets.append(&mut self.options.stroke.create_widgets(
"Stroke", "Stroke",
true, true,
|_| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColor(None)).into(), |_| {
|color_type: ToolColorType| WidgetCallback::new(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColorType(color_type.clone())).into()), PenToolMessage::UpdateOptions {
|color: &ColorInput| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(), options: PenOptionsUpdate::StrokeColor(None),
}
.into()
},
|color_type: ToolColorType| {
WidgetCallback::new(move |_| {
PenToolMessage::UpdateOptions {
options: PenOptionsUpdate::StrokeColorType(color_type.clone()),
}
.into()
})
},
|color: &ColorInput| {
PenToolMessage::UpdateOptions {
options: PenOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb())),
}
.into()
},
)); ));
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
@ -172,11 +216,21 @@ impl LayoutHolder for PenTool {
RadioEntryData::new("all") RadioEntryData::new("all")
.icon("HandleVisibilityAll") .icon("HandleVisibilityAll")
.tooltip("Show all handles regardless of selection") .tooltip("Show all handles regardless of selection")
.on_update(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::OverlayModeType(PenOverlayMode::AllHandles)).into()), .on_update(move |_| {
PenToolMessage::UpdateOptions {
options: PenOptionsUpdate::OverlayModeType(PenOverlayMode::AllHandles),
}
.into()
}),
RadioEntryData::new("frontier") RadioEntryData::new("frontier")
.icon("HandleVisibilityFrontier") .icon("HandleVisibilityFrontier")
.tooltip("Show only handles at the frontiers of the segments connected to selected points") .tooltip("Show only handles at the frontiers of the segments connected to selected points")
.on_update(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::OverlayModeType(PenOverlayMode::FrontierHandles)).into()), .on_update(move |_| {
PenToolMessage::UpdateOptions {
options: PenOptionsUpdate::OverlayModeType(PenOverlayMode::FrontierHandles),
}
.into()
}),
]) ])
.selected_index(Some(self.options.pen_overlay_mode as u32)) .selected_index(Some(self.options.pen_overlay_mode as u32))
.widget_holder(), .widget_holder(),
@ -189,12 +243,12 @@ impl LayoutHolder for PenTool {
#[message_handler_data] #[message_handler_data]
impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for PenTool { impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for PenTool {
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) { fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) {
let ToolMessage::Pen(PenToolMessage::UpdateOptions(action)) = message else { let ToolMessage::Pen(PenToolMessage::UpdateOptions { options }) = message else {
self.fsm_state.process_event(message, &mut self.tool_data, context, &self.options, responses, true); self.fsm_state.process_event(message, &mut self.tool_data, context, &self.options, responses, true);
return; return;
}; };
match action { match options {
PenOptionsUpdate::OverlayModeType(overlay_mode_type) => { PenOptionsUpdate::OverlayModeType(overlay_mode_type) => {
self.options.pen_overlay_mode = overlay_mode_type; self.options.pen_overlay_mode = overlay_mode_type;
responses.add(OverlaysMessage::Draw); responses.add(OverlaysMessage::Draw);
@ -252,7 +306,7 @@ impl ToolTransition for PenTool {
tool_abort: Some(PenToolMessage::Abort.into()), tool_abort: Some(PenToolMessage::Abort.into()),
selection_changed: Some(PenToolMessage::SelectionChanged.into()), selection_changed: Some(PenToolMessage::SelectionChanged.into()),
working_color_changed: Some(PenToolMessage::WorkingColorChanged.into()), working_color_changed: Some(PenToolMessage::WorkingColorChanged.into()),
overlay_provider: Some(|overlay_context| PenToolMessage::Overlays(overlay_context).into()), overlay_provider: Some(|context| PenToolMessage::Overlays { context }.into()),
..Default::default() ..Default::default()
} }
} }
@ -1257,7 +1311,7 @@ impl PenToolData {
let vector = document.network_interface.compute_modified_vector(layer); let vector = document.network_interface.compute_modified_vector(layer);
let (handle_start, in_segment) = if let Some(vector) = &vector { let (handle_start, in_segment) = if let Some(vector) = &vector {
vector vector
.segment_bezier_iter() .segment_iter()
.find_map(|(segment_id, bezier, start, end)| { .find_map(|(segment_id, bezier, start, end)| {
let is_end = point == end; let is_end = point == end;
let is_start = point == start; let is_start = point == start;
@ -1265,15 +1319,11 @@ impl PenToolData {
return None; return None;
} }
let handle = match bezier.handles { let points = pathseg_points(bezier);
BezierHandles::Cubic { handle_start, handle_end, .. } => { let handle = match (points.p1, points.p2) {
if is_start { (Some(p1), Some(_)) if is_start => p1,
handle_start (Some(_), Some(p2)) if !is_start => p2,
} else { (Some(p1), None) | (None, Some(p1)) => p1,
handle_end
}
}
BezierHandles::Quadratic { handle } => handle,
_ => return None, _ => return None,
}; };
Some((segment_id, is_end, handle)) Some((segment_id, is_end, handle))
@ -1550,7 +1600,7 @@ impl Fsm for PenToolFsmState {
responses.add(OverlaysMessage::Draw); responses.add(OverlaysMessage::Draw);
self self
} }
(PenToolFsmState::Ready, PenToolMessage::Overlays(mut overlay_context)) => { (PenToolFsmState::Ready, PenToolMessage::Overlays { context: mut overlay_context }) => {
match tool_options.pen_overlay_mode { match tool_options.pen_overlay_mode {
PenOverlayMode::AllHandles => { PenOverlayMode::AllHandles => {
path_overlays(document, DrawHandles::All, shape_editor, &mut overlay_context); path_overlays(document, DrawHandles::All, shape_editor, &mut overlay_context);
@ -1578,7 +1628,7 @@ impl Fsm for PenToolFsmState {
tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context); tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
self self
} }
(_, PenToolMessage::Overlays(mut overlay_context)) => { (_, PenToolMessage::Overlays { context: mut overlay_context }) => {
let display_anchors = overlay_context.visibility_settings.anchors(); let display_anchors = overlay_context.visibility_settings.anchors();
let display_handles = overlay_context.visibility_settings.handles(); let display_handles = overlay_context.visibility_settings.handles();
@ -1599,9 +1649,8 @@ impl Fsm for PenToolFsmState {
let handle_start = tool_data.latest_point().map(|point| transform.transform_point2(point.handle_start)); let handle_start = tool_data.latest_point().map(|point| transform.transform_point2(point.handle_start));
if let (Some((start, handle_start)), Some(handle_end)) = (tool_data.latest_point().map(|point| (point.pos, point.handle_start)), tool_data.handle_end) { if let (Some((start, handle_start)), Some(handle_end)) = (tool_data.latest_point().map(|point| (point.pos, point.handle_start)), tool_data.handle_end) {
let handles = BezierHandles::Cubic { handle_start, handle_end };
let end = tool_data.next_point; let end = tool_data.next_point;
let bezier = Bezier { start, handles, end }; let bezier = PathSeg::Cubic(CubicBez::new(dvec2_to_point(start), dvec2_to_point(handle_start), dvec2_to_point(handle_end), dvec2_to_point(end)));
if (end - start).length_squared() > f64::EPSILON { if (end - start).length_squared() > f64::EPSILON {
// Draw the curve for the currently-being-placed segment // Draw the curve for the currently-being-placed segment
overlay_context.outline_bezier(bezier, transform); overlay_context.outline_bezier(bezier, transform);
@ -1723,7 +1772,7 @@ impl Fsm for PenToolFsmState {
// We have the point. Join the 2 vertices and check if any path is closed. // We have the point. Join the 2 vertices and check if any path is closed.
if let Some(end) = closest_point { if let Some(end) = closest_point {
let segment_id = SegmentId::generate(); let segment_id = SegmentId::generate();
vector.push(segment_id, start, end, BezierHandles::Cubic { handle_start, handle_end }, StrokeId::ZERO); vector.push(segment_id, start, end, (Some(handle_start), Some(handle_end)), StrokeId::ZERO);
let grouped_segments = vector.auto_join_paths(); let grouped_segments = vector.auto_join_paths();
let closed_paths = grouped_segments.iter().filter(|path| path.is_closed() && path.contains(segment_id)); let closed_paths = grouped_segments.iter().filter(|path| path.is_closed() && path.contains(segment_id));
@ -1757,10 +1806,9 @@ impl Fsm for PenToolFsmState {
self self
} }
(_, PenToolMessage::WorkingColorChanged) => { (_, PenToolMessage::WorkingColorChanged) => {
responses.add(PenToolMessage::UpdateOptions(PenOptionsUpdate::WorkingColors( responses.add(PenToolMessage::UpdateOptions {
Some(global_tool_data.primary_color), options: PenOptionsUpdate::WorkingColors(Some(global_tool_data.primary_color), Some(global_tool_data.secondary_color)),
Some(global_tool_data.secondary_color), });
)));
self self
} }
(PenToolFsmState::Ready, PenToolMessage::DragStart { append_to_selected }) => { (PenToolFsmState::Ready, PenToolMessage::DragStart { append_to_selected }) => {

View file

@ -20,12 +20,12 @@ use crate::messages::tool::common_functionality::shape_editor::SelectionShapeTyp
use crate::messages::tool::common_functionality::snapping::{self, SnapCandidatePoint, SnapData, SnapManager}; use crate::messages::tool::common_functionality::snapping::{self, SnapCandidatePoint, SnapData, SnapManager};
use crate::messages::tool::common_functionality::transformation_cage::*; use crate::messages::tool::common_functionality::transformation_cage::*;
use crate::messages::tool::common_functionality::utility_functions::{resize_bounds, rotate_bounds, skew_bounds, text_bounding_box, transforming_transform_cage}; use crate::messages::tool::common_functionality::utility_functions::{resize_bounds, rotate_bounds, skew_bounds, text_bounding_box, transforming_transform_cage};
use bezier_rs::Subpath;
use glam::DMat2; use glam::DMat2;
use graph_craft::document::NodeId; use graph_craft::document::NodeId;
use graphene_std::path_bool::BooleanOperation; use graphene_std::path_bool::BooleanOperation;
use graphene_std::renderer::Quad; use graphene_std::renderer::Quad;
use graphene_std::renderer::Rect; use graphene_std::renderer::Rect;
use graphene_std::subpath::Subpath;
use graphene_std::transform::ReferencePoint; use graphene_std::transform::ReferencePoint;
use std::fmt; use std::fmt;
@ -78,7 +78,9 @@ pub struct SelectToolPointerKeys {
pub enum SelectToolMessage { pub enum SelectToolMessage {
// Standard messages // Standard messages
Abort, Abort,
Overlays(OverlayContext), Overlays {
context: OverlayContext,
},
// Tool-specific messages // Tool-specific messages
DragStart { DragStart {
@ -92,10 +94,17 @@ pub enum SelectToolMessage {
remove_from_selection: Key, remove_from_selection: Key,
}, },
EditLayer, EditLayer,
EditLayerExec,
Enter, Enter,
PointerMove(SelectToolPointerKeys), PointerMove {
PointerOutsideViewport(SelectToolPointerKeys), modifier_keys: SelectToolPointerKeys,
SelectOptions(SelectOptionsUpdate), },
PointerOutsideViewport {
modifier_keys: SelectToolPointerKeys,
},
SelectOptions {
options: SelectOptionsUpdate,
},
SetPivot { SetPivot {
position: ReferencePoint, position: ReferencePoint,
}, },
@ -126,9 +135,12 @@ impl SelectTool {
let layer_selection_behavior_entries = [NestedSelectionBehavior::Shallowest, NestedSelectionBehavior::Deepest] let layer_selection_behavior_entries = [NestedSelectionBehavior::Shallowest, NestedSelectionBehavior::Deepest]
.iter() .iter()
.map(|mode| { .map(|mode| {
MenuListEntry::new(format!("{mode:?}")) MenuListEntry::new(format!("{mode:?}")).label(mode.to_string()).on_commit(move |_| {
.label(mode.to_string()) SelectToolMessage::SelectOptions {
.on_commit(move |_| SelectToolMessage::SelectOptions(SelectOptionsUpdate::NestedSelectionBehavior(*mode)).into()) options: SelectOptionsUpdate::NestedSelectionBehavior(*mode),
}
.into()
})
}) })
.collect(); .collect();
@ -277,7 +289,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Sele
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) { fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) {
let mut redraw_reference_pivot = false; let mut redraw_reference_pivot = false;
if let ToolMessage::Select(SelectToolMessage::SelectOptions(ref option_update)) = message { if let ToolMessage::Select(SelectToolMessage::SelectOptions { options: ref option_update }) = message {
match option_update { match option_update {
SelectOptionsUpdate::NestedSelectionBehavior(nested_selection_behavior) => { SelectOptionsUpdate::NestedSelectionBehavior(nested_selection_behavior) => {
self.tool_data.nested_selection_behavior = *nested_selection_behavior; self.tool_data.nested_selection_behavior = *nested_selection_behavior;
@ -323,6 +335,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Sele
PointerMove, PointerMove,
Abort, Abort,
EditLayer, EditLayer,
EditLayerExec,
Enter, Enter,
); );
@ -340,7 +353,7 @@ impl ToolTransition for SelectTool {
fn event_to_message_map(&self) -> EventToMessageMap { fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap { EventToMessageMap {
tool_abort: Some(SelectToolMessage::Abort.into()), tool_abort: Some(SelectToolMessage::Abort.into()),
overlay_provider: Some(|overlay_context| SelectToolMessage::Overlays(overlay_context).into()), overlay_provider: Some(|context| SelectToolMessage::Overlays { context }.into()),
..Default::default() ..Default::default()
} }
} }
@ -589,7 +602,7 @@ impl Fsm for SelectToolFsmState {
let ToolMessage::Select(event) = event else { return self }; let ToolMessage::Select(event) = event else { return self };
match (self, event) { match (self, event) {
(_, SelectToolMessage::Overlays(mut overlay_context)) => { (_, SelectToolMessage::Overlays { context: mut overlay_context }) => {
tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context); tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
let selected_layers_count = document.network_interface.selected_nodes().selected_unlocked_layers(&document.network_interface).count(); let selected_layers_count = document.network_interface.selected_nodes().selected_unlocked_layers(&document.network_interface).count();
@ -689,32 +702,28 @@ impl Fsm for SelectToolFsmState {
// Measure with Alt held down // Measure with Alt held down
// TODO: Don't use `Key::Alt` directly, instead take it as a variable from the input mappings list like in all other places // TODO: Don't use `Key::Alt` directly, instead take it as a variable from the input mappings list like in all other places
if overlay_context.visibility_settings.quick_measurement() && !matches!(self, Self::ResizingBounds { .. }) && input.keyboard.get(Key::Alt as usize) { if overlay_context.visibility_settings.quick_measurement() && !matches!(self, Self::ResizingBounds { .. }) && input.keyboard.get(Key::Alt as usize) {
// Get all selected layers and compute their viewport-aligned AABB // Compute document-space bounding box (AABB) of all selected visible & unlocked layers
let selected_bounds_viewport = document let selected_bounds_doc_space = document
.network_interface .network_interface
.selected_nodes() .selected_nodes()
.selected_visible_and_unlocked_layers(&document.network_interface) .selected_visible_and_unlocked_layers(&document.network_interface)
// Exclude layers that are artboards
.filter(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[])) .filter(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[]))
.filter_map(|layer| { // For each remaining layer, try to get its document-space bounding box and convert it to a Rect
// Get the layer's bounding box in its local space .filter_map(|layer| document.metadata().bounding_box_document(layer).map(Rect::from_box))
let local_bounds = document.metadata().bounding_box_with_transform(layer, DAffine2::IDENTITY)?; // Combine all individual bounding boxes into one overall bounding box that contains all selected layers
// Transform the bounds directly to viewport space
let viewport_quad = document.metadata().transform_to_viewport(layer) * Quad::from_box(local_bounds);
// Convert the quad to an AABB in viewport space
Some(Rect::from_box(viewport_quad.bounding_box()))
})
.reduce(Rect::combine_bounds); .reduce(Rect::combine_bounds);
// Get the hovered layer's viewport-aligned AABB // Compute document-space bounding box (AABB) of the currently hovered layer
let hovered_bounds_viewport = document.metadata().bounding_box_with_transform(layer, DAffine2::IDENTITY).map(|bounds| { let hovered_bounds_doc_space = document.metadata().bounding_box_document(layer);
let viewport_quad = document.metadata().transform_to_viewport(layer) * Quad::from_box(bounds);
Rect::from_box(viewport_quad.bounding_box())
});
// Use the viewport-aligned AABBs for measurement // If both selected and hovered bounds exist, overlay measurement lines
if let (Some(selected_bounds), Some(hovered_bounds)) = (selected_bounds_viewport, hovered_bounds_viewport) { if let (Some(selected_bounds), Some(hovered_bounds)) = (selected_bounds_doc_space, hovered_bounds_doc_space.map(Rect::from_box)) {
// Since we're already in viewport space, use identity transform // Both `selected_bounds` and `hovered_bounds` are in document space.
measure::overlay(selected_bounds, hovered_bounds, DAffine2::IDENTITY, DAffine2::IDENTITY, &mut overlay_context); // To correctly render overlay lines in the UI (which is in viewport space), we need to transform both rectangles from document to viewport space.
// Therefore, we pass `document_to_viewport` as both the `transform` and `document_to_viewport` parameters.
let document_to_viewport = document.metadata().document_to_viewport;
measure::overlay(selected_bounds, hovered_bounds, document_to_viewport, document_to_viewport, &mut overlay_context);
} }
} }
} }
@ -985,14 +994,19 @@ impl Fsm for SelectToolFsmState {
self self
} }
(_, SelectToolMessage::EditLayer) => { (_, SelectToolMessage::EditLayer) => {
// Edit the clicked layer responses.add(DeferMessage::AfterGraphRun {
messages: vec![SelectToolMessage::EditLayerExec.into()],
});
self
}
(_, SelectToolMessage::EditLayerExec) => {
if let Some(intersect) = document.click(input) { if let Some(intersect) = document.click(input) {
match tool_data.nested_selection_behavior { match tool_data.nested_selection_behavior {
NestedSelectionBehavior::Shallowest => edit_layer_shallowest_manipulation(document, intersect, responses), NestedSelectionBehavior::Shallowest => edit_layer_shallowest_manipulation(document, intersect, responses),
NestedSelectionBehavior::Deepest => edit_layer_deepest_manipulation(intersect, &document.network_interface, responses), NestedSelectionBehavior::Deepest => edit_layer_deepest_manipulation(intersect, &document.network_interface, responses),
} }
} }
self self
} }
( (
@ -1139,7 +1153,7 @@ impl Fsm for SelectToolFsmState {
deepest, deepest,
remove, remove,
}, },
SelectToolMessage::PointerMove(modifier_keys), SelectToolMessage::PointerMove { modifier_keys },
) => { ) => {
if !has_dragged { if !has_dragged {
responses.add(ToolMessage::UpdateHints); responses.add(ToolMessage::UpdateHints);
@ -1184,8 +1198,8 @@ impl Fsm for SelectToolFsmState {
// Auto-panning // Auto-panning
let messages = [ let messages = [
SelectToolMessage::PointerOutsideViewport(modifier_keys.clone()).into(), SelectToolMessage::PointerOutsideViewport { modifier_keys: modifier_keys.clone() }.into(),
SelectToolMessage::PointerMove(modifier_keys).into(), SelectToolMessage::PointerMove { modifier_keys }.into(),
]; ];
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses); tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses);
@ -1197,7 +1211,7 @@ impl Fsm for SelectToolFsmState {
remove, remove,
} }
} }
(SelectToolFsmState::ResizingBounds, SelectToolMessage::PointerMove(modifier_keys)) => { (SelectToolFsmState::ResizingBounds, SelectToolMessage::PointerMove { modifier_keys }) => {
if let Some(bounds) = &mut tool_data.bounding_box_manager { if let Some(bounds) = &mut tool_data.bounding_box_manager {
resize_bounds( resize_bounds(
document, document,
@ -1212,14 +1226,14 @@ impl Fsm for SelectToolFsmState {
ToolType::Select, ToolType::Select,
); );
let messages = [ let messages = [
SelectToolMessage::PointerOutsideViewport(modifier_keys.clone()).into(), SelectToolMessage::PointerOutsideViewport { modifier_keys: modifier_keys.clone() }.into(),
SelectToolMessage::PointerMove(modifier_keys).into(), SelectToolMessage::PointerMove { modifier_keys }.into(),
]; ];
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses); tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses);
} }
SelectToolFsmState::ResizingBounds SelectToolFsmState::ResizingBounds
} }
(SelectToolFsmState::SkewingBounds { skew }, SelectToolMessage::PointerMove(_)) => { (SelectToolFsmState::SkewingBounds { skew }, SelectToolMessage::PointerMove { .. }) => {
if let Some(bounds) = &mut tool_data.bounding_box_manager { if let Some(bounds) = &mut tool_data.bounding_box_manager {
skew_bounds( skew_bounds(
document, document,
@ -1233,7 +1247,7 @@ impl Fsm for SelectToolFsmState {
} }
SelectToolFsmState::SkewingBounds { skew } SelectToolFsmState::SkewingBounds { skew }
} }
(SelectToolFsmState::RotatingBounds, SelectToolMessage::PointerMove(_)) => { (SelectToolFsmState::RotatingBounds, SelectToolMessage::PointerMove { .. }) => {
if let Some(bounds) = &mut tool_data.bounding_box_manager { if let Some(bounds) = &mut tool_data.bounding_box_manager {
rotate_bounds( rotate_bounds(
document, document,
@ -1249,7 +1263,7 @@ impl Fsm for SelectToolFsmState {
SelectToolFsmState::RotatingBounds SelectToolFsmState::RotatingBounds
} }
(SelectToolFsmState::DraggingPivot, SelectToolMessage::PointerMove(modifier_keys)) => { (SelectToolFsmState::DraggingPivot, SelectToolMessage::PointerMove { modifier_keys }) => {
let mouse_position = input.mouse.position; let mouse_position = input.mouse.position;
let snapped_mouse_position = mouse_position; let snapped_mouse_position = mouse_position;
@ -1259,14 +1273,14 @@ impl Fsm for SelectToolFsmState {
// Auto-panning // Auto-panning
let messages = [ let messages = [
SelectToolMessage::PointerOutsideViewport(modifier_keys.clone()).into(), SelectToolMessage::PointerOutsideViewport { modifier_keys: modifier_keys.clone() }.into(),
SelectToolMessage::PointerMove(modifier_keys).into(), SelectToolMessage::PointerMove { modifier_keys }.into(),
]; ];
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses); tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses);
SelectToolFsmState::DraggingPivot SelectToolFsmState::DraggingPivot
} }
(SelectToolFsmState::Drawing { selection_shape, has_drawn }, SelectToolMessage::PointerMove(modifier_keys)) => { (SelectToolFsmState::Drawing { selection_shape, has_drawn }, SelectToolMessage::PointerMove { modifier_keys }) => {
if !has_drawn { if !has_drawn {
responses.add(ToolMessage::UpdateHints); responses.add(ToolMessage::UpdateHints);
} }
@ -1280,14 +1294,14 @@ impl Fsm for SelectToolFsmState {
// Auto-panning // Auto-panning
let messages = [ let messages = [
SelectToolMessage::PointerOutsideViewport(modifier_keys.clone()).into(), SelectToolMessage::PointerOutsideViewport { modifier_keys: modifier_keys.clone() }.into(),
SelectToolMessage::PointerMove(modifier_keys).into(), SelectToolMessage::PointerMove { modifier_keys }.into(),
]; ];
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses); tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses);
SelectToolFsmState::Drawing { selection_shape, has_drawn: true } SelectToolFsmState::Drawing { selection_shape, has_drawn: true }
} }
(SelectToolFsmState::Ready { .. }, SelectToolMessage::PointerMove(_)) => { (SelectToolFsmState::Ready { .. }, SelectToolMessage::PointerMove { .. }) => {
let dragging_bounds = tool_data let dragging_bounds = tool_data
.bounding_box_manager .bounding_box_manager
.as_mut() .as_mut()
@ -1323,7 +1337,7 @@ impl Fsm for SelectToolFsmState {
deepest, deepest,
remove, remove,
}, },
SelectToolMessage::PointerOutsideViewport(_), SelectToolMessage::PointerOutsideViewport { .. },
) => { ) => {
// Auto-panning // Auto-panning
if let Some(shift) = tool_data.auto_panning.shift_viewport(input, responses) { if let Some(shift) = tool_data.auto_panning.shift_viewport(input, responses) {
@ -1339,7 +1353,7 @@ impl Fsm for SelectToolFsmState {
remove, remove,
} }
} }
(SelectToolFsmState::ResizingBounds | SelectToolFsmState::SkewingBounds { .. }, SelectToolMessage::PointerOutsideViewport(_)) => { (SelectToolFsmState::ResizingBounds | SelectToolFsmState::SkewingBounds { .. }, SelectToolMessage::PointerOutsideViewport { .. }) => {
// Auto-panning // Auto-panning
if let Some(shift) = tool_data.auto_panning.shift_viewport(input, responses) { if let Some(shift) = tool_data.auto_panning.shift_viewport(input, responses) {
if let Some(bounds) = &mut tool_data.bounding_box_manager { if let Some(bounds) = &mut tool_data.bounding_box_manager {
@ -1350,13 +1364,13 @@ impl Fsm for SelectToolFsmState {
self self
} }
(SelectToolFsmState::DraggingPivot, SelectToolMessage::PointerOutsideViewport(_)) => { (SelectToolFsmState::DraggingPivot, SelectToolMessage::PointerOutsideViewport { .. }) => {
// Auto-panning // Auto-panning
let _ = tool_data.auto_panning.shift_viewport(input, responses); let _ = tool_data.auto_panning.shift_viewport(input, responses);
self self
} }
(SelectToolFsmState::Drawing { .. }, SelectToolMessage::PointerOutsideViewport(_)) => { (SelectToolFsmState::Drawing { .. }, SelectToolMessage::PointerOutsideViewport { .. }) => {
// Auto-panning // Auto-panning
if let Some(shift) = tool_data.auto_panning.shift_viewport(input, responses) { if let Some(shift) = tool_data.auto_panning.shift_viewport(input, responses) {
tool_data.drag_start += shift; tool_data.drag_start += shift;
@ -1364,11 +1378,11 @@ impl Fsm for SelectToolFsmState {
self self
} }
(state, SelectToolMessage::PointerOutsideViewport(modifier_keys)) => { (state, SelectToolMessage::PointerOutsideViewport { modifier_keys }) => {
// Auto-panning // Auto-panning
let messages = [ let messages = [
SelectToolMessage::PointerOutsideViewport(modifier_keys.clone()).into(), SelectToolMessage::PointerOutsideViewport { modifier_keys: modifier_keys.clone() }.into(),
SelectToolMessage::PointerMove(modifier_keys).into(), SelectToolMessage::PointerMove { modifier_keys }.into(),
]; ];
tool_data.auto_panning.stop(&messages, responses); tool_data.auto_panning.stop(&messages, responses);

View file

@ -25,7 +25,7 @@ use graphene_std::renderer::Quad;
use graphene_std::vector::misc::{ArcType, GridType}; use graphene_std::vector::misc::{ArcType, GridType};
use std::vec; use std::vec;
#[derive(Default)] #[derive(Default, ExtractField)]
pub struct ShapeTool { pub struct ShapeTool {
fsm_state: ShapeToolFsmState, fsm_state: ShapeToolFsmState,
tool_data: ShapeToolData, tool_data: ShapeToolData,
@ -74,18 +74,18 @@ pub enum ShapeOptionsUpdate {
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum ShapeToolMessage { pub enum ShapeToolMessage {
// Standard messages // Standard messages
Overlays(OverlayContext), Overlays { context: OverlayContext },
Abort, Abort,
WorkingColorChanged, WorkingColorChanged,
// Tool-specific messages // Tool-specific messages
DragStart, DragStart,
DragStop, DragStop,
HideShapeTypeWidget(bool), HideShapeTypeWidget { hide: bool },
PointerMove(ShapeToolModifierKey), PointerMove { modifier: ShapeToolModifierKey },
PointerOutsideViewport(ShapeToolModifierKey), PointerOutsideViewport { modifier: ShapeToolModifierKey },
UpdateOptions(ShapeOptionsUpdate), UpdateOptions { options: ShapeOptionsUpdate },
SetShape(ShapeType), SetShape { shape: ShapeType },
IncreaseSides, IncreaseSides,
DecreaseSides, DecreaseSides,
@ -100,42 +100,71 @@ fn create_sides_widget(vertices: u32) -> WidgetHolder {
.min(3.) .min(3.)
.max(1000.) .max(1000.)
.mode(NumberInputMode::Increment) .mode(NumberInputMode::Increment)
.on_update(|number_input: &NumberInput| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices(number_input.value.unwrap() as u32)).into()) .on_update(|number_input: &NumberInput| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::Vertices(number_input.value.unwrap() as u32),
}
.into()
})
.widget_holder() .widget_holder()
} }
fn create_shape_option_widget(shape_type: ShapeType) -> WidgetHolder { fn create_shape_option_widget(shape_type: ShapeType) -> WidgetHolder {
let entries = vec![vec![ let entries = vec![vec![
MenuListEntry::new("Polygon") MenuListEntry::new("Polygon").label("Polygon").on_commit(move |_| {
.label("Polygon") ShapeToolMessage::UpdateOptions {
.on_commit(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ShapeType(ShapeType::Polygon)).into()), options: ShapeOptionsUpdate::ShapeType(ShapeType::Polygon),
MenuListEntry::new("Star") }
.label("Star") .into()
.on_commit(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ShapeType(ShapeType::Star)).into()), }),
MenuListEntry::new("Circle") MenuListEntry::new("Star").label("Star").on_commit(move |_| {
.label("Circle") ShapeToolMessage::UpdateOptions {
.on_commit(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ShapeType(ShapeType::Circle)).into()), options: ShapeOptionsUpdate::ShapeType(ShapeType::Star),
MenuListEntry::new("Arc") }
.label("Arc") .into()
.on_commit(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ShapeType(ShapeType::Arc)).into()), }),
MenuListEntry::new("Grid") MenuListEntry::new("Circle").label("Circle").on_commit(move |_| {
.label("Grid") ShapeToolMessage::UpdateOptions {
.on_commit(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ShapeType(ShapeType::Grid)).into()), options: ShapeOptionsUpdate::ShapeType(ShapeType::Circle),
}
.into()
}),
MenuListEntry::new("Arc").label("Arc").on_commit(move |_| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::ShapeType(ShapeType::Arc),
}
.into()
}),
MenuListEntry::new("Grid").label("Grid").on_commit(move |_| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::ShapeType(ShapeType::Grid),
}
.into()
}),
]]; ]];
DropdownInput::new(entries).selected_index(Some(shape_type as u32)).widget_holder() DropdownInput::new(entries).selected_index(Some(shape_type as u32)).widget_holder()
} }
fn create_arc_type_widget(arc_type: ArcType) -> WidgetHolder { fn create_arc_type_widget(arc_type: ArcType) -> WidgetHolder {
let entries = vec![ let entries = vec![
RadioEntryData::new("Open") RadioEntryData::new("Open").label("Open").on_update(move |_| {
.label("Open") ShapeToolMessage::UpdateOptions {
.on_update(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ArcType(ArcType::Open)).into()), options: ShapeOptionsUpdate::ArcType(ArcType::Open),
RadioEntryData::new("Closed") }
.label("Closed") .into()
.on_update(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ArcType(ArcType::Closed)).into()), }),
RadioEntryData::new("Pie") RadioEntryData::new("Closed").label("Closed").on_update(move |_| {
.label("Pie") ShapeToolMessage::UpdateOptions {
.on_update(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ArcType(ArcType::PieSlice)).into()), options: ShapeOptionsUpdate::ArcType(ArcType::Closed),
}
.into()
}),
RadioEntryData::new("Pie").label("Pie").on_update(move |_| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::ArcType(ArcType::PieSlice),
}
.into()
}),
]; ];
RadioInput::new(entries).selected_index(Some(arc_type as u32)).widget_holder() RadioInput::new(entries).selected_index(Some(arc_type as u32)).widget_holder()
} }
@ -146,18 +175,29 @@ fn create_weight_widget(line_weight: f64) -> WidgetHolder {
.label("Weight") .label("Weight")
.min(0.) .min(0.)
.max((1_u64 << f64::MANTISSA_DIGITS) as f64) .max((1_u64 << f64::MANTISSA_DIGITS) as f64)
.on_update(|number_input: &NumberInput| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::LineWeight(number_input.value.unwrap())).into()) .on_update(|number_input: &NumberInput| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::LineWeight(number_input.value.unwrap()),
}
.into()
})
.widget_holder() .widget_holder()
} }
fn create_grid_type_widget(grid_type: GridType) -> WidgetHolder { fn create_grid_type_widget(grid_type: GridType) -> WidgetHolder {
let entries = vec![ let entries = vec![
RadioEntryData::new("Rectangular") RadioEntryData::new("Rectangular").label("Rectangular").on_update(move |_| {
.label("Rectangular") ShapeToolMessage::UpdateOptions {
.on_update(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::GridType(GridType::Rectangular)).into()), options: ShapeOptionsUpdate::GridType(GridType::Rectangular),
RadioEntryData::new("Isometric") }
.label("Isometric") .into()
.on_update(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::GridType(GridType::Isometric)).into()), }),
RadioEntryData::new("Isometric").label("Isometric").on_update(move |_| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::GridType(GridType::Isometric),
}
.into()
}),
]; ];
RadioInput::new(entries).selected_index(Some(grid_type as u32)).widget_holder() RadioInput::new(entries).selected_index(Some(grid_type as u32)).widget_holder()
} }
@ -190,9 +230,26 @@ impl LayoutHolder for ShapeTool {
widgets.append(&mut self.options.fill.create_widgets( widgets.append(&mut self.options.fill.create_widgets(
"Fill", "Fill",
true, true,
|_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::FillColor(None)).into(), |_| {
|color_type: ToolColorType| WidgetCallback::new(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::FillColorType(color_type.clone())).into()), ShapeToolMessage::UpdateOptions {
|color: &ColorInput| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(), options: ShapeOptionsUpdate::FillColor(None),
}
.into()
},
|color_type: ToolColorType| {
WidgetCallback::new(move |_| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::FillColorType(color_type.clone()),
}
.into()
})
},
|color: &ColorInput| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb())),
}
.into()
},
)); ));
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
@ -201,9 +258,26 @@ impl LayoutHolder for ShapeTool {
widgets.append(&mut self.options.stroke.create_widgets( widgets.append(&mut self.options.stroke.create_widgets(
"Stroke", "Stroke",
true, true,
|_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::StrokeColor(None)).into(), |_| {
|color_type: ToolColorType| WidgetCallback::new(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::StrokeColorType(color_type.clone())).into()), ShapeToolMessage::UpdateOptions {
|color: &ColorInput| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(), options: ShapeOptionsUpdate::StrokeColor(None),
}
.into()
},
|color_type: ToolColorType| {
WidgetCallback::new(move |_| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::StrokeColorType(color_type.clone()),
}
.into()
})
},
|color: &ColorInput| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb())),
}
.into()
},
)); ));
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
widgets.push(create_weight_widget(self.options.line_weight)); widgets.push(create_weight_widget(self.options.line_weight));
@ -212,13 +286,14 @@ impl LayoutHolder for ShapeTool {
} }
} }
#[message_handler_data]
impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for ShapeTool { impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for ShapeTool {
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) { fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) {
let ToolMessage::Shape(ShapeToolMessage::UpdateOptions(action)) = message else { let ToolMessage::Shape(ShapeToolMessage::UpdateOptions { options }) = message else {
self.fsm_state.process_event(message, &mut self.tool_data, context, &self.options, responses, true); self.fsm_state.process_event(message, &mut self.tool_data, context, &self.options, responses, true);
return; return;
}; };
match action { match options {
ShapeOptionsUpdate::FillColor(color) => { ShapeOptionsUpdate::FillColor(color) => {
self.options.fill.custom_color = color; self.options.fill.custom_color = color;
self.options.fill.color_type = ToolColorType::Custom; self.options.fill.color_type = ToolColorType::Custom;
@ -309,7 +384,7 @@ impl ToolMetadata for ShapeTool {
impl ToolTransition for ShapeTool { impl ToolTransition for ShapeTool {
fn event_to_message_map(&self) -> EventToMessageMap { fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap { EventToMessageMap {
overlay_provider: Some(|overlay_context| ShapeToolMessage::Overlays(overlay_context).into()), overlay_provider: Some(|context| ShapeToolMessage::Overlays { context }.into()),
tool_abort: Some(ShapeToolMessage::Abort.into()), tool_abort: Some(ShapeToolMessage::Abort.into()),
working_color_changed: Some(ShapeToolMessage::WorkingColorChanged.into()), working_color_changed: Some(ShapeToolMessage::WorkingColorChanged.into()),
..Default::default() ..Default::default()
@ -426,7 +501,7 @@ impl Fsm for ShapeToolFsmState {
let ToolMessage::Shape(event) = event else { return self }; let ToolMessage::Shape(event) = event else { return self };
match (self, event) { match (self, event) {
(_, ShapeToolMessage::Overlays(mut overlay_context)) => { (_, ShapeToolMessage::Overlays { context: mut overlay_context }) => {
let mouse_position = tool_data let mouse_position = tool_data
.data .data
.snap_manager .snap_manager
@ -508,11 +583,15 @@ impl Fsm for ShapeToolFsmState {
self self
} }
(ShapeToolFsmState::Ready(_), ShapeToolMessage::IncreaseSides) => { (ShapeToolFsmState::Ready(_), ShapeToolMessage::IncreaseSides) => {
responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices(tool_options.vertices + 1))); responses.add(ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::Vertices(tool_options.vertices + 1),
});
self self
} }
(ShapeToolFsmState::Ready(_), ShapeToolMessage::DecreaseSides) => { (ShapeToolFsmState::Ready(_), ShapeToolMessage::DecreaseSides) => {
responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices((tool_options.vertices - 1).max(3)))); responses.add(ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::Vertices((tool_options.vertices - 1).max(3)),
});
self self
} }
( (
@ -579,7 +658,9 @@ impl Fsm for ShapeToolFsmState {
tool_data.cursor = cursor; tool_data.cursor = cursor;
responses.add(FrontendMessage::UpdateMouseCursor { cursor }); responses.add(FrontendMessage::UpdateMouseCursor { cursor });
// Send a PointerMove message to refresh the cursor icon // Send a PointerMove message to refresh the cursor icon
responses.add(ShapeToolMessage::PointerMove(ShapeToolData::shape_tool_modifier_keys())); responses.add(ShapeToolMessage::PointerMove {
modifier: ShapeToolData::shape_tool_modifier_keys(),
});
responses.add(DocumentMessage::StartTransaction); responses.add(DocumentMessage::StartTransaction);
return ShapeToolFsmState::ModifyingGizmo; return ShapeToolFsmState::ModifyingGizmo;
@ -607,7 +688,9 @@ impl Fsm for ShapeToolFsmState {
let cursor = tool_data.transform_cage_mouse_icon(input); let cursor = tool_data.transform_cage_mouse_icon(input);
tool_data.cursor = cursor; tool_data.cursor = cursor;
responses.add(FrontendMessage::UpdateMouseCursor { cursor }); responses.add(FrontendMessage::UpdateMouseCursor { cursor });
responses.add(ShapeToolMessage::PointerMove(ShapeToolData::shape_tool_modifier_keys())); responses.add(ShapeToolMessage::PointerMove {
modifier: ShapeToolData::shape_tool_modifier_keys(),
});
}; };
match (resize, rotate, skew) { match (resize, rotate, skew) {
@ -688,7 +771,7 @@ impl Fsm for ShapeToolFsmState {
ShapeToolFsmState::Drawing(tool_data.current_shape) ShapeToolFsmState::Drawing(tool_data.current_shape)
} }
(ShapeToolFsmState::Drawing(shape), ShapeToolMessage::PointerMove(modifier)) => { (ShapeToolFsmState::Drawing(shape), ShapeToolMessage::PointerMove { modifier }) => {
let Some(layer) = tool_data.data.layer else { let Some(layer) = tool_data.data.layer else {
return ShapeToolFsmState::Ready(shape); return ShapeToolFsmState::Ready(shape);
}; };
@ -705,33 +788,33 @@ impl Fsm for ShapeToolFsmState {
} }
// Auto-panning // Auto-panning
let messages = [ShapeToolMessage::PointerOutsideViewport(modifier).into(), ShapeToolMessage::PointerMove(modifier).into()]; let messages = [ShapeToolMessage::PointerOutsideViewport { modifier }.into(), ShapeToolMessage::PointerMove { modifier }.into()];
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses); tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses);
self self
} }
(ShapeToolFsmState::DraggingLineEndpoints, ShapeToolMessage::PointerMove(modifier)) => { (ShapeToolFsmState::DraggingLineEndpoints, ShapeToolMessage::PointerMove { modifier }) => {
let Some(layer) = tool_data.line_data.editing_layer else { let Some(layer) = tool_data.line_data.editing_layer else {
return ShapeToolFsmState::Ready(tool_data.current_shape); return ShapeToolFsmState::Ready(tool_data.current_shape);
}; };
Line::update_shape(document, input, layer, tool_data, modifier, responses); Line::update_shape(document, input, layer, tool_data, modifier, responses);
// Auto-panning // Auto-panning
let messages = [ShapeToolMessage::PointerOutsideViewport(modifier).into(), ShapeToolMessage::PointerMove(modifier).into()]; let messages = [ShapeToolMessage::PointerOutsideViewport { modifier }.into(), ShapeToolMessage::PointerMove { modifier }.into()];
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses); tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses);
self self
} }
(ShapeToolFsmState::ModifyingGizmo, ShapeToolMessage::PointerMove(..)) => { (ShapeToolFsmState::ModifyingGizmo, ShapeToolMessage::PointerMove { .. }) => {
tool_data.gizmo_manager.handle_update(tool_data.data.viewport_drag_start(document), document, input, responses); tool_data.gizmo_manager.handle_update(tool_data.data.viewport_drag_start(document), document, input, responses);
responses.add(OverlaysMessage::Draw); responses.add(OverlaysMessage::Draw);
ShapeToolFsmState::ModifyingGizmo ShapeToolFsmState::ModifyingGizmo
} }
(ShapeToolFsmState::ResizingBounds, ShapeToolMessage::PointerMove(modifier)) => { (ShapeToolFsmState::ResizingBounds, ShapeToolMessage::PointerMove { modifier }) => {
if let Some(bounds) = &mut tool_data.bounding_box_manager { if let Some(bounds) = &mut tool_data.bounding_box_manager {
let messages = [ShapeToolMessage::PointerOutsideViewport(modifier).into(), ShapeToolMessage::PointerMove(modifier).into()]; let messages = [ShapeToolMessage::PointerOutsideViewport { modifier }.into(), ShapeToolMessage::PointerMove { modifier }.into()];
resize_bounds( resize_bounds(
document, document,
responses, responses,
@ -750,7 +833,7 @@ impl Fsm for ShapeToolFsmState {
responses.add(OverlaysMessage::Draw); responses.add(OverlaysMessage::Draw);
ShapeToolFsmState::ResizingBounds ShapeToolFsmState::ResizingBounds
} }
(ShapeToolFsmState::RotatingBounds, ShapeToolMessage::PointerMove(modifier)) => { (ShapeToolFsmState::RotatingBounds, ShapeToolMessage::PointerMove { modifier }) => {
if let Some(bounds) = &mut tool_data.bounding_box_manager { if let Some(bounds) = &mut tool_data.bounding_box_manager {
rotate_bounds( rotate_bounds(
document, document,
@ -766,7 +849,7 @@ impl Fsm for ShapeToolFsmState {
ShapeToolFsmState::RotatingBounds ShapeToolFsmState::RotatingBounds
} }
(ShapeToolFsmState::SkewingBounds { skew }, ShapeToolMessage::PointerMove(_)) => { (ShapeToolFsmState::SkewingBounds { skew }, ShapeToolMessage::PointerMove { .. }) => {
if let Some(bounds) = &mut tool_data.bounding_box_manager { if let Some(bounds) = &mut tool_data.bounding_box_manager {
skew_bounds( skew_bounds(
document, document,
@ -782,7 +865,7 @@ impl Fsm for ShapeToolFsmState {
ShapeToolFsmState::SkewingBounds { skew } ShapeToolFsmState::SkewingBounds { skew }
} }
(_, ShapeToolMessage::PointerMove(_)) => { (_, ShapeToolMessage::PointerMove { .. }) => {
let dragging_bounds = tool_data let dragging_bounds = tool_data
.bounding_box_manager .bounding_box_manager
.as_mut() .as_mut()
@ -804,7 +887,7 @@ impl Fsm for ShapeToolFsmState {
responses.add(OverlaysMessage::Draw); responses.add(OverlaysMessage::Draw);
self self
} }
(ShapeToolFsmState::ResizingBounds | ShapeToolFsmState::SkewingBounds { .. }, ShapeToolMessage::PointerOutsideViewport(_)) => { (ShapeToolFsmState::ResizingBounds | ShapeToolFsmState::SkewingBounds { .. }, ShapeToolMessage::PointerOutsideViewport { .. }) => {
// Auto-panning // Auto-panning
if let Some(shift) = tool_data.auto_panning.shift_viewport(input, responses) { if let Some(shift) = tool_data.auto_panning.shift_viewport(input, responses) {
if let Some(bounds) = &mut tool_data.bounding_box_manager { if let Some(bounds) = &mut tool_data.bounding_box_manager {
@ -815,7 +898,7 @@ impl Fsm for ShapeToolFsmState {
self self
} }
(ShapeToolFsmState::Ready(_), ShapeToolMessage::PointerOutsideViewport(..)) => self, (ShapeToolFsmState::Ready(_), ShapeToolMessage::PointerOutsideViewport { .. }) => self,
(_, ShapeToolMessage::PointerOutsideViewport { .. }) => { (_, ShapeToolMessage::PointerOutsideViewport { .. }) => {
// Auto-panning // Auto-panning
let _ = tool_data.auto_panning.shift_viewport(input, responses); let _ = tool_data.auto_panning.shift_viewport(input, responses);
@ -870,21 +953,22 @@ impl Fsm for ShapeToolFsmState {
ShapeToolFsmState::Ready(tool_data.current_shape) ShapeToolFsmState::Ready(tool_data.current_shape)
} }
(_, ShapeToolMessage::WorkingColorChanged) => { (_, ShapeToolMessage::WorkingColorChanged) => {
responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::WorkingColors( responses.add(ShapeToolMessage::UpdateOptions {
Some(global_tool_data.primary_color), options: ShapeOptionsUpdate::WorkingColors(Some(global_tool_data.primary_color), Some(global_tool_data.secondary_color)),
Some(global_tool_data.secondary_color), });
)));
self self
} }
(_, ShapeToolMessage::SetShape(shape)) => { (_, ShapeToolMessage::SetShape { shape }) => {
responses.add(DocumentMessage::AbortTransaction); responses.add(DocumentMessage::AbortTransaction);
tool_data.data.cleanup(responses); tool_data.data.cleanup(responses);
tool_data.current_shape = shape; tool_data.current_shape = shape;
responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ShapeType(shape))); responses.add(ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::ShapeType(shape),
});
ShapeToolFsmState::Ready(shape) ShapeToolFsmState::Ready(shape)
} }
(_, ShapeToolMessage::HideShapeTypeWidget(hide)) => { (_, ShapeToolMessage::HideShapeTypeWidget { hide }) => {
tool_data.hide_shape_option_widget = hide; tool_data.hide_shape_option_widget = hide;
responses.add(ToolMessage::RefreshToolOptions); responses.add(ToolMessage::RefreshToolOptions);
self self

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