diff --git a/editor/src/messages/tool/common_functionality/shapes/grid_shape.rs b/editor/src/messages/tool/common_functionality/shapes/grid_shape.rs index 7a7f1b0b19..8a436898a9 100644 --- a/editor/src/messages/tool/common_functionality/shapes/grid_shape.rs +++ b/editor/src/messages/tool/common_functionality/shapes/grid_shape.rs @@ -254,3 +254,58 @@ fn calculate_isometric_x_position(y_spacing: f64, rad_a: f64, rad_b: f64) -> f64 let spacing_x = y_spacing / (rad_a.tan() + rad_b.tan()); spacing_x * 9. } +#[cfg(test)] +mod tests { + use super::calculate_grid_params; + use glam::DVec2; + + #[test] + fn grid_params_basic_rectangle() { + // Simple downward-right drag: translation = start, dimensions = raw/9, no angle + let (translation, dimensions, angle) = calculate_grid_params(DVec2::ZERO, DVec2::new(90., 90.), false, false, false); + assert_eq!(translation, DVec2::ZERO); + assert_eq!(dimensions, DVec2::splat(10.)); // 90/9 = 10 + assert!(angle.is_none()); + } + + #[test] + fn grid_params_lock_ratio_forces_square_spacing() { + // Non-square drag (90x45) with lock_ratio: uses larger dim (90), dimensions = 90/9 = 10 + let (_, dimensions, angle) = calculate_grid_params(DVec2::ZERO, DVec2::new(90., 45.), false, false, true); + assert_eq!(dimensions, DVec2::splat(10.)); + assert!(angle.is_none()); + } + + #[test] + fn grid_params_center_doubles_dimensions_and_shifts_translation() { + // Center draw: dimensions doubled, translation shifted back by raw_dimensions + let (translation, dimensions, angle) = calculate_grid_params(DVec2::ZERO, DVec2::new(90., 90.), false, true, false); + assert_eq!(translation, DVec2::splat(-90.)); + assert_eq!(dimensions, DVec2::splat(20.)); // 2 * 90/9 = 20 + assert!(angle.is_none()); + } + + #[test] + fn grid_params_negative_drag_adjusts_translation() { + // Drag up-left from (100,100) to (10,10): translation must shift to (10,10) + let (translation, dimensions, angle) = calculate_grid_params(DVec2::splat(100.), DVec2::splat(10.), false, false, false); + assert!((translation.x - 10.).abs() < 1e-10, "Expected translation.x=10, got {}", translation.x); + assert!((translation.y - 10.).abs() < 1e-10, "Expected translation.y=10, got {}", translation.y); + assert_eq!(dimensions, DVec2::splat(10.)); // (90,90)/9 + assert!(angle.is_none()); + } + + #[test] + fn grid_params_isometric_produces_angle() { + // Isometric grid (no lock_ratio): angle is dynamically computed from drag + let (_, _, angle) = calculate_grid_params(DVec2::ZERO, DVec2::new(90., 90.), true, false, false); + assert!(angle.is_some(), "Isometric grid should return an angle"); + } + + #[test] + fn grid_params_isometric_lock_ratio_fixes_angle_at_30() { + // Isometric + lock_ratio: angle is standardized at 30 degrees + let (_, _, angle) = calculate_grid_params(DVec2::ZERO, DVec2::new(90., 90.), true, false, true); + assert_eq!(angle, Some(30.), "Isometric lock_ratio should fix angle at 30°"); + } +} diff --git a/editor/src/messages/tool/common_functionality/shapes/polygon_shape.rs b/editor/src/messages/tool/common_functionality/shapes/polygon_shape.rs index 7b22144a71..e4b0d19c7a 100644 --- a/editor/src/messages/tool/common_functionality/shapes/polygon_shape.rs +++ b/editor/src/messages/tool/common_functionality/shapes/polygon_shape.rs @@ -191,3 +191,83 @@ impl Polygon { responses.add(NodeGraphMessage::RunDocumentGraph); } } + +#[cfg(test)] +mod test_polygon { + use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer; + use crate::test_utils::test_prelude::*; + use graph_craft::document::value::TaggedValue; + + /// Reads sides and radius from the first polygon node found in the document. + fn get_polygon_inputs(editor: &EditorTestUtils) -> Option<(u32, f64)> { + let document = editor.active_document(); + document.metadata().all_layers().find_map(|layer| { + let inputs = NodeGraphLayer::new(layer, &document.network_interface) + .find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::vector_nodes::regular_polygon::IDENTIFIER))?; + let Some(&TaggedValue::U32(sides)) = inputs.get(1).and_then(|i| i.as_value()) else { + return None; + }; + let Some(&TaggedValue::F64(radius)) = inputs.get(2).and_then(|i| i.as_value()) else { + return None; + }; + Some((sides, radius)) + }) + } + + #[tokio::test] + async fn polygon_draw_simple() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + editor.drag_tool(ToolType::Shape, 0., 0., 100., 100., ModifierKeys::empty()).await; + + assert_eq!(editor.active_document().metadata().all_layers().count(), 1); + let (sides, radius) = get_polygon_inputs(&editor).expect("Polygon node should exist after draw"); + assert!(sides >= 3, "Polygon should have at least 3 sides, got {sides}"); + assert!((radius - 50.).abs() < 1., "Expected radius ≈ 50 for 100×100 drag, got {radius}"); + } + + #[tokio::test] + async fn polygon_draw_non_square_uses_shorter_dimension() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + // Drag wider than tall: dimensions = (100, 60), radius = shorter/2 = 30 + editor.drag_tool(ToolType::Shape, 0., 0., 100., 60., ModifierKeys::empty()).await; + + let (_, radius) = get_polygon_inputs(&editor).expect("Polygon node should exist"); + assert!((radius - 30.).abs() < 1., "Expected radius ≈ 30 for 100×60 drag, got {radius}"); + } + + #[tokio::test] + async fn polygon_draw_shift_lock_ratio() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + // SHIFT forces equal dimensions — a 100×60 drag becomes 100×100 + editor.drag_tool(ToolType::Shape, 0., 0., 100., 60., ModifierKeys::SHIFT).await; + + let (_, radius) = get_polygon_inputs(&editor).expect("Polygon node should exist"); + assert!((radius - 50.).abs() < 1., "Expected radius ≈ 50 with SHIFT lock ratio on 100×60 drag, got {radius}"); + } + + #[tokio::test] + async fn polygon_default_six_sides() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + editor.drag_tool(ToolType::Shape, 0., 0., 100., 100., ModifierKeys::empty()).await; + + let (sides, _) = get_polygon_inputs(&editor).expect("Polygon node should exist"); + assert_eq!(sides, 5, "Default polygon should have 5 sides"); + } + + #[tokio::test] + async fn polygon_cancel_rmb_no_layer() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + editor.drag_tool_cancel_rmb(ToolType::Shape).await; + + assert_eq!( + editor.active_document().metadata().all_layers().count(), + 0, + "RMB-cancelled polygon should not create a layer" + ); + } +} diff --git a/editor/src/messages/tool/common_functionality/shapes/star_shape.rs b/editor/src/messages/tool/common_functionality/shapes/star_shape.rs index acdde2c286..a0b225cc1d 100644 --- a/editor/src/messages/tool/common_functionality/shapes/star_shape.rs +++ b/editor/src/messages/tool/common_functionality/shapes/star_shape.rs @@ -170,3 +170,101 @@ impl Star { } } } + +#[cfg(test)] +mod test_star { + use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer; + use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeType; + use crate::messages::tool::tool_messages::shape_tool::ShapeOptionsUpdate; + use crate::test_utils::test_prelude::*; + use graph_craft::document::value::TaggedValue; + + /// Switch to Star shape type, then manually drag to avoid drag_tool re-selecting and resetting options. + async fn draw_star(editor: &mut EditorTestUtils, x1: f64, y1: f64, x2: f64, y2: f64, modifier_keys: ModifierKeys) { + editor.select_tool(ToolType::Shape).await; + editor + .handle_message(ShapeToolMessage::UpdateOptions { + options: ShapeOptionsUpdate::ShapeType(ShapeType::Star), + }) + .await; + editor.move_mouse(x1, y1, modifier_keys, MouseKeys::empty()).await; + editor.left_mousedown(x1, y1, modifier_keys).await; + editor.move_mouse(x2, y2, modifier_keys, MouseKeys::LEFT).await; + editor.left_mouseup(x2, y2, modifier_keys).await; + } + + /// Returns (sides, outer_radius, inner_radius) from the first star node in the document. + fn get_star_inputs(editor: &EditorTestUtils) -> Option<(u32, f64, f64)> { + let document = editor.active_document(); + document.metadata().all_layers().find_map(|layer| { + let inputs = NodeGraphLayer::new(layer, &document.network_interface) + .find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::vector_nodes::star::IDENTIFIER))?; + let Some(&TaggedValue::U32(sides)) = inputs.get(1).and_then(|i| i.as_value()) else { + return None; + }; + let Some(&TaggedValue::F64(outer_radius)) = inputs.get(2).and_then(|i| i.as_value()) else { + return None; + }; + let Some(&TaggedValue::F64(inner_radius)) = inputs.get(3).and_then(|i| i.as_value()) else { + return None; + }; + Some((sides, outer_radius, inner_radius)) + }) + } + + #[tokio::test] + async fn star_draw_simple() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + draw_star(&mut editor, 0., 0., 100., 100., ModifierKeys::empty()).await; + + assert_eq!(editor.active_document().metadata().all_layers().count(), 1); + let (sides, outer_radius, _) = get_star_inputs(&editor).expect("Star node should exist after draw"); + assert!(sides >= 2, "Star should have at least 2 points, got {sides}"); + assert!(outer_radius > 0., "Outer radius should be positive, got {outer_radius}"); + } + + #[tokio::test] + async fn star_inner_radius_is_half_outer() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + draw_star(&mut editor, 0., 0., 100., 100., ModifierKeys::empty()).await; + + let (_, outer_radius, inner_radius) = get_star_inputs(&editor).expect("Star node should exist"); + assert!( + (inner_radius - outer_radius / 2.).abs() < 1e-10, + "Inner radius {inner_radius} should equal outer_radius/2 = {}", + outer_radius / 2. + ); + } + + #[tokio::test] + async fn star_draw_correct_outer_radius() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + // 100x100 drag: dimensions=(100,100), x==y, radius = x/2 = 50 + draw_star(&mut editor, 0., 0., 100., 100., ModifierKeys::empty()).await; + + let (_, outer_radius, _) = get_star_inputs(&editor).expect("Star node should exist"); + assert!((outer_radius - 50.).abs() < 1., "Expected outer radius ~50 for 100x100 drag, got {outer_radius}"); + } + + #[tokio::test] + async fn star_cancel_rmb_no_layer() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + editor.select_tool(ToolType::Shape).await; + editor + .handle_message(ShapeToolMessage::UpdateOptions { + options: ShapeOptionsUpdate::ShapeType(ShapeType::Star), + }) + .await; + editor.drag_tool_cancel_rmb(ToolType::Shape).await; + + assert_eq!( + editor.active_document().metadata().all_layers().count(), + 0, + "RMB-cancelled star should not create a layer" + ); + } +}