Merge branch 'master' into merge_point

This commit is contained in:
Adesh Gupta
2025-08-19 01:39:28 +05:30
committed by GitHub
214 changed files with 4701 additions and 19556 deletions
+4 -36
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
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
+11 -5
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
+1 -1
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": {
Generated
+19 -41
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"
+21 -10
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
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
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 }
+74 -1
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;
@@ -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();
+42 -20
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);
}
}
+4 -8
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
-4
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 {
@@ -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));
@@ -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 });
} }
} }
} }
@@ -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>,
@@ -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),
@@ -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 },
@@ -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(_)
@@ -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)]
@@ -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")]
@@ -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
+2 -4
View File
@@ -33,8 +33,6 @@ pub enum Message {
Preferences(PreferencesMessage), Preferences(PreferencesMessage),
#[child] #[child]
Tool(ToolMessage), Tool(ToolMessage),
#[child]
Workspace(WorkspaceMessage),
// Messages // Messages
NoOp, NoOp,
@@ -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 {
@@ -99,7 +97,7 @@ mod test {
// 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))
-1
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;
@@ -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,
@@ -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))
}
@@ -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::*;
@@ -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 {
@@ -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,12 +381,15 @@ 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(overlay_context) => {
if !overlay_context.visibility_settings.artboard_name() { if !overlay_context.visibility_settings.artboard_name() {
@@ -554,6 +582,7 @@ 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 => {
@@ -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 {
@@ -2549,7 +2577,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 +2739,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 +2985,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 +2999,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 +3022,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 +3075,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)
} }
} }
@@ -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;
@@ -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,
}) })
@@ -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`.
@@ -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);
@@ -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;
@@ -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()
@@ -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 {
@@ -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);
@@ -1485,7 +1489,6 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
}; };
selected_nodes.set_selected_nodes(nodes); selected_nodes.set_selected_nodes(nodes);
responses.add(BroadcastEvent::SelectionChanged); responses.add(BroadcastEvent::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
@@ -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();
@@ -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.));
@@ -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")]
@@ -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;
}; };
@@ -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) => {
@@ -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);
@@ -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,
}); });
} }
} }
@@ -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() {
@@ -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 {
@@ -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);
} }
@@ -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(),
-1
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)]
@@ -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,
@@ -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)
} }
@@ -201,6 +210,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
// 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 });
} }
@@ -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);
@@ -388,7 +399,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
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()),
}); });
} }
@@ -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,14 +1120,14 @@ 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);
@@ -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;
} }
@@ -1,7 +0,0 @@
mod spreadsheet_message;
mod spreadsheet_message_handler;
#[doc(inline)]
pub use spreadsheet_message::*;
#[doc(inline)]
pub use spreadsheet_message_handler::*;
@@ -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 }]
}
}
@@ -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}"),
} }
} }
+1 -2
View File
@@ -17,6 +17,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,12 +25,10 @@ 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::broadcast::broadcast_event::{BroadcastEvent, BroadcastEventDiscriminant};
@@ -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())
@@ -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);
@@ -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.")
@@ -11,10 +11,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::{ArcType, dvec2_to_point}; use graphene_std::vector::misc::{ArcType, dvec2_to_point};
use kurbo::{BezPath, PathEl, Shape}; use kurbo::{BezPath, PathEl, Shape};
@@ -363,9 +363,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);
@@ -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);
@@ -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));
@@ -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,
} }
@@ -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) };
@@ -156,6 +156,9 @@ 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(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(BroadcastEvent::SelectionChanged);
@@ -204,7 +204,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,
@@ -436,14 +435,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);
@@ -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)]
@@ -126,6 +128,7 @@ pub enum PathToolMessage {
overlay_context: OverlayContext, overlay_context: OverlayContext,
}, },
MergeSelectedPoints, MergeSelectedPoints,
StartSlidingPoint,
Copy { Copy {
clipboard: Clipboard, clipboard: Clipboard,
}, },
@@ -429,6 +432,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Path
ClosePath, ClosePath,
PointerMove, PointerMove,
MergeSelectedPoints, MergeSelectedPoints,
StartSlidingPoint,
Copy, Copy,
Cut, Cut,
DeleteSelected, DeleteSelected,
@@ -447,6 +451,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Path
BreakPath, BreakPath,
DeleteAndBreakPath, DeleteAndBreakPath,
SwapSelectedHandles, SwapSelectedHandles,
StartSlidingPoint,
Copy, Copy,
Cut, Cut,
DeleteSelected, DeleteSelected,
@@ -465,6 +470,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Path
DeleteAndBreakPath, DeleteAndBreakPath,
Escape, Escape,
RightClick, RightClick,
StartSlidingPoint,
TogglePointEditing, TogglePointEditing,
ToggleSegmentEditing ToggleSegmentEditing
), ),
@@ -505,7 +511,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,
} }
@@ -910,13 +916,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)
@@ -1277,15 +1282,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 }),
@@ -1325,13 +1325,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)
@@ -1346,50 +1346,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.)
} }
}); });
@@ -1398,7 +1407,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);
@@ -1807,13 +1816,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()) {
@@ -1890,7 +1899,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);
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),
@@ -1935,7 +1944,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);
} }
@@ -2143,10 +2152,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,
@@ -2311,7 +2316,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);
shape_editor.select_all_in_shape( shape_editor.select_all_in_shape(
&document.network_interface, &document.network_interface,
SelectionShape::Box(bbox), SelectionShape::Box(bbox),
@@ -2407,7 +2413,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);
shape_editor.select_all_in_shape( shape_editor.select_all_in_shape(
&document.network_interface, &document.network_interface,
SelectionShape::Box(bbox), SelectionShape::Box(bbox),
@@ -2440,6 +2447,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,
@@ -2460,6 +2469,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
@@ -2468,11 +2478,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;
} }
} }
@@ -2480,6 +2486,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;
@@ -2636,6 +2647,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
@@ -2767,16 +2786,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 };
@@ -2872,7 +2888,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);
@@ -2881,11 +2897,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 };
@@ -3449,7 +3462,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 {
@@ -3457,10 +3470,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;
@@ -3525,9 +3534,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);
} }
@@ -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 {
@@ -1257,7 +1258,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 +1266,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))
@@ -1599,9 +1596,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 +1719,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));
@@ -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;
@@ -92,6 +92,7 @@ pub enum SelectToolMessage {
remove_from_selection: Key, remove_from_selection: Key,
}, },
EditLayer, EditLayer,
EditLayerExec,
Enter, Enter,
PointerMove(SelectToolPointerKeys), PointerMove(SelectToolPointerKeys),
PointerOutsideViewport(SelectToolPointerKeys), PointerOutsideViewport(SelectToolPointerKeys),
@@ -323,6 +324,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Sele
PointerMove, PointerMove,
Abort, Abort,
EditLayer, EditLayer,
EditLayerExec,
Enter, Enter,
); );
@@ -689,32 +691,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 +983,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
} }
( (
@@ -65,6 +65,8 @@ pub struct TransformLayerMessageHandler {
// Ghost outlines for Path Tool // Ghost outlines for Path Tool
ghost_outline: Vec<(Vec<ClickTargetType>, DAffine2)>, ghost_outline: Vec<(Vec<ClickTargetType>, DAffine2)>,
was_grabbing: bool,
} }
impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for TransformLayerMessageHandler { impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for TransformLayerMessageHandler {
@@ -122,7 +124,7 @@ impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for
self.local_pivot = document.metadata().document_to_viewport.inverse().transform_point2(*selected.pivot); self.local_pivot = document.metadata().document_to_viewport.inverse().transform_point2(*selected.pivot);
self.grab_target = self.local_pivot; self.grab_target = self.local_pivot;
} }
// Here vector data from all layers is not considered which can be a problem in pivot calculation // TODO: Here vector data from all layers is not considered which can be a problem in pivot calculation
else if let Some(vector) = selected_layers.first().and_then(|&layer| document.network_interface.compute_modified_vector(layer)) { else if let Some(vector) = selected_layers.first().and_then(|&layer| document.network_interface.compute_modified_vector(layer)) {
*selected.original_transforms = OriginalTransforms::default(); *selected.original_transforms = OriginalTransforms::default();
@@ -301,6 +303,7 @@ impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for
responses.add(SelectToolMessage::PivotShift { offset: None, flush: true }); responses.add(SelectToolMessage::PivotShift { offset: None, flush: true });
if final_transform { if final_transform {
self.was_grabbing = false;
responses.add(OverlaysMessage::RemoveProvider(TRANSFORM_GRS_OVERLAY_PROVIDER)); responses.add(OverlaysMessage::RemoveProvider(TRANSFORM_GRS_OVERLAY_PROVIDER));
} }
} }
@@ -356,34 +359,64 @@ impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for
if (selected_points.is_empty() && selected_segments.is_empty()) if (selected_points.is_empty() && selected_segments.is_empty())
|| (!using_path_tool && !using_select_tool && !using_pen_tool && !using_shape_tool) || (!using_path_tool && !using_select_tool && !using_pen_tool && !using_shape_tool)
|| selected_layers.is_empty() || selected_layers.is_empty()
|| transform_type.equivalent_to(self.transform_operation) || (transform_type.equivalent_to(self.transform_operation) && !self.was_grabbing)
{ {
return; return;
} }
if using_path_tool && transform_type == TransformType::Grab {
// Check if a single point is selected and it's a colinear point
let single_anchor_selected = shape_editor.selected_points().count() == 1 && shape_editor.selected_points().any(|point| point.as_anchor().is_some());
if single_anchor_selected && transform_type.equivalent_to(self.transform_operation) && self.was_grabbing {
let selected_nodes = &document.network_interface.selected_nodes();
let Some(layer) = selected_nodes.selected_layers(document.metadata()).next() else { return };
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { return };
let Some(anchor) = shape_editor.selected_points().next() else { return };
if vector_data.colinear(*anchor) {
responses.add(TransformLayerMessage::CancelTransformOperation);
// Start sliding point
responses.add(DeferMessage::AfterGraphRun {
messages: vec![PathToolMessage::StartSlidingPoint.into()],
});
}
return;
}
if transform_type.equivalent_to(self.transform_operation) {
return;
}
self.was_grabbing = true;
}
} }
if let Some(vector) = selected_layers.first().and_then(|&layer| document.network_interface.compute_modified_vector(layer)) { if let Some(vector) = selected_layers.first().and_then(|&layer| document.network_interface.compute_modified_vector(layer))
if let [point] = selected_points.as_slice() { && let [point] = selected_points.as_slice()
if matches!(point, ManipulatorPointId::Anchor(_)) { {
if let Some([handle1, handle2]) = point.get_handle_pair(&vector) { if matches!(point, ManipulatorPointId::Anchor(_)) {
let handle1_length = handle1.length(&vector); if let Some([handle1, handle2]) = point.get_handle_pair(&vector) {
let handle2_length = handle2.length(&vector); let handle1_length = handle1.length(&vector);
let handle2_length = handle2.length(&vector);
if (handle1_length == 0. && handle2_length == 0. && !using_select_tool) || (handle1_length == f64::MAX && handle2_length == f64::MAX && !using_select_tool) { if (handle1_length == 0. && handle2_length == 0. && !using_select_tool) || (handle1_length == f64::MAX && handle2_length == f64::MAX && !using_select_tool) {
// G should work for this point but not R and S // G should work for this point but not R and S
if matches!(transform_type, TransformType::Rotate | TransformType::Scale) { if matches!(transform_type, TransformType::Rotate | TransformType::Scale) {
selected.original_transforms.clear(); selected.original_transforms.clear();
return; return;
}
} }
} }
} else { }
let handle_length = point.as_handle().map(|handle| handle.length(&vector)); } else {
let handle_length = point.as_handle().map(|handle| handle.length(&vector));
if handle_length == Some(0.) { if handle_length == Some(0.) {
selected.original_transforms.clear(); selected.original_transforms.clear();
return; return;
}
} }
} }
} }
@@ -409,6 +442,7 @@ impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for
TransformLayerMessage::CancelTransformOperation => { TransformLayerMessage::CancelTransformOperation => {
if using_path_tool { if using_path_tool {
self.ghost_outline.clear(); self.ghost_outline.clear();
self.was_grabbing = false;
} }
if using_pen_tool { if using_pen_tool {
@@ -499,7 +533,7 @@ impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for
if self.typing.digits.is_empty() || !self.transform_operation.can_begin_typing() { if self.typing.digits.is_empty() || !self.transform_operation.can_begin_typing() {
match self.transform_operation { match self.transform_operation {
TransformOperation::None => unreachable!(), TransformOperation::None => {}
TransformOperation::Grabbing(translation) => { TransformOperation::Grabbing(translation) => {
let delta_pos = input.mouse.position - self.mouse_position; let delta_pos = input.mouse.position - self.mouse_position;
let delta_pos = (self.initial_transform * document_to_viewport.inverse()).transform_vector2(delta_pos); let delta_pos = (self.initial_transform * document_to_viewport.inverse()).transform_vector2(delta_pos);
-7
View File
@@ -1,7 +0,0 @@
mod workspace_message;
mod workspace_message_handler;
#[doc(inline)]
pub use workspace_message::{WorkspaceMessage, WorkspaceMessageDiscriminant};
#[doc(inline)]
pub use workspace_message_handler::WorkspaceMessageHandler;
@@ -1,8 +0,0 @@
use crate::messages::prelude::*;
#[impl_message(Message, Workspace)]
#[derive(PartialEq, Eq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum WorkspaceMessage {
// Messages
NodeGraphToggleVisibility,
}
@@ -1,24 +0,0 @@
use crate::messages::prelude::*;
#[derive(Debug, Clone, Default, ExtractField)]
pub struct WorkspaceMessageHandler {
node_graph_visible: bool,
}
#[message_handler_data]
impl MessageHandler<WorkspaceMessage, ()> for WorkspaceMessageHandler {
fn process_message(&mut self, message: WorkspaceMessage, _responses: &mut VecDeque<Message>, _: ()) {
match message {
// Messages
WorkspaceMessage::NodeGraphToggleVisibility => {
self.node_graph_visible = !self.node_graph_visible;
}
}
}
fn actions(&self) -> ActionList {
actions!(WorkspaceMessageDiscriminant;
NodeGraphToggleVisibility,
)
}
}
+48 -88
View File
@@ -8,13 +8,11 @@ use graph_craft::proto::GraphErrors;
use graph_craft::wasm_application_io::EditorPreferences; use graph_craft::wasm_application_io::EditorPreferences;
use graphene_std::application_io::TimingInformation; use graphene_std::application_io::TimingInformation;
use graphene_std::application_io::{NodeGraphUpdateMessage, RenderConfig}; use graphene_std::application_io::{NodeGraphUpdateMessage, RenderConfig};
use graphene_std::renderer::RenderSvgSegmentList;
use graphene_std::renderer::{Render, RenderParams, SvgRender};
use graphene_std::renderer::{RenderMetadata, format_transform_matrix}; use graphene_std::renderer::{RenderMetadata, format_transform_matrix};
use graphene_std::text::FontCache; use graphene_std::text::FontCache;
use graphene_std::transform::Footprint; use graphene_std::transform::Footprint;
use graphene_std::vector::Vector; use graphene_std::vector::Vector;
use graphene_std::vector::style::ViewMode; use graphene_std::wasm_application_io::RenderOutputType;
use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypesDelta; use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypesDelta;
mod runtime_io; mod runtime_io;
@@ -33,7 +31,6 @@ pub struct ExecutionResponse {
execution_id: u64, execution_id: u64,
result: Result<TaggedValue, String>, result: Result<TaggedValue, String>,
responses: VecDeque<FrontendMessage>, responses: VecDeque<FrontendMessage>,
transform: DAffine2,
vector_modify: HashMap<NodeId, Vector>, vector_modify: HashMap<NodeId, Vector>,
/// The resulting value from the temporary inspected during execution /// The resulting value from the temporary inspected during execution
inspect_result: Option<InspectResult>, inspect_result: Option<InspectResult>,
@@ -57,7 +54,7 @@ pub struct NodeGraphExecutor {
current_execution_id: u64, current_execution_id: u64,
futures: HashMap<u64, ExecutionContext>, futures: HashMap<u64, ExecutionContext>,
node_graph_hash: u64, node_graph_hash: u64,
old_inspect_node: Option<NodeId>, previous_node_to_inspect: Option<NodeId>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -79,7 +76,7 @@ impl NodeGraphExecutor {
runtime_io: NodeRuntimeIO::with_channels(request_sender, response_receiver), runtime_io: NodeRuntimeIO::with_channels(request_sender, response_receiver),
node_graph_hash: 0, node_graph_hash: 0,
current_execution_id: 0, current_execution_id: 0,
old_inspect_node: None, previous_node_to_inspect: None,
}; };
(node_runtime, node_executor) (node_runtime, node_executor)
} }
@@ -112,24 +109,25 @@ impl NodeGraphExecutor {
let instrumented = Instrumented::new(&mut network); let instrumented = Instrumented::new(&mut network);
self.runtime_io self.runtime_io
.send(GraphRuntimeRequest::GraphUpdate(GraphUpdate { network, inspect_node: None })) .send(GraphRuntimeRequest::GraphUpdate(GraphUpdate { network, node_to_inspect: None }))
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
Ok(instrumented) Ok(instrumented)
} }
/// Update the cached network if necessary. /// Update the cached network if necessary.
fn update_node_graph(&mut self, document: &mut DocumentMessageHandler, inspect_node: Option<NodeId>, ignore_hash: bool) -> Result<(), String> { fn update_node_graph(&mut self, document: &mut DocumentMessageHandler, node_to_inspect: Option<NodeId>, ignore_hash: bool) -> Result<(), String> {
let network_hash = document.network_interface.document_network().current_hash(); let network_hash = document.network_interface.document_network().current_hash();
// Refresh the graph when it changes or the inspect node changes // Refresh the graph when it changes or the inspect node changes
if network_hash != self.node_graph_hash || self.old_inspect_node != inspect_node || ignore_hash { if network_hash != self.node_graph_hash || self.previous_node_to_inspect != node_to_inspect || ignore_hash {
let network = document.network_interface.document_network().clone(); let network = document.network_interface.document_network().clone();
self.old_inspect_node = inspect_node; self.previous_node_to_inspect = node_to_inspect;
self.node_graph_hash = network_hash; self.node_graph_hash = network_hash;
self.runtime_io self.runtime_io
.send(GraphRuntimeRequest::GraphUpdate(GraphUpdate { network, inspect_node })) .send(GraphRuntimeRequest::GraphUpdate(GraphUpdate { network, node_to_inspect }))
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
} }
Ok(()) Ok(())
} }
@@ -172,10 +170,10 @@ impl NodeGraphExecutor {
document_id: DocumentId, document_id: DocumentId,
viewport_resolution: UVec2, viewport_resolution: UVec2,
time: TimingInformation, time: TimingInformation,
inspect_node: Option<NodeId>, node_to_inspect: Option<NodeId>,
ignore_hash: bool, ignore_hash: bool,
) -> Result<Message, String> { ) -> Result<Message, String> {
self.update_node_graph(document, inspect_node, ignore_hash)?; self.update_node_graph(document, node_to_inspect, ignore_hash)?;
self.submit_current_node_graph_evaluation(document, document_id, viewport_resolution, time) self.submit_current_node_graph_evaluation(document, document_id, viewport_resolution, time)
} }
@@ -209,7 +207,7 @@ impl NodeGraphExecutor {
// Execute the node graph // Execute the node graph
self.runtime_io self.runtime_io
.send(GraphRuntimeRequest::GraphUpdate(GraphUpdate { network, inspect_node: None })) .send(GraphRuntimeRequest::GraphUpdate(GraphUpdate { network, node_to_inspect: None }))
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
let execution_id = self.queue_execution(render_config); let execution_id = self.queue_execution(render_config);
let execution_context = ExecutionContext { let execution_context = ExecutionContext {
@@ -223,7 +221,7 @@ impl NodeGraphExecutor {
fn export(&self, node_graph_output: TaggedValue, export_config: ExportConfig, responses: &mut VecDeque<Message>) -> Result<(), String> { fn export(&self, node_graph_output: TaggedValue, export_config: ExportConfig, responses: &mut VecDeque<Message>) -> Result<(), String> {
let TaggedValue::RenderOutput(RenderOutput { let TaggedValue::RenderOutput(RenderOutput {
data: graphene_std::wasm_application_io::RenderOutputType::Svg { svg, .. }, data: RenderOutputType::Svg { svg, .. },
.. ..
}) = node_graph_output }) = node_graph_output
else { else {
@@ -251,6 +249,7 @@ impl NodeGraphExecutor {
let size = (size * scale_factor).into(); let size = (size * scale_factor).into();
responses.add(FrontendMessage::TriggerExportImage { svg, name, mime, size }); responses.add(FrontendMessage::TriggerExportImage { svg, name, mime, size });
} }
Ok(()) Ok(())
} }
@@ -263,7 +262,6 @@ impl NodeGraphExecutor {
execution_id, execution_id,
result, result,
responses: existing_responses, responses: existing_responses,
transform,
vector_modify, vector_modify,
inspect_result, inspect_result,
} = execution_response; } = execution_response;
@@ -286,17 +284,17 @@ impl NodeGraphExecutor {
let execution_context = self.futures.remove(&execution_id).ok_or_else(|| "Invalid generation ID".to_string())?; let execution_context = self.futures.remove(&execution_id).ok_or_else(|| "Invalid generation ID".to_string())?;
if let Some(export_config) = execution_context.export_config { if let Some(export_config) = execution_context.export_config {
// Special handling for exporting the artwork // Special handling for exporting the artwork
self.export(node_graph_output, export_config, responses)? self.export(node_graph_output, export_config, responses)?;
} else { } else {
self.process_node_graph_output(node_graph_output, transform, responses)? self.process_node_graph_output(node_graph_output, responses)?;
} }
responses.add_front(DeferMessage::TriggerGraphRun(execution_id, execution_context.document_id)); responses.add(DeferMessage::TriggerGraphRun(execution_id, execution_context.document_id));
// Update the spreadsheet on the frontend using the value of the inspect result. // Update the Data panel on the frontend using the value of the inspect result.
if self.old_inspect_node.is_some() { if let Some(inspect_result) = (self.previous_node_to_inspect.is_some()).then_some(inspect_result).flatten() {
if let Some(inspect_result) = inspect_result { responses.add(DataPanelMessage::UpdateLayout { inspect_result });
responses.add(SpreadsheetMessage::UpdateLayout { inspect_result }); } else {
} responses.add(DataPanelMessage::ClearLayout);
} }
} }
NodeGraphUpdate::CompilationResponse(execution_response) => { NodeGraphUpdate::CompilationResponse(execution_response) => {
@@ -329,84 +327,45 @@ impl NodeGraphExecutor {
} }
} }
} }
Ok(()) Ok(())
} }
fn debug_render(render_object: impl Render, transform: DAffine2, responses: &mut VecDeque<Message>) { fn process_node_graph_output(&mut self, node_graph_output: TaggedValue, responses: &mut VecDeque<Message>) -> Result<(), String> {
// Setup rendering let TaggedValue::RenderOutput(render_output) = node_graph_output else {
let mut render = SvgRender::new(); return Err(format!("Invalid node graph output type: {node_graph_output:#?}"));
let render_params = RenderParams {
view_mode: ViewMode::Normal,
culling_bounds: None,
thumbnail: false,
hide_artboards: false,
for_export: false,
for_mask: false,
alignment_parent_transform: None,
}; };
// Render SVG match render_output.data {
render_object.render_svg(&mut render, &render_params); RenderOutputType::Svg { svg, image_data } => {
// Send to frontend
// Concatenate the defs and the SVG into one string responses.add(FrontendMessage::UpdateImageData { image_data });
render.wrap_with_transform(transform, None); responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
let svg = render.svg.to_svg_string();
// Send to frontend
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
}
fn process_node_graph_output(&mut self, node_graph_output: TaggedValue, transform: DAffine2, responses: &mut VecDeque<Message>) -> Result<(), String> {
let mut render_output_metadata = RenderMetadata::default();
match node_graph_output {
TaggedValue::RenderOutput(render_output) => {
match render_output.data {
graphene_std::wasm_application_io::RenderOutputType::Svg { svg, image_data } => {
// Send to frontend
responses.add(FrontendMessage::UpdateImageData { image_data });
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
}
graphene_std::wasm_application_io::RenderOutputType::CanvasFrame(frame) => {
let matrix = format_transform_matrix(frame.transform);
let transform = if matrix.is_empty() { String::new() } else { format!(" transform=\"{matrix}\"") };
let svg = format!(
r#"<svg><foreignObject width="{}" height="{}"{transform}><div data-canvas-placeholder="{}"></div></foreignObject></svg>"#,
frame.resolution.x, frame.resolution.y, frame.surface_id.0
);
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
}
graphene_std::wasm_application_io::RenderOutputType::Texture { .. } => {}
_ => {
return Err(format!("Invalid node graph output type: {:#?}", render_output.data));
}
}
render_output_metadata = render_output.metadata;
} }
TaggedValue::Bool(render_object) => Self::debug_render(render_object, transform, responses), RenderOutputType::CanvasFrame(frame) => {
TaggedValue::String(render_object) => Self::debug_render(render_object, transform, responses), let matrix = format_transform_matrix(frame.transform);
TaggedValue::F64(render_object) => Self::debug_render(render_object, transform, responses), let transform = if matrix.is_empty() { String::new() } else { format!(" transform=\"{matrix}\"") };
TaggedValue::DVec2(render_object) => Self::debug_render(render_object, transform, responses), let svg = format!(
TaggedValue::OptionalColor(render_object) => Self::debug_render(render_object, transform, responses), r#"<svg><foreignObject width="{}" height="{}"{transform}><div data-canvas-placeholder="{}"></div></foreignObject></svg>"#,
TaggedValue::Vector(render_object) => Self::debug_render(render_object, transform, responses), frame.resolution.x, frame.resolution.y, frame.surface_id.0
TaggedValue::Graphic(render_object) => Self::debug_render(render_object, transform, responses), );
TaggedValue::Raster(render_object) => Self::debug_render(render_object, transform, responses), responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
TaggedValue::Palette(render_object) => Self::debug_render(render_object, transform, responses),
_ => {
return Err(format!("Invalid node graph output type: {node_graph_output:#?}"));
} }
}; RenderOutputType::Texture { .. } => {}
let graphene_std::renderer::RenderMetadata { _ => return Err(format!("Invalid node graph output type: {:#?}", render_output.data)),
upstream_footprints: footprints, }
let RenderMetadata {
upstream_footprints,
local_transforms, local_transforms,
first_element_source_id, first_element_source_id,
click_targets, click_targets,
clip_targets, clip_targets,
} = render_output_metadata; } = render_output.metadata;
// Run these update state messages immediately // Run these update state messages immediately
responses.add(DocumentMessage::UpdateUpstreamTransforms { responses.add(DocumentMessage::UpdateUpstreamTransforms {
upstream_footprints: footprints, upstream_footprints,
local_transforms, local_transforms,
first_element_source_id, first_element_source_id,
}); });
@@ -415,6 +374,7 @@ impl NodeGraphExecutor {
responses.add(DocumentMessage::RenderScrollbars); responses.add(DocumentMessage::RenderScrollbars);
responses.add(DocumentMessage::RenderRulers); responses.add(DocumentMessage::RenderRulers);
responses.add(OverlaysMessage::Draw); responses.add(OverlaysMessage::Draw);
Ok(()) Ok(())
} }
} }
+46 -45
View File
@@ -8,13 +8,14 @@ use graph_craft::proto::GraphErrors;
use graph_craft::wasm_application_io::EditorPreferences; use graph_craft::wasm_application_io::EditorPreferences;
use graph_craft::{ProtoNodeIdentifier, concrete}; use graph_craft::{ProtoNodeIdentifier, concrete};
use graphene_std::application_io::{ImageTexture, NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig}; use graphene_std::application_io::{ImageTexture, NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig};
use graphene_std::bounds::RenderBoundingBox;
use graphene_std::memo::IORecord; use graphene_std::memo::IORecord;
use graphene_std::renderer::{Render, RenderParams, SvgRender}; use graphene_std::renderer::{Render, RenderParams, SvgRender};
use graphene_std::renderer::{RenderSvgSegmentList, SvgSegment}; use graphene_std::renderer::{RenderSvgSegmentList, SvgSegment};
use graphene_std::table::{Table, TableRow}; use graphene_std::table::{Table, TableRow};
use graphene_std::text::FontCache; use graphene_std::text::FontCache;
use graphene_std::transform::RenderQuality;
use graphene_std::vector::Vector; use graphene_std::vector::Vector;
use graphene_std::vector::style::ViewMode;
use graphene_std::wasm_application_io::{RenderOutputType, WasmApplicationIo, WasmEditorApi}; use graphene_std::wasm_application_io::{RenderOutputType, WasmApplicationIo, WasmEditorApi};
use graphene_std::{Artboard, Context, Graphic}; use graphene_std::{Artboard, Context, Graphic};
use interpreted_executor::dynamic_executor::{DynamicExecutor, IntrospectError, ResolvedDocumentNodeTypesDelta}; use interpreted_executor::dynamic_executor::{DynamicExecutor, IntrospectError, ResolvedDocumentNodeTypesDelta};
@@ -42,7 +43,7 @@ pub struct NodeRuntime {
node_graph_errors: GraphErrors, node_graph_errors: GraphErrors,
monitor_nodes: Vec<Vec<NodeId>>, monitor_nodes: Vec<Vec<NodeId>>,
/// Which node is inspected and which monitor node is used (if any) for the current execution /// Which node is inspected and which monitor node is used (if any) for the current execution.
inspect_state: Option<InspectState>, inspect_state: Option<InspectState>,
/// Mapping of the fully-qualified node paths to their preprocessor substitutions. /// Mapping of the fully-qualified node paths to their preprocessor substitutions.
@@ -67,7 +68,7 @@ pub enum GraphRuntimeRequest {
pub struct GraphUpdate { pub struct GraphUpdate {
pub(super) network: NodeNetwork, pub(super) network: NodeNetwork,
/// The node that should be temporary inspected during execution /// The node that should be temporary inspected during execution
pub(super) inspect_node: Option<NodeId>, pub(super) node_to_inspect: Option<NodeId>,
} }
#[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize)]
@@ -188,22 +189,21 @@ impl NodeRuntime {
let _ = self.update_network(graph).await; let _ = self.update_network(graph).await;
} }
} }
GraphRuntimeRequest::GraphUpdate(GraphUpdate { mut network, inspect_node }) => { GraphRuntimeRequest::GraphUpdate(GraphUpdate { mut network, node_to_inspect }) => {
// Insert the monitor node to manage the inspection // Insert the monitor node to manage the inspection
self.inspect_state = inspect_node.map(|inspect| InspectState::monitor_inspect_node(&mut network, inspect)); self.inspect_state = node_to_inspect.map(|inspect| InspectState::monitor_inspect_node(&mut network, inspect));
self.old_graph = Some(network.clone()); self.old_graph = Some(network.clone());
self.node_graph_errors.clear(); self.node_graph_errors.clear();
let result = self.update_network(network).await; let result = self.update_network(network).await;
let node_graph_errors = self.node_graph_errors.clone();
self.update_thumbnails = true; self.update_thumbnails = true;
self.sender.send_generation_response(CompilationResponse {
result, self.sender.send_generation_response(CompilationResponse { result, node_graph_errors });
node_graph_errors: self.node_graph_errors.clone(),
});
} }
GraphRuntimeRequest::ExecutionRequest(ExecutionRequest { execution_id, render_config, .. }) => { GraphRuntimeRequest::ExecutionRequest(ExecutionRequest { execution_id, render_config, .. }) => {
let transform = render_config.viewport.transform;
let result = self.execute_network(render_config).await; let result = self.execute_network(render_config).await;
let mut responses = VecDeque::new(); let mut responses = VecDeque::new();
// TODO: Only process monitor nodes if the graph has changed, not when only the Footprint changes // TODO: Only process monitor nodes if the graph has changed, not when only the Footprint changes
@@ -227,7 +227,6 @@ impl NodeRuntime {
execution_id, execution_id,
result, result,
responses, responses,
transform,
vector_modify: self.vector_modify.clone(), vector_modify: self.vector_modify.clone(),
inspect_result, inspect_result,
}); });
@@ -292,51 +291,49 @@ impl NodeRuntime {
if self.inspect_state.is_some_and(|inspect_state| monitor_node_path.last().copied() == Some(inspect_state.monitor_node)) { if self.inspect_state.is_some_and(|inspect_state| monitor_node_path.last().copied() == Some(inspect_state.monitor_node)) {
continue; continue;
} }
// The monitor nodes are located within a document node, and are thus children in that network, so this gets the parent document node's ID // The monitor nodes are located within a document node, and are thus children in that network, so this gets the parent document node's ID
let Some(parent_network_node_id) = monitor_node_path.len().checked_sub(2).and_then(|index| monitor_node_path.get(index)).copied() else { let Some(parent_network_node_id) = monitor_node_path.len().checked_sub(2).and_then(|index| monitor_node_path.get(index)).copied() else {
warn!("Monitor node has invalid node id"); warn!("Monitor node has invalid node id");
continue; continue;
}; };
// Extract the monitor node's stored `Graphic` data. // Extract the monitor node's stored `Graphic` data
let Ok(introspected_data) = self.executor.introspect(monitor_node_path) else { let Ok(introspected_data) = self.executor.introspect(monitor_node_path) else {
// TODO: Fix the root of the issue causing the spam of this warning (this at least temporarily disables it in release builds) // TODO: Fix the root of the issue causing the spam of this warning (this at least temporarily disables it in release builds)
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
warn!("Failed to introspect monitor node {}", self.executor.introspect(monitor_node_path).unwrap_err()); warn!("Failed to introspect monitor node {}", self.executor.introspect(monitor_node_path).unwrap_err());
continue; continue;
}; };
// Graphic table: thumbnail
if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, Table<Graphic>>>() { if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, Table<Graphic>>>() {
Self::process_graphic(&mut self.thumbnail_renders, parent_network_node_id, &io.output, responses, update_thumbnails) if update_thumbnails {
} else if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, Table<Artboard>>>() { Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &io.output, responses)
Self::process_graphic(&mut self.thumbnail_renders, parent_network_node_id, &io.output, responses, update_thumbnails) }
// Insert the vector modify if we are dealing with vector data }
} else if let Some(record) = introspected_data.downcast_ref::<IORecord<Context, Table<Vector>>>() { // Artboard table: thumbnail
else if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, Table<Artboard>>>() {
if update_thumbnails {
Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &io.output, responses)
}
}
// Vector table: vector modifications
else if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, Table<Vector>>>() {
// Insert the vector modify
let default = TableRow::default(); let default = TableRow::default();
self.vector_modify self.vector_modify
.insert(parent_network_node_id, record.output.iter().next().unwrap_or_else(|| default.as_ref()).element.clone()); .insert(parent_network_node_id, io.output.iter().next().unwrap_or_else(|| default.as_ref()).element.clone());
} else { }
// Other
else {
log::warn!("Failed to downcast monitor node output {parent_network_node_id:?}"); log::warn!("Failed to downcast monitor node output {parent_network_node_id:?}");
} }
} }
} }
// If this is `Graphic` data, regenerate click targets and thumbnails for the layers in the graph, modifying the state and updating the UI. /// If this is `Graphic` data, regenerate click targets and thumbnails for the layers in the graph, modifying the state and updating the UI.
fn process_graphic( fn render_thumbnail(thumbnail_renders: &mut HashMap<NodeId, Vec<SvgSegment>>, parent_network_node_id: NodeId, graphic: &impl Render, responses: &mut VecDeque<FrontendMessage>) {
thumbnail_renders: &mut HashMap<NodeId, Vec<SvgSegment>>,
parent_network_node_id: NodeId,
graphic: &impl Render,
responses: &mut VecDeque<FrontendMessage>,
update_thumbnails: bool,
) {
// RENDER THUMBNAIL
if !update_thumbnails {
return;
}
// Skip thumbnails if the layer is too complex (for performance) // Skip thumbnails if the layer is too complex (for performance)
if graphic.render_complexity() > 1000 { if graphic.render_complexity() > 1000 {
let old = thumbnail_renders.insert(parent_network_node_id, Vec::new()); let old = thumbnail_renders.insert(parent_network_node_id, Vec::new());
@@ -349,24 +346,28 @@ impl NodeRuntime {
return; return;
} }
let bounds = graphic.bounding_box(DAffine2::IDENTITY, true); let bounds = match graphic.bounding_box(DAffine2::IDENTITY, true) {
RenderBoundingBox::None => return,
RenderBoundingBox::Infinite => [DVec2::ZERO, DVec2::new(300., 200.)],
RenderBoundingBox::Rectangle(bounds) => bounds,
};
let footprint = Footprint {
transform: DAffine2::from_translation(DVec2::new(bounds[0].x, bounds[0].y)),
resolution: UVec2::new((bounds[1].x - bounds[0].x).abs() as u32, (bounds[1].y - bounds[0].y).abs() as u32),
quality: RenderQuality::Full,
};
// Render the thumbnail from a `Graphic` into an SVG string // Render the thumbnail from a `Graphic` into an SVG string
let render_params = RenderParams { let render_params = RenderParams {
view_mode: ViewMode::Normal, footprint,
culling_bounds: bounds,
thumbnail: true, thumbnail: true,
hide_artboards: false, ..Default::default()
for_export: false,
for_mask: false,
alignment_parent_transform: None,
}; };
let mut render = SvgRender::new(); let mut render = SvgRender::new();
graphic.render_svg(&mut render, &render_params); graphic.render_svg(&mut render, &render_params);
// And give the SVG a viewbox and outer <svg>...</svg> wrapper tag // And give the SVG a viewbox and outer <svg>...</svg> wrapper tag
let [min, max] = bounds.unwrap_or_default(); render.format_svg(bounds[0], bounds[1]);
render.format_svg(min, max);
// UPDATE FRONTEND THUMBNAIL // UPDATE FRONTEND THUMBNAIL
-14
View File
@@ -82,18 +82,4 @@ impl DebugMessageTree {
pub fn message_handler_fields(&self) -> Option<&MessageData> { pub fn message_handler_fields(&self) -> Option<&MessageData> {
self.message_handler.as_ref() self.message_handler.as_ref()
} }
pub fn has_message_handler_data_fields(&self) -> bool {
match self.message_handler_data_fields() {
Some(_) => true,
None => false,
}
}
pub fn has_message_handler_fields(&self) -> bool {
match self.message_handler_fields() {
Some(_) => true,
None => false,
}
}
} }
+14 -8
View File
@@ -113,16 +113,22 @@
--color-data-general: #cfcfcf; --color-data-general: #cfcfcf;
--color-data-general-dim: #8a8a8a; --color-data-general-dim: #8a8a8a;
--color-data-raster: #e4bb72;
--color-data-raster-dim: #8b7752;
--color-data-vector: #65bbe5;
--color-data-vector-dim: #4b778c;
--color-data-graphic: #66b195;
--color-data-graphic-dim: #3d725e;
--color-data-artboard: #fbf9eb;
--color-data-artboard-dim: #b9b9a9;
--color-data-number: #c9a699; --color-data-number: #c9a699;
--color-data-number-dim: #886b60; --color-data-number-dim: #886b60;
--color-data-artboard: #fbf9eb;
--color-data-artboard-dim: #b9b9a9;
--color-data-graphic: #68c587;
--color-data-graphic-dim: #37754c;
--color-data-raster: #e4bb72;
--color-data-raster-dim: #9a7b43;
--color-data-vector: #65bbe5;
--color-data-vector-dim: #417892;
--color-data-color: #ce6ea7;
--color-data-color-dim: #924071;
--color-data-gradient: #af81eb;
--color-data-gradient-dim: #6c489b;
--color-data-typography: #eea7a7;
--color-data-typography-dim: #955252;
--color-none: white; --color-none: white;
--color-none-repeat: no-repeat; --color-none-repeat: no-repeat;
@@ -39,6 +39,7 @@
export let colorOrGradient: FillChoice; export let colorOrGradient: FillChoice;
export let allowNone = false; export let allowNone = false;
// export let allowTransparency = false; // TODO: Implement // export let allowTransparency = false; // TODO: Implement
export let disabled = false;
export let direction: MenuDirection = "Bottom"; export let direction: MenuDirection = "Bottom";
// TODO: See if this should be made to follow the pattern of DropdownInput.svelte so this could be removed // TODO: See if this should be made to follow the pattern of DropdownInput.svelte so this could be removed
export let open: boolean; export let open: boolean;
@@ -133,6 +134,8 @@
} }
function onPointerDown(e: PointerEvent) { function onPointerDown(e: PointerEvent) {
if (disabled) return;
const target = (e.target || undefined) as HTMLElement | undefined; const target = (e.target || undefined) as HTMLElement | undefined;
draggingPickerTrack = target?.closest("[data-saturation-value-picker], [data-hue-picker], [data-alpha-picker]") || undefined; draggingPickerTrack = target?.closest("[data-saturation-value-picker], [data-hue-picker], [data-alpha-picker]") || undefined;
@@ -403,7 +406,7 @@
}); });
</script> </script>
<FloatingMenu class="color-picker" {open} on:open {strayCloses} escapeCloses={strayCloses && !gradientSpectrumDragging} {direction} type="Popover" bind:this={self}> <FloatingMenu class="color-picker" classes={{ disabled }} {open} on:open {strayCloses} escapeCloses={strayCloses && !gradientSpectrumDragging} {direction} type="Popover" bind:this={self}>
<LayoutRow <LayoutRow
styles={{ styles={{
"--new-color": newColor.toHexOptionalAlpha(), "--new-color": newColor.toHexOptionalAlpha(),
@@ -418,7 +421,7 @@
> >
<LayoutCol class="pickers-and-gradient"> <LayoutCol class="pickers-and-gradient">
<LayoutRow class="pickers"> <LayoutRow class="pickers">
<LayoutCol class="saturation-value-picker" on:pointerdown={onPointerDown} data-saturation-value-picker> <LayoutCol class="saturation-value-picker" title={disabled ? "Saturation and value (disabled)" : "Saturation and value"} on:pointerdown={onPointerDown} data-saturation-value-picker>
{#if !isNone} {#if !isNone}
<div class="selection-circle" style:top={`${(1 - value) * 100}%`} style:left={`${saturation * 100}%`} /> <div class="selection-circle" style:top={`${(1 - value) * 100}%`} style:left={`${saturation * 100}%`} />
{/if} {/if}
@@ -432,12 +435,12 @@
/> />
{/if} {/if}
</LayoutCol> </LayoutCol>
<LayoutCol class="hue-picker" on:pointerdown={onPointerDown} data-hue-picker> <LayoutCol class="hue-picker" title={disabled ? "Hue (disabled)" : "Hue"} on:pointerdown={onPointerDown} data-hue-picker>
{#if !isNone} {#if !isNone}
<div class="selection-needle" style:top={`${(1 - hue) * 100}%`} /> <div class="selection-needle" style:top={`${(1 - hue) * 100}%`} />
{/if} {/if}
</LayoutCol> </LayoutCol>
<LayoutCol class="alpha-picker" on:pointerdown={onPointerDown} data-alpha-picker> <LayoutCol class="alpha-picker" title={disabled ? "Alpha (disabled)" : "Alpha"} on:pointerdown={onPointerDown} data-alpha-picker>
{#if !isNone} {#if !isNone}
<div class="selection-needle" style:top={`${(1 - alpha) * 100}%`} /> <div class="selection-needle" style:top={`${(1 - alpha) * 100}%`} />
{/if} {/if}
@@ -447,6 +450,7 @@
<LayoutRow class="gradient"> <LayoutRow class="gradient">
<SpectrumInput <SpectrumInput
{gradient} {gradient}
{disabled}
on:gradient={() => { on:gradient={() => {
gradient = gradient; gradient = gradient;
if (gradient) dispatch("colorOrGradient", gradient); if (gradient) dispatch("colorOrGradient", gradient);
@@ -459,6 +463,7 @@
{#if gradientSpectrumInputWidget && activeIndex !== undefined} {#if gradientSpectrumInputWidget && activeIndex !== undefined}
<NumberInput <NumberInput
value={(gradient.positionAtIndex(activeIndex) || 0) * 100} value={(gradient.positionAtIndex(activeIndex) || 0) * 100}
{disabled}
on:value={({ detail }) => { on:value={({ detail }) => {
if (gradientSpectrumInputWidget && activeIndex !== undefined && detail !== undefined) gradientSpectrumInputWidget.setPosition(activeIndex, detail / 100); if (gradientSpectrumInputWidget && activeIndex !== undefined && detail !== undefined) gradientSpectrumInputWidget.setPosition(activeIndex, detail / 100);
}} }}
@@ -478,7 +483,7 @@
styles={{ "--outline-amount": outlineFactor }} styles={{ "--outline-amount": outlineFactor }}
tooltip={!newColor.equals(oldColor) ? "Comparison between the present color choice (left) and the color before any change was made (right)" : "The present color choice"} tooltip={!newColor.equals(oldColor) ? "Comparison between the present color choice (left) and the color before any change was made (right)" : "The present color choice"}
> >
{#if !newColor.equals(oldColor)} {#if !newColor.equals(oldColor) && !disabled}
<div class="swap-button-background"></div> <div class="swap-button-background"></div>
<IconButton class="swap-button" icon="SwapHorizontal" size={16} action={swapNewWithOld} tooltip="Swap" /> <IconButton class="swap-button" icon="SwapHorizontal" size={16} action={swapNewWithOld} tooltip="Swap" />
{/if} {/if}
@@ -500,6 +505,7 @@
<LayoutRow> <LayoutRow>
<TextInput <TextInput
value={newColor.toHexOptionalAlpha() || "-"} value={newColor.toHexOptionalAlpha() || "-"}
{disabled}
on:commitText={({ detail }) => { on:commitText={({ detail }) => {
dispatch("startHistoryTransaction"); dispatch("startHistoryTransaction");
setColorCode(detail); setColorCode(detail);
@@ -520,6 +526,7 @@
{/if} {/if}
<NumberInput <NumberInput
value={strength} value={strength}
{disabled}
on:value={({ detail }) => { on:value={({ detail }) => {
strength = detail; strength = detail;
setColorRGB(channel, detail); setColorRGB(channel, detail);
@@ -547,6 +554,7 @@
{/if} {/if}
<NumberInput <NumberInput
value={strength} value={strength}
{disabled}
on:value={({ detail }) => { on:value={({ detail }) => {
strength = detail; strength = detail;
setColorHSV(channel, detail); setColorHSV(channel, detail);
@@ -573,6 +581,7 @@
<Separator type="Related" /> <Separator type="Related" />
<NumberInput <NumberInput
value={!isNone ? alpha * 100 : undefined} value={!isNone ? alpha * 100 : undefined}
{disabled}
on:value={({ detail }) => { on:value={({ detail }) => {
if (detail !== undefined) alpha = detail / 100; if (detail !== undefined) alpha = detail / 100;
setColorAlphaPercent(detail); setColorAlphaPercent(detail);
@@ -593,14 +602,14 @@
<LayoutRow class="leftover-space" /> <LayoutRow class="leftover-space" />
<LayoutRow> <LayoutRow>
{#if allowNone && !gradient} {#if allowNone && !gradient}
<button class="preset-color none" on:click={() => setColorPreset("none")} title="Set to no color" tabindex="0"></button> <button class="preset-color none" {disabled} on:click={() => setColorPreset("none")} title="Set to no color" tabindex="0"></button>
<Separator type="Related" /> <Separator type="Related" />
{/if} {/if}
<button class="preset-color black" on:click={() => setColorPreset("black")} title="Set to black" tabindex="0"></button> <button class="preset-color black" {disabled} on:click={() => setColorPreset("black")} title="Set to black" tabindex="0"></button>
<Separator type="Related" /> <Separator type="Related" />
<button class="preset-color white" on:click={() => setColorPreset("white")} title="Set to white" tabindex="0"></button> <button class="preset-color white" {disabled} on:click={() => setColorPreset("white")} title="Set to white" tabindex="0"></button>
<Separator type="Related" /> <Separator type="Related" />
<button class="preset-color pure" on:click={setColorPresetSubtile} tabindex="-1"> <button class="preset-color pure" {disabled} on:click={setColorPresetSubtile} tabindex="-1">
<div data-pure-tile="red" style="--pure-color: #ff0000; --pure-color-gray: #4c4c4c" title="Set to red" /> <div data-pure-tile="red" style="--pure-color: #ff0000; --pure-color-gray: #4c4c4c" title="Set to red" />
<div data-pure-tile="yellow" style="--pure-color: #ffff00; --pure-color-gray: #e3e3e3" title="Set to yellow" /> <div data-pure-tile="yellow" style="--pure-color: #ffff00; --pure-color-gray: #e3e3e3" title="Set to yellow" />
<div data-pure-tile="green" style="--pure-color: #00ff00; --pure-color-gray: #969696" title="Set to green" /> <div data-pure-tile="green" style="--pure-color: #00ff00; --pure-color-gray: #969696" title="Set to green" />
@@ -609,7 +618,7 @@
<div data-pure-tile="magenta" style="--pure-color: #ff00ff; --pure-color-gray: #696969" title="Set to magenta" /> <div data-pure-tile="magenta" style="--pure-color: #ff00ff; --pure-color-gray: #696969" title="Set to magenta" />
</button> </button>
<Separator type="Related" /> <Separator type="Related" />
<IconButton icon="Eyedropper" size={24} action={activateEyedropperSample} tooltip="Sample a pixel color from the document" /> <IconButton icon="Eyedropper" size={24} {disabled} action={activateEyedropperSample} tooltip="Sample a pixel color from the document" />
</LayoutRow> </LayoutRow>
</LayoutCol> </LayoutCol>
</LayoutRow> </LayoutRow>
@@ -954,7 +963,7 @@
// For the least jarring luminance conversion, these colors are derived by placing a black layer with the "desaturate" blend mode over the colors. // For the least jarring luminance conversion, these colors are derived by placing a black layer with the "desaturate" blend mode over the colors.
// We don't use the CSS `filter: grayscale(1);` property because it produces overly dark tones for bright colors with a noticeable jump on hover. // We don't use the CSS `filter: grayscale(1);` property because it produces overly dark tones for bright colors with a noticeable jump on hover.
background: var(--pure-color-gray); background: var(--pure-color-gray);
transition: background-color 0.2s ease; transition: background-color 0.1s;
} }
&:hover div { &:hover div {
@@ -963,5 +972,21 @@
} }
} }
} }
&.disabled .pickers-and-gradient .pickers :is(.saturation-value-picker, .hue-picker, .alpha-picker),
&.disabled .details .preset-color,
&.disabled .details .choice-preview {
transition: opacity 0.1s;
&:hover {
opacity: 0.5;
}
}
&.disabled .details .preset-color.pure:hover div {
background: var(--pure-color-gray);
}
} }
// paddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpadding
</style> </style>
@@ -0,0 +1,50 @@
<script lang="ts">
import { getContext, onMount, onDestroy } from "svelte";
import type { Editor } from "@graphite/editor";
import { defaultWidgetLayout, patchWidgetLayout, UpdateDataPanelLayout } from "@graphite/messages";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
const editor = getContext<Editor>("editor");
let dataPanelLayout = defaultWidgetLayout();
onMount(() => {
editor.subscriptions.subscribeJsMessage(UpdateDataPanelLayout, (updateDataPanelLayout) => {
patchWidgetLayout(dataPanelLayout, updateDataPanelLayout);
dataPanelLayout = dataPanelLayout;
});
});
onDestroy(() => {
editor.subscriptions.unsubscribeJsMessage(UpdateDataPanelLayout);
});
</script>
<LayoutCol class="data-panel">
<LayoutCol class="body" scrollableY={true}>
<WidgetLayout layout={dataPanelLayout} />
</LayoutCol>
</LayoutCol>
<style lang="scss" global>
.data-panel {
flex-grow: 1;
padding: 4px;
table {
margin: -4px;
width: calc(100% + 2 * 4px);
.text-label {
white-space: wrap;
}
&:not(:first-child) {
margin-top: 0;
}
}
}
</style>
@@ -675,7 +675,7 @@
&[title^="Coming Soon"] { &[title^="Coming Soon"] {
opacity: 0.25; opacity: 0.25;
transition: opacity 0.2s; transition: opacity 0.1s;
&:hover { &:hover {
opacity: 1; opacity: 1;
@@ -825,7 +825,7 @@
.graph-view { .graph-view {
pointer-events: none; pointer-events: none;
transition: opacity 0.2s ease-in-out; transition: opacity 0.2s;
opacity: 0; opacity: 0;
&.open { &.open {
+20 -4
View File
@@ -98,6 +98,12 @@
}); });
onDestroy(() => { onDestroy(() => {
editor.subscriptions.unsubscribeJsMessage(UpdateLayersPanelControlBarLeftLayout);
editor.subscriptions.unsubscribeJsMessage(UpdateLayersPanelControlBarRightLayout);
editor.subscriptions.unsubscribeJsMessage(UpdateLayersPanelBottomBarLayout);
editor.subscriptions.unsubscribeJsMessage(UpdateDocumentLayerStructureJs);
editor.subscriptions.unsubscribeJsMessage(UpdateDocumentLayerDetails);
removeEventListener("pointermove", clippingHover); removeEventListener("pointermove", clippingHover);
removeEventListener("keydown", clippingKeyPress); removeEventListener("keydown", clippingKeyPress);
removeEventListener("keyup", clippingKeyPress); removeEventListener("keyup", clippingKeyPress);
@@ -489,7 +495,9 @@
<LayoutCol class="layers" on:dragleave={() => (dragInPanel = false)}> <LayoutCol class="layers" on:dragleave={() => (dragInPanel = false)}>
<LayoutRow class="control-bar" scrollableX={true}> <LayoutRow class="control-bar" scrollableX={true}>
<WidgetLayout layout={layersPanelControlBarLeftLayout} /> <WidgetLayout layout={layersPanelControlBarLeftLayout} />
<Separator /> {#if layersPanelControlBarLeftLayout?.layout?.length > 0 && layersPanelControlBarRightLayout?.layout?.length > 0}
<Separator />
{/if}
<WidgetLayout layout={layersPanelControlBarRightLayout} /> <WidgetLayout layout={layersPanelControlBarRightLayout} />
</LayoutRow> </LayoutRow>
<LayoutRow class="list-area" scrollableY={true}> <LayoutRow class="list-area" scrollableY={true}>
@@ -605,6 +613,10 @@
.widget-span:first-child { .widget-span:first-child {
flex: 1 1 auto; flex: 1 1 auto;
} }
&:not(:has(*)) {
display: none;
}
} }
// Bottom bar // Bottom bar
@@ -619,6 +631,10 @@
.widget-span > * { .widget-span > * {
margin: 0; margin: 0;
} }
&:not(:has(*)) {
display: none;
}
} }
// Layer hierarchy // Layer hierarchy
@@ -714,6 +730,7 @@
width: 36px; width: 36px;
height: 24px; height: 24px;
border-radius: 2px; border-radius: 2px;
overflow: hidden;
flex: 0 0 auto; flex: 0 0 auto;
background-image: var(--color-transparent-checkered-background); background-image: var(--color-transparent-checkered-background);
background-size: var(--color-transparent-checkered-background-size-mini); background-size: var(--color-transparent-checkered-background-size-mini);
@@ -721,9 +738,8 @@
background-repeat: var(--color-transparent-checkered-background-repeat); background-repeat: var(--color-transparent-checkered-background-repeat);
svg { svg {
width: calc(100% - 4px); width: 100%;
height: calc(100% - 4px); height: 100%;
margin: 2px;
} }
} }
@@ -1,27 +1,31 @@
<script lang="ts"> <script lang="ts">
import { getContext, onMount } from "svelte"; import { getContext, onMount, onDestroy } from "svelte";
import type { Editor } from "@graphite/editor"; import type { Editor } from "@graphite/editor";
import { defaultWidgetLayout, patchWidgetLayout, UpdatePropertyPanelSectionsLayout } from "@graphite/messages"; import { defaultWidgetLayout, patchWidgetLayout, UpdatePropertiesPanelLayout } from "@graphite/messages";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte"; import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte"; import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
const editor = getContext<Editor>("editor"); const editor = getContext<Editor>("editor");
let propertiesSectionsLayout = defaultWidgetLayout(); let propertiesPanelLayout = defaultWidgetLayout();
onMount(() => { onMount(() => {
editor.subscriptions.subscribeJsMessage(UpdatePropertyPanelSectionsLayout, (updatePropertyPanelSectionsLayout) => { editor.subscriptions.subscribeJsMessage(UpdatePropertiesPanelLayout, (updatePropertiesPanelLayout) => {
patchWidgetLayout(propertiesSectionsLayout, updatePropertyPanelSectionsLayout); patchWidgetLayout(propertiesPanelLayout, updatePropertiesPanelLayout);
propertiesSectionsLayout = propertiesSectionsLayout; propertiesPanelLayout = propertiesPanelLayout;
}); });
}); });
onDestroy(() => {
editor.subscriptions.unsubscribeJsMessage(UpdatePropertiesPanelLayout);
});
</script> </script>
<LayoutCol class="properties"> <LayoutCol class="properties">
<LayoutCol class="sections" scrollableY={true}> <LayoutCol class="sections" scrollableY={true}>
<WidgetLayout layout={propertiesSectionsLayout} /> <WidgetLayout layout={propertiesPanelLayout} />
</LayoutCol> </LayoutCol>
</LayoutCol> </LayoutCol>
@@ -1,50 +0,0 @@
<script lang="ts">
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
import { getContext } from "svelte";
import type { PortfolioState } from "/src/state-providers/portfolio";
const portfolio = getContext<PortfolioState>("portfolio");
</script>
<LayoutCol class="spreadsheet">
<LayoutRow class="control-bar">
<TextLabel>Spreadsheet Data for Node ID {$portfolio.spreadsheetNode}:</TextLabel>
</LayoutRow>
<LayoutCol class="body" scrollableY={true}>
<WidgetLayout layout={$portfolio.spreadsheetWidgets} />
</LayoutCol>
</LayoutCol>
<style lang="scss" global>
.spreadsheet {
flex-grow: 1;
padding: 4px;
.control-bar {
height: 32px;
--widget-height: 24px;
flex: 0 0 auto;
.text-label {
margin: calc((24px - var(--widget-height)) / 2 + 4px) 0;
min-height: var(--widget-height);
line-height: var(--widget-height);
}
}
table {
margin: 0 -4px;
width: calc(100% + 2 * 4px);
margin-top: 8px;
.text-label {
white-space: wrap;
}
}
}
</style>
+56 -50
View File
@@ -32,7 +32,7 @@
// Imports/Export are stored at a key value of 0 // Imports/Export are stored at a key value of 0
$: gridSpacing = calculateGridSpacing($nodeGraph.transform.scale); $: gridSpacing = calculateGridSpacing($nodeGraph.transform.scale);
$: dotRadius = 1 + Math.floor($nodeGraph.transform.scale - 0.5 + 0.001) / 2; $: gridDotRadius = 1 + Math.floor($nodeGraph.transform.scale - 0.5 + 0.001) / 2;
let inputElement: HTMLInputElement; let inputElement: HTMLInputElement;
let hoveringImportIndex: number | undefined = undefined; let hoveringImportIndex: number | undefined = undefined;
@@ -192,7 +192,7 @@
return output.connectedTo return output.connectedTo
.map((inputConnector) => { .map((inputConnector) => {
if ((inputConnector as Node).nodeId === undefined) return `Connected to export index ${inputConnector.index}`; if ((inputConnector as Node).nodeId === undefined) return `Connected to export index ${inputConnector.index}`;
return `Connected to ${(inputConnector as Node).nodeId}, port index ${inputConnector.index}`; return `Connected to ${(inputConnector as Node).nodeId}, connector index ${inputConnector.index}`;
}) })
.join("\n"); .join("\n");
} }
@@ -200,7 +200,7 @@
function inputConnectedToText(input: FrontendGraphInput): string { function inputConnectedToText(input: FrontendGraphInput): string {
if (input.connectedTo === undefined) return "Connected to nothing"; if (input.connectedTo === undefined) return "Connected to nothing";
if ((input.connectedTo as Node).nodeId === undefined) return `Connected to import index ${input.connectedTo.index}`; if ((input.connectedTo as Node).nodeId === undefined) return `Connected to import index ${input.connectedTo.index}`;
return `Connected to ${(input.connectedTo as Node).nodeId}, port index ${input.connectedTo.index}`; return `Connected to ${(input.connectedTo as Node).nodeId}, connector index ${input.connectedTo.index}`;
} }
function primaryOutputConnectedToLayer(node: FrontendNode): boolean { function primaryOutputConnectedToLayer(node: FrontendNode): boolean {
@@ -238,7 +238,7 @@
style:--grid-spacing={`${gridSpacing}px`} style:--grid-spacing={`${gridSpacing}px`}
style:--grid-offset-x={`${$nodeGraph.transform.x}px`} style:--grid-offset-x={`${$nodeGraph.transform.x}px`}
style:--grid-offset-y={`${$nodeGraph.transform.y}px`} style:--grid-offset-y={`${$nodeGraph.transform.y}px`}
style:--dot-radius={`${dotRadius}px`} style:--grid-dot-radius={`${gridDotRadius}px`}
data-node-graph data-node-graph
> >
<!-- Right click menu for adding nodes --> <!-- Right click menu for adding nodes -->
@@ -298,8 +298,8 @@
{#each $nodeGraph.clickTargets.layerClickTargets as pathString} {#each $nodeGraph.clickTargets.layerClickTargets as pathString}
<path class="layer" d={pathString} /> <path class="layer" d={pathString} />
{/each} {/each}
{#each $nodeGraph.clickTargets.portClickTargets as pathString} {#each $nodeGraph.clickTargets.connectorClickTargets as pathString}
<path class="port" d={pathString} /> <path class="connector" d={pathString} />
{/each} {/each}
{#each $nodeGraph.clickTargets.iconClickTargets as pathString} {#each $nodeGraph.clickTargets.iconClickTargets as pathString}
<path class="visibility" d={pathString} /> <path class="visibility" d={pathString} />
@@ -332,14 +332,14 @@
</svg> </svg>
</div> </div>
<!-- Import and Export ports --> <!-- Import and Export connectors -->
<div class="imports-and-exports" style:transform-origin={`0 0`} style:transform={`translate(${$nodeGraph.transform.x}px, ${$nodeGraph.transform.y}px) scale(${$nodeGraph.transform.scale})`}> <div class="imports-and-exports" style:transform-origin={`0 0`} style:transform={`translate(${$nodeGraph.transform.x}px, ${$nodeGraph.transform.y}px) scale(${$nodeGraph.transform.scale})`}>
{#each $nodeGraph.imports as { outputMetadata, position }, index} {#each $nodeGraph.imports as { outputMetadata, position }, index}
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 8 8" viewBox="0 0 8 8"
class="port" class="connector"
data-port="output" data-connector="output"
data-datatype={outputMetadata.dataType} data-datatype={outputMetadata.dataType}
style:--data-color={`var(--color-data-${outputMetadata.dataType.toLowerCase()})`} style:--data-color={`var(--color-data-${outputMetadata.dataType.toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${outputMetadata.dataType.toLowerCase()}-dim)`} style:--data-color-dim={`var(--color-data-${outputMetadata.dataType.toLowerCase()}-dim)`}
@@ -411,8 +411,8 @@
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 8 8" viewBox="0 0 8 8"
class="port" class="connector"
data-port="input" data-connector="input"
data-datatype={inputMetadata.dataType} data-datatype={inputMetadata.dataType}
style:--data-color={`var(--color-data-${inputMetadata.dataType.toLowerCase()})`} style:--data-color={`var(--color-data-${inputMetadata.dataType.toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${inputMetadata.dataType.toLowerCase()}-dim)`} style:--data-color-dim={`var(--color-data-${inputMetadata.dataType.toLowerCase()}-dim)`}
@@ -521,8 +521,8 @@
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 8 12" viewBox="0 0 8 12"
class="port top" class="connector top"
data-port="output" data-connector="output"
data-datatype={node.primaryOutput.dataType} data-datatype={node.primaryOutput.dataType}
style:--data-color={`var(--color-data-${node.primaryOutput.dataType.toLowerCase()})`} style:--data-color={`var(--color-data-${node.primaryOutput.dataType.toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${node.primaryOutput.dataType.toLowerCase()}-dim)`} style:--data-color-dim={`var(--color-data-${node.primaryOutput.dataType.toLowerCase()}-dim)`}
@@ -542,8 +542,8 @@
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 8 12" viewBox="0 0 8 12"
class="port bottom" class="connector bottom"
data-port="input" data-connector="input"
data-datatype={node.primaryInput?.dataType} data-datatype={node.primaryInput?.dataType}
style:--data-color={`var(--color-data-${(node.primaryInput?.dataType || "General").toLowerCase()})`} style:--data-color={`var(--color-data-${(node.primaryInput?.dataType || "General").toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${(node.primaryInput?.dataType || "General").toLowerCase()}-dim)`} style:--data-color-dim={`var(--color-data-${(node.primaryInput?.dataType || "General").toLowerCase()}-dim)`}
@@ -561,14 +561,14 @@
{/if} {/if}
</svg> </svg>
</div> </div>
<!-- Layer input port (from left) --> <!-- Layer input connector (from left) -->
{#if node.exposedInputs.length > 0} {#if node.exposedInputs.length > 0}
<div class="input ports"> <div class="input connectors">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 8 8" viewBox="0 0 8 8"
class="port" class="connector"
data-port="input" data-connector="input"
data-datatype={stackDataInput.dataType} data-datatype={stackDataInput.dataType}
style:--data-color={`var(--color-data-${stackDataInput.dataType.toLowerCase()})`} style:--data-color={`var(--color-data-${stackDataInput.dataType.toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${stackDataInput.dataType.toLowerCase()}-dim)`} style:--data-color-dim={`var(--color-data-${stackDataInput.dataType.toLowerCase()}-dim)`}
@@ -679,14 +679,14 @@
{/each} {/each}
</div> </div>
{/if} {/if}
<!-- Input ports --> <!-- Input connectors -->
<div class="input ports"> <div class="input connectors">
{#if node.primaryInput?.dataType} {#if node.primaryInput?.dataType}
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 8 8" viewBox="0 0 8 8"
class="port primary-port" class="connector primary-connector"
data-port="input" data-connector="input"
data-datatype={node.primaryInput?.dataType} data-datatype={node.primaryInput?.dataType}
style:--data-color={`var(--color-data-${node.primaryInput.dataType.toLowerCase()})`} style:--data-color={`var(--color-data-${node.primaryInput.dataType.toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${node.primaryInput.dataType.toLowerCase()}-dim)`} style:--data-color-dim={`var(--color-data-${node.primaryInput.dataType.toLowerCase()}-dim)`}
@@ -704,8 +704,8 @@
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 8 8" viewBox="0 0 8 8"
class="port" class="connector"
data-port="input" data-connector="input"
data-datatype={secondary.dataType} data-datatype={secondary.dataType}
style:--data-color={`var(--color-data-${secondary.dataType.toLowerCase()})`} style:--data-color={`var(--color-data-${secondary.dataType.toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${secondary.dataType.toLowerCase()}-dim)`} style:--data-color-dim={`var(--color-data-${secondary.dataType.toLowerCase()}-dim)`}
@@ -720,14 +720,14 @@
{/if} {/if}
{/each} {/each}
</div> </div>
<!-- Output ports --> <!-- Output connectors -->
<div class="output ports"> <div class="output connectors">
{#if node.primaryOutput} {#if node.primaryOutput}
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 8 8" viewBox="0 0 8 8"
class="port primary-port" class="connector primary-connector"
data-port="output" data-connector="output"
data-datatype={node.primaryOutput.dataType} data-datatype={node.primaryOutput.dataType}
style:--data-color={`var(--color-data-${node.primaryOutput.dataType.toLowerCase()})`} style:--data-color={`var(--color-data-${node.primaryOutput.dataType.toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${node.primaryOutput.dataType.toLowerCase()}-dim)`} style:--data-color-dim={`var(--color-data-${node.primaryOutput.dataType.toLowerCase()}-dim)`}
@@ -744,8 +744,8 @@
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 8 8" viewBox="0 0 8 8"
class="port" class="connector"
data-port="output" data-connector="output"
data-datatype={secondary.dataType} data-datatype={secondary.dataType}
style:--data-color={`var(--color-data-${secondary.dataType.toLowerCase()})`} style:--data-color={`var(--color-data-${secondary.dataType.toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${secondary.dataType.toLowerCase()}-dim)`} style:--data-color-dim={`var(--color-data-${secondary.dataType.toLowerCase()}-dim)`}
@@ -801,8 +801,8 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
background-size: var(--grid-spacing) var(--grid-spacing); background-size: var(--grid-spacing) var(--grid-spacing);
background-position: calc(var(--grid-offset-x) - var(--dot-radius)) calc(var(--grid-offset-y) - var(--dot-radius)); background-position: calc(var(--grid-offset-x) - var(--grid-dot-radius)) calc(var(--grid-offset-y) - var(--grid-dot-radius));
background-image: radial-gradient(circle at var(--dot-radius) var(--dot-radius), var(--color-3-darkgray) var(--dot-radius), transparent 0); background-image: radial-gradient(circle at var(--grid-dot-radius) var(--grid-dot-radius), var(--color-3-darkgray) var(--grid-dot-radius), transparent 0);
background-repeat: repeat; background-repeat: repeat;
image-rendering: pixelated; image-rendering: pixelated;
mix-blend-mode: screen; mix-blend-mode: screen;
@@ -859,7 +859,7 @@
stroke: blue; stroke: blue;
} }
.port { .connector {
stroke: green; stroke: green;
} }
@@ -898,11 +898,13 @@
} }
.imports-and-exports { .imports-and-exports {
position: absolute;
width: 100%; width: 100%;
height: 100%; height: 100%;
position: absolute;
// Keeps the connectors above the wires
z-index: 1;
.port { .connector {
position: absolute; position: absolute;
width: 8px; width: 8px;
height: 8px; height: 8px;
@@ -1008,7 +1010,7 @@
border-radius: 4px; border-radius: 4px;
bottom: calc(100% + 12px); bottom: calc(100% + 12px);
z-index: -1; z-index: -1;
transition: opacity 0.2s ease-in-out; transition: opacity 0.2s;
opacity: 0.5; opacity: 0.5;
// Tail // Tail
@@ -1040,7 +1042,7 @@
-webkit-user-select: text; -webkit-user-select: text;
user-select: text; user-select: text;
transition: transition:
opacity 0.2s ease-in-out, opacity 0.2s,
z-index 0s 0.2s; z-index 0s 0.2s;
&::selection { &::selection {
@@ -1088,8 +1090,10 @@
border: 1px dashed var(--data-color); border: 1px dashed var(--data-color);
} }
.ports { .connectors {
position: absolute; position: absolute;
// Keeps the connectors above the wires
z-index: 1;
&.input { &.input {
left: -3px; left: -3px;
@@ -1100,7 +1104,7 @@
} }
} }
.port { .connector {
// Double the intended value because of margin collapsing, but for the first and last we divide it by two as intended // Double the intended value because of margin collapsing, but for the first and last we divide it by two as intended
margin: calc(24px - 8px) 0; margin: calc(24px - 8px) 0;
width: 8px; width: 8px;
@@ -1117,7 +1121,7 @@
border-radius: 8px; border-radius: 8px;
--extra-width-to-reach-grid-multiple: 8px; --extra-width-to-reach-grid-multiple: 8px;
--node-chain-area-left-extension: 0; --node-chain-area-left-extension: 0;
// Keep this equation in sync with the equivalent one in the Svelte template `<clipPath><path d="layerBorderMask(...)" /></clipPath>` above, as well as the `left` port offset CSS rule above in `.ports.input` above. // Keep this equation in sync with the equivalent one in the Svelte template `<clipPath><path d="layerBorderMask(...)" /></clipPath>` above, as well as the `left` connector offset CSS rule above in `.connectors.input` above.
width: calc((var(--layer-area-width) - 0.5) * 24px); width: calc((var(--layer-area-width) - 0.5) * 24px);
padding-left: calc(var(--node-chain-area-left-extension) * 24px); padding-left: calc(var(--node-chain-area-left-extension) * 24px);
margin-left: calc((0.5 - var(--node-chain-area-left-extension)) * 24px); margin-left: calc((0.5 - var(--node-chain-area-left-extension)) * 24px);
@@ -1141,8 +1145,10 @@
border-radius: 2px; border-radius: 2px;
position: relative; position: relative;
box-sizing: border-box; box-sizing: border-box;
width: 72px;
height: 48px; height: 48px;
// We shorten the width by 1px on the left and right so the inner thumbnail graphic maintains a perfect 3:2 aspect ratio
width: calc(72px - 2px);
margin: 0 1px;
&::before { &::before {
content: ""; content: "";
@@ -1153,7 +1159,7 @@
} }
&::before, &::before,
svg:not(.port) { svg:not(.connector) {
pointer-events: none; pointer-events: none;
position: absolute; position: absolute;
margin: auto; margin: auto;
@@ -1163,7 +1169,7 @@
height: calc(100% - 2px); height: calc(100% - 2px);
} }
.port { .connector {
position: absolute; position: absolute;
margin: 0 auto; margin: 0 auto;
left: 0; left: 0;
@@ -1211,21 +1217,21 @@
right: -12px; right: -12px;
} }
.input.ports { .input.connectors {
left: calc(-3px + var(--node-chain-area-left-extension) * 24px - 36px); left: calc(-3px + var(--node-chain-area-left-extension) * 24px - 36px);
} }
.solo-drag-grip, .solo-drag-grip,
.visibility, .visibility,
.input.ports, .input.connectors,
.input.ports .port { .input.connectors .connector {
position: absolute; position: absolute;
margin: auto 0; margin: auto 0;
top: 0; top: 0;
bottom: 0; bottom: 0;
} }
.input.ports .port { .input.connectors .connector {
left: 24px; left: 24px;
} }
} }
@@ -1259,11 +1265,11 @@
} }
} }
.port { .connector {
&:first-of-type { &:first-of-type {
margin-top: calc((24px - 8px) / 2); margin-top: calc((24px - 8px) / 2);
&:not(.primary-port) { &:not(.primary-connector) {
margin-top: calc((24px - 8px) / 2 + 24px); margin-top: calc((24px - 8px) / 2 + 24px);
} }
} }
@@ -25,6 +25,7 @@
import TextInput from "@graphite/components/widgets/inputs/TextInput.svelte"; import TextInput from "@graphite/components/widgets/inputs/TextInput.svelte";
import WorkingColorsInput from "@graphite/components/widgets/inputs/WorkingColorsInput.svelte"; import WorkingColorsInput from "@graphite/components/widgets/inputs/WorkingColorsInput.svelte";
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte"; import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
import ImageLabel from "@graphite/components/widgets/labels/ImageLabel.svelte";
import Separator from "@graphite/components/widgets/labels/Separator.svelte"; import Separator from "@graphite/components/widgets/labels/Separator.svelte";
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte"; import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte"; import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
@@ -124,6 +125,10 @@
{#if iconLabel} {#if iconLabel}
<IconLabel {...exclude(iconLabel)} /> <IconLabel {...exclude(iconLabel)} />
{/if} {/if}
{@const imageLabel = narrowWidgetProps(component.props, "ImageLabel")}
{#if imageLabel}
<ImageLabel {...exclude(imageLabel)} />
{/if}
{@const imageButton = narrowWidgetProps(component.props, "ImageButton")} {@const imageButton = narrowWidgetProps(component.props, "ImageButton")}
{#if imageButton} {#if imageButton}
<ImageButton {...exclude(imageButton)} action={() => widgetValueCommitAndUpdate(index, undefined)} /> <ImageButton {...exclude(imageButton)} action={() => widgetValueCommitAndUpdate(index, undefined)} />
@@ -17,14 +17,15 @@
.join(" "); .join(" ");
</script> </script>
<img src={IMAGE_BASE64_STRINGS[image]} style:width style:height class={`image-label ${className} ${extraClasses}`.trim()} title={tooltip} alt="" on:click={action} /> <img src={IMAGE_BASE64_STRINGS[image]} style:width style:height class={`image-button ${className} ${extraClasses}`.trim()} title={tooltip} alt="" on:click={action} />
<style lang="scss" global> <style lang="scss" global>
.image-label { .image-button {
width: auto; width: auto;
height: auto; height: auto;
border-radius: 2px;
+ .image-label.image-label { + .image-button.image-button {
margin-left: 8px; margin-left: 8px;
} }
} }
@@ -1,12 +1,11 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import type { FillChoice } from "@graphite/messages"; import type { FillChoice, MenuDirection } from "@graphite/messages";
import { Color, contrastingOutlineFactor, Gradient } from "@graphite/messages"; import { Color, contrastingOutlineFactor, Gradient } from "@graphite/messages";
import ColorPicker from "@graphite/components/floating-menus/ColorPicker.svelte"; import ColorPicker from "@graphite/components/floating-menus/ColorPicker.svelte";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte"; import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
const dispatch = createEventDispatcher<{ value: FillChoice; startHistoryTransaction: undefined }>(); const dispatch = createEventDispatcher<{ value: FillChoice; startHistoryTransaction: undefined }>();
@@ -15,6 +14,7 @@
export let value: FillChoice; export let value: FillChoice;
export let disabled = false; export let disabled = false;
export let allowNone = false; export let allowNone = false;
export let menuDirection: MenuDirection = "Bottom";
// export let allowTransparency = false; // TODO: Implement // export let allowTransparency = false; // TODO: Implement
export let tooltip: string | undefined = undefined; export let tooltip: string | undefined = undefined;
@@ -25,16 +25,18 @@
$: transparency = value instanceof Gradient ? value.stops.some((stop) => stop.color.alpha < 1) : value.alpha < 1; $: transparency = value instanceof Gradient ? value.stops.some((stop) => stop.color.alpha < 1) : value.alpha < 1;
</script> </script>
<LayoutCol class="color-button" classes={{ open, disabled, none, transparency, outlined }} {tooltip}> <LayoutCol class="color-button" classes={{ open, disabled, none, transparency, outlined, "direction-top": menuDirection === "Top" }} {tooltip}>
<button {disabled} style:--chosen-gradient={chosenGradient} style:--outline-amount={outlineFactor} on:click={() => (open = true)} tabindex="0" data-floating-menu-spawner> <button style:--chosen-gradient={chosenGradient} style:--outline-amount={outlineFactor} on:click={() => (open = true)} tabindex="0" data-floating-menu-spawner>
{#if disabled && value instanceof Color && !value.none} <!-- {#if disabled && value instanceof Color && !value.none}
<TextLabel>sRGB</TextLabel> <TextLabel>sRGB</TextLabel>
{/if} {/if} -->
</button> </button>
<ColorPicker <ColorPicker
{open} {open}
on:open={({ detail }) => (open = detail)} {disabled}
colorOrGradient={value} colorOrGradient={value}
direction={menuDirection || "Bottom"}
on:open={({ detail }) => (open = detail)}
on:colorOrGradient={({ detail }) => { on:colorOrGradient={({ detail }) => {
value = detail; value = detail;
dispatch("value", detail); dispatch("value", detail);
@@ -137,5 +139,9 @@
left: 50%; left: 50%;
bottom: 0; bottom: 0;
} }
&.direction-top > .floating-menu {
bottom: 100%;
}
} }
</style> </style>
@@ -13,6 +13,7 @@
const dispatch = createEventDispatcher<{ activeMarkerIndexChange: number | undefined; gradient: Gradient; dragging: boolean }>(); const dispatch = createEventDispatcher<{ activeMarkerIndexChange: number | undefined; gradient: Gradient; dragging: boolean }>();
export let gradient: Gradient; export let gradient: Gradient;
export let disabled = false;
export let activeMarkerIndex = 0 as number | undefined; export let activeMarkerIndex = 0 as number | undefined;
// export let disabled = false; // export let disabled = false;
// export let tooltip: string | undefined = undefined; // export let tooltip: string | undefined = undefined;
@@ -22,6 +23,8 @@
let deletionRestore: boolean | undefined = undefined; let deletionRestore: boolean | undefined = undefined;
function markerPointerDown(e: PointerEvent, index: number) { function markerPointerDown(e: PointerEvent, index: number) {
if (disabled) return;
// Left-click to select and begin potentially dragging // Left-click to select and begin potentially dragging
if (e.button === BUTTON_LEFT) { if (e.button === BUTTON_LEFT) {
activeMarkerIndex = index; activeMarkerIndex = index;
@@ -47,6 +50,8 @@
} }
function insertStop(e: MouseEvent) { function insertStop(e: MouseEvent) {
if (disabled) return;
if (e.button !== BUTTON_LEFT) return; if (e.button !== BUTTON_LEFT) return;
let position = markerPosition(e); let position = markerPosition(e);
@@ -79,6 +84,8 @@
} }
function deleteStop(e: KeyboardEvent) { function deleteStop(e: KeyboardEvent) {
if (disabled) return;
if (e.key !== "Delete" && e.key !== "Backspace") return; if (e.key !== "Delete" && e.key !== "Backspace") return;
if (activeMarkerIndex === undefined) return; if (activeMarkerIndex === undefined) return;
@@ -88,6 +95,8 @@
} }
function deleteStopByIndex(index: number) { function deleteStopByIndex(index: number) {
if (disabled) return;
if (gradient.stops.length <= 2) return; if (gradient.stops.length <= 2) return;
gradient.stops.splice(index, 1); gradient.stops.splice(index, 1);
@@ -103,6 +112,8 @@
} }
function moveMarker(e: PointerEvent, index: number) { function moveMarker(e: PointerEvent, index: number) {
if (disabled) return;
// Just in case the mouseup event is lost // Just in case the mouseup event is lost
if (e.buttons === 0) stopDrag(); if (e.buttons === 0) stopDrag();
@@ -120,6 +131,8 @@
} }
export function setPosition(index: number, position: number) { export function setPosition(index: number, position: number) {
if (disabled) return;
const active = gradient.stops[index]; const active = gradient.stops[index];
active.position = position; active.position = position;
gradient.stops.sort((a, b) => a.position - b.position); gradient.stops.sort((a, b) => a.position - b.position);
@@ -131,6 +144,8 @@
} }
function abortDrag() { function abortDrag() {
if (disabled) return;
if (activeMarkerIndex === undefined) return; if (activeMarkerIndex === undefined) return;
if (deletionRestore) { if (deletionRestore) {
@@ -143,6 +158,8 @@
} }
function stopDrag() { function stopDrag() {
if (disabled) return;
removeEvents(); removeEvents();
positionRestore = undefined; positionRestore = undefined;
@@ -152,19 +169,27 @@
} }
function onPointerMove(e: PointerEvent) { function onPointerMove(e: PointerEvent) {
if (disabled) return;
if (activeMarkerIndex !== undefined) moveMarker(e, activeMarkerIndex); if (activeMarkerIndex !== undefined) moveMarker(e, activeMarkerIndex);
} }
function onPointerUp() { function onPointerUp() {
if (disabled) return;
stopDrag(); stopDrag();
} }
function onMouseDown(e: MouseEvent) { function onMouseDown(e: MouseEvent) {
if (disabled) return;
const BUTTONS_RIGHT = 0b0000_0010; const BUTTONS_RIGHT = 0b0000_0010;
if (e.buttons & BUTTONS_RIGHT) abortDrag(); if (e.buttons & BUTTONS_RIGHT) abortDrag();
} }
function onKeyDown(e: KeyboardEvent) { function onKeyDown(e: KeyboardEvent) {
if (disabled) return;
if (e.key === "Escape") { if (e.key === "Escape") {
const element = markerTrack?.div(); const element = markerTrack?.div();
if (element) preventEscapeClosingParentFloatingMenu(element); if (element) preventEscapeClosingParentFloatingMenu(element);
@@ -193,25 +218,28 @@
document.removeEventListener("keydown", deleteStop); document.removeEventListener("keydown", deleteStop);
}); });
// Future design notes:
//
// # Backend -> Frontend // # Backend -> Frontend
// Populate(gradient, { position, color }[], active) // The only way indexes get changed. Frontend drops marker if it's being dragged. // Populate(gradient, { position, color }[], active) // The only way indexes get changed. Frontend drops marker if it's being dragged.
// UpdateGradient(gradient) // UpdateGradient(gradient)
// UpdateMarkers({ index, position, color }[]) // UpdateMarkers({ index, position, color }[])
//
// # Frontend -> Backend // # Frontend -> Backend
// SendNewActive(index) // SendNewActive(index)
// SendPositions({ index, position }[]) // SendPositions({ index, position }[])
// AddMarker(position) // AddMarker(position)
// RemoveMarkers(index[]) // RemoveMarkers(index[])
// ResetMarkerToDefault(index) // ResetMarkerToDefault(index)
//
// // We need a way to encode constraints on some markers, like locking them in place or preventing reordering // We need a way to encode constraints on some markers, like locking them in place or preventing reordering
// // We need a way to encode the allowability of adding new markers between certain markers, or preventing the deletion of certain markers // We need a way to encode the allowability of adding new markers between certain markers, or preventing the deletion of certain markers
// // We need the ability to multi-select markers and move them all at once // We need the ability to multi-select markers and move them all at once
</script> </script>
<LayoutCol <LayoutCol
class="spectrum-input" class="spectrum-input"
classes={{ disabled }}
styles={{ styles={{
"--gradient-start": gradient.firstColor()?.toHexOptionalAlpha() || "black", "--gradient-start": gradient.firstColor()?.toHexOptionalAlpha() || "black",
"--gradient-end": gradient.lastColor()?.toHexOptionalAlpha() || "black", "--gradient-end": gradient.lastColor()?.toHexOptionalAlpha() || "black",
@@ -232,6 +260,9 @@
viewBox="0 0 12 12" viewBox="0 0 12 12"
> >
<path class="inner-fill" d="M10,11.5H2c-0.8,0-1.5-0.7-1.5-1.5V6.8c0-0.4,0.2-0.8,0.4-1.1L6,0.7l5.1,5.1c0.3,0.3,0.4,0.7,0.4,1.1V10C11.5,10.8,10.8,11.5,10,11.5z" /> <path class="inner-fill" d="M10,11.5H2c-0.8,0-1.5-0.7-1.5-1.5V6.8c0-0.4,0.2-0.8,0.4-1.1L6,0.7l5.1,5.1c0.3,0.3,0.4,0.7,0.4,1.1V10C11.5,10.8,10.8,11.5,10,11.5z" />
{#if disabled}
<path class="disabled-fill" d="M10,11.5H2c-0.8,0-1.5-0.7-1.5-1.5V6.8c0-0.4,0.2-0.8,0.4-1.1L6,0.7l5.1,5.1c0.3,0.3,0.4,0.7,0.4,1.1V10C11.5,10.8,10.8,11.5,10,11.5z" />
{/if}
<path <path
class="outer-border" class="outer-border"
d="M6,1.4L1.3,6.1C1.1,6.3,1,6.6,1,6.8V10c0,0.6,0.4,1,1,1h8c0.6,0,1-0.4,1-1V6.8c0-0.3-0.1-0.5-0.3-0.7L6,1.4M6,0l5.4,5.4C11.8,5.8,12,6.3,12,6.8V10c0,1.1-0.9,2-2,2H2c-1.1,0-2-0.9-2-2V6.8c0-0.5,0.2-1,0.6-1.4L6,0z" d="M6,1.4L1.3,6.1C1.1,6.3,1,6.6,1,6.8V10c0,0.6,0.4,1,1,1h8c0.6,0,1-0.4,1-1V6.8c0-0.3-0.1-0.5-0.3-0.7L6,1.4M6,0l5.4,5.4C11.8,5.8,12,6.3,12,6.8V10c0,1.1-0.9,2-2,2H2c-1.1,0-2-0.9-2-2V6.8c0-0.5,0.2-1,0.6-1.4L6,0z"
@@ -269,6 +300,14 @@
border-radius: 2px; border-radius: 2px;
} }
&.disabled .gradient-strip {
transition: opacity 0.1s;
&:hover {
opacity: 0.5;
}
}
.marker-track { .marker-track {
margin-top: calc(24px - 16px - 12px); margin-top: calc(24px - 16px - 12px);
margin-left: var(--marker-half-width); margin-left: var(--marker-half-width);
@@ -294,28 +333,40 @@
.outer-border { .outer-border {
fill: var(--color-5-dullgray); fill: var(--color-5-dullgray);
} }
}
}
&:not(.active) { &.disabled .marker-track .marker {
.inner-fill:hover + .outer-border, .disabled-fill {
.outer-border:hover { opacity: 0.5;
fill: var(--color-6-lowergray); }
}
.outer-border {
fill: var(--color-4-dimgray);
}
}
&:not(.disabled) .marker-track .marker {
&:not(.active) {
.inner-fill:hover + .outer-border,
.outer-border:hover {
fill: var(--color-6-lowergray);
}
}
&.active {
.inner-fill {
filter: drop-shadow(0 0 1px var(--color-2-mildblack)) drop-shadow(0 0 1px var(--color-2-mildblack));
} }
&.active { // Outer border when active
.inner-fill { .outer-border {
filter: drop-shadow(0 0 1px var(--color-2-mildblack)) drop-shadow(0 0 1px var(--color-2-mildblack)); fill: var(--color-e-nearwhite);
} }
// Outer border when active .inner-fill:hover + .outer-border,
.outer-border { .outer-border:hover {
fill: var(--color-e-nearwhite); fill: var(--color-f-white);
}
.inner-fill:hover + .outer-border,
.outer-border:hover {
fill: var(--color-f-white);
}
} }
} }
} }
@@ -16,6 +16,7 @@
// Styling // Styling
export let centered = false; export let centered = false;
export let minWidth = 0; export let minWidth = 0;
export let maxWidth = 0;
let className = ""; let className = "";
export { className as class }; export { className as class };
@@ -63,7 +64,10 @@
<FieldInput <FieldInput
class={`text-input ${className}`.trim()} class={`text-input ${className}`.trim()}
classes={{ centered, ...classes }} classes={{ centered, ...classes }}
styles={{ ...(minWidth > 0 ? { "min-width": `${minWidth}px` } : {}) }} styles={{
...(minWidth > 0 ? { "min-width": `${minWidth}px` } : {}),
...(maxWidth > 0 ? { "max-width": `${maxWidth}px` } : {}),
}}
{value} {value}
on:value on:value
on:textFocused={onTextFocused} on:textFocused={onTextFocused}
@@ -0,0 +1,32 @@
<script lang="ts">
let className = "";
export { className as class };
export let classes: Record<string, boolean> = {};
export let url: string;
export let width: string | undefined;
export let height: string | undefined;
export let tooltip: string | undefined = undefined;
$: extraClasses = Object.entries(classes)
.flatMap(([className, stateName]) => (stateName ? [className] : []))
.join(" ");
</script>
<img src={url} style:width style:height class={`image-label ${className} ${extraClasses}`.trim()} title={tooltip} alt="" />
<style lang="scss" global>
.image-label {
width: auto;
height: auto;
border-radius: 2px;
background-image: var(--color-transparent-checkered-background);
background-size: var(--color-transparent-checkered-background-size);
background-position: var(--color-transparent-checkered-background-position);
background-repeat: var(--color-transparent-checkered-background-repeat);
+ .image-label.image-label {
margin-left: 8px;
}
}
</style>
@@ -1,14 +1,14 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
import Data from "@graphite/components/panels/Data.svelte";
import Document from "@graphite/components/panels/Document.svelte"; import Document from "@graphite/components/panels/Document.svelte";
import Layers from "@graphite/components/panels/Layers.svelte"; import Layers from "@graphite/components/panels/Layers.svelte";
import Properties from "@graphite/components/panels/Properties.svelte"; import Properties from "@graphite/components/panels/Properties.svelte";
import Spreadsheet from "@graphite/components/panels/Spreadsheet.svelte";
const PANEL_COMPONENTS = { const PANEL_COMPONENTS = {
Document, Document,
Layers, Layers,
Properties, Properties,
Spreadsheet, Data,
}; };
type PanelType = keyof typeof PANEL_COMPONENTS; type PanelType = keyof typeof PANEL_COMPONENTS;
</script> </script>
@@ -16,7 +16,7 @@
/**/ root: 100, /**/ root: 100,
/* ├─ */ content: 80, /* ├─ */ content: 80,
/* │ ├─ */ document: 70, /* │ ├─ */ document: 70,
/* │ └─ */ spreadsheet: 30, /* │ └─ */ data: 30,
/* └─ */ details: 20, /* └─ */ details: 20,
/* ├─ */ properties: 45, /* ├─ */ properties: 45,
/* └─ */ layers: 55, /* └─ */ layers: 55,
@@ -148,23 +148,31 @@
bind:this={documentPanel} bind:this={documentPanel}
/> />
</LayoutRow> </LayoutRow>
{#if $portfolio.spreadsheetOpen} {#if $portfolio.dataPanelOpen}
<LayoutRow class="workspace-grid-resize-gutter" data-gutter-vertical on:pointerdown={(e) => resizePanel(e)} /> <LayoutRow class="workspace-grid-resize-gutter" data-gutter-vertical on:pointerdown={(e) => resizePanel(e)} />
<LayoutRow class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["spreadsheet"] }} data-subdivision-name="spreadsheet"> <LayoutRow class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["data"] }} data-subdivision-name="data">
<Panel panelType="Spreadsheet" tabLabels={[{ name: "Spreadsheet" }]} tabActiveIndex={0} /> <Panel panelType="Data" tabLabels={[{ name: "Data" }]} tabActiveIndex={0} />
</LayoutRow> </LayoutRow>
{/if} {/if}
</LayoutCol> </LayoutCol>
<LayoutCol class="workspace-grid-resize-gutter" data-gutter-horizontal on:pointerdown={(e) => resizePanel(e)} /> {#if $portfolio.propertiesPanelOpen || $portfolio.layersPanelOpen}
<LayoutCol class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["details"] }} data-subdivision-name="details"> <LayoutCol class="workspace-grid-resize-gutter" data-gutter-horizontal on:pointerdown={(e) => resizePanel(e)} />
<LayoutRow class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["properties"] }} data-subdivision-name="properties"> <LayoutCol class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["details"] }} data-subdivision-name="details">
<Panel panelType="Properties" tabLabels={[{ name: "Properties" }]} tabActiveIndex={0} /> {#if $portfolio.propertiesPanelOpen}
</LayoutRow> <LayoutRow class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["properties"] }} data-subdivision-name="properties">
<LayoutRow class="workspace-grid-resize-gutter" data-gutter-vertical on:pointerdown={(e) => resizePanel(e)} /> <Panel panelType="Properties" tabLabels={[{ name: "Properties" }]} tabActiveIndex={0} />
<LayoutRow class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["layers"] }} data-subdivision-name="layers"> </LayoutRow>
<Panel panelType="Layers" tabLabels={[{ name: "Layers" }]} tabActiveIndex={0} /> {/if}
</LayoutRow> {#if $portfolio.propertiesPanelOpen && $portfolio.layersPanelOpen}
</LayoutCol> <LayoutRow class="workspace-grid-resize-gutter" data-gutter-vertical on:pointerdown={(e) => resizePanel(e)} />
{/if}
{#if $portfolio.layersPanelOpen}
<LayoutRow class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["layers"] }} data-subdivision-name="layers">
<Panel panelType="Layers" tabLabels={[{ name: "Layers" }]} tabActiveIndex={0} />
</LayoutRow>
{/if}
</LayoutCol>
{/if}
</LayoutRow> </LayoutRow>
{#if $dialog.visible} {#if $dialog.visible}
<Dialog /> <Dialog />
+54 -17
View File
@@ -180,7 +180,7 @@ export class Box {
export type FrontendClickTargets = { export type FrontendClickTargets = {
readonly nodeClickTargets: string[]; readonly nodeClickTargets: string[];
readonly layerClickTargets: string[]; readonly layerClickTargets: string[];
readonly portClickTargets: string[]; readonly connectorClickTargets: string[];
readonly iconClickTargets: string[]; readonly iconClickTargets: string[];
readonly allNodesBoundingBox: string; readonly allNodesBoundingBox: string;
readonly importExportsBoundingBox: string; readonly importExportsBoundingBox: string;
@@ -192,7 +192,7 @@ export type ContextMenuInformation = {
contextMenuData: "CreateNode" | { type: "CreateNode"; compatibleType: string } | { nodeId: bigint; currentlyIsNode: boolean }; contextMenuData: "CreateNode" | { type: "CreateNode"; compatibleType: string } | { nodeId: bigint; currentlyIsNode: boolean };
}; };
export type FrontendGraphDataType = "General" | "Raster" | "Vector" | "Number" | "Graphic" | "Artboard"; export type FrontendGraphDataType = "General" | "Number" | "Artboard" | "Graphic" | "Raster" | "Vector" | "Color";
export class Node { export class Node {
readonly index!: bigint; readonly index!: bigint;
@@ -763,10 +763,16 @@ export class UpdateGraphFadeArtwork extends JsMessage {
readonly percentage!: number; readonly percentage!: number;
} }
export class UpdateSpreadsheetState extends JsMessage { export class UpdateDataPanelState extends JsMessage {
readonly open!: boolean; readonly open!: boolean;
}
readonly node!: bigint | undefined; export class UpdatePropertiesPanelState extends JsMessage {
readonly open!: boolean;
}
export class UpdateLayersPanelState extends JsMessage {
readonly open!: boolean;
} }
export class UpdateMouseCursor extends JsMessage { export class UpdateMouseCursor extends JsMessage {
@@ -981,9 +987,11 @@ export class ColorInput extends WidgetProps {
}) })
value!: FillChoice; value!: FillChoice;
allowNone!: boolean;
disabled!: boolean; disabled!: boolean;
allowNone!: boolean; menuDirection!: MenuDirection | undefined;
// allowTransparency!: boolean; // TODO: Implement // allowTransparency!: boolean; // TODO: Implement
@@ -1140,6 +1148,19 @@ export class ImageButton extends WidgetProps {
tooltip!: string | undefined; tooltip!: string | undefined;
} }
export class ImageLabel extends WidgetProps {
url!: string;
@Transform(({ value }: { value: string }) => value || undefined)
width!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
height!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
}
export type NumberInputIncrementBehavior = "Add" | "Multiply" | "Callback" | "None"; export type NumberInputIncrementBehavior = "Add" | "Multiply" | "Callback" | "None";
export type NumberInputMode = "Increment" | "Range"; export type NumberInputMode = "Increment" | "Range";
@@ -1332,6 +1353,8 @@ export class TextInput extends WidgetProps {
minWidth!: number; minWidth!: number;
maxWidth!: number;
@Transform(({ value }: { value: string }) => value || undefined) @Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined; tooltip!: string | undefined;
} }
@@ -1383,6 +1406,7 @@ const widgetSubTypes = [
{ value: FontInput, name: "FontInput" }, { value: FontInput, name: "FontInput" },
{ value: IconButton, name: "IconButton" }, { value: IconButton, name: "IconButton" },
{ value: ImageButton, name: "ImageButton" }, { value: ImageButton, name: "ImageButton" },
{ value: ImageLabel, name: "ImageLabel" },
{ value: IconLabel, name: "IconLabel" }, { value: IconLabel, name: "IconLabel" },
{ value: NodeCatalog, name: "NodeCatalog" }, { value: NodeCatalog, name: "NodeCatalog" },
{ value: NumberInput, name: "NumberInput" }, { value: NumberInput, name: "NumberInput" },
@@ -1472,11 +1496,11 @@ export function patchWidgetLayout(layout: /* &mut */ WidgetLayout, updates: Widg
updates.diff.forEach((update) => { updates.diff.forEach((update) => {
// Find the object where the diff applies to // Find the object where the diff applies to
const diffObject = update.widgetPath.reduce((targetLayout, index) => { const diffObject = update.widgetPath.reduce((targetLayout: UIItem | undefined, index: number): UIItem | undefined => {
if ("columnWidgets" in targetLayout) return targetLayout.columnWidgets[index]; if (targetLayout && "columnWidgets" in targetLayout) return targetLayout.columnWidgets[index];
if ("rowWidgets" in targetLayout) return targetLayout.rowWidgets[index]; if (targetLayout && "rowWidgets" in targetLayout) return targetLayout.rowWidgets[index];
if ("tableWidgets" in targetLayout) return targetLayout.tableWidgets[index]; if (targetLayout && "tableWidgets" in targetLayout) return targetLayout.tableWidgets[index];
if ("layout" in targetLayout) return targetLayout.layout[index]; if (targetLayout && "layout" in targetLayout) return targetLayout.layout[index];
if (targetLayout instanceof Widget) { if (targetLayout instanceof Widget) {
if (targetLayout.props.kind === "PopoverButton" && targetLayout.props instanceof PopoverButton && targetLayout.props.popoverLayout) { if (targetLayout.props.kind === "PopoverButton" && targetLayout.props instanceof PopoverButton && targetLayout.props.popoverLayout) {
return targetLayout.props.popoverLayout[index]; return targetLayout.props.popoverLayout[index];
@@ -1487,10 +1511,21 @@ export function patchWidgetLayout(layout: /* &mut */ WidgetLayout, updates: Widg
} }
// This is a path traversal so we can assume from the backend that it exists // This is a path traversal so we can assume from the backend that it exists
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if ("action" in targetLayout) return targetLayout.children![index]; if (targetLayout && "action" in targetLayout) return targetLayout.children![index];
return targetLayout[index];
return targetLayout?.[index];
}, layout.layout as UIItem); }, layout.layout as UIItem);
// Exit if we failed to produce a valid patch for the existing layout.
// This means that the backend assumed an existing layout that doesn't exist in the frontend. This can happen, for
// example, if a panel is destroyed in the frontend but was never cleared in the backend, so the next time the backend
// tries to update the layout, it attempts to insert only the changes against the old layout that no longer exists.
if (diffObject === undefined) {
// eslint-disable-next-line no-console
console.error("In `patchWidgetLayout`, the `diffObject` is undefined. The layout has not been updated. See the source code comment above this error for hints.");
return;
}
// If this is a list with a length, then set the length to 0 to clear the list // If this is a list with a length, then set the length to 0 to clear the list
if ("length" in diffObject) { if ("length" in diffObject) {
diffObject.length = 0; diffObject.length = 0;
@@ -1613,9 +1648,9 @@ export class UpdateMenuBarLayout extends JsMessage {
export class UpdateNodeGraphControlBarLayout extends WidgetDiffUpdate {} export class UpdateNodeGraphControlBarLayout extends WidgetDiffUpdate {}
export class UpdatePropertyPanelSectionsLayout extends WidgetDiffUpdate {} export class UpdatePropertiesPanelLayout extends WidgetDiffUpdate {}
export class UpdateSpreadsheetLayout extends WidgetDiffUpdate {} export class UpdateDataPanelLayout extends WidgetDiffUpdate {}
export class UpdateToolOptionsLayout extends WidgetDiffUpdate {} export class UpdateToolOptionsLayout extends WidgetDiffUpdate {}
@@ -1712,9 +1747,11 @@ export const messageMakers: Record<string, MessageMaker> = {
UpdateNodeThumbnail, UpdateNodeThumbnail,
UpdateOpenDocumentsList, UpdateOpenDocumentsList,
UpdatePlatform, UpdatePlatform,
UpdatePropertyPanelSectionsLayout, UpdatePropertiesPanelLayout,
UpdateSpreadsheetLayout, UpdateDataPanelLayout,
UpdateSpreadsheetState, UpdateDataPanelState,
UpdatePropertiesPanelState,
UpdateLayersPanelState,
UpdateToolOptionsLayout, UpdateToolOptionsLayout,
UpdateToolShelfLayout, UpdateToolShelfLayout,
UpdateViewportHolePunch, UpdateViewportHolePunch,
+16 -12
View File
@@ -13,10 +13,9 @@ import {
TriggerOpenDocument, TriggerOpenDocument,
UpdateActiveDocument, UpdateActiveDocument,
UpdateOpenDocumentsList, UpdateOpenDocumentsList,
UpdateSpreadsheetState, UpdateDataPanelState,
defaultWidgetLayout, UpdatePropertiesPanelState,
patchWidgetLayout, UpdateLayersPanelState,
UpdateSpreadsheetLayout,
} from "@graphite/messages"; } from "@graphite/messages";
import { downloadFile, downloadFileBlob, upload } from "@graphite/utility-functions/files"; import { downloadFile, downloadFileBlob, upload } from "@graphite/utility-functions/files";
import { extractPixelData, rasterizeSVG } from "@graphite/utility-functions/rasterization"; import { extractPixelData, rasterizeSVG } from "@graphite/utility-functions/rasterization";
@@ -27,9 +26,9 @@ export function createPortfolioState(editor: Editor) {
unsaved: false, unsaved: false,
documents: [] as FrontendDocumentDetails[], documents: [] as FrontendDocumentDetails[],
activeDocumentIndex: 0, activeDocumentIndex: 0,
spreadsheetOpen: false, dataPanelOpen: false,
spreadsheetNode: BigInt(0) as bigint | undefined, propertiesPanelOpen: true,
spreadsheetWidgets: defaultWidgetLayout(), layersPanelOpen: true,
}); });
// Set up message subscriptions on creation // Set up message subscriptions on creation
@@ -107,16 +106,21 @@ export function createPortfolioState(editor: Editor) {
// Fail silently if there's an error rasterizing the SVG, such as a zero-sized image // Fail silently if there's an error rasterizing the SVG, such as a zero-sized image
} }
}); });
editor.subscriptions.subscribeJsMessage(UpdateSpreadsheetState, async (updateSpreadsheetState) => { editor.subscriptions.subscribeJsMessage(UpdateDataPanelState, async (updateDataPanelState) => {
update((state) => { update((state) => {
state.spreadsheetOpen = updateSpreadsheetState.open; state.dataPanelOpen = updateDataPanelState.open;
state.spreadsheetNode = updateSpreadsheetState.node;
return state; return state;
}); });
}); });
editor.subscriptions.subscribeJsMessage(UpdateSpreadsheetLayout, (updateSpreadsheetLayout) => { editor.subscriptions.subscribeJsMessage(UpdatePropertiesPanelState, async (updatePropertiesPanelState) => {
update((state) => { update((state) => {
patchWidgetLayout(state.spreadsheetWidgets, updateSpreadsheetLayout); state.propertiesPanelOpen = updatePropertiesPanelState.open;
return state;
});
});
editor.subscriptions.subscribeJsMessage(UpdateLayersPanelState, async (updateLayersPanelState) => {
update((state) => {
state.layersPanelOpen = updateLayersPanelState.open;
return state; return state;
}); });
}); });
+5
View File
@@ -17,6 +17,10 @@ export function createSubscriptionRouter() {
subscriptions[messageType.name] = callback; subscriptions[messageType.name] = callback;
}; };
const unsubscribeJsMessage = <T extends JsMessage>(messageType: new () => T) => {
delete subscriptions[messageType.name];
};
const handleJsMessage = (messageType: JsMessageType, messageData: Record<string, unknown>, wasm: WebAssembly.Memory, handle: EditorHandle) => { const handleJsMessage = (messageType: JsMessageType, messageData: Record<string, unknown>, wasm: WebAssembly.Memory, handle: EditorHandle) => {
// Find the message maker for the message type, which can either be a JS class constructor or a function that returns an instance of the JS class // Find the message maker for the message type, which can either be a JS class constructor or a function that returns an instance of the JS class
const messageMaker = messageMakers[messageType]; const messageMaker = messageMakers[messageType];
@@ -68,6 +72,7 @@ export function createSubscriptionRouter() {
return { return {
subscribeJsMessage, subscribeJsMessage,
unsubscribeJsMessage,
handleJsMessage, handleJsMessage,
}; };
} }
-1
View File
@@ -262,7 +262,6 @@ function generateRustLicenses(): LicenseInfo[] | undefined {
const { stdout, stderr, status } = spawnSync("cargo", ["about", "generate", "about.hbs"], { const { stdout, stderr, status } = spawnSync("cargo", ["about", "generate", "about.hbs"], {
cwd: path.join(__dirname, ".."), cwd: path.join(__dirname, ".."),
encoding: "utf8", encoding: "utf8",
timeout: 60000, // One minute
shell: true, shell: true,
windowsHide: true, // Hide the terminal on Windows windowsHide: true, // Hide the terminal on Windows
}); });
+1 -5
View File
@@ -231,11 +231,7 @@ impl EditorHandle {
let js_return_value = self.frontend_message_handler_callback.call2(&JsValue::null(), &JsValue::from(message_type), &message_data); let js_return_value = self.frontend_message_handler_callback.call2(&JsValue::null(), &JsValue::from(message_type), &message_data);
if let Err(error) = js_return_value { if let Err(error) = js_return_value {
error!( error!("While handling FrontendMessage {:?}, JavaScript threw an error:\n{:?}", message.to_discriminant().local_name(), error,)
"While handling FrontendMessage \"{:?}\", JavaScript threw an error: {:?}",
message.to_discriminant().local_name(),
error,
)
} }
} }
-25
View File
@@ -1,25 +0,0 @@
[package]
name = "bezier-rs"
version = "0.4.0"
rust-version = "1.85"
edition = "2024"
authors = ["Graphite Authors <contact@graphite.rs>"]
description = "Computational geometry algorithms for Bézier segments and shapes useful in the context of 2D graphics"
license = "MIT OR Apache-2.0"
readme = "README.md"
keywords = ["bezier", "curve", "geometry", "2d", "graphics"]
categories = ["graphics", "mathematics"]
homepage = "https://github.com/GraphiteEditor/Graphite/tree/master/libraries/bezier-rs"
repository = "https://github.com/GraphiteEditor/Graphite/tree/master/libraries/bezier-rs"
documentation = "https://graphite.rs/libraries/bezier-rs/"
[dependencies]
# Required dependencies
glam = { workspace = true }
# Optional local dependencies
dyn-any = { version = "0.3.0", path = "../dyn-any", optional = true }
# Optional workspace dependencies
kurbo = { workspace = true, optional = true }
serde = { workspace = true, optional = true }

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