From dd32204b0bb102eee66a47d54ebde662c71a0575 Mon Sep 17 00:00:00 2001 From: Ayush Amawate Date: Sun, 28 Dec 2025 03:34:20 +0530 Subject: [PATCH 01/11] fix: handle PointId remapping when joining paths from different layers When using Ctrl+J to join two points from different layers, the operation failed because PointIds change during merge due to collision resolution. The fix uses index-based point tracking instead of PointIds: 1. Before merge: Record the index of each selected point in its layer 2. Calculate post-merge indices using the point offset from layer1 3. After merge: Retrieve the actual PointIds at those indices 4. Create the connecting segment using the resolved PointIds This works because Vector::concat() preserves point ordering during merge, so we can use exact index arithmetic to find points after remapping. Fixes #3519 --- .../tool/common_functionality/shape_editor.rs | 36 +++++++++++++------ .../messages/tool/tool_messages/path_tool.rs | 34 ++++++++++++++++++ 2 files changed, 60 insertions(+), 10 deletions(-) diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index a7ab903dfe..31dad4e801 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -447,6 +447,7 @@ impl ShapeState { let (layer2, end_point) = all_selected_points[1]; if layer1 == layer2 { + // Same layer: create segment directly if start_point == end_point { return; } @@ -459,16 +460,31 @@ impl ShapeState { }; responses.add(GraphOperationMessage::Vector { layer: layer1, modification_type }); } else { - // Merge the layers - merge_layers(document, layer1, layer2, responses); - // Create segment between the two points - let segment_id = SegmentId::generate(); - let modification_type = VectorModificationType::InsertSegment { - id: segment_id, - points: [end_point, start_point], - handles: [None, None], - }; - responses.add(GraphOperationMessage::Vector { layer: layer1, modification_type }); + // Different layers: merge first, then create segment + + // Get the indices of the selected points in their respective vectors + let start_index = document.network_interface.compute_modified_vector(layer1) + .and_then(|v| v.point_domain.resolve_id(start_point)); + let end_index = document.network_interface.compute_modified_vector(layer2) + .and_then(|v| v.point_domain.resolve_id(end_point)); + + // Get the number of points in layer1 (this will be the offset for layer2 points after merge) + let layer1_point_count = document.network_interface.compute_modified_vector(layer1) + .map(|v| v.point_domain.ids().len()); + + if let (Some(start_idx), Some(end_idx), Some(point_offset)) = (start_index, end_index, layer1_point_count) { + // Merge the layers + merge_layers(document, layer1, layer2, responses); + + // After the graph runs and the merge is complete, restore selection and close path + responses.add(DeferMessage::AfterGraphRun { + messages: vec![ToolMessage::Path(PathToolMessage::RestoreSelectionAndClosePath { + layer: layer1, + start_index: start_idx, + end_index: end_idx + point_offset, + }).into()], + }); + } } return; } diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index 35fd18e940..c063373b76 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -73,6 +73,11 @@ pub enum PathToolMessage { }, Escape, ClosePath, + RestoreSelectionAndClosePath { + layer: LayerNodeIdentifier, + start_index: usize, + end_index: usize, + }, DoubleClick { extend_selection: Key, shrink_selection: Key, @@ -2669,6 +2674,35 @@ impl Fsm for PathToolFsmState { self } + (_, PathToolMessage::RestoreSelectionAndClosePath { layer, start_index, end_index }) => { + // Get the merged vector + let Some(vector) = document.network_interface.compute_modified_vector(layer) else { + return self; + }; + + // Get the point IDs at the calculated indices + let point_ids = vector.point_domain.ids(); + + if start_index >= point_ids.len() || end_index >= point_ids.len() { + return self; + } + + let start_point_id = point_ids[start_index]; + let end_point_id = point_ids[end_index]; + + // Clear existing selection and select the merged layer + shape_editor.deselect_all_points(); + shape_editor.set_selected_layers(vec![layer]); + + // Select the two points + shape_editor.select_point_by_layer_and_id(ManipulatorPointId::Anchor(start_point_id), layer); + shape_editor.select_point_by_layer_and_id(ManipulatorPointId::Anchor(end_point_id), layer); + + // Now call ClosePath which will see the selection and create the segment + responses.add(PathToolMessage::ClosePath); + + self + } (_, PathToolMessage::StartSlidingPoint) => { responses.add(DocumentMessage::StartTransaction); if tool_data.start_sliding_point(shape_editor, document) { From bcc883ea5e131893dfb5fcb3f77e673f92e385f3 Mon Sep 17 00:00:00 2001 From: Ayush Amawate Date: Mon, 29 Dec 2025 16:44:05 +0530 Subject: [PATCH 02/11] fix: use position-based lookup when joining paths from different layers --- .../tool/common_functionality/shape_editor.rs | 34 +++++++------- .../messages/tool/tool_messages/path_tool.rs | 47 ++++++++++++------- 2 files changed, 47 insertions(+), 34 deletions(-) diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index 31dad4e801..fd042b17da 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -462,26 +462,28 @@ impl ShapeState { } else { // Different layers: merge first, then create segment - // Get the indices of the selected points in their respective vectors - let start_index = document.network_interface.compute_modified_vector(layer1) - .and_then(|v| v.point_domain.resolve_id(start_point)); - let end_index = document.network_interface.compute_modified_vector(layer2) - .and_then(|v| v.point_domain.resolve_id(end_point)); - - // Get the number of points in layer1 (this will be the offset for layer2 points after merge) - let layer1_point_count = document.network_interface.compute_modified_vector(layer1) - .map(|v| v.point_domain.ids().len()); - - if let (Some(start_idx), Some(end_idx), Some(point_offset)) = (start_index, end_index, layer1_point_count) { - // Merge the layers + // Get the local positions of the selected points + let start_local_pos = document.network_interface.compute_modified_vector(layer1) + .and_then(|v| v.point_domain.position_from_id(start_point)); + let end_local_pos = document.network_interface.compute_modified_vector(layer2) + .and_then(|v| v.point_domain.position_from_id(end_point)); + + // Transform to document/world space + let start_transform = document.metadata().transform_to_document(layer1); + let end_transform = document.metadata().transform_to_document(layer2); + + if let (Some(start_local), Some(end_local)) = (start_local_pos, end_local_pos) { + let start_pos = start_transform.transform_point2(start_local); + let end_pos = end_transform.transform_point2(end_local); + merge_layers(document, layer1, layer2, responses); - // After the graph runs and the merge is complete, restore selection and close path + // Connect the points responses.add(DeferMessage::AfterGraphRun { - messages: vec![ToolMessage::Path(PathToolMessage::RestoreSelectionAndClosePath { + messages: vec![ToolMessage::Path(PathToolMessage::ConnectPointsByPosition { layer: layer1, - start_index: start_idx, - end_index: end_idx + point_offset, + start_position: start_pos, + end_position: end_pos, }).into()], }); } diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index c063373b76..364db71b57 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -73,10 +73,10 @@ pub enum PathToolMessage { }, Escape, ClosePath, - RestoreSelectionAndClosePath { + ConnectPointsByPosition { layer: LayerNodeIdentifier, - start_index: usize, - end_index: usize, + start_position: DVec2, + end_position: DVec2, }, DoubleClick { extend_selection: Key, @@ -2674,32 +2674,43 @@ impl Fsm for PathToolFsmState { self } - (_, PathToolMessage::RestoreSelectionAndClosePath { layer, start_index, end_index }) => { + (_, PathToolMessage::ConnectPointsByPosition { layer, start_position, end_position }) => { // Get the merged vector let Some(vector) = document.network_interface.compute_modified_vector(layer) else { return self; }; - // Get the point IDs at the calculated indices + // Find points by their positions (with small tolerance for floating point comparison) + const POSITION_TOLERANCE: f64 = 0.01; + + let positions = vector.point_domain.positions(); let point_ids = vector.point_domain.ids(); - if start_index >= point_ids.len() || end_index >= point_ids.len() { - return self; - } + let mut start_point_id = None; + let mut end_point_id = None; - let start_point_id = point_ids[start_index]; - let end_point_id = point_ids[end_index]; + for (i, &pos) in positions.iter().enumerate() { + if start_point_id.is_none() && (pos - start_position).length() < POSITION_TOLERANCE { + start_point_id = Some(point_ids[i]); + } + if end_point_id.is_none() && (pos - end_position).length() < POSITION_TOLERANCE { + end_point_id = Some(point_ids[i]); + } + if start_point_id.is_some() && end_point_id.is_some() { + break; + } + } - // Clear existing selection and select the merged layer - shape_editor.deselect_all_points(); - shape_editor.set_selected_layers(vec![layer]); + if let (Some(start_id), Some(end_id)) = (start_point_id, end_point_id) { + // Clear existing selection + shape_editor.deselect_all_points(); + shape_editor.set_selected_layers(vec![layer]); - // Select the two points - shape_editor.select_point_by_layer_and_id(ManipulatorPointId::Anchor(start_point_id), layer); - shape_editor.select_point_by_layer_and_id(ManipulatorPointId::Anchor(end_point_id), layer); + shape_editor.select_point_by_layer_and_id(ManipulatorPointId::Anchor(start_id), layer); + shape_editor.select_point_by_layer_and_id(ManipulatorPointId::Anchor(end_id), layer); - // Now call ClosePath which will see the selection and create the segment - responses.add(PathToolMessage::ClosePath); + responses.add(PathToolMessage::ClosePath); + } self } From 666d6a7e41072a2e164d4b39331c9ece1836c772 Mon Sep 17 00:00:00 2001 From: Ayush Amawate Date: Sat, 3 Jan 2026 00:55:45 +0530 Subject: [PATCH 03/11] fix cmd + g group case --- .../tool/common_functionality/shape_editor.rs | 94 +++++++++++++++---- .../messages/tool/tool_messages/path_tool.rs | 38 +++++--- 2 files changed, 102 insertions(+), 30 deletions(-) diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index fd042b17da..c1f276a101 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -421,6 +421,55 @@ impl ShapeState { (point.as_handle().is_some() && self.ignore_handles) || (point.as_anchor().is_some() && self.ignore_anchors) } + /// Creates a dummy segment insertion to trigger graph reorganization. + /// This dummy segment will fail to insert (because PointIds become invalid during execution), + /// but the important side effect is that it triggers the reorganization we need. + fn create_dummy_segment_to_trigger_graph_reorganization(layer: LayerNodeIdentifier, start_point: PointId, end_point: PointId, responses: &mut VecDeque) { + let dummy_segment_id = SegmentId::generate(); + let dummy_modification = VectorModificationType::InsertSegment { + id: dummy_segment_id, + points: [start_point, end_point], + handles: [None, None], + }; + responses.add(GraphOperationMessage::Vector { + layer, + modification_type: dummy_modification, + }); + responses.add(NodeGraphMessage::RunDocumentGraph); + } + + /// a two-step process: trigger reorganization, then use position-based lookup. + fn handle_grouped_transform_close_path(document: &DocumentMessageHandler, layer: LayerNodeIdentifier, start_point: PointId, end_point: PointId, responses: &mut VecDeque) { + // Get the layer's transform (handles rotation, scaling, translation) + let layer_transform = document.metadata().transform_to_document(layer); + + let start_local_pos = document.network_interface.compute_modified_vector(layer).and_then(|v| v.point_domain.position_from_id(start_point)); + let end_local_pos = document.network_interface.compute_modified_vector(layer).and_then(|v| v.point_domain.position_from_id(end_point)); + + if let (Some(start_local), Some(end_local)) = (start_local_pos, end_local_pos) { + // Transform positions to document/world space + // These positions are stable (won't change during reorganization) + let start_pos = layer_transform.transform_point2(start_local); + let end_pos = layer_transform.transform_point2(end_local); + + // This segment insertion will fail, but causes point domain reorganization + Self::create_dummy_segment_to_trigger_graph_reorganization(layer, start_point, end_point, responses); + + // Defer position-based connection to run after reorganization completes + // By then, PointIds will be stable with their new remapped values + responses.add(DeferMessage::AfterGraphRun { + messages: vec![ + ToolMessage::Path(PathToolMessage::ConnectPointsByPosition { + layer, + start_position: start_pos, + end_position: end_pos, + }) + .into(), + ], + }); + } + } + pub fn close_selected_path(&self, document: &DocumentMessageHandler, responses: &mut VecDeque) { // First collect all selected anchor points across all layers let all_selected_points: Vec<(LayerNodeIdentifier, PointId)> = self @@ -447,26 +496,33 @@ impl ShapeState { let (layer2, end_point) = all_selected_points[1]; if layer1 == layer2 { - // Same layer: create segment directly + // Same layer case if start_point == end_point { return; } - let segment_id = SegmentId::generate(); - let modification_type = VectorModificationType::InsertSegment { - id: segment_id, - points: [end_point, start_point], - handles: [None, None], - }; - responses.add(GraphOperationMessage::Vector { layer: layer1, modification_type }); + // Check if this is a grouped layer with multiple disconnected segments + let has_multiple_segments = document.network_interface.compute_modified_vector(layer1).map(|v| v.segment_domain.ids().len() > 1).unwrap_or(false); + + if has_multiple_segments { + // Grouped paths: use helper function to handle reorganization + Self::handle_grouped_transform_close_path(document, layer1, start_point, end_point, responses); + } else { + // Single segment: PointIDs are stable, use immediate insertion + let segment_id = SegmentId::generate(); + let modification_type = VectorModificationType::InsertSegment { + id: segment_id, + points: [end_point, start_point], + handles: [None, None], + }; + responses.add(GraphOperationMessage::Vector { layer: layer1, modification_type }); + } } else { // Different layers: merge first, then create segment // Get the local positions of the selected points - let start_local_pos = document.network_interface.compute_modified_vector(layer1) - .and_then(|v| v.point_domain.position_from_id(start_point)); - let end_local_pos = document.network_interface.compute_modified_vector(layer2) - .and_then(|v| v.point_domain.position_from_id(end_point)); + let start_local_pos = document.network_interface.compute_modified_vector(layer1).and_then(|v| v.point_domain.position_from_id(start_point)); + let end_local_pos = document.network_interface.compute_modified_vector(layer2).and_then(|v| v.point_domain.position_from_id(end_point)); // Transform to document/world space let start_transform = document.metadata().transform_to_document(layer1); @@ -478,13 +534,15 @@ impl ShapeState { merge_layers(document, layer1, layer2, responses); - // Connect the points responses.add(DeferMessage::AfterGraphRun { - messages: vec![ToolMessage::Path(PathToolMessage::ConnectPointsByPosition { - layer: layer1, - start_position: start_pos, - end_position: end_pos, - }).into()], + messages: vec![ + ToolMessage::Path(PathToolMessage::ConnectPointsByPosition { + layer: layer1, + start_position: start_pos, + end_position: end_pos, + }) + .into(), + ], }); } } diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index 364db71b57..e192a19829 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -2689,11 +2689,20 @@ impl Fsm for PathToolFsmState { let mut start_point_id = None; let mut end_point_id = None; - for (i, &pos) in positions.iter().enumerate() { - if start_point_id.is_none() && (pos - start_position).length() < POSITION_TOLERANCE { + // Get the merged layer's transform to convert local positions to document space + let layer_transform = document.metadata().transform_to_document(layer); + + for (i, &local_pos) in positions.iter().enumerate() { + // Transform the local position to document space for comparison + let doc_pos = layer_transform.transform_point2(local_pos); + + let start_distance = (doc_pos - start_position).length(); + let end_distance = (doc_pos - end_position).length(); + + if start_point_id.is_none() && start_distance < POSITION_TOLERANCE { start_point_id = Some(point_ids[i]); } - if end_point_id.is_none() && (pos - end_position).length() < POSITION_TOLERANCE { + if end_point_id.is_none() && end_distance < POSITION_TOLERANCE { end_point_id = Some(point_ids[i]); } if start_point_id.is_some() && end_point_id.is_some() { @@ -2701,18 +2710,23 @@ impl Fsm for PathToolFsmState { } } - if let (Some(start_id), Some(end_id)) = (start_point_id, end_point_id) { - // Clear existing selection - shape_editor.deselect_all_points(); - shape_editor.set_selected_layers(vec![layer]); + if let (Some(start_id), Some(end_id)) = (start_point_id, end_point_id) { + // Create segment directly + responses.add(DocumentMessage::StartTransaction); - shape_editor.select_point_by_layer_and_id(ManipulatorPointId::Anchor(start_id), layer); - shape_editor.select_point_by_layer_and_id(ManipulatorPointId::Anchor(end_id), layer); + let segment_id = SegmentId::generate(); + let modification_type = VectorModificationType::InsertSegment { + id: segment_id, + points: [end_id, start_id], + handles: [None, None], + }; - responses.add(PathToolMessage::ClosePath); - } + responses.add(GraphOperationMessage::Vector { layer, modification_type }); + responses.add(DocumentMessage::EndTransaction); + responses.add(OverlaysMessage::Draw); + } - self + self } (_, PathToolMessage::StartSlidingPoint) => { responses.add(DocumentMessage::StartTransaction); From 6f0b7cfd05a4e17f404c17a625e3951a9101ba2a Mon Sep 17 00:00:00 2001 From: Ayush Amawate Date: Sat, 3 Jan 2026 17:46:38 +0530 Subject: [PATCH 04/11] fix check for grouped folder --- .../messages/tool/common_functionality/shape_editor.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index c1f276a101..482b4019e7 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -501,11 +501,11 @@ impl ShapeState { return; } - // Check if this is a grouped layer with multiple disconnected segments - let has_multiple_segments = document.network_interface.compute_modified_vector(layer1).map(|v| v.segment_domain.ids().len() > 1).unwrap_or(false); + // Check if this layer itself has children (is a merged/grouped layer created with Cmd+G) + let is_grouped = layer1.has_children(document.metadata()); - if has_multiple_segments { - // Grouped paths: use helper function to handle reorganization + if is_grouped { + // Grouped/merged layer: use helper function to handle reorganization Self::handle_grouped_transform_close_path(document, layer1, start_point, end_point, responses); } else { // Single segment: PointIDs are stable, use immediate insertion From c2b129fedebd0974f06373d058bb989dc92df0ef Mon Sep 17 00:00:00 2001 From: Ayush Amawate Date: Sun, 4 Jan 2026 02:51:51 +0530 Subject: [PATCH 05/11] add in the single layer inside a group case --- .../src/messages/tool/common_functionality/shape_editor.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index 482b4019e7..04be9c7d19 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -501,8 +501,9 @@ impl ShapeState { return; } - // Check if this layer itself has children (is a merged/grouped layer created with Cmd+G) - let is_grouped = layer1.has_children(document.metadata()); + // Check if this layer has multiple children (is a merged/grouped layer created with Cmd+G) + let num_children = layer1.children(document.metadata()).count(); + let is_grouped = num_children > 1; if is_grouped { // Grouped/merged layer: use helper function to handle reorganization From 631a2cea984efbc65641eb6dac3982ea51f892fa Mon Sep 17 00:00:00 2001 From: Ayush Amawate Date: Sun, 4 Jan 2026 04:06:23 +0530 Subject: [PATCH 06/11] use ApplyPointDelta instead of creating a dummy segment --- .../tool/common_functionality/shape_editor.rs | 19 +++++------- .../messages/tool/tool_messages/path_tool.rs | 30 +++++++++---------- 2 files changed, 23 insertions(+), 26 deletions(-) diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index 04be9c7d19..5e90bdd0db 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -421,15 +421,12 @@ impl ShapeState { (point.as_handle().is_some() && self.ignore_handles) || (point.as_anchor().is_some() && self.ignore_anchors) } - /// Creates a dummy segment insertion to trigger graph reorganization. - /// This dummy segment will fail to insert (because PointIds become invalid during execution), - /// but the important side effect is that it triggers the reorganization we need. - fn create_dummy_segment_to_trigger_graph_reorganization(layer: LayerNodeIdentifier, start_point: PointId, end_point: PointId, responses: &mut VecDeque) { - let dummy_segment_id = SegmentId::generate(); - let dummy_modification = VectorModificationType::InsertSegment { - id: dummy_segment_id, - points: [start_point, end_point], - handles: [None, None], + /// Creates a dummy modification to trigger graph reorganization. + fn add_dummy_modification_to_trigger_graph_reorganization(layer: LayerNodeIdentifier, start_point: PointId, _end_point: PointId, responses: &mut VecDeque) { + // Apply a zero-delta to one of the points to trigger reorganization + let dummy_modification = VectorModificationType::ApplyPointDelta { + point: start_point, + delta: DVec2::ZERO, }; responses.add(GraphOperationMessage::Vector { layer, @@ -452,8 +449,8 @@ impl ShapeState { let start_pos = layer_transform.transform_point2(start_local); let end_pos = layer_transform.transform_point2(end_local); - // This segment insertion will fail, but causes point domain reorganization - Self::create_dummy_segment_to_trigger_graph_reorganization(layer, start_point, end_point, responses); + // This zero-delta modification triggers point domain reorganization + Self::add_dummy_modification_to_trigger_graph_reorganization(layer, start_point, end_point, responses); // Defer position-based connection to run after reorganization completes // By then, PointIds will be stable with their new remapped values diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index e192a19829..2d074778c4 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -2710,23 +2710,23 @@ impl Fsm for PathToolFsmState { } } - if let (Some(start_id), Some(end_id)) = (start_point_id, end_point_id) { - // Create segment directly - responses.add(DocumentMessage::StartTransaction); - - let segment_id = SegmentId::generate(); - let modification_type = VectorModificationType::InsertSegment { - id: segment_id, - points: [end_id, start_id], - handles: [None, None], - }; + if let (Some(start_id), Some(end_id)) = (start_point_id, end_point_id) { + // Create segment directly + responses.add(DocumentMessage::StartTransaction); + + let segment_id = SegmentId::generate(); + let modification_type = VectorModificationType::InsertSegment { + id: segment_id, + points: [end_id, start_id], + handles: [None, None], + }; - responses.add(GraphOperationMessage::Vector { layer, modification_type }); - responses.add(DocumentMessage::EndTransaction); - responses.add(OverlaysMessage::Draw); - } + responses.add(GraphOperationMessage::Vector { layer, modification_type }); + responses.add(DocumentMessage::EndTransaction); + responses.add(OverlaysMessage::Draw); + } - self + self } (_, PathToolMessage::StartSlidingPoint) => { responses.add(DocumentMessage::StartTransaction); From 6ec21bda8dfe0bd79b13666028b8e9c3a7da6fb3 Mon Sep 17 00:00:00 2001 From: Ayush Amawate <97389618+Ayush2k02@users.noreply.github.com> Date: Mon, 5 Jan 2026 04:31:20 +0530 Subject: [PATCH 07/11] Update editor/src/messages/tool/common_functionality/shape_editor.rs Co-authored-by: James Lindsay <78500760+0HyperCube@users.noreply.github.com> --- .../src/messages/tool/common_functionality/shape_editor.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index 5e90bdd0db..6a1c7c63dd 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -443,7 +443,10 @@ impl ShapeState { let start_local_pos = document.network_interface.compute_modified_vector(layer).and_then(|v| v.point_domain.position_from_id(start_point)); let end_local_pos = document.network_interface.compute_modified_vector(layer).and_then(|v| v.point_domain.position_from_id(end_point)); - if let (Some(start_local), Some(end_local)) = (start_local_pos, end_local_pos) { + let (Some(start_local), Some(end_local)) = (start_local_pos, end_local_pos) else { + warn!("Unable to resolve point ids for joining"); + return; + }; // Transform positions to document/world space // These positions are stable (won't change during reorganization) let start_pos = layer_transform.transform_point2(start_local); From f84687c02fca7a0cd1cd6eaacf949fb0160316a5 Mon Sep 17 00:00:00 2001 From: Ayush Amawate <97389618+Ayush2k02@users.noreply.github.com> Date: Mon, 5 Jan 2026 04:38:44 +0530 Subject: [PATCH 08/11] Update editor/src/messages/tool/common_functionality/shape_editor.rs Co-authored-by: James Lindsay <78500760+0HyperCube@users.noreply.github.com> --- editor/src/messages/tool/common_functionality/shape_editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index 6a1c7c63dd..76d58082be 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -421,7 +421,7 @@ impl ShapeState { (point.as_handle().is_some() && self.ignore_handles) || (point.as_anchor().is_some() && self.ignore_anchors) } - /// Creates a dummy modification to trigger graph reorganization. + /// Applies a dummy vector modification to the layer. In the case where a group containing some vector data is selected, this triggers the creation of a «Flatten Path» node. fn add_dummy_modification_to_trigger_graph_reorganization(layer: LayerNodeIdentifier, start_point: PointId, _end_point: PointId, responses: &mut VecDeque) { // Apply a zero-delta to one of the points to trigger reorganization let dummy_modification = VectorModificationType::ApplyPointDelta { From 46c9ea33acfcbe50f3a822ccf0ce86c8369c27f5 Mon Sep 17 00:00:00 2001 From: Ayush Amawate Date: Mon, 5 Jan 2026 04:53:31 +0530 Subject: [PATCH 09/11] encapsulate point connection logic in defer_connect_points_by_position --- .../tool/common_functionality/shape_editor.rs | 94 +++++++++---------- 1 file changed, 44 insertions(+), 50 deletions(-) diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index 76d58082be..169aeab934 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -436,38 +436,54 @@ impl ShapeState { } /// a two-step process: trigger reorganization, then use position-based lookup. - fn handle_grouped_transform_close_path(document: &DocumentMessageHandler, layer: LayerNodeIdentifier, start_point: PointId, end_point: PointId, responses: &mut VecDeque) { - // Get the layer's transform (handles rotation, scaling, translation) - let layer_transform = document.metadata().transform_to_document(layer); + /// Helper function to connect two points by their positions after graph reorganization. + /// This transforms local point positions to document space and defers the connection until after the graph runs. + fn defer_connect_points_by_position( + document: &DocumentMessageHandler, + layer1: LayerNodeIdentifier, + start_point: PointId, + layer2: LayerNodeIdentifier, + end_point: PointId, + target_layer: LayerNodeIdentifier, + responses: &mut VecDeque, + ) { + // Get the local positions of the selected points + let start_local_pos = document.network_interface.compute_modified_vector(layer1).and_then(|v| v.point_domain.position_from_id(start_point)); + let end_local_pos = document.network_interface.compute_modified_vector(layer2).and_then(|v| v.point_domain.position_from_id(end_point)); - let start_local_pos = document.network_interface.compute_modified_vector(layer).and_then(|v| v.point_domain.position_from_id(start_point)); - let end_local_pos = document.network_interface.compute_modified_vector(layer).and_then(|v| v.point_domain.position_from_id(end_point)); + // Transform to document/world space + let start_transform = document.metadata().transform_to_document(layer1); + let end_transform = document.metadata().transform_to_document(layer2); let (Some(start_local), Some(end_local)) = (start_local_pos, end_local_pos) else { warn!("Unable to resolve point ids for joining"); return; }; - // Transform positions to document/world space - // These positions are stable (won't change during reorganization) - let start_pos = layer_transform.transform_point2(start_local); - let end_pos = layer_transform.transform_point2(end_local); - - // This zero-delta modification triggers point domain reorganization - Self::add_dummy_modification_to_trigger_graph_reorganization(layer, start_point, end_point, responses); - - // Defer position-based connection to run after reorganization completes - // By then, PointIds will be stable with their new remapped values - responses.add(DeferMessage::AfterGraphRun { - messages: vec![ - ToolMessage::Path(PathToolMessage::ConnectPointsByPosition { - layer, - start_position: start_pos, - end_position: end_pos, - }) - .into(), - ], + // Transform positions to document/world space + // These positions are stable (won't change during reorganization) + let start_pos = start_transform.transform_point2(start_local); + let end_pos = end_transform.transform_point2(end_local); + + // Defer position-based connection to run after reorganization completes + // By then, PointIds will be stable with their new remapped values + responses.add(DeferMessage::AfterGraphRun { + messages: vec![ + ToolMessage::Path(PathToolMessage::ConnectPointsByPosition { + layer: target_layer, + start_position: start_pos, + end_position: end_pos, + }) + .into(), + ], }); - } + } + + fn handle_grouped_transform_close_path(document: &DocumentMessageHandler, layer: LayerNodeIdentifier, start_point: PointId, end_point: PointId, responses: &mut VecDeque) { + // This zero-delta modification triggers point domain reorganization + Self::add_dummy_modification_to_trigger_graph_reorganization(layer, start_point, end_point, responses); + + // Use the helper to defer the connection until after reorganization + Self::defer_connect_points_by_position(document, layer, start_point, layer, end_point, layer, responses); } pub fn close_selected_path(&self, document: &DocumentMessageHandler, responses: &mut VecDeque) { @@ -520,32 +536,10 @@ impl ShapeState { } } else { // Different layers: merge first, then create segment + merge_layers(document, layer1, layer2, responses); - // Get the local positions of the selected points - let start_local_pos = document.network_interface.compute_modified_vector(layer1).and_then(|v| v.point_domain.position_from_id(start_point)); - let end_local_pos = document.network_interface.compute_modified_vector(layer2).and_then(|v| v.point_domain.position_from_id(end_point)); - - // Transform to document/world space - let start_transform = document.metadata().transform_to_document(layer1); - let end_transform = document.metadata().transform_to_document(layer2); - - if let (Some(start_local), Some(end_local)) = (start_local_pos, end_local_pos) { - let start_pos = start_transform.transform_point2(start_local); - let end_pos = end_transform.transform_point2(end_local); - - merge_layers(document, layer1, layer2, responses); - - responses.add(DeferMessage::AfterGraphRun { - messages: vec![ - ToolMessage::Path(PathToolMessage::ConnectPointsByPosition { - layer: layer1, - start_position: start_pos, - end_position: end_pos, - }) - .into(), - ], - }); - } + // Use the helper to defer the connection until after reorganization + Self::defer_connect_points_by_position(document, layer1, start_point, layer2, end_point, layer1, responses); } return; } From dafbc5c9f07a0d694914e5a80c22b3d9354339ff Mon Sep 17 00:00:00 2001 From: Ayush Amawate Date: Mon, 5 Jan 2026 10:44:45 +0530 Subject: [PATCH 10/11] reduce tolerance to 1e-6 --- 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 2d074778c4..9a37ea6fa3 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -2681,7 +2681,7 @@ impl Fsm for PathToolFsmState { }; // Find points by their positions (with small tolerance for floating point comparison) - const POSITION_TOLERANCE: f64 = 0.01; + const POSITION_TOLERANCE: f64 = 1e-6; let positions = vector.point_domain.positions(); let point_ids = vector.point_domain.ids(); From 233d1608f1741360bf044aec6e697664141f589d Mon Sep 17 00:00:00 2001 From: Ayush Amawate Date: Mon, 5 Jan 2026 13:14:34 +0530 Subject: [PATCH 11/11] remove wrong comments --- .../src/messages/tool/common_functionality/shape_editor.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index 169aeab934..f102bbb56f 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -435,9 +435,6 @@ impl ShapeState { responses.add(NodeGraphMessage::RunDocumentGraph); } - /// a two-step process: trigger reorganization, then use position-based lookup. - /// Helper function to connect two points by their positions after graph reorganization. - /// This transforms local point positions to document space and defers the connection until after the graph runs. fn defer_connect_points_by_position( document: &DocumentMessageHandler, layer1: LayerNodeIdentifier, @@ -475,7 +472,7 @@ impl ShapeState { }) .into(), ], - }); + }); } fn handle_grouped_transform_close_path(document: &DocumentMessageHandler, layer: LayerNodeIdentifier, start_point: PointId, end_point: PointId, responses: &mut VecDeque) {