resolved conflicts

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

View file

@ -63,14 +63,14 @@ jobs:
mkdir artifacts
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

1
.gitignore vendored
View file

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

View file

@ -40,10 +40,10 @@
};
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

View file

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

60
Cargo.lock generated
View file

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

View file

@ -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,8 +42,13 @@ 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"] }
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" }
@ -82,7 +85,6 @@ thiserror = "2"
anyhow = "1.0"
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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

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

View file

@ -7,8 +7,12 @@ use crate::dialogs::dialog_save_graphite_file;
use crate::render::GraphicsState;
use crate::render::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;
@ -57,8 +61,8 @@ impl WinitApp {
}
fn send_messages_to_editor(&mut self, mut responses: Vec<FrontendMessage>) {
for message in responses.extract_if(.., |m| matches!(m, FrontendMessage::RenderOverlays(_))) {
let FrontendMessage::RenderOverlays(overlay_context) = message else { unreachable!() };
for message in responses.extract_if(.., |m| matches!(m, FrontendMessage::RenderOverlays { .. })) {
let FrontendMessage::RenderOverlays { context: overlay_context } = message else { unreachable!() };
if let Some(graphics_state) = &mut self.graphics_state {
let scene = overlay_context.take_scene();
graphics_state.set_overlays_scene(scene);
@ -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();

View file

@ -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));
if (ui_srgb.a < 0.001) {
return composite_linear;
}
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)
);
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);
}
}

View file

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

View file

