mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-24 17:58:12 +08:00
Merge branch 'master' into merge_point
This commit is contained in:
@@ -63,14 +63,14 @@ jobs:
|
||||
mkdir artifacts
|
||||
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: |
|
||||
mv artifacts/* website/other/editor-structure
|
||||
mv artifacts/* .
|
||||
|
||||
- name: 🔧 Build auto-generated code docs artifacts into HTML
|
||||
run: |
|
||||
cd website/other/editor-structure
|
||||
node generate.js hierarchical_message_system_tree.txt replacement.html
|
||||
cd website
|
||||
npm run generate-editor-structure
|
||||
|
||||
- name: 🌐 Build Graphite website with Zola
|
||||
env:
|
||||
@@ -80,38 +80,6 @@ jobs:
|
||||
npm run install-fonts
|
||||
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
|
||||
id: cloudflare
|
||||
uses: cloudflare/pages-action@1
|
||||
|
||||
@@ -8,3 +8,4 @@ flamegraph.svg
|
||||
.idea/
|
||||
.direnv
|
||||
hierarchical_message_system_tree.txt
|
||||
hierarchical_message_system_tree.html
|
||||
|
||||
+11
-5
@@ -33,17 +33,17 @@
|
||||
pkgs = import nixpkgs {
|
||||
inherit system overlays;
|
||||
};
|
||||
|
||||
|
||||
rustc-wasm = pkgs.rust-bin.stable.latest.default.override {
|
||||
targets = [ "wasm32-unknown-unknown" ];
|
||||
extensions = [ "rust-src" "rust-analyzer" "clippy" "cargo" ];
|
||||
};
|
||||
|
||||
libcef = pkgs.libcef.overrideAttrs (finalAttrs: previousAttrs: {
|
||||
version = "138.0.26";
|
||||
gitRevision = "84f2d27";
|
||||
chromiumVersion = "138.0.7204.158";
|
||||
srcHash = "sha256-d9jQJX7rgdoHfROD3zmOdMSesRdKE3slB5ZV+U2wlbQ=";
|
||||
version = "139.0.17";
|
||||
gitRevision = "6c347eb";
|
||||
chromiumVersion = "139.0.7258.31";
|
||||
srcHash = "sha256-kRMO8DP4El1qytDsAZBdHvR9AAHXce90nPdyfJailBg=";
|
||||
|
||||
__intentionallyOverridingVersion = true;
|
||||
|
||||
@@ -75,6 +75,12 @@
|
||||
vulkan-loader
|
||||
libraw
|
||||
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
|
||||
|
||||
Vendored
+1
-1
@@ -35,7 +35,7 @@
|
||||
"rust-analyzer.cargo.allTargets": false,
|
||||
// ESLint config
|
||||
"eslint.format.enable": true,
|
||||
"eslint.workingDirectories": ["./frontend", "./website/other/bezier-rs-demos", "./website"],
|
||||
"eslint.workingDirectories": ["./frontend", "./website"],
|
||||
"eslint.validate": ["javascript", "typescript", "svelte"],
|
||||
// Svelte config
|
||||
"svelte.plugin.svelte.compilerWarnings": {
|
||||
|
||||
Generated
+19
-41
@@ -461,27 +461,6 @@ version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "bincode"
|
||||
version = "1.3.3"
|
||||
@@ -683,19 +662,20 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
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"
|
||||
checksum = "7bfa138b538b29584b02e990a631ef44261d9a70d35a77ce4ef624410046fe91"
|
||||
checksum = "39b749cfc4124f9505b3fbe32279c0e93f30831f1ecf3c2cf85863179319cd7b"
|
||||
dependencies = [
|
||||
"cef-dll-sys",
|
||||
"libloading",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
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"
|
||||
checksum = "658450855d0ef25af50420b95fbdfacb5df17c7eb20a1615ec995470c26ed643"
|
||||
checksum = "fc42adb0adc477860b705e967d9f899be05ba3775fe4d6dc4d844a1391ffa3dd"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cmake",
|
||||
@@ -1883,7 +1863,6 @@ dependencies = [
|
||||
name = "graph-craft"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bezier-rs",
|
||||
"criterion",
|
||||
"dyn-any",
|
||||
"glam",
|
||||
@@ -1961,7 +1940,6 @@ name = "graphene-core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bezier-rs",
|
||||
"bytemuck",
|
||||
"ctor",
|
||||
"dyn-any",
|
||||
@@ -1974,6 +1952,7 @@ dependencies = [
|
||||
"num-traits",
|
||||
"parley",
|
||||
"petgraph 0.7.1",
|
||||
"poly-cool",
|
||||
"rand 0.9.1",
|
||||
"rand_chacha 0.9.0",
|
||||
"rustc-hash 2.1.1",
|
||||
@@ -2018,7 +1997,6 @@ dependencies = [
|
||||
name = "graphene-path-bool"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bezier-rs",
|
||||
"dyn-any",
|
||||
"glam",
|
||||
"graphene-core",
|
||||
@@ -2033,7 +2011,6 @@ dependencies = [
|
||||
name = "graphene-raster-nodes"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bezier-rs",
|
||||
"bytemuck",
|
||||
"dyn-any",
|
||||
"fastnoise-lite",
|
||||
@@ -2042,6 +2019,7 @@ dependencies = [
|
||||
"graphene-core",
|
||||
"graphene-core-shaders",
|
||||
"image",
|
||||
"kurbo",
|
||||
"ndarray",
|
||||
"node-macro",
|
||||
"rand 0.9.1",
|
||||
@@ -2056,10 +2034,7 @@ name = "graphene-std"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytemuck",
|
||||
"dyn-any",
|
||||
"fastnoise-lite",
|
||||
"futures",
|
||||
"glam",
|
||||
"graph-craft",
|
||||
"graphene-application-io",
|
||||
@@ -2071,10 +2046,7 @@ dependencies = [
|
||||
"graphene-svg-renderer",
|
||||
"image",
|
||||
"log",
|
||||
"ndarray",
|
||||
"node-macro",
|
||||
"rand 0.9.1",
|
||||
"rand_chacha 0.9.0",
|
||||
"reqwest",
|
||||
"tokio",
|
||||
"vello",
|
||||
@@ -2089,10 +2061,10 @@ name = "graphene-svg-renderer"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bezier-rs",
|
||||
"dyn-any",
|
||||
"glam",
|
||||
"graphene-core",
|
||||
"kurbo",
|
||||
"log",
|
||||
"num-traits",
|
||||
"serde",
|
||||
@@ -2113,6 +2085,7 @@ dependencies = [
|
||||
"graph-craft",
|
||||
"graphene-std",
|
||||
"graphite-editor",
|
||||
"image",
|
||||
"include_dir",
|
||||
"open",
|
||||
"rfd",
|
||||
@@ -2130,9 +2103,8 @@ dependencies = [
|
||||
name = "graphite-editor"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"bezier-rs",
|
||||
"base64 0.22.1",
|
||||
"bitflags 2.9.1",
|
||||
"bytemuck",
|
||||
"derivative",
|
||||
"dyn-any",
|
||||
"env_logger",
|
||||
@@ -2148,18 +2120,15 @@ dependencies = [
|
||||
"num_enum",
|
||||
"once_cell",
|
||||
"preprocessor",
|
||||
"ron",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"specta",
|
||||
"spin",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"usvg",
|
||||
"vello",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"wgpu-executor",
|
||||
]
|
||||
@@ -4040,6 +4009,15 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "portable-atomic"
|
||||
version = "1.11.1"
|
||||
|
||||
+21
-10
@@ -20,9 +20,7 @@ members = [
|
||||
"node-graph/preprocessor",
|
||||
"libraries/dyn-any",
|
||||
"libraries/path-bool",
|
||||
"libraries/bezier-rs",
|
||||
"libraries/math-parser",
|
||||
"website/other/bezier-rs-demos/wasm",
|
||||
]
|
||||
default-members = [
|
||||
"editor",
|
||||
@@ -44,9 +42,14 @@ resolver = "2"
|
||||
|
||||
[workspace.dependencies]
|
||||
# Local dependencies
|
||||
bezier-rs = { path = "libraries/bezier-rs", features = ["dyn-any", "serde"] }
|
||||
dyn-any = { path = "libraries/dyn-any", features = ["derive", "glam", "reqwest", "log-bad-types", "rc"] }
|
||||
preprocessor = { path = "node-graph/preprocessor"}
|
||||
dyn-any = { path = "libraries/dyn-any", features = [
|
||||
"derive",
|
||||
"glam",
|
||||
"reqwest",
|
||||
"log-bad-types",
|
||||
"rc",
|
||||
] }
|
||||
preprocessor = { path = "node-graph/preprocessor" }
|
||||
math-parser = { path = "libraries/math-parser" }
|
||||
path-bool = { path = "libraries/path-bool" }
|
||||
graphene-application-io = { path = "node-graph/gapplication-io" }
|
||||
@@ -80,9 +83,8 @@ convert_case = "0.7"
|
||||
derivative = "2.2"
|
||||
thiserror = "2"
|
||||
anyhow = "1.0"
|
||||
proc-macro2 = { version = "1", features = [ "span-locations" ] }
|
||||
proc-macro2 = { version = "1", features = ["span-locations"] }
|
||||
quote = "1.0"
|
||||
axum = "0.8"
|
||||
chrono = "0.4"
|
||||
ron = "0.8"
|
||||
fastnoise-lite = "1.1"
|
||||
@@ -122,9 +124,17 @@ resvg = "0.44"
|
||||
usvg = "0.44"
|
||||
rand = { version = "0.9", default-features = false, features = ["std_rng"] }
|
||||
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"
|
||||
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"
|
||||
skrifa = "0.32.0"
|
||||
pretty_assertions = "1.4.1"
|
||||
@@ -159,12 +169,13 @@ iai-callgrind = { version = "0.12.3" }
|
||||
ndarray = "0.16.1"
|
||||
strum = { version = "0.26.3", features = ["derive"] }
|
||||
dirs = "6.0"
|
||||
cef = "138.5.0"
|
||||
cef = "139.0.1"
|
||||
include_dir = "0.7.4"
|
||||
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
||||
tracing = "0.1.41"
|
||||
rfd = "0.15.4"
|
||||
open = "5.3.2"
|
||||
poly-cool = "0.2.0"
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 1
|
||||
|
||||
Generated
+1
-1
File diff suppressed because one or more lines are too long
Generated
+1
-1
File diff suppressed because one or more lines are too long
Generated
+1
-1
File diff suppressed because one or more lines are too long
Generated
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Generated
+1
-1
File diff suppressed because one or more lines are too long
Generated
+1
-1
File diff suppressed because one or more lines are too long
@@ -39,3 +39,4 @@ vello = { workspace = true }
|
||||
derivative = { workspace = true }
|
||||
rfd = { workspace = true }
|
||||
open = { workspace = true }
|
||||
image = { workspace = true }
|
||||
|
||||
+74
-1
@@ -7,8 +7,12 @@ use crate::dialogs::dialog_save_graphite_file;
|
||||
use crate::render::GraphicsState;
|
||||
use crate::render::WgpuContext;
|
||||
use graph_craft::wasm_application_io::WasmApplicationIo;
|
||||
use graphene_std::Color;
|
||||
use graphene_std::raster::Image;
|
||||
use graphite_editor::application::Editor;
|
||||
use graphite_editor::consts::DEFAULT_DOCUMENT_NAME;
|
||||
use graphite_editor::messages::prelude::*;
|
||||
use std::fs;
|
||||
use std::sync::Arc;
|
||||
use std::sync::mpsc::Sender;
|
||||
use std::thread;
|
||||
@@ -75,7 +79,7 @@ impl WinitApp {
|
||||
String::new()
|
||||
});
|
||||
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,
|
||||
};
|
||||
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 };
|
||||
|
||||
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 => {
|
||||
tracing::info!("The close button was pressed; stopping");
|
||||
event_loop.exit();
|
||||
|
||||
@@ -44,34 +44,34 @@ var s_diffuse: sampler;
|
||||
|
||||
@fragment
|
||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
let ui = textureSample(t_ui, s_diffuse, in.tex_coords);
|
||||
if (ui.a >= 0.999) {
|
||||
return ui;
|
||||
let ui_linear = textureSample(t_ui, s_diffuse, in.tex_coords);
|
||||
if (ui_linear.a >= 0.999) {
|
||||
return ui_linear;
|
||||
}
|
||||
|
||||
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
|
||||
// which renders to an `Srgb` surface, gamma mapping is applied twice. This converts back to linear to compensate.
|
||||
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);
|
||||
let overlay_srgb = textureSample(t_overlays, s_diffuse, viewport_coordinate);
|
||||
let viewport_srgb = textureSample(t_viewport, s_diffuse, viewport_coordinate);
|
||||
|
||||
if (overlay.a < 0.001) {
|
||||
return blend(ui, viewport);
|
||||
// UI texture is premultiplied, we need to unpremultiply before blending
|
||||
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);
|
||||
return blend(ui, composite);
|
||||
}
|
||||
let composite_linear = blend(srgb_to_linear(overlay_srgb), srgb_to_linear(viewport_srgb));
|
||||
|
||||
fn srgb_to_linear(srgb: vec3<f32>) -> vec3<f32> {
|
||||
return select(
|
||||
pow((srgb + 0.055) / 1.055, vec3<f32>(2.4)),
|
||||
srgb / 12.92,
|
||||
srgb <= vec3<f32>(0.04045)
|
||||
);
|
||||
if (ui_srgb.a < 0.001) {
|
||||
return composite_linear;
|
||||
}
|
||||
|
||||
return srgb_to_linear(blend(ui_srgb, linear_to_srgb(composite_linear)));
|
||||
}
|
||||
|
||||
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);
|
||||
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
@@ -12,18 +12,18 @@ license = "Apache-2.0"
|
||||
|
||||
[features]
|
||||
default = ["wasm"]
|
||||
wasm = ["wasm-bindgen", "graphene-std/wasm", "wasm-bindgen-futures"]
|
||||
wasm = ["wasm-bindgen", "graphene-std/wasm"]
|
||||
gpu = ["interpreted-executor/gpu", "wgpu-executor"]
|
||||
resvg = ["graphene-std/resvg"]
|
||||
vello = ["graphene-std/vello", "resvg"]
|
||||
ron = ["dep:ron"]
|
||||
ron = []
|
||||
|
||||
[dependencies]
|
||||
# Local dependencies
|
||||
graphite-proc-macros = { workspace = true }
|
||||
graph-craft = { 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 }
|
||||
|
||||
# Workspace dependencies
|
||||
@@ -33,7 +33,6 @@ bitflags = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
bezier-rs = { workspace = true }
|
||||
kurbo = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
glam = { workspace = true }
|
||||
@@ -44,9 +43,8 @@ num_enum = { workspace = true }
|
||||
usvg = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
web-sys = { workspace = true }
|
||||
bytemuck = { workspace = true }
|
||||
vello = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
|
||||
# Required dependencies
|
||||
spin = "0.9.8"
|
||||
@@ -56,8 +54,6 @@ wgpu-executor = { workspace = true, optional = true }
|
||||
|
||||
# Optional workspace dependencies
|
||||
wasm-bindgen = { workspace = true, optional = true }
|
||||
wasm-bindgen-futures = { workspace = true, optional = true }
|
||||
ron = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
# Workspace dependencies
|
||||
|
||||
@@ -26,7 +26,6 @@ pub struct DispatcherMessageHandlers {
|
||||
pub portfolio_message_handler: PortfolioMessageHandler,
|
||||
preferences_message_handler: PreferencesMessageHandler,
|
||||
tool_message_handler: ToolMessageHandler,
|
||||
workspace_message_handler: WorkspaceMessageHandler,
|
||||
}
|
||||
|
||||
impl DispatcherMessageHandlers {
|
||||
@@ -231,9 +230,6 @@ impl Dispatcher {
|
||||
|
||||
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::Batched { messages } => {
|
||||
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() {
|
||||
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() {
|
||||
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 {
|
||||
open: bool,
|
||||
},
|
||||
UpdateSpreadsheetState {
|
||||
UpdateDataPanelState {
|
||||
open: bool,
|
||||
node: Option<NodeId>,
|
||||
},
|
||||
UpdateSpreadsheetLayout {
|
||||
UpdatePropertiesPanelState {
|
||||
open: bool,
|
||||
},
|
||||
UpdateLayersPanelState {
|
||||
open: bool,
|
||||
},
|
||||
UpdateDataPanelLayout {
|
||||
#[serde(rename = "layoutTarget")]
|
||||
layout_target: LayoutTarget,
|
||||
diff: Vec<WidgetDiff>,
|
||||
@@ -296,7 +301,7 @@ pub enum FrontendMessage {
|
||||
#[serde(rename = "openDocuments")]
|
||||
open_documents: Vec<FrontendDocumentDetails>,
|
||||
},
|
||||
UpdatePropertyPanelSectionsLayout {
|
||||
UpdatePropertiesPanelLayout {
|
||||
#[serde(rename = "layoutTarget")]
|
||||
layout_target: LayoutTarget,
|
||||
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(KeyC); modifiers=[Accel], action_dispatch=PortfolioMessage::Copy { clipboard: Clipboard::Device }),
|
||||
entry!(KeyDown(KeyR); modifiers=[Alt], action_dispatch=PortfolioMessage::ToggleRulers),
|
||||
entry!(KeyDown(KeyD); modifiers=[Alt], action_dispatch=PortfolioMessage::ToggleDataPanelOpen),
|
||||
//
|
||||
// FrontendMessage
|
||||
entry!(KeyDown(KeyV); modifiers=[Accel], action_dispatch=FrontendMessage::TriggerPaste),
|
||||
|
||||
@@ -308,6 +308,7 @@ impl LayoutMessageHandler {
|
||||
|
||||
responses.add(callback_message);
|
||||
}
|
||||
Widget::ImageLabel(_) => {}
|
||||
Widget::IconLabel(_) => {}
|
||||
Widget::InvisibleStandinInput(invisible) => {
|
||||
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));
|
||||
|
||||
let message = match layout_target {
|
||||
LayoutTarget::MenuBar => unreachable!("Menu bar is not diffed"),
|
||||
LayoutTarget::DialogButtons => FrontendMessage::UpdateDialogButtons { layout_target, diff },
|
||||
LayoutTarget::DialogColumn1 => FrontendMessage::UpdateDialogColumn1 { layout_target, diff },
|
||||
LayoutTarget::DialogColumn2 => FrontendMessage::UpdateDialogColumn2 { layout_target, diff },
|
||||
LayoutTarget::DocumentBar => FrontendMessage::UpdateDocumentBarLayout { 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::LayersPanelControlRightBar => FrontendMessage::UpdateLayersPanelControlBarRightLayout { 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::PropertiesSections => FrontendMessage::UpdatePropertyPanelSectionsLayout { layout_target, diff },
|
||||
LayoutTarget::Spreadsheet => FrontendMessage::UpdateSpreadsheetLayout { layout_target, diff },
|
||||
LayoutTarget::ToolOptions => FrontendMessage::UpdateToolOptionsLayout { layout_target, diff },
|
||||
LayoutTarget::ToolShelf => FrontendMessage::UpdateToolShelfLayout { 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.
|
||||
NodeGraphControlBar,
|
||||
/// The body of the Properties panel containing many collapsable sections.
|
||||
PropertiesSections,
|
||||
PropertiesPanel,
|
||||
/// 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.
|
||||
ToolOptions,
|
||||
/// 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::IconLabel(x) => &mut x.tooltip,
|
||||
Widget::ImageButton(x) => &mut x.tooltip,
|
||||
Widget::ImageLabel(x) => &mut x.tooltip,
|
||||
Widget::NumberInput(x) => &mut x.tooltip,
|
||||
Widget::ParameterExposeButton(x) => &mut x.tooltip,
|
||||
Widget::PopoverButton(x) => &mut x.tooltip,
|
||||
@@ -546,6 +547,7 @@ pub enum Widget {
|
||||
IconButton(IconButton),
|
||||
IconLabel(IconLabel),
|
||||
ImageButton(ImageButton),
|
||||
ImageLabel(ImageLabel),
|
||||
InvisibleStandinInput(InvisibleStandinInput),
|
||||
NodeCatalog(NodeCatalog),
|
||||
NumberInput(NumberInput),
|
||||
@@ -622,6 +624,7 @@ impl DiffUpdate {
|
||||
Widget::TextButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
|
||||
Widget::ImageButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
|
||||
Widget::IconLabel(_)
|
||||
| Widget::ImageLabel(_)
|
||||
| Widget::CurveInput(_)
|
||||
| Widget::InvisibleStandinInput(_)
|
||||
| Widget::NodeCatalog(_)
|
||||
|
||||
@@ -168,8 +168,6 @@ pub struct ColorInput {
|
||||
#[widget_builder(constructor)]
|
||||
pub value: FillChoice,
|
||||
|
||||
pub disabled: bool,
|
||||
|
||||
// TODO: Implement
|
||||
// #[serde(rename = "allowTransparency")]
|
||||
// #[derivative(Default(value = "false"))]
|
||||
@@ -179,9 +177,11 @@ pub struct ColorInput {
|
||||
#[derivative(Default(value = "true"))]
|
||||
pub allow_none: bool,
|
||||
|
||||
// TODO: Implement
|
||||
// pub disabled: bool,
|
||||
//
|
||||
pub disabled: bool,
|
||||
|
||||
#[serde(rename = "menuDirection")]
|
||||
pub menu_direction: Option<MenuDirection>,
|
||||
|
||||
pub tooltip: String,
|
||||
|
||||
#[serde(skip)]
|
||||
|
||||
@@ -416,6 +416,9 @@ pub struct TextInput {
|
||||
#[serde(rename = "minWidth")]
|
||||
pub min_width: u32,
|
||||
|
||||
#[serde(rename = "maxWidth")]
|
||||
pub max_width: u32,
|
||||
|
||||
// Callbacks
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
|
||||
@@ -65,4 +65,17 @@ pub struct TextLabel {
|
||||
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
|
||||
|
||||
@@ -33,8 +33,6 @@ pub enum Message {
|
||||
Preferences(PreferencesMessage),
|
||||
#[child]
|
||||
Tool(ToolMessage),
|
||||
#[child]
|
||||
Workspace(WorkspaceMessage),
|
||||
|
||||
// Messages
|
||||
NoOp,
|
||||
@@ -71,7 +69,7 @@ mod test {
|
||||
|
||||
fn print_tree_node(tree: &DebugMessageTree, prefix: &str, is_last: bool, file: &mut std::fs::File) {
|
||||
// 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))
|
||||
} else {
|
||||
if is_last {
|
||||
@@ -99,7 +97,7 @@ mod test {
|
||||
// Print handler field if any
|
||||
if let Some(data) = tree.message_handler_fields() {
|
||||
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))
|
||||
} else {
|
||||
("└── ", format!("{} ", prefix))
|
||||
|
||||
@@ -16,4 +16,3 @@ pub mod portfolio;
|
||||
pub mod preferences;
|
||||
pub mod prelude;
|
||||
pub mod tool;
|
||||
pub mod workspace;
|
||||
|
||||
+8
-8
@@ -1,16 +1,15 @@
|
||||
use crate::messages::prelude::*;
|
||||
use crate::node_graph_executor::InspectResult;
|
||||
|
||||
/// The spreadsheet UI allows for graph data to be previewed.
|
||||
#[impl_message(Message, PortfolioMessage, Spreadsheet)]
|
||||
/// The Data panel UI allows the user to visualize the output data of the selected node.
|
||||
#[impl_message(Message, DocumentMessage, DataPanel)]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum SpreadsheetMessage {
|
||||
ToggleOpen,
|
||||
|
||||
pub enum DataPanelMessage {
|
||||
UpdateLayout {
|
||||
#[serde(skip)]
|
||||
inspect_result: InspectResult,
|
||||
},
|
||||
ClearLayout,
|
||||
|
||||
PushToElementPath {
|
||||
index: usize,
|
||||
@@ -19,14 +18,15 @@ pub enum SpreadsheetMessage {
|
||||
len: usize,
|
||||
},
|
||||
|
||||
ViewVectorDomain {
|
||||
domain: VectorDomain,
|
||||
ViewVectorTableTab {
|
||||
tab: VectorTableTab,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum VectorDomain {
|
||||
pub enum VectorTableTab {
|
||||
#[default]
|
||||
Properties,
|
||||
Points,
|
||||
Segments,
|
||||
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 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::OverlaysType;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
@@ -33,6 +34,8 @@ pub enum DocumentMessage {
|
||||
Overlays(OverlaysMessage),
|
||||
#[child]
|
||||
PropertiesPanel(PropertiesPanelMessage),
|
||||
#[child]
|
||||
DataPanel(DataPanelMessage),
|
||||
|
||||
// Messages
|
||||
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::messages::input_mapper::utility_types::macros::action_keys;
|
||||
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::node_graph::NodeGraphMessageContext;
|
||||
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::network_interface::{FlowType, InputConnector, NodeTemplate};
|
||||
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::prelude::*;
|
||||
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::utility_types::ToolType;
|
||||
use crate::node_graph_executor::NodeGraphExecutor;
|
||||
use bezier_rs::Subpath;
|
||||
use glam::{DAffine2, DVec2, IVec2};
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
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::raster::BlendMode;
|
||||
use graphene_std::raster_types::Raster;
|
||||
use graphene_std::subpath::Subpath;
|
||||
use graphene_std::table::Table;
|
||||
use graphene_std::vector::PointId;
|
||||
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 kurbo::{Affine, CubicBez, Line, ParamCurve, PathSeg, QuadBez};
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -49,6 +53,9 @@ pub struct DocumentMessageContext<'a> {
|
||||
pub current_tool: &'a ToolType,
|
||||
pub preferences: &'a PreferencesMessageHandler,
|
||||
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)]
|
||||
@@ -63,9 +70,11 @@ pub struct DocumentMessageHandler {
|
||||
#[serde(skip)]
|
||||
pub node_graph_handler: NodeGraphMessageHandler,
|
||||
#[serde(skip)]
|
||||
overlays_message_handler: OverlaysMessageHandler,
|
||||
pub overlays_message_handler: OverlaysMessageHandler,
|
||||
#[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
|
||||
@@ -144,6 +153,7 @@ impl Default for DocumentMessageHandler {
|
||||
node_graph_handler: NodeGraphMessageHandler::default(),
|
||||
overlays_message_handler: OverlaysMessageHandler::default(),
|
||||
properties_panel_message_handler: PropertiesPanelMessageHandler::default(),
|
||||
data_panel_message_handler: DataPanelMessageHandler::default(),
|
||||
// ============================================
|
||||
// Fields that are saved in the document format
|
||||
// ============================================
|
||||
@@ -186,6 +196,9 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
current_tool,
|
||||
preferences,
|
||||
device_pixel_ratio,
|
||||
data_panel_open,
|
||||
layers_panel_open,
|
||||
properties_panel_open,
|
||||
} = context;
|
||||
|
||||
match message {
|
||||
@@ -223,9 +236,20 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
document_name: self.name.as_str(),
|
||||
executor,
|
||||
persistent_data,
|
||||
properties_panel_open,
|
||||
};
|
||||
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) => {
|
||||
self.node_graph_handler.process_message(
|
||||
message,
|
||||
@@ -241,6 +265,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
graph_fade_artwork_percentage: self.graph_fade_artwork_percentage,
|
||||
navigation_handler: &self.navigation_handler,
|
||||
preferences,
|
||||
layers_panel_open,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -356,12 +381,15 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
DocumentMessage::DocumentHistoryBackward => self.undo_with_history(ipp, responses),
|
||||
DocumentMessage::DocumentHistoryForward => self.redo_with_history(ipp, responses),
|
||||
DocumentMessage::DocumentStructureChanged => {
|
||||
self.update_layers_panel_control_bar_widgets(responses);
|
||||
self.update_layers_panel_bottom_bar_widgets(responses);
|
||||
if layers_panel_open {
|
||||
self.network_interface.load_structure();
|
||||
let data_buffer: RawBuffer = self.serialize_root();
|
||||
|
||||
self.network_interface.load_structure();
|
||||
let data_buffer: RawBuffer = self.serialize_root();
|
||||
responses.add(FrontendMessage::UpdateDocumentLayerStructure { data_buffer });
|
||||
self.update_layers_panel_control_bar_widgets(layers_panel_open, responses);
|
||||
self.update_layers_panel_bottom_bar_widgets(layers_panel_open, responses);
|
||||
|
||||
responses.add(FrontendMessage::UpdateDocumentLayerStructure { data_buffer });
|
||||
}
|
||||
}
|
||||
DocumentMessage::DrawArtboardOverlays(overlay_context) => {
|
||||
if !overlay_context.visibility_settings.artboard_name() {
|
||||
@@ -554,6 +582,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
responses.add(NodeGraphMessage::UpdateHints);
|
||||
} else {
|
||||
responses.add(ToolMessage::ActivateTool { tool_type: *current_tool });
|
||||
responses.add(OverlaysMessage::Draw); // Redraw overlays when graph is closed
|
||||
}
|
||||
}
|
||||
DocumentMessage::GraphViewOverlayToggle => {
|
||||
@@ -1128,7 +1157,6 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
}
|
||||
}
|
||||
DocumentMessage::SetActivePanel { active_panel: panel } => {
|
||||
use crate::messages::portfolio::utility_types::PanelType;
|
||||
match panel {
|
||||
PanelType::Document => {
|
||||
if self.graph_view_overlay_open {
|
||||
@@ -2549,7 +2577,11 @@ impl DocumentMessageHandler {
|
||||
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).
|
||||
let selected_nodes = self.network_interface.selected_nodes();
|
||||
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 mut selected_layers = selected_nodes.selected_layers(self.metadata());
|
||||
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> {
|
||||
let segment = |bezier: bezier_rs::Bezier| match bezier.handles {
|
||||
bezier_rs::BezierHandles::Linear => path_bool_lib::PathSegment::Line(bezier.start, bezier.end),
|
||||
bezier_rs::BezierHandles::Quadratic { handle } => path_bool_lib::PathSegment::Quadratic(bezier.start, handle, bezier.end),
|
||||
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => path_bool_lib::PathSegment::Cubic(bezier.start, handle_start, handle_end, bezier.end),
|
||||
let segment = |bezier: PathSeg| match bezier {
|
||||
PathSeg::Line(line) => path_bool_lib::PathSegment::Line(point_to_dvec2(line.p0), point_to_dvec2(line.p1)),
|
||||
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)),
|
||||
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
|
||||
.filter_map(|target| {
|
||||
@@ -2963,7 +2999,7 @@ fn click_targets_to_path_lib_segments<'a>(click_targets: impl Iterator<Item = &'
|
||||
}
|
||||
})
|
||||
.flatten()
|
||||
.map(|bezier| segment(bezier.apply_transformation(|x| transform.transform_point2(x))))
|
||||
.map(|bezier| segment(Affine::new(transform.to_cols_array()) * bezier))
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -2986,11 +3022,11 @@ impl<'a> ClickXRayIter<'a> {
|
||||
|
||||
/// 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 {
|
||||
// Convert back to Bezier-rs types for intersections
|
||||
// Convert back to Kurbo types for intersections
|
||||
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::Cubic(start, h1, h2, end) => bezier_rs::Bezier::from_cubic_dvec2(start, h1, h2, end),
|
||||
path_bool_lib::PathSegment::Quadratic(start, h1, end) => bezier_rs::Bezier::from_quadratic_dvec2(start, h1, 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) => 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) => PathSeg::Quad(QuadBez::new(dvec2_to_point(start), dvec2_to_point(h1), dvec2_to_point(end))),
|
||||
path_bool_lib::PathSegment::Arc(_, _, _, _, _, _, _) => unimplemented!(),
|
||||
};
|
||||
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::Path(path) => self.check_layer_area_target(click_targets, clip, layer, path.clone(), transform),
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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::network_interface::NodeTemplate;
|
||||
use crate::messages::prelude::*;
|
||||
use bezier_rs::Subpath;
|
||||
use glam::{DAffine2, IVec2};
|
||||
use graph_craft::document::NodeId;
|
||||
use graphene_std::Artboard;
|
||||
use graphene_std::brush::brush_stroke::BrushStroke;
|
||||
use graphene_std::raster::BlendMode;
|
||||
use graphene_std::raster_types::{CPU, Raster};
|
||||
use graphene_std::subpath::Subpath;
|
||||
use graphene_std::table::Table;
|
||||
use graphene_std::text::{Font, TypesettingConfig};
|
||||
use graphene_std::vector::PointId;
|
||||
|
||||
-2
@@ -426,7 +426,6 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, t
|
||||
Fill::Gradient(Gradient {
|
||||
start,
|
||||
end,
|
||||
transform: DAffine2::IDENTITY,
|
||||
gradient_type: GradientType::Linear,
|
||||
stops,
|
||||
})
|
||||
@@ -453,7 +452,6 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, t
|
||||
Fill::Gradient(Gradient {
|
||||
start,
|
||||
end,
|
||||
transform: DAffine2::IDENTITY,
|
||||
gradient_type: GradientType::Radial,
|
||||
stops,
|
||||
})
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeNetworkInterface};
|
||||
use bezier_rs::Subpath;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{NodeId, NodeInput};
|
||||
use graphene_std::subpath::Subpath;
|
||||
use graphene_std::vector::PointId;
|
||||
|
||||
/// 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::network_interface::{self, InputConnector, NodeNetworkInterface, OutputConnector};
|
||||
use crate::messages::prelude::*;
|
||||
use bezier_rs::Subpath;
|
||||
use glam::{DAffine2, IVec2};
|
||||
use graph_craft::concrete;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
@@ -12,6 +11,7 @@ use graphene_std::Artboard;
|
||||
use graphene_std::brush::brush_stroke::BrushStroke;
|
||||
use graphene_std::raster::BlendMode;
|
||||
use graphene_std::raster_types::{CPU, Raster};
|
||||
use graphene_std::subpath::Subpath;
|
||||
use graphene_std::table::Table;
|
||||
use graphene_std::text::{Font, TypesettingConfig};
|
||||
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::DVec2(artboard.location.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)),
|
||||
]);
|
||||
self.network_interface.insert_node(new_id, artboard_node_template, &[]);
|
||||
@@ -329,11 +329,11 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
match &fill {
|
||||
Fill::None => {
|
||||
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) => {
|
||||
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) => {
|
||||
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) {
|
||||
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);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::OptionalColor(stroke.color), false), true);
|
||||
let stroke_color = if let Some(color) = stroke.color { Table::new_from_element(color) } else { Table::new() };
|
||||
|
||||
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);
|
||||
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);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
mod document_message;
|
||||
mod document_message_handler;
|
||||
|
||||
pub mod data_panel;
|
||||
pub mod graph_operation;
|
||||
pub mod navigation;
|
||||
pub mod node_graph;
|
||||
|
||||
@@ -399,7 +399,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
NodeInput::value(TaggedValue::Graphic(Default::default()), true),
|
||||
NodeInput::value(TaggedValue::DVec2(DVec2::ZERO), 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),
|
||||
],
|
||||
..Default::default()
|
||||
|
||||
@@ -42,6 +42,7 @@ pub struct NodeGraphMessageContext<'a> {
|
||||
pub graph_fade_artwork_percentage: f64,
|
||||
pub navigation_handler: &'a NavigationMessageHandler,
|
||||
pub preferences: &'a PreferencesMessageHandler,
|
||||
pub layers_panel_open: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, ExtractField)]
|
||||
@@ -88,7 +89,7 @@ pub struct NodeGraphMessageHandler {
|
||||
reordering_import: Option<usize>,
|
||||
/// The index of the export that is being moved
|
||||
reordering_export: Option<usize>,
|
||||
/// The end index of the moved port
|
||||
/// The end index of the moved connector
|
||||
end_index: Option<usize>,
|
||||
/// Used to keep track of what nodes are sent to the front end so that only visible ones are sent to the frontend
|
||||
frontend_nodes: Vec<NodeId>,
|
||||
@@ -111,6 +112,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
graph_fade_artwork_percentage,
|
||||
navigation_handler,
|
||||
preferences,
|
||||
layers_panel_open,
|
||||
} = context;
|
||||
|
||||
match message {
|
||||
@@ -155,11 +157,13 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
}
|
||||
responses.add(MenuBarMessage::SendLayout);
|
||||
responses.add(NodeGraphMessage::UpdateLayerPanel);
|
||||
responses.add(PropertiesPanelMessage::Refresh);
|
||||
responses.add(NodeGraphMessage::SendSelectedNodes);
|
||||
responses.add(ArtboardToolMessage::UpdateSelectedArtboard);
|
||||
responses.add(DocumentMessage::DocumentStructureChanged);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
responses.add(NodeGraphMessage::SendGraph);
|
||||
responses.add(PortfolioMessage::SubmitActiveGraphRender);
|
||||
}
|
||||
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
|
||||
@@ -1215,7 +1219,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
{
|
||||
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 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);
|
||||
responses.add(BroadcastEvent::SelectionChanged);
|
||||
responses.add(PropertiesPanelMessage::Refresh);
|
||||
}
|
||||
NodeGraphMessage::SendClickTargets => responses.add(FrontendMessage::UpdateClickTargets {
|
||||
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::RenderScrollbars);
|
||||
responses.add(NodeGraphMessage::SendGraph);
|
||||
responses.add(OverlaysMessage::Draw); // Redraw overlays to update artboard names
|
||||
}
|
||||
NodeGraphMessage::SetDisplayNameImpl { node_id, alias } => {
|
||||
network_interface.set_display_name(&node_id, alias, selection_network_path);
|
||||
@@ -1833,7 +1837,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
continue;
|
||||
};
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1873,7 +1877,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
}
|
||||
|
||||
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 => {
|
||||
// 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()))
|
||||
.tooltip("Add an operation to the end of this layer's chain of nodes")
|
||||
.popover_layout({
|
||||
let layer_identifier = LayerNodeIdentifier::new(layer, &context.network_interface);
|
||||
let layer_identifier = LayerNodeIdentifier::new(layer, context.network_interface);
|
||||
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);
|
||||
if let Some(node_id) = node_type {
|
||||
let (output_type, _) = context.network_interface.output_type(&node_id, 0, &[]);
|
||||
@@ -2413,7 +2417,7 @@ impl NodeGraphMessageHandler {
|
||||
} else {
|
||||
added_wires.push(WirePathUpdate {
|
||||
id: NodeId(u64::MAX),
|
||||
input_index: usize::MAX,
|
||||
input_index: u32::MAX as usize,
|
||||
wire_path_update: None,
|
||||
})
|
||||
}
|
||||
@@ -2585,7 +2589,11 @@ impl NodeGraphMessageHandler {
|
||||
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
|
||||
.selected_nodes()
|
||||
.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
|
||||
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,
|
||||
SelectiveColorChoice,
|
||||
};
|
||||
use graphene_std::table::{Table, TableRow};
|
||||
use graphene_std::text::{Font, TextAlign};
|
||||
use graphene_std::transform::{Footprint, ReferencePoint, Transform};
|
||||
use graphene_std::vector::misc::{ArcType, CentroidType, GridType, MergeByDistanceAlgorithm, PointSpacingType};
|
||||
@@ -180,8 +181,8 @@ pub(crate) fn property_from_type(
|
||||
// ============
|
||||
// STRUCT TYPES
|
||||
// ============
|
||||
Some(x) if x == TypeId::of::<Color>() => color_widget(default_info, ColorInput::default().allow_none(false)),
|
||||
Some(x) if x == TypeId::of::<Option<Color>>() => color_widget(default_info, ColorInput::default().allow_none(true)),
|
||||
Some(x) if x == TypeId::of::<Table<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::<Font>() => font_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
|
||||
match &**tagged_value {
|
||||
TaggedValue::Color(color) => widgets.push(
|
||||
TaggedValue::Color(color_table) => widgets.push(
|
||||
color_button
|
||||
.value(FillChoice::Solid(*color))
|
||||
.on_update(update_value(|x: &ColorInput| TaggedValue::Color(x.value.as_solid().unwrap_or_default()), node_id, index))
|
||||
.on_commit(commit_value)
|
||||
.widget_holder(),
|
||||
),
|
||||
TaggedValue::OptionalColor(color) => widgets.push(
|
||||
color_button
|
||||
.value(match color {
|
||||
Some(color) => FillChoice::Solid(*color),
|
||||
.value(match color_table.iter().next() {
|
||||
Some(color) => FillChoice::Solid(*color.element),
|
||||
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)
|
||||
.widget_holder(),
|
||||
),
|
||||
TaggedValue::GradientStops(x) => widgets.push(
|
||||
TaggedValue::GradientTable(gradient_table) => widgets.push(
|
||||
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(
|
||||
|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,
|
||||
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[BackupColorInput::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 }];
|
||||
};
|
||||
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();
|
||||
|
||||
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 {
|
||||
node_id,
|
||||
input_index: BackupColorInput::INDEX,
|
||||
value: TaggedValue::OptionalColor(None),
|
||||
value: TaggedValue::Color(Table::new()),
|
||||
}
|
||||
.into(),
|
||||
Fill::Solid(color) => NodeGraphMessage::SetInputValue {
|
||||
node_id,
|
||||
input_index: BackupColorInput::INDEX,
|
||||
value: TaggedValue::OptionalColor(Some(*color)),
|
||||
value: TaggedValue::Color(Table::new_from_element(*color)),
|
||||
}
|
||||
.into(),
|
||||
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 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(),
|
||||
);
|
||||
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 {
|
||||
#[default]
|
||||
General,
|
||||
Number,
|
||||
Artboard,
|
||||
Graphic,
|
||||
Raster,
|
||||
Vector,
|
||||
Number,
|
||||
Graphic,
|
||||
Artboard,
|
||||
Color,
|
||||
Gradient,
|
||||
Typography,
|
||||
}
|
||||
|
||||
impl FrontendGraphDataType {
|
||||
pub fn from_type(input: &Type) -> Self {
|
||||
match TaggedValue::from_type_or_none(input) {
|
||||
TaggedValue::Raster(_) => Self::Raster,
|
||||
TaggedValue::Vector(_) => Self::Vector,
|
||||
TaggedValue::U32(_)
|
||||
| TaggedValue::U64(_)
|
||||
| TaggedValue::F64(_)
|
||||
@@ -28,8 +29,13 @@ impl FrontendGraphDataType {
|
||||
| TaggedValue::VecF64(_)
|
||||
| TaggedValue::VecDVec2(_)
|
||||
| TaggedValue::DAffine2(_) => Self::Number,
|
||||
TaggedValue::Graphic(_) => Self::Graphic,
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -177,8 +183,8 @@ pub struct FrontendClickTargets {
|
||||
pub node_click_targets: Vec<String>,
|
||||
#[serde(rename = "layerClickTargets")]
|
||||
pub layer_click_targets: Vec<String>,
|
||||
#[serde(rename = "portClickTargets")]
|
||||
pub port_click_targets: Vec<String>,
|
||||
#[serde(rename = "connectorClickTargets")]
|
||||
pub connector_click_targets: Vec<String>,
|
||||
#[serde(rename = "iconClickTargets")]
|
||||
pub icon_click_targets: Vec<String>,
|
||||
#[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::tool::common_functionality::shape_editor::{SelectedLayerState, ShapeState};
|
||||
use crate::messages::tool::tool_messages::tool_prelude::{DocumentMessageHandler, PreferencesMessageHandler};
|
||||
use bezier_rs::{Bezier, BezierHandles};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graphene_std::subpath::{Bezier, BezierHandles};
|
||||
use graphene_std::vector::misc::ManipulatorPointId;
|
||||
use graphene_std::vector::{PointId, SegmentId};
|
||||
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
|
||||
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 {
|
||||
continue;
|
||||
};
|
||||
|
||||
@@ -5,14 +5,16 @@ use crate::consts::{
|
||||
PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, SEGMENT_SELECTED_THICKNESS,
|
||||
};
|
||||
use crate::messages::prelude::Message;
|
||||
use bezier_rs::{Bezier, Subpath};
|
||||
use core::borrow::Borrow;
|
||||
use core::f64::consts::{FRAC_PI_2, PI, TAU};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graphene_std::Color;
|
||||
use graphene_std::math::quad::Quad;
|
||||
use graphene_std::subpath::Subpath;
|
||||
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 kurbo::{self, Affine, CubicBez, ParamCurve, PathSeg};
|
||||
use std::collections::HashMap;
|
||||
use wasm_bindgen::{JsCast, JsValue};
|
||||
use web_sys::{OffscreenCanvas, OffscreenCanvasRenderingContext2d};
|
||||
@@ -571,11 +573,7 @@ impl OverlayContext {
|
||||
let handle_start = start + start_vec.perp() * radius * factor;
|
||||
let handle_end = end - end_vec.perp() * radius * factor;
|
||||
|
||||
let bezier = Bezier {
|
||||
start,
|
||||
end,
|
||||
handles: bezier_rs::BezierHandles::Cubic { handle_start, handle_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)));
|
||||
|
||||
self.bezier_command(bezier, DAffine2::IDENTITY, i == 0);
|
||||
}
|
||||
@@ -762,7 +760,7 @@ impl OverlayContext {
|
||||
|
||||
self.render_context.begin_path();
|
||||
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);
|
||||
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.
|
||||
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.render_context.begin_path();
|
||||
@@ -788,7 +786,7 @@ impl OverlayContext {
|
||||
}
|
||||
|
||||
/// 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.render_context.begin_path();
|
||||
@@ -802,7 +800,7 @@ impl OverlayContext {
|
||||
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.render_context.begin_path();
|
||||
@@ -816,18 +814,18 @@ impl OverlayContext {
|
||||
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();
|
||||
|
||||
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 {
|
||||
self.render_context.move_to(start.x, start.y);
|
||||
self.render_context.move_to(bezier.start().x, bezier.start().y);
|
||||
}
|
||||
|
||||
match handles {
|
||||
bezier_rs::BezierHandles::Linear => self.render_context.line_to(end.x, end.y),
|
||||
bezier_rs::BezierHandles::Quadratic { handle } => self.render_context.quadratic_curve_to(handle.x, handle.y, end.x, end.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),
|
||||
match bezier.as_path_el() {
|
||||
kurbo::PathEl::LineTo(point) => self.render_context.line_to(point.x, point.y),
|
||||
kurbo::PathEl::QuadTo(point, point1) => self.render_context.quadratic_curve_to(point.x, point.y, point1.x, point1.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),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
self.end_dpi_aware_transform();
|
||||
@@ -841,36 +839,35 @@ impl OverlayContext {
|
||||
let subpath = subpath.borrow();
|
||||
let mut curves = subpath.iter().peekable();
|
||||
|
||||
let Some(first) = curves.peek() else {
|
||||
let Some(&first) = curves.peek() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
self.render_context.move_to(transform.transform_point2(first.start()).x, transform.transform_point2(first.start()).y);
|
||||
for curve in curves {
|
||||
match curve.handles {
|
||||
bezier_rs::BezierHandles::Linear => {
|
||||
let a = transform.transform_point2(curve.end());
|
||||
let a = a.round() - DVec2::splat(0.5);
|
||||
let start_point = transform.transform_point2(point_to_dvec2(first.start()));
|
||||
self.render_context.move_to(start_point.x, start_point.y);
|
||||
|
||||
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 } => {
|
||||
let a = transform.transform_point2(handle);
|
||||
let b = transform.transform_point2(curve.end());
|
||||
PathSeg::Quad(quad_bez) => {
|
||||
let a = transform.transform_point2(point_to_dvec2(quad_bez.p1));
|
||||
let b = transform.transform_point2(point_to_dvec2(quad_bez.p2));
|
||||
let a = a.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 } => {
|
||||
let a = transform.transform_point2(handle_start);
|
||||
let b = transform.transform_point2(handle_end);
|
||||
let c = transform.transform_point2(curve.end());
|
||||
PathSeg::Cubic(cubic_bez) => {
|
||||
let a = transform.transform_point2(point_to_dvec2(cubic_bez.p1));
|
||||
let b = transform.transform_point2(point_to_dvec2(cubic_bez.p2));
|
||||
let c = transform.transform_point2(point_to_dvec2(cubic_bez.p3));
|
||||
let a = a.round() - DVec2::splat(0.5);
|
||||
let b = b.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.
|
||||
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() {
|
||||
ClickTargetType::FreePoint(point) => {
|
||||
|
||||
@@ -4,20 +4,22 @@ use crate::consts::{
|
||||
PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER,
|
||||
};
|
||||
use crate::messages::prelude::Message;
|
||||
use bezier_rs::{Bezier, Subpath};
|
||||
use core::borrow::Borrow;
|
||||
use core::f64::consts::{FRAC_PI_2, PI, TAU};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graphene_std::Color;
|
||||
use graphene_std::math::quad::Quad;
|
||||
use graphene_std::subpath::{self, Subpath};
|
||||
use graphene_std::table::Table;
|
||||
use graphene_std::text::{TextAlign, TypesettingConfig, load_font, to_path};
|
||||
use graphene_std::vector::click_target::ClickTargetType;
|
||||
use graphene_std::vector::misc::point_to_dvec2;
|
||||
use graphene_std::vector::{PointId, SegmentId, Vector};
|
||||
use kurbo::{self, BezPath, ParamCurve};
|
||||
use kurbo::{Affine, PathSeg};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex, MutexGuard};
|
||||
use vello::Scene;
|
||||
use vello::kurbo::{self, BezPath};
|
||||
use vello::peniko;
|
||||
|
||||
pub type OverlayProvider = fn(OverlayContext) -> Message;
|
||||
@@ -200,6 +202,7 @@ impl core::hash::Hash for OverlayContext {
|
||||
}
|
||||
|
||||
impl OverlayContext {
|
||||
#[allow(dead_code)]
|
||||
pub(super) fn new(size: DVec2, device_pixel_ratio: f64, visibility_settings: OverlaysVisibilitySettings) -> Self {
|
||||
Self {
|
||||
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.
|
||||
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);
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -842,7 +845,7 @@ impl OverlayContextInternal {
|
||||
let mut path = BezPath::new();
|
||||
|
||||
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);
|
||||
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.
|
||||
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 mut path = BezPath::new();
|
||||
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.
|
||||
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 mut path = BezPath::new();
|
||||
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);
|
||||
}
|
||||
|
||||
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 mut path = BezPath::new();
|
||||
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);
|
||||
}
|
||||
|
||||
fn bezier_to_path(&self, bezier: Bezier, transform: DAffine2, move_to: bool, path: &mut BezPath) {
|
||||
let Bezier { start, end, handles } = bezier.apply_transformation(|point| transform.transform_point2(point));
|
||||
fn bezier_to_path(&self, bezier: PathSeg, transform: DAffine2, move_to: bool, path: &mut BezPath) {
|
||||
let bezier = Affine::new(transform.to_cols_array()) * bezier;
|
||||
if move_to {
|
||||
path.move_to(kurbo::Point::new(start.x, start.y));
|
||||
}
|
||||
|
||||
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.move_to(bezier.start());
|
||||
}
|
||||
path.push(bezier.as_path_el());
|
||||
}
|
||||
|
||||
fn push_path(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2) -> BezPath {
|
||||
@@ -906,27 +900,27 @@ impl OverlayContextInternal {
|
||||
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));
|
||||
|
||||
for curve in curves {
|
||||
match curve.handles {
|
||||
bezier_rs::BezierHandles::Linear => {
|
||||
let a = transform.transform_point2(curve.end());
|
||||
match curve {
|
||||
PathSeg::Line(line) => {
|
||||
let a = transform.transform_point2(point_to_dvec2(line.p1));
|
||||
let a = a.round() - DVec2::splat(0.5);
|
||||
path.line_to(kurbo::Point::new(a.x, a.y));
|
||||
}
|
||||
bezier_rs::BezierHandles::Quadratic { handle } => {
|
||||
let a = transform.transform_point2(handle);
|
||||
let b = transform.transform_point2(curve.end());
|
||||
PathSeg::Quad(quad_bez) => {
|
||||
let a = transform.transform_point2(point_to_dvec2(quad_bez.p1));
|
||||
let b = transform.transform_point2(point_to_dvec2(quad_bez.p2));
|
||||
let a = a.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));
|
||||
}
|
||||
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => {
|
||||
let a = transform.transform_point2(handle_start);
|
||||
let b = transform.transform_point2(handle_end);
|
||||
let c = transform.transform_point2(curve.end());
|
||||
PathSeg::Cubic(cubic_bez) => {
|
||||
let a = transform.transform_point2(point_to_dvec2(cubic_bez.p1));
|
||||
let b = transform.transform_point2(point_to_dvec2(cubic_bez.p2));
|
||||
let c = transform.transform_point2(point_to_dvec2(cubic_bez.p3));
|
||||
let a = a.round() - DVec2::splat(0.5);
|
||||
let b = b.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.
|
||||
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 {
|
||||
match target_type.borrow() {
|
||||
@@ -1118,13 +1112,13 @@ impl OverlayContextInternal {
|
||||
|
||||
// Add handle points if they exist
|
||||
match transformed_bezier.handles {
|
||||
bezier_rs::BezierHandles::Quadratic { handle } => {
|
||||
subpath::BezierHandles::Quadratic { handle } => {
|
||||
min_x = min_x.min(handle.x);
|
||||
min_y = min_y.min(handle.y);
|
||||
max_x = max_x.max(handle.x);
|
||||
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] {
|
||||
min_x = min_x.min(handle.x);
|
||||
min_y = min_y.min(handle.y);
|
||||
@@ -1154,7 +1148,7 @@ impl OverlayContextInternal {
|
||||
let mut path = BezPath::new();
|
||||
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);
|
||||
last_point = Some(end_id);
|
||||
|
||||
|
||||
+9
-2
@@ -14,6 +14,7 @@ pub struct PropertiesPanelMessageContext<'a> {
|
||||
pub document_name: &'a str,
|
||||
pub executor: &'a mut NodeGraphExecutor,
|
||||
pub persistent_data: &'a PersistentData,
|
||||
pub properties_panel_open: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, ExtractField)]
|
||||
@@ -28,16 +29,22 @@ impl MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageContext<'_>> f
|
||||
document_name,
|
||||
executor,
|
||||
persistent_data,
|
||||
properties_panel_open,
|
||||
} = context;
|
||||
|
||||
match message {
|
||||
PropertiesPanelMessage::Clear => {
|
||||
responses.add(LayoutMessage::SendLayout {
|
||||
layout: Layout::WidgetLayout(WidgetLayout::new(vec![])),
|
||||
layout_target: LayoutTarget::PropertiesSections,
|
||||
layout_target: LayoutTarget::PropertiesPanel,
|
||||
});
|
||||
}
|
||||
PropertiesPanelMessage::Refresh => {
|
||||
if !properties_panel_open {
|
||||
responses.add(PropertiesPanelMessage::Clear);
|
||||
return;
|
||||
}
|
||||
|
||||
let mut node_properties_context = NodePropertiesContext {
|
||||
persistent_data,
|
||||
responses,
|
||||
@@ -50,7 +57,7 @@ impl MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageContext<'_>> f
|
||||
|
||||
node_properties_context.responses.add(LayoutMessage::SendLayout {
|
||||
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 graph_craft::document::NodeId;
|
||||
use graphene_std::math::quad::Quad;
|
||||
use graphene_std::subpath;
|
||||
use graphene_std::transform::Footprint;
|
||||
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
|
||||
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)
|
||||
}
|
||||
|
||||
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();
|
||||
let click_targets = self.click_targets.get(&layer).unwrap_or(&EMPTY);
|
||||
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::tool::common_functionality::graph_modification_utils;
|
||||
use crate::messages::tool::tool_messages::tool_prelude::NumberInputMode;
|
||||
use bezier_rs::Subpath;
|
||||
use glam::{DAffine2, DVec2, IVec2};
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork, OldDocumentNodeImplementation, OldNodeNetwork};
|
||||
use graph_craft::{Type, concrete};
|
||||
use graphene_std::Artboard;
|
||||
use graphene_std::math::quad::Quad;
|
||||
use graphene_std::subpath::Subpath;
|
||||
use graphene_std::table::Table;
|
||||
use graphene_std::transform::Footprint;
|
||||
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 clip_input = artboard.unwrap().inputs.get(5).unwrap();
|
||||
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(
|
||||
self.document_metadata.bounding_box_document(layer).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::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()
|
||||
}
|
||||
|
||||
@@ -2725,7 +2725,7 @@ impl NodeNetworkInterface {
|
||||
|
||||
Some(WirePathUpdate {
|
||||
id: NodeId(u64::MAX),
|
||||
input_index: usize::MAX,
|
||||
input_index: u32::MAX as usize,
|
||||
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 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.);
|
||||
|
||||
DocumentNodeClickTargets {
|
||||
@@ -2871,7 +2871,7 @@ impl NodeNetworkInterface {
|
||||
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 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.);
|
||||
|
||||
DocumentNodeClickTargets {
|
||||
@@ -3048,7 +3048,7 @@ impl NodeNetworkInterface {
|
||||
|
||||
pub fn collect_frontend_click_targets(&mut self, network_path: &[NodeId]) -> FrontendClickTargets {
|
||||
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 Some(network_metadata) = self.network_metadata(network_path) else {
|
||||
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();
|
||||
|
||||
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));
|
||||
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() {
|
||||
let mut port_path = String::new();
|
||||
let _ = subpath.subpath_to_svg(&mut port_path, DAffine2::IDENTITY);
|
||||
port_click_targets.push(port_path);
|
||||
connector_click_targets.push(subpath.to_bezpath().to_svg());
|
||||
}
|
||||
}
|
||||
if let NodeTypeClickTargets::Layer(layer_metadata) = &node_click_targets.node_type_metadata {
|
||||
if let ClickTargetType::Subpath(subpath) = layer_metadata.visibility_click_target.target_type() {
|
||||
let mut port_path = String::new();
|
||||
let _ = subpath.subpath_to_svg(&mut port_path, DAffine2::IDENTITY);
|
||||
icon_click_targets.push(port_path);
|
||||
icon_click_targets.push(subpath.to_bezpath().to_svg());
|
||||
}
|
||||
|
||||
if let ClickTargetType::Subpath(subpath) = layer_metadata.grip_click_target.target_type() {
|
||||
let mut port_path = String::new();
|
||||
let _ = subpath.subpath_to_svg(&mut port_path, DAffine2::IDENTITY);
|
||||
icon_click_targets.push(port_path);
|
||||
icon_click_targets.push(subpath.to_bezpath().to_svg());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3095,9 +3089,8 @@ impl NodeNetworkInterface {
|
||||
});
|
||||
|
||||
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 mut all_nodes_bounding_box = String::new();
|
||||
let _ = rect.subpath_to_svg(&mut all_nodes_bounding_box, DAffine2::IDENTITY);
|
||||
let rect = Subpath::<PointId>::new_rect(bounds[0], bounds[1]);
|
||||
let all_nodes_bounding_box = rect.to_bezpath().to_svg();
|
||||
|
||||
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");
|
||||
@@ -3123,9 +3116,8 @@ impl NodeNetworkInterface {
|
||||
.inverse()
|
||||
.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 mut import_exports_bounding_box = String::new();
|
||||
let _ = import_exports_target.subpath_to_svg(&mut import_exports_bounding_box, DAffine2::IDENTITY);
|
||||
let import_exports_target = Subpath::<PointId>::new_rect(node_graph_top_left, node_graph_bottom_right);
|
||||
let import_exports_bounding_box = import_exports_target.to_bezpath().to_svg();
|
||||
|
||||
let mut modify_import_export = Vec::new();
|
||||
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())
|
||||
{
|
||||
if let ClickTargetType::Subpath(subpath) = click_target.target_type() {
|
||||
let mut remove_string = String::new();
|
||||
let _ = subpath.subpath_to_svg(&mut remove_string, DAffine2::IDENTITY);
|
||||
modify_import_export.push(remove_string);
|
||||
modify_import_export.push(subpath.to_bezpath().to_svg());
|
||||
}
|
||||
}
|
||||
}
|
||||
FrontendClickTargets {
|
||||
node_click_targets,
|
||||
layer_click_targets,
|
||||
port_click_targets,
|
||||
connector_click_targets,
|
||||
icon_click_targets,
|
||||
all_nodes_bounding_box,
|
||||
import_exports_bounding_box,
|
||||
@@ -3407,7 +3397,7 @@ impl NodeNetworkInterface {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -6047,7 +6037,7 @@ impl Iterator for FlowIter<'_> {
|
||||
} else {
|
||||
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 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::network_interface::{InputConnector, NodeTemplate, OutputConnector};
|
||||
use crate::messages::prelude::DocumentMessageHandler;
|
||||
use bezier_rs::Subpath;
|
||||
use glam::IVec2;
|
||||
use graph_craft::document::DocumentNode;
|
||||
use graph_craft::document::{DocumentNodeImplementation, NodeInput, value::TaggedValue};
|
||||
use graphene_std::ProtoNodeIdentifier;
|
||||
use graphene_std::subpath::Subpath;
|
||||
use graphene_std::table::Table;
|
||||
use graphene_std::text::{TextAlign, TypesettingConfig};
|
||||
use graphene_std::uuid::NodeId;
|
||||
@@ -300,10 +300,6 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
|
||||
"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 {
|
||||
node: graphene_std::raster_nodes::blending_nodes::color_overlay::IDENTIFIER,
|
||||
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));
|
||||
|
||||
// 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 {
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
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 {
|
||||
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_layers: bool,
|
||||
pub has_selection_history: (bool, bool),
|
||||
pub spreadsheet_view_open: bool,
|
||||
pub message_logging_verbosity: MessageLoggingVerbosity,
|
||||
pub reset_node_definitions_on_open: 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]
|
||||
@@ -585,18 +587,40 @@ impl LayoutHolder for MenuBarMessageHandler {
|
||||
disabled: no_active_document,
|
||||
..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 {
|
||||
label: "Window: Spreadsheet".into(),
|
||||
icon: Some(if self.spreadsheet_view_open { "CheckboxChecked" } else { "CheckboxUnchecked" }.into()),
|
||||
action: MenuBarEntry::create_action(|_| SpreadsheetMessage::ToggleOpen.into()),
|
||||
disabled: no_active_document,
|
||||
label: "Data".into(),
|
||||
icon: Some(if self.data_panel_open { "CheckboxChecked" } else { "CheckboxUnchecked" }.into()),
|
||||
shortcut: action_keys!(PortfolioMessageDiscriminant::ToggleDataPanelOpen),
|
||||
action: MenuBarEntry::create_action(|_| PortfolioMessage::ToggleDataPanelOpen.into()),
|
||||
..MenuBarEntry::default()
|
||||
}],
|
||||
]),
|
||||
),
|
||||
MenuBarEntry::new_root(
|
||||
"Help".into(),
|
||||
true,
|
||||
false,
|
||||
MenuBarEntryChildren(vec![
|
||||
vec![MenuBarEntry {
|
||||
label: "About Graphite…".into(),
|
||||
|
||||
@@ -4,7 +4,6 @@ mod portfolio_message_handler;
|
||||
pub mod document;
|
||||
pub mod document_migration;
|
||||
pub mod menu_bar;
|
||||
pub mod spreadsheet;
|
||||
pub mod utility_types;
|
||||
|
||||
#[doc(inline)]
|
||||
|
||||
@@ -15,8 +15,6 @@ pub enum PortfolioMessage {
|
||||
MenuBar(MenuBarMessage),
|
||||
#[child]
|
||||
Document(DocumentMessage),
|
||||
#[child]
|
||||
Spreadsheet(SpreadsheetMessage),
|
||||
|
||||
// Messages
|
||||
Init,
|
||||
@@ -128,6 +126,9 @@ pub enum PortfolioMessage {
|
||||
document_id: DocumentId,
|
||||
ignore_hash: bool,
|
||||
},
|
||||
ToggleDataPanelOpen,
|
||||
TogglePropertiesPanelOpen,
|
||||
ToggleLayersPanelOpen,
|
||||
ToggleRulers,
|
||||
UpdateDocumentWidgets,
|
||||
UpdateOpenDocumentsList,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use super::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use super::document::utility_types::network_interface;
|
||||
use super::spreadsheet::SpreadsheetMessageHandler;
|
||||
use super::utility_types::{PanelType, PersistentData};
|
||||
use crate::application::generate_uuid;
|
||||
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::utility_types::{HintData, HintGroup, ToolType};
|
||||
use crate::node_graph_executor::{ExportConfig, NodeGraphExecutor};
|
||||
use bezier_rs::BezierHandles;
|
||||
use derivative::*;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graph_craft::document::NodeId;
|
||||
use graphene_std::Color;
|
||||
use graphene_std::renderer::Quad;
|
||||
use graphene_std::subpath::BezierHandles;
|
||||
use graphene_std::text::Font;
|
||||
use graphene_std::vector::misc::HandleId;
|
||||
use graphene_std::vector::{PointId, SegmentId, Vector, VectorModificationType};
|
||||
@@ -37,14 +37,15 @@ use std::vec;
|
||||
pub struct PortfolioMessageContext<'a> {
|
||||
pub ipp: &'a InputPreprocessorMessageHandler,
|
||||
pub preferences: &'a PreferencesMessageHandler,
|
||||
pub animation: &'a AnimationMessageHandler,
|
||||
pub current_tool: &'a ToolType,
|
||||
pub message_logging_verbosity: MessageLoggingVerbosity,
|
||||
pub reset_node_definitions_on_open: bool,
|
||||
pub timing_information: TimingInformation,
|
||||
pub animation: &'a AnimationMessageHandler,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, ExtractField)]
|
||||
#[derive(Debug, Derivative, ExtractField)]
|
||||
#[derivative(Default)]
|
||||
pub struct PortfolioMessageHandler {
|
||||
menu_bar_message_handler: MenuBarMessageHandler,
|
||||
pub documents: HashMap<DocumentId, DocumentMessageHandler>,
|
||||
@@ -55,10 +56,13 @@ pub struct PortfolioMessageHandler {
|
||||
pub persistent_data: PersistentData,
|
||||
pub executor: NodeGraphExecutor,
|
||||
pub selection_mode: SelectionMode,
|
||||
/// The spreadsheet UI allows for graph data to be previewed.
|
||||
pub spreadsheet: SpreadsheetMessageHandler,
|
||||
device_pixel_ratio: Option<f64>,
|
||||
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]
|
||||
@@ -67,11 +71,11 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
let PortfolioMessageContext {
|
||||
ipp,
|
||||
preferences,
|
||||
animation,
|
||||
current_tool,
|
||||
message_logging_verbosity,
|
||||
reset_node_definitions_on_open,
|
||||
timing_information,
|
||||
animation,
|
||||
} = context;
|
||||
|
||||
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_selection_history = (false, 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.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, ());
|
||||
}
|
||||
PortfolioMessage::Spreadsheet(message) => {
|
||||
self.spreadsheet.process_message(message, responses, ());
|
||||
}
|
||||
PortfolioMessage::Document(message) => {
|
||||
if let Some(document_id) = self.active_document_id {
|
||||
if let Some(document) = self.documents.get_mut(&document_id) {
|
||||
@@ -122,6 +125,9 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
current_tool,
|
||||
preferences,
|
||||
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)
|
||||
}
|
||||
@@ -156,6 +162,9 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
current_tool,
|
||||
preferences,
|
||||
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)
|
||||
}
|
||||
@@ -201,6 +210,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
// Clear relevant UI layouts if there are no documents
|
||||
responses.add(PropertiesPanelMessage::Clear);
|
||||
responses.add(DocumentMessage::ClearLayersPanel);
|
||||
responses.add(DataPanelMessage::ClearLayout);
|
||||
let hint_data = HintData(vec![HintGroup(vec![])]);
|
||||
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
|
||||
responses.add(PropertiesPanelMessage::Clear);
|
||||
responses.add(DocumentMessage::ClearLayersPanel);
|
||||
responses.add(DataPanelMessage::ClearLayout);
|
||||
let hint_data = HintData(vec![HintGroup(vec![])]);
|
||||
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.executor.update_font_cache(self.persistent_data.font_cache.clone());
|
||||
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(
|
||||
self.documents.get_mut(document_id).expect("Tried to render non-existent document"),
|
||||
*document_id,
|
||||
ipp.viewport_bounds.size().as_uvec2(),
|
||||
timing_information,
|
||||
inspect_node,
|
||||
node_to_inspect,
|
||||
true,
|
||||
) {
|
||||
responses.add_front(message);
|
||||
@@ -388,7 +399,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
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.extend(responses.drain(..));
|
||||
*responses = new_responses;
|
||||
@@ -504,7 +515,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
document.set_save_state(document_is_saved);
|
||||
|
||||
// Load the document into the portfolio so it opens in the editor
|
||||
self.load_document(document, document_id, responses, to_front);
|
||||
self.load_document(document, document_id, self.layers_panel_open, responses, to_front);
|
||||
}
|
||||
PortfolioMessage::PasteIntoFolder { clipboard, parent, insert_index } => {
|
||||
let mut all_new_ids = Vec::new();
|
||||
@@ -770,7 +781,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
|
||||
if create_document {
|
||||
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 {
|
||||
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 } => {
|
||||
let inspect_node = self.inspect_node_id();
|
||||
let node_to_inspect = self.node_to_inspect();
|
||||
let result = self.executor.submit_node_graph_evaluation(
|
||||
self.documents.get_mut(&document_id).expect("Tried to render non-existent document"),
|
||||
document_id,
|
||||
ipp.viewport_bounds.size().as_uvec2(),
|
||||
timing_information,
|
||||
inspect_node,
|
||||
node_to_inspect,
|
||||
ignore_hash,
|
||||
);
|
||||
|
||||
@@ -938,6 +949,58 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
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 => {
|
||||
if let Some(document) = self.active_document_mut() {
|
||||
document.rulers_visible = !document.rulers_visible;
|
||||
@@ -987,6 +1050,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
PasteIntoFolder,
|
||||
PrevDocument,
|
||||
ToggleRulers,
|
||||
ToggleDataPanelOpen,
|
||||
);
|
||||
|
||||
// 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 {
|
||||
self.document_ids.push_front(document_id);
|
||||
} else {
|
||||
self.document_ids.push_back(document_id);
|
||||
}
|
||||
new_document.update_layers_panel_control_bar_widgets(responses);
|
||||
new_document.update_layers_panel_bottom_bar_widgets(responses);
|
||||
new_document.update_layers_panel_control_bar_widgets(layers_panel_open, responses);
|
||||
new_document.update_layers_panel_bottom_bar_widgets(layers_panel_open, responses);
|
||||
|
||||
self.documents.insert(document_id, new_document);
|
||||
|
||||
@@ -1111,17 +1175,17 @@ impl PortfolioMessageHandler {
|
||||
result
|
||||
}
|
||||
|
||||
/// Get the id of the node that should be used as the target for the spreadsheet
|
||||
pub fn inspect_node_id(&self) -> Option<NodeId> {
|
||||
// Spreadsheet not open, skipping
|
||||
if !self.spreadsheet.spreadsheet_view_open {
|
||||
/// Get the ID of the selected node that should be used as the current source for the Data panel.
|
||||
pub fn node_to_inspect(&self) -> Option<NodeId> {
|
||||
// Skip if the Data panel is not open
|
||||
if !self.data_panel_open {
|
||||
return None;
|
||||
}
|
||||
|
||||
let document = self.documents.get(&self.active_document_id?)?;
|
||||
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 {
|
||||
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,
|
||||
Layers,
|
||||
Properties,
|
||||
Spreadsheet,
|
||||
DataPanel,
|
||||
}
|
||||
|
||||
impl From<String> for PanelType {
|
||||
@@ -52,7 +52,7 @@ impl From<String> for PanelType {
|
||||
"Document" => PanelType::Document,
|
||||
"Layers" => PanelType::Layers,
|
||||
"Properties" => PanelType::Properties,
|
||||
"Spreadsheet" => PanelType::Spreadsheet,
|
||||
"Data" => PanelType::DataPanel,
|
||||
_ => panic!("Unknown panel type: {value}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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_preprocessor::{InputPreprocessorMessage, InputPreprocessorMessageContext, InputPreprocessorMessageDiscriminant, InputPreprocessorMessageHandler};
|
||||
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::navigation::{NavigationMessage, NavigationMessageContext, NavigationMessageDiscriminant, NavigationMessageHandler};
|
||||
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::{DocumentMessage, DocumentMessageContext, DocumentMessageDiscriminant, DocumentMessageHandler};
|
||||
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::preferences::{PreferencesMessage, PreferencesMessageDiscriminant, PreferencesMessageHandler};
|
||||
pub use crate::messages::tool::transform_layer::{TransformLayerMessage, TransformLayerMessageDiscriminant, TransformLayerMessageHandler};
|
||||
pub use crate::messages::tool::{ToolMessage, ToolMessageContext, ToolMessageDiscriminant, ToolMessageHandler};
|
||||
pub use crate::messages::workspace::{WorkspaceMessage, WorkspaceMessageDiscriminant, WorkspaceMessageHandler};
|
||||
|
||||
// Message, MessageDiscriminant
|
||||
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::network_interface::{FlowType, InputConnector, NodeNetworkInterface, NodeTemplate};
|
||||
use crate::messages::prelude::*;
|
||||
use bezier_rs::Subpath;
|
||||
use glam::DVec2;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{NodeId, NodeInput};
|
||||
@@ -12,10 +11,11 @@ use graphene_std::Color;
|
||||
use graphene_std::NodeInputDecleration;
|
||||
use graphene_std::raster::BlendMode;
|
||||
use graphene_std::raster_types::{CPU, GPU, Raster};
|
||||
use graphene_std::subpath::Subpath;
|
||||
use graphene_std::table::Table;
|
||||
use graphene_std::text::{Font, TypesettingConfig};
|
||||
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 std::collections::VecDeque;
|
||||
|
||||
@@ -273,7 +273,7 @@ pub fn get_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkI
|
||||
let fill_index = 1;
|
||||
|
||||
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;
|
||||
};
|
||||
Some(gradient.clone())
|
||||
@@ -284,7 +284,7 @@ pub fn get_fill_color(layer: LayerNodeIdentifier, network_interface: &NodeNetwor
|
||||
let fill_index = 1;
|
||||
|
||||
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;
|
||||
};
|
||||
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) {
|
||||
let min = rect.min();
|
||||
let max = rect.max();
|
||||
|
||||
// Create the four corners of the rectangle
|
||||
let top_left = transform.transform_point2(DVec2::new(min.x, min.y));
|
||||
let top_right = transform.transform_point2(DVec2::new(max.x, min.y));
|
||||
let bottom_right = transform.transform_point2(DVec2::new(max.x, max.y));
|
||||
let bottom_left = transform.transform_point2(DVec2::new(min.x, max.y));
|
||||
// Define corners in document space
|
||||
let top_left = DVec2::new(min.x, min.y);
|
||||
let top_right = DVec2::new(max.x, min.y);
|
||||
let bottom_right = DVec2::new(max.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_right, bottom_right, 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::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::messages::portfolio::document::overlays::utility_functions::selected_segments;
|
||||
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::prelude::*;
|
||||
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 bezier_rs::{Bezier, BezierHandles, Subpath, TValue};
|
||||
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 kurbo::{Affine, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveNearest, PathSeg, Rect, Shape};
|
||||
use std::f64::consts::TAU;
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
@@ -26,7 +29,7 @@ pub enum SelectionChange {
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum SelectionShape<'a> {
|
||||
Box([DVec2; 2]),
|
||||
Box(Rect),
|
||||
Lasso(&'a Vec<DVec2>),
|
||||
}
|
||||
|
||||
@@ -185,7 +188,7 @@ pub type OpposingHandleLengths = HashMap<LayerNodeIdentifier, HashMap<HandleId,
|
||||
pub struct ClosestSegment {
|
||||
layer: LayerNodeIdentifier,
|
||||
segment: SegmentId,
|
||||
bezier: Bezier,
|
||||
bezier: PathSeg,
|
||||
points: [PointId; 2],
|
||||
colinear: [Option<HandleId>; 2],
|
||||
t: f64,
|
||||
@@ -205,12 +208,12 @@ impl ClosestSegment {
|
||||
self.points
|
||||
}
|
||||
|
||||
pub fn bezier(&self) -> Bezier {
|
||||
pub fn pathseg(&self) -> PathSeg {
|
||||
self.bezier
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -219,7 +222,7 @@ impl ClosestSegment {
|
||||
|
||||
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 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)
|
||||
}
|
||||
|
||||
@@ -228,10 +231,10 @@ impl ClosestSegment {
|
||||
let transform = document_metadata.transform_to_viewport_if_feeds(self.layer, network_interface);
|
||||
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;
|
||||
|
||||
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);
|
||||
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);
|
||||
|
||||
// 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
|
||||
let first_handle = first.handle_end().map(|handle| transform.transform_point2(handle));
|
||||
let second_handle = second.handle_start().map(|handle| transform.transform_point2(handle));
|
||||
let first_handle = pathseg_points(first).p2.map(|handle| transform.transform_point2(handle));
|
||||
let second_handle = pathseg_points(second).p1.map(|handle| transform.transform_point2(handle));
|
||||
|
||||
(first_handle, second_handle)
|
||||
}
|
||||
|
||||
pub fn adjusted_insert(&self, responses: &mut VecDeque<Message>) -> (PointId, [SegmentId; 2]) {
|
||||
let layer = self.layer;
|
||||
let [first, second] = self.bezier.split(TValue::Parametric(self.t));
|
||||
let first = pathseg_points(self.bezier.subsegment(0_f64..self.t));
|
||||
let second = pathseg_points(self.bezier.subsegment(self.t..1.));
|
||||
|
||||
// Point
|
||||
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 });
|
||||
|
||||
// First segment
|
||||
@@ -272,7 +277,7 @@ impl ClosestSegment {
|
||||
let modification_type = VectorModificationType::InsertSegment {
|
||||
id: segment_ids[0],
|
||||
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 });
|
||||
|
||||
@@ -280,12 +285,12 @@ impl ClosestSegment {
|
||||
let modification_type = VectorModificationType::InsertSegment {
|
||||
id: segment_ids[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 });
|
||||
|
||||
// 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 modification_type = VectorModificationType::SetG1Continuous { handles, enabled: true };
|
||||
responses.add(GraphOperationMessage::Vector { layer, modification_type });
|
||||
@@ -353,8 +358,8 @@ impl ClosestSegment {
|
||||
) -> Option<[Option<HandleId>; 2]> {
|
||||
let transform = document.metadata().transform_to_viewport_if_feeds(self.layer, &document.network_interface);
|
||||
|
||||
let start = self.bezier.start;
|
||||
let end = self.bezier.end;
|
||||
let start = point_to_dvec2(self.bezier.start());
|
||||
let end = point_to_dvec2(self.bezier.end());
|
||||
|
||||
// Apply the drag delta to the segment's handles
|
||||
let b = self.bezier_point_to_viewport;
|
||||
@@ -1686,9 +1691,9 @@ impl ShapeState {
|
||||
|
||||
let vector = network_interface.compute_modified_vector(layer)?;
|
||||
|
||||
for (segment, mut bezier, start, end) in vector.segment_bezier_iter() {
|
||||
let t = bezier.project(layer_pos);
|
||||
let layerspace = bezier.evaluate(TValue::Parametric(t));
|
||||
for (segment_id, mut segment, start, end) in vector.segment_iter() {
|
||||
let t = segment.nearest(dvec2_to_point(layer_pos), DEFAULT_ACCURACY).t;
|
||||
let layerspace = point_to_dvec2(segment.eval(t));
|
||||
|
||||
let screenspace = transform.transform_point2(layerspace);
|
||||
let distance_squared = screenspace.distance_squared(position);
|
||||
@@ -1697,20 +1702,22 @@ impl ShapeState {
|
||||
closest_distance_squared = distance_squared;
|
||||
|
||||
// Convert to linear if handes are on top of control points
|
||||
if let bezier_rs::BezierHandles::Cubic { handle_start, handle_end } = bezier.handles {
|
||||
if handle_start.abs_diff_eq(bezier.start(), f64::EPSILON * 100.) && handle_end.abs_diff_eq(bezier.end(), f64::EPSILON * 100.) {
|
||||
bezier = Bezier::from_linear_dvec2(bezier.start, bezier.end);
|
||||
let PathSegPoints { p0: _, p1, p2, p3: _ } = pathseg_points(segment);
|
||||
if let (Some(p1), Some(p2)) = (p1, p2) {
|
||||
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 end_handle = vector.colinear_manipulators.iter().find(|handles| handles.contains(&HandleId::end(segment)));
|
||||
let primary_handle = primary_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));
|
||||
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_id)));
|
||||
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_id));
|
||||
|
||||
closest = Some(ClosestSegment {
|
||||
segment,
|
||||
bezier,
|
||||
segment: segment_id,
|
||||
bezier: segment,
|
||||
points: [start, end],
|
||||
colinear: [primary_handle, end_handle],
|
||||
t,
|
||||
@@ -2076,21 +2083,24 @@ impl ShapeState {
|
||||
};
|
||||
|
||||
// Selection segments
|
||||
for (id, bezier, _, _) in vector.segment_bezier_iter() {
|
||||
for (id, segment, _, _) in vector.segment_iter() {
|
||||
if select_segments {
|
||||
// Select segments if they lie inside the bounding box or lasso polygon
|
||||
let segment_bbox = calculate_bezier_bbox(bezier);
|
||||
let bottom_left = transform.transform_point2(segment_bbox[0]);
|
||||
let top_right = transform.transform_point2(segment_bbox[1]);
|
||||
let transformed_segment = Affine::new(transform.to_cols_array()) * segment;
|
||||
let segment_bbox = transformed_segment.bounding_box();
|
||||
|
||||
let select = match selection_shape {
|
||||
SelectionShape::Box(quad) => {
|
||||
let enclosed = quad[0].min(quad[1]).cmple(bottom_left).all() && quad[0].max(quad[1]).cmpge(top_right).all();
|
||||
SelectionShape::Box(rect) => {
|
||||
let enclosed = segment_bbox.contains_rect(rect);
|
||||
match selection_mode {
|
||||
SelectionMode::Enclosed => enclosed,
|
||||
_ => {
|
||||
// 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.");
|
||||
|
||||
// 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 {
|
||||
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)),
|
||||
@@ -2111,13 +2121,15 @@ impl ShapeState {
|
||||
}
|
||||
}
|
||||
|
||||
let segment_points = pathseg_points(segment);
|
||||
|
||||
// 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 transformed_position = transform.transform_point2(position);
|
||||
|
||||
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
|
||||
.as_ref()
|
||||
.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 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
|
||||
.as_ref()
|
||||
.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::tool_messages::tool_prelude::Key;
|
||||
use crate::messages::tool::utility_types::*;
|
||||
use bezier_rs::Subpath;
|
||||
use glam::{DAffine2, DMat2, DVec2};
|
||||
use graph_craft::document::NodeInput;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graphene_std::subpath::{self, Subpath};
|
||||
use graphene_std::vector::click_target::ClickTargetType;
|
||||
use graphene_std::vector::misc::{ArcType, dvec2_to_point};
|
||||
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,
|
||||
sweep_angle / 360. * std::f64::consts::TAU,
|
||||
match arc_type {
|
||||
ArcType::Open => bezier_rs::ArcType::Open,
|
||||
ArcType::Closed => bezier_rs::ArcType::Closed,
|
||||
ArcType::PieSlice => bezier_rs::ArcType::PieSlice,
|
||||
ArcType::Open => subpath::ArcType::Open,
|
||||
ArcType::Closed => subpath::ArcType::Closed,
|
||||
ArcType::PieSlice => subpath::ArcType::PieSlice,
|
||||
},
|
||||
))];
|
||||
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::prelude::*;
|
||||
pub use alignment_snapper::*;
|
||||
use bezier_rs::TValue;
|
||||
pub use distribution_snapper::*;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graphene_std::renderer::Quad;
|
||||
use graphene_std::renderer::Rect;
|
||||
use graphene_std::vector::NoHashBuilder;
|
||||
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::*;
|
||||
use kurbo::ParamCurve;
|
||||
pub use layer_snapper::*;
|
||||
pub use snap_results::*;
|
||||
use std::cmp::Ordering;
|
||||
@@ -81,6 +83,7 @@ impl SnapConstraint {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn snap_tolerance(document: &DocumentMessageHandler) -> f64 {
|
||||
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> {
|
||||
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)
|
||||
}
|
||||
|
||||
fn get_closest_line(lines: &[SnappedLine]) -> Option<&SnappedPoint> {
|
||||
lines.iter().map(|curve| &curve.point).min_by(compare_points)
|
||||
}
|
||||
|
||||
fn get_closest_intersection(snap_to: DVec2, curves: &[SnappedCurve]) -> Option<SnappedPoint> {
|
||||
let mut best = None;
|
||||
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 {
|
||||
continue;
|
||||
}
|
||||
for curve_i_t in curve_i.document_curve.intersections(&curve_j.document_curve, None, None) {
|
||||
let snapped_point_document = curve_i.document_curve.evaluate(TValue::Parametric(curve_i_t));
|
||||
for curve_i_t in filtered_segment_intersections(curve_i.document_curve, curve_j.document_curve, None, None) {
|
||||
let snapped_point_document = point_to_dvec2(curve_i.document_curve.eval(curve_i_t));
|
||||
let distance = snap_to.distance(snapped_point_document);
|
||||
let i_closer = curve_i.point.distance < curve_j.point.distance;
|
||||
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
|
||||
}
|
||||
|
||||
fn get_grid_intersection(snap_to: DVec2, lines: &[SnappedLine]) -> Option<SnappedPoint> {
|
||||
let mut best = None;
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
impl SnapManager {
|
||||
pub fn update_indicator(&mut self, snapped_point: SnappedPoint) {
|
||||
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::misc::*;
|
||||
use crate::messages::prelude::*;
|
||||
use bezier_rs::{Bezier, Identifier, Subpath, TValue};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graphene_std::math::math_ext::QuadExt;
|
||||
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::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)]
|
||||
pub struct LayerSnapper {
|
||||
@@ -37,7 +43,7 @@ impl LayerSnapper {
|
||||
return;
|
||||
}
|
||||
|
||||
for document_curve in bounds.bezier_lines() {
|
||||
for document_curve in bounds.to_lines() {
|
||||
self.paths_to_snap.push(SnapCandidatePath {
|
||||
document_curve,
|
||||
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)) {
|
||||
for subpath in document.metadata().layer_outline(layer) {
|
||||
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;
|
||||
if snap_data.ignore_manipulator(layer, start) || snap_data.ignore_manipulator(layer, subpath.manipulator_groups()[(start_index + 1) % subpath.len()].id) {
|
||||
continue;
|
||||
@@ -98,13 +104,12 @@ impl LayerSnapper {
|
||||
|
||||
for path in &self.paths_to_snap {
|
||||
// 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;
|
||||
}
|
||||
let time = path.document_curve.project(point.document_point);
|
||||
let snapped_point_document = path.document_curve.evaluate(bezier_rs::TValue::Parametric(time));
|
||||
|
||||
let distance = snapped_point_document.distance(point.document_point);
|
||||
let Nearest { distance_sq, t } = path.document_curve.nearest(dvec2_to_point(point.document_point), DEFAULT_ACCURACY);
|
||||
let snapped_point_document = point_to_dvec2(path.document_curve.eval(t));
|
||||
let distance = distance_sq.sqrt();
|
||||
|
||||
if distance < tolerance {
|
||||
snap_results.curves.push(SnappedCurve {
|
||||
@@ -144,8 +149,8 @@ impl LayerSnapper {
|
||||
|
||||
for path in &self.paths_to_snap {
|
||||
for constraint_path in constraint_path.iter() {
|
||||
for time in path.document_curve.intersections(&constraint_path, None, None) {
|
||||
let snapped_point_document = path.document_curve.evaluate(bezier_rs::TValue::Parametric(time));
|
||||
for time in filtered_segment_intersections(path.document_curve, constraint_path, None, None) {
|
||||
let snapped_point_document = point_to_dvec2(path.document_curve.eval(time));
|
||||
|
||||
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) {
|
||||
if normals && path.bounds.is_none() {
|
||||
for &neighbor in &point.neighbors {
|
||||
for t in path.document_curve.normals_to_point(neighbor) {
|
||||
let normal_point = path.document_curve.evaluate(TValue::Parametric(t));
|
||||
for t in pathseg_normals_to_point(path.document_curve, dvec2_to_point(neighbor)) {
|
||||
let normal_point = point_to_dvec2(path.document_curve.eval(t));
|
||||
let distance = normal_point.distance(point.document_point);
|
||||
if distance > tolerance {
|
||||
continue;
|
||||
@@ -287,8 +292,8 @@ fn normals_and_tangents(path: &SnapCandidatePath, normals: bool, tangents: bool,
|
||||
}
|
||||
if tangents && path.bounds.is_none() {
|
||||
for &neighbor in &point.neighbors {
|
||||
for t in path.document_curve.tangents_to_point(neighbor) {
|
||||
let tangent_point = path.document_curve.evaluate(TValue::Parametric(t));
|
||||
for t in pathseg_tangents_to_point(path.document_curve, dvec2_to_point(neighbor)) {
|
||||
let tangent_point = point_to_dvec2(path.document_curve.eval(t));
|
||||
let distance = tangent_point.distance(point.document_point);
|
||||
if distance > tolerance {
|
||||
continue;
|
||||
@@ -310,7 +315,7 @@ fn normals_and_tangents(path: &SnapCandidatePath, normals: bool, tangents: bool,
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct SnapCandidatePath {
|
||||
document_curve: Bezier,
|
||||
document_curve: PathSeg,
|
||||
layer: LayerNodeIdentifier,
|
||||
start: PointId,
|
||||
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 {
|
||||
return;
|
||||
}
|
||||
let curve = pathseg_points(curve);
|
||||
|
||||
let in_handle = curve.handle_start().map(|handle| handle - curve.start).filter(handle_not_under(to_document));
|
||||
let out_handle = curve.handle_end().map(|handle| handle - curve.end).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.p2.map(|handle| handle - curve.p3).filter(handle_not_under(to_document));
|
||||
if in_handle.is_none() && out_handle.is_none() {
|
||||
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),
|
||||
SnapTarget::Path(PathSnapTarget::LineMidpoint),
|
||||
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 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));
|
||||
|
||||
@@ -2,11 +2,11 @@ use super::DistributionMatch;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::misc::{DistributionSnapTarget, SnapSource, SnapTarget};
|
||||
use crate::messages::tool::common_functionality::snapping::SnapCandidatePoint;
|
||||
use bezier_rs::Bezier;
|
||||
use glam::DVec2;
|
||||
use graphene_std::renderer::Quad;
|
||||
use graphene_std::renderer::Rect;
|
||||
use graphene_std::vector::PointId;
|
||||
use kurbo::PathSeg;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
@@ -120,5 +120,5 @@ pub struct SnappedCurve {
|
||||
pub layer: LayerNodeIdentifier,
|
||||
pub start: PointId,
|
||||
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::tool_messages::path_tool::PathOverlayMode;
|
||||
use crate::messages::tool::utility_types::ToolType;
|
||||
use bezier_rs::{Bezier, BezierHandles};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graph_craft::concrete;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graphene_std::renderer::Quad;
|
||||
use graphene_std::subpath::{Bezier, BezierHandles};
|
||||
use graphene_std::table::Table;
|
||||
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 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.
|
||||
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 {
|
||||
let to_layerspace = transform.inverse();
|
||||
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 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
|
||||
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
|
||||
}
|
||||
|
||||
/// Calculates optimal handle lengths with adam optimization.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn find_two_param_best_approximate(p1: DVec2, p3: DVec2, d1: DVec2, d2: DVec2, min_len1: f64, min_len2: f64, farther_segment: Bezier, other_segment: Bezier) -> (DVec2, DVec2) {
|
||||
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 tol = 1e-6;
|
||||
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 farther_segment = if farther_segment.start.distance(p1) >= f64::EPSILON {
|
||||
farther_segment.reverse()
|
||||
let further_segment = if further_segment.start().distance(dvec2_to_point(p1)) >= f64::EPSILON {
|
||||
further_segment.reverse()
|
||||
} 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
|
||||
let l1 = farther_segment.length(None);
|
||||
let l2 = other_segment.length(None);
|
||||
let l1 = further_segment.perimeter(DEFAULT_ACCURACY);
|
||||
let l2 = other_segment.perimeter(DEFAULT_ACCURACY);
|
||||
let ratio = l1 / (l1 + l2);
|
||||
let n_points1 = ((2 * n) as f64 * ratio).floor() as usize;
|
||||
let mut points1 = farther_segment.compute_lookup_table(Some(n_points1), None).collect::<Vec<_>>();
|
||||
let mut points2 = other_segment.compute_lookup_table(Some(n), None).collect::<Vec<_>>();
|
||||
let mut points1 = pathseg_compute_lookup_table(further_segment, Some(n_points1), false).collect::<Vec<_>>();
|
||||
let mut points2 = pathseg_compute_lookup_table(other_segment, Some(n), false).collect::<Vec<_>>();
|
||||
points1.append(&mut points2);
|
||||
|
||||
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
|
||||
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
|
||||
responses.add(BroadcastEvent::SelectionChanged);
|
||||
|
||||
|
||||
@@ -204,7 +204,6 @@ impl SelectedGradient {
|
||||
|
||||
/// Update the layer fill to the current gradient
|
||||
pub fn render_gradient(&mut self, responses: &mut VecDeque<Message>) {
|
||||
self.gradient.transform = self.transform;
|
||||
if let Some(layer) = self.layer {
|
||||
responses.add(GraphOperationMessage::FillSet {
|
||||
layer,
|
||||
@@ -436,14 +435,7 @@ impl Fsm for GradientToolFsmState {
|
||||
gradient.clone()
|
||||
} else {
|
||||
// Generate a new gradient
|
||||
Gradient::new(
|
||||
DVec2::ZERO,
|
||||
global_tool_data.secondary_color,
|
||||
DVec2::ONE,
|
||||
global_tool_data.primary_color,
|
||||
DAffine2::IDENTITY,
|
||||
tool_options.gradient_type,
|
||||
)
|
||||
Gradient::new(DVec2::ZERO, global_tool_data.secondary_color, DVec2::ONE, global_tool_data.primary_color, tool_options.gradient_type)
|
||||
};
|
||||
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::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::renderer::Quad;
|
||||
use graphene_std::subpath::pathseg_points;
|
||||
use graphene_std::transform::ReferencePoint;
|
||||
use graphene_std::uuid::NodeId;
|
||||
use graphene_std::vector::algorithms::util::pathseg_tangent;
|
||||
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 kurbo::{DEFAULT_ACCURACY, ParamCurve, ParamCurveNearest, PathSeg, Rect};
|
||||
use std::vec;
|
||||
|
||||
#[derive(Default, ExtractField)]
|
||||
@@ -126,6 +128,7 @@ pub enum PathToolMessage {
|
||||
overlay_context: OverlayContext,
|
||||
},
|
||||
MergeSelectedPoints,
|
||||
StartSlidingPoint,
|
||||
Copy {
|
||||
clipboard: Clipboard,
|
||||
},
|
||||
@@ -429,6 +432,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Path
|
||||
ClosePath,
|
||||
PointerMove,
|
||||
MergeSelectedPoints,
|
||||
StartSlidingPoint,
|
||||
Copy,
|
||||
Cut,
|
||||
DeleteSelected,
|
||||
@@ -447,6 +451,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Path
|
||||
BreakPath,
|
||||
DeleteAndBreakPath,
|
||||
SwapSelectedHandles,
|
||||
StartSlidingPoint,
|
||||
Copy,
|
||||
Cut,
|
||||
DeleteSelected,
|
||||
@@ -465,6 +470,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Path
|
||||
DeleteAndBreakPath,
|
||||
Escape,
|
||||
RightClick,
|
||||
StartSlidingPoint,
|
||||
TogglePointEditing,
|
||||
ToggleSegmentEditing
|
||||
),
|
||||
@@ -505,7 +511,7 @@ pub enum PointSelectState {
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct SlidingSegmentData {
|
||||
segment_id: SegmentId,
|
||||
bezier: Bezier,
|
||||
bezier: PathSeg,
|
||||
start: PointId,
|
||||
}
|
||||
|
||||
@@ -910,13 +916,12 @@ impl PathToolData {
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
PathToolFsmState::Dragging(self.dragging_state)
|
||||
} else {
|
||||
let start_pos = segment.bezier().start;
|
||||
let end_pos = segment.bezier().end;
|
||||
let points = pathseg_points(segment.pathseg());
|
||||
|
||||
let [pos1, pos2] = match segment.bezier().handles {
|
||||
BezierHandles::Cubic { handle_start, handle_end } => [handle_start, handle_end],
|
||||
BezierHandles::Quadratic { handle } => [handle, end_pos],
|
||||
BezierHandles::Linear => [start_pos + (end_pos - start_pos) / 3., end_pos + (start_pos - end_pos) / 3.],
|
||||
let [pos1, pos2] = match (points.p1, points.p2) {
|
||||
(Some(p1), Some(p2)) => [p1, p2],
|
||||
(Some(p1), None) | (None, Some(p1)) => [p1, points.p3],
|
||||
(None, None) => [points.p0 + (points.p3 - points.p0) / 3., points.p3 + (points.p0 - points.p3) / 3.],
|
||||
};
|
||||
self.molding_info = Some((pos1, pos2));
|
||||
PathToolFsmState::Dragging(self.dragging_state)
|
||||
@@ -1277,15 +1282,10 @@ impl PathToolData {
|
||||
};
|
||||
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 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 {
|
||||
match (connected_segments[0], connected_segments[1]) {
|
||||
(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 t1 = segments[0].bezier.project(layer_pos);
|
||||
let position1 = segments[0].bezier.evaluate(TValue::Parametric(t1));
|
||||
let t1 = segments[0].bezier.nearest(dvec2_to_point(layer_pos), DEFAULT_ACCURACY).t;
|
||||
let position1 = point_to_dvec2(segments[0].bezier.eval(t1));
|
||||
|
||||
let t2 = segments[1].bezier.project(layer_pos);
|
||||
let position2 = segments[1].bezier.evaluate(TValue::Parametric(t2));
|
||||
let t2 = segments[1].bezier.nearest(dvec2_to_point(layer_pos), DEFAULT_ACCURACY).t;
|
||||
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)
|
||||
} else {
|
||||
(segments[0], segments[1], t1, position1)
|
||||
@@ -1346,50 +1346,59 @@ impl PathToolData {
|
||||
shape_editor.move_anchor(anchor, &vector, delta, layer, None, responses);
|
||||
|
||||
// Make a split at the t_value
|
||||
let [first, second] = closer_segment.bezier.split(TValue::Parametric(t_value));
|
||||
let closer_segment_other_point = if anchor == closer_segment.start { closer_segment.bezier.end } else { closer_segment.bezier.start };
|
||||
let first = closer_segment.bezier.subsegment(0_f64..t_value);
|
||||
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
|
||||
let closer_primary_handle = HandleId::primary(closer_segment.segment_id);
|
||||
let Some(handle_position) = split_segment.handle_start() else { return };
|
||||
let relative_position1 = handle_position - split_segment.start;
|
||||
let Some(handle_position) = split_segment_points.p1 else { return };
|
||||
let relative_position1 = handle_position - split_segment_points.p0;
|
||||
let modification_type = closer_primary_handle.set_relative_position(relative_position1);
|
||||
responses.add(GraphOperationMessage::Vector { layer, modification_type });
|
||||
|
||||
let closer_secondary_handle = HandleId::end(closer_segment.segment_id);
|
||||
let Some(handle_position) = split_segment.handle_end() else { return };
|
||||
let relative_position2 = handle_position - split_segment.end;
|
||||
let Some(handle_position) = split_segment_points.p2 else { return };
|
||||
let relative_position2 = handle_position - split_segment_points.p3;
|
||||
let modification_type = closer_secondary_handle.set_relative_position(relative_position2);
|
||||
responses.add(GraphOperationMessage::Vector { layer, modification_type });
|
||||
|
||||
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,
|
||||
HandleId::end(farther_segment.segment_id),
|
||||
HandleId::primary(farther_segment.segment_id),
|
||||
farther_segment.bezier.handle_end(),
|
||||
further_segment_points.p3,
|
||||
HandleId::end(further_segment.segment_id),
|
||||
HandleId::primary(further_segment.segment_id),
|
||||
further_segment_points.p2,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
farther_segment.bezier.start,
|
||||
HandleId::primary(farther_segment.segment_id),
|
||||
HandleId::end(farther_segment.segment_id),
|
||||
farther_segment.bezier.handle_start(),
|
||||
further_segment_points.p0,
|
||||
HandleId::primary(further_segment.segment_id),
|
||||
HandleId::end(further_segment.segment_id),
|
||||
further_segment_points.p1,
|
||||
)
|
||||
};
|
||||
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
|
||||
let d1 = start_handle_direction.try_normalize().unwrap_or({
|
||||
if anchor == farther_segment.start {
|
||||
-farther_segment.bezier.tangent(TValue::Parametric(0.99))
|
||||
if anchor == further_segment.start {
|
||||
-pathseg_tangent(further_segment.bezier, 1.)
|
||||
} 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_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
|
||||
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 {
|
||||
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
|
||||
let display_anchors = overlay_context.visibility_settings.anchors();
|
||||
if display_anchors {
|
||||
let start_pos = transform.transform_point2(closest_segment.bezier().start);
|
||||
let end_pos = transform.transform_point2(closest_segment.bezier().end);
|
||||
let start_pos = transform.transform_point2(point_to_dvec2(closest_segment.pathseg().start()));
|
||||
let end_pos = transform.transform_point2(point_to_dvec2(closest_segment.pathseg().end()));
|
||||
let start_id = closest_segment.points()[0];
|
||||
let end_id = closest_segment.points()[1];
|
||||
if let Some(shape_state) = shape_editor.selected_shape_state.get_mut(&closest_segment.layer()) {
|
||||
@@ -1890,7 +1899,7 @@ impl Fsm for PathToolFsmState {
|
||||
let (points_inside, segments_inside) = match selection_shape {
|
||||
SelectionShapeType::Box => {
|
||||
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(
|
||||
&document.network_interface,
|
||||
SelectionShape::Box(bbox),
|
||||
@@ -1935,7 +1944,7 @@ impl Fsm for PathToolFsmState {
|
||||
|
||||
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) {
|
||||
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 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(
|
||||
equidistant_state,
|
||||
lock_angle_state,
|
||||
@@ -2311,7 +2316,8 @@ impl Fsm for PathToolFsmState {
|
||||
|
||||
match selection_shape {
|
||||
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(
|
||||
&document.network_interface,
|
||||
SelectionShape::Box(bbox),
|
||||
@@ -2407,7 +2413,8 @@ impl Fsm for PathToolFsmState {
|
||||
} else {
|
||||
match selection_shape {
|
||||
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(
|
||||
&document.network_interface,
|
||||
SelectionShape::Box(bbox),
|
||||
@@ -2440,6 +2447,8 @@ impl Fsm for PathToolFsmState {
|
||||
tool_data.ghost_outline.clear();
|
||||
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 mut segment_dissolved = false;
|
||||
let mut point_inserted = false;
|
||||
|
||||
let nearest_point = shape_editor.find_nearest_visible_point_indices(
|
||||
&document.network_interface,
|
||||
@@ -2460,6 +2469,7 @@ impl Fsm for PathToolFsmState {
|
||||
if tool_data.delete_segment_pressed {
|
||||
if let Some(vector) = document.network_interface.compute_modified_vector(segment.layer()) {
|
||||
shape_editor.dissolve_segment(responses, segment.layer(), &vector, segment.segment(), segment.points());
|
||||
segment_dissolved = true;
|
||||
}
|
||||
} else {
|
||||
let is_segment_selected = shape_editor
|
||||
@@ -2468,11 +2478,7 @@ impl Fsm for PathToolFsmState {
|
||||
.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);
|
||||
tool_data.segment = None;
|
||||
tool_data.molding_info = None;
|
||||
tool_data.molding_segment = false;
|
||||
tool_data.temporary_adjacent_handles_while_molding = None;
|
||||
return PathToolFsmState::Ready;
|
||||
point_inserted = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2480,6 +2486,11 @@ impl Fsm for PathToolFsmState {
|
||||
tool_data.molding_info = None;
|
||||
tool_data.molding_segment = false;
|
||||
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;
|
||||
@@ -2636,6 +2647,14 @@ impl Fsm for PathToolFsmState {
|
||||
shape_editor.delete_point_and_break_path(document, responses);
|
||||
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 }) => {
|
||||
// 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
|
||||
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();
|
||||
|
||||
segments_map.insert(segment_id, new_segment_id);
|
||||
|
||||
let handles = match bezier.handles {
|
||||
BezierHandles::Linear => [None, None],
|
||||
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 = pathseg_points(bezier);
|
||||
let handles = [points.p1, points.p2];
|
||||
|
||||
let points = [points_map[&start], points_map[&end]];
|
||||
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();
|
||||
|
||||
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 segment_selected = layer_selection_state.is_segment_selected(segment_id);
|
||||
@@ -2881,11 +2897,8 @@ impl Fsm for PathToolFsmState {
|
||||
let new_id = SegmentId::generate();
|
||||
segments_map.insert(segment_id, new_id);
|
||||
|
||||
let handles = match bezier.handles {
|
||||
BezierHandles::Linear => [None, None],
|
||||
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 = pathseg_points(bezier);
|
||||
let handles = [points.p1, points.p2];
|
||||
|
||||
let points = [points_map[&start], points_map[&end]];
|
||||
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")];
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
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 point_edit = tool_options.path_editing_mode.point_editing_mode;
|
||||
|
||||
@@ -3525,9 +3534,15 @@ fn update_dynamic_hints(
|
||||
let mut groups = vec![
|
||||
HintGroup(drag_selected_hints),
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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::snapping::{SnapCache, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration};
|
||||
use crate::messages::tool::common_functionality::utility_functions::{calculate_segment_angle, closest_point, should_extend};
|
||||
use bezier_rs::{Bezier, BezierHandles};
|
||||
use graph_craft::document::NodeId;
|
||||
use graphene_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 kurbo::{CubicBez, PathSeg};
|
||||
|
||||
#[derive(Default, ExtractField)]
|
||||
pub struct PenTool {
|
||||
@@ -1257,7 +1258,7 @@ impl PenToolData {
|
||||
let vector = document.network_interface.compute_modified_vector(layer);
|
||||
let (handle_start, in_segment) = if let Some(vector) = &vector {
|
||||
vector
|
||||
.segment_bezier_iter()
|
||||
.segment_iter()
|
||||
.find_map(|(segment_id, bezier, start, end)| {
|
||||
let is_end = point == end;
|
||||
let is_start = point == start;
|
||||
@@ -1265,15 +1266,11 @@ impl PenToolData {
|
||||
return None;
|
||||
}
|
||||
|
||||
let handle = match bezier.handles {
|
||||
BezierHandles::Cubic { handle_start, handle_end, .. } => {
|
||||
if is_start {
|
||||
handle_start
|
||||
} else {
|
||||
handle_end
|
||||
}
|
||||
}
|
||||
BezierHandles::Quadratic { handle } => handle,
|
||||
let points = pathseg_points(bezier);
|
||||
let handle = match (points.p1, points.p2) {
|
||||
(Some(p1), Some(_)) if is_start => p1,
|
||||
(Some(_), Some(p2)) if !is_start => p2,
|
||||
(Some(p1), None) | (None, Some(p1)) => p1,
|
||||
_ => return None,
|
||||
};
|
||||
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));
|
||||
|
||||
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 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 {
|
||||
// Draw the curve for the currently-being-placed segment
|
||||
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.
|
||||
if let Some(end) = closest_point {
|
||||
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 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::transformation_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 graph_craft::document::NodeId;
|
||||
use graphene_std::path_bool::BooleanOperation;
|
||||
use graphene_std::renderer::Quad;
|
||||
use graphene_std::renderer::Rect;
|
||||
use graphene_std::subpath::Subpath;
|
||||
use graphene_std::transform::ReferencePoint;
|
||||
use std::fmt;
|
||||
|
||||
@@ -92,6 +92,7 @@ pub enum SelectToolMessage {
|
||||
remove_from_selection: Key,
|
||||
},
|
||||
EditLayer,
|
||||
EditLayerExec,
|
||||
Enter,
|
||||
PointerMove(SelectToolPointerKeys),
|
||||
PointerOutsideViewport(SelectToolPointerKeys),
|
||||
@@ -323,6 +324,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Sele
|
||||
PointerMove,
|
||||
Abort,
|
||||
EditLayer,
|
||||
EditLayerExec,
|
||||
Enter,
|
||||
);
|
||||
|
||||
@@ -689,32 +691,28 @@ impl Fsm for SelectToolFsmState {
|
||||
// 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
|
||||
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
|
||||
let selected_bounds_viewport = document
|
||||
// Compute document-space bounding box (AABB) of all selected visible & unlocked layers
|
||||
let selected_bounds_doc_space = document
|
||||
.network_interface
|
||||
.selected_nodes()
|
||||
.selected_visible_and_unlocked_layers(&document.network_interface)
|
||||
// Exclude layers that are artboards
|
||||
.filter(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[]))
|
||||
.filter_map(|layer| {
|
||||
// Get the layer's bounding box in its local space
|
||||
let local_bounds = document.metadata().bounding_box_with_transform(layer, DAffine2::IDENTITY)?;
|
||||
// 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()))
|
||||
})
|
||||
// For each remaining layer, try to get its document-space bounding box and convert it to a Rect
|
||||
.filter_map(|layer| document.metadata().bounding_box_document(layer).map(Rect::from_box))
|
||||
// Combine all individual bounding boxes into one overall bounding box that contains all selected layers
|
||||
.reduce(Rect::combine_bounds);
|
||||
|
||||
// Get the hovered layer's viewport-aligned AABB
|
||||
let hovered_bounds_viewport = document.metadata().bounding_box_with_transform(layer, DAffine2::IDENTITY).map(|bounds| {
|
||||
let viewport_quad = document.metadata().transform_to_viewport(layer) * Quad::from_box(bounds);
|
||||
Rect::from_box(viewport_quad.bounding_box())
|
||||
});
|
||||
// Compute document-space bounding box (AABB) of the currently hovered layer
|
||||
let hovered_bounds_doc_space = document.metadata().bounding_box_document(layer);
|
||||
|
||||
// Use the viewport-aligned AABBs for measurement
|
||||
if let (Some(selected_bounds), Some(hovered_bounds)) = (selected_bounds_viewport, hovered_bounds_viewport) {
|
||||
// Since we're already in viewport space, use identity transform
|
||||
measure::overlay(selected_bounds, hovered_bounds, DAffine2::IDENTITY, DAffine2::IDENTITY, &mut overlay_context);
|
||||
// If both selected and hovered bounds exist, overlay measurement lines
|
||||
if let (Some(selected_bounds), Some(hovered_bounds)) = (selected_bounds_doc_space, hovered_bounds_doc_space.map(Rect::from_box)) {
|
||||
// Both `selected_bounds` and `hovered_bounds` are in document space.
|
||||
// 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
|
||||
}
|
||||
(_, 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) {
|
||||
match tool_data.nested_selection_behavior {
|
||||
NestedSelectionBehavior::Shallowest => edit_layer_shallowest_manipulation(document, intersect, responses),
|
||||
NestedSelectionBehavior::Deepest => edit_layer_deepest_manipulation(intersect, &document.network_interface, responses),
|
||||
}
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
(
|
||||
|
||||
@@ -65,6 +65,8 @@ pub struct TransformLayerMessageHandler {
|
||||
|
||||
// Ghost outlines for Path Tool
|
||||
ghost_outline: Vec<(Vec<ClickTargetType>, DAffine2)>,
|
||||
|
||||
was_grabbing: bool,
|
||||
}
|
||||
|
||||
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.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)) {
|
||||
*selected.original_transforms = OriginalTransforms::default();
|
||||
|
||||
@@ -301,6 +303,7 @@ impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for
|
||||
responses.add(SelectToolMessage::PivotShift { offset: None, flush: true });
|
||||
|
||||
if final_transform {
|
||||
self.was_grabbing = false;
|
||||
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())
|
||||
|| (!using_path_tool && !using_select_tool && !using_pen_tool && !using_shape_tool)
|
||||
|| selected_layers.is_empty()
|
||||
|| transform_type.equivalent_to(self.transform_operation)
|
||||
|| (transform_type.equivalent_to(self.transform_operation) && !self.was_grabbing)
|
||||
{
|
||||
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 [point] = selected_points.as_slice() {
|
||||
if matches!(point, ManipulatorPointId::Anchor(_)) {
|
||||
if let Some([handle1, handle2]) = point.get_handle_pair(&vector) {
|
||||
let handle1_length = handle1.length(&vector);
|
||||
let handle2_length = handle2.length(&vector);
|
||||
if let Some(vector) = selected_layers.first().and_then(|&layer| document.network_interface.compute_modified_vector(layer))
|
||||
&& let [point] = selected_points.as_slice()
|
||||
{
|
||||
if matches!(point, ManipulatorPointId::Anchor(_)) {
|
||||
if let Some([handle1, handle2]) = point.get_handle_pair(&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) {
|
||||
// G should work for this point but not R and S
|
||||
if matches!(transform_type, TransformType::Rotate | TransformType::Scale) {
|
||||
selected.original_transforms.clear();
|
||||
return;
|
||||
}
|
||||
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
|
||||
if matches!(transform_type, TransformType::Rotate | TransformType::Scale) {
|
||||
selected.original_transforms.clear();
|
||||
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.) {
|
||||
selected.original_transforms.clear();
|
||||
return;
|
||||
}
|
||||
if handle_length == Some(0.) {
|
||||
selected.original_transforms.clear();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -409,6 +442,7 @@ impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for
|
||||
TransformLayerMessage::CancelTransformOperation => {
|
||||
if using_path_tool {
|
||||
self.ghost_outline.clear();
|
||||
self.was_grabbing = false;
|
||||
}
|
||||
|
||||
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() {
|
||||
match self.transform_operation {
|
||||
TransformOperation::None => unreachable!(),
|
||||
TransformOperation::None => {}
|
||||
TransformOperation::Grabbing(translation) => {
|
||||
let delta_pos = input.mouse.position - self.mouse_position;
|
||||
let delta_pos = (self.initial_transform * document_to_viewport.inverse()).transform_vector2(delta_pos);
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -8,13 +8,11 @@ use graph_craft::proto::GraphErrors;
|
||||
use graph_craft::wasm_application_io::EditorPreferences;
|
||||
use graphene_std::application_io::TimingInformation;
|
||||
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::text::FontCache;
|
||||
use graphene_std::transform::Footprint;
|
||||
use graphene_std::vector::Vector;
|
||||
use graphene_std::vector::style::ViewMode;
|
||||
use graphene_std::wasm_application_io::RenderOutputType;
|
||||
use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypesDelta;
|
||||
|
||||
mod runtime_io;
|
||||
@@ -33,7 +31,6 @@ pub struct ExecutionResponse {
|
||||
execution_id: u64,
|
||||
result: Result<TaggedValue, String>,
|
||||
responses: VecDeque<FrontendMessage>,
|
||||
transform: DAffine2,
|
||||
vector_modify: HashMap<NodeId, Vector>,
|
||||
/// The resulting value from the temporary inspected during execution
|
||||
inspect_result: Option<InspectResult>,
|
||||
@@ -57,7 +54,7 @@ pub struct NodeGraphExecutor {
|
||||
current_execution_id: u64,
|
||||
futures: HashMap<u64, ExecutionContext>,
|
||||
node_graph_hash: u64,
|
||||
old_inspect_node: Option<NodeId>,
|
||||
previous_node_to_inspect: Option<NodeId>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -79,7 +76,7 @@ impl NodeGraphExecutor {
|
||||
runtime_io: NodeRuntimeIO::with_channels(request_sender, response_receiver),
|
||||
node_graph_hash: 0,
|
||||
current_execution_id: 0,
|
||||
old_inspect_node: None,
|
||||
previous_node_to_inspect: None,
|
||||
};
|
||||
(node_runtime, node_executor)
|
||||
}
|
||||
@@ -112,24 +109,25 @@ impl NodeGraphExecutor {
|
||||
let instrumented = Instrumented::new(&mut network);
|
||||
|
||||
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())?;
|
||||
Ok(instrumented)
|
||||
}
|
||||
|
||||
/// 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();
|
||||
// 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();
|
||||
self.old_inspect_node = inspect_node;
|
||||
self.previous_node_to_inspect = node_to_inspect;
|
||||
self.node_graph_hash = network_hash;
|
||||
|
||||
self.runtime_io
|
||||
.send(GraphRuntimeRequest::GraphUpdate(GraphUpdate { network, inspect_node }))
|
||||
.send(GraphRuntimeRequest::GraphUpdate(GraphUpdate { network, node_to_inspect }))
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -172,10 +170,10 @@ impl NodeGraphExecutor {
|
||||
document_id: DocumentId,
|
||||
viewport_resolution: UVec2,
|
||||
time: TimingInformation,
|
||||
inspect_node: Option<NodeId>,
|
||||
node_to_inspect: Option<NodeId>,
|
||||
ignore_hash: bool,
|
||||
) -> 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)
|
||||
}
|
||||
|
||||
@@ -209,7 +207,7 @@ impl NodeGraphExecutor {
|
||||
|
||||
// Execute the node graph
|
||||
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())?;
|
||||
let execution_id = self.queue_execution(render_config);
|
||||
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> {
|
||||
let TaggedValue::RenderOutput(RenderOutput {
|
||||
data: graphene_std::wasm_application_io::RenderOutputType::Svg { svg, .. },
|
||||
data: RenderOutputType::Svg { svg, .. },
|
||||
..
|
||||
}) = node_graph_output
|
||||
else {
|
||||
@@ -251,6 +249,7 @@ impl NodeGraphExecutor {
|
||||
let size = (size * scale_factor).into();
|
||||
responses.add(FrontendMessage::TriggerExportImage { svg, name, mime, size });
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -263,7 +262,6 @@ impl NodeGraphExecutor {
|
||||
execution_id,
|
||||
result,
|
||||
responses: existing_responses,
|
||||
transform,
|
||||
vector_modify,
|
||||
inspect_result,
|
||||
} = execution_response;
|
||||
@@ -286,17 +284,17 @@ impl NodeGraphExecutor {
|
||||
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 {
|
||||
// Special handling for exporting the artwork
|
||||
self.export(node_graph_output, export_config, responses)?
|
||||
self.export(node_graph_output, export_config, responses)?;
|
||||
} 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.
|
||||
if self.old_inspect_node.is_some() {
|
||||
if let Some(inspect_result) = inspect_result {
|
||||
responses.add(SpreadsheetMessage::UpdateLayout { inspect_result });
|
||||
}
|
||||
// Update the Data panel on the frontend using the value of the inspect result.
|
||||
if let Some(inspect_result) = (self.previous_node_to_inspect.is_some()).then_some(inspect_result).flatten() {
|
||||
responses.add(DataPanelMessage::UpdateLayout { inspect_result });
|
||||
} else {
|
||||
responses.add(DataPanelMessage::ClearLayout);
|
||||
}
|
||||
}
|
||||
NodeGraphUpdate::CompilationResponse(execution_response) => {
|
||||
@@ -329,84 +327,45 @@ impl NodeGraphExecutor {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn debug_render(render_object: impl Render, transform: DAffine2, responses: &mut VecDeque<Message>) {
|
||||
// Setup rendering
|
||||
let mut render = SvgRender::new();
|
||||
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,
|
||||
fn process_node_graph_output(&mut self, node_graph_output: TaggedValue, responses: &mut VecDeque<Message>) -> Result<(), String> {
|
||||
let TaggedValue::RenderOutput(render_output) = node_graph_output else {
|
||||
return Err(format!("Invalid node graph output type: {node_graph_output:#?}"));
|
||||
};
|
||||
|
||||
// Render SVG
|
||||
render_object.render_svg(&mut render, &render_params);
|
||||
|
||||
// Concatenate the defs and the SVG into one string
|
||||
render.wrap_with_transform(transform, None);
|
||||
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;
|
||||
match render_output.data {
|
||||
RenderOutputType::Svg { svg, image_data } => {
|
||||
// Send to frontend
|
||||
responses.add(FrontendMessage::UpdateImageData { image_data });
|
||||
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
|
||||
}
|
||||
TaggedValue::Bool(render_object) => Self::debug_render(render_object, transform, responses),
|
||||
TaggedValue::String(render_object) => Self::debug_render(render_object, transform, responses),
|
||||
TaggedValue::F64(render_object) => Self::debug_render(render_object, transform, responses),
|
||||
TaggedValue::DVec2(render_object) => Self::debug_render(render_object, transform, responses),
|
||||
TaggedValue::OptionalColor(render_object) => Self::debug_render(render_object, transform, responses),
|
||||
TaggedValue::Vector(render_object) => Self::debug_render(render_object, transform, responses),
|
||||
TaggedValue::Graphic(render_object) => Self::debug_render(render_object, transform, responses),
|
||||
TaggedValue::Raster(render_object) => Self::debug_render(render_object, transform, responses),
|
||||
TaggedValue::Palette(render_object) => Self::debug_render(render_object, transform, responses),
|
||||
_ => {
|
||||
return Err(format!("Invalid node graph output type: {node_graph_output:#?}"));
|
||||
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 });
|
||||
}
|
||||
};
|
||||
let graphene_std::renderer::RenderMetadata {
|
||||
upstream_footprints: footprints,
|
||||
RenderOutputType::Texture { .. } => {}
|
||||
_ => return Err(format!("Invalid node graph output type: {:#?}", render_output.data)),
|
||||
}
|
||||
|
||||
let RenderMetadata {
|
||||
upstream_footprints,
|
||||
local_transforms,
|
||||
first_element_source_id,
|
||||
click_targets,
|
||||
clip_targets,
|
||||
} = render_output_metadata;
|
||||
} = render_output.metadata;
|
||||
|
||||
// Run these update state messages immediately
|
||||
responses.add(DocumentMessage::UpdateUpstreamTransforms {
|
||||
upstream_footprints: footprints,
|
||||
upstream_footprints,
|
||||
local_transforms,
|
||||
first_element_source_id,
|
||||
});
|
||||
@@ -415,6 +374,7 @@ impl NodeGraphExecutor {
|
||||
responses.add(DocumentMessage::RenderScrollbars);
|
||||
responses.add(DocumentMessage::RenderRulers);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,13 +8,14 @@ use graph_craft::proto::GraphErrors;
|
||||
use graph_craft::wasm_application_io::EditorPreferences;
|
||||
use graph_craft::{ProtoNodeIdentifier, concrete};
|
||||
use graphene_std::application_io::{ImageTexture, NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig};
|
||||
use graphene_std::bounds::RenderBoundingBox;
|
||||
use graphene_std::memo::IORecord;
|
||||
use graphene_std::renderer::{Render, RenderParams, SvgRender};
|
||||
use graphene_std::renderer::{RenderSvgSegmentList, SvgSegment};
|
||||
use graphene_std::table::{Table, TableRow};
|
||||
use graphene_std::text::FontCache;
|
||||
use graphene_std::transform::RenderQuality;
|
||||
use graphene_std::vector::Vector;
|
||||
use graphene_std::vector::style::ViewMode;
|
||||
use graphene_std::wasm_application_io::{RenderOutputType, WasmApplicationIo, WasmEditorApi};
|
||||
use graphene_std::{Artboard, Context, Graphic};
|
||||
use interpreted_executor::dynamic_executor::{DynamicExecutor, IntrospectError, ResolvedDocumentNodeTypesDelta};
|
||||
@@ -42,7 +43,7 @@ pub struct NodeRuntime {
|
||||
node_graph_errors: GraphErrors,
|
||||
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>,
|
||||
|
||||
/// Mapping of the fully-qualified node paths to their preprocessor substitutions.
|
||||
@@ -67,7 +68,7 @@ pub enum GraphRuntimeRequest {
|
||||
pub struct GraphUpdate {
|
||||
pub(super) network: NodeNetwork,
|
||||
/// 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)]
|
||||
@@ -188,22 +189,21 @@ impl NodeRuntime {
|
||||
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
|
||||
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.node_graph_errors.clear();
|
||||
let result = self.update_network(network).await;
|
||||
let node_graph_errors = self.node_graph_errors.clone();
|
||||
|
||||
self.update_thumbnails = true;
|
||||
self.sender.send_generation_response(CompilationResponse {
|
||||
result,
|
||||
node_graph_errors: self.node_graph_errors.clone(),
|
||||
});
|
||||
|
||||
self.sender.send_generation_response(CompilationResponse { result, node_graph_errors });
|
||||
}
|
||||
GraphRuntimeRequest::ExecutionRequest(ExecutionRequest { execution_id, render_config, .. }) => {
|
||||
let transform = render_config.viewport.transform;
|
||||
|
||||
let result = self.execute_network(render_config).await;
|
||||
let mut responses = VecDeque::new();
|
||||
// TODO: Only process monitor nodes if the graph has changed, not when only the Footprint changes
|
||||
@@ -227,7 +227,6 @@ impl NodeRuntime {
|
||||
execution_id,
|
||||
result,
|
||||
responses,
|
||||
transform,
|
||||
vector_modify: self.vector_modify.clone(),
|
||||
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)) {
|
||||
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
|
||||
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");
|
||||
|
||||
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 {
|
||||
// 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)]
|
||||
warn!("Failed to introspect monitor node {}", self.executor.introspect(monitor_node_path).unwrap_err());
|
||||
|
||||
continue;
|
||||
};
|
||||
|
||||
// Graphic table: thumbnail
|
||||
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)
|
||||
} else if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, Table<Artboard>>>() {
|
||||
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>>>() {
|
||||
if update_thumbnails {
|
||||
Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &io.output, responses)
|
||||
}
|
||||
}
|
||||
// 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();
|
||||
self.vector_modify
|
||||
.insert(parent_network_node_id, record.output.iter().next().unwrap_or_else(|| default.as_ref()).element.clone());
|
||||
} else {
|
||||
.insert(parent_network_node_id, io.output.iter().next().unwrap_or_else(|| default.as_ref()).element.clone());
|
||||
}
|
||||
// Other
|
||||
else {
|
||||
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.
|
||||
fn process_graphic(
|
||||
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;
|
||||
}
|
||||
|
||||
/// If this is `Graphic` data, regenerate click targets and thumbnails for the layers in the graph, modifying the state and updating the UI.
|
||||
fn render_thumbnail(thumbnail_renders: &mut HashMap<NodeId, Vec<SvgSegment>>, parent_network_node_id: NodeId, graphic: &impl Render, responses: &mut VecDeque<FrontendMessage>) {
|
||||
// Skip thumbnails if the layer is too complex (for performance)
|
||||
if graphic.render_complexity() > 1000 {
|
||||
let old = thumbnail_renders.insert(parent_network_node_id, Vec::new());
|
||||
@@ -349,24 +346,28 @@ impl NodeRuntime {
|
||||
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
|
||||
let render_params = RenderParams {
|
||||
view_mode: ViewMode::Normal,
|
||||
culling_bounds: bounds,
|
||||
footprint,
|
||||
thumbnail: true,
|
||||
hide_artboards: false,
|
||||
for_export: false,
|
||||
for_mask: false,
|
||||
alignment_parent_transform: None,
|
||||
..Default::default()
|
||||
};
|
||||
let mut render = SvgRender::new();
|
||||
graphic.render_svg(&mut render, &render_params);
|
||||
|
||||
// And give the SVG a viewbox and outer <svg>...</svg> wrapper tag
|
||||
let [min, max] = bounds.unwrap_or_default();
|
||||
render.format_svg(min, max);
|
||||
render.format_svg(bounds[0], bounds[1]);
|
||||
|
||||
// UPDATE FRONTEND THUMBNAIL
|
||||
|
||||
|
||||
@@ -82,18 +82,4 @@ impl DebugMessageTree {
|
||||
pub fn message_handler_fields(&self) -> Option<&MessageData> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,16 +113,22 @@
|
||||
|
||||
--color-data-general: #cfcfcf;
|
||||
--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-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-repeat: no-repeat;
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
export let colorOrGradient: FillChoice;
|
||||
export let allowNone = false;
|
||||
// export let allowTransparency = false; // TODO: Implement
|
||||
export let disabled = false;
|
||||
export let direction: MenuDirection = "Bottom";
|
||||
// TODO: See if this should be made to follow the pattern of DropdownInput.svelte so this could be removed
|
||||
export let open: boolean;
|
||||
@@ -133,6 +134,8 @@
|
||||
}
|
||||
|
||||
function onPointerDown(e: PointerEvent) {
|
||||
if (disabled) return;
|
||||
|
||||
const target = (e.target || undefined) as HTMLElement | undefined;
|
||||
draggingPickerTrack = target?.closest("[data-saturation-value-picker], [data-hue-picker], [data-alpha-picker]") || undefined;
|
||||
|
||||
@@ -403,7 +406,7 @@
|
||||
});
|
||||
</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
|
||||
styles={{
|
||||
"--new-color": newColor.toHexOptionalAlpha(),
|
||||
@@ -418,7 +421,7 @@
|
||||
>
|
||||
<LayoutCol class="pickers-and-gradient">
|
||||
<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}
|
||||
<div class="selection-circle" style:top={`${(1 - value) * 100}%`} style:left={`${saturation * 100}%`} />
|
||||
{/if}
|
||||
@@ -432,12 +435,12 @@
|
||||
/>
|
||||
{/if}
|
||||
</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}
|
||||
<div class="selection-needle" style:top={`${(1 - hue) * 100}%`} />
|
||||
{/if}
|
||||
</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}
|
||||
<div class="selection-needle" style:top={`${(1 - alpha) * 100}%`} />
|
||||
{/if}
|
||||
@@ -447,6 +450,7 @@
|
||||
<LayoutRow class="gradient">
|
||||
<SpectrumInput
|
||||
{gradient}
|
||||
{disabled}
|
||||
on:gradient={() => {
|
||||
gradient = gradient;
|
||||
if (gradient) dispatch("colorOrGradient", gradient);
|
||||
@@ -459,6 +463,7 @@
|
||||
{#if gradientSpectrumInputWidget && activeIndex !== undefined}
|
||||
<NumberInput
|
||||
value={(gradient.positionAtIndex(activeIndex) || 0) * 100}
|
||||
{disabled}
|
||||
on:value={({ detail }) => {
|
||||
if (gradientSpectrumInputWidget && activeIndex !== undefined && detail !== undefined) gradientSpectrumInputWidget.setPosition(activeIndex, detail / 100);
|
||||
}}
|
||||
@@ -478,7 +483,7 @@
|
||||
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"}
|
||||
>
|
||||
{#if !newColor.equals(oldColor)}
|
||||
{#if !newColor.equals(oldColor) && !disabled}
|
||||
<div class="swap-button-background"></div>
|
||||
<IconButton class="swap-button" icon="SwapHorizontal" size={16} action={swapNewWithOld} tooltip="Swap" />
|
||||
{/if}
|
||||
@@ -500,6 +505,7 @@
|
||||
<LayoutRow>
|
||||
<TextInput
|
||||
value={newColor.toHexOptionalAlpha() || "-"}
|
||||
{disabled}
|
||||
on:commitText={({ detail }) => {
|
||||
dispatch("startHistoryTransaction");
|
||||
setColorCode(detail);
|
||||
@@ -520,6 +526,7 @@
|
||||
{/if}
|
||||
<NumberInput
|
||||
value={strength}
|
||||
{disabled}
|
||||
on:value={({ detail }) => {
|
||||
strength = detail;
|
||||
setColorRGB(channel, detail);
|
||||
@@ -547,6 +554,7 @@
|
||||
{/if}
|
||||
<NumberInput
|
||||
value={strength}
|
||||
{disabled}
|
||||
on:value={({ detail }) => {
|
||||
strength = detail;
|
||||
setColorHSV(channel, detail);
|
||||
@@ -573,6 +581,7 @@
|
||||
<Separator type="Related" />
|
||||
<NumberInput
|
||||
value={!isNone ? alpha * 100 : undefined}
|
||||
{disabled}
|
||||
on:value={({ detail }) => {
|
||||
if (detail !== undefined) alpha = detail / 100;
|
||||
setColorAlphaPercent(detail);
|
||||
@@ -593,14 +602,14 @@
|
||||
<LayoutRow class="leftover-space" />
|
||||
<LayoutRow>
|
||||
{#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" />
|
||||
{/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" />
|
||||
<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" />
|
||||
<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="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" />
|
||||
@@ -609,7 +618,7 @@
|
||||
<div data-pure-tile="magenta" style="--pure-color: #ff00ff; --pure-color-gray: #696969" title="Set to magenta" />
|
||||
</button>
|
||||
<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>
|
||||
</LayoutCol>
|
||||
</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.
|
||||
// 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);
|
||||
transition: background-color 0.2s ease;
|
||||
transition: background-color 0.1s;
|
||||
}
|
||||
|
||||
&: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>
|
||||
|
||||
@@ -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"] {
|
||||
opacity: 0.25;
|
||||
transition: opacity 0.2s;
|
||||
transition: opacity 0.1s;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
@@ -825,7 +825,7 @@
|
||||
|
||||
.graph-view {
|
||||
pointer-events: none;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
transition: opacity 0.2s;
|
||||
opacity: 0;
|
||||
|
||||
&.open {
|
||||
|
||||
@@ -98,6 +98,12 @@
|
||||
});
|
||||
|
||||
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("keydown", clippingKeyPress);
|
||||
removeEventListener("keyup", clippingKeyPress);
|
||||
@@ -489,7 +495,9 @@
|
||||
<LayoutCol class="layers" on:dragleave={() => (dragInPanel = false)}>
|
||||
<LayoutRow class="control-bar" scrollableX={true}>
|
||||
<WidgetLayout layout={layersPanelControlBarLeftLayout} />
|
||||
<Separator />
|
||||
{#if layersPanelControlBarLeftLayout?.layout?.length > 0 && layersPanelControlBarRightLayout?.layout?.length > 0}
|
||||
<Separator />
|
||||
{/if}
|
||||
<WidgetLayout layout={layersPanelControlBarRightLayout} />
|
||||
</LayoutRow>
|
||||
<LayoutRow class="list-area" scrollableY={true}>
|
||||
@@ -605,6 +613,10 @@
|
||||
.widget-span:first-child {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
&:not(:has(*)) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Bottom bar
|
||||
@@ -619,6 +631,10 @@
|
||||
.widget-span > * {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&:not(:has(*)) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Layer hierarchy
|
||||
@@ -714,6 +730,7 @@
|
||||
width: 36px;
|
||||
height: 24px;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
flex: 0 0 auto;
|
||||
background-image: var(--color-transparent-checkered-background);
|
||||
background-size: var(--color-transparent-checkered-background-size-mini);
|
||||
@@ -721,9 +738,8 @@
|
||||
background-repeat: var(--color-transparent-checkered-background-repeat);
|
||||
|
||||
svg {
|
||||
width: calc(100% - 4px);
|
||||
height: calc(100% - 4px);
|
||||
margin: 2px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { getContext, onMount } from "svelte";
|
||||
import { getContext, onMount, onDestroy } from "svelte";
|
||||
|
||||
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 WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
|
||||
|
||||
const editor = getContext<Editor>("editor");
|
||||
|
||||
let propertiesSectionsLayout = defaultWidgetLayout();
|
||||
let propertiesPanelLayout = defaultWidgetLayout();
|
||||
|
||||
onMount(() => {
|
||||
editor.subscriptions.subscribeJsMessage(UpdatePropertyPanelSectionsLayout, (updatePropertyPanelSectionsLayout) => {
|
||||
patchWidgetLayout(propertiesSectionsLayout, updatePropertyPanelSectionsLayout);
|
||||
propertiesSectionsLayout = propertiesSectionsLayout;
|
||||
editor.subscriptions.subscribeJsMessage(UpdatePropertiesPanelLayout, (updatePropertiesPanelLayout) => {
|
||||
patchWidgetLayout(propertiesPanelLayout, updatePropertiesPanelLayout);
|
||||
propertiesPanelLayout = propertiesPanelLayout;
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
editor.subscriptions.unsubscribeJsMessage(UpdatePropertiesPanelLayout);
|
||||
});
|
||||
</script>
|
||||
|
||||
<LayoutCol class="properties">
|
||||
<LayoutCol class="sections" scrollableY={true}>
|
||||
<WidgetLayout layout={propertiesSectionsLayout} />
|
||||
<WidgetLayout layout={propertiesPanelLayout} />
|
||||
</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>
|
||||
@@ -32,7 +32,7 @@
|
||||
// Imports/Export are stored at a key value of 0
|
||||
|
||||
$: 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 hoveringImportIndex: number | undefined = undefined;
|
||||
@@ -192,7 +192,7 @@
|
||||
return output.connectedTo
|
||||
.map((inputConnector) => {
|
||||
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");
|
||||
}
|
||||
@@ -200,7 +200,7 @@
|
||||
function inputConnectedToText(input: FrontendGraphInput): string {
|
||||
if (input.connectedTo === undefined) return "Connected to nothing";
|
||||
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 {
|
||||
@@ -238,7 +238,7 @@
|
||||
style:--grid-spacing={`${gridSpacing}px`}
|
||||
style:--grid-offset-x={`${$nodeGraph.transform.x}px`}
|
||||
style:--grid-offset-y={`${$nodeGraph.transform.y}px`}
|
||||
style:--dot-radius={`${dotRadius}px`}
|
||||
style:--grid-dot-radius={`${gridDotRadius}px`}
|
||||
data-node-graph
|
||||
>
|
||||
<!-- Right click menu for adding nodes -->
|
||||
@@ -298,8 +298,8 @@
|
||||
{#each $nodeGraph.clickTargets.layerClickTargets as pathString}
|
||||
<path class="layer" d={pathString} />
|
||||
{/each}
|
||||
{#each $nodeGraph.clickTargets.portClickTargets as pathString}
|
||||
<path class="port" d={pathString} />
|
||||
{#each $nodeGraph.clickTargets.connectorClickTargets as pathString}
|
||||
<path class="connector" d={pathString} />
|
||||
{/each}
|
||||
{#each $nodeGraph.clickTargets.iconClickTargets as pathString}
|
||||
<path class="visibility" d={pathString} />
|
||||
@@ -332,14 +332,14 @@
|
||||
</svg>
|
||||
</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})`}>
|
||||
{#each $nodeGraph.imports as { outputMetadata, position }, index}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 8 8"
|
||||
class="port"
|
||||
data-port="output"
|
||||
class="connector"
|
||||
data-connector="output"
|
||||
data-datatype={outputMetadata.dataType}
|
||||
style:--data-color={`var(--color-data-${outputMetadata.dataType.toLowerCase()})`}
|
||||
style:--data-color-dim={`var(--color-data-${outputMetadata.dataType.toLowerCase()}-dim)`}
|
||||
@@ -411,8 +411,8 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 8 8"
|
||||
class="port"
|
||||
data-port="input"
|
||||
class="connector"
|
||||
data-connector="input"
|
||||
data-datatype={inputMetadata.dataType}
|
||||
style:--data-color={`var(--color-data-${inputMetadata.dataType.toLowerCase()})`}
|
||||
style:--data-color-dim={`var(--color-data-${inputMetadata.dataType.toLowerCase()}-dim)`}
|
||||
@@ -521,8 +521,8 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 8 12"
|
||||
class="port top"
|
||||
data-port="output"
|
||||
class="connector top"
|
||||
data-connector="output"
|
||||
data-datatype={node.primaryOutput.dataType}
|
||||
style:--data-color={`var(--color-data-${node.primaryOutput.dataType.toLowerCase()})`}
|
||||
style:--data-color-dim={`var(--color-data-${node.primaryOutput.dataType.toLowerCase()}-dim)`}
|
||||
@@ -542,8 +542,8 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 8 12"
|
||||
class="port bottom"
|
||||
data-port="input"
|
||||
class="connector bottom"
|
||||
data-connector="input"
|
||||
data-datatype={node.primaryInput?.dataType}
|
||||
style:--data-color={`var(--color-data-${(node.primaryInput?.dataType || "General").toLowerCase()})`}
|
||||
style:--data-color-dim={`var(--color-data-${(node.primaryInput?.dataType || "General").toLowerCase()}-dim)`}
|
||||
@@ -561,14 +561,14 @@
|
||||
{/if}
|
||||
</svg>
|
||||
</div>
|
||||
<!-- Layer input port (from left) -->
|
||||
<!-- Layer input connector (from left) -->
|
||||
{#if node.exposedInputs.length > 0}
|
||||
<div class="input ports">
|
||||
<div class="input connectors">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 8 8"
|
||||
class="port"
|
||||
data-port="input"
|
||||
class="connector"
|
||||
data-connector="input"
|
||||
data-datatype={stackDataInput.dataType}
|
||||
style:--data-color={`var(--color-data-${stackDataInput.dataType.toLowerCase()})`}
|
||||
style:--data-color-dim={`var(--color-data-${stackDataInput.dataType.toLowerCase()}-dim)`}
|
||||
@@ -679,14 +679,14 @@
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<!-- Input ports -->
|
||||
<div class="input ports">
|
||||
<!-- Input connectors -->
|
||||
<div class="input connectors">
|
||||
{#if node.primaryInput?.dataType}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 8 8"
|
||||
class="port primary-port"
|
||||
data-port="input"
|
||||
class="connector primary-connector"
|
||||
data-connector="input"
|
||||
data-datatype={node.primaryInput?.dataType}
|
||||
style:--data-color={`var(--color-data-${node.primaryInput.dataType.toLowerCase()})`}
|
||||
style:--data-color-dim={`var(--color-data-${node.primaryInput.dataType.toLowerCase()}-dim)`}
|
||||
@@ -704,8 +704,8 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 8 8"
|
||||
class="port"
|
||||
data-port="input"
|
||||
class="connector"
|
||||
data-connector="input"
|
||||
data-datatype={secondary.dataType}
|
||||
style:--data-color={`var(--color-data-${secondary.dataType.toLowerCase()})`}
|
||||
style:--data-color-dim={`var(--color-data-${secondary.dataType.toLowerCase()}-dim)`}
|
||||
@@ -720,14 +720,14 @@
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
<!-- Output ports -->
|
||||
<div class="output ports">
|
||||
<!-- Output connectors -->
|
||||
<div class="output connectors">
|
||||
{#if node.primaryOutput}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 8 8"
|
||||
class="port primary-port"
|
||||
data-port="output"
|
||||
class="connector primary-connector"
|
||||
data-connector="output"
|
||||
data-datatype={node.primaryOutput.dataType}
|
||||
style:--data-color={`var(--color-data-${node.primaryOutput.dataType.toLowerCase()})`}
|
||||
style:--data-color-dim={`var(--color-data-${node.primaryOutput.dataType.toLowerCase()}-dim)`}
|
||||
@@ -744,8 +744,8 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 8 8"
|
||||
class="port"
|
||||
data-port="output"
|
||||
class="connector"
|
||||
data-connector="output"
|
||||
data-datatype={secondary.dataType}
|
||||
style:--data-color={`var(--color-data-${secondary.dataType.toLowerCase()})`}
|
||||
style:--data-color-dim={`var(--color-data-${secondary.dataType.toLowerCase()}-dim)`}
|
||||
@@ -801,8 +801,8 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
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-image: radial-gradient(circle at var(--dot-radius) var(--dot-radius), var(--color-3-darkgray) var(--dot-radius), transparent 0);
|
||||
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(--grid-dot-radius) var(--grid-dot-radius), var(--color-3-darkgray) var(--grid-dot-radius), transparent 0);
|
||||
background-repeat: repeat;
|
||||
image-rendering: pixelated;
|
||||
mix-blend-mode: screen;
|
||||
@@ -859,7 +859,7 @@
|
||||
stroke: blue;
|
||||
}
|
||||
|
||||
.port {
|
||||
.connector {
|
||||
stroke: green;
|
||||
}
|
||||
|
||||
@@ -898,11 +898,13 @@
|
||||
}
|
||||
|
||||
.imports-and-exports {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
// Keeps the connectors above the wires
|
||||
z-index: 1;
|
||||
|
||||
.port {
|
||||
.connector {
|
||||
position: absolute;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
@@ -1008,7 +1010,7 @@
|
||||
border-radius: 4px;
|
||||
bottom: calc(100% + 12px);
|
||||
z-index: -1;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
transition: opacity 0.2s;
|
||||
opacity: 0.5;
|
||||
|
||||
// Tail
|
||||
@@ -1040,7 +1042,7 @@
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
transition:
|
||||
opacity 0.2s ease-in-out,
|
||||
opacity 0.2s,
|
||||
z-index 0s 0.2s;
|
||||
|
||||
&::selection {
|
||||
@@ -1088,8 +1090,10 @@
|
||||
border: 1px dashed var(--data-color);
|
||||
}
|
||||
|
||||
.ports {
|
||||
.connectors {
|
||||
position: absolute;
|
||||
// Keeps the connectors above the wires
|
||||
z-index: 1;
|
||||
|
||||
&.input {
|
||||
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
|
||||
margin: calc(24px - 8px) 0;
|
||||
width: 8px;
|
||||
@@ -1117,7 +1121,7 @@
|
||||
border-radius: 8px;
|
||||
--extra-width-to-reach-grid-multiple: 8px;
|
||||
--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);
|
||||
padding-left: calc(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;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
width: 72px;
|
||||
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 {
|
||||
content: "";
|
||||
@@ -1153,7 +1159,7 @@
|
||||
}
|
||||
|
||||
&::before,
|
||||
svg:not(.port) {
|
||||
svg:not(.connector) {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
margin: auto;
|
||||
@@ -1163,7 +1169,7 @@
|
||||
height: calc(100% - 2px);
|
||||
}
|
||||
|
||||
.port {
|
||||
.connector {
|
||||
position: absolute;
|
||||
margin: 0 auto;
|
||||
left: 0;
|
||||
@@ -1211,21 +1217,21 @@
|
||||
right: -12px;
|
||||
}
|
||||
|
||||
.input.ports {
|
||||
.input.connectors {
|
||||
left: calc(-3px + var(--node-chain-area-left-extension) * 24px - 36px);
|
||||
}
|
||||
|
||||
.solo-drag-grip,
|
||||
.visibility,
|
||||
.input.ports,
|
||||
.input.ports .port {
|
||||
.input.connectors,
|
||||
.input.connectors .connector {
|
||||
position: absolute;
|
||||
margin: auto 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.input.ports .port {
|
||||
.input.connectors .connector {
|
||||
left: 24px;
|
||||
}
|
||||
}
|
||||
@@ -1259,11 +1265,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.port {
|
||||
.connector {
|
||||
&:first-of-type {
|
||||
margin-top: calc((24px - 8px) / 2);
|
||||
|
||||
&:not(.primary-port) {
|
||||
&:not(.primary-connector) {
|
||||
margin-top: calc((24px - 8px) / 2 + 24px);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
import TextInput from "@graphite/components/widgets/inputs/TextInput.svelte";
|
||||
import WorkingColorsInput from "@graphite/components/widgets/inputs/WorkingColorsInput.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 TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
|
||||
@@ -124,6 +125,10 @@
|
||||
{#if iconLabel}
|
||||
<IconLabel {...exclude(iconLabel)} />
|
||||
{/if}
|
||||
{@const imageLabel = narrowWidgetProps(component.props, "ImageLabel")}
|
||||
{#if imageLabel}
|
||||
<ImageLabel {...exclude(imageLabel)} />
|
||||
{/if}
|
||||
{@const imageButton = narrowWidgetProps(component.props, "ImageButton")}
|
||||
{#if imageButton}
|
||||
<ImageButton {...exclude(imageButton)} action={() => widgetValueCommitAndUpdate(index, undefined)} />
|
||||
|
||||
@@ -17,14 +17,15 @@
|
||||
.join(" ");
|
||||
</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>
|
||||
.image-label {
|
||||
.image-button {
|
||||
width: auto;
|
||||
height: auto;
|
||||
border-radius: 2px;
|
||||
|
||||
+ .image-label.image-label {
|
||||
+ .image-button.image-button {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<script lang="ts">
|
||||
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 ColorPicker from "@graphite/components/floating-menus/ColorPicker.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 }>();
|
||||
|
||||
@@ -15,6 +14,7 @@
|
||||
export let value: FillChoice;
|
||||
export let disabled = false;
|
||||
export let allowNone = false;
|
||||
export let menuDirection: MenuDirection = "Bottom";
|
||||
// export let allowTransparency = false; // TODO: Implement
|
||||
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;
|
||||
</script>
|
||||
|
||||
<LayoutCol class="color-button" classes={{ open, disabled, none, transparency, outlined }} {tooltip}>
|
||||
<button {disabled} 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}
|
||||
<LayoutCol class="color-button" classes={{ open, disabled, none, transparency, outlined, "direction-top": menuDirection === "Top" }} {tooltip}>
|
||||
<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}
|
||||
<TextLabel>sRGB</TextLabel>
|
||||
{/if}
|
||||
{/if} -->
|
||||
</button>
|
||||
<ColorPicker
|
||||
{open}
|
||||
on:open={({ detail }) => (open = detail)}
|
||||
{disabled}
|
||||
colorOrGradient={value}
|
||||
direction={menuDirection || "Bottom"}
|
||||
on:open={({ detail }) => (open = detail)}
|
||||
on:colorOrGradient={({ detail }) => {
|
||||
value = detail;
|
||||
dispatch("value", detail);
|
||||
@@ -137,5 +139,9 @@
|
||||
left: 50%;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
&.direction-top > .floating-menu {
|
||||
bottom: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
const dispatch = createEventDispatcher<{ activeMarkerIndexChange: number | undefined; gradient: Gradient; dragging: boolean }>();
|
||||
|
||||
export let gradient: Gradient;
|
||||
export let disabled = false;
|
||||
export let activeMarkerIndex = 0 as number | undefined;
|
||||
// export let disabled = false;
|
||||
// export let tooltip: string | undefined = undefined;
|
||||
@@ -22,6 +23,8 @@
|
||||
let deletionRestore: boolean | undefined = undefined;
|
||||
|
||||
function markerPointerDown(e: PointerEvent, index: number) {
|
||||
if (disabled) return;
|
||||
|
||||
// Left-click to select and begin potentially dragging
|
||||
if (e.button === BUTTON_LEFT) {
|
||||
activeMarkerIndex = index;
|
||||
@@ -47,6 +50,8 @@
|
||||
}
|
||||
|
||||
function insertStop(e: MouseEvent) {
|
||||
if (disabled) return;
|
||||
|
||||
if (e.button !== BUTTON_LEFT) return;
|
||||
|
||||
let position = markerPosition(e);
|
||||
@@ -79,6 +84,8 @@
|
||||
}
|
||||
|
||||
function deleteStop(e: KeyboardEvent) {
|
||||
if (disabled) return;
|
||||
|
||||
if (e.key !== "Delete" && e.key !== "Backspace") return;
|
||||
if (activeMarkerIndex === undefined) return;
|
||||
|
||||
@@ -88,6 +95,8 @@
|
||||
}
|
||||
|
||||
function deleteStopByIndex(index: number) {
|
||||
if (disabled) return;
|
||||
|
||||
if (gradient.stops.length <= 2) return;
|
||||
|
||||
gradient.stops.splice(index, 1);
|
||||
@@ -103,6 +112,8 @@
|
||||
}
|
||||
|
||||
function moveMarker(e: PointerEvent, index: number) {
|
||||
if (disabled) return;
|
||||
|
||||
// Just in case the mouseup event is lost
|
||||
if (e.buttons === 0) stopDrag();
|
||||
|
||||
@@ -120,6 +131,8 @@
|
||||
}
|
||||
|
||||
export function setPosition(index: number, position: number) {
|
||||
if (disabled) return;
|
||||
|
||||
const active = gradient.stops[index];
|
||||
active.position = position;
|
||||
gradient.stops.sort((a, b) => a.position - b.position);
|
||||
@@ -131,6 +144,8 @@
|
||||
}
|
||||
|
||||
function abortDrag() {
|
||||
if (disabled) return;
|
||||
|
||||
if (activeMarkerIndex === undefined) return;
|
||||
|
||||
if (deletionRestore) {
|
||||
@@ -143,6 +158,8 @@
|
||||
}
|
||||
|
||||
function stopDrag() {
|
||||
if (disabled) return;
|
||||
|
||||
removeEvents();
|
||||
|
||||
positionRestore = undefined;
|
||||
@@ -152,19 +169,27 @@
|
||||
}
|
||||
|
||||
function onPointerMove(e: PointerEvent) {
|
||||
if (disabled) return;
|
||||
|
||||
if (activeMarkerIndex !== undefined) moveMarker(e, activeMarkerIndex);
|
||||
}
|
||||
|
||||
function onPointerUp() {
|
||||
if (disabled) return;
|
||||
|
||||
stopDrag();
|
||||
}
|
||||
|
||||
function onMouseDown(e: MouseEvent) {
|
||||
if (disabled) return;
|
||||
|
||||
const BUTTONS_RIGHT = 0b0000_0010;
|
||||
if (e.buttons & BUTTONS_RIGHT) abortDrag();
|
||||
}
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
if (disabled) return;
|
||||
|
||||
if (e.key === "Escape") {
|
||||
const element = markerTrack?.div();
|
||||
if (element) preventEscapeClosingParentFloatingMenu(element);
|
||||
@@ -193,25 +218,28 @@
|
||||
document.removeEventListener("keydown", deleteStop);
|
||||
});
|
||||
|
||||
// Future design notes:
|
||||
//
|
||||
// # Backend -> Frontend
|
||||
// Populate(gradient, { position, color }[], active) // The only way indexes get changed. Frontend drops marker if it's being dragged.
|
||||
// UpdateGradient(gradient)
|
||||
// UpdateMarkers({ index, position, color }[])
|
||||
|
||||
//
|
||||
// # Frontend -> Backend
|
||||
// SendNewActive(index)
|
||||
// SendPositions({ index, position }[])
|
||||
// AddMarker(position)
|
||||
// RemoveMarkers(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 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 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 the ability to multi-select markers and move them all at once
|
||||
</script>
|
||||
|
||||
<LayoutCol
|
||||
class="spectrum-input"
|
||||
classes={{ disabled }}
|
||||
styles={{
|
||||
"--gradient-start": gradient.firstColor()?.toHexOptionalAlpha() || "black",
|
||||
"--gradient-end": gradient.lastColor()?.toHexOptionalAlpha() || "black",
|
||||
@@ -232,6 +260,9 @@
|
||||
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" />
|
||||
{#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
|
||||
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"
|
||||
@@ -269,6 +300,14 @@
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
&.disabled .gradient-strip {
|
||||
transition: opacity 0.1s;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.marker-track {
|
||||
margin-top: calc(24px - 16px - 12px);
|
||||
margin-left: var(--marker-half-width);
|
||||
@@ -294,28 +333,40 @@
|
||||
.outer-border {
|
||||
fill: var(--color-5-dullgray);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.active) {
|
||||
.inner-fill:hover + .outer-border,
|
||||
.outer-border:hover {
|
||||
fill: var(--color-6-lowergray);
|
||||
}
|
||||
&.disabled .marker-track .marker {
|
||||
.disabled-fill {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.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 {
|
||||
.inner-fill {
|
||||
filter: drop-shadow(0 0 1px var(--color-2-mildblack)) drop-shadow(0 0 1px var(--color-2-mildblack));
|
||||
}
|
||||
// Outer border when active
|
||||
.outer-border {
|
||||
fill: var(--color-e-nearwhite);
|
||||
}
|
||||
|
||||
// Outer border when active
|
||||
.outer-border {
|
||||
fill: var(--color-e-nearwhite);
|
||||
}
|
||||
|
||||
.inner-fill:hover + .outer-border,
|
||||
.outer-border:hover {
|
||||
fill: var(--color-f-white);
|
||||
}
|
||||
.inner-fill:hover + .outer-border,
|
||||
.outer-border:hover {
|
||||
fill: var(--color-f-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
// Styling
|
||||
export let centered = false;
|
||||
export let minWidth = 0;
|
||||
export let maxWidth = 0;
|
||||
|
||||
let className = "";
|
||||
export { className as class };
|
||||
@@ -63,7 +64,10 @@
|
||||
<FieldInput
|
||||
class={`text-input ${className}`.trim()}
|
||||
classes={{ centered, ...classes }}
|
||||
styles={{ ...(minWidth > 0 ? { "min-width": `${minWidth}px` } : {}) }}
|
||||
styles={{
|
||||
...(minWidth > 0 ? { "min-width": `${minWidth}px` } : {}),
|
||||
...(maxWidth > 0 ? { "max-width": `${maxWidth}px` } : {}),
|
||||
}}
|
||||
{value}
|
||||
on:value
|
||||
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">
|
||||
import Data from "@graphite/components/panels/Data.svelte";
|
||||
import Document from "@graphite/components/panels/Document.svelte";
|
||||
import Layers from "@graphite/components/panels/Layers.svelte";
|
||||
import Properties from "@graphite/components/panels/Properties.svelte";
|
||||
import Spreadsheet from "@graphite/components/panels/Spreadsheet.svelte";
|
||||
|
||||
const PANEL_COMPONENTS = {
|
||||
Document,
|
||||
Layers,
|
||||
Properties,
|
||||
Spreadsheet,
|
||||
Data,
|
||||
};
|
||||
type PanelType = keyof typeof PANEL_COMPONENTS;
|
||||
</script>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
/**/ root: 100,
|
||||
/* ├─ */ content: 80,
|
||||
/* │ ├─ */ document: 70,
|
||||
/* │ └─ */ spreadsheet: 30,
|
||||
/* │ └─ */ data: 30,
|
||||
/* └─ */ details: 20,
|
||||
/* ├─ */ properties: 45,
|
||||
/* └─ */ layers: 55,
|
||||
@@ -148,23 +148,31 @@
|
||||
bind:this={documentPanel}
|
||||
/>
|
||||
</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-subdivision" styles={{ "flex-grow": panelSizes["spreadsheet"] }} data-subdivision-name="spreadsheet">
|
||||
<Panel panelType="Spreadsheet" tabLabels={[{ name: "Spreadsheet" }]} tabActiveIndex={0} />
|
||||
<LayoutRow class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["data"] }} data-subdivision-name="data">
|
||||
<Panel panelType="Data" tabLabels={[{ name: "Data" }]} tabActiveIndex={0} />
|
||||
</LayoutRow>
|
||||
{/if}
|
||||
</LayoutCol>
|
||||
<LayoutCol class="workspace-grid-resize-gutter" data-gutter-horizontal on:pointerdown={(e) => resizePanel(e)} />
|
||||
<LayoutCol class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["details"] }} data-subdivision-name="details">
|
||||
<LayoutRow class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["properties"] }} data-subdivision-name="properties">
|
||||
<Panel panelType="Properties" tabLabels={[{ name: "Properties" }]} tabActiveIndex={0} />
|
||||
</LayoutRow>
|
||||
<LayoutRow class="workspace-grid-resize-gutter" data-gutter-vertical on:pointerdown={(e) => resizePanel(e)} />
|
||||
<LayoutRow class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["layers"] }} data-subdivision-name="layers">
|
||||
<Panel panelType="Layers" tabLabels={[{ name: "Layers" }]} tabActiveIndex={0} />
|
||||
</LayoutRow>
|
||||
</LayoutCol>
|
||||
{#if $portfolio.propertiesPanelOpen || $portfolio.layersPanelOpen}
|
||||
<LayoutCol class="workspace-grid-resize-gutter" data-gutter-horizontal on:pointerdown={(e) => resizePanel(e)} />
|
||||
<LayoutCol class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["details"] }} data-subdivision-name="details">
|
||||
{#if $portfolio.propertiesPanelOpen}
|
||||
<LayoutRow class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["properties"] }} data-subdivision-name="properties">
|
||||
<Panel panelType="Properties" tabLabels={[{ name: "Properties" }]} tabActiveIndex={0} />
|
||||
</LayoutRow>
|
||||
{/if}
|
||||
{#if $portfolio.propertiesPanelOpen && $portfolio.layersPanelOpen}
|
||||
<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>
|
||||
{#if $dialog.visible}
|
||||
<Dialog />
|
||||
|
||||
+54
-17
@@ -180,7 +180,7 @@ export class Box {
|
||||
export type FrontendClickTargets = {
|
||||
readonly nodeClickTargets: string[];
|
||||
readonly layerClickTargets: string[];
|
||||
readonly portClickTargets: string[];
|
||||
readonly connectorClickTargets: string[];
|
||||
readonly iconClickTargets: string[];
|
||||
readonly allNodesBoundingBox: string;
|
||||
readonly importExportsBoundingBox: string;
|
||||
@@ -192,7 +192,7 @@ export type ContextMenuInformation = {
|
||||
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 {
|
||||
readonly index!: bigint;
|
||||
@@ -763,10 +763,16 @@ export class UpdateGraphFadeArtwork extends JsMessage {
|
||||
readonly percentage!: number;
|
||||
}
|
||||
|
||||
export class UpdateSpreadsheetState extends JsMessage {
|
||||
export class UpdateDataPanelState extends JsMessage {
|
||||
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 {
|
||||
@@ -981,9 +987,11 @@ export class ColorInput extends WidgetProps {
|
||||
})
|
||||
value!: FillChoice;
|
||||
|
||||
allowNone!: boolean;
|
||||
|
||||
disabled!: boolean;
|
||||
|
||||
allowNone!: boolean;
|
||||
menuDirection!: MenuDirection | undefined;
|
||||
|
||||
// allowTransparency!: boolean; // TODO: Implement
|
||||
|
||||
@@ -1140,6 +1148,19 @@ export class ImageButton extends WidgetProps {
|
||||
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 NumberInputMode = "Increment" | "Range";
|
||||
|
||||
@@ -1332,6 +1353,8 @@ export class TextInput extends WidgetProps {
|
||||
|
||||
minWidth!: number;
|
||||
|
||||
maxWidth!: number;
|
||||
|
||||
@Transform(({ value }: { value: string }) => value || undefined)
|
||||
tooltip!: string | undefined;
|
||||
}
|
||||
@@ -1383,6 +1406,7 @@ const widgetSubTypes = [
|
||||
{ value: FontInput, name: "FontInput" },
|
||||
{ value: IconButton, name: "IconButton" },
|
||||
{ value: ImageButton, name: "ImageButton" },
|
||||
{ value: ImageLabel, name: "ImageLabel" },
|
||||
{ value: IconLabel, name: "IconLabel" },
|
||||
{ value: NodeCatalog, name: "NodeCatalog" },
|
||||
{ value: NumberInput, name: "NumberInput" },
|
||||
@@ -1472,11 +1496,11 @@ export function patchWidgetLayout(layout: /* &mut */ WidgetLayout, updates: Widg
|
||||
|
||||
updates.diff.forEach((update) => {
|
||||
// Find the object where the diff applies to
|
||||
const diffObject = update.widgetPath.reduce((targetLayout, index) => {
|
||||
if ("columnWidgets" in targetLayout) return targetLayout.columnWidgets[index];
|
||||
if ("rowWidgets" in targetLayout) return targetLayout.rowWidgets[index];
|
||||
if ("tableWidgets" in targetLayout) return targetLayout.tableWidgets[index];
|
||||
if ("layout" in targetLayout) return targetLayout.layout[index];
|
||||
const diffObject = update.widgetPath.reduce((targetLayout: UIItem | undefined, index: number): UIItem | undefined => {
|
||||
if (targetLayout && "columnWidgets" in targetLayout) return targetLayout.columnWidgets[index];
|
||||
if (targetLayout && "rowWidgets" in targetLayout) return targetLayout.rowWidgets[index];
|
||||
if (targetLayout && "tableWidgets" in targetLayout) return targetLayout.tableWidgets[index];
|
||||
if (targetLayout && "layout" in targetLayout) return targetLayout.layout[index];
|
||||
if (targetLayout instanceof Widget) {
|
||||
if (targetLayout.props.kind === "PopoverButton" && targetLayout.props instanceof PopoverButton && targetLayout.props.popoverLayout) {
|
||||
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
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
if ("action" in targetLayout) return targetLayout.children![index];
|
||||
return targetLayout[index];
|
||||
if (targetLayout && "action" in targetLayout) return targetLayout.children![index];
|
||||
|
||||
return targetLayout?.[index];
|
||||
}, 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 ("length" in diffObject) {
|
||||
diffObject.length = 0;
|
||||
@@ -1613,9 +1648,9 @@ export class UpdateMenuBarLayout extends JsMessage {
|
||||
|
||||
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 {}
|
||||
|
||||
@@ -1712,9 +1747,11 @@ export const messageMakers: Record<string, MessageMaker> = {
|
||||
UpdateNodeThumbnail,
|
||||
UpdateOpenDocumentsList,
|
||||
UpdatePlatform,
|
||||
UpdatePropertyPanelSectionsLayout,
|
||||
UpdateSpreadsheetLayout,
|
||||
UpdateSpreadsheetState,
|
||||
UpdatePropertiesPanelLayout,
|
||||
UpdateDataPanelLayout,
|
||||
UpdateDataPanelState,
|
||||
UpdatePropertiesPanelState,
|
||||
UpdateLayersPanelState,
|
||||
UpdateToolOptionsLayout,
|
||||
UpdateToolShelfLayout,
|
||||
UpdateViewportHolePunch,
|
||||
|
||||
@@ -13,10 +13,9 @@ import {
|
||||
TriggerOpenDocument,
|
||||
UpdateActiveDocument,
|
||||
UpdateOpenDocumentsList,
|
||||
UpdateSpreadsheetState,
|
||||
defaultWidgetLayout,
|
||||
patchWidgetLayout,
|
||||
UpdateSpreadsheetLayout,
|
||||
UpdateDataPanelState,
|
||||
UpdatePropertiesPanelState,
|
||||
UpdateLayersPanelState,
|
||||
} from "@graphite/messages";
|
||||
import { downloadFile, downloadFileBlob, upload } from "@graphite/utility-functions/files";
|
||||
import { extractPixelData, rasterizeSVG } from "@graphite/utility-functions/rasterization";
|
||||
@@ -27,9 +26,9 @@ export function createPortfolioState(editor: Editor) {
|
||||
unsaved: false,
|
||||
documents: [] as FrontendDocumentDetails[],
|
||||
activeDocumentIndex: 0,
|
||||
spreadsheetOpen: false,
|
||||
spreadsheetNode: BigInt(0) as bigint | undefined,
|
||||
spreadsheetWidgets: defaultWidgetLayout(),
|
||||
dataPanelOpen: false,
|
||||
propertiesPanelOpen: true,
|
||||
layersPanelOpen: true,
|
||||
});
|
||||
|
||||
// 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
|
||||
}
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateSpreadsheetState, async (updateSpreadsheetState) => {
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDataPanelState, async (updateDataPanelState) => {
|
||||
update((state) => {
|
||||
state.spreadsheetOpen = updateSpreadsheetState.open;
|
||||
state.spreadsheetNode = updateSpreadsheetState.node;
|
||||
state.dataPanelOpen = updateDataPanelState.open;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateSpreadsheetLayout, (updateSpreadsheetLayout) => {
|
||||
editor.subscriptions.subscribeJsMessage(UpdatePropertiesPanelState, async (updatePropertiesPanelState) => {
|
||||
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;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,6 +17,10 @@ export function createSubscriptionRouter() {
|
||||
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) => {
|
||||
// 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];
|
||||
@@ -68,6 +72,7 @@ export function createSubscriptionRouter() {
|
||||
|
||||
return {
|
||||
subscribeJsMessage,
|
||||
unsubscribeJsMessage,
|
||||
handleJsMessage,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -262,7 +262,6 @@ function generateRustLicenses(): LicenseInfo[] | undefined {
|
||||
const { stdout, stderr, status } = spawnSync("cargo", ["about", "generate", "about.hbs"], {
|
||||
cwd: path.join(__dirname, ".."),
|
||||
encoding: "utf8",
|
||||
timeout: 60000, // One minute
|
||||
shell: true,
|
||||
windowsHide: true, // Hide the terminal on Windows
|
||||
});
|
||||
|
||||
@@ -231,11 +231,7 @@ impl EditorHandle {
|
||||
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 {
|
||||
error!(
|
||||
"While handling FrontendMessage \"{:?}\", JavaScript threw an error: {:?}",
|
||||
message.to_discriminant().local_name(),
|
||||
error,
|
||||
)
|
||||
error!("While handling FrontendMessage {:?}, JavaScript threw an error:\n{:?}", message.to_discriminant().local_name(), error,)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user