From 24ddd882e4fdeba3bd714c08419da9ddb2236a43 Mon Sep 17 00:00:00 2001 From: Mateo <45690579+c-mateo@users.noreply.github.com> Date: Sun, 6 Apr 2025 05:35:16 +0000 Subject: [PATCH 01/18] Make the Pen tool show a path being closed by drawing a filled overlay when hovering the endpoint --- .../messages/input_mapper/input_mappings.rs | 1 + .../document/overlays/utility_types.rs | 44 ++++++++++++++++++- .../messages/tool/tool_messages/fill_tool.rs | 32 +++++++++++++- .../messages/tool/tool_messages/pen_tool.rs | 42 ++++++++++++++++-- 4 files changed, 113 insertions(+), 6 deletions(-) diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index cc83a32ee1..6a25bc4844 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -281,6 +281,7 @@ pub fn input_mappings() -> Mapping { entry!(KeyDown(Enter); action_dispatch=SplineToolMessage::Confirm), // // FillToolMessage + entry!(PointerMove; action_dispatch=FillToolMessage::PointerMove), entry!(KeyDown(MouseLeft); action_dispatch=FillToolMessage::FillPrimaryColor), entry!(KeyDown(MouseLeft); modifiers=[Shift], action_dispatch=FillToolMessage::FillSecondaryColor), entry!(KeyUp(MouseLeft); action_dispatch=FillToolMessage::PointerUp), diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index 79cc7f20c3..6090fea205 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -447,6 +447,7 @@ impl OverlayContext { self.end_dpi_aware_transform(); } + /// This is used by the pen and path tool to outline the path of the shape pub fn outline_vector(&mut self, vector_data: &VectorData, transform: DAffine2) { self.start_dpi_aware_transform(); @@ -465,6 +466,7 @@ impl OverlayContext { self.end_dpi_aware_transform(); } + /// This is used by the pen tool in order to show how the bezier curve would look like pub fn outline_bezier(&mut self, bezier: Bezier, transform: DAffine2) { self.start_dpi_aware_transform(); @@ -493,7 +495,7 @@ impl OverlayContext { self.end_dpi_aware_transform(); } - pub fn outline(&mut self, subpaths: impl Iterator>>, transform: DAffine2) { + fn push_path(&mut self, subpaths: impl Iterator>>, transform: DAffine2) { self.start_dpi_aware_transform(); self.render_context.begin_path(); @@ -540,10 +542,48 @@ impl OverlayContext { } } + self.end_dpi_aware_transform(); + } + + /// This is used by the select tool to outline a path selected or hovered + pub fn outline(&mut self, subpaths: impl Iterator>>, transform: DAffine2) { + self.push_path(subpaths, transform); + self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE); self.render_context.stroke(); + } - self.end_dpi_aware_transform(); + /// Outline a path and draw diagonal lines inside. + /// This isn't used by any tool, but it's kept in case we need it in the future + pub fn outline_striped(&mut self, subpaths: impl Iterator>>, transform: DAffine2) { + self.push_path(subpaths, transform); + + // Canvas state must be saved before clipping + self.render_context.save(); + self.render_context.clip(); + + // Draw the diagonal lines + let line_separation = 50 as f64; // in px + let max_dimension = if self.size.x > self.size.y { self.size.x } else { self.size.y }; + let end = (max_dimension / line_separation * 2.0).ceil() as i32; + for n in 1..end { + let factor = n as f64; + self.render_context.move_to(line_separation * factor, 0.0); + self.render_context.line_to(0.0, line_separation * factor); + self.render_context.stroke(); + } + + // Undo the clipping + self.render_context.restore(); + } + + /// Fills the area inside the path + /// This is used by the fill tool to show the area to be filled and 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(); } 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 fe61621373..ce68f8514c 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -1,18 +1,23 @@ 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::raster::{Alpha, RGB}; use graphene_core::vector::style::Fill; + #[derive(Default)] pub struct FillTool { fsm_state: FillToolFsmState, } #[impl_message(Message, ToolMessage, Fill)] -#[derive(PartialEq, Eq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)] +#[derive(PartialEq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)] pub enum FillToolMessage { // Standard messages Abort, + Overlays(OverlayContext), // Tool-specific messages + PointerMove, PointerUp, FillPrimaryColor, FillSecondaryColor, @@ -45,8 +50,10 @@ impl<'a> MessageHandler> for FillToo FillToolFsmState::Ready => actions!(FillToolMessageDiscriminant; FillPrimaryColor, FillSecondaryColor, + PointerMove, ), FillToolFsmState::Filling => actions!(FillToolMessageDiscriminant; + PointerMove, PointerUp, Abort, ), @@ -58,6 +65,7 @@ impl ToolTransition for FillTool { fn event_to_message_map(&self) -> EventToMessageMap { EventToMessageMap { tool_abort: Some(FillToolMessage::Abort.into()), + overlay_provider: Some(|overlay_context| FillToolMessage::Overlays(overlay_context).into()), ..Default::default() } } @@ -82,6 +90,28 @@ impl Fsm for FillToolFsmState { let ToolMessage::Fill(event) = event else { return self }; match (self, event) { + (_, FillToolMessage::Overlays(mut overlay_context)) => { + // When not in Drawing State + // Only highlight layers if the viewport is not being panned (middle mouse button is pressed) + // TODO: Don't use `Key::MouseMiddle` directly, instead take it as a variable from the input mappings list like in all other places; or find a better way than checking the key state + if !input.keyboard.get(Key::MouseMiddle as usize) { + let primary_color = global_tool_data.primary_color; + let preview_color = primary_color.to_gamma_srgb().with_alpha(0.4).to_css(); + + // Get the layer the user is hovering over + let click = document.click(input); + let not_selected_click = click.filter(|&hovered_layer| !document.network_interface.selected_nodes().selected_layers_contains(hovered_layer, document.metadata())); + if let Some(layer) = not_selected_click { + overlay_context.fill_path(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer), preview_color.as_str()); + } + } + self + } + (_, FillToolMessage::PointerMove) => { + // Generate the hover outline + responses.add(OverlaysMessage::Draw); + FillToolFsmState::Ready + } (FillToolFsmState::Ready, color_event) => { let Some(layer_identifier) = document.click(input) else { return self; diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index 761a4b04fb..73ed4d9cd0 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -1,5 +1,5 @@ use super::tool_prelude::*; -use crate::consts::{DEFAULT_STROKE_WIDTH, HIDE_HANDLE_DISTANCE, LINE_ROTATE_SNAP_ANGLE}; +use crate::consts::{COLOR_OVERLAY_BLUE, DEFAULT_STROKE_WIDTH, HIDE_HANDLE_DISTANCE, LINE_ROTATE_SNAP_ANGLE}; use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type; use crate::messages::portfolio::document::overlays::utility_functions::path_overlays; use crate::messages::portfolio::document::overlays::utility_types::{DrawHandles, OverlayContext}; @@ -9,11 +9,11 @@ use crate::messages::tool::common_functionality::color_selector::{ToolColorOptio use crate::messages::tool::common_functionality::graph_modification_utils::{self, merge_layers}; use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration}; use crate::messages::tool::common_functionality::utility_functions::{closest_point, should_extend}; -use bezier_rs::{Bezier, BezierHandles}; +use bezier_rs::{Bezier, BezierHandles, Subpath}; use graph_craft::document::NodeId; use graphene_core::Color; use graphene_core::vector::{PointId, VectorModificationType}; -use graphene_std::vector::{HandleId, ManipulatorPointId, NoHashBuilder, SegmentId, VectorData}; +use graphene_std::vector::{HandleId, ManipulatorPointId, NoHashBuilder, SegmentId, StrokeId, VectorData}; #[derive(Default)] pub struct PenTool { @@ -1336,6 +1336,42 @@ impl Fsm for PenToolFsmState { overlay_context.manipulator_anchor(next_anchor, false, None); } + let mut vector_data = document.network_interface.compute_modified_vector(layer.unwrap()).unwrap(); + + if tool_data.latest_point().is_some() { + let latest_point = tool_data.latest_point().unwrap(); + + 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; + + // Check if the next point is close to any other point in the vector data + let mut end = None; + + let start = tool_data.latest_point().unwrap().id; + for id in vector_data.extendable_points(preferences.vector_meshes).filter(|&point| point != start) { + let Some(pos) = vector_data.point_domain.position_from_id(id) else { continue }; + let transformed_distance_between_squared = transform.transform_point2(pos).distance_squared(transform.transform_point2(next_point)); + let snap_point_tolerance_squared = crate::consts::SNAP_POINT_TOLERANCE.powi(2); + if transformed_distance_between_squared < snap_point_tolerance_squared { + end = Some(id); + } + } + + // We have the point. Close the path and draw the enclosed area + if end.is_some() { + let id: SegmentId = SegmentId::generate(); + vector_data.push(id, start, end.unwrap(), BezierHandles::Cubic { handle_start, handle_end }, StrokeId::ZERO); + + let beziers: Vec = vector_data.segment_bezier_iter().map(|(_, bezier, _, _)| bezier).collect(); + let subpath = [Subpath::from_beziers(&beziers[..], false)]; + + let base_color = Color::from_rgb_str(COLOR_OVERLAY_BLUE.replace("#", "").as_str()).unwrap(); + let color = base_color.to_gamma_srgb().with_alpha(0.4); + overlay_context.fill_path(subpath.iter(), transform, color.to_css().as_str()); + } + } + // Draw the overlays that visualize current snapping tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context); From 6e39bc23bf5239266345f33dbd76dcb7cf4e3974 Mon Sep 17 00:00:00 2001 From: Mateo <45690579+c-mateo@users.noreply.github.com> Date: Sun, 6 Apr 2025 05:51:06 +0000 Subject: [PATCH 02/18] Add to_css to color.rs --- node-graph/gcore/src/raster/color.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/node-graph/gcore/src/raster/color.rs b/node-graph/gcore/src/raster/color.rs index 3be18d1952..5d0f4fc0ce 100644 --- a/node-graph/gcore/src/raster/color.rs +++ b/node-graph/gcore/src/raster/color.rs @@ -955,6 +955,12 @@ impl Color { } } + /// Produces a CSS color in the format `rgb(red green blue / alpha%)`. + /// To get the expected color you might have to use `to_gamma_srgb`. + pub fn to_css(&self) -> String { + format!("rgb({} {} {} / {}%)", self.red * 255.0, self.green * 255.0, self.blue * 255.0, self.alpha * 100.0) + } + #[inline(always)] pub fn srgb_to_linear(channel: f32) -> f32 { if channel <= 0.04045 { channel / 12.92 } else { ((channel + 0.055) / 1.055).powf(2.4) } From affa69f0ec31e7dad36e42981295a1a79d0c7259 Mon Sep 17 00:00:00 2001 From: Mateo <45690579+c-mateo@users.noreply.github.com> Date: Sun, 6 Apr 2025 04:03:55 -0300 Subject: [PATCH 03/18] Check before unwrapping layer --- .../messages/tool/tool_messages/pen_tool.rs | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index 73ed4d9cd0..8f87b425db 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -1335,9 +1335,7 @@ impl Fsm for PenToolFsmState { // Draw the anchor square for the most recently placed anchor overlay_context.manipulator_anchor(next_anchor, false, None); } - - let mut vector_data = document.network_interface.compute_modified_vector(layer.unwrap()).unwrap(); - + if tool_data.latest_point().is_some() { let latest_point = tool_data.latest_point().unwrap(); @@ -1348,13 +1346,16 @@ impl Fsm for PenToolFsmState { // Check if the next point is close to any other point in the vector data let mut end = None; - let start = tool_data.latest_point().unwrap().id; - for id in vector_data.extendable_points(preferences.vector_meshes).filter(|&point| point != start) { - let Some(pos) = vector_data.point_domain.position_from_id(id) else { continue }; - let transformed_distance_between_squared = transform.transform_point2(pos).distance_squared(transform.transform_point2(next_point)); - let snap_point_tolerance_squared = crate::consts::SNAP_POINT_TOLERANCE.powi(2); - if transformed_distance_between_squared < snap_point_tolerance_squared { - end = Some(id); + let start = latest_point().id; + if layer.is_some() { + let mut vector_data = document.network_interface.compute_modified_vector(layer.unwrap()).unwrap(); + for id in vector_data.extendable_points(preferences.vector_meshes).filter(|&point| point != start) { + let Some(pos) = vector_data.point_domain.position_from_id(id) else { continue }; + let transformed_distance_between_squared = transform.transform_point2(pos).distance_squared(transform.transform_point2(next_point)); + let snap_point_tolerance_squared = crate::consts::SNAP_POINT_TOLERANCE.powi(2); + if transformed_distance_between_squared < snap_point_tolerance_squared { + end = Some(id); + } } } From 3a7606ce8c9b1446455123d2a8068ab37f753e50 Mon Sep 17 00:00:00 2001 From: Mateo <45690579+c-mateo@users.noreply.github.com> Date: Sun, 6 Apr 2025 04:09:41 -0300 Subject: [PATCH 04/18] Close if in the right place --- .../messages/tool/tool_messages/pen_tool.rs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index 8f87b425db..5becf449b6 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -1357,19 +1357,19 @@ impl Fsm for PenToolFsmState { end = Some(id); } } - } - // We have the point. Close the path and draw the enclosed area - if end.is_some() { - let id: SegmentId = SegmentId::generate(); - vector_data.push(id, start, end.unwrap(), BezierHandles::Cubic { handle_start, handle_end }, StrokeId::ZERO); + // We have the point. Close the path and draw the enclosed area + if end.is_some() { + let id: SegmentId = SegmentId::generate(); + vector_data.push(id, start, end.unwrap(), BezierHandles::Cubic { handle_start, handle_end }, StrokeId::ZERO); - let beziers: Vec = vector_data.segment_bezier_iter().map(|(_, bezier, _, _)| bezier).collect(); - let subpath = [Subpath::from_beziers(&beziers[..], false)]; + let beziers: Vec = vector_data.segment_bezier_iter().map(|(_, bezier, _, _)| bezier).collect(); + let subpath = [Subpath::from_beziers(&beziers[..], false)]; - let base_color = Color::from_rgb_str(COLOR_OVERLAY_BLUE.replace("#", "").as_str()).unwrap(); - let color = base_color.to_gamma_srgb().with_alpha(0.4); - overlay_context.fill_path(subpath.iter(), transform, color.to_css().as_str()); + let base_color = Color::from_rgb_str(COLOR_OVERLAY_BLUE.replace("#", "").as_str()).unwrap(); + let color = base_color.to_gamma_srgb().with_alpha(0.4); + overlay_context.fill_path(subpath.iter(), transform, color.to_css().as_str()); + } } } From 54dae4083944fe505f0c450f90b59666caff879a Mon Sep 17 00:00:00 2001 From: Mateo <45690579+c-mateo@users.noreply.github.com> Date: Sun, 6 Apr 2025 04:15:39 -0300 Subject: [PATCH 05/18] Fix typo --- editor/src/messages/tool/tool_messages/pen_tool.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index 5becf449b6..71571732a9 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -1346,7 +1346,7 @@ impl Fsm for PenToolFsmState { // Check if the next point is close to any other point in the vector data let mut end = None; - let start = latest_point().id; + let start = latest_point.id; if layer.is_some() { let mut vector_data = document.network_interface.compute_modified_vector(layer.unwrap()).unwrap(); for id in vector_data.extendable_points(preferences.vector_meshes).filter(|&point| point != start) { From a47cb6ea29762932ae8d5abb8843561ff663d84d Mon Sep 17 00:00:00 2001 From: Mateo <45690579+c-mateo@users.noreply.github.com> Date: Sun, 6 Apr 2025 15:29:34 +0000 Subject: [PATCH 06/18] Format code --- editor/src/messages/tool/tool_messages/fill_tool.rs | 4 +--- editor/src/messages/tool/tool_messages/pen_tool.rs | 2 +- 2 files changed, 2 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 ce68f8514c..6df5ec6104 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::raster::{Alpha, RGB}; use graphene_core::vector::style::Fill; #[derive(Default)] @@ -100,8 +99,7 @@ impl Fsm for FillToolFsmState { // Get the layer the user is hovering over let click = document.click(input); - let not_selected_click = click.filter(|&hovered_layer| !document.network_interface.selected_nodes().selected_layers_contains(hovered_layer, document.metadata())); - if let Some(layer) = not_selected_click { + if let Some(layer) = click { overlay_context.fill_path(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer), preview_color.as_str()); } } diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index 71571732a9..00838a1446 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -1335,7 +1335,7 @@ impl Fsm for PenToolFsmState { // Draw the anchor square for the most recently placed anchor overlay_context.manipulator_anchor(next_anchor, false, None); } - + if tool_data.latest_point().is_some() { let latest_point = tool_data.latest_point().unwrap(); From 5a801e96bdea87c99700a7053e4a23f9dc860017 Mon Sep 17 00:00:00 2001 From: Mateo <45690579+c-mateo@users.noreply.github.com> Date: Wed, 9 Apr 2025 16:43:22 -0300 Subject: [PATCH 07/18] Support discontinuous paths for closing preview --- .../messages/tool/tool_messages/pen_tool.rs | 27 ++- .../src/vector/vector_data/attributes.rs | 204 ++++++++++++++++++ 2 files changed, 226 insertions(+), 5 deletions(-) diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index 16375665fc..b3b855d318 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -10,7 +10,7 @@ use crate::messages::tool::common_functionality::graph_modification_utils::{self use crate::messages::tool::common_functionality::shape_editor::ShapeState; use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration}; use crate::messages::tool::common_functionality::utility_functions::{closest_point, should_extend}; -use bezier_rs::{Bezier, BezierHandles, Subpath}; +use bezier_rs::{Bezier, BezierHandles}; use graph_craft::document::NodeId; use graphene_core::Color; use graphene_core::vector::{PointId, VectorModificationType}; @@ -1622,17 +1622,34 @@ impl Fsm for PenToolFsmState { } } - // We have the point. Close the path and draw the enclosed area + // We have the point. Join the 2 vertices and check if any path is closed if end.is_some() { let id: SegmentId = SegmentId::generate(); vector_data.push(id, start, end.unwrap(), BezierHandles::Cubic { handle_start, handle_end }, StrokeId::ZERO); - let beziers: Vec = vector_data.segment_bezier_iter().map(|(_, bezier, _, _)| bezier).collect(); - let subpath = [Subpath::from_beziers(&beziers[..], false)]; + let grouped_segments = vector_data.auto_join_paths(); + + // Find the closed paths with the last added segment + let closed_paths = grouped_segments.iter().filter(|path| path.is_closed() && path.contains(id)); + + // Get the bezier curves of the closed path + 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 base_color = Color::from_rgb_str(COLOR_OVERLAY_BLUE.replace("#", "").as_str()).unwrap(); let color = base_color.to_gamma_srgb().with_alpha(0.4); - overlay_context.fill_path(subpath.iter(), transform, color.to_css().as_str()); + + overlay_context.fill_path(subpaths.iter(), transform, color.to_css().as_str()); } } } diff --git a/node-graph/gcore/src/vector/vector_data/attributes.rs b/node-graph/gcore/src/vector/vector_data/attributes.rs index 3b84eb8193..a8026252c0 100644 --- a/node-graph/gcore/src/vector/vector_data/attributes.rs +++ b/node-graph/gcore/src/vector/vector_data/attributes.rs @@ -544,6 +544,84 @@ impl RegionDomain { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct HalfEdge { + pub id: SegmentId, + pub start: usize, + pub end: usize, + pub reverse: bool, +} + +impl HalfEdge { + pub fn new(id: SegmentId, start: usize, end: usize, reverse: bool) -> Self { + Self { id, start, end, reverse } + } + + pub fn reversed(&self) -> Self { + Self { + id: self.id, + start: self.start, + end: self.end, + reverse: !self.reverse, + } + } + + pub fn normalize_direction(&self) -> Self { + if self.reverse { + Self { + id: self.id, + start: self.end, + end: self.start, + reverse: false, + } + } else { + self.clone() + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Subpath_ { + pub edges: Vec, +} + +impl Subpath_ { + pub fn new(segments: Vec) -> Self { + Self { edges: segments } + } + + pub fn first_segment(&self) -> Option<&HalfEdge> { + self.edges.first() + } + + pub fn last_segment(&self) -> Option<&HalfEdge> { + self.edges.last() + } + + pub fn push(&mut self, segment: HalfEdge) { + self.edges.push(segment); + } + + pub fn insert(&mut self, index: usize, segment: HalfEdge) { + self.edges.insert(index, segment); + } + + pub fn is_closed(&self) -> bool { + match (self.edges.first(), self.edges.last()) { + (Some(first), Some(last)) => first.start == last.end, + _ => false, + } + } + + pub fn from_segment(segment: HalfEdge) -> Self { + Self { edges: vec![segment] } + } + + pub fn contains(&self, segment_id: SegmentId) -> bool { + self.edges.iter().any(|s| s.id == segment_id) + } +} + impl VectorData { /// Construct a [`bezier_rs::Bezier`] curve spanning from the resolved position of the start and end points with the specified handles. fn segment_to_bezier_with_index(&self, start: usize, end: usize, handles: bezier_rs::BezierHandles) -> bezier_rs::Bezier { @@ -583,6 +661,132 @@ impl VectorData { .map(to_bezier) } + pub fn auto_join_paths(&self) -> Vec { + let segments = self.segment_domain.iter().map(|(id, start, end, _)| HalfEdge::new(id, start, end, false)); + + let mut paths: Vec = Vec::new(); + let mut current_path: Option<&mut Subpath_> = None; + let mut previous: Option<(usize, usize)> = None; + + // First pass. Generates subpaths from continuous segments. + for seg_ref in segments { + let start = seg_ref.start; + let end = seg_ref.end; + + if let Some((_, prev_end)) = previous { + if start == prev_end { + if let Some(path) = current_path.as_mut() { + path.push(seg_ref); + } + } else { + paths.push(Subpath_::from_segment(seg_ref)); + current_path = paths.last_mut(); + } + } else { + paths.push(Subpath_::from_segment(seg_ref)); + current_path = paths.last_mut(); + } + + previous = Some((start, end)); + } + + // Print the paths + // debug!("Paths: {:?}", paths); + + // Second pass. Tries to join paths together. + let mut joined_paths: Vec = Vec::new(); + + loop { + let mut previous_subpath: Option<&mut Subpath_> = None; + let paths_length = paths.len(); + + for current_subpath in paths.into_iter() { + if previous_subpath.is_none() { + joined_paths.push(current_subpath); + previous_subpath = Some(joined_paths.last_mut().unwrap()); + continue; + } + + // Take ownership of the previous subpath + let prev: &mut Subpath_ = previous_subpath.unwrap(); + let prev_last = prev.last_segment().unwrap(); + let prev_first = prev.first_segment().unwrap(); + let cur_last = current_subpath.last_segment().unwrap(); + let cur_first = current_subpath.first_segment().unwrap(); + + if prev_last.end == cur_first.start { + for segment in current_subpath.edges { + prev.push(segment.normalize_direction()); + } + } else if prev_first.start == cur_last.end { + for segment in current_subpath.edges.into_iter().rev() { + prev.insert(0, segment.normalize_direction()); + } + } else if prev_last.end == cur_last.end { + for segment in current_subpath.edges.into_iter().rev() { + prev.push(segment.reversed().normalize_direction()); + } + } else if prev_first.start == cur_first.start { + for segment in current_subpath.edges { + prev.insert(0, segment.reversed().normalize_direction()); + } + } else { + joined_paths.push(current_subpath); + previous_subpath = Some(joined_paths.last_mut().unwrap()); + continue; + } + + // Return ownership + previous_subpath = Some(prev); + } + + if paths_length == joined_paths.len() { + break; + }; + + paths = joined_paths; // Move + joined_paths = Vec::new(); // Reset for next iteration + } + + joined_paths + } + + /// 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> { + 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; From e7066ec8cd3ab1d0fdd50e30ff7dfcf25c7eb0ba Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Sun, 13 Apr 2025 23:32:25 -0700 Subject: [PATCH 08/18] Code review --- editor/src/messages/tool/tool_messages/fill_tool.rs | 3 ++- editor/src/messages/tool/tool_messages/pen_tool.rs | 6 +++--- 2 files changed, 5 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 8083287c56..ee1a8d81ff 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -95,7 +95,8 @@ impl Fsm for FillToolFsmState { // TODO: Don't use `Key::MouseMiddle` directly, instead take it as a variable from the input mappings list like in all other places; or find a better way than checking the key state if !input.keyboard.get(Key::MouseMiddle as usize) { let primary_color = global_tool_data.primary_color; - let preview_color = primary_color.to_gamma_srgb().with_alpha(0.4).to_css(); + let mut preview_color = primary_color.to_gamma_srgb().with_alpha(0.25).to_rgba_hex_srgb(); + preview_color.insert(0, '#'); // Get the layer the user is hovering over let click = document.click(input); diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index 649cf9a707..93efe269e3 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -1659,10 +1659,10 @@ impl Fsm for PenToolFsmState { }) .collect(); - let base_color = Color::from_rgb_str(COLOR_OVERLAY_BLUE.replace("#", "").as_str()).unwrap(); - let color = base_color.to_gamma_srgb().with_alpha(0.4); + let mut fill_color = Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()).unwrap().with_alpha(0.05).to_rgba_hex_srgb(); + fill_color.insert(0, '#'); - overlay_context.fill_path(subpaths.iter(), transform, color.to_css().as_str()); + overlay_context.fill_path(subpaths.iter(), transform, fill_color.as_str()); } } } From 1824450325f4d77bdb1c58edd22e6aec1d39e6b0 Mon Sep 17 00:00:00 2001 From: Mateo <45690579+c-mateo@users.noreply.github.com> Date: Mon, 14 Apr 2025 17:05:09 -0300 Subject: [PATCH 09/18] Denser fill lines --- .../document/overlays/utility_types.rs | 29 ++++++++++--------- .../messages/tool/tool_messages/fill_tool.rs | 4 +-- .../messages/tool/tool_messages/pen_tool.rs | 2 +- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index 6090fea205..d662b99231 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -553,17 +553,29 @@ impl OverlayContext { self.render_context.stroke(); } - /// Outline a path and draw diagonal lines inside. - /// This isn't used by any tool, but it's kept in case we need it in the future - pub fn outline_striped(&mut self, subpaths: impl Iterator>>, transform: DAffine2) { + /// Fills the area inside the path + /// This is used by the fill tool to show the area to be filled and 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(); + } + + pub fn fill_striped_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(); + // Canvas state must be saved before clipping self.render_context.save(); self.render_context.clip(); + self.render_context.set_line_width(0.8); + // Draw the diagonal lines - let line_separation = 50 as f64; // in px + let line_separation = 25 as f64; // in px let max_dimension = if self.size.x > self.size.y { self.size.x } else { self.size.y }; let end = (max_dimension / line_separation * 2.0).ceil() as i32; for n in 1..end { @@ -577,15 +589,6 @@ impl OverlayContext { self.render_context.restore(); } - /// Fills the area inside the path - /// This is used by the fill tool to show the area to be filled and 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(); - } - 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 ee1a8d81ff..a7218e487a 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -95,13 +95,13 @@ impl Fsm for FillToolFsmState { // TODO: Don't use `Key::MouseMiddle` directly, instead take it as a variable from the input mappings list like in all other places; or find a better way than checking the key state if !input.keyboard.get(Key::MouseMiddle as usize) { let primary_color = global_tool_data.primary_color; - let mut preview_color = primary_color.to_gamma_srgb().with_alpha(0.25).to_rgba_hex_srgb(); + let mut preview_color = primary_color.to_gamma_srgb().to_rgba_hex_srgb(); preview_color.insert(0, '#'); // Get the layer the user is hovering over let click = document.click(input); if let Some(layer) = click { - overlay_context.fill_path(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer), preview_color.as_str()); + overlay_context.fill_striped_path(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer), preview_color.as_str()); } } self diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index 93efe269e3..4e957d625d 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -1659,7 +1659,7 @@ impl Fsm for PenToolFsmState { }) .collect(); - let mut fill_color = Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()).unwrap().with_alpha(0.05).to_rgba_hex_srgb(); + let mut fill_color = Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()).unwrap().with_alpha(0.2).to_rgba_hex_srgb(); fill_color.insert(0, '#'); overlay_context.fill_path(subpaths.iter(), transform, fill_color.as_str()); From 1dc0625a51a744caa0cfe8271f2b751387129e9b Mon Sep 17 00:00:00 2001 From: Mateo <45690579+c-mateo@users.noreply.github.com> Date: Tue, 15 Apr 2025 09:12:02 -0300 Subject: [PATCH 10/18] Fill tool preview with strip lines only and revert pen shape-closing opacity --- .../messages/portfolio/document/overlays/utility_types.rs | 8 +++----- editor/src/messages/tool/tool_messages/fill_tool.rs | 5 ++--- editor/src/messages/tool/tool_messages/pen_tool.rs | 3 +-- 3 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 d662b99231..bf92764197 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -562,17 +562,15 @@ impl OverlayContext { self.render_context.fill(); } - pub fn fill_striped_path(&mut self, subpaths: impl Iterator>>, transform: DAffine2, color: &str) { + pub fn strip_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(); - // Canvas state must be saved before clipping self.render_context.save(); self.render_context.clip(); - self.render_context.set_line_width(0.8); + self.render_context.set_line_width(1.5); + self.render_context.set_stroke_style_str(color); // Draw the diagonal lines let line_separation = 25 as f64; // in px diff --git a/editor/src/messages/tool/tool_messages/fill_tool.rs b/editor/src/messages/tool/tool_messages/fill_tool.rs index a7218e487a..0d4beeff0d 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -95,13 +95,12 @@ impl Fsm for FillToolFsmState { // TODO: Don't use `Key::MouseMiddle` directly, instead take it as a variable from the input mappings list like in all other places; or find a better way than checking the key state if !input.keyboard.get(Key::MouseMiddle as usize) { let primary_color = global_tool_data.primary_color; - let mut preview_color = primary_color.to_gamma_srgb().to_rgba_hex_srgb(); - preview_color.insert(0, '#'); + let preview_color = primary_color.to_css(); // Get the layer the user is hovering over let click = document.click(input); if let Some(layer) = click { - overlay_context.fill_striped_path(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer), preview_color.as_str()); + overlay_context.strip_path(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer), preview_color.as_str()); } } self diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index 4e957d625d..cde5f2f383 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -1659,8 +1659,7 @@ impl Fsm for PenToolFsmState { }) .collect(); - let mut fill_color = Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()).unwrap().with_alpha(0.2).to_rgba_hex_srgb(); - fill_color.insert(0, '#'); + let fill_color = Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()).unwrap().with_alpha(0.05).to_css(); overlay_context.fill_path(subpaths.iter(), transform, fill_color.as_str()); } From 83173153fdfeb598099d931c6d613f50fb77c4ed Mon Sep 17 00:00:00 2001 From: Mateo <45690579+c-mateo@users.noreply.github.com> Date: Wed, 16 Apr 2025 00:09:25 -0300 Subject: [PATCH 11/18] Small adjustments to fill preview --- .../src/messages/portfolio/document/overlays/utility_types.rs | 4 ++-- editor/src/messages/tool/tool_messages/pen_tool.rs | 1 + 2 files changed, 3 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 bf92764197..478b38cdd1 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -569,11 +569,11 @@ impl OverlayContext { self.render_context.save(); self.render_context.clip(); - self.render_context.set_line_width(1.5); + self.render_context.begin_path(); self.render_context.set_stroke_style_str(color); // Draw the diagonal lines - let line_separation = 25 as f64; // in px + let line_separation = 24 as f64; // in px let max_dimension = if self.size.x > self.size.y { self.size.x } else { self.size.y }; let end = (max_dimension / line_separation * 2.0).ceil() as i32; for n in 1..end { diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index cde5f2f383..4d9e29253d 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -1613,6 +1613,7 @@ impl Fsm for PenToolFsmState { overlay_context.manipulator_anchor(next_anchor, false, None); } + // Fill the shape if the new point closes the path if tool_data.latest_point().is_some() { let latest_point = tool_data.latest_point().unwrap(); From 2873bb86753b2a6c5d087072c3a8c4ee47422e47 Mon Sep 17 00:00:00 2001 From: Mateo <45690579+c-mateo@users.noreply.github.com> Date: Thu, 17 Apr 2025 16:04:22 -0300 Subject: [PATCH 12/18] Fix line width of fill preview --- .../src/messages/portfolio/document/overlays/utility_types.rs | 1 + editor/src/messages/tool/tool_messages/fill_tool.rs | 2 +- 2 files changed, 2 insertions(+), 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 478b38cdd1..b4f4eba3cd 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -570,6 +570,7 @@ impl OverlayContext { self.render_context.clip(); self.render_context.begin_path(); + self.render_context.set_line_width(1.0); self.render_context.set_stroke_style_str(color); // Draw the diagonal lines diff --git a/editor/src/messages/tool/tool_messages/fill_tool.rs b/editor/src/messages/tool/tool_messages/fill_tool.rs index 0d4beeff0d..70f5d1d128 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -95,7 +95,7 @@ impl Fsm for FillToolFsmState { // TODO: Don't use `Key::MouseMiddle` directly, instead take it as a variable from the input mappings list like in all other places; or find a better way than checking the key state if !input.keyboard.get(Key::MouseMiddle as usize) { let primary_color = global_tool_data.primary_color; - let preview_color = primary_color.to_css(); + let preview_color = primary_color.to_gamma_srgb().to_css(); // Get the layer the user is hovering over let click = document.click(input); From 51b5f6c7a6a3a5bbe8b55b22984b8f45d14184cc Mon Sep 17 00:00:00 2001 From: Mateo <45690579+c-mateo@users.noreply.github.com> Date: Fri, 18 Apr 2025 17:50:15 -0300 Subject: [PATCH 13/18] Use a pattern to preview the fill tool and fix canvas clearing --- editor/Cargo.toml | 3 + .../overlays/overlays_message_handler.rs | 2 +- .../document/overlays/utility_types.rs | 64 +++++++++++++------ .../src/messages/tool/tool_message_handler.rs | 2 +- .../messages/tool/tool_messages/fill_tool.rs | 11 ++-- 5 files changed, 54 insertions(+), 28 deletions(-) diff --git a/editor/Cargo.toml b/editor/Cargo.toml index 34b9ffa1e6..9a076a01fb 100644 --- a/editor/Cargo.toml +++ b/editor/Cargo.toml @@ -58,6 +58,9 @@ web-sys = { workspace = true, features = [ "Element", "HtmlCanvasElement", "CanvasRenderingContext2d", + "OffscreenCanvasRenderingContext2d", + "CanvasPattern", + "OffscreenCanvas", "TextMetrics", ] } 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 2ddee78dab..0a6f0b6902 100644 --- a/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs +++ b/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs @@ -47,7 +47,7 @@ impl MessageHandler> for OverlaysMessag let [a, b, c, d, e, f] = DAffine2::from_scale(DVec2::splat(device_pixel_ratio)).to_cols_array(); let _ = context.set_transform(a, b, c, d, e, f); - context.clear_rect(0., 0., ipp.viewport_bounds.size().x, ipp.viewport_bounds.size().y); + context.clear_rect(0., 0., canvas.width().into(), canvas.height().into()); let _ = context.reset_transform(); if overlays_visible { diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index b4f4eba3cd..820e2f61ce 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -8,10 +8,13 @@ use bezier_rs::{Bezier, Subpath}; use core::borrow::Borrow; use core::f64::consts::{FRAC_PI_2, TAU}; use glam::{DAffine2, DVec2}; +use graphene_core::Color; use graphene_core::renderer::Quad; use graphene_std::vector::{PointId, SegmentId, VectorData}; use std::collections::HashMap; +use wasm_bindgen::JsCast; use wasm_bindgen::JsValue; +use web_sys::{OffscreenCanvas, OffscreenCanvasRenderingContext2d}; pub type OverlayProvider = fn(OverlayContext) -> Message; @@ -562,30 +565,49 @@ impl OverlayContext { self.render_context.fill(); } - pub fn strip_path(&mut self, subpaths: impl Iterator>>, transform: DAffine2, color: &str) { - self.push_path(subpaths, transform); + pub fn fill_path_pattern(&mut self, subpaths: impl Iterator>>, transform: DAffine2, color: &Color) { + let pattern_width = 6; + let pattern_height = 6; + let pattern_canvas = OffscreenCanvas::new(pattern_width, pattern_height).unwrap(); + + let pattern_ctx: OffscreenCanvasRenderingContext2d = pattern_canvas + .get_context("2d") + .ok() + .flatten() + .expect("Failed to get canvas context") + .dyn_into() + .expect("Context should be a canvas 2d context"); + + let mut data = vec![0u8; (4 * pattern_width * pattern_height) as usize]; // 4x4 pixels, 4 components (RGBA) per pixel + + let mut set_pixel = |x: usize, y: usize, color: &[u8; 4]| { + let index = (x + y * pattern_width as usize) * 4; + data[index] = color[0]; + data[index + 1] = color[1]; + data[index + 2] = color[2]; + data[index + 3] = color[3]; + }; - // Canvas state must be saved before clipping - self.render_context.save(); - self.render_context.clip(); + let color = color.to_rgba8_srgb(); + set_pixel(0, 0, &color); + set_pixel(0, 1, &color); + set_pixel(1, 0, &color); + set_pixel(1, 1, &color); - self.render_context.begin_path(); - self.render_context.set_line_width(1.0); - self.render_context.set_stroke_style_str(color); - - // Draw the diagonal lines - let line_separation = 24 as f64; // in px - let max_dimension = if self.size.x > self.size.y { self.size.x } else { self.size.y }; - let end = (max_dimension / line_separation * 2.0).ceil() as i32; - for n in 1..end { - let factor = n as f64; - self.render_context.move_to(line_separation * factor, 0.0); - self.render_context.line_to(0.0, line_separation * factor); - self.render_context.stroke(); - } + set_pixel(3, 3, &color); + set_pixel(3, 4, &color); + set_pixel(4, 3, &color); + set_pixel(4, 4, &color); - // Undo the clipping - self.render_context.restore(); + let image_data = web_sys::ImageData::new_with_u8_clamped_array_and_sh(wasm_bindgen::Clamped(&data), pattern_width, pattern_height).unwrap(); + pattern_ctx.put_image_data(&image_data, 0.0, 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.set_transform(2.0, 0.0, 0.0, 2.0, 0.0, 0.0).expect("Failed to set transform"); + self.render_context.fill(); } pub fn get_width(&self, text: &str) -> f64 { diff --git a/editor/src/messages/tool/tool_message_handler.rs b/editor/src/messages/tool/tool_message_handler.rs index 91aa73ac41..b70affe51b 100644 --- a/editor/src/messages/tool/tool_message_handler.rs +++ b/editor/src/messages/tool/tool_message_handler.rs @@ -218,7 +218,7 @@ impl MessageHandler> for ToolMessageHandler { let document_data = &mut self.tool_state.document_tool_data; document_data.primary_color = color; - self.tool_state.document_tool_data.update_working_colors(responses); // TODO: Make this an event + document_data.update_working_colors(responses); // TODO: Make this an event } ToolMessage::SelectRandomPrimaryColor => { // Select a random primary color (rgba) based on an UUID diff --git a/editor/src/messages/tool/tool_messages/fill_tool.rs b/editor/src/messages/tool/tool_messages/fill_tool.rs index 70f5d1d128..bbf1170e34 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -13,6 +13,7 @@ pub struct FillTool { pub enum FillToolMessage { // Standard messages Abort, + WorkingColorChanged, Overlays(OverlayContext), // Tool-specific messages @@ -64,6 +65,7 @@ impl ToolTransition for FillTool { fn event_to_message_map(&self) -> EventToMessageMap { EventToMessageMap { tool_abort: Some(FillToolMessage::Abort.into()), + working_color_changed: Some(FillToolMessage::WorkingColorChanged.into()), overlay_provider: Some(|overlay_context| FillToolMessage::Overlays(overlay_context).into()), ..Default::default() } @@ -94,21 +96,20 @@ impl Fsm for FillToolFsmState { // Only highlight layers if the viewport is not being panned (middle mouse button is pressed) // TODO: Don't use `Key::MouseMiddle` directly, instead take it as a variable from the input mappings list like in all other places; or find a better way than checking the key state if !input.keyboard.get(Key::MouseMiddle as usize) { - let primary_color = global_tool_data.primary_color; - let preview_color = primary_color.to_gamma_srgb().to_css(); + let preview_color = global_tool_data.primary_color; // Get the layer the user is hovering over let click = document.click(input); if let Some(layer) = click { - overlay_context.strip_path(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer), preview_color.as_str()); + overlay_context.fill_path_pattern(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer), &preview_color); } } self } - (_, FillToolMessage::PointerMove) => { + (_, FillToolMessage::PointerMove | FillToolMessage::WorkingColorChanged) => { // Generate the hover outline responses.add(OverlaysMessage::Draw); - FillToolFsmState::Ready + self } (FillToolFsmState::Ready, color_event) => { let Some(layer_identifier) = document.click(input) else { From 804acfd536f34802c2664636668b124929c4e855 Mon Sep 17 00:00:00 2001 From: Mateo <45690579+c-mateo@users.noreply.github.com> Date: Mon, 21 Apr 2025 09:10:32 -0300 Subject: [PATCH 14/18] Update pattern --- .../portfolio/document/overlays/utility_types.rs | 14 +++----------- 1 file changed, 3 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 820e2f61ce..1ab55de6c3 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -566,8 +566,8 @@ impl OverlayContext { } pub fn fill_path_pattern(&mut self, subpaths: impl Iterator>>, transform: DAffine2, color: &Color) { - let pattern_width = 6; - let pattern_height = 6; + let pattern_width = 4; + let pattern_height = 4; let pattern_canvas = OffscreenCanvas::new(pattern_width, pattern_height).unwrap(); let pattern_ctx: OffscreenCanvasRenderingContext2d = pattern_canvas @@ -590,14 +590,7 @@ impl OverlayContext { let color = color.to_rgba8_srgb(); set_pixel(0, 0, &color); - set_pixel(0, 1, &color); - set_pixel(1, 0, &color); - set_pixel(1, 1, &color); - - set_pixel(3, 3, &color); - set_pixel(3, 4, &color); - set_pixel(4, 3, &color); - set_pixel(4, 4, &color); + set_pixel(2, 2, &color); let image_data = web_sys::ImageData::new_with_u8_clamped_array_and_sh(wasm_bindgen::Clamped(&data), pattern_width, pattern_height).unwrap(); pattern_ctx.put_image_data(&image_data, 0.0, 0.0).unwrap(); @@ -606,7 +599,6 @@ impl OverlayContext { self.push_path(subpaths, transform); self.render_context.set_fill_style_canvas_pattern(&pattern); - self.render_context.set_transform(2.0, 0.0, 0.0, 2.0, 0.0, 0.0).expect("Failed to set transform"); self.render_context.fill(); } From c56415ede169f15ae07dc8df63fabd4b74a9ae8f Mon Sep 17 00:00:00 2001 From: Mateo <45690579+c-mateo@users.noreply.github.com> Date: Mon, 21 Apr 2025 21:34:58 -0300 Subject: [PATCH 15/18] Simplify code --- .../document/overlays/utility_types.rs | 25 ++-- .../messages/tool/tool_messages/fill_tool.rs | 1 - .../messages/tool/tool_messages/pen_tool.rs | 57 ++++----- node-graph/gcore/src/raster/color.rs | 8 +- .../src/vector/vector_data/attributes.rs | 120 ++++++++---------- 5 files changed, 97 insertions(+), 114 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index 1ab55de6c3..b95a4111c4 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -12,8 +12,7 @@ use graphene_core::Color; use graphene_core::renderer::Quad; use graphene_std::vector::{PointId, SegmentId, VectorData}; use std::collections::HashMap; -use wasm_bindgen::JsCast; -use wasm_bindgen::JsValue; +use wasm_bindgen::{JsCast, JsValue}; use web_sys::{OffscreenCanvas, OffscreenCanvasRenderingContext2d}; pub type OverlayProvider = fn(OverlayContext) -> Message; @@ -556,8 +555,8 @@ impl OverlayContext { self.render_context.stroke(); } - /// Fills the area inside the path - /// This is used by the fill tool to show the area to be filled and by the pen tool to show the path being closed + /// Fills the area inside the path. `color` is in gamma space + /// This is 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); @@ -565,6 +564,9 @@ impl OverlayContext { self.render_context.fill(); } + + /// Fills the area inside the path with a pattern. `color` is in gamma space + /// This is 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) { let pattern_width = 4; let pattern_height = 4; @@ -580,17 +582,12 @@ impl OverlayContext { let mut data = vec![0u8; (4 * pattern_width * pattern_height) as usize]; // 4x4 pixels, 4 components (RGBA) per pixel - let mut set_pixel = |x: usize, y: usize, color: &[u8; 4]| { - let index = (x + y * pattern_width as usize) * 4; - data[index] = color[0]; - data[index + 1] = color[1]; - data[index + 2] = color[2]; - data[index + 3] = color[3]; - }; - let color = color.to_rgba8_srgb(); - set_pixel(0, 0, &color); - set_pixel(2, 2, &color); + 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); + } let image_data = web_sys::ImageData::new_with_u8_clamped_array_and_sh(wasm_bindgen::Clamped(&data), pattern_width, pattern_height).unwrap(); pattern_ctx.put_image_data(&image_data, 0.0, 0.0).unwrap(); diff --git a/editor/src/messages/tool/tool_messages/fill_tool.rs b/editor/src/messages/tool/tool_messages/fill_tool.rs index bbf1170e34..daaa5c4da7 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -92,7 +92,6 @@ impl Fsm for FillToolFsmState { let ToolMessage::Fill(event) = event else { return self }; match (self, event) { (_, FillToolMessage::Overlays(mut overlay_context)) => { - // When not in Drawing State // Only highlight layers if the viewport is not being panned (middle mouse button is pressed) // TODO: Don't use `Key::MouseMiddle` directly, instead take it as a variable from the input mappings list like in all other places; or find a better way than checking the key state if !input.keyboard.get(Key::MouseMiddle as usize) { diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index e4bc242e1b..0bd047e138 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -1609,54 +1609,49 @@ impl Fsm for PenToolFsmState { } // Fill the shape if the new point closes the path - if tool_data.latest_point().is_some() { - let latest_point = tool_data.latest_point().unwrap(); - + 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; - - // Check if the next point is close to any other point in the vector data - let mut end = None; - let start = latest_point.id; - if layer.is_some() { - let mut vector_data = document.network_interface.compute_modified_vector(layer.unwrap()).unwrap(); - for id in vector_data.extendable_points(preferences.vector_meshes).filter(|&point| point != start) { - let Some(pos) = vector_data.point_domain.position_from_id(id) else { continue }; - let transformed_distance_between_squared = transform.transform_point2(pos).distance_squared(transform.transform_point2(next_point)); - let snap_point_tolerance_squared = crate::consts::SNAP_POINT_TOLERANCE.powi(2); - if transformed_distance_between_squared < snap_point_tolerance_squared { - end = Some(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 end.is_some() { - let id: SegmentId = SegmentId::generate(); - vector_data.push(id, start, end.unwrap(), BezierHandles::Cubic { handle_start, handle_end }, StrokeId::ZERO); + 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)); - // Find the closed paths with the last added segment - let closed_paths = grouped_segments.iter().filter(|path| path.is_closed() && path.contains(id)); - - // Get the bezier curves of the closed path 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.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 = Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()).unwrap().with_alpha(0.05).to_css(); - + let fill_color = Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()).unwrap().with_alpha(0.05).to_css_from_gamma(); overlay_context.fill_path(subpaths.iter(), transform, fill_color.as_str()); } } diff --git a/node-graph/gcore/src/raster/color.rs b/node-graph/gcore/src/raster/color.rs index 5d0f4fc0ce..eb2c577176 100644 --- a/node-graph/gcore/src/raster/color.rs +++ b/node-graph/gcore/src/raster/color.rs @@ -955,9 +955,13 @@ impl Color { } } - /// Produces a CSS color in the format `rgb(red green blue / alpha%)`. - /// To get the expected color you might have to use `to_gamma_srgb`. + /// Produces a CSS color in the format `rgb(red green blue / alpha%)`. Use this if the [`Color`] is in linear space. pub fn to_css(&self) -> String { + self.to_gamma_srgb().to_css_from_gamma() + } + + /// Produces a CSS color in the format `rgb(red green blue / alpha%)`. Use this if the [`Color`] is in gamma space. + pub fn to_css_from_gamma(&self) -> String { format!("rgb({} {} {} / {}%)", self.red * 255.0, self.green * 255.0, self.blue * 255.0, self.alpha * 100.0) } diff --git a/node-graph/gcore/src/vector/vector_data/attributes.rs b/node-graph/gcore/src/vector/vector_data/attributes.rs index a8026252c0..b0100cd5ab 100644 --- a/node-graph/gcore/src/vector/vector_data/attributes.rs +++ b/node-graph/gcore/src/vector/vector_data/attributes.rs @@ -581,21 +581,20 @@ impl HalfEdge { } #[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Subpath_ { +pub struct FoundSubpath { pub edges: Vec, } -impl Subpath_ { +impl FoundSubpath { pub fn new(segments: Vec) -> Self { Self { edges: segments } } - pub fn first_segment(&self) -> Option<&HalfEdge> { - self.edges.first() - } - - pub fn last_segment(&self) -> Option<&HalfEdge> { - self.edges.last() + pub fn endpoints(&self) -> Option<(&HalfEdge, &HalfEdge)> { + match (self.edges.first(), self.edges.last()) { + (Some(first), Some(last)) => Some((first, last)), + _ => None, + } } pub fn push(&mut self, segment: HalfEdge) { @@ -606,6 +605,14 @@ impl Subpath_ { self.edges.insert(index, segment); } + pub fn extend(&mut self, segments: impl IntoIterator) { + self.edges.extend(segments); + } + + pub fn splice(&mut self, range: std::ops::Range, replace_with: I) where I: IntoIterator { + self.edges.splice(range, replace_with); + } + pub fn is_closed(&self) -> bool { match (self.edges.first(), self.edges.last()) { (Some(first), Some(last)) => first.start == last.end, @@ -661,94 +668,75 @@ impl VectorData { .map(to_bezier) } - pub fn auto_join_paths(&self) -> Vec { + pub fn auto_join_paths(&self) -> Vec { let segments = self.segment_domain.iter().map(|(id, start, end, _)| HalfEdge::new(id, start, end, false)); - let mut paths: Vec = Vec::new(); - let mut current_path: Option<&mut Subpath_> = None; + let mut paths: Vec = Vec::new(); + let mut current_path: Option<&mut FoundSubpath> = None; let mut previous: Option<(usize, usize)> = None; // First pass. Generates subpaths from continuous segments. for seg_ref in segments { - let start = seg_ref.start; - let end = seg_ref.end; - - if let Some((_, prev_end)) = previous { - if start == prev_end { - if let Some(path) = current_path.as_mut() { - path.push(seg_ref); - } - } else { - paths.push(Subpath_::from_segment(seg_ref)); - current_path = paths.last_mut(); + let (start, end) = (seg_ref.start, seg_ref.end); + + if previous.map_or(false, |(_, prev_end)| start == prev_end) { + if let Some(path) = current_path.as_mut() { + path.push(seg_ref); } } else { - paths.push(Subpath_::from_segment(seg_ref)); + paths.push(FoundSubpath::from_segment(seg_ref)); current_path = paths.last_mut(); } previous = Some((start, end)); } - // Print the paths - // debug!("Paths: {:?}", paths); - - // Second pass. Tries to join paths together. - let mut joined_paths: Vec = Vec::new(); + // Second pass. Try to join paths together. + let mut joined_paths = Vec::new(); loop { - let mut previous_subpath: Option<&mut Subpath_> = None; - let paths_length = paths.len(); - - for current_subpath in paths.into_iter() { - if previous_subpath.is_none() { - joined_paths.push(current_subpath); - previous_subpath = Some(joined_paths.last_mut().unwrap()); + let mut prev_index: Option = None; + let original_len = paths.len(); + + for current in paths.into_iter() { + // If there's no previous subpath, start a new one + if prev_index.is_none() { + joined_paths.push(current); + prev_index = Some(joined_paths.len() - 1); continue; } - // Take ownership of the previous subpath - let prev: &mut Subpath_ = previous_subpath.unwrap(); - let prev_last = prev.last_segment().unwrap(); - let prev_first = prev.first_segment().unwrap(); - let cur_last = current_subpath.last_segment().unwrap(); - let cur_first = current_subpath.first_segment().unwrap(); + let prev = &mut joined_paths[prev_index.unwrap()]; + // Compare segment connections + let (prev_first, prev_last) = prev.endpoints().unwrap(); + let (cur_first, cur_last) = current.endpoints().unwrap(); + + // Join paths if the endpoints connect if prev_last.end == cur_first.start { - for segment in current_subpath.edges { - prev.push(segment.normalize_direction()); - } + prev.edges.extend(current.edges.into_iter().map(|s| s.normalize_direction())); } else if prev_first.start == cur_last.end { - for segment in current_subpath.edges.into_iter().rev() { - prev.insert(0, segment.normalize_direction()); - } + prev.edges.splice(0..0, current.edges.into_iter().rev().map(|s| s.normalize_direction())); } else if prev_last.end == cur_last.end { - for segment in current_subpath.edges.into_iter().rev() { - prev.push(segment.reversed().normalize_direction()); - } + prev.edges.extend(current.edges.into_iter().rev().map(|s| s.reversed().normalize_direction())); } else if prev_first.start == cur_first.start { - for segment in current_subpath.edges { - prev.insert(0, segment.reversed().normalize_direction()); - } + prev.edges.splice(0..0, current.edges.into_iter().map(|s| s.reversed().normalize_direction())); } else { - joined_paths.push(current_subpath); - previous_subpath = Some(joined_paths.last_mut().unwrap()); - continue; + // If not connected, start a new subpath + joined_paths.push(current); + prev_index = Some(joined_paths.len() - 1); } - - // Return ownership - previous_subpath = Some(prev); } - if paths_length == joined_paths.len() { - break; - }; + // If no paths were joined in this pass, we're done + if joined_paths.len() == original_len { + return joined_paths; + } - paths = joined_paths; // Move - joined_paths = Vec::new(); // Reset for next iteration + // Repeat pass with newly joined paths + paths = joined_paths; + joined_paths = Vec::new(); } - - joined_paths } /// Construct a [`bezier_rs::Bezier`] curve from an iterator of segments with (handles, start point, end point) independently of discontinuities. From ca598cd0ec72d8f7fb17c3a25be5d18a7fc7f34f Mon Sep 17 00:00:00 2001 From: Mateo <45690579+c-mateo@users.noreply.github.com> Date: Mon, 21 Apr 2025 22:04:11 -0300 Subject: [PATCH 16/18] Format code --- .../document/overlays/utility_types.rs | 1 - .../messages/tool/tool_messages/pen_tool.rs | 27 ++++++++----------- .../src/vector/vector_data/attributes.rs | 5 +++- 3 files changed, 15 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 b95a4111c4..0cc4624b4a 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -564,7 +564,6 @@ impl OverlayContext { self.render_context.fill(); } - /// Fills the area inside the path with a pattern. `color` is in gamma space /// This is 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) { diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index 0bd047e138..cf59f66295 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -1618,15 +1618,12 @@ impl Fsm for PenToolFsmState { 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) - }) - }); + 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 { @@ -1639,13 +1636,11 @@ impl Fsm for PenToolFsmState { 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 + .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) }) diff --git a/node-graph/gcore/src/vector/vector_data/attributes.rs b/node-graph/gcore/src/vector/vector_data/attributes.rs index b0100cd5ab..d3beb65446 100644 --- a/node-graph/gcore/src/vector/vector_data/attributes.rs +++ b/node-graph/gcore/src/vector/vector_data/attributes.rs @@ -609,7 +609,10 @@ impl FoundSubpath { self.edges.extend(segments); } - pub fn splice(&mut self, range: std::ops::Range, replace_with: I) where I: IntoIterator { + pub fn splice(&mut self, range: std::ops::Range, replace_with: I) + where + I: IntoIterator, + { self.edges.splice(range, replace_with); } From b4eb2b2f44907e02c41e93ce830037e300c308ad Mon Sep 17 00:00:00 2001 From: Mateo <45690579+c-mateo@users.noreply.github.com> Date: Wed, 23 Apr 2025 16:08:41 -0300 Subject: [PATCH 17/18] Use secondary color to preview fill if shift is pressed --- editor/src/messages/input_mapper/input_mappings.rs | 2 +- editor/src/messages/tool/tool_messages/fill_tool.rs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index 6d6247d6b7..b75940bf09 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -282,7 +282,7 @@ pub fn input_mappings() -> Mapping { entry!(KeyDown(Enter); action_dispatch=SplineToolMessage::Confirm), // // FillToolMessage - entry!(PointerMove; action_dispatch=FillToolMessage::PointerMove), + entry!(PointerMove; refresh_keys=[Shift], action_dispatch=FillToolMessage::PointerMove), entry!(KeyDown(MouseLeft); action_dispatch=FillToolMessage::FillPrimaryColor), entry!(KeyDown(MouseLeft); modifiers=[Shift], action_dispatch=FillToolMessage::FillSecondaryColor), entry!(KeyUp(MouseLeft); action_dispatch=FillToolMessage::PointerUp), diff --git a/editor/src/messages/tool/tool_messages/fill_tool.rs b/editor/src/messages/tool/tool_messages/fill_tool.rs index daaa5c4da7..5ca8dfe320 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -95,7 +95,8 @@ impl Fsm for FillToolFsmState { // Only highlight layers if the viewport is not being panned (middle mouse button is pressed) // TODO: Don't use `Key::MouseMiddle` directly, instead take it as a variable from the input mappings list like in all other places; or find a better way than checking the key state if !input.keyboard.get(Key::MouseMiddle as usize) { - let preview_color = global_tool_data.primary_color; + 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 let click = document.click(input); From d474312444846d0a5dffe185cfcc42b868163930 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Wed, 23 Apr 2025 18:22:43 -0700 Subject: [PATCH 18/18] Code review --- editor/Cargo.toml | 6 ++- .../document/overlays/utility_types.rs | 39 +++++++++++-------- .../messages/tool/tool_messages/fill_tool.rs | 19 ++++----- .../messages/tool/tool_messages/path_tool.rs | 2 +- .../messages/tool/tool_messages/pen_tool.rs | 10 +++-- .../tool/tool_messages/select_tool.rs | 6 +-- .../messages/tool/tool_messages/text_tool.rs | 4 +- node-graph/gcore/src/raster/color.rs | 10 ----- .../src/vector/vector_data/attributes.rs | 9 +++-- 9 files changed, 52 insertions(+), 53 deletions(-) diff --git a/editor/Cargo.toml b/editor/Cargo.toml index 9a076a01fb..2191161bcb 100644 --- a/editor/Cargo.toml +++ b/editor/Cargo.toml @@ -30,7 +30,9 @@ ron = ["dep:ron"] # Local dependencies graphite-proc-macros = { path = "../proc-macros" } graph-craft = { path = "../node-graph/graph-craft" } -interpreted-executor = { path = "../node-graph/interpreted-executor", features = ["serde"] } +interpreted-executor = { path = "../node-graph/interpreted-executor", features = [ + "serde", +] } graphene-core = { path = "../node-graph/gcore" } graphene-std = { path = "../node-graph/gstd", features = ["serde"] } @@ -58,9 +60,9 @@ web-sys = { workspace = true, features = [ "Element", "HtmlCanvasElement", "CanvasRenderingContext2d", - "OffscreenCanvasRenderingContext2d", "CanvasPattern", "OffscreenCanvas", + "OffscreenCanvasRenderingContext2d", "TextMetrics", ] } diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index 0cc4624b4a..2ddb8169db 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -449,7 +449,7 @@ impl OverlayContext { self.end_dpi_aware_transform(); } - /// This is used by the pen and path tool to outline the path of the shape + /// 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.start_dpi_aware_transform(); @@ -468,7 +468,7 @@ impl OverlayContext { self.end_dpi_aware_transform(); } - /// This is used by the pen tool in order to show how the bezier curve would look like + /// Used by the Pen tool in order to show how the bezier curve would look like. pub fn outline_bezier(&mut self, bezier: Bezier, transform: DAffine2) { self.start_dpi_aware_transform(); @@ -547,7 +547,7 @@ impl OverlayContext { self.end_dpi_aware_transform(); } - /// This is used by the select tool to outline a path selected or hovered + /// Used by the Select tool to outline a path selected or hovered. pub fn outline(&mut self, subpaths: impl Iterator>>, transform: DAffine2) { self.push_path(subpaths, transform); @@ -555,8 +555,8 @@ impl OverlayContext { self.render_context.stroke(); } - /// Fills the area inside the path. `color` is in gamma space - /// This is used by the pen tool to show the path being closed + /// 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); @@ -564,14 +564,14 @@ impl OverlayContext { self.render_context.fill(); } - /// Fills the area inside the path with a pattern. `color` is in gamma space - /// This is used by the fill tool to show the area to be filled + /// 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) { - let pattern_width = 4; - let pattern_height = 4; - let pattern_canvas = OffscreenCanvas::new(pattern_width, pattern_height).unwrap(); + const PATTERN_WIDTH: usize = 4; + const PATTERN_HEIGHT: usize = 4; - let pattern_ctx: OffscreenCanvasRenderingContext2d = pattern_canvas + 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() @@ -579,17 +579,22 @@ impl OverlayContext { .dyn_into() .expect("Context should be a canvas 2d context"); - let mut data = vec![0u8; (4 * pattern_width * pattern_height) as usize]; // 4x4 pixels, 4 components (RGBA) per pixel + // 4x4 pixels, 4 components (RGBA) per pixel + let mut data = [0_u8; 4 * PATTERN_WIDTH * PATTERN_HEIGHT]; - let color = color.to_rgba8_srgb(); + // ┌▄▄┬──┬──┬──┐ + // ├▀▀┼──┼──┼──┤ + // ├──┼──┼▄▄┼──┤ + // ├──┼──┼▀▀┼──┤ + // └──┴──┴──┴──┘ 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); + 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(&data), pattern_width, pattern_height).unwrap(); - pattern_ctx.put_image_data(&image_data, 0.0, 0.0).unwrap(); + 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.push_path(subpaths, transform); diff --git a/editor/src/messages/tool/tool_messages/fill_tool.rs b/editor/src/messages/tool/tool_messages/fill_tool.rs index 5ca8dfe320..0eae252bcd 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -92,18 +92,15 @@ impl Fsm for FillToolFsmState { let ToolMessage::Fill(event) = event else { return self }; match (self, event) { (_, FillToolMessage::Overlays(mut overlay_context)) => { - // Only highlight layers if the viewport is not being panned (middle mouse button is pressed) - // TODO: Don't use `Key::MouseMiddle` directly, instead take it as a variable from the input mappings list like in all other places; or find a better way than checking the key state - if !input.keyboard.get(Key::MouseMiddle as usize) { - let use_secondary = input.keyboard.get(Key::Shift as usize); - let preview_color = if use_secondary { global_tool_data.secondary_color } else { global_tool_data.primary_color }; - - // Get the layer the user is hovering over - let click = document.click(input); - if let Some(layer) = click { - overlay_context.fill_path_pattern(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer), &preview_color); - } + // Choose the working color to preview + let use_secondary = input.keyboard.get(Key::Shift as usize); + let preview_color = if use_secondary { global_tool_data.secondary_color } else { global_tool_data.primary_color }; + + // 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); } + self } (_, FillToolMessage::PointerMove | FillToolMessage::WorkingColorChanged) => { diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index 97ee44ace1..537f3fd091 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -1030,7 +1030,7 @@ impl Fsm for PathToolFsmState { match self { Self::Drawing { selection_shape } => { - let mut fill_color = graphene_std::Color::from_rgb_str(crate::consts::COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()) + 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(); diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index 5382aa45c0..0f9e7cc0c2 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -1614,7 +1614,7 @@ impl Fsm for PenToolFsmState { overlay_context.manipulator_anchor(next_anchor, false, None); } - // Fill the shape if the new point closes the path + // 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); @@ -1631,7 +1631,7 @@ impl Fsm for PenToolFsmState { }) }); - // We have the point. Join the 2 vertices and check if any path is closed + // We have the point. Join the 2 vertices and check if any path is closed. if let Some(end) = closest_point { let segment_id = SegmentId::generate(); vector_data.push(segment_id, start, end, BezierHandles::Cubic { handle_start, handle_end }, StrokeId::ZERO); @@ -1652,7 +1652,11 @@ impl Fsm for PenToolFsmState { }) .collect(); - let fill_color = Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()).unwrap().with_alpha(0.05).to_css_from_gamma(); + 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, '#'); overlay_context.fill_path(subpaths.iter(), transform, fill_color.as_str()); } } diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 191c9c03ef..277172dfcc 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -2,8 +2,8 @@ use super::tool_prelude::*; use crate::consts::{ - COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COMPASS_ROSE_HOVER_RING_DIAMETER, DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD, RESIZE_HANDLE_SIZE, ROTATE_INCREMENT, SELECTION_DRAG_ANGLE, - SELECTION_TOLERANCE, + COLOR_OVERLAY_BLUE, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COMPASS_ROSE_HOVER_RING_DIAMETER, DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD, RESIZE_HANDLE_SIZE, ROTATE_INCREMENT, + SELECTION_DRAG_ANGLE, SELECTION_TOLERANCE, }; use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition; use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; @@ -759,7 +759,7 @@ impl Fsm for SelectToolFsmState { } // Update the selection box - let mut fill_color = graphene_std::Color::from_rgb_str(crate::consts::COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()) + 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(); diff --git a/editor/src/messages/tool/tool_messages/text_tool.rs b/editor/src/messages/tool/tool_messages/text_tool.rs index b1c5fe5a73..9e144ae6dc 100644 --- a/editor/src/messages/tool/tool_messages/text_tool.rs +++ b/editor/src/messages/tool/tool_messages/text_tool.rs @@ -1,7 +1,7 @@ #![allow(clippy::too_many_arguments)] use super::tool_prelude::*; -use crate::consts::{COLOR_OVERLAY_RED, DRAG_THRESHOLD}; +use crate::consts::{COLOR_OVERLAY_BLUE, COLOR_OVERLAY_RED, DRAG_THRESHOLD}; use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; @@ -456,7 +456,7 @@ impl Fsm for TextToolFsmState { font_cache, .. } = transition_data; - let fill_color = graphene_std::Color::from_rgb_str(crate::consts::COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()) + let fill_color = graphene_std::Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()) .unwrap() .with_alpha(0.05) .to_rgba_hex_srgb(); diff --git a/node-graph/gcore/src/raster/color.rs b/node-graph/gcore/src/raster/color.rs index eb2c577176..3be18d1952 100644 --- a/node-graph/gcore/src/raster/color.rs +++ b/node-graph/gcore/src/raster/color.rs @@ -955,16 +955,6 @@ impl Color { } } - /// Produces a CSS color in the format `rgb(red green blue / alpha%)`. Use this if the [`Color`] is in linear space. - pub fn to_css(&self) -> String { - self.to_gamma_srgb().to_css_from_gamma() - } - - /// Produces a CSS color in the format `rgb(red green blue / alpha%)`. Use this if the [`Color`] is in gamma space. - pub fn to_css_from_gamma(&self) -> String { - format!("rgb({} {} {} / {}%)", self.red * 255.0, self.green * 255.0, self.blue * 255.0, self.alpha * 100.0) - } - #[inline(always)] pub fn srgb_to_linear(channel: f32) -> f32 { if channel <= 0.04045 { channel / 12.92 } else { ((channel + 0.055) / 1.055).powf(2.4) } diff --git a/node-graph/gcore/src/vector/vector_data/attributes.rs b/node-graph/gcore/src/vector/vector_data/attributes.rs index 022853b990..d5e1fe0241 100644 --- a/node-graph/gcore/src/vector/vector_data/attributes.rs +++ b/node-graph/gcore/src/vector/vector_data/attributes.rs @@ -584,7 +584,7 @@ impl HalfEdge { reverse: false, } } else { - self.clone() + *self } } } @@ -691,7 +691,7 @@ impl VectorData { for seg_ref in segments { let (start, end) = (seg_ref.start, seg_ref.end); - if previous.map_or(false, |(_, prev_end)| start == prev_end) { + if previous.is_some_and(|(_, prev_end)| start == prev_end) { if let Some(path) = current_path.as_mut() { path.push(seg_ref); } @@ -784,6 +784,7 @@ impl VectorData { }); } } + Some(bezier_rs::Subpath::new(groups, closed)) } @@ -865,12 +866,12 @@ impl VectorData { /// Construct a [`bezier_rs::Bezier`] curve for stroke. pub fn stroke_bezier_paths(&self) -> impl Iterator> { - self.build_stroke_path_iter().into_iter().map(|(group, closed)| bezier_rs::Subpath::new(group, closed)) + self.build_stroke_path_iter().map(|(group, closed)| bezier_rs::Subpath::new(group, closed)) } /// Construct a [`kurbo::BezPath`] curve for stroke. pub fn stroke_bezpath_iter(&self) -> impl Iterator { - self.build_stroke_path_iter().into_iter().map(|(group, closed)| { + self.build_stroke_path_iter().map(|(group, closed)| { let mut bezpath = kurbo::BezPath::new(); let mut out_handle;