@ -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 {
@ -53,7 +52,7 @@ const SIDE_EFFECT_FREE_MESSAGES: &[MessageDiscriminant] = &[
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::TriggerFontLoad),
];
const DEBUG_MESSAGE_BLOCK_LIST: &[MessageDiscriminant] = &[
MessageDiscriminant::Broadcast(BroadcastMessageDiscriminant::TriggerEvent(BroadcastEventDiscriminant::AnimationFrame)),
MessageDiscriminant::Broadcast(BroadcastMessageDiscriminant::TriggerEvent(EventMessageDiscriminant::AnimationFrame)),
MessageDiscriminant::Animation(AnimationMessageDiscriminant::IncrementFrameCounter),
];
// TODO: Find a way to combine these with the list above. We use strings for now since these are the standard variant names used by multiple messages. But having these also type-checked would be best.
@ -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));

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -24,10 +24,10 @@ impl MessageHandler<DeferMessage, DeferMessageContext<'_>> for DeferMessageHandl
DeferMessage::AfterNavigationReady { messages } => {
self.after_viewport_resize.extend_from_slice(&messages);
}
DeferMessage::SetGraphSubmissionIndex(execution_id) => {
DeferMessage::SetGraphSubmissionIndex { execution_id } => {
self.current_graph_submission_id = execution_id + 1;
}
DeferMessage::TriggerGraphRun(execution_id, document_id) => {
DeferMessage::TriggerGraphRun { execution_id, document_id } => {
let after_graph_run = self.after_graph_run.entry(document_id).or_default();
if after_graph_run.is_empty() {
return;
@ -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 });
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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>,
@ -330,9 +335,9 @@ pub enum FrontendMessage {
active: bool,
},
#[cfg(not(target_family = "wasm"))]
RenderOverlays(
RenderOverlays {
#[serde(skip, default = "OverlayContext::default")]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
OverlayContext,
),
context: OverlayContext,
},
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -42,9 +42,9 @@ pub enum LayoutTarget {
/// Bar at the top of the node graph containing the location and the "Preview" and "Hide" buttons.
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(_)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,6 +2,7 @@ use std::path::PathBuf;
use super::utility_types::misc::{GroupFolderType, SnappingState};
use 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 {
@ -50,7 +53,9 @@ pub enum DocumentMessage {
DocumentHistoryBackward,
DocumentHistoryForward,
DocumentStructureChanged,
DrawArtboardOverlays(OverlayContext),
DrawArtboardOverlays {
context: OverlayContext,
},
DuplicateSelectedLayers,
EnterNestedNetwork {
node_id: NodeId,
@ -69,9 +74,15 @@ pub enum DocumentMessage {
open: bool,
},
GraphViewOverlayToggle,
GridOptions(GridSnapping),
GridOverlays(OverlayContext),
GridVisibility(bool),
GridOptions {
options: GridSnapping,
},
GridOverlays {
context: OverlayContext,
},
GridVisibility {
visible: bool,
},
GroupSelectedLayers {
group_folder_type: GroupFolderType,
},

View file

@ -9,6 +9,7 @@ use crate::application::{GRAPHITE_GIT_COMMIT_HASH, generate_uuid};
use crate::consts::{ASYMPTOTIC_EFFECT, COLOR_OVERLAY_GRAY, DEFAULT_DOCUMENT_NAME, FILE_SAVE_SUFFIX, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ROTATE_SNAP_INTERVAL};
use crate::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,14 +381,17 @@ 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.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) => {
}
DocumentMessage::DrawArtboardOverlays { context: overlay_context } => {
if !overlay_context.visibility_settings.artboard_name() {
return;
}
@ -554,24 +582,25 @@ 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 => {
responses.add(DocumentMessage::GraphViewOverlay { open: !self.graph_view_overlay_open });
}
DocumentMessage::GridOptions(grid) => {
self.snapping_state.grid = grid;
DocumentMessage::GridOptions { options } => {
self.snapping_state.grid = options;
self.snapping_state.grid_snapping = true;
responses.add(OverlaysMessage::Draw);
responses.add(PortfolioMessage::UpdateDocumentWidgets);
}
DocumentMessage::GridOverlays(mut overlay_context) => {
DocumentMessage::GridOverlays { context: mut overlay_context } => {
if self.snapping_state.grid_snapping {
grid_overlay(self, &mut overlay_context)
}
}
DocumentMessage::GridVisibility(enabled) => {
self.snapping_state.grid_snapping = enabled;
DocumentMessage::GridVisibility { visible } => {
self.snapping_state.grid_snapping = visible;
responses.add(OverlaysMessage::Draw);
}
DocumentMessage::GroupSelectedLayers { group_folder_type } => {
@ -1033,7 +1062,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
if !parent_layers.is_empty() {
let nodes = parent_layers.into_iter().collect();
responses.add(NodeGraphMessage::SelectedNodesSet { nodes });
responses.add(BroadcastEvent::SelectionChanged);
responses.add(EventMessage::SelectionChanged);
}
}
DocumentMessage::SelectAllLayers => {
@ -1108,7 +1137,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
} else {
responses.add_front(NodeGraphMessage::SelectedNodesAdd { nodes: vec![id] });
}
responses.add(BroadcastEvent::SelectionChanged);
responses.add(EventMessage::SelectionChanged);
} else {
nodes.push(id);
}
@ -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 {
@ -1178,7 +1206,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
Some(overlays_type) => overlays_type,
None => {
visibility_settings.all = visible;
responses.add(BroadcastEvent::ToolAbort);
responses.add(EventMessage::ToolAbort);
responses.add(OverlaysMessage::Draw);
return;
}
@ -1201,7 +1229,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
OverlaysType::Handles => visibility_settings.handles = visible,
}
responses.add(BroadcastEvent::ToolAbort);
responses.add(EventMessage::ToolAbort);
responses.add(OverlaysMessage::Draw);
}
DocumentMessage::SetRangeSelectionLayer { new_layer } => {
@ -1415,12 +1443,14 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
let transform = self.navigation_handler.calculate_offset_transform(ipp.viewport_bounds.center(), &self.document_ptz);
self.network_interface.set_document_to_viewport_transform(transform);
// Ensure selection box is kept in sync with the pointer when the PTZ changes
responses.add(SelectToolMessage::PointerMove(SelectToolPointerKeys {
responses.add(SelectToolMessage::PointerMove {
modifier_keys: SelectToolPointerKeys {
axis_align: Key::Shift,
snap_angle: Key::Shift,
center: Key::Alt,
duplicate: Key::Alt,
}));
},
});
responses.add(NodeGraphMessage::RunDocumentGraph);
} else {
let Some(network_metadata) = self.network_interface.network_metadata(&self.breadcrumb_network_path) else {
@ -1449,11 +1479,11 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
}
DocumentMessage::SelectionStepBack => {
self.network_interface.selection_step_back(&self.selection_network_path);
responses.add(BroadcastEvent::SelectionChanged);
responses.add(EventMessage::SelectionChanged);
}
DocumentMessage::SelectionStepForward => {
self.network_interface.selection_step_forward(&self.selection_network_path);
responses.add(BroadcastEvent::SelectionChanged);
responses.add(EventMessage::SelectionChanged);
}
DocumentMessage::WrapContentInArtboard { place_artboard_at_origin } => {
// Get bounding box of all layers
@ -2456,7 +2486,7 @@ impl DocumentMessageHandler {
.icon("Grid")
.tooltip("Grid")
.tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::ToggleGridVisibility))
.on_update(|optional_input: &CheckboxInput| DocumentMessage::GridVisibility(optional_input.checked).into())
.on_update(|optional_input: &CheckboxInput| DocumentMessage::GridVisibility { visible: optional_input.checked }.into())
.widget_holder(),
PopoverButton::new()
.popover_layout(overlay_options(&self.snapping_state.grid))
@ -2549,7 +2579,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 +2741,11 @@ impl DocumentMessageHandler {
});
}
pub fn update_layers_panel_bottom_bar_widgets(&self, responses: &mut VecDeque<Message>) {
pub fn update_layers_panel_bottom_bar_widgets(&self, layers_panel_open: bool, responses: &mut VecDeque<Message>) {
if !layers_panel_open {
return;
}
let selected_nodes = self.network_interface.selected_nodes();
let mut selected_layers = selected_nodes.selected_layers(self.metadata());
let selected_layer = selected_layers.next();
@ -2949,10 +2987,10 @@ fn quad_to_path_lib_segments(quad: Quad) -> Vec<path_bool_lib::PathSegment> {
}
fn click_targets_to_path_lib_segments<'a>(click_targets: impl Iterator<Item = &'a ClickTarget>, transform: DAffine2) -> Vec<path_bool_lib::PathSegment> {
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 +3001,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 +3024,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 +3077,10 @@ impl<'a> ClickXRayIter<'a> {
XRayTarget::Quad(quad) => self.check_layer_area_target(click_targets, clip, layer, quad_to_path_lib_segments(*quad), transform),
XRayTarget::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)
}
}

View file

@ -2,13 +2,13 @@ use super::utility_types::TransformIn;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::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;

View file

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

View file

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

View file

@ -3,7 +3,6 @@ use crate::messages::portfolio::document::node_graph::document_node_definitions:
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::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);

View file

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

View file

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

View file

@ -399,7 +399,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
NodeInput::value(TaggedValue::Graphic(Default::default()), true),
NodeInput::value(TaggedValue::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()

View file

@ -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 {
@ -127,7 +129,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
NodeGraphMessage::AddPathNode => {
if let Some(layer) = make_path_editable_is_allowed(network_interface, network_interface.document_metadata()) {
responses.add(NodeGraphMessage::CreateNodeInLayerWithTransaction { node_type: "Path".to_string(), layer });
responses.add(BroadcastEvent::SelectionChanged);
responses.add(EventMessage::SelectionChanged);
}
}
NodeGraphMessage::AddImport => {
@ -140,7 +142,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
}
NodeGraphMessage::Init => {
responses.add(BroadcastMessage::SubscribeEvent {
on: BroadcastEvent::SelectionChanged,
on: EventMessage::SelectionChanged,
send: Box::new(NodeGraphMessage::SelectedNodesUpdated.into()),
});
network_interface.load_structure();
@ -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);
@ -1468,7 +1472,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
return;
};
selected_nodes.add_selected_nodes(nodes);
responses.add(BroadcastEvent::SelectionChanged);
responses.add(EventMessage::SelectionChanged);
}
NodeGraphMessage::SelectedNodesRemove { nodes } => {
let Some(selected_nodes) = network_interface.selected_nodes_mut(selection_network_path) else {
@ -1476,7 +1480,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
return;
};
selected_nodes.retain_selected_nodes(|node| !nodes.contains(node));
responses.add(BroadcastEvent::SelectionChanged);
responses.add(EventMessage::SelectionChanged);
}
NodeGraphMessage::SelectedNodesSet { nodes } => {
let Some(selected_nodes) = network_interface.selected_nodes_mut(selection_network_path) else {
@ -1484,8 +1488,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
return;
};
selected_nodes.set_selected_nodes(nodes);
responses.add(BroadcastEvent::SelectionChanged);
responses.add(PropertiesPanelMessage::Refresh);
responses.add(EventMessage::SelectionChanged);
}
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
@ -1884,7 +1888,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
return;
};
selected_nodes.clear_selected_nodes();
responses.add(BroadcastEvent::SelectionChanged);
responses.add(EventMessage::SelectionChanged);
responses.add(NodeGraphMessage::SendGraph);
}
@ -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();

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,8 +3,8 @@ use crate::consts::HIDE_HANDLE_DISTANCE;
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
use crate::messages::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;
};

View file

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

View file

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

View file

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

View file

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

View file

@ -8,13 +8,13 @@ use crate::messages::portfolio::document::node_graph::utility_types::{Direction,
use crate::messages::portfolio::document::utility_types::wires::{GraphWireStyle, WirePath, WirePathUpdate, build_vector_wire};
use crate::messages::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 {

View file

@ -5,11 +5,11 @@ use crate::messages::portfolio::document::node_graph::document_node_definitions:
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::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);
}

View file

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

View file

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

View file

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

View file

@ -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)
}
@ -195,12 +204,13 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
}
PortfolioMessage::CloseAllDocuments => {
if self.active_document_id.is_some() {
responses.add(BroadcastEvent::ToolAbort);
responses.add(EventMessage::ToolAbort);
responses.add(ToolMessage::DeactivateTools);
// 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 });
}
@ -239,7 +250,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
PortfolioMessage::CloseDocumentWithConfirmation { document_id } => {
let target_document = self.documents.get(&document_id).unwrap();
if target_document.is_saved() {
responses.add(BroadcastEvent::ToolAbort);
responses.add(EventMessage::ToolAbort);
responses.add(PortfolioMessage::CloseDocument { document_id });
} else {
let dialog = simple_dialogs::CloseDocumentDialog {
@ -344,13 +355,13 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
self.persistent_data.font_cache.insert(font, preview_url, data);
self.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);
@ -384,11 +395,11 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
let document_id = DocumentId(generate_uuid());
if self.active_document().is_some() {
new_responses.add(BroadcastEvent::ToolAbort);
new_responses.add(EventMessage::ToolAbort);
new_responses.add(NavigationMessage::CanvasPan { delta: (0., 0.).into() });
}
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()),
});
}
@ -863,8 +874,8 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
responses.add(ToolMessage::InitTools);
responses.add(NodeGraphMessage::Init);
responses.add(OverlaysMessage::Draw);
responses.add(BroadcastEvent::ToolAbort);
responses.add(BroadcastEvent::SelectionChanged);
responses.add(EventMessage::ToolAbort);
responses.add(EventMessage::SelectionChanged);
responses.add(NavigationMessage::CanvasPan { delta: (0., 0.).into() });
responses.add(NodeGraphMessage::RunDocumentGraph);
responses.add(DocumentMessage::GraphViewOverlay { open: node_graph_open });
@ -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,19 +1120,19 @@ impl PortfolioMessageHandler {
}
}
fn load_document(&mut self, new_document: DocumentMessageHandler, document_id: DocumentId, responses: &mut VecDeque<Message>, to_front: bool) {
fn load_document(&mut self, new_document: DocumentMessageHandler, document_id: DocumentId, layers_panel_open: bool, responses: &mut VecDeque<Message>, to_front: bool) {
if to_front {
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);
if self.active_document().is_some() {
responses.add(BroadcastEvent::ToolAbort);
responses.add(EventMessage::ToolAbort);
responses.add(ToolMessage::DeactivateTools);
} else {
// Load the default font upon creating the first 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;
}

View file

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

View file

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

View file

@ -43,7 +43,7 @@ pub enum PanelType {
Document,
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}"),
}
}

