From 2c3d8ac3b99eb793f73959b5bf491bc73560fc6c Mon Sep 17 00:00:00 2001 From: seam0s Date: Thu, 22 May 2025 12:57:10 +0300 Subject: [PATCH 01/25] Add initial support for fill tool on strokes --- .../graph_operation_message.rs | 4 +++ .../graph_operation_message_handler.rs | 5 +++ .../document/graph_operation/utility_types.rs | 7 ++++ .../document/overlays/utility_types.rs | 35 +++++++++++++++++++ .../messages/tool/tool_messages/fill_tool.rs | 9 +++++ 5 files changed, 60 insertions(+) diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs index a6e3fb38e9..90567e0873 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs @@ -33,6 +33,10 @@ pub enum GraphOperationMessage { layer: LayerNodeIdentifier, stroke: Stroke, }, + StrokeColorSet { + layer: LayerNodeIdentifier, + stroke_color: Color, + }, TransformChange { layer: LayerNodeIdentifier, transform: DAffine2, diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs index 9f8b8586f1..deb1cf36d7 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs @@ -56,6 +56,11 @@ impl MessageHandler> for Gr modify_inputs.stroke_set(stroke); } } + GraphOperationMessage::StrokeColorSet { layer, stroke_color } => { + if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) { + modify_inputs.stroke_color_set(Some(stroke_color)); + } + } GraphOperationMessage::TransformChange { layer, transform, diff --git a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs index 27d879a43d..c6384a9188 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -366,6 +366,13 @@ impl<'a> ModifyInputsContext<'a> { self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.line_join_miter_limit), false), false); } + pub fn stroke_color_set(&mut self, color: Option) { + let Some(stroke_node_id) = self.existing_node_id("Stroke", false) else { return }; + + let input_connector = InputConnector::node(stroke_node_id, 1); + self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::OptionalColor(color), false), false); + } + /// Update the transform value of the upstream Transform node based a change to its existing value and the given parent transform. /// A new Transform node is created if one does not exist, unless it would be given the identity transform. pub fn transform_change_with_parent(&mut self, transform: DAffine2, transform_in: TransformIn, parent_transform: DAffine2, skip_rerender: bool) { diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index f249f0c56c..d8c61f8992 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -704,6 +704,41 @@ impl OverlayContext { self.render_context.fill(); } + // fn fill_stroke_pattern(&mut self, color: &Color) { + // const PATTERN_WIDTH: usize = 4; + // const PATTERN_HEIGHT: usize = 4; + + // let pattern_canvas = OffscreenCanvas::new(PATTERN_WIDTH as u32, PATTERN_HEIGHT as u32).unwrap(); + // let pattern_context: OffscreenCanvasRenderingContext2d = pattern_canvas + // .get_context("2d") + // .ok() + // .flatten() + // .expect("Failed to get canvas context") + // .dyn_into() + // .expect("Context should be a canvas 2d context"); + + // // 4x4 pixels, 4 components (RGBA) per pixel + // let mut data = [0_u8; 4 * PATTERN_WIDTH * PATTERN_HEIGHT]; + + // // ┌▄▄┬──┬──┬──┐ + // // ├▀▀┼──┼──┼──┤ + // // ├──┼──┼▄▄┼──┤ + // // ├──┼──┼▀▀┼──┤ + // // └──┴──┴──┴──┘ + // let pixels = [(0, 0), (2, 2)]; + // for &(x, y) in &pixels { + // let index = (x + y * PATTERN_WIDTH as usize) * 4; + // data[index..index + 4].copy_from_slice(&color.to_rgba8_srgb()); + // } + + // let image_data = web_sys::ImageData::new_with_u8_clamped_array_and_sh(wasm_bindgen::Clamped(&mut data), PATTERN_WIDTH as u32, PATTERN_HEIGHT as u32).unwrap(); + // pattern_context.put_image_data(&image_data, 0., 0.).unwrap(); + // let pattern = self.render_context.create_pattern_with_offscreen_canvas(&pattern_canvas, "repeat").unwrap().unwrap(); + + // self.render_context.set_stroke_style_canvas_pattern(&pattern); + // self.render_context.stroke(); + // } + pub fn get_width(&self, text: &str) -> f64 { self.render_context.measure_text(text).expect("Failed to measure text dimensions").width() } diff --git a/editor/src/messages/tool/tool_messages/fill_tool.rs b/editor/src/messages/tool/tool_messages/fill_tool.rs index 0eae252bcd..0d86893397 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -121,9 +121,18 @@ impl Fsm for FillToolFsmState { FillToolMessage::FillSecondaryColor => Fill::Solid(global_tool_data.secondary_color.to_gamma_srgb()), _ => return self, }; + let stroke_color = match color_event { + FillToolMessage::FillPrimaryColor => global_tool_data.primary_color.to_gamma_srgb(), + FillToolMessage::FillSecondaryColor => global_tool_data.secondary_color.to_gamma_srgb(), + _ => return self, + }; responses.add(DocumentMessage::AddTransaction); responses.add(GraphOperationMessage::FillSet { layer: layer_identifier, fill }); + responses.add(GraphOperationMessage::StrokeColorSet { + layer: layer_identifier, + stroke_color, + }); FillToolFsmState::Filling } From 90730951c1f8bd08eddee6f49c2ab693090a6fea Mon Sep 17 00:00:00 2001 From: seam0s Date: Mon, 26 May 2025 14:12:46 +0300 Subject: [PATCH 02/25] Add stroke detection on click --- .../document/overlays/utility_types.rs | 74 +++++++++---------- .../messages/tool/tool_messages/fill_tool.rs | 37 ++++++++-- .../gcore/src/graphic_element/renderer.rs | 4 + 3 files changed, 71 insertions(+), 44 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index d8c61f8992..0e3b5bc3c6 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -597,7 +597,7 @@ impl OverlayContext { self.end_dpi_aware_transform(); } - fn push_path(&mut self, subpaths: impl Iterator>>, transform: DAffine2) { + pub fn push_path(&mut self, subpaths: impl Iterator>>, transform: DAffine2) { self.start_dpi_aware_transform(); self.render_context.begin_path(); @@ -667,7 +667,7 @@ impl OverlayContext { /// Fills the area inside the path with a pattern. Assumes `color` is in gamma space. /// Used by the fill tool to show the area to be filled. - pub fn fill_path_pattern(&mut self, subpaths: impl Iterator>>, transform: DAffine2, color: &Color) { + pub fn fill_path_pattern(&mut self, color: &Color) { const PATTERN_WIDTH: usize = 4; const PATTERN_HEIGHT: usize = 4; @@ -698,46 +698,44 @@ impl OverlayContext { pattern_context.put_image_data(&image_data, 0., 0.).unwrap(); let pattern = self.render_context.create_pattern_with_offscreen_canvas(&pattern_canvas, "repeat").unwrap().unwrap(); - self.push_path(subpaths, transform); - self.render_context.set_fill_style_canvas_pattern(&pattern); self.render_context.fill(); } - // fn fill_stroke_pattern(&mut self, color: &Color) { - // const PATTERN_WIDTH: usize = 4; - // const PATTERN_HEIGHT: usize = 4; - - // let pattern_canvas = OffscreenCanvas::new(PATTERN_WIDTH as u32, PATTERN_HEIGHT as u32).unwrap(); - // let pattern_context: OffscreenCanvasRenderingContext2d = pattern_canvas - // .get_context("2d") - // .ok() - // .flatten() - // .expect("Failed to get canvas context") - // .dyn_into() - // .expect("Context should be a canvas 2d context"); - - // // 4x4 pixels, 4 components (RGBA) per pixel - // let mut data = [0_u8; 4 * PATTERN_WIDTH * PATTERN_HEIGHT]; - - // // ┌▄▄┬──┬──┬──┐ - // // ├▀▀┼──┼──┼──┤ - // // ├──┼──┼▄▄┼──┤ - // // ├──┼──┼▀▀┼──┤ - // // └──┴──┴──┴──┘ - // let pixels = [(0, 0), (2, 2)]; - // for &(x, y) in &pixels { - // let index = (x + y * PATTERN_WIDTH as usize) * 4; - // data[index..index + 4].copy_from_slice(&color.to_rgba8_srgb()); - // } - - // let image_data = web_sys::ImageData::new_with_u8_clamped_array_and_sh(wasm_bindgen::Clamped(&mut data), PATTERN_WIDTH as u32, PATTERN_HEIGHT as u32).unwrap(); - // pattern_context.put_image_data(&image_data, 0., 0.).unwrap(); - // let pattern = self.render_context.create_pattern_with_offscreen_canvas(&pattern_canvas, "repeat").unwrap().unwrap(); - - // self.render_context.set_stroke_style_canvas_pattern(&pattern); - // self.render_context.stroke(); - // } + pub fn fill_stroke_pattern(&mut self, color: &Color) { + const PATTERN_WIDTH: usize = 4; + const PATTERN_HEIGHT: usize = 4; + + let pattern_canvas = OffscreenCanvas::new(PATTERN_WIDTH as u32, PATTERN_HEIGHT as u32).unwrap(); + let pattern_context: OffscreenCanvasRenderingContext2d = pattern_canvas + .get_context("2d") + .ok() + .flatten() + .expect("Failed to get canvas context") + .dyn_into() + .expect("Context should be a canvas 2d context"); + + // 4x4 pixels, 4 components (RGBA) per pixel + let mut data = [0_u8; 4 * PATTERN_WIDTH * PATTERN_HEIGHT]; + + // ┌▄▄┬──┬──┬──┐ + // ├▀▀┼──┼──┼──┤ + // ├──┼──┼▄▄┼──┤ + // ├──┼──┼▀▀┼──┤ + // └──┴──┴──┴──┘ + let pixels = [(0, 0), (2, 2)]; + for &(x, y) in &pixels { + let index = (x + y * PATTERN_WIDTH as usize) * 4; + data[index..index + 4].copy_from_slice(&color.to_rgba8_srgb()); + } + + let image_data = web_sys::ImageData::new_with_u8_clamped_array_and_sh(wasm_bindgen::Clamped(&mut data), PATTERN_WIDTH as u32, PATTERN_HEIGHT as u32).unwrap(); + pattern_context.put_image_data(&image_data, 0., 0.).unwrap(); + let pattern = self.render_context.create_pattern_with_offscreen_canvas(&pattern_canvas, "repeat").unwrap().unwrap(); + + self.render_context.set_stroke_style_canvas_pattern(&pattern); + self.render_context.stroke(); + } pub fn get_width(&self, text: &str) -> f64 { self.render_context.measure_text(text).expect("Failed to measure text dimensions").width() diff --git a/editor/src/messages/tool/tool_messages/fill_tool.rs b/editor/src/messages/tool/tool_messages/fill_tool.rs index 0d86893397..5c34f70cc3 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -1,7 +1,9 @@ use super::tool_prelude::*; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer; +use graphene_core::vector::PointId; use graphene_core::vector::style::Fill; +use graphene_std::renderer::ClickTarget; #[derive(Default)] pub struct FillTool { @@ -98,7 +100,12 @@ impl Fsm for FillToolFsmState { // Get the layer the user is hovering over if let Some(layer) = document.click(input) { - overlay_context.fill_path_pattern(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer), &preview_color); + overlay_context.push_path(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer)); + overlay_context.fill_path_pattern(&preview_color); + + document.metadata().layer_outline(layer).any(|subpath| subpath.contains_point_autoclose(input.mouse.position)).then(|| { + overlay_context.fill_stroke_pattern(&preview_color); + }); } self @@ -127,12 +134,30 @@ impl Fsm for FillToolFsmState { _ => return self, }; + let close_to_stroke = |mouse_pos: DVec2, click_target: &ClickTarget, to_document_transform: DAffine2| { + let mut subpath = click_target.subpath().clone(); + let lut = subpath.compute_lookup_table(Some(15), None); + // const RANGE: f64 = 100.0; + subpath.apply_transform(to_document_transform); + lut.iter().any(|&point| (mouse_pos - point).perp().length() <= click_target.stroke_width()) + }; responses.add(DocumentMessage::AddTransaction); - responses.add(GraphOperationMessage::FillSet { layer: layer_identifier, fill }); - responses.add(GraphOperationMessage::StrokeColorSet { - layer: layer_identifier, - stroke_color, - }); + document + .metadata() + .click_targets(layer_identifier) + .unwrap() + .into_iter() + .any(|click_target| close_to_stroke(input.mouse.position, click_target, document.metadata().transform_to_document(layer_identifier))) + .then(|| { + responses.add(GraphOperationMessage::StrokeColorSet { + layer: layer_identifier, + stroke_color, + }); + }) + .or_else(|| { + responses.add(GraphOperationMessage::FillSet { layer: layer_identifier, fill }); + Some(()) + }); FillToolFsmState::Filling } diff --git a/node-graph/gcore/src/graphic_element/renderer.rs b/node-graph/gcore/src/graphic_element/renderer.rs index 6b1f4f436f..afbf171b49 100644 --- a/node-graph/gcore/src/graphic_element/renderer.rs +++ b/node-graph/gcore/src/graphic_element/renderer.rs @@ -38,6 +38,10 @@ impl ClickTarget { &self.subpath } + pub fn stroke_width(&self) -> f64 { + self.stroke_width + } + pub fn bounding_box(&self) -> Option<[DVec2; 2]> { self.bounding_box } From 5355e064fc5c09fcaf79ba0d589bb8f208e71712 Mon Sep 17 00:00:00 2001 From: seam0s Date: Mon, 26 May 2025 17:00:13 +0300 Subject: [PATCH 03/25] Fix Rust formatting --- editor/src/messages/tool/tool_messages/fill_tool.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/editor/src/messages/tool/tool_messages/fill_tool.rs b/editor/src/messages/tool/tool_messages/fill_tool.rs index 5c34f70cc3..749082a775 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -1,7 +1,6 @@ use super::tool_prelude::*; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer; -use graphene_core::vector::PointId; use graphene_core::vector::style::Fill; use graphene_std::renderer::ClickTarget; From 42ca0b0ff412e947dcd9ca0ea35a1dc71c87c9de Mon Sep 17 00:00:00 2001 From: seam0s Date: Wed, 28 May 2025 01:06:06 +0300 Subject: [PATCH 04/25] Update transformation on subpath from document to viewport space --- .../messages/tool/tool_messages/fill_tool.rs | 38 +++++++++---------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/editor/src/messages/tool/tool_messages/fill_tool.rs b/editor/src/messages/tool/tool_messages/fill_tool.rs index 749082a775..b1837b2c1e 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -133,31 +133,29 @@ impl Fsm for FillToolFsmState { _ => return self, }; - let close_to_stroke = |mouse_pos: DVec2, click_target: &ClickTarget, to_document_transform: DAffine2| { + responses.add(DocumentMessage::AddTransaction); + let close_to_stroke = |mouse_pos: DVec2, click_target: &ClickTarget, to_viewport_transform: DAffine2| { let mut subpath = click_target.subpath().clone(); let lut = subpath.compute_lookup_table(Some(15), None); - // const RANGE: f64 = 100.0; - subpath.apply_transform(to_document_transform); + subpath.apply_transform(to_viewport_transform); lut.iter().any(|&point| (mouse_pos - point).perp().length() <= click_target.stroke_width()) }; - responses.add(DocumentMessage::AddTransaction); - document - .metadata() - .click_targets(layer_identifier) - .unwrap() - .into_iter() - .any(|click_target| close_to_stroke(input.mouse.position, click_target, document.metadata().transform_to_document(layer_identifier))) - .then(|| { - responses.add(GraphOperationMessage::StrokeColorSet { - layer: layer_identifier, - stroke_color, + let _ = document.metadata().click_targets(layer_identifier).is_some_and(|target| { + target + .iter() + .any(|click_target| close_to_stroke(input.mouse.position, click_target, document.metadata().transform_to_viewport(layer_identifier))) + .then(|| { + responses.add(GraphOperationMessage::StrokeColorSet { + layer: layer_identifier, + stroke_color, + }); + }) + .or_else(|| { + responses.add(GraphOperationMessage::FillSet { layer: layer_identifier, fill }); + Some(()) }); - }) - .or_else(|| { - responses.add(GraphOperationMessage::FillSet { layer: layer_identifier, fill }); - Some(()) - }); - + true + }); FillToolFsmState::Filling } (FillToolFsmState::Filling, FillToolMessage::PointerUp) => FillToolFsmState::Ready, From 433bdb60770e2489661093eb49a3a5c41d5b22b0 Mon Sep 17 00:00:00 2001 From: seam0s Date: Wed, 28 May 2025 01:13:50 +0300 Subject: [PATCH 05/25] Minor fix --- editor/src/messages/tool/tool_messages/fill_tool.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/src/messages/tool/tool_messages/fill_tool.rs b/editor/src/messages/tool/tool_messages/fill_tool.rs index b1837b2c1e..a44959eb43 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -136,8 +136,8 @@ impl Fsm for FillToolFsmState { responses.add(DocumentMessage::AddTransaction); let close_to_stroke = |mouse_pos: DVec2, click_target: &ClickTarget, to_viewport_transform: DAffine2| { let mut subpath = click_target.subpath().clone(); - let lut = subpath.compute_lookup_table(Some(15), None); subpath.apply_transform(to_viewport_transform); + let lut = subpath.compute_lookup_table(Some(15), None); lut.iter().any(|&point| (mouse_pos - point).perp().length() <= click_target.stroke_width()) }; let _ = document.metadata().click_targets(layer_identifier).is_some_and(|target| { From 48b192c511c963fcd4b780df630788bf0764d684 Mon Sep 17 00:00:00 2001 From: seam0s Date: Wed, 28 May 2025 10:43:43 +0300 Subject: [PATCH 06/25] Add stroke detection support for fill tool overlays --- .../messages/tool/tool_messages/fill_tool.rs | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/editor/src/messages/tool/tool_messages/fill_tool.rs b/editor/src/messages/tool/tool_messages/fill_tool.rs index a44959eb43..d873dd65f3 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -100,10 +100,25 @@ impl Fsm for FillToolFsmState { // Get the layer the user is hovering over if let Some(layer) = document.click(input) { overlay_context.push_path(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer)); - overlay_context.fill_path_pattern(&preview_color); - document.metadata().layer_outline(layer).any(|subpath| subpath.contains_point_autoclose(input.mouse.position)).then(|| { - overlay_context.fill_stroke_pattern(&preview_color); + let close_to_stroke = |mouse_pos: DVec2, click_target: &ClickTarget, to_viewport_transform: DAffine2| { + let mut subpath = click_target.subpath().clone(); + subpath.apply_transform(to_viewport_transform); + let lut = subpath.compute_lookup_table(Some(15), None); + lut.iter().any(|&point| (mouse_pos - point).perp().length() <= click_target.stroke_width() * 1.5) + }; + let _ = document.metadata().click_targets(layer).is_some_and(|target| { + target + .iter() + .any(|click_target| close_to_stroke(input.mouse.position, click_target, document.metadata().transform_to_viewport(layer))) + .then(|| { + overlay_context.fill_stroke_pattern(&preview_color); + }) + .or_else(|| { + overlay_context.fill_path_pattern(&preview_color); + Some(()) + }); + true }); } @@ -138,7 +153,7 @@ impl Fsm for FillToolFsmState { let mut subpath = click_target.subpath().clone(); subpath.apply_transform(to_viewport_transform); let lut = subpath.compute_lookup_table(Some(15), None); - lut.iter().any(|&point| (mouse_pos - point).perp().length() <= click_target.stroke_width()) + lut.iter().any(|&point| (mouse_pos - point).perp().length() <= click_target.stroke_width() * 1.5) }; let _ = document.metadata().click_targets(layer_identifier).is_some_and(|target| { target From 5b8f59bc0f3c26150c307846a0659c542f66afe6 Mon Sep 17 00:00:00 2001 From: seam0s Date: Wed, 28 May 2025 18:02:42 +0300 Subject: [PATCH 07/25] Refactor stroke detection logic --- .../messages/tool/tool_messages/fill_tool.rs | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/editor/src/messages/tool/tool_messages/fill_tool.rs b/editor/src/messages/tool/tool_messages/fill_tool.rs index d873dd65f3..af8e63fe8b 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -73,6 +73,14 @@ impl ToolTransition for FillTool { } } +pub fn close_to_stroke(mouse_pos: DVec2, click_target: &ClickTarget, to_viewport_transform: DAffine2) -> bool { + let subpath = click_target.subpath().clone(); + let lut = subpath.compute_lookup_table(Some(25), None); + + lut.iter() + .any(|&point| (to_viewport_transform.inverse().transform_point2(mouse_pos) - point).length() <= click_target.stroke_width()) +} + #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] enum FillToolFsmState { #[default] @@ -100,13 +108,6 @@ impl Fsm for FillToolFsmState { // Get the layer the user is hovering over if let Some(layer) = document.click(input) { overlay_context.push_path(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer)); - - let close_to_stroke = |mouse_pos: DVec2, click_target: &ClickTarget, to_viewport_transform: DAffine2| { - let mut subpath = click_target.subpath().clone(); - subpath.apply_transform(to_viewport_transform); - let lut = subpath.compute_lookup_table(Some(15), None); - lut.iter().any(|&point| (mouse_pos - point).perp().length() <= click_target.stroke_width() * 1.5) - }; let _ = document.metadata().click_targets(layer).is_some_and(|target| { target .iter() @@ -149,12 +150,6 @@ impl Fsm for FillToolFsmState { }; responses.add(DocumentMessage::AddTransaction); - let close_to_stroke = |mouse_pos: DVec2, click_target: &ClickTarget, to_viewport_transform: DAffine2| { - let mut subpath = click_target.subpath().clone(); - subpath.apply_transform(to_viewport_transform); - let lut = subpath.compute_lookup_table(Some(15), None); - lut.iter().any(|&point| (mouse_pos - point).perp().length() <= click_target.stroke_width() * 1.5) - }; let _ = document.metadata().click_targets(layer_identifier).is_some_and(|target| { target .iter() From 1a64e29cb33fc528e4fe9aa53e63b3ba20f6c519 Mon Sep 17 00:00:00 2001 From: seam0s Date: Sun, 1 Jun 2025 20:53:36 +0300 Subject: [PATCH 08/25] Refactor push_path and add scaling to stroke overlay --- .../document/overlays/utility_types.rs | 105 ++++++++++-------- .../messages/tool/tool_messages/fill_tool.rs | 22 +++- .../messages/tool/tool_messages/pen_tool.rs | 2 +- 3 files changed, 73 insertions(+), 56 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index 0e3b5bc3c6..7cf9d56b18 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -597,7 +597,7 @@ impl OverlayContext { self.end_dpi_aware_transform(); } - pub fn push_path(&mut self, subpaths: impl Iterator>>, transform: DAffine2) { + pub fn draw_path(&mut self, subpaths: impl Iterator>>, transform: DAffine2) { self.start_dpi_aware_transform(); self.render_context.begin_path(); @@ -611,18 +611,19 @@ impl OverlayContext { self.render_context.move_to(transform.transform_point2(first.start()).x, transform.transform_point2(first.start()).y); for curve in curves { + let splat_value = 0.5; match curve.handles { bezier_rs::BezierHandles::Linear => { let a = transform.transform_point2(curve.end()); - let a = a.round() - DVec2::splat(0.5); + let a = a.round() - DVec2::splat(splat_value); 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()); - let a = a.round() - DVec2::splat(0.5); - let b = b.round() - DVec2::splat(0.5); + let a = a.round() - DVec2::splat(splat_value); + let b = b.round() - DVec2::splat(splat_value); self.render_context.quadratic_curve_to(a.x, a.y, b.x, b.y) } @@ -630,9 +631,9 @@ impl OverlayContext { let a = transform.transform_point2(handle_start); let b = transform.transform_point2(handle_end); let c = transform.transform_point2(curve.end()); - let a = a.round() - DVec2::splat(0.5); - let b = b.round() - DVec2::splat(0.5); - let c = c.round() - DVec2::splat(0.5); + let a = a.round() - DVec2::splat(splat_value); + let b = b.round() - DVec2::splat(splat_value); + let c = c.round() - DVec2::splat(splat_value); self.render_context.bezier_curve_to(a.x, a.y, b.x, b.y, c.x, c.y) } @@ -649,60 +650,64 @@ impl OverlayContext { /// Used by the Select tool to outline a path selected or hovered. pub fn outline(&mut self, subpaths: impl Iterator>>, transform: DAffine2, color: Option<&str>) { - self.push_path(subpaths, transform); + self.draw_path(subpaths, transform); let color = color.unwrap_or(COLOR_OVERLAY_BLUE); self.render_context.set_stroke_style_str(color); self.render_context.stroke(); } - /// Fills the area inside the path. Assumes `color` is in gamma space. - /// Used by the Pen tool to show the path being closed. - pub fn fill_path(&mut self, subpaths: impl Iterator>>, transform: DAffine2, color: &str) { - self.push_path(subpaths, transform); - - self.render_context.set_fill_style_str(color); - self.render_context.fill(); - } - - /// Fills the area inside the path with a pattern. Assumes `color` is in gamma space. - /// Used by the fill tool to show the area to be filled. - pub fn fill_path_pattern(&mut self, color: &Color) { - const PATTERN_WIDTH: usize = 4; - const PATTERN_HEIGHT: usize = 4; - - let pattern_canvas = OffscreenCanvas::new(PATTERN_WIDTH as u32, PATTERN_HEIGHT as u32).unwrap(); - let pattern_context: OffscreenCanvasRenderingContext2d = pattern_canvas - .get_context("2d") - .ok() - .flatten() - .expect("Failed to get canvas context") - .dyn_into() - .expect("Context should be a canvas 2d context"); + /// Fills the area inside the path (with an optional pattern). Assumes `color` is in gamma space. + /// Used by the Pen tool to show the path being closed and by the Fill tool to show the area to be filled with a pattern. + pub fn fill_path(&mut self, subpaths: impl Iterator>>, transform: DAffine2, color: &str, with_pattern: bool, width_for_inner_boundary: Option) { + self.render_context.set_line_width(width_for_inner_boundary.unwrap_or(1.) * self.device_pixel_ratio); + self.draw_path(subpaths, transform); + + if with_pattern { + const PATTERN_WIDTH: usize = 4; + const PATTERN_HEIGHT: usize = 4; + + let pattern_canvas = OffscreenCanvas::new(PATTERN_WIDTH as u32, PATTERN_HEIGHT as u32).unwrap(); + let pattern_context: OffscreenCanvasRenderingContext2d = pattern_canvas + .get_context("2d") + .ok() + .flatten() + .expect("Failed to get canvas context") + .dyn_into() + .expect("Context should be a canvas 2d context"); + + // 4x4 pixels, 4 components (RGBA) per pixel + let mut data = [0_u8; 4 * PATTERN_WIDTH * PATTERN_HEIGHT]; + + // ┌▄▄┬──┬──┬──┐ + // ├▀▀┼──┼──┼──┤ + // ├──┼──┼▄▄┼──┤ + // ├──┼──┼▀▀┼──┤ + // └──┴──┴──┴──┘ + let pixels = [(0, 0), (2, 2)]; + for &(x, y) in &pixels { + let index = (x + y * PATTERN_WIDTH as usize) * 4; + data[index..index + 4].copy_from_slice(&Color::from_rgba_str(color).unwrap().to_rgba8_srgb()); + } - // 4x4 pixels, 4 components (RGBA) per pixel - let mut data = [0_u8; 4 * PATTERN_WIDTH * PATTERN_HEIGHT]; + let image_data = web_sys::ImageData::new_with_u8_clamped_array_and_sh(wasm_bindgen::Clamped(&mut data), PATTERN_WIDTH as u32, PATTERN_HEIGHT as u32).unwrap(); + pattern_context.put_image_data(&image_data, 0., 0.).unwrap(); + let pattern = self.render_context.create_pattern_with_offscreen_canvas(&pattern_canvas, "repeat").unwrap().unwrap(); - // ┌▄▄┬──┬──┬──┐ - // ├▀▀┼──┼──┼──┤ - // ├──┼──┼▄▄┼──┤ - // ├──┼──┼▀▀┼──┤ - // └──┴──┴──┴──┘ - let pixels = [(0, 0), (2, 2)]; - for &(x, y) in &pixels { - let index = (x + y * PATTERN_WIDTH as usize) * 4; - data[index..index + 4].copy_from_slice(&color.to_rgba8_srgb()); + self.render_context.set_fill_style_canvas_pattern(&pattern); + self.render_context.fill(); + } else { + self.render_context.set_fill_style_str(color); + self.render_context.fill(); } - let image_data = web_sys::ImageData::new_with_u8_clamped_array_and_sh(wasm_bindgen::Clamped(&mut data), PATTERN_WIDTH as u32, PATTERN_HEIGHT as u32).unwrap(); - pattern_context.put_image_data(&image_data, 0., 0.).unwrap(); - let pattern = self.render_context.create_pattern_with_offscreen_canvas(&pattern_canvas, "repeat").unwrap().unwrap(); - - self.render_context.set_fill_style_canvas_pattern(&pattern); - self.render_context.fill(); + self.render_context.set_line_width(1.); } - pub fn fill_stroke_pattern(&mut self, color: &Color) { + pub fn fill_stroke(&mut self, subpaths: impl Iterator>>, transform: DAffine2, color: &Color, width: Option) { + self.render_context.set_line_width(width.unwrap_or(1.) * self.device_pixel_ratio); + self.draw_path(subpaths, transform); + const PATTERN_WIDTH: usize = 4; const PATTERN_HEIGHT: usize = 4; @@ -735,6 +740,8 @@ impl OverlayContext { self.render_context.set_stroke_style_canvas_pattern(&pattern); self.render_context.stroke(); + + self.render_context.set_line_width(1.); } pub fn get_width(&self, text: &str) -> f64 { diff --git a/editor/src/messages/tool/tool_messages/fill_tool.rs b/editor/src/messages/tool/tool_messages/fill_tool.rs index af8e63fe8b..a9a0709167 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -1,6 +1,6 @@ use super::tool_prelude::*; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; -use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer; +use crate::messages::tool::common_functionality::graph_modification_utils::{NodeGraphLayer, get_stroke_width}; use graphene_core::vector::style::Fill; use graphene_std::renderer::ClickTarget; @@ -107,16 +107,26 @@ impl Fsm for FillToolFsmState { // Get the layer the user is hovering over if let Some(layer) = document.click(input) { - overlay_context.push_path(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer)); - let _ = document.metadata().click_targets(layer).is_some_and(|target| { - target + let _ = document.metadata().click_targets(layer).is_some_and(|targets| { + targets .iter() .any(|click_target| close_to_stroke(input.mouse.position, click_target, document.metadata().transform_to_viewport(layer))) .then(|| { - overlay_context.fill_stroke_pattern(&preview_color); + overlay_context.fill_stroke( + document.metadata().layer_outline(layer), + document.metadata().transform_to_viewport(layer), + &preview_color, + get_stroke_width(layer, &document.network_interface), + ); }) .or_else(|| { - overlay_context.fill_path_pattern(&preview_color); + overlay_context.fill_path( + document.metadata().layer_outline(layer), + document.metadata().transform_to_viewport(layer), + &preview_color.to_rgba_hex_srgb(), + true, + get_stroke_width(layer, &document.network_interface), + ); Some(()) }); true diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index f0ceb00b9c..baf079bb38 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -1641,7 +1641,7 @@ impl Fsm for PenToolFsmState { .with_alpha(0.05) .to_rgba_hex_srgb(); fill_color.insert(0, '#'); - overlay_context.fill_path(subpaths.iter(), transform, fill_color.as_str()); + overlay_context.fill_path(subpaths.iter(), transform, fill_color.as_str(), false, None); } } } From 5ce0177c180efeb0a4acdd0ad821c92754ad0490 Mon Sep 17 00:00:00 2001 From: seam0s Date: Mon, 2 Jun 2025 11:30:49 +0300 Subject: [PATCH 09/25] Refine scaling on stroke overlay --- .../messages/portfolio/document/overlays/utility_types.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index 7cf9d56b18..e595e3d96f 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -10,6 +10,7 @@ use core::f64::consts::{FRAC_PI_2, TAU}; use glam::{DAffine2, DVec2}; use graphene_core::Color; use graphene_core::renderer::Quad; +use graphene_std::transform::Transform; use graphene_std::vector::{PointId, SegmentId, VectorData}; use std::collections::HashMap; use wasm_bindgen::{JsCast, JsValue}; @@ -660,7 +661,8 @@ impl OverlayContext { /// Fills the area inside the path (with an optional pattern). Assumes `color` is in gamma space. /// Used by the Pen tool to show the path being closed and by the Fill tool to show the area to be filled with a pattern. pub fn fill_path(&mut self, subpaths: impl Iterator>>, transform: DAffine2, color: &str, with_pattern: bool, width_for_inner_boundary: Option) { - self.render_context.set_line_width(width_for_inner_boundary.unwrap_or(1.) * self.device_pixel_ratio); + self.render_context + .set_line_width(width_for_inner_boundary.unwrap_or(1.) * (transform.decompose_scale().length() * 0.7)); self.draw_path(subpaths, transform); if with_pattern { @@ -705,7 +707,7 @@ impl OverlayContext { } pub fn fill_stroke(&mut self, subpaths: impl Iterator>>, transform: DAffine2, color: &Color, width: Option) { - self.render_context.set_line_width(width.unwrap_or(1.) * self.device_pixel_ratio); + self.render_context.set_line_width(width.unwrap_or(1.) * (transform.decompose_scale().length() * 0.7)); self.draw_path(subpaths, transform); const PATTERN_WIDTH: usize = 4; From 33849ff41fab2956f67896953a58173d8105a7cb Mon Sep 17 00:00:00 2001 From: seam0s Date: Tue, 3 Jun 2025 00:53:28 +0300 Subject: [PATCH 10/25] Refactor stroke and fill canvas pattern and fix stroke overlays on open subpaths --- .../document/overlays/utility_types.rs | 96 +++++++------------ .../messages/tool/tool_messages/fill_tool.rs | 12 ++- .../messages/tool/tool_messages/pen_tool.rs | 3 +- 3 files changed, 46 insertions(+), 65 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index e595e3d96f..e8d23ea39d 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -550,8 +550,7 @@ impl OverlayContext { self.end_dpi_aware_transform(); } - /// Used by the Pen and Path tools to outline the path of the shape. - pub fn outline_vector(&mut self, vector_data: &VectorData, transform: DAffine2) { + fn draw_path_from_vector_data(&mut self, vector_data: &VectorData, transform: DAffine2) { self.start_dpi_aware_transform(); self.render_context.begin_path(); @@ -563,10 +562,14 @@ impl OverlayContext { self.bezier_command(bezier, transform, move_to); } + self.end_dpi_aware_transform(); + } + + /// Used by the Pen and Path tools to outline the path of the shape. + pub fn outline_vector(&mut self, vector_data: &VectorData, transform: DAffine2) { + self.draw_path_from_vector_data(vector_data, transform); self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE); self.render_context.stroke(); - - self.end_dpi_aware_transform(); } /// Used by the Pen tool in order to show how the bezier curve would look like. @@ -598,7 +601,7 @@ impl OverlayContext { self.end_dpi_aware_transform(); } - pub fn draw_path(&mut self, subpaths: impl Iterator>>, transform: DAffine2) { + pub fn draw_path_from_subpaths(&mut self, subpaths: impl Iterator>>, transform: DAffine2) { self.start_dpi_aware_transform(); self.render_context.begin_path(); @@ -651,65 +654,15 @@ impl OverlayContext { /// Used by the Select tool to outline a path selected or hovered. pub fn outline(&mut self, subpaths: impl Iterator>>, transform: DAffine2, color: Option<&str>) { - self.draw_path(subpaths, transform); + self.draw_path_from_subpaths(subpaths, transform); let color = color.unwrap_or(COLOR_OVERLAY_BLUE); self.render_context.set_stroke_style_str(color); self.render_context.stroke(); } - /// Fills the area inside the path (with an optional pattern). Assumes `color` is in gamma space. - /// Used by the Pen tool to show the path being closed and by the Fill tool to show the area to be filled with a pattern. - pub fn fill_path(&mut self, subpaths: impl Iterator>>, transform: DAffine2, color: &str, with_pattern: bool, width_for_inner_boundary: Option) { - self.render_context - .set_line_width(width_for_inner_boundary.unwrap_or(1.) * (transform.decompose_scale().length() * 0.7)); - self.draw_path(subpaths, transform); - - if with_pattern { - const PATTERN_WIDTH: usize = 4; - const PATTERN_HEIGHT: usize = 4; - - let pattern_canvas = OffscreenCanvas::new(PATTERN_WIDTH as u32, PATTERN_HEIGHT as u32).unwrap(); - let pattern_context: OffscreenCanvasRenderingContext2d = pattern_canvas - .get_context("2d") - .ok() - .flatten() - .expect("Failed to get canvas context") - .dyn_into() - .expect("Context should be a canvas 2d context"); - - // 4x4 pixels, 4 components (RGBA) per pixel - let mut data = [0_u8; 4 * PATTERN_WIDTH * PATTERN_HEIGHT]; - - // ┌▄▄┬──┬──┬──┐ - // ├▀▀┼──┼──┼──┤ - // ├──┼──┼▄▄┼──┤ - // ├──┼──┼▀▀┼──┤ - // └──┴──┴──┴──┘ - let pixels = [(0, 0), (2, 2)]; - for &(x, y) in &pixels { - let index = (x + y * PATTERN_WIDTH as usize) * 4; - data[index..index + 4].copy_from_slice(&Color::from_rgba_str(color).unwrap().to_rgba8_srgb()); - } - - let image_data = web_sys::ImageData::new_with_u8_clamped_array_and_sh(wasm_bindgen::Clamped(&mut data), PATTERN_WIDTH as u32, PATTERN_HEIGHT as u32).unwrap(); - pattern_context.put_image_data(&image_data, 0., 0.).unwrap(); - let pattern = self.render_context.create_pattern_with_offscreen_canvas(&pattern_canvas, "repeat").unwrap().unwrap(); - - self.render_context.set_fill_style_canvas_pattern(&pattern); - self.render_context.fill(); - } else { - self.render_context.set_fill_style_str(color); - self.render_context.fill(); - } - - self.render_context.set_line_width(1.); - } - - pub fn fill_stroke(&mut self, subpaths: impl Iterator>>, transform: DAffine2, color: &Color, width: Option) { - self.render_context.set_line_width(width.unwrap_or(1.) * (transform.decompose_scale().length() * 0.7)); - self.draw_path(subpaths, transform); - + /// Default canvas pattern used for filling stroke or fill of a path. + fn fill_canvas_pattern(&self, color: &Color) -> web_sys::CanvasPattern { const PATTERN_WIDTH: usize = 4; const PATTERN_HEIGHT: usize = 4; @@ -738,9 +691,32 @@ impl OverlayContext { let image_data = web_sys::ImageData::new_with_u8_clamped_array_and_sh(wasm_bindgen::Clamped(&mut data), PATTERN_WIDTH as u32, PATTERN_HEIGHT as u32).unwrap(); pattern_context.put_image_data(&image_data, 0., 0.).unwrap(); - let pattern = self.render_context.create_pattern_with_offscreen_canvas(&pattern_canvas, "repeat").unwrap().unwrap(); + return self.render_context.create_pattern_with_offscreen_canvas(&pattern_canvas, "repeat").unwrap().unwrap(); + } + + /// Fills the area inside the path (with an optional pattern). Assumes `color` is in gamma space. + /// Used by the Pen tool to show the path being closed and by the Fill tool to show the area to be filled with a pattern. + pub fn fill_path(&mut self, subpaths: impl Iterator>>, transform: DAffine2, color: &Color, with_pattern: bool, width_for_inner_boundary: Option) { + self.render_context + .set_line_width(width_for_inner_boundary.unwrap_or(1.) * (transform.decompose_scale().length() * 0.7)); + self.draw_path_from_subpaths(subpaths, transform); + + if with_pattern { + self.render_context.set_fill_style_canvas_pattern(&self.fill_canvas_pattern(color)); + self.render_context.fill(); + } else { + self.render_context.set_fill_style_str(color.to_rgba_hex_srgb().as_str()); + self.render_context.fill(); + } + + self.render_context.set_line_width(1.); + } + + pub fn fill_stroke(&mut self, vector_data: &VectorData, transform: DAffine2, color: &Color, width: Option) { + self.render_context.set_line_width(width.unwrap_or(1.) * (transform.decompose_scale().length() * 0.7)); + self.draw_path_from_vector_data(vector_data, transform); - self.render_context.set_stroke_style_canvas_pattern(&pattern); + self.render_context.set_stroke_style_canvas_pattern(&self.fill_canvas_pattern(color)); self.render_context.stroke(); self.render_context.set_line_width(1.); diff --git a/editor/src/messages/tool/tool_messages/fill_tool.rs b/editor/src/messages/tool/tool_messages/fill_tool.rs index a9a0709167..4a805c7bf6 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -75,9 +75,10 @@ impl ToolTransition for FillTool { pub fn close_to_stroke(mouse_pos: DVec2, click_target: &ClickTarget, to_viewport_transform: DAffine2) -> bool { let subpath = click_target.subpath().clone(); - let lut = subpath.compute_lookup_table(Some(25), None); + let lookup = subpath.compute_lookup_table(Some(25), None); - lut.iter() + lookup + .iter() .any(|&point| (to_viewport_transform.inverse().transform_point2(mouse_pos) - point).length() <= click_target.stroke_width()) } @@ -112,8 +113,11 @@ impl Fsm for FillToolFsmState { .iter() .any(|click_target| close_to_stroke(input.mouse.position, click_target, document.metadata().transform_to_viewport(layer))) .then(|| { + let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { + return; + }; overlay_context.fill_stroke( - document.metadata().layer_outline(layer), + &vector_data, document.metadata().transform_to_viewport(layer), &preview_color, get_stroke_width(layer, &document.network_interface), @@ -123,7 +127,7 @@ impl Fsm for FillToolFsmState { overlay_context.fill_path( document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer), - &preview_color.to_rgba_hex_srgb(), + &preview_color, true, get_stroke_width(layer, &document.network_interface), ); diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index baf079bb38..ae237af3d3 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -1641,7 +1641,8 @@ impl Fsm for PenToolFsmState { .with_alpha(0.05) .to_rgba_hex_srgb(); fill_color.insert(0, '#'); - overlay_context.fill_path(subpaths.iter(), transform, fill_color.as_str(), false, None); + let fill_color = Color::from_rgba_str(fill_color.as_str()).unwrap(); + overlay_context.fill_path(subpaths.iter(), transform, &fill_color, false, None); } } } From c196aa151a783c914381d8857dfdf89c613385be Mon Sep 17 00:00:00 2001 From: Mateo <45690579+c-mateo@users.noreply.github.com> Date: Wed, 4 Jun 2025 01:16:43 -0300 Subject: [PATCH 11/25] Prevent the dotted overlays from extending into the stroke area --- .../document/overlays/overlays_message_handler.rs | 12 ++++++------ .../portfolio/document/overlays/utility_types.rs | 12 ++++++++++-- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs b/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs index 0db542da75..38686c2bb4 100644 --- a/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs +++ b/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs @@ -51,12 +51,6 @@ impl MessageHandler> for OverlaysMessag let _ = context.reset_transform(); if visibility_settings.all() { - responses.add(DocumentMessage::GridOverlays(OverlayContext { - render_context: 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: context.clone(), @@ -65,6 +59,12 @@ impl MessageHandler> for OverlaysMessag visibility_settings: visibility_settings.clone(), })); } + responses.add(DocumentMessage::GridOverlays(OverlayContext { + render_context: context.clone(), + size: size.as_dvec2(), + device_pixel_ratio, + visibility_settings: visibility_settings.clone(), + })); } } #[cfg(not(target_arch = "wasm32"))] diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index e8d23ea39d..7510ac1876 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -698,12 +698,20 @@ impl OverlayContext { /// Used by the Pen tool to show the path being closed and by the Fill tool to show the area to be filled with a pattern. pub fn fill_path(&mut self, subpaths: impl Iterator>>, transform: DAffine2, color: &Color, with_pattern: bool, width_for_inner_boundary: Option) { self.render_context - .set_line_width(width_for_inner_boundary.unwrap_or(1.) * (transform.decompose_scale().length() * 0.7)); + .set_line_width(width_for_inner_boundary.unwrap_or(1.) * (transform.decompose_scale().length() * 0.75)); self.draw_path_from_subpaths(subpaths, transform); if with_pattern { self.render_context.set_fill_style_canvas_pattern(&self.fill_canvas_pattern(color)); + self.render_context.set_stroke_style_str(&"#000000"); self.render_context.fill(); + + // Erase the part over the stroke area + self.render_context.save(); + self.render_context.set_global_composite_operation("destination-out").expect("Failed to set global composite operation"); + self.render_context.stroke(); + + self.render_context.restore(); } else { self.render_context.set_fill_style_str(color.to_rgba_hex_srgb().as_str()); self.render_context.fill(); @@ -713,7 +721,7 @@ impl OverlayContext { } pub fn fill_stroke(&mut self, vector_data: &VectorData, transform: DAffine2, color: &Color, width: Option) { - self.render_context.set_line_width(width.unwrap_or(1.) * (transform.decompose_scale().length() * 0.7)); + self.render_context.set_line_width(width.unwrap_or(1.) * (transform.decompose_scale().length() * 0.75)); self.draw_path_from_vector_data(vector_data, transform); self.render_context.set_stroke_style_canvas_pattern(&self.fill_canvas_pattern(color)); From b190c1e88befcec05daced8a4899d74f2fae04cb Mon Sep 17 00:00:00 2001 From: Mateo <45690579+c-mateo@users.noreply.github.com> Date: Wed, 4 Jun 2025 01:47:35 -0300 Subject: [PATCH 12/25] Fix crash when closing path and display filled overlay of shape first --- .../overlays/overlays_message_handler.rs | 12 +-- .../document/overlays/utility_types.rs | 12 +-- .../messages/tool/tool_messages/pen_tool.rs | 93 +++++++++---------- 3 files changed, 56 insertions(+), 61 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs b/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs index 38686c2bb4..d69f288a77 100644 --- a/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs +++ b/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs @@ -58,13 +58,13 @@ impl MessageHandler> for OverlaysMessag device_pixel_ratio, visibility_settings: visibility_settings.clone(), })); + responses.add(DocumentMessage::GridOverlays(OverlayContext { + render_context: context.clone(), + size: size.as_dvec2(), + device_pixel_ratio, + visibility_settings: visibility_settings.clone(), + })); } - responses.add(DocumentMessage::GridOverlays(OverlayContext { - render_context: context.clone(), - size: size.as_dvec2(), - device_pixel_ratio, - visibility_settings: visibility_settings.clone(), - })); } } #[cfg(not(target_arch = "wasm32"))] diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index 7510ac1876..7aee74bc7e 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -697,6 +697,7 @@ impl OverlayContext { /// Fills the area inside the path (with an optional pattern). Assumes `color` is in gamma space. /// Used by the Pen tool to show the path being closed and by the Fill tool to show the area to be filled with a pattern. pub fn fill_path(&mut self, subpaths: impl Iterator>>, transform: DAffine2, color: &Color, with_pattern: bool, width_for_inner_boundary: Option) { + self.render_context.save(); self.render_context .set_line_width(width_for_inner_boundary.unwrap_or(1.) * (transform.decompose_scale().length() * 0.75)); self.draw_path_from_subpaths(subpaths, transform); @@ -707,27 +708,26 @@ impl OverlayContext { self.render_context.fill(); // Erase the part over the stroke area - self.render_context.save(); self.render_context.set_global_composite_operation("destination-out").expect("Failed to set global composite operation"); self.render_context.stroke(); - - self.render_context.restore(); } else { - self.render_context.set_fill_style_str(color.to_rgba_hex_srgb().as_str()); + let color_str = "#".to_owned() + color.to_rgba_hex_srgb().as_str(); + self.render_context.set_fill_style_str(&color_str); self.render_context.fill(); } - self.render_context.set_line_width(1.); + self.render_context.restore(); } pub fn fill_stroke(&mut self, vector_data: &VectorData, transform: DAffine2, color: &Color, width: Option) { + self.render_context.save(); self.render_context.set_line_width(width.unwrap_or(1.) * (transform.decompose_scale().length() * 0.75)); self.draw_path_from_vector_data(vector_data, transform); self.render_context.set_stroke_style_canvas_pattern(&self.fill_canvas_pattern(color)); self.render_context.stroke(); - self.render_context.set_line_width(1.); + self.render_context.restore(); } pub fn get_width(&self, text: &str) -> f64 { diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index ae237af3d3..c48f96db67 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -1516,6 +1516,50 @@ impl Fsm for PenToolFsmState { // The most recently placed anchor's outgoing handle (which is currently influencing the currently-being-placed segment) let handle_start = tool_data.latest_point().map(|point| transform.transform_point2(point.handle_start)); + // Display a filled overlay of the shape if the new point closes the path + if let Some(latest_point) = tool_data.latest_point() { + let handle_start = latest_point.handle_start; + let handle_end = tool_data.handle_end.unwrap_or(tool_data.next_handle_start); + let next_point = tool_data.next_point; + let start = latest_point.id; + + if let Some(layer) = layer { + let mut vector_data = document.network_interface.compute_modified_vector(layer).unwrap(); + + let closest_point = vector_data.extendable_points(preferences.vector_meshes).filter(|&id| id != start).find(|&id| { + vector_data.point_domain.position_from_id(id).map_or(false, |pos| { + let dist_sq = transform.transform_point2(pos).distance_squared(transform.transform_point2(next_point)); + dist_sq < crate::consts::SNAP_POINT_TOLERANCE.powi(2) + }) + }); + + // 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_data.push(segment_id, start, end, BezierHandles::Cubic { handle_start, handle_end }, StrokeId::ZERO); + + let grouped_segments = vector_data.auto_join_paths(); + let closed_paths = grouped_segments.iter().filter(|path| path.is_closed() && path.contains(segment_id)); + + let subpaths: Vec<_> = closed_paths + .filter_map(|path| { + let segments = path.edges.iter().filter_map(|edge| { + vector_data + .segment_domain + .iter() + .find(|(id, _, _, _)| id == &edge.id) + .map(|(_, start, end, bezier)| if start == edge.start { (bezier, start, end) } else { (bezier.reversed(), end, start) }) + }); + vector_data.subpath_from_segments_ignore_discontinuities(segments) + }) + .collect(); + + let fill_color = graphene_std::Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()).unwrap().with_alpha(0.05); + overlay_context.fill_path(subpaths.iter(), transform, &fill_color, false, None); + } + } + } + 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; @@ -1598,55 +1642,6 @@ impl Fsm for PenToolFsmState { overlay_context.manipulator_anchor(next_anchor, false, None); } - // Display a filled overlay of the shape if the new point closes the path - if let Some(latest_point) = tool_data.latest_point() { - let handle_start = latest_point.handle_start; - let handle_end = tool_data.handle_end.unwrap_or(tool_data.next_handle_start); - let next_point = tool_data.next_point; - let start = latest_point.id; - - if let Some(layer) = layer { - let mut vector_data = document.network_interface.compute_modified_vector(layer).unwrap(); - - let closest_point = vector_data.extendable_points(preferences.vector_meshes).filter(|&id| id != start).find(|&id| { - vector_data.point_domain.position_from_id(id).map_or(false, |pos| { - let dist_sq = transform.transform_point2(pos).distance_squared(transform.transform_point2(next_point)); - dist_sq < crate::consts::SNAP_POINT_TOLERANCE.powi(2) - }) - }); - - // 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_data.push(segment_id, start, end, BezierHandles::Cubic { handle_start, handle_end }, StrokeId::ZERO); - - let grouped_segments = vector_data.auto_join_paths(); - let closed_paths = grouped_segments.iter().filter(|path| path.is_closed() && path.contains(segment_id)); - - let subpaths: Vec<_> = closed_paths - .filter_map(|path| { - let segments = path.edges.iter().filter_map(|edge| { - vector_data - .segment_domain - .iter() - .find(|(id, _, _, _)| id == &edge.id) - .map(|(_, start, end, bezier)| if start == edge.start { (bezier, start, end) } else { (bezier.reversed(), end, start) }) - }); - vector_data.subpath_from_segments_ignore_discontinuities(segments) - }) - .collect(); - - let mut fill_color = graphene_std::Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()) - .unwrap() - .with_alpha(0.05) - .to_rgba_hex_srgb(); - fill_color.insert(0, '#'); - let fill_color = Color::from_rgba_str(fill_color.as_str()).unwrap(); - overlay_context.fill_path(subpaths.iter(), transform, &fill_color, false, None); - } - } - } - // Draw the overlays that visualize current snapping tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context); From b3fc8fdddeb9b86da9c5da880a4db344d26ccb79 Mon Sep 17 00:00:00 2001 From: seam0s Date: Thu, 5 Jun 2025 21:17:36 +0300 Subject: [PATCH 13/25] Add support to sync Stroke node inputs with stroke fill overlays --- .../overlays/overlays_message_handler.rs | 12 +++---- .../document/overlays/utility_types.rs | 27 ++++++++------ .../messages/tool/tool_messages/fill_tool.rs | 36 ++++++++++++------- node-graph/gcore/src/vector/style.rs | 16 +++++++++ node-graph/graph-craft/src/document/value.rs | 2 +- 5 files changed, 64 insertions(+), 29 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs b/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs index d69f288a77..38686c2bb4 100644 --- a/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs +++ b/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs @@ -58,13 +58,13 @@ impl MessageHandler> for OverlaysMessag device_pixel_ratio, visibility_settings: visibility_settings.clone(), })); - responses.add(DocumentMessage::GridOverlays(OverlayContext { - render_context: context.clone(), - size: size.as_dvec2(), - device_pixel_ratio, - visibility_settings: visibility_settings.clone(), - })); } + responses.add(DocumentMessage::GridOverlays(OverlayContext { + render_context: context.clone(), + size: size.as_dvec2(), + device_pixel_ratio, + visibility_settings: visibility_settings.clone(), + })); } } #[cfg(not(target_arch = "wasm32"))] diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index 7aee74bc7e..5aa5f0bfa5 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -11,6 +11,7 @@ use glam::{DAffine2, DVec2}; use graphene_core::Color; use graphene_core::renderer::Quad; use graphene_std::transform::Transform; +use graphene_std::vector::style::Stroke; use graphene_std::vector::{PointId, SegmentId, VectorData}; use std::collections::HashMap; use wasm_bindgen::{JsCast, JsValue}; @@ -698,33 +699,39 @@ impl OverlayContext { /// Used by the Pen tool to show the path being closed and by the Fill tool to show the area to be filled with a pattern. pub fn fill_path(&mut self, subpaths: impl Iterator>>, transform: DAffine2, color: &Color, with_pattern: bool, width_for_inner_boundary: Option) { self.render_context.save(); - self.render_context - .set_line_width(width_for_inner_boundary.unwrap_or(1.) * (transform.decompose_scale().length() * 0.75)); + let transform_scale = transform.decompose_scale().x.max(transform.decompose_scale().y); + self.render_context.set_line_width(width_for_inner_boundary.unwrap_or(1.) * transform_scale); self.draw_path_from_subpaths(subpaths, transform); if with_pattern { self.render_context.set_fill_style_canvas_pattern(&self.fill_canvas_pattern(color)); - self.render_context.set_stroke_style_str(&"#000000"); self.render_context.fill(); - // Erase the part over the stroke area + // Make the stroke transparent and erase the fill area overlapping the stroke. self.render_context.set_global_composite_operation("destination-out").expect("Failed to set global composite operation"); + self.render_context.set_stroke_style_str(&"#000000"); self.render_context.stroke(); } else { - let color_str = "#".to_owned() + color.to_rgba_hex_srgb().as_str(); - self.render_context.set_fill_style_str(&color_str); + let color_str = format!("#{:?}", color.to_rgba_hex_srgb()); + self.render_context.set_fill_style_str(&color_str.as_str()); self.render_context.fill(); } self.render_context.restore(); } - pub fn fill_stroke(&mut self, vector_data: &VectorData, transform: DAffine2, color: &Color, width: Option) { + pub fn fill_stroke(&mut self, vector_data: &VectorData, overlay_stroke: &Stroke) { self.render_context.save(); - self.render_context.set_line_width(width.unwrap_or(1.) * (transform.decompose_scale().length() * 0.75)); - self.draw_path_from_vector_data(vector_data, transform); - self.render_context.set_stroke_style_canvas_pattern(&self.fill_canvas_pattern(color)); + let transform_scale = overlay_stroke.transform.decompose_scale().x.max(overlay_stroke.transform.decompose_scale().y); + self.render_context.set_line_width(overlay_stroke.weight * transform_scale); + self.draw_path_from_vector_data(vector_data, overlay_stroke.transform); + + self.render_context + .set_stroke_style_canvas_pattern(&self.fill_canvas_pattern(&overlay_stroke.color.expect("Color should be set for fill_stroke()"))); + self.render_context.set_line_cap(overlay_stroke.line_cap.html_canvas_name().as_str()); + self.render_context.set_line_join(overlay_stroke.line_join.html_canvas_name().as_str()); + self.render_context.set_miter_limit(overlay_stroke.line_join_miter_limit); self.render_context.stroke(); self.render_context.restore(); diff --git a/editor/src/messages/tool/tool_messages/fill_tool.rs b/editor/src/messages/tool/tool_messages/fill_tool.rs index 4a805c7bf6..4d2352c149 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -1,8 +1,10 @@ use super::tool_prelude::*; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; -use crate::messages::tool::common_functionality::graph_modification_utils::{NodeGraphLayer, get_stroke_width}; +use crate::messages::tool::common_functionality::graph_modification_utils::{self, NodeGraphLayer, get_stroke_width}; +use graph_craft::document::value::TaggedValue; use graphene_core::vector::style::Fill; use graphene_std::renderer::ClickTarget; +use graphene_std::vector::style::Stroke; #[derive(Default)] pub struct FillTool { @@ -73,7 +75,7 @@ impl ToolTransition for FillTool { } } -pub fn close_to_stroke(mouse_pos: DVec2, click_target: &ClickTarget, to_viewport_transform: DAffine2) -> bool { +pub fn close_to_subpath(mouse_pos: DVec2, click_target: &ClickTarget, to_viewport_transform: DAffine2) -> bool { let subpath = click_target.subpath().clone(); let lookup = subpath.compute_lookup_table(Some(25), None); @@ -111,17 +113,27 @@ impl Fsm for FillToolFsmState { let _ = document.metadata().click_targets(layer).is_some_and(|targets| { targets .iter() - .any(|click_target| close_to_stroke(input.mouse.position, click_target, document.metadata().transform_to_viewport(layer))) + .any(|click_target| close_to_subpath(input.mouse.position, click_target, document.metadata().transform_to_viewport(layer))) .then(|| { - let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { - return; + let overlay_stroke = || { + let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, &document.network_interface); + let mut stroke = Stroke::new(Some(preview_color), get_stroke_width(layer, &document.network_interface).unwrap()); + + stroke.transform = document.metadata().transform_to_viewport(layer); + let line_cap = graph_layer.find_input("Stroke", 5).unwrap(); + stroke.line_cap = if let TaggedValue::LineCap(line_cap) = line_cap { *line_cap } else { return None }; + let line_join = graph_layer.find_input("Stroke", 6).unwrap(); + stroke.line_join = if let TaggedValue::LineJoin(line_join) = line_join { *line_join } else { return None }; + let miter_limit = graph_layer.find_input("Stroke", 7).unwrap(); + stroke.line_join_miter_limit = if let TaggedValue::F64(miter_limit) = miter_limit { *miter_limit } else { return None }; + + Some(stroke) }; - overlay_context.fill_stroke( - &vector_data, - document.metadata().transform_to_viewport(layer), - &preview_color, - get_stroke_width(layer, &document.network_interface), - ); + let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { return }; + + if let Some(stroke) = overlay_stroke() { + overlay_context.fill_stroke(&vector_data, &stroke); + } }) .or_else(|| { overlay_context.fill_path( @@ -167,7 +179,7 @@ impl Fsm for FillToolFsmState { let _ = document.metadata().click_targets(layer_identifier).is_some_and(|target| { target .iter() - .any(|click_target| close_to_stroke(input.mouse.position, click_target, document.metadata().transform_to_viewport(layer_identifier))) + .any(|click_target| close_to_subpath(input.mouse.position, click_target, document.metadata().transform_to_viewport(layer_identifier))) .then(|| { responses.add(GraphOperationMessage::StrokeColorSet { layer: layer_identifier, diff --git a/node-graph/gcore/src/vector/style.rs b/node-graph/gcore/src/vector/style.rs index ffb307749e..f1e154cccc 100644 --- a/node-graph/gcore/src/vector/style.rs +++ b/node-graph/gcore/src/vector/style.rs @@ -490,6 +490,14 @@ impl LineCap { LineCap::Square => "square", } } + + pub fn html_canvas_name(&self) -> String { + match self { + LineCap::Butt => String::from("butt"), + LineCap::Round => String::from("round"), + LineCap::Square => String::from("square"), + } + } } #[repr(C)] @@ -510,6 +518,14 @@ impl LineJoin { LineJoin::Round => "round", } } + + pub fn html_canvas_name(&self) -> String { + match self { + LineJoin::Bevel => String::from("bevel"), + LineJoin::Miter => String::from("miter"), + LineJoin::Round => String::from("round"), + } + } } fn daffine2_identity() -> DAffine2 { diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index b851392e94..751ad30a6c 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -369,7 +369,7 @@ impl TaggedValue { pub fn to_u32(&self) -> u32 { match self { TaggedValue::U32(x) => *x, - _ => panic!("Passed value is not of type u32"), + _ => panic!("Cannot convert to u32"), } } } From 4e193b9af1b92693de888b4640cf23f5f9d39f32 Mon Sep 17 00:00:00 2001 From: seam0s Date: Fri, 6 Jun 2025 13:09:14 +0300 Subject: [PATCH 14/25] Add option in Overlays popover to switch fillable indicator state --- .../document/document_message_handler.rs | 23 ++++++- .../document/overlays/utility_types.rs | 35 ++++++++--- .../messages/tool/tool_messages/fill_tool.rs | 61 ++++++++++--------- 3 files changed, 80 insertions(+), 39 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 3981cfbbbf..803797d835 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -1202,8 +1202,8 @@ impl MessageHandler> for DocumentMessag responses.add(PortfolioMessage::UpdateDocumentWidgets); } OverlaysType::Handles => visibility_settings.handles = visible, + OverlaysType::FillableIndicator => visibility_settings.fillable_indicator = visible, } - responses.add(BroadcastEvent::ToolAbort); responses.add(OverlaysMessage::Draw); } @@ -2351,6 +2351,27 @@ impl DocumentMessageHandler { ] }, }, + LayoutGroup::Row { + widgets: vec![TextLabel::new("Fill Tool").widget_holder()], + }, + LayoutGroup::Row { + widgets: { + let mut checkbox_id = CheckboxId::default(); + vec![ + CheckboxInput::new(self.overlays_visibility_settings.fillable_indicator) + .on_update(|optional_input: &CheckboxInput| { + DocumentMessage::SetOverlaysVisibility { + visible: optional_input.checked, + overlays_type: Some(OverlaysType::FillableIndicator), + } + .into() + }) + .for_label(checkbox_id.clone()) + .widget_holder(), + TextLabel::new("Fillable Indicator".to_string()).for_checkbox(&mut checkbox_id).widget_holder(), + ] + }, + }, ]) .widget_holder(), Separator::new(SeparatorType::Related).widget_holder(), diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index 5aa5f0bfa5..0437382d09 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -26,33 +26,47 @@ pub fn empty_provider() -> OverlayProvider { // Types of overlays used by DocumentMessage to enable/disable select group of overlays in the frontend #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] pub enum OverlaysType { + // ======= + // General + // ======= ArtboardName, - CompassRose, - QuickMeasurement, TransformMeasurement, + // =========== + // Select Tool + // =========== + QuickMeasurement, TransformCage, + CompassRose, + Pivot, HoverOutline, SelectionOutline, - Pivot, + // ================ + // Pen & Path Tools + // ================ Path, Anchors, Handles, + // ========= + // Fill Tool + // ========= + FillableIndicator, } #[derive(PartialEq, Copy, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] pub struct OverlaysVisibilitySettings { pub all: bool, pub artboard_name: bool, - pub compass_rose: bool, - pub quick_measurement: bool, pub transform_measurement: bool, + pub quick_measurement: bool, pub transform_cage: bool, + pub compass_rose: bool, + pub pivot: bool, pub hover_outline: bool, pub selection_outline: bool, - pub pivot: bool, pub path: bool, pub anchors: bool, pub handles: bool, + pub fillable_indicator: bool, } impl Default for OverlaysVisibilitySettings { @@ -70,6 +84,7 @@ impl Default for OverlaysVisibilitySettings { path: true, anchors: true, handles: true, + fillable_indicator: true, } } } @@ -122,6 +137,10 @@ impl OverlaysVisibilitySettings { pub fn handles(&self) -> bool { self.all && self.anchors && self.handles } + + pub fn fillable_indicator(&self) -> bool { + self.all && self.fillable_indicator + } } #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] @@ -697,10 +716,10 @@ impl OverlayContext { /// Fills the area inside the path (with an optional pattern). Assumes `color` is in gamma space. /// Used by the Pen tool to show the path being closed and by the Fill tool to show the area to be filled with a pattern. - pub fn fill_path(&mut self, subpaths: impl Iterator>>, transform: DAffine2, color: &Color, with_pattern: bool, width_for_inner_boundary: Option) { + pub fn fill_path(&mut self, subpaths: impl Iterator>>, transform: DAffine2, color: &Color, with_pattern: bool, stroke_width: Option) { self.render_context.save(); let transform_scale = transform.decompose_scale().x.max(transform.decompose_scale().y); - self.render_context.set_line_width(width_for_inner_boundary.unwrap_or(1.) * transform_scale); + self.render_context.set_line_width(stroke_width.unwrap_or(1.) * transform_scale); self.draw_path_from_subpaths(subpaths, transform); if with_pattern { diff --git a/editor/src/messages/tool/tool_messages/fill_tool.rs b/editor/src/messages/tool/tool_messages/fill_tool.rs index 4d2352c149..280c5a2ea0 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -109,44 +109,45 @@ impl Fsm for FillToolFsmState { let preview_color = if use_secondary { global_tool_data.secondary_color } else { global_tool_data.primary_color }; // Get the layer the user is hovering over + if !overlay_context.visibility_settings.fillable_indicator() { + return self; + }; if let Some(layer) = document.click(input) { - let _ = document.metadata().click_targets(layer).is_some_and(|targets| { - targets + if let Some(targets) = document.metadata().click_targets(layer) { + let close_to_stroke = targets .iter() - .any(|click_target| close_to_subpath(input.mouse.position, click_target, document.metadata().transform_to_viewport(layer))) - .then(|| { - let overlay_stroke = || { - let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, &document.network_interface); - let mut stroke = Stroke::new(Some(preview_color), get_stroke_width(layer, &document.network_interface).unwrap()); + .any(|click_target| close_to_subpath(input.mouse.position, click_target, document.metadata().transform_to_viewport(layer))); + if close_to_stroke { + let overlay_stroke = || { + let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, &document.network_interface); + let mut stroke = Stroke::new(Some(preview_color), get_stroke_width(layer, &document.network_interface).unwrap()); - stroke.transform = document.metadata().transform_to_viewport(layer); - let line_cap = graph_layer.find_input("Stroke", 5).unwrap(); - stroke.line_cap = if let TaggedValue::LineCap(line_cap) = line_cap { *line_cap } else { return None }; - let line_join = graph_layer.find_input("Stroke", 6).unwrap(); - stroke.line_join = if let TaggedValue::LineJoin(line_join) = line_join { *line_join } else { return None }; - let miter_limit = graph_layer.find_input("Stroke", 7).unwrap(); - stroke.line_join_miter_limit = if let TaggedValue::F64(miter_limit) = miter_limit { *miter_limit } else { return None }; + stroke.transform = document.metadata().transform_to_viewport(layer); + let line_cap = graph_layer.find_input("Stroke", 5).unwrap(); + stroke.line_cap = if let TaggedValue::LineCap(line_cap) = line_cap { *line_cap } else { return None }; + let line_join = graph_layer.find_input("Stroke", 6).unwrap(); + stroke.line_join = if let TaggedValue::LineJoin(line_join) = line_join { *line_join } else { return None }; + let miter_limit = graph_layer.find_input("Stroke", 7).unwrap(); + stroke.line_join_miter_limit = if let TaggedValue::F64(miter_limit) = miter_limit { *miter_limit } else { return None }; - Some(stroke) - }; - let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { return }; + Some(stroke) + }; + if let Some(vector_data) = document.network_interface.compute_modified_vector(layer) { if let Some(stroke) = overlay_stroke() { overlay_context.fill_stroke(&vector_data, &stroke); } - }) - .or_else(|| { - overlay_context.fill_path( - document.metadata().layer_outline(layer), - document.metadata().transform_to_viewport(layer), - &preview_color, - true, - get_stroke_width(layer, &document.network_interface), - ); - Some(()) - }); - true - }); + } + } else { + overlay_context.fill_path( + document.metadata().layer_outline(layer), + document.metadata().transform_to_viewport(layer), + &preview_color, + true, + get_stroke_width(layer, &document.network_interface), + ); + } + } } self From cb6fa2c87a6d4e06abd8096a4ceb848e48f958f4 Mon Sep 17 00:00:00 2001 From: seam0s Date: Sun, 8 Jun 2025 09:35:53 +0300 Subject: [PATCH 15/25] Refactor fill tool overlays to use VectorData to obtain subpaths --- .../messages/tool/tool_messages/fill_tool.rs | 66 ++++++++++--------- .../messages/tool/tool_messages/pen_tool.rs | 2 +- .../src/vector/vector_data/attributes.rs | 46 ++----------- 3 files changed, 41 insertions(+), 73 deletions(-) diff --git a/editor/src/messages/tool/tool_messages/fill_tool.rs b/editor/src/messages/tool/tool_messages/fill_tool.rs index cbaadf9bf6..3a08e4b286 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -3,7 +3,7 @@ use crate::messages::portfolio::document::overlays::utility_types::OverlayContex use crate::messages::tool::common_functionality::graph_modification_utils::{self, NodeGraphLayer, get_stroke_width}; use graph_craft::document::value::TaggedValue; use graphene_core::vector::style::Fill; -use graphene_std::renderer::ClickTarget; +use graphene_std::vector::PointId; use graphene_std::vector::style::Stroke; #[derive(Default)] @@ -75,13 +75,12 @@ impl ToolTransition for FillTool { } } -pub fn close_to_subpath(mouse_pos: DVec2, click_target: &ClickTarget, to_viewport_transform: DAffine2) -> bool { - let subpath = click_target.subpath().clone(); +pub fn close_to_subpath(mouse_pos: DVec2, subpath: bezier_rs::Subpath, stroke_width: f64, to_viewport_transform: DAffine2) -> bool { let lookup = subpath.compute_lookup_table(Some(25), None); lookup .iter() - .any(|&point| (to_viewport_transform.inverse().transform_point2(mouse_pos) - point).length() <= click_target.stroke_width()) + .any(|&point| (to_viewport_transform.inverse().transform_point2(mouse_pos) - point).length() <= stroke_width) } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] @@ -113,10 +112,17 @@ impl Fsm for FillToolFsmState { return self; }; if let Some(layer) = document.click(input) { - if let Some(targets) = document.metadata().click_targets(layer) { - let close_to_stroke = targets - .iter() - .any(|click_target| close_to_subpath(input.mouse.position, click_target, document.metadata().transform_to_viewport(layer))); + if let Some(vector_data) = document.network_interface.compute_modified_vector(layer) { + let mut subpaths = vector_data.stroke_bezier_paths(); + let close_to_stroke = subpaths.any(|subpath| { + close_to_subpath( + input.mouse.position, + subpath, + get_stroke_width(layer, &document.network_interface).unwrap_or(1.0), + document.metadata().transform_to_viewport(layer), + ) + }); + if close_to_stroke { let overlay_stroke = || { let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, &document.network_interface); @@ -133,10 +139,8 @@ impl Fsm for FillToolFsmState { Some(stroke) }; - if let Some(vector_data) = document.network_interface.compute_modified_vector(layer) { - if let Some(stroke) = overlay_stroke() { - overlay_context.fill_stroke(&vector_data, &stroke); - } + if let Some(stroke) = overlay_stroke() { + overlay_context.fill_stroke(&vector_data, &stroke); } } else { overlay_context.fill_path( @@ -149,7 +153,6 @@ impl Fsm for FillToolFsmState { } } } - self } (_, FillToolMessage::PointerMove | FillToolMessage::WorkingColorChanged) => { @@ -158,11 +161,11 @@ impl Fsm for FillToolFsmState { self } (FillToolFsmState::Ready, color_event) => { - let Some(layer_identifier) = document.click(input) else { + let Some(layer) = document.click(input) else { return self; }; // If the layer is a raster layer, don't fill it, wait till the flood fill tool is implemented - if NodeGraphLayer::is_raster_layer(layer_identifier, &mut document.network_interface) { + if NodeGraphLayer::is_raster_layer(layer, &mut document.network_interface) { return self; } let fill = match color_event { @@ -177,22 +180,23 @@ impl Fsm for FillToolFsmState { }; responses.add(DocumentMessage::AddTransaction); - let _ = document.metadata().click_targets(layer_identifier).is_some_and(|target| { - target - .iter() - .any(|click_target| close_to_subpath(input.mouse.position, click_target, document.metadata().transform_to_viewport(layer_identifier))) - .then(|| { - responses.add(GraphOperationMessage::StrokeColorSet { - layer: layer_identifier, - stroke_color, - }); - }) - .or_else(|| { - responses.add(GraphOperationMessage::FillSet { layer: layer_identifier, fill }); - Some(()) - }); - true - }); + if let Some(vector_data) = document.network_interface.compute_modified_vector(layer) { + let mut subpaths = vector_data.stroke_bezier_paths(); + let close_to_stroke = subpaths.any(|subpath| { + close_to_subpath( + input.mouse.position, + subpath, + get_stroke_width(layer, &document.network_interface).unwrap_or(1.0), + document.metadata().transform_to_viewport(layer), + ) + }); + + if close_to_stroke { + responses.add(GraphOperationMessage::StrokeColorSet { layer, stroke_color }); + } else { + responses.add(GraphOperationMessage::FillSet { layer, fill }); + } + } FillToolFsmState::Filling } (FillToolFsmState::Filling, FillToolMessage::PointerUp) => FillToolFsmState::Ready, diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index c48f96db67..c6661339ba 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -1550,7 +1550,7 @@ impl Fsm for PenToolFsmState { .find(|(id, _, _, _)| id == &edge.id) .map(|(_, start, end, bezier)| if start == edge.start { (bezier, start, end) } else { (bezier.reversed(), end, start) }) }); - vector_data.subpath_from_segments_ignore_discontinuities(segments) + vector_data.subpath_from_segments(segments, true) }) .collect(); diff --git a/node-graph/gcore/src/vector/vector_data/attributes.rs b/node-graph/gcore/src/vector/vector_data/attributes.rs index d5e1fe0241..a8d7cd6459 100644 --- a/node-graph/gcore/src/vector/vector_data/attributes.rs +++ b/node-graph/gcore/src/vector/vector_data/attributes.rs @@ -751,51 +751,15 @@ impl VectorData { } } - /// Construct a [`bezier_rs::Bezier`] curve from an iterator of segments with (handles, start point, end point) independently of discontinuities. - pub fn subpath_from_segments_ignore_discontinuities(&self, segments: impl Iterator) -> Option> { + /// Construct a [`bezier_rs::Bezier`] curve from an iterator of segments with (handles, start point, end point), optionally ignoring discontinuities. + /// Returns None if any ids are invalid or if the segments are not continuous. + pub fn subpath_from_segments(&self, segments: impl Iterator, ignore_discontinuities: bool) -> Option> { let mut first_point = None; let mut groups = Vec::new(); let mut last: Option<(usize, bezier_rs::BezierHandles)> = None; for (handle, start, end) in segments { - first_point = Some(first_point.unwrap_or(start)); - - groups.push(bezier_rs::ManipulatorGroup { - anchor: self.point_domain.positions()[start], - in_handle: last.and_then(|(_, handle)| handle.end()), - out_handle: handle.start(), - id: self.point_domain.ids()[start], - }); - - last = Some((end, handle)); - } - - let closed = groups.len() > 1 && last.map(|(point, _)| point) == first_point; - - if let Some((end, last_handle)) = last { - if closed { - groups[0].in_handle = last_handle.end(); - } else { - groups.push(bezier_rs::ManipulatorGroup { - anchor: self.point_domain.positions()[end], - in_handle: last_handle.end(), - out_handle: None, - id: self.point_domain.ids()[end], - }); - } - } - - Some(bezier_rs::Subpath::new(groups, closed)) - } - - /// Construct a [`bezier_rs::Bezier`] curve from an iterator of segments with (handles, start point, end point). Returns None if any ids are invalid or if the segments are not continuous. - fn subpath_from_segments(&self, segments: impl Iterator) -> Option> { - let mut first_point = None; - let mut groups = Vec::new(); - let mut last: Option<(usize, bezier_rs::BezierHandles)> = None; - - for (handle, start, end) in segments { - if last.is_some_and(|(previous_end, _)| previous_end != start) { + if !ignore_discontinuities && last.is_some_and(|(previous_end, _)| previous_end != start) { warn!("subpath_from_segments that were not continuous"); return None; } @@ -845,7 +809,7 @@ impl VectorData { .zip(self.segment_domain.end_point.get(range)?) .map(|((&handles, &start), &end)| (handles, start, end)); - self.subpath_from_segments(segments_iter).map(|subpath| (id, subpath)) + self.subpath_from_segments(segments_iter, false).map(|subpath| (id, subpath)) }) } From 7f709528729ee0537155664762c5efc89395ebd8 Mon Sep 17 00:00:00 2001 From: seam0s Date: Sun, 8 Jun 2025 13:15:15 +0300 Subject: [PATCH 16/25] Add logic to check for the presence of Stroke node --- .../document/overlays/utility_types.rs | 1 + .../messages/tool/tool_messages/fill_tool.rs | 38 ++++++++++--------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index 0437382d09..ff9f56f718 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -66,6 +66,7 @@ pub struct OverlaysVisibilitySettings { pub path: bool, pub anchors: bool, pub handles: bool, + #[serde(default)] pub fillable_indicator: bool, } diff --git a/editor/src/messages/tool/tool_messages/fill_tool.rs b/editor/src/messages/tool/tool_messages/fill_tool.rs index 3a08e4b286..46311b03fa 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -114,20 +114,20 @@ impl Fsm for FillToolFsmState { if let Some(layer) = document.click(input) { if let Some(vector_data) = document.network_interface.compute_modified_vector(layer) { let mut subpaths = vector_data.stroke_bezier_paths(); - let close_to_stroke = subpaths.any(|subpath| { - close_to_subpath( - input.mouse.position, - subpath, - get_stroke_width(layer, &document.network_interface).unwrap_or(1.0), - document.metadata().transform_to_viewport(layer), - ) - }); + let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, &document.network_interface); + let close_to_stroke = graph_layer.upstream_node_id_from_name("Stroke").is_some() + && subpaths.any(|subpath| { + close_to_subpath( + input.mouse.position, + subpath, + get_stroke_width(layer, &document.network_interface).unwrap_or(1.0), + document.metadata().transform_to_viewport(layer), + ) + }); if close_to_stroke { let overlay_stroke = || { - let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, &document.network_interface); let mut stroke = Stroke::new(Some(preview_color), get_stroke_width(layer, &document.network_interface).unwrap()); - stroke.transform = document.metadata().transform_to_viewport(layer); let line_cap = graph_layer.find_input("Stroke", 5).unwrap(); stroke.line_cap = if let TaggedValue::LineCap(line_cap) = line_cap { *line_cap } else { return None }; @@ -182,14 +182,16 @@ impl Fsm for FillToolFsmState { responses.add(DocumentMessage::AddTransaction); if let Some(vector_data) = document.network_interface.compute_modified_vector(layer) { let mut subpaths = vector_data.stroke_bezier_paths(); - let close_to_stroke = subpaths.any(|subpath| { - close_to_subpath( - input.mouse.position, - subpath, - get_stroke_width(layer, &document.network_interface).unwrap_or(1.0), - document.metadata().transform_to_viewport(layer), - ) - }); + let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, &document.network_interface); + let close_to_stroke = graph_layer.upstream_node_id_from_name("Stroke").is_some() + && subpaths.any(|subpath| { + close_to_subpath( + input.mouse.position, + subpath, + get_stroke_width(layer, &document.network_interface).unwrap_or(1.0), + document.metadata().transform_to_viewport(layer), + ) + }); if close_to_stroke { responses.add(GraphOperationMessage::StrokeColorSet { layer, stroke_color }); From 3716236bc7e14c8572cfa3f57e05cdbdf0131536 Mon Sep 17 00:00:00 2001 From: seam0s Date: Sun, 8 Jun 2025 21:06:46 +0300 Subject: [PATCH 17/25] Refine stroke detection threshold --- editor/src/messages/tool/tool_messages/fill_tool.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/editor/src/messages/tool/tool_messages/fill_tool.rs b/editor/src/messages/tool/tool_messages/fill_tool.rs index 46311b03fa..08f2ec53ad 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -3,6 +3,7 @@ use crate::messages::portfolio::document::overlays::utility_types::OverlayContex use crate::messages::tool::common_functionality::graph_modification_utils::{self, NodeGraphLayer, get_stroke_width}; use graph_craft::document::value::TaggedValue; use graphene_core::vector::style::Fill; +use graphene_std::transform::Transform; use graphene_std::vector::PointId; use graphene_std::vector::style::Stroke; @@ -77,6 +78,9 @@ impl ToolTransition for FillTool { pub fn close_to_subpath(mouse_pos: DVec2, subpath: bezier_rs::Subpath, stroke_width: f64, to_viewport_transform: DAffine2) -> bool { let lookup = subpath.compute_lookup_table(Some(25), None); + // TODO: This solution is not ideal, it introduces hysteresis at certain zoom levels when hovering over a stroke. + let transform_scale = to_viewport_transform.decompose_scale().x.max(to_viewport_transform.decompose_scale().y); + let stroke_width = stroke_width * transform_scale * 1.2; lookup .iter() From cc54f0f56e2f7117eeb7bc00a4da57752a5c0d11 Mon Sep 17 00:00:00 2001 From: seam0s Date: Sun, 8 Jun 2025 22:26:42 +0300 Subject: [PATCH 18/25] Add serde default to OverlaysVisibilitySettings --- .../src/messages/portfolio/document/overlays/utility_types.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index ff9f56f718..e868291de6 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -53,6 +53,7 @@ pub enum OverlaysType { } #[derive(PartialEq, Copy, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] +#[serde(default = "OverlaysVisibilitySettings::default")] pub struct OverlaysVisibilitySettings { pub all: bool, pub artboard_name: bool, @@ -66,7 +67,6 @@ pub struct OverlaysVisibilitySettings { pub path: bool, pub anchors: bool, pub handles: bool, - #[serde(default)] pub fillable_indicator: bool, } From 255f5e8abccd2a9d934db8e28c895c9f1e2910a0 Mon Sep 17 00:00:00 2001 From: seam0s Date: Mon, 9 Jun 2025 00:27:47 +0300 Subject: [PATCH 19/25] Revert changes on stroke detection threshold --- editor/src/messages/tool/tool_messages/fill_tool.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/editor/src/messages/tool/tool_messages/fill_tool.rs b/editor/src/messages/tool/tool_messages/fill_tool.rs index 08f2ec53ad..46311b03fa 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -3,7 +3,6 @@ use crate::messages::portfolio::document::overlays::utility_types::OverlayContex use crate::messages::tool::common_functionality::graph_modification_utils::{self, NodeGraphLayer, get_stroke_width}; use graph_craft::document::value::TaggedValue; use graphene_core::vector::style::Fill; -use graphene_std::transform::Transform; use graphene_std::vector::PointId; use graphene_std::vector::style::Stroke; @@ -78,9 +77,6 @@ impl ToolTransition for FillTool { pub fn close_to_subpath(mouse_pos: DVec2, subpath: bezier_rs::Subpath, stroke_width: f64, to_viewport_transform: DAffine2) -> bool { let lookup = subpath.compute_lookup_table(Some(25), None); - // TODO: This solution is not ideal, it introduces hysteresis at certain zoom levels when hovering over a stroke. - let transform_scale = to_viewport_transform.decompose_scale().x.max(to_viewport_transform.decompose_scale().y); - let stroke_width = stroke_width * transform_scale * 1.2; lookup .iter() From 1df00b252f6b083bbbb057c19d06fcf15d9d402a Mon Sep 17 00:00:00 2001 From: Mateo <45690579+c-mateo@users.noreply.github.com> Date: Mon, 9 Jun 2025 01:36:19 -0300 Subject: [PATCH 20/25] Use better approach to fill strokes --- .../portfolio/document/overlays/utility_types.rs | 2 +- editor/src/messages/tool/tool_messages/fill_tool.rs | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index 74022b71b8..494e21e699 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -732,7 +732,7 @@ impl OverlayContext { pub fn fill_path(&mut self, subpaths: impl Iterator>>, transform: DAffine2, color: &Color, with_pattern: bool, stroke_width: Option) { self.render_context.save(); let transform_scale = transform.decompose_scale().x.max(transform.decompose_scale().y); - self.render_context.set_line_width(stroke_width.unwrap_or(1.) * transform_scale); + self.render_context.set_line_width(stroke_width.unwrap_or(1.) * transform_scale + 2.0); self.draw_path_from_subpaths(subpaths, transform); if with_pattern { diff --git a/editor/src/messages/tool/tool_messages/fill_tool.rs b/editor/src/messages/tool/tool_messages/fill_tool.rs index 46311b03fa..dd1d45d028 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -76,11 +76,15 @@ impl ToolTransition for FillTool { } pub fn close_to_subpath(mouse_pos: DVec2, subpath: bezier_rs::Subpath, stroke_width: f64, to_viewport_transform: DAffine2) -> bool { - let lookup = subpath.compute_lookup_table(Some(25), None); + let stroke_width = stroke_width + 1.0; + let mouse_pos = to_viewport_transform.inverse().transform_point2(mouse_pos); - lookup - .iter() - .any(|&point| (to_viewport_transform.inverse().transform_point2(mouse_pos) - point).length() <= stroke_width) + if let Some((segment_index, t)) = subpath.project(mouse_pos) { + let nearest_point = subpath.evaluate(bezier_rs::SubpathTValue::Parametric { segment_index, t }); + (mouse_pos - nearest_point).length() <= stroke_width + } else { + false + } } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] From de70263ddccb01bf1fcbba5e11cc5fd26752a017 Mon Sep 17 00:00:00 2001 From: Mateo <45690579+c-mateo@users.noreply.github.com> Date: Mon, 9 Jun 2025 16:14:08 -0300 Subject: [PATCH 21/25] Limit the stroke distance to half width --- .../messages/portfolio/document/overlays/utility_types.rs | 2 +- editor/src/messages/tool/tool_messages/fill_tool.rs | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index 494e21e699..74022b71b8 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -732,7 +732,7 @@ impl OverlayContext { pub fn fill_path(&mut self, subpaths: impl Iterator>>, transform: DAffine2, color: &Color, with_pattern: bool, stroke_width: Option) { self.render_context.save(); let transform_scale = transform.decompose_scale().x.max(transform.decompose_scale().y); - self.render_context.set_line_width(stroke_width.unwrap_or(1.) * transform_scale + 2.0); + self.render_context.set_line_width(stroke_width.unwrap_or(1.) * transform_scale); self.draw_path_from_subpaths(subpaths, transform); if with_pattern { diff --git a/editor/src/messages/tool/tool_messages/fill_tool.rs b/editor/src/messages/tool/tool_messages/fill_tool.rs index dd1d45d028..3424f8e7d6 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -3,6 +3,7 @@ use crate::messages::portfolio::document::overlays::utility_types::OverlayContex use crate::messages::tool::common_functionality::graph_modification_utils::{self, NodeGraphLayer, get_stroke_width}; use graph_craft::document::value::TaggedValue; use graphene_core::vector::style::Fill; +use graphene_std::transform::Transform; use graphene_std::vector::PointId; use graphene_std::vector::style::Stroke; @@ -76,12 +77,14 @@ impl ToolTransition for FillTool { } pub fn close_to_subpath(mouse_pos: DVec2, subpath: bezier_rs::Subpath, stroke_width: f64, to_viewport_transform: DAffine2) -> bool { - let stroke_width = stroke_width + 1.0; let mouse_pos = to_viewport_transform.inverse().transform_point2(mouse_pos); + let transform_scale = to_viewport_transform.decompose_scale().x.max(to_viewport_transform.decompose_scale().y); + let threshold = (2.0 - transform_scale).exp2(); + let max_stroke_distance = stroke_width * 0.5 + threshold; if let Some((segment_index, t)) = subpath.project(mouse_pos) { let nearest_point = subpath.evaluate(bezier_rs::SubpathTValue::Parametric { segment_index, t }); - (mouse_pos - nearest_point).length() <= stroke_width + (mouse_pos - nearest_point).length() <= max_stroke_distance } else { false } From 0e535b116b8ce0d1f4b3ab2fbfa140b8197fde03 Mon Sep 17 00:00:00 2001 From: seam0s Date: Wed, 11 Jun 2025 20:18:16 +0300 Subject: [PATCH 22/25] Temporary fix to adjust stroke overlays on shapes with multiple upstream transforms --- .../document/overlays/utility_types.rs | 15 ++++++--- .../messages/tool/tool_messages/fill_tool.rs | 31 ++++++++++++++++--- .../messages/tool/tool_messages/pen_tool.rs | 2 +- 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index 74022b71b8..27d95ddc54 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -11,7 +11,6 @@ use glam::{DAffine2, DVec2}; use graphene_core::Color; use graphene_core::renderer::Quad; use graphene_std::renderer::ClickTargetType; -use graphene_std::transform::Transform; use graphene_std::vector::style::Stroke; use graphene_std::vector::{PointId, SegmentId, VectorData}; use std::collections::HashMap; @@ -729,9 +728,16 @@ impl OverlayContext { /// Fills the area inside the path (with an optional pattern). Assumes `color` is in gamma space. /// Used by the Pen tool to show the path being closed and by the Fill tool to show the area to be filled with a pattern. - pub fn fill_path(&mut self, subpaths: impl Iterator>>, transform: DAffine2, color: &Color, with_pattern: bool, stroke_width: Option) { + pub fn fill_path( + &mut self, + subpaths: impl Iterator>>, + transform: DAffine2, + transform_scale: f64, + color: &Color, + with_pattern: bool, + stroke_width: Option, + ) { self.render_context.save(); - let transform_scale = transform.decompose_scale().x.max(transform.decompose_scale().y); self.render_context.set_line_width(stroke_width.unwrap_or(1.) * transform_scale); self.draw_path_from_subpaths(subpaths, transform); @@ -752,10 +758,9 @@ impl OverlayContext { self.render_context.restore(); } - pub fn fill_stroke(&mut self, vector_data: &VectorData, overlay_stroke: &Stroke) { + pub fn fill_stroke(&mut self, vector_data: &VectorData, overlay_stroke: &Stroke, transform_scale: f64) { self.render_context.save(); - let transform_scale = overlay_stroke.transform.decompose_scale().x.max(overlay_stroke.transform.decompose_scale().y); self.render_context.set_line_width(overlay_stroke.weight * transform_scale); self.draw_path_from_vector_data(vector_data, overlay_stroke.transform); diff --git a/editor/src/messages/tool/tool_messages/fill_tool.rs b/editor/src/messages/tool/tool_messages/fill_tool.rs index 3424f8e7d6..f97fa4a7d8 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -76,9 +76,15 @@ impl ToolTransition for FillTool { } } -pub fn close_to_subpath(mouse_pos: DVec2, subpath: bezier_rs::Subpath, stroke_width: f64, to_viewport_transform: DAffine2) -> bool { - let mouse_pos = to_viewport_transform.inverse().transform_point2(mouse_pos); - let transform_scale = to_viewport_transform.decompose_scale().x.max(to_viewport_transform.decompose_scale().y); +pub fn close_to_subpath( + mouse_pos: DVec2, + subpath: bezier_rs::Subpath, + stroke_width: f64, + layer_to_viewport_transform: DAffine2, + // downstream_layer_to_viewport_transform: DAffine2, + transform_scale: f64, +) -> bool { + let mouse_pos = layer_to_viewport_transform.inverse().transform_point2(mouse_pos); let threshold = (2.0 - transform_scale).exp2(); let max_stroke_distance = stroke_width * 0.5 + threshold; @@ -119,6 +125,13 @@ impl Fsm for FillToolFsmState { return self; }; if let Some(layer) = document.click(input) { + let transform_scale = document + .metadata() + .downstream_transform_to_viewport(layer) + .decompose_scale() + .x + .max(document.metadata().downstream_transform_to_viewport(layer).decompose_scale().y); + if let Some(vector_data) = document.network_interface.compute_modified_vector(layer) { let mut subpaths = vector_data.stroke_bezier_paths(); let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, &document.network_interface); @@ -129,9 +142,9 @@ impl Fsm for FillToolFsmState { subpath, get_stroke_width(layer, &document.network_interface).unwrap_or(1.0), document.metadata().transform_to_viewport(layer), + transform_scale, ) }); - if close_to_stroke { let overlay_stroke = || { let mut stroke = Stroke::new(Some(preview_color), get_stroke_width(layer, &document.network_interface).unwrap()); @@ -147,12 +160,13 @@ impl Fsm for FillToolFsmState { }; if let Some(stroke) = overlay_stroke() { - overlay_context.fill_stroke(&vector_data, &stroke); + overlay_context.fill_stroke(&vector_data, &stroke, transform_scale); } } else { overlay_context.fill_path( document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer), + transform_scale, &preview_color, true, get_stroke_width(layer, &document.network_interface), @@ -185,6 +199,12 @@ impl Fsm for FillToolFsmState { FillToolMessage::FillSecondaryColor => global_tool_data.secondary_color.to_gamma_srgb(), _ => return self, }; + let transform_scale = document + .metadata() + .downstream_transform_to_viewport(layer) + .decompose_scale() + .x + .max(document.metadata().downstream_transform_to_viewport(layer).decompose_scale().y); responses.add(DocumentMessage::AddTransaction); if let Some(vector_data) = document.network_interface.compute_modified_vector(layer) { @@ -197,6 +217,7 @@ impl Fsm for FillToolFsmState { subpath, get_stroke_width(layer, &document.network_interface).unwrap_or(1.0), document.metadata().transform_to_viewport(layer), + transform_scale, ) }); diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index c6661339ba..e880c221e8 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -1555,7 +1555,7 @@ impl Fsm for PenToolFsmState { .collect(); let fill_color = graphene_std::Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()).unwrap().with_alpha(0.05); - overlay_context.fill_path(subpaths.iter(), transform, &fill_color, false, None); + overlay_context.fill_path(subpaths.iter(), transform, 1., &fill_color, false, None); } } } From a6b0f524be483fab186ce804299ff34b7e9a7f1a Mon Sep 17 00:00:00 2001 From: seam0s Date: Sun, 15 Jun 2025 19:21:18 +0300 Subject: [PATCH 23/25] Replace transform_scale for PTZ zoom and add checks for node visibility while showing overlays --- .../document/overlays/utility_types.rs | 17 +--- .../messages/tool/tool_messages/fill_tool.rs | 91 ++++++++----------- .../messages/tool/tool_messages/pen_tool.rs | 2 +- 3 files changed, 43 insertions(+), 67 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index 27d95ddc54..1afae174d0 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -728,17 +728,9 @@ impl OverlayContext { /// Fills the area inside the path (with an optional pattern). Assumes `color` is in gamma space. /// Used by the Pen tool to show the path being closed and by the Fill tool to show the area to be filled with a pattern. - pub fn fill_path( - &mut self, - subpaths: impl Iterator>>, - transform: DAffine2, - transform_scale: f64, - color: &Color, - with_pattern: bool, - stroke_width: Option, - ) { + pub fn fill_path(&mut self, subpaths: impl Iterator>>, transform: DAffine2, color: &Color, with_pattern: bool, stroke_width: Option) { self.render_context.save(); - self.render_context.set_line_width(stroke_width.unwrap_or(1.) * transform_scale); + self.render_context.set_line_width(stroke_width.unwrap_or(1.)); self.draw_path_from_subpaths(subpaths, transform); if with_pattern { @@ -758,10 +750,11 @@ impl OverlayContext { self.render_context.restore(); } - pub fn fill_stroke(&mut self, vector_data: &VectorData, overlay_stroke: &Stroke, transform_scale: f64) { + pub fn fill_stroke(&mut self, vector_data: &VectorData, overlay_stroke: &Stroke) { self.render_context.save(); - self.render_context.set_line_width(overlay_stroke.weight * transform_scale); + // debug!("overlay_stroke.weight * ptz.zoom(): {:?}", overlay_stroke.weight); + self.render_context.set_line_width(overlay_stroke.weight); self.draw_path_from_vector_data(vector_data, overlay_stroke.transform); self.render_context diff --git a/editor/src/messages/tool/tool_messages/fill_tool.rs b/editor/src/messages/tool/tool_messages/fill_tool.rs index f97fa4a7d8..a8849e3716 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -3,7 +3,6 @@ use crate::messages::portfolio::document::overlays::utility_types::OverlayContex use crate::messages::tool::common_functionality::graph_modification_utils::{self, NodeGraphLayer, get_stroke_width}; use graph_craft::document::value::TaggedValue; use graphene_core::vector::style::Fill; -use graphene_std::transform::Transform; use graphene_std::vector::PointId; use graphene_std::vector::style::Stroke; @@ -76,21 +75,15 @@ impl ToolTransition for FillTool { } } -pub fn close_to_subpath( - mouse_pos: DVec2, - subpath: bezier_rs::Subpath, - stroke_width: f64, - layer_to_viewport_transform: DAffine2, - // downstream_layer_to_viewport_transform: DAffine2, - transform_scale: f64, -) -> bool { +pub fn close_to_subpath(mouse_pos: DVec2, subpath: bezier_rs::Subpath, stroke_width: f64, layer_to_viewport_transform: DAffine2) -> bool { let mouse_pos = layer_to_viewport_transform.inverse().transform_point2(mouse_pos); - let threshold = (2.0 - transform_scale).exp2(); - let max_stroke_distance = stroke_width * 0.5 + threshold; + let max_stroke_distance = stroke_width; if let Some((segment_index, t)) = subpath.project(mouse_pos) { let nearest_point = subpath.evaluate(bezier_rs::SubpathTValue::Parametric { segment_index, t }); - (mouse_pos - nearest_point).length() <= max_stroke_distance + // debug!("max_stroke_distance: {max_stroke_distance}"); + // debug!("mouse-stroke distance: {:?}", (mouse_pos - nearest_point).length()); + (mouse_pos - nearest_point).length_squared() <= max_stroke_distance } else { false } @@ -120,34 +113,30 @@ impl Fsm for FillToolFsmState { 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 }; - // Get the layer the user is hovering over if !overlay_context.visibility_settings.fillable_indicator() { return self; }; + // Get the layer the user is hovering over if let Some(layer) = document.click(input) { - let transform_scale = document - .metadata() - .downstream_transform_to_viewport(layer) - .decompose_scale() - .x - .max(document.metadata().downstream_transform_to_viewport(layer).decompose_scale().y); - if let Some(vector_data) = document.network_interface.compute_modified_vector(layer) { let mut subpaths = vector_data.stroke_bezier_paths(); let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, &document.network_interface); - let close_to_stroke = graph_layer.upstream_node_id_from_name("Stroke").is_some() - && subpaths.any(|subpath| { - close_to_subpath( - input.mouse.position, - subpath, - get_stroke_width(layer, &document.network_interface).unwrap_or(1.0), - document.metadata().transform_to_viewport(layer), - transform_scale, - ) - }); - if close_to_stroke { + + // Stroke + let stroke_node = graph_layer.upstream_node_id_from_name("Stroke"); + let stroke_width = get_stroke_width(layer, &document.network_interface).unwrap_or(1.0); + let zoom = document.document_ptz.zoom(); + let modified_stroke_width = stroke_width * zoom; + let stroke_exists_and_visible = stroke_node.is_some_and(|stroke| document.network_interface.is_visible(&stroke, &[])); + let close_to_stroke = subpaths.any(|subpath| close_to_subpath(input.mouse.position, subpath, stroke_width, document.metadata().transform_to_viewport(layer))); + + // Fill + let fill_node = graph_layer.upstream_node_id_from_name("Fill"); + let fill_exists_and_visible = fill_node.is_some_and(|fill| document.network_interface.is_visible(&fill, &[])); + + if stroke_exists_and_visible && close_to_stroke { let overlay_stroke = || { - let mut stroke = Stroke::new(Some(preview_color), get_stroke_width(layer, &document.network_interface).unwrap()); + let mut stroke = Stroke::new(Some(preview_color), modified_stroke_width); stroke.transform = document.metadata().transform_to_viewport(layer); let line_cap = graph_layer.find_input("Stroke", 5).unwrap(); stroke.line_cap = if let TaggedValue::LineCap(line_cap) = line_cap { *line_cap } else { return None }; @@ -160,16 +149,15 @@ impl Fsm for FillToolFsmState { }; if let Some(stroke) = overlay_stroke() { - overlay_context.fill_stroke(&vector_data, &stroke, transform_scale); + overlay_context.fill_stroke(&vector_data, &stroke); } - } else { + } else if fill_exists_and_visible { overlay_context.fill_path( document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer), - transform_scale, &preview_color, true, - get_stroke_width(layer, &document.network_interface), + Some(modified_stroke_width), ); } } @@ -182,6 +170,7 @@ impl Fsm for FillToolFsmState { self } (FillToolFsmState::Ready, color_event) => { + // Get the layer the user is hovering over let Some(layer) = document.click(input) else { return self; }; @@ -199,31 +188,25 @@ impl Fsm for FillToolFsmState { FillToolMessage::FillSecondaryColor => global_tool_data.secondary_color.to_gamma_srgb(), _ => return self, }; - let transform_scale = document - .metadata() - .downstream_transform_to_viewport(layer) - .decompose_scale() - .x - .max(document.metadata().downstream_transform_to_viewport(layer).decompose_scale().y); responses.add(DocumentMessage::AddTransaction); if let Some(vector_data) = document.network_interface.compute_modified_vector(layer) { let mut subpaths = vector_data.stroke_bezier_paths(); let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, &document.network_interface); - let close_to_stroke = graph_layer.upstream_node_id_from_name("Stroke").is_some() - && subpaths.any(|subpath| { - close_to_subpath( - input.mouse.position, - subpath, - get_stroke_width(layer, &document.network_interface).unwrap_or(1.0), - document.metadata().transform_to_viewport(layer), - transform_scale, - ) - }); - if close_to_stroke { + // Stroke + let stroke_node = graph_layer.upstream_node_id_from_name("Stroke"); + let stroke_width = get_stroke_width(layer, &document.network_interface).unwrap_or(1.0); + let stroke_exists_and_visible = stroke_node.is_some_and(|stroke| document.network_interface.is_visible(&stroke, &[])); + let close_to_stroke = subpaths.any(|subpath| close_to_subpath(input.mouse.position, subpath, stroke_width, document.metadata().transform_to_viewport(layer))); + + // Fill + let fill_node = graph_layer.upstream_node_id_from_name("Fill"); + let fill_exists_and_visible = fill_node.is_some_and(|fill| document.network_interface.is_visible(&fill, &[])); + + if stroke_exists_and_visible && close_to_stroke { responses.add(GraphOperationMessage::StrokeColorSet { layer, stroke_color }); - } else { + } else if fill_exists_and_visible { responses.add(GraphOperationMessage::FillSet { layer, fill }); } } diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index e880c221e8..c6661339ba 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -1555,7 +1555,7 @@ impl Fsm for PenToolFsmState { .collect(); let fill_color = graphene_std::Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()).unwrap().with_alpha(0.05); - overlay_context.fill_path(subpaths.iter(), transform, 1., &fill_color, false, None); + overlay_context.fill_path(subpaths.iter(), transform, &fill_color, false, None); } } } From d8e5691aa80c6f2aeb9da9d431e53a1512f1e216 Mon Sep 17 00:00:00 2001 From: seam0s Date: Sun, 15 Jun 2025 23:21:18 +0300 Subject: [PATCH 24/25] Send fill_stroke subpaths instead of vector_data --- .../portfolio/document/overlays/utility_types.rs | 4 ++-- editor/src/messages/tool/tool_messages/fill_tool.rs | 12 ++++-------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index 1afae174d0..60cbefe3f7 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -750,12 +750,12 @@ impl OverlayContext { self.render_context.restore(); } - pub fn fill_stroke(&mut self, vector_data: &VectorData, overlay_stroke: &Stroke) { + pub fn fill_stroke(&mut self, subpaths: impl Iterator>>, overlay_stroke: &Stroke) { self.render_context.save(); // debug!("overlay_stroke.weight * ptz.zoom(): {:?}", overlay_stroke.weight); self.render_context.set_line_width(overlay_stroke.weight); - self.draw_path_from_vector_data(vector_data, overlay_stroke.transform); + self.draw_path_from_subpaths(subpaths, overlay_stroke.transform); self.render_context .set_stroke_style_canvas_pattern(&self.fill_canvas_pattern(&overlay_stroke.color.expect("Color should be set for fill_stroke()"))); diff --git a/editor/src/messages/tool/tool_messages/fill_tool.rs b/editor/src/messages/tool/tool_messages/fill_tool.rs index a8849e3716..2d5561f51e 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -149,16 +149,12 @@ impl Fsm for FillToolFsmState { }; if let Some(stroke) = overlay_stroke() { - overlay_context.fill_stroke(&vector_data, &stroke); + subpaths = vector_data.stroke_bezier_paths(); + overlay_context.fill_stroke(subpaths, &stroke); } } else if fill_exists_and_visible { - overlay_context.fill_path( - document.metadata().layer_outline(layer), - document.metadata().transform_to_viewport(layer), - &preview_color, - true, - Some(modified_stroke_width), - ); + subpaths = vector_data.stroke_bezier_paths(); + overlay_context.fill_path(subpaths, document.metadata().transform_to_viewport(layer), &preview_color, true, Some(modified_stroke_width)); } } } From e14e6e143828e19d98ed5b5212f9ea7dce339c3e Mon Sep 17 00:00:00 2001 From: Mateo <45690579+c-mateo@users.noreply.github.com> Date: Mon, 16 Jun 2025 20:47:45 -0300 Subject: [PATCH 25/25] Erase the fill area overlapping the stroke only if the stroke is shown --- .../portfolio/document/overlays/utility_types.rs | 15 ++++++++------- .../src/messages/tool/tool_messages/fill_tool.rs | 2 +- .../src/messages/tool/tool_messages/pen_tool.rs | 4 ++-- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index 60cbefe3f7..388343ba62 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -728,23 +728,24 @@ impl OverlayContext { /// Fills the area inside the path (with an optional pattern). Assumes `color` is in gamma space. /// Used by the Pen tool to show the path being closed and by the Fill tool to show the area to be filled with a pattern. - pub fn fill_path(&mut self, subpaths: impl Iterator>>, transform: DAffine2, color: &Color, with_pattern: bool, stroke_width: Option) { + pub fn fill_path(&mut self, subpaths: impl Iterator>>, transform: DAffine2, color: &Color, with_pattern: bool, clear_stroke_part: bool, stroke_width: Option) { self.render_context.save(); self.render_context.set_line_width(stroke_width.unwrap_or(1.)); self.draw_path_from_subpaths(subpaths, transform); if with_pattern { self.render_context.set_fill_style_canvas_pattern(&self.fill_canvas_pattern(color)); - self.render_context.fill(); + } else { + let color_str = format!("#{:?}", color.to_rgba_hex_srgb()); + self.render_context.set_fill_style_str(&color_str.as_str()); + } + self.render_context.fill(); - // Make the stroke transparent and erase the fill area overlapping the stroke. + // Make the stroke transparent and erase the fill area overlapping the stroke. + if clear_stroke_part { self.render_context.set_global_composite_operation("destination-out").expect("Failed to set global composite operation"); self.render_context.set_stroke_style_str(&"#000000"); self.render_context.stroke(); - } else { - let color_str = format!("#{:?}", color.to_rgba_hex_srgb()); - self.render_context.set_fill_style_str(&color_str.as_str()); - self.render_context.fill(); } self.render_context.restore(); diff --git a/editor/src/messages/tool/tool_messages/fill_tool.rs b/editor/src/messages/tool/tool_messages/fill_tool.rs index 2d5561f51e..8b13c94173 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -154,7 +154,7 @@ impl Fsm for FillToolFsmState { } } else if fill_exists_and_visible { subpaths = vector_data.stroke_bezier_paths(); - overlay_context.fill_path(subpaths, document.metadata().transform_to_viewport(layer), &preview_color, true, Some(modified_stroke_width)); + overlay_context.fill_path(subpaths, document.metadata().transform_to_viewport(layer), &preview_color, true, stroke_exists_and_visible, Some(modified_stroke_width)); } } } diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index cb167733e1..c76dd79a8e 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -1640,7 +1640,7 @@ impl Fsm for PenToolFsmState { .collect(); let fill_color = graphene_std::Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()).unwrap().with_alpha(0.05); - overlay_context.fill_path(subpaths.iter(), transform, &fill_color, false, None); + overlay_context.fill_path(subpaths.iter(), transform, &fill_color, false, false, None); } } } @@ -1789,7 +1789,7 @@ impl Fsm for PenToolFsmState { .collect(); let fill_color = graphene_std::Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()).unwrap().with_alpha(0.05); - overlay_context.fill_path(subpaths.iter(), transform, &fill_color, false, None); + overlay_context.fill_path(subpaths.iter(), transform, &fill_color, false, false, None); } } }