From 6a8ff4d0e6ba97d272bfd4f4fd92b048835ab978 Mon Sep 17 00:00:00 2001 From: Adesh Gupta Date: Sun, 10 Aug 2025 10:28:53 +0000 Subject: [PATCH 1/2] Add MergeSegments Message in Path tool --- .../messages/tool/tool_messages/path_tool.rs | 158 +++++++++++++++++- 1 file changed, 157 insertions(+), 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 00c99bd97..eb51d210b 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -138,6 +138,7 @@ pub enum PathToolMessage { Duplicate, TogglePointEditing, ToggleSegmentEditing, + MergeSelectedSegments, } #[derive(PartialEq, Eq, Hash, Copy, Clone, Debug, Default, serde::Serialize, serde::Deserialize, specta::Type)] @@ -293,6 +294,12 @@ impl LayoutHolder for PathTool { .disabled(!self.tool_data.make_path_editable_is_allowed) .widget_holder(); + let merge_segments_button = IconButton::new("Folder", 24) + .tooltip("Merge selected selgments") + .on_update(|_| PathToolMessage::MergeSelectedSegments.into()) + .disabled(!self.tool_data.merging_segments_enabled) + .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()] @@ -324,6 +331,9 @@ impl LayoutHolder for PathTool { path_overlay_mode_widget, unrelated_seperator.clone(), path_node_button, + unrelated_seperator.clone(), + merge_segments_button, + unrelated_seperator.clone(), // checkbox.clone(), // related_seperator.clone(), // dropdown.clone(), @@ -424,7 +434,8 @@ impl<'a> MessageHandler> for Path Paste, Duplicate, TogglePointEditing, - ToggleSegmentEditing + ToggleSegmentEditing, + MergeSelectedSegments ), PathToolFsmState::Dragging(_) => actions!(PathToolMessageDiscriminant; Escape, @@ -571,6 +582,7 @@ struct PathToolData { hovered_layers: Vec, ghost_outline: Vec<(Vec, LayerNodeIdentifier)>, make_path_editable_is_allowed: bool, + merging_segments_enabled: bool, } impl PathToolData { @@ -632,6 +644,11 @@ impl PathToolData { self.selection_status = selection_status; } + fn update_merge_segments_toggle(&mut self, _shape_editor: &mut ShapeState, _document: &DocumentMessageHandler) { + //TODO: Implement this + self.merging_segments_enabled = true; + } + fn remove_saved_points(&mut self) { self.saved_points_before_anchor_select_toggle.clear(); } @@ -3000,6 +3017,144 @@ impl Fsm for PathToolFsmState { } PathToolFsmState::Ready } + (_, PathToolMessage::MergeSelectedSegments) => { + // TODO: If two segments are selected and they are close within certain threshold then we need to merge both of them together + + // 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()); + + // If all layers are empty, or no layer selected + let Some((layer, _)) = non_empty_layers.next() else { + return PathToolFsmState::Ready; + }; + + // If selected segments are of more than one layer + if non_empty_layers.next().is_some() { + return PathToolFsmState::Ready; + } + + // Given two segments and they are close we will create new points at the midpoint of their endpoints + let segments = shape_editor.selected_segments().collect::>(); + + if segments.len() != 2 { + return PathToolFsmState::Ready; + } + let segment1 = segments[0]; + let segment2 = segments[1]; + + let Some(vector) = document.network_interface.compute_modified_vector(*layer) else { + return PathToolFsmState::Ready; + }; + + responses.add(DocumentMessage::StartTransaction); + + let (_, bezier1, start1, end1) = vector.segment_bezier_iter().find(|(id, _, _, _)| id == segment1).expect(" Couldn't get segment data"); + let (_, bezier2, start2, end2) = vector.segment_bezier_iter().find(|(id, _, _, _)| id == segment2).expect(" Couldn't get segment data"); + + // Two cases + let mut bezier2 = bezier2; + let mut start2 = start2; + let mut end2 = end2; + let dist1 = bezier1.start.distance(bezier2.start); + let dist2 = bezier1.start.distance(bezier2.end); + + let new_id1 = PointId::generate(); + let new_id2 = PointId::generate(); + + if dist1 > dist2 { + // Invert bezier2 and swap start2 and end2 + bezier2 = bezier2.reverse(); + let bin = start2; + start2 = end2; + end2 = bin; + } + + // newpoint1: start1 -> start2, newpoint2: end1 -> end2 + let pos1 = ManipulatorPointId::Anchor(start1).get_position(&vector).expect("No position for point"); + let pos2 = ManipulatorPointId::Anchor(start2).get_position(&vector).expect("No position for point"); + + let new_pos1 = (pos1 + pos2) / 2.; + + let pos1 = ManipulatorPointId::Anchor(end1).get_position(&vector).expect("No position for point"); + let pos2 = ManipulatorPointId::Anchor(end2).get_position(&vector).expect("No position for point"); + + let new_pos2 = (pos1 + pos2) / 2.; + + // Add the two new points + let modification_type = VectorModificationType::InsertPoint { id: new_id1, position: new_pos1 }; + responses.add(GraphOperationMessage::Vector { layer: *layer, modification_type }); + let modification_type = VectorModificationType::InsertPoint { id: new_id2, position: new_pos2 }; + responses.add(GraphOperationMessage::Vector { layer: *layer, modification_type }); + + // Remove old points + let points_to_remove = [start1, start2, end1, end2]; + for point in points_to_remove { + let modification_type = VectorModificationType::RemovePoint { id: point }; + responses.add(GraphOperationMessage::Vector { layer: *layer, modification_type }); + } + + // Connect the segments to their respective anchors + 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)], + } + }; + + for (_, bezier, start, end) in vector.segment_bezier_iter() { + let id = SegmentId::generate(); + + // Connecting the segments to the start point + if start == start1 || start == start2 { + let points = [new_id1, end]; + let handles = handles(bezier); + let modification_type = VectorModificationType::InsertSegment { id, points, handles }; + responses.add(GraphOperationMessage::Vector { layer: *layer, modification_type }); + } else if end == start1 || end == start2 { + let points = [start, new_id1]; + let handles = handles(bezier); + let modification_type = VectorModificationType::InsertSegment { id, points, handles }; + responses.add(GraphOperationMessage::Vector { layer: *layer, modification_type }); + } + + // Connecting handles to the end point of new segement + if start == end1 || start == end2 { + let points = [new_id2, end]; + let handles = handles(bezier); + let modification_type = VectorModificationType::InsertSegment { id, points, handles }; + responses.add(GraphOperationMessage::Vector { layer: *layer, modification_type }); + } else if end == end1 || end == end2 { + let points = [start, new_id2]; + let handles = handles(bezier); + let modification_type = VectorModificationType::InsertSegment { id, points, handles }; + responses.add(GraphOperationMessage::Vector { layer: *layer, modification_type }); + } + } + + // Adding the new segment + let handles1 = handles(bezier1); + let handles2 = handles(bezier2); + + // Now we construct a conmbined handles for the new segment + let mut new_handles = [Some(DVec2::ZERO), Some(DVec2::ZERO)]; + let mut index = 0; + for (handle1, handle2) in handles1.iter().zip(handles2) { + let pos1 = handle1.unwrap_or_default(); + let pos2 = handle2.unwrap_or_default(); + let new_pos = if pos1 == DVec2::default() && pos2 == DVec2::default() { None } else { Some((pos1 + pos2) / 2.) }; + new_handles[index] = new_pos; + index += 1; + } + + let id = SegmentId::generate(); + let points = [new_id1, new_id2]; + let modification_type = VectorModificationType::InsertSegment { id, points, handles: new_handles }; + responses.add(GraphOperationMessage::Vector { layer: *layer, modification_type }); + + responses.add(DocumentMessage::EndTransaction); + PathToolFsmState::Ready + } (_, PathToolMessage::SelectedPointUpdated) => { let colinear = shape_editor.selected_manipulator_angles(&document.network_interface); tool_data.dragging_state = DraggingState { @@ -3009,6 +3164,7 @@ 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_segments_toggle(shape_editor, document); self } (_, PathToolMessage::ManipulatorMakeHandlesColinear) => { From b2653b566e5947ce0f533ee649c332a1f94d024a Mon Sep 17 00:00:00 2001 From: Adesh Gupta Date: Sun, 10 Aug 2025 18:09:51 +0000 Subject: [PATCH 2/2] Add toggle for button --- .../messages/tool/tool_messages/path_tool.rs | 56 +++++++++++++++---- 1 file changed, 44 insertions(+), 12 deletions(-) diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index eb51d210b..eb26a618e 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -644,8 +644,47 @@ impl PathToolData { self.selection_status = selection_status; } - fn update_merge_segments_toggle(&mut self, _shape_editor: &mut ShapeState, _document: &DocumentMessageHandler) { - //TODO: Implement this + fn update_merge_segments_toggle(&mut self, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, vector_meshes: bool) { + if !vector_meshes { + self.merging_segments_enabled = false; + return; + } + + 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_segments_enabled = false; + return; + }; + + if non_empty_layers.next().is_some() { + self.merging_segments_enabled = false; + return; + } + + let segments = shape_editor.selected_segments().collect::>(); + if segments.len() != 2 { + self.merging_segments_enabled = false; + return; + } + let segment1 = segments[0]; + let segment2 = segments[1]; + // Both of the segments should be close to each other + let Some(vector) = document.network_interface.compute_modified_vector(*layer) else { return }; + let Some((_, bezier1, _, _)) = vector.segment_bezier_iter().find(|(id, _, _, _)| id == segment1) else { + return; + }; + let Some((_, bezier2, _, _)) = vector.segment_bezier_iter().find(|(id, _, _, _)| id == segment2) else { + return; + }; + let segments_in_same_direction = bezier1.start.distance(bezier2.start) < SEGMENT_INSERTION_DISTANCE && bezier1.end.distance(bezier2.end) < SEGMENT_INSERTION_DISTANCE; + let segments_in_opposite_direction = bezier1.start.distance(bezier2.end) < SEGMENT_INSERTION_DISTANCE && bezier1.end.distance(bezier2.start) < SEGMENT_INSERTION_DISTANCE; + let near_segments = segments_in_same_direction || segments_in_opposite_direction; + + if !near_segments { + self.merging_segments_enabled = false; + return; + } + self.merging_segments_enabled = true; } @@ -3018,27 +3057,20 @@ impl Fsm for PathToolFsmState { PathToolFsmState::Ready } (_, PathToolMessage::MergeSelectedSegments) => { - // TODO: If two segments are selected and they are close within certain threshold then we need to merge both of them together - - // Assuming that all these points are on the same layer + // Assuming that two segments selected are on the same layer let mut non_empty_layers = shape_editor.selected_shape_state.iter().filter(|(_, state)| !state.is_empty()); - - // If all layers are empty, or no layer selected let Some((layer, _)) = non_empty_layers.next() else { return PathToolFsmState::Ready; }; - - // If selected segments are of more than one layer if non_empty_layers.next().is_some() { return PathToolFsmState::Ready; } - // Given two segments and they are close we will create new points at the midpoint of their endpoints let segments = shape_editor.selected_segments().collect::>(); - if segments.len() != 2 { return PathToolFsmState::Ready; } + let segment1 = segments[0]; let segment2 = segments[1]; @@ -3164,7 +3196,7 @@ 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_segments_toggle(shape_editor, document); + tool_data.update_merge_segments_toggle(shape_editor, document, tool_action_data.preferences.vector_meshes); self } (_, PathToolMessage::ManipulatorMakeHandlesColinear) => {