View file

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

View file

@ -1,9 +1,11 @@
// Root
pub use crate::utility_traits::{ActionList, AsMessage, HierarchicalTree, MessageHandler, ToDiscriminant, TransitiveChild};
// Message-related
pub use crate::utility_traits::{ActionList, AsMessage, ExtractField, HierarchicalTree, MessageHandler, ToDiscriminant, TransitiveChild};
pub use crate::utility_types::{DebugMessageTree, MessageData};
// Message, MessageData, MessageDiscriminant, MessageHandler
pub use crate::messages::animation::{AnimationMessage, AnimationMessageDiscriminant, AnimationMessageHandler};
pub use crate::messages::app_window::{AppWindowMessage, AppWindowMessageDiscriminant, AppWindowMessageHandler};
pub use crate::messages::broadcast::event::{EventMessage, EventMessageContext, EventMessageDiscriminant, EventMessageHandler};
pub use crate::messages::broadcast::{BroadcastMessage, BroadcastMessageDiscriminant, BroadcastMessageHandler};
pub use crate::messages::debug::{DebugMessage, DebugMessageDiscriminant, DebugMessageHandler};
pub use crate::messages::defer::{DeferMessage, DeferMessageDiscriminant, DeferMessageHandler};
@ -17,6 +19,7 @@ pub use crate::messages::input_mapper::key_mapping::{KeyMappingMessage, KeyMappi
pub use crate::messages::input_mapper::{InputMapperMessage, InputMapperMessageContext, InputMapperMessageDiscriminant, InputMapperMessageHandler};
pub use crate::messages::input_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,15 +27,12 @@ pub use crate::messages::portfolio::document::overlays::{OverlaysMessage, Overla
pub use crate::messages::portfolio::document::properties_panel::{PropertiesPanelMessage, PropertiesPanelMessageDiscriminant, PropertiesPanelMessageHandler};
pub use crate::messages::portfolio::document::{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};
pub use crate::messages::message::{Message, MessageDiscriminant};
pub use crate::messages::tool::tool_messages::artboard_tool::{ArtboardToolMessage, ArtboardToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::brush_tool::{BrushToolMessage, BrushToolMessageDiscriminant};
@ -48,7 +48,7 @@ pub use crate::messages::tool::tool_messages::shape_tool::{ShapeToolMessage, Sha
pub use crate::messages::tool::tool_messages::spline_tool::{SplineToolMessage, SplineToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::text_tool::{TextToolMessage, TextToolMessageDiscriminant};
// Helper
// Helper/miscellaneous
pub use crate::messages::globals::global_variables::*;
pub use crate::messages::portfolio::document::utility_types::misc::DocumentId;
pub use graphite_proc_macros::*;

View file

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

View file

@ -3,7 +3,6 @@ use crate::messages::portfolio::document::node_graph::document_node_definitions;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::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())

View file

@ -43,18 +43,19 @@ fn draw_line_with_length(line_start: DVec2, line_end: DVec2, transform: DAffine2
}
}
/// Draws a dashed outline around a rectangle to visualize the AABB
/// Draws a dashed outline around the given rectangle (assumed to be in document space).
/// The provided transform is applied to convert coordinates (e.g., to viewport space) during rendering.
fn draw_dashed_rect_outline(rect: Rect, transform: DAffine2, overlay_context: &mut OverlayContext) {
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);

View file

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

View file

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

View file

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

View file

@ -12,10 +12,10 @@ use crate::messages::tool::common_functionality::shape_editor::ShapeState;
use crate::messages::tool::common_functionality::transformation_cage::BoundingBoxManager;
use crate::messages::tool::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, {GridType, dvec2_to_point},
@ -368,9 +368,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);

View file

@ -10,14 +10,16 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye
use crate::messages::portfolio::document::utility_types::misc::{GridSnapTarget, PathSnapTarget, SnapTarget};
use crate::messages::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);

View file

@ -3,11 +3,17 @@ use crate::consts::HIDE_HANDLE_DISTANCE;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::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));

