From aa5af815697b5cd1060740dd924048803b482c8d Mon Sep 17 00:00:00 2001 From: Adesh Gupta Date: Fri, 1 Aug 2025 13:28:39 +0530 Subject: [PATCH 1/4] Add Merge Point message --- .../messages/tool/tool_messages/path_tool.rs | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index e04b75d3f4..9ef50791fc 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -119,6 +119,7 @@ pub enum PathToolMessage { UpdateSelectedPointsStatus { overlay_context: OverlayContext, }, + MergeSelectedPoints, } #[derive(PartialEq, Eq, Hash, Copy, Clone, Debug, Default, serde::Serialize, serde::Deserialize, specta::Type)] @@ -274,6 +275,11 @@ impl LayoutHolder for PathTool { .disabled(!self.tool_data.single_path_node_compatible_layer_selected) .widget_holder(); + let merge_button = IconButton::new("Folder", 24) + .tooltip("Merge selected points") + .on_update(|_| PathToolMessage::MergeSelectedPoints.into()) + .widget_holder(); + let [_checkbox, _dropdown] = { let pivot_gizmo_type_widget = pivot_gizmo_type_widget(self.tool_data.pivot_gizmo.state, PivotToolSource::Path); [pivot_gizmo_type_widget[0].clone(), pivot_gizmo_type_widget[2].clone()] @@ -305,6 +311,9 @@ impl LayoutHolder for PathTool { path_overlay_mode_widget, unrelated_seperator.clone(), path_node_button, + unrelated_seperator.clone(), + merge_button, + unrelated_seperator.clone(), // checkbox.clone(), // related_seperator.clone(), // dropdown.clone(), @@ -399,6 +408,7 @@ impl<'a> MessageHandler> for Path DeleteAndBreakPath, ClosePath, PointerMove, + MergeSelectedPoints, ), PathToolFsmState::Dragging(_) => actions!(PathToolMessageDiscriminant; Escape, @@ -2454,6 +2464,77 @@ impl Fsm for PathToolFsmState { self } + (_, PathToolMessage::MergeSelectedPoints) => { + // Get all the selected points and merge the selected points + // Assuming that all these points are on the same layer + + //TODO: Check here that there are more than two points selected and all are within certain threshold + + if let Some(layer) = shape_editor.selected_layers().next() { + responses.add(DocumentMessage::AddTransaction); + let state = shape_editor.selected_shape_state.get(layer).expect("No state for selected layer"); + let points = state + .selected_points() + .filter_map(|point| if let ManipulatorPointId::Anchor(anchor) = point { Some(anchor) } else { None }); + + // Calculate the centroid + + if let Some(vector_data) = document.network_interface.compute_modified_vector(*layer) { + let positions = points.filter_map(|point| ManipulatorPointId::Anchor(point).get_position(&vector_data)); + + let mut sum = DVec2::default(); + let mut count = 0 as f64; + + for position in positions { + sum += position; + count += 1.; + } + + let centroid = sum / count; + + // Add a new point with the new coordinates + let new_id = PointId::generate(); + let modification_type = VectorModificationType::InsertPoint { id: new_id, position: centroid }; + responses.add(GraphOperationMessage::Vector { layer: *layer, modification_type }); + + // Remove old points + for point in state + .selected_points() + .filter_map(|point| if let ManipulatorPointId::Anchor(anchor) = point { Some(anchor) } else { None }) + { + let modification_type = VectorModificationType::RemovePoint { id: point }; + responses.add(GraphOperationMessage::Vector { layer: *layer, modification_type }); + } + + // Find those segments which were connected to just one of the selected points + for (_, bezier, start, end) in vector_data.segment_bezier_iter() { + if state.is_point_selected(ManipulatorPointId::Anchor(start)) { + let id = SegmentId::generate(); + let points = [new_id, end]; + let handles = match bezier.handles { + BezierHandles::Linear => [None, None], + BezierHandles::Quadratic { handle } => [Some(handle - bezier.start), None], + BezierHandles::Cubic { handle_start, handle_end } => [Some(handle_start - bezier.start), Some(handle_end - bezier.end)], + }; + let modification_type = VectorModificationType::InsertSegment { id, points, handles }; + responses.add(GraphOperationMessage::Vector { layer: *layer, modification_type }); + } else if state.is_point_selected(ManipulatorPointId::Anchor(end)) { + let id = SegmentId::generate(); + let points = [start, new_id]; + let handles = match bezier.handles { + BezierHandles::Linear => [None, None], + BezierHandles::Quadratic { handle } => [Some(handle - bezier.start), None], + BezierHandles::Cubic { handle_start, handle_end } => [Some(handle_start - bezier.start), Some(handle_end - bezier.end)], + }; + let modification_type = VectorModificationType::InsertSegment { id, points, handles }; + responses.add(GraphOperationMessage::Vector { layer: *layer, modification_type }); + } + } + } + } + + PathToolFsmState::Ready + } (_, _) => PathToolFsmState::Ready, } } From 2b67cddb550887df1eb8c5411be1a793b257e3d7 Mon Sep 17 00:00:00 2001 From: Adesh Gupta Date: Fri, 8 Aug 2025 18:25:12 +0000 Subject: [PATCH 2/4] Add merge button disable --- editor/src/consts.rs | 1 + .../messages/tool/tool_messages/path_tool.rs | 182 +++++++++++++----- 2 files changed, 130 insertions(+), 53 deletions(-) diff --git a/editor/src/consts.rs b/editor/src/consts.rs index 44a5b1d210..8597e169da 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -108,6 +108,7 @@ pub const SEGMENT_INSERTION_DISTANCE: f64 = 5.; pub const SEGMENT_OVERLAY_SIZE: f64 = 10.; pub const SEGMENT_SELECTED_THICKNESS: f64 = 3.; pub const HANDLE_LENGTH_FACTOR: f64 = 0.5; +pub const POINT_MERGE_THRESHOLD: f64 = 10.; // PEN TOOL pub const CREATE_CURVE_THRESHOLD: f64 = 5.; diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index 0d29a4bac8..ceb73fa30b 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -2,7 +2,7 @@ use super::select_tool::extend_lasso; use super::tool_prelude::*; use crate::consts::{ COLOR_OVERLAY_BLUE, COLOR_OVERLAY_GRAY, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, DEFAULT_STROKE_WIDTH, DOUBLE_CLICK_MILLISECONDS, DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD, DRAG_THRESHOLD, - DRILL_THROUGH_THRESHOLD, HANDLE_ROTATE_SNAP_ANGLE, SEGMENT_INSERTION_DISTANCE, SEGMENT_OVERLAY_SIZE, SELECTION_THRESHOLD, SELECTION_TOLERANCE, + DRILL_THROUGH_THRESHOLD, HANDLE_ROTATE_SNAP_ANGLE, POINT_MERGE_THRESHOLD, SEGMENT_INSERTION_DISTANCE, SEGMENT_OVERLAY_SIZE, SELECTION_THRESHOLD, SELECTION_TOLERANCE, }; use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type; @@ -297,6 +297,7 @@ impl LayoutHolder for PathTool { let merge_button = IconButton::new("Folder", 24) .tooltip("Merge selected points") .on_update(|_| PathToolMessage::MergeSelectedPoints.into()) + .disabled(!self.tool_data.merging_points_enabled) .widget_holder(); let [_checkbox, _dropdown] = { @@ -581,6 +582,7 @@ struct PathToolData { hovered_layers: Vec, ghost_outline: Vec<(Vec, LayerNodeIdentifier)>, make_path_editable_is_allowed: bool, + merging_points_enabled: bool, } impl PathToolData { @@ -642,6 +644,64 @@ impl PathToolData { self.selection_status = selection_status; } + fn update_merge_point_toggle(&mut self, shape_editor: &ShapeState, document: &DocumentMessageHandler, vector_meshes: bool) { + let mut non_empty_layers = shape_editor.selected_shape_state.iter().filter(|(_, state)| !state.is_empty()); + let Some((layer, _)) = non_empty_layers.next() else { + self.merging_points_enabled = false; + return; + }; + + if non_empty_layers.next().is_some() { + self.merging_points_enabled = false; + return; + } + + if !vector_meshes { + // Check that only two points are selected such that they are endpoints + let all_anchors = shape_editor.selected_points().all(|point| matches!(point, ManipulatorPointId::Anchor(_))); + let points = shape_editor + .selected_points() + .filter_map(|point| if let ManipulatorPointId::Anchor(anchor) = point { Some(anchor) } else { None }) + .collect::>(); + + if points.len() == 2 && all_anchors { + let Some(layer) = shape_editor.selected_layers().next() else { return }; + let Some(vector) = document.network_interface.compute_modified_vector(*layer) else { return }; + if points.iter().all(|point| vector.all_connected(**point).count() == 1) { + self.merging_points_enabled = true; + return; + } + } + self.merging_points_enabled = false; + return; + } + + let points = shape_editor.selected_points().collect::>(); + let all_anchors = points.iter().all(|point| matches!(point, ManipulatorPointId::Anchor(_))); + + if points.len() < 2 || !all_anchors { + self.merging_points_enabled = false; + return; + } + + let Some(vector) = document.network_interface.compute_modified_vector(*layer) else { return }; + let positions = points.iter().filter_map(|point| point.get_position(&vector)).collect::>(); + + let mut sum = DVec2::default(); + for position in &positions { + sum += position; + } + let centroid = sum / (positions.len() as f64); + + for position in positions { + if position.distance(centroid) > POINT_MERGE_THRESHOLD { + self.merging_points_enabled = false; + return; + } + } + self.merging_points_enabled = true; + } + fn remove_saved_points(&mut self) { self.saved_points_before_anchor_select_toggle.clear(); } @@ -3019,6 +3079,10 @@ impl Fsm for PathToolFsmState { tool_data.make_path_editable_is_allowed = make_path_editable_is_allowed(&document.network_interface, document.metadata()).is_some(); tool_data.update_selection_status(shape_editor, document); + tool_data.update_merge_point_toggle(shape_editor, document, tool_action_data.preferences.vector_meshes); + + // TODO: Here add a toggle for the disable of the merge points button + self } (_, PathToolMessage::ManipulatorMakeHandlesColinear) => { @@ -3047,71 +3111,83 @@ impl Fsm for PathToolFsmState { self } (_, PathToolMessage::MergeSelectedPoints) => { - // Get all the selected points and merge the selected points // Assuming that all these points are on the same layer + let mut non_empty_layers = shape_editor.selected_shape_state.iter().filter(|(_, state)| !state.is_empty()); - //TODO: Check here that there are more than two points selected and all are within certain threshold - - if let Some(layer) = shape_editor.selected_layers().next() { - responses.add(DocumentMessage::AddTransaction); - let state = shape_editor.selected_shape_state.get(layer).expect("No state for selected layer"); - let points = state - .selected_points() - .filter_map(|point| if let ManipulatorPointId::Anchor(anchor) = point { Some(anchor) } else { None }); + // If all layers are empty, or no layer selected + let Some((layer, _)) = non_empty_layers.next() else { + return PathToolFsmState::Ready; + }; - // Calculate the centroid + // If selected points are of more than one layer + if non_empty_layers.next().is_some() { + return PathToolFsmState::Ready; + } - if let Some(vector_data) = document.network_interface.compute_modified_vector(*layer) { - let positions = points.filter_map(|point| ManipulatorPointId::Anchor(point).get_position(&vector_data)); + let points = shape_editor.selected_points().collect::>(); + let all_anchors = points.iter().all(|point| matches!(point, ManipulatorPointId::Anchor(_))); + if points.len() < 2 || !all_anchors { + return PathToolFsmState::Ready; + } - let mut sum = DVec2::default(); - let mut count = 0 as f64; + responses.add(DocumentMessage::StartTransaction); + let state = shape_editor.selected_shape_state.get(layer).expect("No state for selected layer"); + let points = state + .selected_points() + .filter_map(|point| if let ManipulatorPointId::Anchor(anchor) = point { Some(anchor) } else { None }); + + // Calculate the centroid + if let Some(vector) = document.network_interface.compute_modified_vector(*layer) { + let positions = points.filter_map(|point| ManipulatorPointId::Anchor(point).get_position(&vector)).collect::>(); + let mut sum = DVec2::default(); + for position in &positions { + sum += position; + } + let centroid = sum / (positions.len() as f64); - for position in positions { - sum += position; - count += 1.; + for position in &positions { + if position.distance(centroid) > POINT_MERGE_THRESHOLD { + return PathToolFsmState::Ready; } + } - let centroid = sum / count; + // Add a new point with the new coordinates + let new_id = PointId::generate(); + let modification_type = VectorModificationType::InsertPoint { id: new_id, position: centroid }; + responses.add(GraphOperationMessage::Vector { layer: *layer, modification_type }); - // Add a new point with the new coordinates - let new_id = PointId::generate(); - let modification_type = VectorModificationType::InsertPoint { id: new_id, position: centroid }; + // Remove old points + for point in state + .selected_points() + .filter_map(|point| if let ManipulatorPointId::Anchor(anchor) = point { Some(anchor) } else { None }) + { + let modification_type = VectorModificationType::RemovePoint { id: point }; responses.add(GraphOperationMessage::Vector { layer: *layer, modification_type }); + } - // Remove old points - for point in state - .selected_points() - .filter_map(|point| if let ManipulatorPointId::Anchor(anchor) = point { Some(anchor) } else { None }) - { - let modification_type = VectorModificationType::RemovePoint { id: point }; - responses.add(GraphOperationMessage::Vector { layer: *layer, modification_type }); - } - - // Find those segments which were connected to just one of the selected points - for (_, bezier, start, end) in vector_data.segment_bezier_iter() { - if state.is_point_selected(ManipulatorPointId::Anchor(start)) { - let id = SegmentId::generate(); - let points = [new_id, end]; - let handles = match bezier.handles { - BezierHandles::Linear => [None, None], - BezierHandles::Quadratic { handle } => [Some(handle - bezier.start), None], - BezierHandles::Cubic { handle_start, handle_end } => [Some(handle_start - bezier.start), Some(handle_end - bezier.end)], - }; - let modification_type = VectorModificationType::InsertSegment { id, points, handles }; - responses.add(GraphOperationMessage::Vector { layer: *layer, modification_type }); - } else if state.is_point_selected(ManipulatorPointId::Anchor(end)) { - let id = SegmentId::generate(); - let points = [start, new_id]; - let handles = match bezier.handles { - BezierHandles::Linear => [None, None], - BezierHandles::Quadratic { handle } => [Some(handle - bezier.start), None], - BezierHandles::Cubic { handle_start, handle_end } => [Some(handle_start - bezier.start), Some(handle_end - bezier.end)], - }; - let modification_type = VectorModificationType::InsertSegment { id, points, handles }; - responses.add(GraphOperationMessage::Vector { layer: *layer, modification_type }); + // Find those segments which were connected to just one of the selected points + for (_, bezier, start, end) in vector.segment_bezier_iter() { + let id = SegmentId::generate(); + let handles = |handles: BezierHandles| -> [Option; 2] { + match handles { + BezierHandles::Linear => [None, None], + BezierHandles::Quadratic { handle } => [Some(handle - bezier.start), None], + BezierHandles::Cubic { handle_start, handle_end } => [Some(handle_start - bezier.start), Some(handle_end - bezier.end)], } + }; + + if state.is_point_selected(ManipulatorPointId::Anchor(start)) { + let points = [new_id, end]; + let handles = handles(bezier.handles); + let modification_type = VectorModificationType::InsertSegment { id, points, handles }; + responses.add(GraphOperationMessage::Vector { layer: *layer, modification_type }); + } else if state.is_point_selected(ManipulatorPointId::Anchor(end)) { + let points = [start, new_id]; + let handles = handles(bezier.handles); + let modification_type = VectorModificationType::InsertSegment { id, points, handles }; + responses.add(GraphOperationMessage::Vector { layer: *layer, modification_type }); } + responses.add(DocumentMessage::EndTransaction); } } From 17f8d2e17e6b3b06bcd66e23cc07a507b493925d Mon Sep 17 00:00:00 2001 From: Adesh Gupta Date: Sun, 10 Aug 2025 20:09:41 +0000 Subject: [PATCH 3/4] Fix transaction issue --- .../messages/tool/tool_messages/path_tool.rs | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index ceb73fa30b..778812c48f 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -3165,30 +3165,31 @@ impl Fsm for PathToolFsmState { responses.add(GraphOperationMessage::Vector { layer: *layer, modification_type }); } + let handles = |bezier: Bezier| -> [Option; 2] { + match bezier.handles { + BezierHandles::Linear => [None, None], + BezierHandles::Quadratic { handle } => [Some(handle - bezier.start), None], + BezierHandles::Cubic { handle_start, handle_end } => [Some(handle_start - bezier.start), Some(handle_end - bezier.end)], + } + }; + // Find those segments which were connected to just one of the selected points for (_, bezier, start, end) in vector.segment_bezier_iter() { let id = SegmentId::generate(); - let handles = |handles: BezierHandles| -> [Option; 2] { - match handles { - BezierHandles::Linear => [None, None], - BezierHandles::Quadratic { handle } => [Some(handle - bezier.start), None], - BezierHandles::Cubic { handle_start, handle_end } => [Some(handle_start - bezier.start), Some(handle_end - bezier.end)], - } - }; if state.is_point_selected(ManipulatorPointId::Anchor(start)) { let points = [new_id, end]; - let handles = handles(bezier.handles); + let handles = handles(bezier); let modification_type = VectorModificationType::InsertSegment { id, points, handles }; responses.add(GraphOperationMessage::Vector { layer: *layer, modification_type }); } else if state.is_point_selected(ManipulatorPointId::Anchor(end)) { let points = [start, new_id]; - let handles = handles(bezier.handles); + let handles = handles(bezier); let modification_type = VectorModificationType::InsertSegment { id, points, handles }; responses.add(GraphOperationMessage::Vector { layer: *layer, modification_type }); } - responses.add(DocumentMessage::EndTransaction); } + responses.add(DocumentMessage::EndTransaction); } PathToolFsmState::Ready From 6fd0aaab8ed19843713f404ca94edf1bc2d813fc Mon Sep 17 00:00:00 2001 From: Adesh Gupta Date: Mon, 18 Aug 2025 20:18:03 +0000 Subject: [PATCH 4/4] Fix import error --- editor/src/messages/tool/tool_messages/path_tool.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index 016e31a47e..b7ec117c64 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -23,7 +23,7 @@ use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandi use crate::messages::tool::common_functionality::utility_functions::{calculate_segment_angle, find_two_param_best_approximate, make_path_editable_is_allowed}; use graphene_std::Color; use graphene_std::renderer::Quad; -use graphene_std::subpath::pathseg_points; +use graphene_std::subpath::{Bezier, BezierHandles, pathseg_points}; use graphene_std::transform::ReferencePoint; use graphene_std::uuid::NodeId; use graphene_std::vector::algorithms::util::pathseg_tangent;