From 6594bb6e32bc61ed7268b8bc02bf3b540f21ad16 Mon Sep 17 00:00:00 2001 From: VimYoung Date: Wed, 23 Sep 2026 16:07:09 +0530 Subject: [PATCH 01/14] Test: Inspecting current changes --- .../portfolio/portfolio_message_handler.rs | 2 +- .../src/messages/tool/tool_message_handler.rs | 2 + .../messages/tool/tool_messages/pen_tool.rs | 48 ++++++++++++------- 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 4a4e6f468ef..e54ab8b45e9 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -207,7 +207,7 @@ impl MessageHandler> for Portfolio } } - responses.add(PortfolioMessage::GarbageCollectResources); + // responses.add(PortfolioMessage::GarbageCollectResources); } PortfolioMessage::AutoSaveDocument { document_id } => { let validate = preferences.validate_storage_round_trip; diff --git a/editor/src/messages/tool/tool_message_handler.rs b/editor/src/messages/tool/tool_message_handler.rs index 2d37d474704..2df3b335828 100644 --- a/editor/src/messages/tool/tool_message_handler.rs +++ b/editor/src/messages/tool/tool_message_handler.rs @@ -68,6 +68,7 @@ impl MessageHandler> for ToolMessageHandler ToolMessage::ActivateToolGradient => responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Gradient }), ToolMessage::ActivateToolPath => responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Path }), + ToolMessage::ActivateToolPen => responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Pen }), ToolMessage::ActivateToolFreehand => responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Freehand }), ToolMessage::ActivateToolSpline => responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Spline }), @@ -146,6 +147,7 @@ impl MessageHandler> for ToolMessageHandler // If a G/R/S transform is active while using Path, Select, Pen, or Shape, // and the user switches to a different tool, cancel the current transform // operation to avoid leaving it in an inconsistent state + if matches!(old_tool, ToolType::Path | ToolType::Select | ToolType::Pen | ToolType::Shape) { responses.add(TransformLayerMessage::CancelTransformOperation); } diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index 40bb43490cb..2be959a7183 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -403,8 +403,7 @@ struct PenToolData { auto_panning: AutoPanning, modifiers: ModifierState, - buffering_merged_vector: bool, - + // buffering_merged_vector: bool, previous_handle_start_pos: DVec2, previous_handle_end_pos: Option, toggle_colinear_debounce: bool, @@ -1914,26 +1913,41 @@ impl Fsm for PenToolFsmState { self } + (PenToolFsmState::PlacingAnchor, PenToolMessage::RecalculateLatestPointsPosition) => { + tool_data.recalculate_latest_points_position(document); + + // If we were placing anchors then it would be a good idea to update the anchor if possible + if let Some(layer) = layer { + tool_data.handle_mode = HandleMode::ColinearLocked; + tool_data.bend_from_previous_point(SnapData::new(document, input, viewport), transform, layer, shape_editor, responses); + tool_data.place_anchor(SnapData::new(document, input, viewport), transform, input.mouse.position, responses); + PenToolFsmState::DraggingHandle(tool_data.handle_mode) + } else { + PenToolFsmState::Ready + } + } (state, PenToolMessage::RecalculateLatestPointsPosition) => { tool_data.recalculate_latest_points_position(document); state } - (PenToolFsmState::PlacingAnchor, PenToolMessage::DragStart { append_to_selected }) => { + (PenToolFsmState::PlacingAnchor, PenToolMessage::DragStart { .. }) => { let point = SnapCandidatePoint::handle(document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position)); let snapped = tool_data.snap_manager.free_snap(&SnapData::new(document, input, viewport), &point, SnapTypeConfiguration::default()); let viewport_vec = document.metadata().document_to_viewport.transform_point2(snapped.snapped_point_document); // Early return if the buffer was started and this message is being run again after the buffer (so that place_anchor updates the state with the newly merged vector) - if tool_data.buffering_merged_vector { - if let Some(layer) = layer { - tool_data.buffering_merged_vector = false; - tool_data.handle_mode = HandleMode::ColinearLocked; - tool_data.bend_from_previous_point(SnapData::new(document, input, viewport), transform, layer, shape_editor, responses); - tool_data.place_anchor(SnapData::new(document, input, viewport), transform, input.mouse.position, responses); - } - tool_data.buffering_merged_vector = false; - PenToolFsmState::DraggingHandle(tool_data.handle_mode) - } else { + // if tool_data.buffering_merged_vector { + // if let Some(layer) = layer { + // tool_data.buffering_merged_vector = false; + // tool_data.handle_mode = HandleMode::ColinearLocked; + // tool_data.bend_from_previous_point(SnapData::new(document, input, viewport), transform, layer, shape_editor, responses); + // tool_data.place_anchor(SnapData::new(document, input, viewport), transform, input.mouse.position, responses); + // } + // tool_data.buffering_merged_vector = false; + // PenToolFsmState::DraggingHandle(tool_data.handle_mode) + // } else { + let mut is_merging = false; + { // Each segment placement is its own history step, spanning from this click through the release that finalizes it responses.add(DocumentMessage::StartTransaction); @@ -1953,12 +1967,14 @@ impl Fsm for PenToolFsmState { .or(tool_data.current_layer.filter(|layer| *layer != other_layer)) { merge_layers(document, current_layer, other_layer, responses); + is_merging = true; } } - // Even if no buffer was started, the message still has to be run again in order to call bend_from_previous_point - tool_data.buffering_merged_vector = true; - responses.add(PenToolMessage::DragStart { append_to_selected }); + if !is_merging { + responses.add(PenToolMessage::RecalculateLatestPointsPosition); + } + PenToolFsmState::PlacingAnchor } } From 8d8899c9a25df66f4f2690111b5dc8acb2b4ae04 Mon Sep 17 00:00:00 2001 From: VimYoung Date: Wed, 23 Sep 2026 16:38:04 +0530 Subject: [PATCH 02/14] Misc: Minor Changes --- .../messages/tool/tool_messages/pen_tool.rs | 61 ++++++++----------- 1 file changed, 24 insertions(+), 37 deletions(-) diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index 2be959a7183..37338ab46c3 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -1935,48 +1935,35 @@ impl Fsm for PenToolFsmState { let snapped = tool_data.snap_manager.free_snap(&SnapData::new(document, input, viewport), &point, SnapTypeConfiguration::default()); let viewport_vec = document.metadata().document_to_viewport.transform_point2(snapped.snapped_point_document); - // Early return if the buffer was started and this message is being run again after the buffer (so that place_anchor updates the state with the newly merged vector) - // if tool_data.buffering_merged_vector { - // if let Some(layer) = layer { - // tool_data.buffering_merged_vector = false; - // tool_data.handle_mode = HandleMode::ColinearLocked; - // tool_data.bend_from_previous_point(SnapData::new(document, input, viewport), transform, layer, shape_editor, responses); - // tool_data.place_anchor(SnapData::new(document, input, viewport), transform, input.mouse.position, responses); - // } - // tool_data.buffering_merged_vector = false; - // PenToolFsmState::DraggingHandle(tool_data.handle_mode) - // } else { let mut is_merging = false; - { - // Each segment placement is its own history step, spanning from this click through the release that finalizes it - responses.add(DocumentMessage::StartTransaction); - - // Merge two layers if the point is connected to the end point of another path - - // This might not be the correct solution to artboards being included as the other layer, - // which occurs due to the `compute_modified_vector` call in `should_extend` using the click targets for a layer instead of vector. - let layers = LayerNodeIdentifier::ROOT_PARENT - .descendants(document.metadata()) - .filter(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[])); - if let Some((other_layer, _, _)) = should_extend(document, viewport_vec, crate::consts::SNAP_POINT_TOLERANCE, layers) { - let selected_nodes = document.network_interface.selected_nodes(); - let mut selected_layers = selected_nodes.selected_layers(document.metadata()); - if let Some(current_layer) = selected_layers - .next() - .filter(|current_layer| selected_layers.next().is_none() && *current_layer != other_layer) - .or(tool_data.current_layer.filter(|layer| *layer != other_layer)) - { - merge_layers(document, current_layer, other_layer, responses); - is_merging = true; - } - } + // Each segment placement is its own history step, spanning from this click through the release that finalizes it + responses.add(DocumentMessage::StartTransaction); - if !is_merging { - responses.add(PenToolMessage::RecalculateLatestPointsPosition); + // Merge two layers if the point is connected to the end point of another path + + // This might not be the correct solution to artboards being included as the other layer, + // which occurs due to the `compute_modified_vector` call in `should_extend` using the click targets for a layer instead of vector. + let layers = LayerNodeIdentifier::ROOT_PARENT + .descendants(document.metadata()) + .filter(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[])); + if let Some((other_layer, _, _)) = should_extend(document, viewport_vec, crate::consts::SNAP_POINT_TOLERANCE, layers) { + let selected_nodes = document.network_interface.selected_nodes(); + let mut selected_layers = selected_nodes.selected_layers(document.metadata()); + if let Some(current_layer) = selected_layers + .next() + .filter(|current_layer| selected_layers.next().is_none() && *current_layer != other_layer) + .or(tool_data.current_layer.filter(|layer| *layer != other_layer)) + { + merge_layers(document, current_layer, other_layer, responses); + is_merging = true; } + } - PenToolFsmState::PlacingAnchor + if !is_merging { + responses.add(PenToolMessage::RecalculateLatestPointsPosition); } + + PenToolFsmState::PlacingAnchor } (PenToolFsmState::PlacingAnchor, PenToolMessage::RemovePreviousHandle) => { if let Some(last_point) = tool_data.latest_point_mut() { From f195b15d0b15576a1a493ff959b390c16b58721e Mon Sep 17 00:00:00 2001 From: VimYoung Date: Sun, 27 Sep 2026 20:50:54 +0530 Subject: [PATCH 03/14] Fix: Garbage collection readded --- editor/src/messages/portfolio/portfolio_message_handler.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index e54ab8b45e9..4a4e6f468ef 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -207,7 +207,7 @@ impl MessageHandler> for Portfolio } } - // responses.add(PortfolioMessage::GarbageCollectResources); + responses.add(PortfolioMessage::GarbageCollectResources); } PortfolioMessage::AutoSaveDocument { document_id } => { let validate = preferences.validate_storage_round_trip; From 8d8dd662b3068a6498989599949110a6c92fe666 Mon Sep 17 00:00:00 2001 From: VimYoung Date: Sat, 3 Oct 2026 18:58:17 +0530 Subject: [PATCH 04/14] Add: Added anchor placement test --- .../messages/tool/tool_messages/pen_tool.rs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index c45afeb2145..db43470bb9f 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -2445,6 +2445,33 @@ mod test_pen_tool { const C: DVec2 = DVec2::new(200., 200.); const D: DVec2 = DVec2::new(100., 200.); + async fn create_a_rectangle() -> EditorTestUtils { + let mut editor = EditorTestUtils::create(); + + editor.new_document().await; + + editor.select_primary_color(Color::RED).await; + editor.draw_rect(D.x, D.y, C.x, C.x).await; + + editor + } + + #[tokio::test] + async fn offset_change_on_snap() { + let mut editor = create_a_rectangle().await; + // Ultimately brings the pointer over to D's location + let move_dir = A + DVec2::new(0., 100.); + + click_pen(&mut editor, A).await; + + // Process of snapping the pen to the rectangle + editor.move_mouse(A.x, A.y, ModifierKeys::empty(), MouseKeys::empty()).await; + editor.left_mousedown(A.x, A.y, ModifierKeys::empty()).await; + editor.move_mouse(move_dir.x, move_dir.y, ModifierKeys::empty(), MouseKeys::LEFT).await; + editor.left_mouseup(move_dir.x, move_dir.y, ModifierKeys::empty()).await; + assert_anchors(&editor, &[A]); + } + #[tokio::test] async fn each_segment_and_the_closing_click_are_their_own_history_steps() { let mut editor = EditorTestUtils::create(); From 232d6cd2674b416f7edf52740ea8be8e61145a57 Mon Sep 17 00:00:00 2001 From: VimYoung Date: Sun, 4 Oct 2026 23:53:42 +0530 Subject: [PATCH 05/14] Fix: Revert changes from tool_message_handler and cleanup --- editor/src/messages/tool/tool_message_handler.rs | 2 -- editor/src/messages/tool/tool_messages/pen_tool.rs | 1 - 2 files changed, 3 deletions(-) diff --git a/editor/src/messages/tool/tool_message_handler.rs b/editor/src/messages/tool/tool_message_handler.rs index 2df3b335828..2d37d474704 100644 --- a/editor/src/messages/tool/tool_message_handler.rs +++ b/editor/src/messages/tool/tool_message_handler.rs @@ -68,7 +68,6 @@ impl MessageHandler> for ToolMessageHandler ToolMessage::ActivateToolGradient => responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Gradient }), ToolMessage::ActivateToolPath => responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Path }), - ToolMessage::ActivateToolPen => responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Pen }), ToolMessage::ActivateToolFreehand => responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Freehand }), ToolMessage::ActivateToolSpline => responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Spline }), @@ -147,7 +146,6 @@ impl MessageHandler> for ToolMessageHandler // If a G/R/S transform is active while using Path, Select, Pen, or Shape, // and the user switches to a different tool, cancel the current transform // operation to avoid leaving it in an inconsistent state - if matches!(old_tool, ToolType::Path | ToolType::Select | ToolType::Pen | ToolType::Shape) { responses.add(TransformLayerMessage::CancelTransformOperation); } diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index db43470bb9f..8162020607b 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -403,7 +403,6 @@ struct PenToolData { auto_panning: AutoPanning, modifiers: ModifierState, - // buffering_merged_vector: bool, previous_handle_start_pos: DVec2, previous_handle_end_pos: Option, toggle_colinear_debounce: bool, From e119013a34af0ef4723c5676824675b02dffae76 Mon Sep 17 00:00:00 2001 From: VimYoung Date: Mon, 5 Oct 2026 00:06:26 +0530 Subject: [PATCH 06/14] Add: Minor modification in incorrect test --- .../src/messages/tool/tool_messages/pen_tool.rs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index 8162020607b..f8926120fcb 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -402,7 +402,6 @@ struct PenToolData { angle: f64, auto_panning: AutoPanning, modifiers: ModifierState, - previous_handle_start_pos: DVec2, previous_handle_end_pos: Option, toggle_colinear_debounce: bool, @@ -2468,7 +2467,21 @@ mod test_pen_tool { editor.left_mousedown(A.x, A.y, ModifierKeys::empty()).await; editor.move_mouse(move_dir.x, move_dir.y, ModifierKeys::empty(), MouseKeys::LEFT).await; editor.left_mouseup(move_dir.x, move_dir.y, ModifierKeys::empty()).await; - assert_anchors(&editor, &[A]); + + // Ensure that the changes have taken place, this may not be necessary. + editor.runtime.run().await; + + let all_layers: Vec<_> = editor.active_document().metadata().all_layers().collect(); + // TODO: remove the print statements before merging. + println!("ALL LAYERS: {all_layers:?}"); + for l in &all_layers { + let trans = editor.active_document().metadata().transform_to_viewport(*l); + println!("LAYER {l:?} trans: {:?}", trans.translation); + } + + let (layer, vector) = drawn_path(&editor).expect("Expected a drawn path"); + let layer_to_viewport = editor.active_document().metadata().transform_to_viewport(layer); + assert_eq!(layer_to_viewport.translation, A); } #[tokio::test] From b0d777594ff930dd7b5259013c98110f8c9948b2 Mon Sep 17 00:00:00 2001 From: VimYoung Date: Tue, 6 Oct 2026 21:57:05 +0530 Subject: [PATCH 07/14] Fix: Single layer test fix --- .../messages/tool/tool_messages/pen_tool.rs | 45 +++++++++---------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index f8926120fcb..b06e404c116 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -2449,39 +2449,38 @@ mod test_pen_tool { editor.new_document().await; editor.select_primary_color(Color::RED).await; - editor.draw_rect(D.x, D.y, C.x, C.x).await; + // Draw a rectangle not at the origin (so will end up with a non-identity transform) + editor.draw_rect(A.x, A.y, C.x, C.y).await; editor } + /// Using the path tool to merge layers (by setting the endpoint to an anchor of another layer) should produce only expected anchor positions. #[tokio::test] - async fn offset_change_on_snap() { + async fn merging_layers_simple() { let mut editor = create_a_rectangle().await; - // Ultimately brings the pointer over to D's location - let move_dir = A + DVec2::new(0., 100.); - click_pen(&mut editor, A).await; - - // Process of snapping the pen to the rectangle - editor.move_mouse(A.x, A.y, ModifierKeys::empty(), MouseKeys::empty()).await; - editor.left_mousedown(A.x, A.y, ModifierKeys::empty()).await; - editor.move_mouse(move_dir.x, move_dir.y, ModifierKeys::empty(), MouseKeys::LEFT).await; - editor.left_mouseup(move_dir.x, move_dir.y, ModifierKeys::empty()).await; - - // Ensure that the changes have taken place, this may not be necessary. - editor.runtime.run().await; - - let all_layers: Vec<_> = editor.active_document().metadata().all_layers().collect(); - // TODO: remove the print statements before merging. - println!("ALL LAYERS: {all_layers:?}"); - for l in &all_layers { - let trans = editor.active_document().metadata().transform_to_viewport(*l); - println!("LAYER {l:?} trans: {:?}", trans.translation); - } + // Start the pen somewhere random + let pen_start = DVec2::new(999., 999.); + click_pen(&mut editor, pen_start).await; + // Connect to the top right of the rectangle + click_pen(&mut editor, B).await; + // Validate that these anchors are the only ones that exist (TODO: improve code reuse) + let expected_anchors = [A, B, C, D, pen_start]; let (layer, vector) = drawn_path(&editor).expect("Expected a drawn path"); let layer_to_viewport = editor.active_document().metadata().transform_to_viewport(layer); - assert_eq!(layer_to_viewport.translation, A); + let mut viewport_points: Vec = vector.point_domain.positions().iter().map(|&pos| layer_to_viewport.transform_point2(pos)).collect(); + + for (expected_index, &expected_position) in expected_anchors.iter().enumerate() { + let Some(viewport_index) = viewport_points.iter().position(|viewport| viewport.distance_squared(expected_position) < 1e-10) else { + panic!("The expected anchor index {expected_index} and position {expected_position} was not found in the actual anchors {viewport_points:?}"); + }; + println!("Successfully found expected position {expected_position} (index {expected_index}) in viewport points as index {viewport_index}"); + // Remove so no other one matches + viewport_points.remove(viewport_index); + } + assert!(viewport_points.is_empty(), "Viewport point(s) were not matched: {viewport_points:?}"); } #[tokio::test] From 678e1558b3c9b47adebbee36fc064cf27b066f79 Mon Sep 17 00:00:00 2001 From: VimYoung Date: Wed, 7 Oct 2026 07:50:11 +0530 Subject: [PATCH 08/14] Add: Two pen strokes merge on anchor points test --- .../messages/tool/tool_messages/pen_tool.rs | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index b06e404c116..c719667ee53 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -2483,6 +2483,39 @@ mod test_pen_tool { assert!(viewport_points.is_empty(), "Viewport point(s) were not matched: {viewport_points:?}"); } + #[tokio::test] + async fn merging_two_pen_paths() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + + // Create First Pen path + click_pen(&mut editor, A).await; + click_pen(&mut editor, C).await; + + // Abort the first pen process + editor.handle_message(ToolMessage::Pen(PenToolMessage::Abort)).await; + + // Create a second pen stroke + click_pen(&mut editor, B).await; + click_pen(&mut editor, C).await; + + // Validate that these anchors are the only ones that exist (TODO: improve code reuse) + let expected_anchors = [A, B, C]; + let (layer, vector) = drawn_path(&editor).expect("Expected a drawn path"); + let layer_to_viewport = editor.active_document().metadata().transform_to_viewport(layer); + let mut viewport_points: Vec = vector.point_domain.positions().iter().map(|&pos| layer_to_viewport.transform_point2(pos)).collect(); + + for (expected_index, &expected_position) in expected_anchors.iter().enumerate() { + let Some(viewport_index) = viewport_points.iter().position(|viewport| viewport.distance_squared(expected_position) < 1e-10) else { + panic!("The expected anchor index {expected_index} and position {expected_position} was not found in the actual anchors {viewport_points:?}"); + }; + println!("Successfully found expected position {expected_position} (index {expected_index}) in viewport points as index {viewport_index}"); + // Remove so no other one matches + viewport_points.remove(viewport_index); + } + assert!(viewport_points.is_empty(), "Viewport point(s) were not matched: {viewport_points:?}"); + } + #[tokio::test] async fn each_segment_and_the_closing_click_are_their_own_history_steps() { let mut editor = EditorTestUtils::create(); From 59dd19b60364b6ea9247df4e5bcdb13cf2e984dd Mon Sep 17 00:00:00 2001 From: VimYoung Date: Sun, 11 Oct 2026 12:05:59 +0530 Subject: [PATCH 09/14] Fix: Code abstraction for checking anchor points --- .../messages/tool/tool_messages/pen_tool.rs | 42 ++++++++----------- 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index 61ecb3938b0..5f74f982f24 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -2437,6 +2437,21 @@ mod test_pen_tool { use graphene_std::vector::misc::ManipulatorPointId; use graphene_std::vector::{SegmentId, Vector}; + fn assert_expected_anchors(editor: &EditorTestUtils, expected_anchors: &[DVec2]) { + let (layer, vector) = drawn_path(editor).expect("Expected a drawn path"); + let layer_to_viewport = editor.active_document().metadata().transform_to_viewport(layer); + let mut viewport_points: Vec = vector.point_domain.positions().iter().map(|&pos| layer_to_viewport.transform_point2(pos)).collect(); + + for (expected_index, &expected_position) in expected_anchors.iter().enumerate() { + let Some(viewport_index) = viewport_points.iter().position(|viewport| viewport.distance_squared(expected_position) < 1e-10) else { + panic!("The expected anchor index {expected_index} and position {expected_position} was not found in the actual anchors {viewport_points:?}"); + }; + println!("Successfully found expected position {expected_position} (index {expected_index}) in viewport points as index {viewport_index}"); + viewport_points.remove(viewport_index); + } + assert!(viewport_points.is_empty(), "Viewport point(s) were not matched: {viewport_points:?}"); + } + /// The single Pen-drawn path layer and its vector, or `None` once the layer has been undone away. fn drawn_path(editor: &EditorTestUtils) -> Option<(LayerNodeIdentifier, Vector)> { let document = editor.active_document(); @@ -2523,19 +2538,7 @@ mod test_pen_tool { // Validate that these anchors are the only ones that exist (TODO: improve code reuse) let expected_anchors = [A, B, C, D, pen_start]; - let (layer, vector) = drawn_path(&editor).expect("Expected a drawn path"); - let layer_to_viewport = editor.active_document().metadata().transform_to_viewport(layer); - let mut viewport_points: Vec = vector.point_domain.positions().iter().map(|&pos| layer_to_viewport.transform_point2(pos)).collect(); - - for (expected_index, &expected_position) in expected_anchors.iter().enumerate() { - let Some(viewport_index) = viewport_points.iter().position(|viewport| viewport.distance_squared(expected_position) < 1e-10) else { - panic!("The expected anchor index {expected_index} and position {expected_position} was not found in the actual anchors {viewport_points:?}"); - }; - println!("Successfully found expected position {expected_position} (index {expected_index}) in viewport points as index {viewport_index}"); - // Remove so no other one matches - viewport_points.remove(viewport_index); - } - assert!(viewport_points.is_empty(), "Viewport point(s) were not matched: {viewport_points:?}"); + assert_expected_anchors(&editor, &expected_anchors); } #[tokio::test] @@ -2556,19 +2559,10 @@ mod test_pen_tool { // Validate that these anchors are the only ones that exist (TODO: improve code reuse) let expected_anchors = [A, B, C]; - let (layer, vector) = drawn_path(&editor).expect("Expected a drawn path"); - let layer_to_viewport = editor.active_document().metadata().transform_to_viewport(layer); - let mut viewport_points: Vec = vector.point_domain.positions().iter().map(|&pos| layer_to_viewport.transform_point2(pos)).collect(); + assert_expected_anchors(&editor, &expected_anchors); + } - for (expected_index, &expected_position) in expected_anchors.iter().enumerate() { - let Some(viewport_index) = viewport_points.iter().position(|viewport| viewport.distance_squared(expected_position) < 1e-10) else { - panic!("The expected anchor index {expected_index} and position {expected_position} was not found in the actual anchors {viewport_points:?}"); - }; - println!("Successfully found expected position {expected_position} (index {expected_index}) in viewport points as index {viewport_index}"); - // Remove so no other one matches - viewport_points.remove(viewport_index); } - assert!(viewport_points.is_empty(), "Viewport point(s) were not matched: {viewport_points:?}"); } #[tokio::test] From b2fc0a4999f93ed7808b8913df0c7d6209e0fad5 Mon Sep 17 00:00:00 2001 From: VimYoung Date: Sun, 11 Oct 2026 12:07:46 +0530 Subject: [PATCH 10/14] Misc: syntax fix --- editor/src/messages/tool/tool_messages/pen_tool.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index 5f74f982f24..6933e84057c 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -2562,9 +2562,6 @@ mod test_pen_tool { assert_expected_anchors(&editor, &expected_anchors); } - } - } - #[tokio::test] async fn each_segment_and_the_closing_click_are_their_own_history_steps() { let mut editor = EditorTestUtils::create(); From 62444019d91778e602659f7350a4b8ded47999b1 Mon Sep 17 00:00:00 2001 From: VimYoung Date: Sun, 11 Oct 2026 12:12:56 +0530 Subject: [PATCH 11/14] Add: Group with offset test pass --- .../messages/tool/tool_messages/pen_tool.rs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index 6933e84057c..365e05bfcf9 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -2432,6 +2432,7 @@ impl Fsm for PenToolFsmState { mod test_pen_tool { use crate::messages::input_mapper::utility_types::pointer::EditorPointerState; use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; + use crate::messages::portfolio::document::utility_types::misc::GroupFolderType; use crate::test_utils::test_prelude::*; use glam::DAffine2; use graphene_std::vector::misc::ManipulatorPointId; @@ -2562,6 +2563,39 @@ mod test_pen_tool { assert_expected_anchors(&editor, &expected_anchors); } + #[tokio::test] + async fn merging_layers_in_group_with_offset() { + let mut editor = create_a_rectangle().await; + + // Group rectangle into Group + editor + .handle_message(DocumentMessage::GroupSelectedLayers { + group_folder_type: GroupFolderType::Layer, + }) + .await; + + let group = editor.active_document().metadata().all_layers().next().unwrap(); + let offset = DVec2::new(50., 60.); + editor + .handle_message(GraphOperationMessage::TransformSet { + layer: group, + transform: DAffine2::from_translation(offset), + transform_in: TransformIn::Local, + skip_rerender: false, + }) + .await; + + // Start the pen somewhere random + let pen_start = DVec2::new(999., 999.); + click_pen(&mut editor, pen_start).await; + // Connect to the top right of the rectangle (offset by the group transform) + click_pen(&mut editor, B + offset).await; + + let expected_anchors = [A + offset, B + offset, C + offset, D + offset, pen_start]; + assert_expected_anchors(&editor, &expected_anchors); + } + + } #[tokio::test] async fn each_segment_and_the_closing_click_are_their_own_history_steps() { let mut editor = EditorTestUtils::create(); From 729fb9ae15164d8e4f22a1f2c106efa94ebb5352 Mon Sep 17 00:00:00 2001 From: VimYoung Date: Sun, 11 Oct 2026 12:14:17 +0530 Subject: [PATCH 12/14] Add: Merge layers with different offset --- .../messages/tool/tool_messages/pen_tool.rs | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index 365e05bfcf9..d1542de1fdb 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -2595,6 +2595,58 @@ mod test_pen_tool { assert_expected_anchors(&editor, &expected_anchors); } + #[tokio::test] + async fn merging_layers_in_different_groups_with_offsets() { + let mut editor = create_a_rectangle().await; + + // Group rectangle into Group 1 + editor + .handle_message(DocumentMessage::GroupSelectedLayers { + group_folder_type: GroupFolderType::Layer, + }) + .await; + + let group1 = editor.active_document().metadata().all_layers().next().unwrap(); + let offset1 = DVec2::new(50., 60.); + editor + .handle_message(GraphOperationMessage::TransformSet { + layer: group1, + transform: DAffine2::from_translation(offset1), + transform_in: TransformIn::Local, + skip_rerender: false, + }) + .await; + + // Nest Group 1 inside Group 2 with a different offset + editor + .handle_message(DocumentMessage::GroupSelectedLayers { + group_folder_type: GroupFolderType::Layer, + }) + .await; + + let group2 = editor.active_document().metadata().all_layers().next().unwrap(); + let offset2 = DVec2::new(30., -20.); + editor + .handle_message(GraphOperationMessage::TransformSet { + layer: group2, + transform: DAffine2::from_translation(offset2), + transform_in: TransformIn::Local, + skip_rerender: false, + }) + .await; + + let total_offset = offset1 + offset2; + + // Start the pen somewhere random + let pen_start = DVec2::new(999., 999.); + click_pen(&mut editor, pen_start).await; + // Connect to the top right of the rectangle (offset by the nested group transforms) + click_pen(&mut editor, B + total_offset).await; + + let expected_anchors = [A + total_offset, B + total_offset, C + total_offset, D + total_offset, pen_start]; + assert_expected_anchors(&editor, &expected_anchors); + } + } #[tokio::test] async fn each_segment_and_the_closing_click_are_their_own_history_steps() { From e8088c5c5bec4cfc21a6eac577447bb6b78185ab Mon Sep 17 00:00:00 2001 From: VimYoung Date: Sun, 11 Oct 2026 12:18:24 +0530 Subject: [PATCH 13/14] Add: Artboard offsets test add --- .../messages/tool/tool_messages/pen_tool.rs | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index d1542de1fdb..241e8af330f 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -2647,6 +2647,90 @@ mod test_pen_tool { assert_expected_anchors(&editor, &expected_anchors); } + #[tokio::test] + async fn merging_layers_in_artboard_with_offset() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + + let offset = DVec2::new(60., 70.); + editor.drag_tool(ToolType::Artboard, 0., 0., 500., 500., ModifierKeys::empty()).await; + let artboard = editor.active_document().metadata().all_layers().next().unwrap(); + editor + .handle_message(GraphOperationMessage::TransformSet { + layer: artboard, + transform: DAffine2::from_translation(offset), + transform_in: TransformIn::Local, + skip_rerender: false, + }) + .await; + + editor.select_primary_color(Color::RED).await; + editor.draw_rect(A.x + offset.x, A.y + offset.y, C.x + offset.x, C.y + offset.y).await; + + let pen_start = DVec2::new(999., 999.); + click_pen(&mut editor, pen_start).await; + click_pen(&mut editor, B + offset).await; + + let expected_anchors = [A + offset, B + offset, C + offset, D + offset, pen_start]; + assert_expected_anchors(&editor, &expected_anchors); + } + + #[tokio::test] + async fn merging_layers_in_artboards_with_different_offsets() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + + // Create first artboard with offset + editor.drag_tool(ToolType::Artboard, 0., 0., 400., 400., ModifierKeys::empty()).await; + let artboard1 = editor.active_document().metadata().all_layers().next().unwrap(); + let offset1 = DVec2::new(50., 60.); + editor + .handle_message(GraphOperationMessage::TransformSet { + layer: artboard1, + transform: DAffine2::from_translation(offset1), + transform_in: TransformIn::Local, + skip_rerender: false, + }) + .await; + + // Draw rectangle in first artboard + editor.select_primary_color(Color::RED).await; + editor.draw_rect(A.x + offset1.x, A.y + offset1.y, C.x + offset1.x, C.y + offset1.y).await; + + // Create second artboard with different offset + let offset2 = DVec2::new(-30., 40.); + let artboard2_origin = DVec2::new(600., 600.); + editor + .drag_tool( + ToolType::Artboard, + artboard2_origin.x, + artboard2_origin.y, + artboard2_origin.x + 400., + artboard2_origin.y + 400., + ModifierKeys::empty(), + ) + .await; + let artboard2 = editor.active_document().metadata().all_layers().next().unwrap(); + editor + .handle_message(GraphOperationMessage::TransformSet { + layer: artboard2, + transform: DAffine2::from_translation(offset2), + transform_in: TransformIn::Local, + skip_rerender: false, + }) + .await; + + // Start pen in second artboard + let pen_start = artboard2_origin + offset2 + DVec2::new(50., 50.); + click_pen(&mut editor, pen_start).await; + + // Connect to anchor B of the rectangle in the first artboard + click_pen(&mut editor, B + offset1).await; + + let expected_anchors = [A + offset1, B + offset1, C + offset1, D + offset1, pen_start]; + assert_expected_anchors(&editor, &expected_anchors); + } + } #[tokio::test] async fn each_segment_and_the_closing_click_are_their_own_history_steps() { From ba3f723d99093c6924c60efaf7e8435ad859af29 Mon Sep 17 00:00:00 2001 From: VimYoung Date: Sun, 11 Oct 2026 12:19:01 +0530 Subject: [PATCH 14/14] Add: Boolean Operation test --- .../messages/tool/tool_messages/pen_tool.rs | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 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 241e8af330f..ec57a3a5138 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -2435,7 +2435,7 @@ mod test_pen_tool { use crate::messages::portfolio::document::utility_types::misc::GroupFolderType; use crate::test_utils::test_prelude::*; use glam::DAffine2; - use graphene_std::vector::misc::ManipulatorPointId; + use graphene_std::vector::misc::{BooleanOperation, ManipulatorPointId}; use graphene_std::vector::{SegmentId, Vector}; fn assert_expected_anchors(editor: &EditorTestUtils, expected_anchors: &[DVec2]) { @@ -2731,7 +2731,50 @@ mod test_pen_tool { assert_expected_anchors(&editor, &expected_anchors); } + async fn test_boolean_operation(operation: BooleanOperation) { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + + editor.select_primary_color(Color::RED).await; + editor.draw_rect(A.x, A.y, C.x, C.y).await; + editor.draw_rect(150., 150., 250., 250.).await; + + // Select both rectangles and create boolean operation layer + let layers: Vec<_> = editor.active_document().metadata().all_layers().collect(); + editor + .handle_message(NodeGraphMessage::SelectedNodesSet { + nodes: layers.iter().map(|l| l.to_node()).collect(), + }) + .await; + + editor + .handle_message(DocumentMessage::GroupSelectedLayers { + group_folder_type: GroupFolderType::BooleanOperation(operation), + }) + .await; + + let bool_layer = editor.active_document().metadata().all_layers().next().unwrap(); + let bool_vector = editor.active_document().network_interface.compute_modified_vector(bool_layer).expect("Boolean layer vector"); + let bool_to_viewport = editor.active_document().metadata().transform_to_viewport(bool_layer); + let mut expected_anchors: Vec = bool_vector.point_domain.positions().iter().map(|&pos| bool_to_viewport.transform_point2(pos)).collect(); + + // Connect pen to one of the anchors of the boolean operation + let connect_anchor = expected_anchors[0]; + let pen_start = DVec2::new(999., 999.); + click_pen(&mut editor, pen_start).await; + click_pen(&mut editor, connect_anchor).await; + + expected_anchors.push(pen_start); + assert_expected_anchors(&editor, &expected_anchors); + } + + #[tokio::test] + async fn merging_layers_with_different_boolean_outputs() { + for operation in [BooleanOperation::Union, BooleanOperation::SubtractFront, BooleanOperation::Intersect, BooleanOperation::Difference] { + test_boolean_operation(operation).await; + } } + #[tokio::test] async fn each_segment_and_the_closing_click_are_their_own_history_steps() { let mut editor = EditorTestUtils::create();