From b73bb8ca40c1b99a9b2928c04ba4368ddf8bd19a Mon Sep 17 00:00:00 2001 From: Saloni Tarone Date: Fri, 27 Mar 2026 13:18:02 +0530 Subject: [PATCH 1/2] Add unit tests for shape_utility pure functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #3954 Tests cover all branches of the following pure functions in shape_utility.rs: - wrap_to_tau: zero, positive within range, exactly TAU, beyond TAU, negative values (wraps correctly into [0, 2π)) - format_rounded: trailing-zero trimming, dot trimming, rounding up, zero precision, zero value, all-significant digits retained - calculate_display_angle: positive within range, positive wrapping past 360°/720°, exactly 360°, negative small angle, negative beyond -360°, positive-zero input - arc_end_points_ignore_layer: zero sweep, quarter sweep, half-circle sweep, radius scaling, identity viewport vs. no viewport - star_vertex_position: even index (outer radius), odd index (inner radius), second outer vertex — verifying angle and radius selection math - polygon_vertex_position: vertex 0 (up), vertex 1 of square (right), vertex 2 of square (down) - inside_polygon: center inside, far point outside (bbox early exit), point beyond vertex outside, point near center inside - inside_star: center inside, far point outside (bbox early exit), point beyond outer tip outside, point near center inside --- .../shapes/shape_utility.rs | 271 ++++++++++++++++++ 1 file changed, 271 insertions(+) diff --git a/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs b/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs index 96dbdd5165..7c07018bde 100644 --- a/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs +++ b/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs @@ -593,3 +593,274 @@ pub fn extract_grid_parameters(layer: LayerNodeIdentifier, document: &DocumentMe Some((grid_type, spacing, columns, rows, angles)) } + +#[cfg(test)] +mod tests { + use super::{arc_end_points_ignore_layer, calculate_display_angle, format_rounded, inside_polygon, inside_star, polygon_vertex_position, star_vertex_position, wrap_to_tau}; + use glam::{DAffine2, DVec2}; + use std::f64::consts::{PI, TAU}; + + // ── wrap_to_tau ───────────────────────────────────────────────────────────── + + #[test] + fn wrap_zero_stays_zero() { + assert_eq!(wrap_to_tau(0.), 0.); + } + + #[test] + fn wrap_pi_stays_pi() { + assert!((wrap_to_tau(PI) - PI).abs() < 1e-10); + } + + #[test] + fn wrap_tau_becomes_zero() { + assert!(wrap_to_tau(TAU).abs() < 1e-10); + } + + #[test] + fn wrap_beyond_tau_reduces_to_remainder() { + // TAU + 1 wraps back to 1 + assert!((wrap_to_tau(TAU + 1.) - 1.).abs() < 1e-10); + } + + #[test] + fn wrap_negative_pi_becomes_pi() { + // -π + 2π = π + assert!((wrap_to_tau(-PI) - PI).abs() < 1e-10); + } + + #[test] + fn wrap_negative_small_angle_wraps_near_tau() { + // -0.5 → TAU - 0.5 + assert!((wrap_to_tau(-0.5) - (TAU - 0.5)).abs() < 1e-10); + } + + #[test] + fn wrap_two_full_turns_returns_zero() { + assert!(wrap_to_tau(2. * TAU).abs() < 1e-10); + } + + // ── format_rounded ────────────────────────────────────────────────────────── + + #[test] + fn format_rounded_trims_trailing_zeros_and_dot() { + assert_eq!(format_rounded(1.0, 2), "1"); + } + + #[test] + fn format_rounded_keeps_significant_decimal() { + assert_eq!(format_rounded(1.5, 2), "1.5"); + } + + #[test] + fn format_rounded_trims_trailing_zero_only() { + assert_eq!(format_rounded(1.50, 3), "1.5"); + } + + #[test] + fn format_rounded_zero_precision_integer() { + assert_eq!(format_rounded(100.0, 0), "100"); + } + + #[test] + fn format_rounded_rounds_last_digit() { + assert_eq!(format_rounded(3.14159, 3), "3.142"); + } + + #[test] + fn format_rounded_zero_value() { + assert_eq!(format_rounded(0.0, 3), "0"); + } + + #[test] + fn format_rounded_preserves_all_significant_digits() { + assert_eq!(format_rounded(1.23, 2), "1.23"); + } + + // ── calculate_display_angle ───────────────────────────────────────────────── + + #[test] + fn display_angle_positive_within_range_unchanged() { + assert!((calculate_display_angle(45.) - 45.).abs() < 1e-10); + } + + #[test] + fn display_angle_positive_beyond_360_wraps() { + // 400° → 40° + assert!((calculate_display_angle(400.) - 40.).abs() < 1e-10); + } + + #[test] + fn display_angle_exactly_360_becomes_zero() { + assert!(calculate_display_angle(360.).abs() < 1e-10); + } + + #[test] + fn display_angle_720_becomes_zero() { + assert!(calculate_display_angle(720.).abs() < 1e-10); + } + + #[test] + fn display_angle_negative_small_unchanged() { + // -45 is in (−360, 0): formula returns -45 + assert!((calculate_display_angle(-45.) - (-45.)).abs() < 1e-10); + } + + #[test] + fn display_angle_negative_beyond_neg_360_wraps() { + // -400° → -40° + assert!((calculate_display_angle(-400.) - (-40.)).abs() < 1e-10); + } + + #[test] + fn display_angle_positive_zero_returns_zero() { + // +0.0 is sign-positive, first branch: 0 − 0 = 0 + assert_eq!(calculate_display_angle(0.), 0.); + } + + // ── arc_end_points_ignore_layer ───────────────────────────────────────────── + + #[test] + fn arc_endpoints_no_viewport_zero_start_zero_sweep_at_unit_radius() { + // start=0°, sweep=0°: both points at (1, 0) + let (start, end) = arc_end_points_ignore_layer(1., 0., 0., None).unwrap(); + assert!((start.x - 1.).abs() < 1e-10, "start.x expected 1, got {}", start.x); + assert!(start.y.abs() < 1e-10, "start.y expected 0, got {}", start.y); + assert!((end.x - 1.).abs() < 1e-10, "end.x expected 1, got {}", end.x); + assert!(end.y.abs() < 1e-10, "end.y expected 0, got {}", end.y); + } + + #[test] + fn arc_endpoints_no_viewport_quarter_sweep() { + // start=0°, sweep=90°: start at (1,0), end at (0,1) + let (start, end) = arc_end_points_ignore_layer(1., 0., 90., None).unwrap(); + assert!((start.x - 1.).abs() < 1e-10, "start.x expected 1, got {}", start.x); + assert!(start.y.abs() < 1e-10, "start.y expected 0, got {}", start.y); + assert!(end.x.abs() < 1e-10, "end.x expected 0, got {}", end.x); + assert!((end.y - 1.).abs() < 1e-10, "end.y expected 1, got {}", end.y); + } + + #[test] + fn arc_endpoints_scales_with_radius() { + // Radius 5 at start=0°, sweep=0°: start at (5, 0) + let (start, _) = arc_end_points_ignore_layer(5., 0., 0., None).unwrap(); + assert!((start.x - 5.).abs() < 1e-10, "start.x expected 5, got {}", start.x); + } + + #[test] + fn arc_endpoints_with_identity_viewport_matches_no_viewport() { + // Identity transform must not change coordinates + let (start_id, end_id) = arc_end_points_ignore_layer(1., 0., 90., Some(DAffine2::IDENTITY)).unwrap(); + let (start_none, end_none) = arc_end_points_ignore_layer(1., 0., 90., None).unwrap(); + assert!((start_id - start_none).length() < 1e-10); + assert!((end_id - end_none).length() < 1e-10); + } + + #[test] + fn arc_endpoints_half_circle_sweep() { + // start=0°, sweep=180°: end lands at (-1, 0) for unit radius + let (_, end) = arc_end_points_ignore_layer(1., 0., 180., None).unwrap(); + assert!((end.x - (-1.)).abs() < 1e-10, "end.x expected -1, got {}", end.x); + assert!(end.y.abs() < 1e-10, "end.y expected 0, got {}", end.y); + } + + // ── star_vertex_position ──────────────────────────────────────────────────── + + #[test] + fn star_vertex_even_index_uses_outer_radius() { + // vertex_index=0 (even) → outer radius, angle=0 → (0, -radius1) + let pos = star_vertex_position(DAffine2::IDENTITY, 0, 5, 10., 5.); + assert!(pos.x.abs() < 1e-10, "x expected ~0, got {}", pos.x); + assert!((pos.y - (-10.)).abs() < 1e-10, "y expected -10, got {}", pos.y); + } + + #[test] + fn star_vertex_odd_index_uses_inner_radius() { + // vertex_index=1 (odd) → inner radius + let pos = star_vertex_position(DAffine2::IDENTITY, 1, 5, 10., 5.); + let angle = PI / 5.; + assert!((pos.x - 5. * angle.sin()).abs() < 1e-10, "x mismatch, got {}", pos.x); + assert!((pos.y - (-5. * angle.cos())).abs() < 1e-10, "y mismatch, got {}", pos.y); + } + + #[test] + fn star_vertex_second_outer_point() { + // vertex_index=2 (even) → outer radius, angle = 2π/5 + let pos = star_vertex_position(DAffine2::IDENTITY, 2, 5, 10., 5.); + let angle = 2. * PI / 5.; + assert!((pos.x - 10. * angle.sin()).abs() < 1e-10, "x mismatch, got {}", pos.x); + assert!((pos.y - (-10. * angle.cos())).abs() < 1e-10, "y mismatch, got {}", pos.y); + } + + // ── polygon_vertex_position ────────────────────────────────────────────────── + + #[test] + fn polygon_vertex_zero_index_points_up() { + // vertex 0: angle=0 → x=0, y=−radius + let pos = polygon_vertex_position(DAffine2::IDENTITY, 0, 4, 10.); + assert!(pos.x.abs() < 1e-10, "x expected ~0, got {}", pos.x); + assert!((pos.y - (-10.)).abs() < 1e-10, "y expected -10, got {}", pos.y); + } + + #[test] + fn polygon_vertex_first_of_square_points_right() { + // n=4, vertex 1: angle=TAU/4=90° → x=radius, y=0 + let pos = polygon_vertex_position(DAffine2::IDENTITY, 1, 4, 10.); + assert!((pos.x - 10.).abs() < 1e-10, "x expected 10, got {}", pos.x); + assert!(pos.y.abs() < 1e-10, "y expected ~0, got {}", pos.y); + } + + #[test] + fn polygon_vertex_halfway_around_points_down() { + // n=4, vertex 2: angle=TAU/2=180° → x=0, y=+radius + let pos = polygon_vertex_position(DAffine2::IDENTITY, 2, 4, 10.); + assert!(pos.x.abs() < 1e-10, "x expected ~0, got {}", pos.x); + assert!((pos.y - 10.).abs() < 1e-10, "y expected 10, got {}", pos.y); + } + + // ── inside_polygon ─────────────────────────────────────────────────────────── + + #[test] + fn inside_polygon_center_is_inside() { + assert!(inside_polygon(DAffine2::IDENTITY, 6, 50., DVec2::ZERO), "Center of hexagon should be inside"); + } + + #[test] + fn inside_polygon_far_point_is_outside() { + assert!(!inside_polygon(DAffine2::IDENTITY, 6, 50., DVec2::new(1000., 1000.)), "Far point should be outside"); + } + + #[test] + fn inside_polygon_point_beyond_vertex_is_outside() { + // Hexagon radius=50, topmost vertex at (0,−50); point at (0,−60) is beyond + assert!(!inside_polygon(DAffine2::IDENTITY, 6, 50., DVec2::new(0., -60.)), "Point beyond outer vertex should be outside"); + } + + #[test] + fn inside_polygon_point_near_center_is_inside() { + assert!(inside_polygon(DAffine2::IDENTITY, 6, 50., DVec2::new(10., 10.)), "Point near center should be inside hexagon"); + } + + // ── inside_star ────────────────────────────────────────────────────────────── + + #[test] + fn inside_star_center_is_inside() { + assert!(inside_star(DAffine2::IDENTITY, 5, 50., 25., DVec2::ZERO), "Center should be inside 5-point star"); + } + + #[test] + fn inside_star_far_point_is_outside() { + assert!(!inside_star(DAffine2::IDENTITY, 5, 50., 25., DVec2::new(1000., 0.)), "Far point should be outside"); + } + + #[test] + fn inside_star_point_beyond_outer_tip_is_outside() { + // Outermost tip at (0,−50); point at (0,−60) is outside + assert!(!inside_star(DAffine2::IDENTITY, 5, 50., 25., DVec2::new(0., -60.)), "Point beyond outer tip should be outside"); + } + + #[test] + fn inside_star_point_near_center_is_inside() { + assert!(inside_star(DAffine2::IDENTITY, 5, 50., 25., DVec2::new(5., 5.)), "Point near center should be inside star"); + } +} From 649c8c2025c86c7d9f758c91278e85ebc4384b4e Mon Sep 17 00:00:00 2001 From: Saloni Tarone Date: Sat, 28 Mar 2026 19:09:06 +0530 Subject: [PATCH 2/2] Add unit tests for spiral shape and arrow shape tools MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds #[cfg(test)] coverage to spiral_shape.rs and arrow_shape.rs, following the pattern established in ellipse_shape.rs and line_shape.rs. Spiral tests use raw editor message sends (bypassing handle_message's eval_graph call) because the instrumented graph evaluator does not support SpiralType as a node input. Node inputs are read directly from document.network_interface instead. Tests added: - spiral_draw_simple: outer_radius matches drag distance - spiral_archimedean_inner_radius_default: inner_radius == 0.0 - spiral_logarithmic_inner_radius_default: inner_radius == 0.1 - spiral_cancel_rmb: no layer created on RMB cancel - spiral_default_turns: turns >= 1.0 after draw - arrow_draw_simple: arrow_to matches horizontal drag - arrow_draw_diagonal: arrow_to length matches 3-4-5 drag - arrow_cancel_rmb: no layer created on RMB cancel - arrow_snap_angle_shift: angle snaps to 15° multiple with SHIFT - arrow_zero_length_no_layer: zero-length drag produces no meaningful arrow_to --- .../shapes/arrow_shape.rs | 100 ++++++++++++++ .../shapes/spiral_shape.rs | 126 ++++++++++++++++++ 2 files changed, 226 insertions(+) diff --git a/editor/src/messages/tool/common_functionality/shapes/arrow_shape.rs b/editor/src/messages/tool/common_functionality/shapes/arrow_shape.rs index df04b578ff..a91c25ce33 100644 --- a/editor/src/messages/tool/common_functionality/shapes/arrow_shape.rs +++ b/editor/src/messages/tool/common_functionality/shapes/arrow_shape.rs @@ -114,3 +114,103 @@ impl Arrow { shape_tool_data.line_data.selected_layers_with_position.extend(arrow_layers); } } + +#[cfg(test)] +mod test_arrow { + use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeType; + use crate::messages::tool::tool_messages::shape_tool::{ShapeOptionsUpdate, ShapeToolMessage}; + use crate::test_utils::test_prelude::*; + use graph_craft::document::value::TaggedValue; + + async fn select_arrow(editor: &mut EditorTestUtils) { + editor.select_tool(ToolType::Shape).await; + editor + .handle_message(ToolMessage::Shape(ShapeToolMessage::UpdateOptions { + options: ShapeOptionsUpdate::ShapeType(ShapeType::Arrow), + })) + .await; + } + + fn get_arrow_to(editor: &EditorTestUtils) -> Option { + let document = editor.active_document(); + document + .metadata() + .all_layers() + .find_map(|layer| { + let node_inputs = NodeGraphLayer::new(layer, &document.network_interface) + .find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::vector_nodes::arrow::IDENTIFIER))?; + let Some(&TaggedValue::DVec2(arrow_to)) = node_inputs[1].as_value() else { + return None; + }; + Some(arrow_to) + }) + } + + #[tokio::test] + async fn arrow_draw_simple() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + select_arrow(&mut editor).await; + editor.drag_tool(ToolType::Shape, 0., 0., 100., 0., ModifierKeys::empty()).await; + + assert_eq!(editor.active_document().metadata().all_layers().count(), 1); + + let arrow_to = get_arrow_to(&editor).expect("Expected arrow_to value"); + assert!((arrow_to.x - 100.).abs() < 1., "arrow_to.x should be ~100, got {}", arrow_to.x); + assert!(arrow_to.y.abs() < 1., "arrow_to.y should be ~0, got {}", arrow_to.y); + } + + #[tokio::test] + async fn arrow_draw_diagonal() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + select_arrow(&mut editor).await; + editor.drag_tool(ToolType::Shape, 0., 0., 60., 80., ModifierKeys::empty()).await; + + assert_eq!(editor.active_document().metadata().all_layers().count(), 1); + + let arrow_to = get_arrow_to(&editor).expect("Expected arrow_to value"); + let length = arrow_to.length(); + assert!((length - 100.).abs() < 1., "arrow length should be ~100, got {length}"); + } + + #[tokio::test] + async fn arrow_cancel_rmb() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + select_arrow(&mut editor).await; + editor.drag_tool_cancel_rmb(ToolType::Shape).await; + + assert_eq!(editor.active_document().metadata().all_layers().count(), 0, "No layer should be created on RMB cancel"); + assert!(get_arrow_to(&editor).is_none()); + } + + #[tokio::test] + async fn arrow_snap_angle_shift() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + select_arrow(&mut editor).await; + editor.drag_tool(ToolType::Shape, 0., 0., 80., 30., ModifierKeys::SHIFT).await; + + assert_eq!(editor.active_document().metadata().all_layers().count(), 1); + + let arrow_to = get_arrow_to(&editor).expect("Expected arrow_to value"); + let angle_degrees = arrow_to.angle_to(DVec2::X).to_degrees(); + let nearest_snap = (angle_degrees / 15.).round() * 15.; + assert!((angle_degrees - nearest_snap).abs() < 1., "Angle should snap to 15° multiple, got {angle_degrees}°"); + } + + #[tokio::test] + async fn arrow_zero_length_no_layer() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + select_arrow(&mut editor).await; + // Drag start == end: the 1e-6 guard in update_shape should prevent a valid arrow_to + editor.drag_tool(ToolType::Shape, 50., 50., 50., 50., ModifierKeys::empty()).await; + + // Either no layer created, or arrow_to is zero/near-zero + if let Some(arrow_to) = get_arrow_to(&editor) { + assert!(arrow_to.length() < 1e-4, "Zero-length drag should produce no meaningful arrow_to, got {arrow_to:?}"); + } + } +} diff --git a/editor/src/messages/tool/common_functionality/shapes/spiral_shape.rs b/editor/src/messages/tool/common_functionality/shapes/spiral_shape.rs index 99890cb6aa..531d7b316d 100644 --- a/editor/src/messages/tool/common_functionality/shapes/spiral_shape.rs +++ b/editor/src/messages/tool/common_functionality/shapes/spiral_shape.rs @@ -202,3 +202,129 @@ impl Spiral { }); } } + +#[cfg(test)] +mod test_spiral { + use crate::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, MouseKeys, ScrollDelta}; + use crate::messages::tool::common_functionality::shapes::shape_utility::{ShapeType, extract_spiral_parameters}; + use crate::messages::tool::tool_messages::shape_tool::{ShapeOptionsUpdate, ShapeToolMessage}; + use crate::test_utils::test_prelude::*; + use graphene_std::vector::misc::SpiralType; + + /// Draws a spiral by sending raw editor messages, bypassing `handle_message`'s + /// `eval_graph` call. The instrumented graph evaluator does not support `SpiralType` + /// as a node input, so `eval_graph` would panic if we used the normal helpers. + /// Node inputs are read directly from `document.network_interface` instead. + fn draw_spiral_raw(editor: &mut EditorTestUtils, x1: f64, y1: f64, x2: f64, y2: f64, spiral_type: SpiralType) { + editor.editor.handle_message(ToolMessage::ActivateTool { tool_type: ToolType::Shape }); + editor.editor.handle_message(ToolMessage::Shape(ShapeToolMessage::UpdateOptions { + options: ShapeOptionsUpdate::ShapeType(ShapeType::Spiral), + })); + editor.editor.handle_message(ToolMessage::Shape(ShapeToolMessage::UpdateOptions { + options: ShapeOptionsUpdate::SpiralType(spiral_type), + })); + editor.editor.handle_message(Message::InputPreprocessor(InputPreprocessorMessage::PointerMove { + editor_mouse_state: EditorMouseState { editor_position: DVec2::new(x1, y1), mouse_keys: MouseKeys::empty(), scroll_delta: ScrollDelta::default() }, + modifier_keys: ModifierKeys::empty(), + })); + editor.editor.handle_message(Message::InputPreprocessor(InputPreprocessorMessage::PointerDown { + editor_mouse_state: EditorMouseState { editor_position: DVec2::new(x1, y1), mouse_keys: MouseKeys::LEFT, scroll_delta: ScrollDelta::default() }, + modifier_keys: ModifierKeys::empty(), + })); + editor.editor.handle_message(Message::InputPreprocessor(InputPreprocessorMessage::PointerMove { + editor_mouse_state: EditorMouseState { editor_position: DVec2::new(x2, y2), mouse_keys: MouseKeys::LEFT, scroll_delta: ScrollDelta::default() }, + modifier_keys: ModifierKeys::empty(), + })); + editor.editor.handle_message(Message::InputPreprocessor(InputPreprocessorMessage::PointerUp { + editor_mouse_state: EditorMouseState { editor_position: DVec2::new(x2, y2), mouse_keys: MouseKeys::empty(), scroll_delta: ScrollDelta::default() }, + modifier_keys: ModifierKeys::empty(), + })); + } + + fn cancel_spiral_raw(editor: &mut EditorTestUtils) { + editor.editor.handle_message(ToolMessage::ActivateTool { tool_type: ToolType::Shape }); + editor.editor.handle_message(ToolMessage::Shape(ShapeToolMessage::UpdateOptions { + options: ShapeOptionsUpdate::ShapeType(ShapeType::Spiral), + })); + editor.editor.handle_message(Message::InputPreprocessor(InputPreprocessorMessage::PointerMove { + editor_mouse_state: EditorMouseState { editor_position: DVec2::new(50., 50.), mouse_keys: MouseKeys::empty(), scroll_delta: ScrollDelta::default() }, + modifier_keys: ModifierKeys::empty(), + })); + editor.editor.handle_message(Message::InputPreprocessor(InputPreprocessorMessage::PointerDown { + editor_mouse_state: EditorMouseState { editor_position: DVec2::new(50., 50.), mouse_keys: MouseKeys::LEFT, scroll_delta: ScrollDelta::default() }, + modifier_keys: ModifierKeys::empty(), + })); + editor.editor.handle_message(Message::InputPreprocessor(InputPreprocessorMessage::PointerMove { + editor_mouse_state: EditorMouseState { editor_position: DVec2::new(100., 100.), mouse_keys: MouseKeys::LEFT, scroll_delta: ScrollDelta::default() }, + modifier_keys: ModifierKeys::empty(), + })); + // RMB press while LMB held cancels the drag + editor.editor.handle_message(Message::InputPreprocessor(InputPreprocessorMessage::PointerDown { + editor_mouse_state: EditorMouseState { editor_position: DVec2::new(100., 100.), mouse_keys: MouseKeys::LEFT | MouseKeys::RIGHT, scroll_delta: ScrollDelta::default() }, + modifier_keys: ModifierKeys::default(), + })); + } + + #[tokio::test] + async fn spiral_draw_simple() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + draw_spiral_raw(&mut editor, 0., 0., 40., 0., SpiralType::Archimedean); + + assert_eq!(editor.active_document().metadata().all_layers().count(), 1); + + let document = editor.active_document(); + let layer = document.metadata().all_layers().next().expect("Expected a layer"); + let (_, _, _, outer_radius, _, _) = extract_spiral_parameters(layer, document).expect("Expected spiral parameters"); + + assert!((outer_radius - 40.).abs() < 1., "outer_radius should be ~40, got {outer_radius}"); + } + + #[tokio::test] + async fn spiral_archimedean_inner_radius_default() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + draw_spiral_raw(&mut editor, 0., 0., 50., 0., SpiralType::Archimedean); + + let document = editor.active_document(); + let layer = document.metadata().all_layers().next().expect("Expected a layer"); + let (_, _, inner_radius, _, _, _) = extract_spiral_parameters(layer, document).expect("Expected spiral parameters"); + + assert_eq!(inner_radius, 0., "Archimedean spiral inner_radius should default to 0.0"); + } + + #[tokio::test] + async fn spiral_logarithmic_inner_radius_default() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + draw_spiral_raw(&mut editor, 0., 0., 50., 0., SpiralType::Logarithmic); + + let document = editor.active_document(); + let layer = document.metadata().all_layers().next().expect("Expected a layer"); + let (_, _, inner_radius, _, _, _) = extract_spiral_parameters(layer, document).expect("Expected spiral parameters"); + + assert_eq!(inner_radius, 0.1, "Logarithmic spiral inner_radius should default to 0.1"); + } + + #[tokio::test] + async fn spiral_cancel_rmb() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + cancel_spiral_raw(&mut editor); + + assert_eq!(editor.active_document().metadata().all_layers().count(), 0, "No layer should be created on RMB cancel"); + } + + #[tokio::test] + async fn spiral_default_turns() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + draw_spiral_raw(&mut editor, 0., 0., 60., 0., SpiralType::Archimedean); + + let document = editor.active_document(); + let layer = document.metadata().all_layers().next().expect("Expected a layer"); + let (_, _, _, _, turns, _) = extract_spiral_parameters(layer, document).expect("Expected spiral parameters"); + + assert!(turns >= 1., "Turns should be at least 1, got {turns}"); + } +}