diff --git a/AGENTS.md b/AGENTS.md index d49c632..52884c7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -124,7 +124,7 @@ let tx = turtle_tx.clone(); std::thread::spawn(move || { loop { let letter = get_input(); // Blocks - let mut plan = create_turtle(); + let mut plan = create_turtle_plan(); plan.forward(50.0); tx.send(plan.build()).ok(); } diff --git a/README.md b/README.md index b4b5089..8aa6467 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A modern turtle graphics library for Rust built on [Macroquad](https://macroquad - 🎨 **Simple Builder API**: Chain commands like `forward(100).right(90)` - ⚡ **Smooth Animations**: Tweening support with easing functions and live fill preview -- 🚀 **Instant Mode**: Execute commands immediately without animation (speed ≥ 999) +- 🚀 **Instant Mode**: Execute commands immediately without animation (speed ≥ 1000) - 🎯 **High-Quality Rendering**: Complete Lyon tessellation pipeline with GPU acceleration - 🫟 **Multi-Contour Fills**: Automatic hole detection with EvenOdd fill rule - draw cheese with holes! - 📐 **Self-Intersecting Paths**: Stars, complex shapes - all handled correctly @@ -99,14 +99,14 @@ plan.begin_fill(); plan.end_fill(); // Auto-closes and applies fill // Appearance -plan.set_color(RED); +plan.set_pen_color(RED); plan.set_pen_width(5.0); plan.hide(); plan.show(); // Speed control (dynamic) -plan.set_speed(100); // Animated mode (< 999) -plan.set_speed(1000); // Instant mode (>= 999) +plan.set_speed(100); // Animated mode (< 1000) +plan.set_speed(1000); // Instant mode (>= 1000) // Turtle shapes plan.shape(ShapeType::Triangle); @@ -136,7 +136,7 @@ let mut plan = create_turtle_plan(); // Fast initial positioning (instant mode) plan.set_speed(1000); plan.pen_up(); -plan.goto(vec2(-100.0, -100.0)); +plan.go_to(vec2(-100.0, -100.0)); // Slow animated drawing plan.set_speed(50); @@ -219,7 +219,7 @@ You can also export SVG programmatically from your code: use turtle_lib::*; // Create your drawing -let mut plan = create_turtle(); +let mut plan = create_turtle_plan(); plan.forward(100).right(90).forward(100); // Create app @@ -252,8 +252,8 @@ cargo run --example square cargo run --example koch cargo run --example shapes cargo run --example yinyang -cargo run --example stern -cargo run --example nikolaus +cargo run --example star +cargo run --example house_of_nikolaus # SVG export example (requires --features svg) cargo run --example export_svg --features svg @@ -274,8 +274,8 @@ RUST_LOG=turtle_lib=debug cargo run --example logging_example - **square.rs**: Basic square drawing - **koch.rs**: Koch snowflake fractal - **shapes.rs**: Demonstrates different turtle shapes -- **stern.rs**: Star pattern drawing -- **nikolaus.rs**: Nikolaus (Santa) drawing +- **star.rs**: Star pattern drawing +- **house_of_nikolaus.rs**: House of Nikolaus (Eulerian path puzzle) #### Fill Examples @@ -297,7 +297,7 @@ RUST_LOG=turtle_lib=debug cargo run --example logging_example ### Basic Fill ```rust -let mut plan = create_turtle(); +let mut plan = create_turtle_plan(); plan.set_fill_color(RED); plan.begin_fill(); @@ -321,7 +321,7 @@ plan.circle_left(90.0, 360.0, 72); // pen_up() closes current contour plan.pen_up(); -plan.goto(vec2(0.0, -30.0)); +plan.go_to(vec2(0.0, -30.0)); // pen_down() starts new contour plan.pen_down(); diff --git a/turtle-lib/examples/breadboard.rs b/turtle-lib/examples/breadboard.rs index 0466417..aece5b8 100644 --- a/turtle-lib/examples/breadboard.rs +++ b/turtle-lib/examples/breadboard.rs @@ -1,12 +1,12 @@ use turtle_lib::*; + #[cfg(feature = "svg")] #[macroquad::main("Export SVG")] - async fn main() { // Create turtle plan let mut turtle = create_turtle_plan(); - // Set instant mode so commands execute imqmediately + // Set instant mode so commands execute immediately turtle.set_speed(1200).set_pen_width(0.5); breadboard(&mut turtle, 65); @@ -24,12 +24,12 @@ async fn main() { app.update(); app.render(); - draw_text("Drücke E für SVG-Export", 20.0, 40.0, 32.0, BLACK); + draw_text("Press E for SVG export", 20.0, 40.0, 32.0, BLACK); if is_key_pressed(KeyCode::E) { match app.export_drawing("test.svg", export::DrawingFormat::Svg) { - Ok(_) => println!("SVG exportiert nach test.svg"), - Err(e) => println!("Fehler beim Export: {:?}", e), + Ok(_) => println!("SVG exported to test.svg"), + Err(e) => eprintln!("Export error: {:?}", e), } } @@ -39,9 +39,10 @@ async fn main() { #[cfg(not(feature = "svg"))] fn main() { - println!("SVG-Export ist nicht aktiviert. Baue mit --features svg"); + println!("SVG export is not enabled. Build with --features svg"); } +#[cfg(feature = "svg")] fn pin(t: &mut TurtlePlan, size: f32) { t.left(90.0).forward(size / 2.0); for _ in 0..5 { @@ -50,52 +51,56 @@ fn pin(t: &mut TurtlePlan, size: f32) { t.right(90.0).forward(size / 2.0).left(90.0); } -fn pin_reihe(t: &mut TurtlePlan, anzahl: usize) { - for x in 0..anzahl { +#[cfg(feature = "svg")] +fn pin_row(t: &mut TurtlePlan, count: usize) { + for x in 0..count { pin(t, 5.0); - if x < anzahl - 1 { + if x < count - 1 { t.forward(5.0); } } } -fn pin_spalte(t: &mut TurtlePlan, anzahl: usize, x_coord: f32) { - for x in 0..anzahl { +#[cfg(feature = "svg")] +fn pin_column(t: &mut TurtlePlan, count: usize, x_coord: f32) { + for x in 0..count { t.pen_up().go_to(vec2(x_coord, x as f32 * 10.0)).pen_down(); - pin_reihe(t, 5); + pin_row(t, 5); } } -fn pin_seite(t: &mut TurtlePlan, anzahl: usize, x_coord: f32, color: Color) { +#[cfg(feature = "svg")] +fn pin_side(t: &mut TurtlePlan, count: usize, x_coord: f32, color: Color) { t.pen_up() .go_to(vec2(x_coord, -2.5)) .pen_down() .set_pen_color(color) .set_heading(90.0); - for x in 0..anzahl { + for x in 0..count { pin(t, 5.0); - if x < anzahl - 1 { + if x < count - 1 { t.forward(5.0); } } } -fn breadboard(t: &mut TurtlePlan, anzahl_reihen: usize) { - pin_spalte(t, anzahl_reihen, 0.0); - pin_spalte(t, anzahl_reihen, 65.0); - pin_seite(t, anzahl_reihen, -15.0, BLUE); - pin_seite(t, anzahl_reihen, -25.0, RED); - pin_seite(t, anzahl_reihen, 125.0, BLUE); - pin_seite(t, anzahl_reihen, 135.0, RED); +#[cfg(feature = "svg")] +fn breadboard(t: &mut TurtlePlan, row_count: usize) { + pin_column(t, row_count, 0.0); + pin_column(t, row_count, 65.0); + pin_side(t, row_count, -15.0, BLUE); + pin_side(t, row_count, -25.0, RED); + pin_side(t, row_count, 125.0, BLUE); + pin_side(t, row_count, 135.0, RED); // draw outline t.pen_up().go_to(vec2(-30.0, -5.0)).pen_down(); t.set_pen_color(BLACK) - .forward(anzahl_reihen as f32 * 10.0 + 10.0) + .forward(row_count as f32 * 10.0 + 10.0) .right(90.0) .forward(170.0) .right(90.0) - .forward(anzahl_reihen as f32 * 10.0 + 10.0) + .forward(row_count as f32 * 10.0 + 10.0) .right(90.0) .forward(170.0) .right(90.0); diff --git a/turtle-lib/examples/circle_test.rs b/turtle-lib/examples/circle_test.rs index 3284075..05e7efc 100644 --- a/turtle-lib/examples/circle_test.rs +++ b/turtle-lib/examples/circle_test.rs @@ -10,7 +10,7 @@ fn draw(turtle: &mut TurtlePlan) { turtle.set_pen_color(RED); turtle.set_pen_width(0.5); turtle.left(90.0); - turtle.set_speed(999); + turtle.set_speed(1000); turtle.circle_left(100.0, 540.0, 72); // partial circle to the left turtle.begin_fill(); diff --git a/turtle-lib/examples/export_svg.rs b/turtle-lib/examples/export_svg.rs index 1cf011d..f40b91d 100644 --- a/turtle-lib/examples/export_svg.rs +++ b/turtle-lib/examples/export_svg.rs @@ -1,4 +1,4 @@ -//! Beispiel: Exportiere ein SVG aus einer einfachen Zeichnung +//! Example: Export an SVG from a simple drawing #[cfg(feature = "svg")] use turtle_lib::*; @@ -52,12 +52,12 @@ async fn main() { app.update(); app.render(); - draw_text("Drücke E für SVG-Export", 20.0, 40.0, 32.0, BLACK); + draw_text("Press E for SVG export", 20.0, 40.0, 32.0, BLACK); if is_key_pressed(KeyCode::E) { match app.export_drawing("test.svg", export::DrawingFormat::Svg) { - Ok(_) => println!("SVG exportiert nach test.svg"), - Err(e) => println!("Fehler beim Export: {:?}", e), + Ok(_) => println!("SVG exported to test.svg"), + Err(e) => eprintln!("Export error: {:?}", e), } } @@ -67,5 +67,5 @@ async fn main() { #[cfg(not(feature = "svg"))] fn main() { - println!("SVG-Export ist nicht aktiviert. Baue mit --features svg"); + println!("SVG export is not enabled. Build with --features svg"); } diff --git a/turtle-lib/examples/house_of_nikolaus.rs b/turtle-lib/examples/house_of_nikolaus.rs new file mode 100644 index 0000000..f711af1 --- /dev/null +++ b/turtle-lib/examples/house_of_nikolaus.rs @@ -0,0 +1,57 @@ +//! House of Nikolaus example - draws the classic house figure (Eulerian path puzzle) + +use turtle_lib::*; + +fn house_square(turtle: &mut TurtlePlan, size: f32) { + turtle.forward(size); + turtle.left(90.0); + turtle.forward(size); + turtle.left(90.0); + turtle.forward(size); + turtle.left(90.0); + turtle.forward(size); + turtle.left(90.0); +} + +fn house_diagonal(turtle: &mut TurtlePlan, size: f32) { + let square = size * size; + let diag = (square + square).sqrt(); + + turtle.left(45.0); + turtle.forward(diag); + turtle.left(45.0); + house_roof(turtle, size); + turtle.left(45.0); + turtle.forward(diag); + turtle.left(45.0); +} + +fn house_roof(turtle: &mut TurtlePlan, size: f32) { + let square = size * size; + let diag = (square + square).sqrt(); + turtle.left(45.0); + turtle.forward(diag / 2.0); + turtle.left(90.0); + turtle.forward(diag / 2.0); + turtle.left(45.0); +} + +fn house_of_nikolaus(turtle: &mut TurtlePlan, size: f32) { + house_square(turtle, size); + house_diagonal(turtle, size); +} + +#[turtle_main("House of Nikolaus")] +fn draw(turtle: &mut TurtlePlan) { + turtle.shape(ShapeType::Turtle); + + // Position the turtle (pen up, move, pen down) + turtle.pen_up(); + turtle.backward(80.0); + turtle.left(90.0); + turtle.backward(50.0); + turtle.right(90.0); + turtle.pen_down(); + + house_of_nikolaus(turtle, 100.0); +} diff --git a/turtle-lib/examples/nikolaus.rs b/turtle-lib/examples/nikolaus.rs deleted file mode 100644 index 74db8be..0000000 --- a/turtle-lib/examples/nikolaus.rs +++ /dev/null @@ -1,57 +0,0 @@ -//! Nikolaus example - draws a house-like figure - -use turtle_lib::*; - -fn nikolausquadrat(turtle: &mut TurtlePlan, groesse: f32) { - turtle.forward(groesse); - turtle.left(90.0); - turtle.forward(groesse); - turtle.left(90.0); - turtle.forward(groesse); - turtle.left(90.0); - turtle.forward(groesse); - turtle.left(90.0); -} - -fn nikolausdiag(turtle: &mut TurtlePlan, groesse: f32) { - let quadrat = groesse * groesse; - let diag = (quadrat + quadrat).sqrt(); - - turtle.left(45.0); - turtle.forward(diag); - turtle.left(45.0); - nikolausdach2(turtle, groesse); - turtle.left(45.0); - turtle.forward(diag); - turtle.left(45.0); -} - -fn nikolausdach2(turtle: &mut TurtlePlan, groesse: f32) { - let quadrat = groesse * groesse; - let diag = (quadrat + quadrat).sqrt(); - turtle.left(45.0); - turtle.forward(diag / 2.0); - turtle.left(90.0); - turtle.forward(diag / 2.0); - turtle.left(45.0); -} - -fn nikolaus(turtle: &mut TurtlePlan, groesse: f32) { - nikolausquadrat(turtle, groesse); - nikolausdiag(turtle, groesse); -} - -#[turtle_main("Nikolaus")] -fn draw(turtle: &mut TurtlePlan) { - turtle.shape(ShapeType::Turtle); - - // Position the turtle (pen up, move, pen down) - turtle.pen_up(); - turtle.backward(80.0); - turtle.left(90.0); - turtle.backward(50.0); - turtle.right(90.0); - turtle.pen_down(); - - nikolaus(turtle, 100.0); -} diff --git a/turtle-lib/examples/stern.rs b/turtle-lib/examples/star.rs similarity index 100% rename from turtle-lib/examples/stern.rs rename to turtle-lib/examples/star.rs diff --git a/turtle-lib/src/builders.rs b/turtle-lib/src/builders.rs index 1cc5ec8..b7fcab1 100644 --- a/turtle-lib/src/builders.rs +++ b/turtle-lib/src/builders.rs @@ -1,7 +1,7 @@ //! Builder pattern traits for creating turtle command sequences use crate::commands::{CommandQueue, TurtleCommand}; -use crate::general::{AnimationSpeed, Color, Coordinate, Degrees, FontSize, Precision}; +use crate::general::{AnimationSpeed, Color, Coordinate, Degrees, FontSize, Length, Precision}; use crate::shapes::{ShapeType, TurtleShape}; /// Trait for adding commands to a queue @@ -33,9 +33,9 @@ pub trait DirectionalMovement: WithCommands { /// ``` fn forward(&mut self, distance: T) -> &mut Self where - T: Into, + T: Into, { - let dist: Precision = distance.into(); + let dist: Length = distance.into(); self.get_commands_mut().push(TurtleCommand::Move(dist)); self } @@ -61,9 +61,9 @@ pub trait DirectionalMovement: WithCommands { /// ``` fn backward(&mut self, distance: T) -> &mut Self where - T: Into, + T: Into, { - let dist: Precision = distance.into(); + let dist: Length = distance.into(); self.get_commands_mut().push(TurtleCommand::Move(-dist)); self } @@ -159,10 +159,10 @@ pub trait CurvedMovement: WithCommands { /// ``` fn circle_left(&mut self, radius: R, angle: A, steps: usize) -> &mut Self where - R: Into, + R: Into, A: Into, { - let r: Precision = radius.into(); + let r: Length = radius.into(); self.get_commands_mut().push(TurtleCommand::Circle { radius: r, angle: angle.into(), @@ -205,10 +205,10 @@ pub trait CurvedMovement: WithCommands { /// ``` fn circle_right(&mut self, radius: R, angle: A, steps: usize) -> &mut Self where - R: Into, + R: Into, A: Into, { - let r: Precision = radius.into(); + let r: Length = radius.into(); self.get_commands_mut().push(TurtleCommand::Circle { radius: r, angle: angle.into(), @@ -370,10 +370,7 @@ impl TurtlePlan { /// } /// ``` pub fn set_heading>(&mut self, heading: T) -> &mut Self { - // Convert user-facing turtle heading (degrees, Y-up mental model) - // to internal radians used by the render-space pipeline. - self.queue - .push(TurtleCommand::SetHeading(-heading.into().as_radians())); + self.queue.push(TurtleCommand::SetHeading(heading.into())); self } diff --git a/turtle-lib/src/command_behavior.rs b/turtle-lib/src/command_behavior.rs index c27146b..ece90f4 100644 --- a/turtle-lib/src/command_behavior.rs +++ b/turtle-lib/src/command_behavior.rs @@ -31,8 +31,8 @@ impl TurtleCommand { pub(crate) fn apply_to_params(&self, params: &mut TurtleParams) { match self { TurtleCommand::Move(dist) => { - let dx = dist * params.heading.cos(); - let dy = dist * params.heading.sin(); + let dx = dist.value() * params.heading.cos(); + let dy = dist.value() * params.heading.sin(); params.position = vec2(params.position.x + dx, params.position.y + dy); } TurtleCommand::Turn(angle) => { @@ -47,7 +47,7 @@ impl TurtleCommand { let geom = CircleGeometry::new( params.position, Radians::new(params.heading), - *radius, + radius.value(), *direction, ); let angle_rad = angle.as_radians().value(); @@ -62,7 +62,7 @@ impl TurtleCommand { params.position = vec2(coord.x, -coord.y); } TurtleCommand::SetHeading(heading) => { - params.heading = normalize_angle(heading.value()); + params.heading = normalize_angle(-heading.as_radians().value()); } TurtleCommand::SetColor(color) => { params.color = *color; @@ -119,15 +119,16 @@ impl TurtleCommand { } let base: f32 = match self { - TurtleCommand::Move(dist) => dist.abs() / spd, + TurtleCommand::Move(dist) => dist.value().abs() / spd, TurtleCommand::Turn(angle) => angle.value().abs() / (spd * 1.8), TurtleCommand::Circle { radius, angle, .. } => { - let arc_length = radius * angle.as_radians().value().abs(); + let arc_length = radius.value() * angle.as_radians().value().abs(); arc_length / spd } TurtleCommand::Goto(target) => { - let dx = target.x - params.position.x; - let dy = target.y - params.position.y; + let screen_target = vec2(target.x, -target.y); + let dx = screen_target.x - params.position.x; + let dy = screen_target.y - params.position.y; (dx * dx + dy * dy).sqrt() / spd } _ => 0.0, @@ -148,3 +149,77 @@ impl TurtleCommand { ) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::general::{AnimationSpeed, Color, Coordinate, Degrees}; + use crate::shapes::TurtleShape; + + fn make_test_params() -> TurtleParams { + TurtleParams { + position: vec2(0.0, 0.0), + heading: 0.0, + pen_down: true, + pen_width: 1.0, + color: Color::new(0.0, 0.0, 0.0, 1.0), + fill_color: None, + visible: true, + shape: TurtleShape::turtle(), + speed: AnimationSpeed::Animated(100.0), + } + } + + #[test] + fn test_goto_duration_cartesian_inversion() { + let mut params = make_test_params(); + // Set screen position to (0, 100), which corresponds to Cartesian (0, -100) + params.position = vec2(0.0, 100.0); + + // Move to Cartesian (0, 100), which in screen space is (0, -100) + // Distance should be 200 pixels! + let cmd = TurtleCommand::Goto(Coordinate::new(0.0, 100.0)); + let duration = cmd.animation_duration(¶ms, AnimationSpeed::Animated(100.0)); + + // At 100 px/sec across 200 pixels, duration should be 2.0 seconds + assert!( + (duration - 2.0).abs() < 0.01, + "Expected duration ~2.0s for 200px move, got {duration}" + ); + } + + #[test] + fn test_set_heading_degrees_and_instant_duration() { + let mut params = make_test_params(); + + // 90° = North (in screen coordinates: -pi/2) + let cmd_north = TurtleCommand::SetHeading(Degrees::new(90.0)); + cmd_north.apply_to_params(&mut params); + let expected_north = -std::f32::consts::FRAC_PI_2; + assert!( + (params.heading - expected_north).abs() < 0.001, + "Heading 90° should be North (-π/2), got {}", + params.heading + ); + + // SetHeading should be instant (0.01 minimum duration) + let duration = cmd_north.animation_duration(¶ms, AnimationSpeed::Animated(100.0)); + assert!((duration - 0.01).abs() < 0.001); + + // 0° = East (0 radians) + let cmd_east = TurtleCommand::SetHeading(Degrees::new(0.0)); + cmd_east.apply_to_params(&mut params); + assert!((params.heading - 0.0).abs() < 0.001); + + // 270° = South (+pi/2 radians in screen coords) + let cmd_south = TurtleCommand::SetHeading(Degrees::new(270.0)); + cmd_south.apply_to_params(&mut params); + let expected_south = std::f32::consts::FRAC_PI_2; + assert!( + (params.heading - expected_south).abs() < 0.001, + "Heading 270° should be South (π/2), got {}", + params.heading + ); + } +} + diff --git a/turtle-lib/src/commands.rs b/turtle-lib/src/commands.rs index 1f91773..a5559b1 100644 --- a/turtle-lib/src/commands.rs +++ b/turtle-lib/src/commands.rs @@ -1,13 +1,13 @@ //! Turtle commands and command queue -use crate::general::{AnimationSpeed, Color, Coordinate, Degrees, FontSize, Precision, Radians}; +use crate::general::{AnimationSpeed, Color, Coordinate, Degrees, FontSize, Length, Precision}; use crate::shapes::TurtleShape; /// Individual turtle commands #[derive(Clone, Debug)] pub enum TurtleCommand { // Movement (positive = forward, negative = backward) - Move(Precision), + Move(Length), // Rotation (positive = right/clockwise, negative = left/counter-clockwise) // Stored in degrees — the natural unit at the user-facing API boundary. @@ -15,7 +15,7 @@ pub enum TurtleCommand { // Circle drawing Circle { - radius: Precision, + radius: Length, angle: Degrees, // sweep angle — degrees, as supplied by the user steps: usize, direction: crate::circle_geometry::CircleDirection, @@ -34,10 +34,9 @@ pub enum TurtleCommand { // Position Goto(Coordinate), - /// Heading stored as internal radians (Y-down render-space convention). - /// Values passed via `TurtlePlan::set_heading` are converted from - /// user-facing degrees before this command is enqueued. - SetHeading(Radians), + /// Heading stored in user degrees (Cartesian convention: 0° = East, 90° = North). + /// Conversion to internal screen-space heading is performed when executed. + SetHeading(Degrees), // Visibility ShowTurtle, diff --git a/turtle-lib/src/commands_channel.rs b/turtle-lib/src/commands_channel.rs index 7fd1128..627aa60 100644 --- a/turtle-lib/src/commands_channel.rs +++ b/turtle-lib/src/commands_channel.rs @@ -203,13 +203,10 @@ impl TurtleCommandReceiver { /// Panics if `buffer_size` is 0. /// /// # Examples -/// ```no_run -/// # use turtle_lib::*; -/// # fn example() { +/// ```ignore /// let (tx, _rx) = turtle_command_channel(0, 100); /// // Sender goes to game threads /// // Receiver stays in render thread (or `TurtleApp`) -/// # } /// ``` #[must_use] pub(crate) fn turtle_command_channel( diff --git a/turtle-lib/src/drawing.rs b/turtle-lib/src/drawing.rs index 1d2971d..9f02241 100644 --- a/turtle-lib/src/drawing.rs +++ b/turtle-lib/src/drawing.rs @@ -62,7 +62,7 @@ pub(crate) fn render_world_with_tweens(world: &TurtleWorld, zoom_level: f32) { direction, } => { // Draw arc segments from start to current position - draw_tween_arc(tween, *radius, *angle, *steps, *direction); + draw_tween_arc(tween, radius.value(), *angle, *steps, *direction); } _ if should_draw_tween_line(&tween.command) => { // Draw straight line for other movement commands (use tween's current position) @@ -133,7 +133,7 @@ pub(crate) fn render_world_with_tweens(world: &TurtleWorld, zoom_level: f32) { let geom = CircleGeometry::new( tween.start_params.position, Radians::new(tween.start_params.heading), - *radius, + radius.value(), *direction, ); let elapsed = get_time() - tween.start_time; @@ -147,7 +147,7 @@ pub(crate) fn render_world_with_tweens(world: &TurtleWorld, zoom_level: f32) { let sweep_so_far = angle.as_radians().value() * eased_progress; for pt in arc_points( geom.center, - *radius, + radius.value(), geom.start_angle_from_center, sweep_so_far, samples_to_draw, diff --git a/turtle-lib/src/execution.rs b/turtle-lib/src/execution.rs index 7e0ffd3..503d5f7 100644 --- a/turtle-lib/src/execution.rs +++ b/turtle-lib/src/execution.rs @@ -242,7 +242,7 @@ pub(crate) fn record_fill_vertices_after_movement( let geom = CircleGeometry::new( start_state.position, Radians::new(start_state.heading), - *radius, + radius.value(), *direction, ); if let Some(ref mut fill_state) = filling { @@ -252,7 +252,7 @@ pub(crate) fn record_fill_vertices_after_movement( turtle_id, center_x = geom.center.x, center_y = geom.center.y, - radius, + radius = radius.value(), steps, num_samples, "Recording arc vertices" @@ -268,8 +268,8 @@ pub(crate) fn record_fill_vertices_after_movement( } }; let vertex = Coordinate::new( - geom.center.x + radius * current_angle.cos(), - geom.center.y + radius * current_angle.sin(), + geom.center.x + radius.value() * current_angle.cos(), + geom.center.y + radius.value() * current_angle.sin(), ); tracing::trace!( turtle_id, @@ -347,12 +347,12 @@ pub(crate) fn tessellate_command( let geom = CircleGeometry::new( start.position, Radians::new(start.heading), - *radius, + radius.value(), *direction, ); let mesh = tessellation::tessellate_arc( geom.center, - *radius, + radius.value(), geom.start_angle_from_center.to_degrees(), angle.value(), start.color, @@ -402,7 +402,7 @@ pub(crate) fn push_svg_for_draw( svg_log.push(SvgRecord::Arc { start_position: start.position, start_heading: start.heading, - radius: *radius, + radius: radius.value(), angle: *angle, direction: *direction, color: start.color, @@ -474,7 +474,7 @@ pub(crate) fn execute_command_with_id( mod tests { use super::*; use crate::commands::TurtleCommand; - use crate::general::Degrees; + use crate::general::{Degrees, Length}; use crate::shapes::TurtleShape; use crate::tweening::TweenController; @@ -509,7 +509,7 @@ mod tests { assert_eq!(state.params.heading, 0.0); // Forward 100 - should move to (100, 0) - execute_command(&TurtleCommand::Move(100.0), &mut state); + execute_command(&TurtleCommand::Move(Length::new(100.0)), &mut state); assert!( (state.params.position.x - 100.0).abs() < 0.01, "After forward(100): x = {}", @@ -544,7 +544,7 @@ mod tests { ); // Forward 50 - should move north (negative Y) to (100, -50) - execute_command(&TurtleCommand::Move(50.0), &mut state); + execute_command(&TurtleCommand::Move(Length::new(50.0)), &mut state); assert!( (state.params.position.x - 100.0).abs() < 0.01, "Final position: x = {} (expected 100.0)", diff --git a/turtle-lib/src/export.rs b/turtle-lib/src/export.rs index 2d22678..e1624ba 100644 --- a/turtle-lib/src/export.rs +++ b/turtle-lib/src/export.rs @@ -59,7 +59,7 @@ where let mut app = crate::TurtleApp::new().with_commands(turtle.build()); app.set_all_turtles_speed(crate::AnimationSpeed::Instant(1000)); - while !app.all_animations_complete() { + while !app.is_complete() { app.update(); } diff --git a/turtle-lib/src/general/length.rs b/turtle-lib/src/general/length.rs index 4603050..767ac4c 100644 --- a/turtle-lib/src/general/length.rs +++ b/turtle-lib/src/general/length.rs @@ -1,13 +1,33 @@ //! Length type for distance measurements use super::Precision; +use std::ops::Neg; -#[derive(Default, Copy, Clone, Debug, PartialEq)] +/// A spatial distance or length measurement. +/// +/// Used at the public API boundary for movement distances and arc radii. +#[derive(Default, Copy, Clone, Debug, PartialEq, PartialOrd)] pub struct Length(pub Precision); -impl From for Length { - fn from(i: i16) -> Self { - Self(Precision::from(i)) +impl Length { + /// Create a new `Length` from a raw value. + #[must_use] + pub const fn new(v: Precision) -> Self { + Self(v) + } + + /// Extract the raw `Precision` (`f32`) value. + #[must_use] + pub const fn value(self) -> Precision { + self.0 + } +} + +impl Neg for Length { + type Output = Self; + + fn neg(self) -> Self { + Self(-self.0) } } @@ -17,8 +37,56 @@ impl From for Length { } } +impl From for Length { + fn from(f: f64) -> Self { + Self(f as Precision) + } +} + +impl From for Length { + fn from(i: i16) -> Self { + Self(Precision::from(i)) + } +} + impl From for Length { fn from(i: i32) -> Self { Self(i as Precision) } } + +impl From for Length { + fn from(u: usize) -> Self { + Self(u as Precision) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_length_conversions_and_negation() { + let l_f32: Length = 42.5_f32.into(); + assert_eq!(l_f32.value(), 42.5); + + let l_f64: Length = 100.0_f64.into(); + assert_eq!(l_f64.value(), 100.0); + + let l_i32: Length = 50_i32.into(); + assert_eq!(l_i32.value(), 50.0); + + let l_i16: Length = 25_i16.into(); + assert_eq!(l_i16.value(), 25.0); + + let l_usize: Length = 10_usize.into(); + assert_eq!(l_usize.value(), 10.0); + + let neg = -l_f32; + assert_eq!(neg.value(), -42.5); + + assert!(Length::new(10.0) < Length::new(20.0)); + } +} + + diff --git a/turtle-lib/src/lib.rs b/turtle-lib/src/lib.rs index a52a04e..d4c8694 100644 --- a/turtle-lib/src/lib.rs +++ b/turtle-lib/src/lib.rs @@ -106,6 +106,7 @@ impl TurtleApp { filename: &str, format: export::DrawingFormat, ) -> Result<(), export::ExportError> { + let _ = filename; match format { #[cfg(feature = "svg")] export::DrawingFormat::Svg => { @@ -371,12 +372,6 @@ impl TurtleApp { .all(|turtle| turtle.tween_controller.is_complete()) } - /// Check if all animations are complete (alias for is_complete) - #[must_use] - pub fn all_animations_complete(&self) -> bool { - self.is_complete() - } - /// Set the animation speed for all turtles /// /// # Arguments diff --git a/turtle-lib/src/tweening.rs b/turtle-lib/src/tweening.rs index f93176f..a4b2a70 100644 --- a/turtle-lib/src/tweening.rs +++ b/turtle-lib/src/tweening.rs @@ -180,7 +180,7 @@ impl TweenController { calculate_circle_position( tween.start_params.position, Radians::new(tween.start_params.heading), - *radius, + radius.value(), angle_traveled, *direction, ) @@ -413,7 +413,7 @@ fn current_time() -> f64 { mod tests { use super::*; use crate::commands::TurtleCommand; - use crate::general::Degrees; + use crate::general::{Degrees, Length}; use crate::state::TurtleParams; fn make_test_params() -> TurtleParams { @@ -433,10 +433,10 @@ mod tests { #[test] fn test_instant_mode_drains_queue() { let mut queue = CommandQueue::new(); - queue.push(TurtleCommand::Move(100.0)); + queue.push(TurtleCommand::Move(Length::new(100.0))); queue.push(TurtleCommand::Turn(Degrees::new(90.0))); queue.push(TurtleCommand::PenUp); - queue.push(TurtleCommand::Move(50.0)); + queue.push(TurtleCommand::Move(Length::new(50.0))); let mut controller = TweenController::new(queue, AnimationSpeed::Instant(100)); assert_eq!(controller.queue.len(), 4); @@ -467,7 +467,7 @@ mod tests { let mut batch = CommandQueue::new(); batch.push(TurtleCommand::Reset); batch.push(TurtleCommand::PenDown); - batch.push(TurtleCommand::Move(10.0)); + batch.push(TurtleCommand::Move(Length::new(10.0))); batch.push(TurtleCommand::Turn(Degrees::new(30.0))); controller.append_commands(batch); @@ -485,11 +485,11 @@ mod tests { fn test_instant_mode_respects_batch_limit_and_retains_pending() { let mut queue = CommandQueue::new(); // 5 drawing commands - queue.push(TurtleCommand::Move(10.0)); - queue.push(TurtleCommand::Move(20.0)); - queue.push(TurtleCommand::Move(30.0)); - queue.push(TurtleCommand::Move(40.0)); - queue.push(TurtleCommand::Move(50.0)); + queue.push(TurtleCommand::Move(Length::new(10.0))); + queue.push(TurtleCommand::Move(Length::new(20.0))); + queue.push(TurtleCommand::Move(Length::new(30.0))); + queue.push(TurtleCommand::Move(Length::new(40.0))); + queue.push(TurtleCommand::Move(Length::new(50.0))); // Limit to 2 draw calls per frame let mut controller = TweenController::new(queue, AnimationSpeed::Instant(2)); @@ -520,8 +520,8 @@ mod tests { #[test] fn test_animated_mode_pops_to_current_tween() { let mut queue = CommandQueue::new(); - queue.push(TurtleCommand::Move(100.0)); - queue.push(TurtleCommand::Move(50.0)); + queue.push(TurtleCommand::Move(Length::new(100.0))); + queue.push(TurtleCommand::Move(Length::new(50.0))); let mut controller = TweenController::new( queue,