View file

@ -2,11 +2,11 @@ use super::DistributionMatch;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::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,
}

View file

@ -9,16 +9,17 @@ use crate::messages::tool::common_functionality::graph_modification_utils::{Node
use crate::messages::tool::common_functionality::transformation_cage::SelectedEdges;
use crate::messages::tool::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) };

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -21,14 +21,16 @@ use crate::messages::tool::common_functionality::shape_editor::{
};
use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager};
use crate::messages::tool::common_functionality::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)]
@ -49,8 +51,10 @@ pub struct PathToolOptions {
pub enum PathToolMessage {
// Standard messages
Abort,
Overlays(OverlayContext),
SelectionChanged,
Overlays {
context: OverlayContext,
},
// Tool-specific messages
BreakPath,
@ -121,10 +125,13 @@ pub enum PathToolMessage {
position: ReferencePoint,
},
SwapSelectedHandles,
UpdateOptions(PathOptionsUpdate),
UpdateOptions {
options: PathOptionsUpdate,
},
UpdateSelectedPointsStatus {
overlay_context: OverlayContext,
},
StartSlidingPoint,
Copy {
clipboard: Clipboard,
},
@ -272,15 +279,30 @@ impl LayoutHolder for PathTool {
RadioEntryData::new("all")
.icon("HandleVisibilityAll")
.tooltip("Show all handles regardless of selection")
.on_update(move |_| PathToolMessage::UpdateOptions(PathOptionsUpdate::OverlayModeType(PathOverlayMode::AllHandles)).into()),
.on_update(move |_| {
PathToolMessage::UpdateOptions {
options: PathOptionsUpdate::OverlayModeType(PathOverlayMode::AllHandles),
}
.into()
}),
RadioEntryData::new("selected")
.icon("HandleVisibilitySelected")
.tooltip("Show only handles of the segments connected to selected points")
.on_update(move |_| PathToolMessage::UpdateOptions(PathOptionsUpdate::OverlayModeType(PathOverlayMode::SelectedPointHandles)).into()),
.on_update(move |_| {
PathToolMessage::UpdateOptions {
options: PathOptionsUpdate::OverlayModeType(PathOverlayMode::SelectedPointHandles),
}
.into()
}),
RadioEntryData::new("frontier")
.icon("HandleVisibilityFrontier")
.tooltip("Show only handles at the frontiers of the segments connected to selected points")
.on_update(move |_| PathToolMessage::UpdateOptions(PathOptionsUpdate::OverlayModeType(PathOverlayMode::FrontierHandles)).into()),
.on_update(move |_| {
PathToolMessage::UpdateOptions {
options: PathOptionsUpdate::OverlayModeType(PathOverlayMode::FrontierHandles),
}
.into()
}),
])
.selected_index(Some(self.options.path_overlay_mode as u32))
.widget_holder();
@ -342,7 +364,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Path
let updating_point = message == ToolMessage::Path(PathToolMessage::SelectedPointUpdated);
match message {
ToolMessage::Path(PathToolMessage::UpdateOptions(action)) => match action {
ToolMessage::Path(PathToolMessage::UpdateOptions { options }) => match options {
PathOptionsUpdate::OverlayModeType(overlay_mode_type) => {
self.options.path_overlay_mode = overlay_mode_type;
responses.add(OverlaysMessage::Draw);
@ -418,6 +440,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Path
DeleteAndBreakPath,
ClosePath,
PointerMove,
StartSlidingPoint,
Copy,
Cut,
DeleteSelected,
@ -436,6 +459,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Path
BreakPath,
DeleteAndBreakPath,
SwapSelectedHandles,
StartSlidingPoint,
Copy,
Cut,
DeleteSelected,
@ -454,6 +478,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Path
DeleteAndBreakPath,
Escape,
RightClick,
StartSlidingPoint,
TogglePointEditing,
ToggleSegmentEditing
),
@ -472,7 +497,7 @@ impl ToolTransition for PathTool {
EventToMessageMap {
tool_abort: Some(PathToolMessage::Abort.into()),
selection_changed: Some(PathToolMessage::SelectionChanged.into()),
overlay_provider: Some(|overlay_context| PathToolMessage::Overlays(overlay_context).into()),
overlay_provider: Some(|context| PathToolMessage::Overlays { context }.into()),
..Default::default()
}
}
@ -494,7 +519,7 @@ pub enum PointSelectState {
#[derive(Clone, Copy)]
pub struct SlidingSegmentData {
segment_id: SegmentId,
bezier: Bezier,
bezier: PathSeg,
start: PointId,
}
@ -840,13 +865,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)
@ -1207,15 +1231,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 }),
@ -1255,13 +1274,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)
@ -1276,50 +1295,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.)
}
});
@ -1328,7 +1356,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);
@ -1548,14 +1576,22 @@ impl Fsm for PathToolFsmState {
match (multiple_toggle, point_edit) {
(true, true) => {
responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::PointEditingMode { enabled: false }));
responses.add(PathToolMessage::UpdateOptions {
options: PathOptionsUpdate::PointEditingMode { enabled: false },
});
}
(true, false) => {
responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::PointEditingMode { enabled: true }));
responses.add(PathToolMessage::UpdateOptions {
options: PathOptionsUpdate::PointEditingMode { enabled: true },
});
}
(_, _) => {
responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::PointEditingMode { enabled: true }));
responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::SegmentEditingMode { enabled: false }));
responses.add(PathToolMessage::UpdateOptions {
options: PathOptionsUpdate::PointEditingMode { enabled: true },
});
responses.add(PathToolMessage::UpdateOptions {
options: PathOptionsUpdate::SegmentEditingMode { enabled: false },
});
// Select all of the end points of selected segments
let selected_layers = shape_editor.selected_layers().cloned().collect::<Vec<_>>();
@ -1593,14 +1629,22 @@ impl Fsm for PathToolFsmState {
match (multiple_toggle, segment_edit) {
(true, true) => {
responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::SegmentEditingMode { enabled: false }));
responses.add(PathToolMessage::UpdateOptions {
options: PathOptionsUpdate::SegmentEditingMode { enabled: false },
});
}
(true, false) => {
responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::SegmentEditingMode { enabled: true }));
responses.add(PathToolMessage::UpdateOptions {
options: PathOptionsUpdate::SegmentEditingMode { enabled: true },
});
}
(_, _) => {
responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::PointEditingMode { enabled: false }));
responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::SegmentEditingMode { enabled: true }));
responses.add(PathToolMessage::UpdateOptions {
options: PathOptionsUpdate::PointEditingMode { enabled: false },
});
responses.add(PathToolMessage::UpdateOptions {
options: PathOptionsUpdate::SegmentEditingMode { enabled: true },
});
// Select all the segments which have both of the ends selected
let selected_layers = shape_editor.selected_layers().cloned().collect::<Vec<_>>();
@ -1623,7 +1667,7 @@ impl Fsm for PathToolFsmState {
self
}
(_, PathToolMessage::Overlays(mut overlay_context)) => {
(_, PathToolMessage::Overlays { context: mut overlay_context }) => {
// Set this to show ghost line only if drag actually happened
if matches!(self, Self::Dragging(_)) && tool_data.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD {
for (outline, layer) in &tool_data.ghost_outline {
@ -1737,13 +1781,13 @@ impl Fsm for PathToolFsmState {
if tool_options.path_editing_mode.segment_editing_mode && !tool_data.segment_editing_modifier {
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()) {
@ -1820,7 +1864,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).abs();
shape_editor.get_inside_points_and_segments(
&document.network_interface,
SelectionShape::Box(bbox),
@ -1865,7 +1909,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);
}
@ -2073,10 +2117,6 @@ impl Fsm for PathToolFsmState {
}
if !tool_data.update_colinear(equidistant_state, toggle_colinear_state, tool_action_data.shape_editor, tool_action_data.document, responses) {
if 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,
@ -2241,7 +2281,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).abs();
shape_editor.select_all_in_shape(
&document.network_interface,
SelectionShape::Box(bbox),
@ -2337,7 +2378,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).abs();
shape_editor.select_all_in_shape(
&document.network_interface,
SelectionShape::Box(bbox),
@ -2370,6 +2412,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,
@ -2390,6 +2434,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
@ -2398,11 +2443,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;
}
}
@ -2410,6 +2451,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;
@ -2566,6 +2612,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
@ -2697,16 +2751,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 };
@ -2802,7 +2853,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);
@ -2811,11 +2862,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 };
@ -3291,7 +3339,7 @@ fn update_dynamic_hints(
}
}
let mut drag_selected_hints = vec![HintInfo::mouse(MouseMotion::LmbDrag, "Drag Selected")];
let drag_selected_hints = vec![HintInfo::mouse(MouseMotion::LmbDrag, "Drag Selected")];
let mut delete_selected_hints = vec![HintInfo::keys([Key::Delete], "Delete Selected")];
if at_least_one_anchor_selected {
@ -3299,10 +3347,6 @@ fn update_dynamic_hints(
delete_selected_hints.push(HintInfo::keys([Key::Shift], "Cut Anchor").prepend_plus());
}
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;
@ -3367,9 +3411,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);
}

