diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index 737df21f54a..ec57a3a5138 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -2432,11 +2432,27 @@ 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; + use graphene_std::vector::misc::{BooleanOperation, 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(); @@ -2498,6 +2514,267 @@ 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; + // 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 merging_layers_simple() { + let mut editor = create_a_rectangle().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 + 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]; + assert_expected_anchors(&editor, &expected_anchors); + } + + #[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]; + 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 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 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); + } + + 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();