View file

@ -11,11 +11,12 @@ use crate::messages::tool::common_functionality::graph_modification_utils::{self
use crate::messages::tool::common_functionality::shape_editor::ShapeState;
use crate::messages::tool::common_functionality::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 {
@ -49,7 +50,9 @@ pub enum PenToolMessage {
Abort,
SelectionChanged,
WorkingColorChanged,
Overlays(OverlayContext),
Overlays {
context: OverlayContext,
},
// Tool-specific messages
@ -79,7 +82,9 @@ pub enum PenToolMessage {
},
Redo,
Undo,
UpdateOptions(PenOptionsUpdate),
UpdateOptions {
options: PenOptionsUpdate,
},
RecalculateLatestPointsPosition,
RemovePreviousHandle,
GRS {
@ -137,7 +142,12 @@ fn create_weight_widget(line_weight: f64) -> WidgetHolder {
.label("Weight")
.min(0.)
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
.on_update(|number_input: &NumberInput| PenToolMessage::UpdateOptions(PenOptionsUpdate::LineWeight(number_input.value.unwrap())).into())
.on_update(|number_input: &NumberInput| {
PenToolMessage::UpdateOptions {
options: PenOptionsUpdate::LineWeight(number_input.value.unwrap()),
}
.into()
})
.widget_holder()
}
@ -146,9 +156,26 @@ impl LayoutHolder for PenTool {
let mut widgets = self.options.fill.create_widgets(
"Fill",
true,
|_| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColor(None)).into(),
|color_type: ToolColorType| WidgetCallback::new(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColorType(color_type.clone())).into()),
|color: &ColorInput| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(),
|_| {
PenToolMessage::UpdateOptions {
options: PenOptionsUpdate::FillColor(None),
}
.into()
},
|color_type: ToolColorType| {
WidgetCallback::new(move |_| {
PenToolMessage::UpdateOptions {
options: PenOptionsUpdate::FillColorType(color_type.clone()),
}
.into()
})
},
|color: &ColorInput| {
PenToolMessage::UpdateOptions {
options: PenOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb())),
}
.into()
},
);
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
@ -156,9 +183,26 @@ impl LayoutHolder for PenTool {
widgets.append(&mut self.options.stroke.create_widgets(
"Stroke",
true,
|_| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColor(None)).into(),
|color_type: ToolColorType| WidgetCallback::new(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|color: &ColorInput| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(),
|_| {
PenToolMessage::UpdateOptions {
options: PenOptionsUpdate::StrokeColor(None),
}
.into()
},
|color_type: ToolColorType| {
WidgetCallback::new(move |_| {
PenToolMessage::UpdateOptions {
options: PenOptionsUpdate::StrokeColorType(color_type.clone()),
}
.into()
})
},
|color: &ColorInput| {
PenToolMessage::UpdateOptions {
options: PenOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb())),
}
.into()
},
));
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
@ -172,11 +216,21 @@ impl LayoutHolder for PenTool {
RadioEntryData::new("all")
.icon("HandleVisibilityAll")
.tooltip("Show all handles regardless of selection")
.on_update(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::OverlayModeType(PenOverlayMode::AllHandles)).into()),
.on_update(move |_| {
PenToolMessage::UpdateOptions {
options: PenOptionsUpdate::OverlayModeType(PenOverlayMode::AllHandles),
}
.into()
}),
RadioEntryData::new("frontier")
.icon("HandleVisibilityFrontier")
.tooltip("Show only handles at the frontiers of the segments connected to selected points")
.on_update(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::OverlayModeType(PenOverlayMode::FrontierHandles)).into()),
.on_update(move |_| {
PenToolMessage::UpdateOptions {
options: PenOptionsUpdate::OverlayModeType(PenOverlayMode::FrontierHandles),
}
.into()
}),
])
.selected_index(Some(self.options.pen_overlay_mode as u32))
.widget_holder(),
@ -189,12 +243,12 @@ impl LayoutHolder for PenTool {
#[message_handler_data]
impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for PenTool {
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) {
let ToolMessage::Pen(PenToolMessage::UpdateOptions(action)) = message else {
let ToolMessage::Pen(PenToolMessage::UpdateOptions { options }) = message else {
self.fsm_state.process_event(message, &mut self.tool_data, context, &self.options, responses, true);
return;
};
match action {
match options {
PenOptionsUpdate::OverlayModeType(overlay_mode_type) => {
self.options.pen_overlay_mode = overlay_mode_type;
responses.add(OverlaysMessage::Draw);
@ -252,7 +306,7 @@ impl ToolTransition for PenTool {
tool_abort: Some(PenToolMessage::Abort.into()),
selection_changed: Some(PenToolMessage::SelectionChanged.into()),
working_color_changed: Some(PenToolMessage::WorkingColorChanged.into()),
overlay_provider: Some(|overlay_context| PenToolMessage::Overlays(overlay_context).into()),
overlay_provider: Some(|context| PenToolMessage::Overlays { context }.into()),
..Default::default()
}
}
@ -1257,7 +1311,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 +1319,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))
@ -1550,7 +1600,7 @@ impl Fsm for PenToolFsmState {
responses.add(OverlaysMessage::Draw);
self
}
(PenToolFsmState::Ready, PenToolMessage::Overlays(mut overlay_context)) => {
(PenToolFsmState::Ready, PenToolMessage::Overlays { context: mut overlay_context }) => {
match tool_options.pen_overlay_mode {
PenOverlayMode::AllHandles => {
path_overlays(document, DrawHandles::All, shape_editor, &mut overlay_context);
@ -1578,7 +1628,7 @@ impl Fsm for PenToolFsmState {
tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
self
}
(_, PenToolMessage::Overlays(mut overlay_context)) => {
(_, PenToolMessage::Overlays { context: mut overlay_context }) => {
let display_anchors = overlay_context.visibility_settings.anchors();
let display_handles = overlay_context.visibility_settings.handles();
@ -1599,9 +1649,8 @@ impl Fsm for PenToolFsmState {
let handle_start = tool_data.latest_point().map(|point| transform.transform_point2(point.handle_start));
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 +1772,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));
@ -1757,10 +1806,9 @@ impl Fsm for PenToolFsmState {
self
}
(_, PenToolMessage::WorkingColorChanged) => {
responses.add(PenToolMessage::UpdateOptions(PenOptionsUpdate::WorkingColors(
Some(global_tool_data.primary_color),
Some(global_tool_data.secondary_color),
)));
responses.add(PenToolMessage::UpdateOptions {
options: PenOptionsUpdate::WorkingColors(Some(global_tool_data.primary_color), Some(global_tool_data.secondary_color)),
});
self
}
(PenToolFsmState::Ready, PenToolMessage::DragStart { append_to_selected }) => {

View file

@ -20,12 +20,12 @@ use crate::messages::tool::common_functionality::shape_editor::SelectionShapeTyp
use crate::messages::tool::common_functionality::snapping::{self, SnapCandidatePoint, SnapData, SnapManager};
use crate::messages::tool::common_functionality::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;
@ -78,7 +78,9 @@ pub struct SelectToolPointerKeys {
pub enum SelectToolMessage {
// Standard messages
Abort,
Overlays(OverlayContext),
Overlays {
context: OverlayContext,
},
// Tool-specific messages
DragStart {
@ -92,10 +94,17 @@ pub enum SelectToolMessage {
remove_from_selection: Key,
},
EditLayer,
EditLayerExec,
Enter,
PointerMove(SelectToolPointerKeys),
PointerOutsideViewport(SelectToolPointerKeys),
SelectOptions(SelectOptionsUpdate),
PointerMove {
modifier_keys: SelectToolPointerKeys,
},
PointerOutsideViewport {
modifier_keys: SelectToolPointerKeys,
},
SelectOptions {
options: SelectOptionsUpdate,
},
SetPivot {
position: ReferencePoint,
},
@ -126,9 +135,12 @@ impl SelectTool {
let layer_selection_behavior_entries = [NestedSelectionBehavior::Shallowest, NestedSelectionBehavior::Deepest]
.iter()
.map(|mode| {
MenuListEntry::new(format!("{mode:?}"))
.label(mode.to_string())
.on_commit(move |_| SelectToolMessage::SelectOptions(SelectOptionsUpdate::NestedSelectionBehavior(*mode)).into())
MenuListEntry::new(format!("{mode:?}")).label(mode.to_string()).on_commit(move |_| {
SelectToolMessage::SelectOptions {
options: SelectOptionsUpdate::NestedSelectionBehavior(*mode),
}
.into()
})
})
.collect();
@ -277,7 +289,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Sele
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) {
let mut redraw_reference_pivot = false;
if let ToolMessage::Select(SelectToolMessage::SelectOptions(ref option_update)) = message {
if let ToolMessage::Select(SelectToolMessage::SelectOptions { options: ref option_update }) = message {
match option_update {
SelectOptionsUpdate::NestedSelectionBehavior(nested_selection_behavior) => {
self.tool_data.nested_selection_behavior = *nested_selection_behavior;
@ -323,6 +335,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Sele
PointerMove,
Abort,
EditLayer,
EditLayerExec,
Enter,
);
@ -340,7 +353,7 @@ impl ToolTransition for SelectTool {
fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap {
tool_abort: Some(SelectToolMessage::Abort.into()),
overlay_provider: Some(|overlay_context| SelectToolMessage::Overlays(overlay_context).into()),
overlay_provider: Some(|context| SelectToolMessage::Overlays { context }.into()),
..Default::default()
}
}
@ -589,7 +602,7 @@ impl Fsm for SelectToolFsmState {
let ToolMessage::Select(event) = event else { return self };
match (self, event) {
(_, SelectToolMessage::Overlays(mut overlay_context)) => {
(_, SelectToolMessage::Overlays { context: mut overlay_context }) => {
tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
let selected_layers_count = document.network_interface.selected_nodes().selected_unlocked_layers(&document.network_interface).count();
@ -689,32 +702,28 @@ impl Fsm for SelectToolFsmState {
// Measure with Alt held down
// 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 +994,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
}
(
@ -1139,7 +1153,7 @@ impl Fsm for SelectToolFsmState {
deepest,
remove,
},
SelectToolMessage::PointerMove(modifier_keys),
SelectToolMessage::PointerMove { modifier_keys },
) => {
if !has_dragged {
responses.add(ToolMessage::UpdateHints);
@ -1184,8 +1198,8 @@ impl Fsm for SelectToolFsmState {
// Auto-panning
let messages = [
SelectToolMessage::PointerOutsideViewport(modifier_keys.clone()).into(),
SelectToolMessage::PointerMove(modifier_keys).into(),
SelectToolMessage::PointerOutsideViewport { modifier_keys: modifier_keys.clone() }.into(),
SelectToolMessage::PointerMove { modifier_keys }.into(),
];
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses);
@ -1197,7 +1211,7 @@ impl Fsm for SelectToolFsmState {
remove,
}
}
(SelectToolFsmState::ResizingBounds, SelectToolMessage::PointerMove(modifier_keys)) => {
(SelectToolFsmState::ResizingBounds, SelectToolMessage::PointerMove { modifier_keys }) => {
if let Some(bounds) = &mut tool_data.bounding_box_manager {
resize_bounds(
document,
@ -1212,14 +1226,14 @@ impl Fsm for SelectToolFsmState {
ToolType::Select,
);
let messages = [
SelectToolMessage::PointerOutsideViewport(modifier_keys.clone()).into(),
SelectToolMessage::PointerMove(modifier_keys).into(),
SelectToolMessage::PointerOutsideViewport { modifier_keys: modifier_keys.clone() }.into(),
SelectToolMessage::PointerMove { modifier_keys }.into(),
];
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses);
}
SelectToolFsmState::ResizingBounds
}
(SelectToolFsmState::SkewingBounds { skew }, SelectToolMessage::PointerMove(_)) => {
(SelectToolFsmState::SkewingBounds { skew }, SelectToolMessage::PointerMove { .. }) => {
if let Some(bounds) = &mut tool_data.bounding_box_manager {
skew_bounds(
document,
@ -1233,7 +1247,7 @@ impl Fsm for SelectToolFsmState {
}
SelectToolFsmState::SkewingBounds { skew }
}
(SelectToolFsmState::RotatingBounds, SelectToolMessage::PointerMove(_)) => {
(SelectToolFsmState::RotatingBounds, SelectToolMessage::PointerMove { .. }) => {
if let Some(bounds) = &mut tool_data.bounding_box_manager {
rotate_bounds(
document,
@ -1249,7 +1263,7 @@ impl Fsm for SelectToolFsmState {
SelectToolFsmState::RotatingBounds
}
(SelectToolFsmState::DraggingPivot, SelectToolMessage::PointerMove(modifier_keys)) => {
(SelectToolFsmState::DraggingPivot, SelectToolMessage::PointerMove { modifier_keys }) => {
let mouse_position = input.mouse.position;
let snapped_mouse_position = mouse_position;
@ -1259,14 +1273,14 @@ impl Fsm for SelectToolFsmState {
// Auto-panning
let messages = [
SelectToolMessage::PointerOutsideViewport(modifier_keys.clone()).into(),
SelectToolMessage::PointerMove(modifier_keys).into(),
SelectToolMessage::PointerOutsideViewport { modifier_keys: modifier_keys.clone() }.into(),
SelectToolMessage::PointerMove { modifier_keys }.into(),
];
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses);
SelectToolFsmState::DraggingPivot
}
(SelectToolFsmState::Drawing { selection_shape, has_drawn }, SelectToolMessage::PointerMove(modifier_keys)) => {
(SelectToolFsmState::Drawing { selection_shape, has_drawn }, SelectToolMessage::PointerMove { modifier_keys }) => {
if !has_drawn {
responses.add(ToolMessage::UpdateHints);
}
@ -1280,14 +1294,14 @@ impl Fsm for SelectToolFsmState {
// Auto-panning
let messages = [
SelectToolMessage::PointerOutsideViewport(modifier_keys.clone()).into(),
SelectToolMessage::PointerMove(modifier_keys).into(),
SelectToolMessage::PointerOutsideViewport { modifier_keys: modifier_keys.clone() }.into(),
SelectToolMessage::PointerMove { modifier_keys }.into(),
];
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses);
SelectToolFsmState::Drawing { selection_shape, has_drawn: true }
}
(SelectToolFsmState::Ready { .. }, SelectToolMessage::PointerMove(_)) => {
(SelectToolFsmState::Ready { .. }, SelectToolMessage::PointerMove { .. }) => {
let dragging_bounds = tool_data
.bounding_box_manager
.as_mut()
@ -1323,7 +1337,7 @@ impl Fsm for SelectToolFsmState {
deepest,
remove,
},
SelectToolMessage::PointerOutsideViewport(_),
SelectToolMessage::PointerOutsideViewport { .. },
) => {
// Auto-panning
if let Some(shift) = tool_data.auto_panning.shift_viewport(input, responses) {
@ -1339,7 +1353,7 @@ impl Fsm for SelectToolFsmState {
remove,
}
}
(SelectToolFsmState::ResizingBounds | SelectToolFsmState::SkewingBounds { .. }, SelectToolMessage::PointerOutsideViewport(_)) => {
(SelectToolFsmState::ResizingBounds | SelectToolFsmState::SkewingBounds { .. }, SelectToolMessage::PointerOutsideViewport { .. }) => {
// Auto-panning
if let Some(shift) = tool_data.auto_panning.shift_viewport(input, responses) {
if let Some(bounds) = &mut tool_data.bounding_box_manager {
@ -1350,13 +1364,13 @@ impl Fsm for SelectToolFsmState {
self
}
(SelectToolFsmState::DraggingPivot, SelectToolMessage::PointerOutsideViewport(_)) => {
(SelectToolFsmState::DraggingPivot, SelectToolMessage::PointerOutsideViewport { .. }) => {
// Auto-panning
let _ = tool_data.auto_panning.shift_viewport(input, responses);
self
}
(SelectToolFsmState::Drawing { .. }, SelectToolMessage::PointerOutsideViewport(_)) => {
(SelectToolFsmState::Drawing { .. }, SelectToolMessage::PointerOutsideViewport { .. }) => {
// Auto-panning
if let Some(shift) = tool_data.auto_panning.shift_viewport(input, responses) {
tool_data.drag_start += shift;
@ -1364,11 +1378,11 @@ impl Fsm for SelectToolFsmState {
self
}
(state, SelectToolMessage::PointerOutsideViewport(modifier_keys)) => {
(state, SelectToolMessage::PointerOutsideViewport { modifier_keys }) => {
// Auto-panning
let messages = [
SelectToolMessage::PointerOutsideViewport(modifier_keys.clone()).into(),
SelectToolMessage::PointerMove(modifier_keys).into(),
SelectToolMessage::PointerOutsideViewport { modifier_keys: modifier_keys.clone() }.into(),
SelectToolMessage::PointerMove { modifier_keys }.into(),
];
tool_data.auto_panning.stop(&messages, responses);

View file

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

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