From 6b558ca8a05796558c0c74be57c7aeaa50c2ffb5 Mon Sep 17 00:00:00 2001 From: Franz Dietrich Date: Thu, 21 May 2026 15:02:57 +0200 Subject: [PATCH] more consistent use of angle types --- turtle-lib/src/builders.rs | 38 ++-- turtle-lib/src/circle_geometry.rs | 18 +- turtle-lib/src/command_behavior.rs | 25 ++- turtle-lib/src/commands.rs | 12 +- turtle-lib/src/drawing.rs | 14 +- turtle-lib/src/execution.rs | 20 +- turtle-lib/src/general.rs | 2 +- turtle-lib/src/general/angle.rs | 305 +++++++++++++---------------- turtle-lib/src/lib.rs | 2 +- turtle-lib/src/state.rs | 6 +- turtle-lib/src/tweening.rs | 23 ++- 11 files changed, 222 insertions(+), 243 deletions(-) diff --git a/turtle-lib/src/builders.rs b/turtle-lib/src/builders.rs index f92efcd..3088c78 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, FontSize, Precision}; +use crate::general::{AnimationSpeed, Color, Coordinate, Degrees, FontSize, Precision}; use crate::shapes::{ShapeType, TurtleShape}; /// Trait for adding commands to a queue @@ -91,10 +91,10 @@ pub trait Turnable: WithCommands { /// ``` fn left(&mut self, angle: T) -> &mut Self where - T: Into, + T: Into, { - let degrees: Precision = angle.into(); - self.get_commands_mut().push(TurtleCommand::Turn(-degrees)); + self.get_commands_mut() + .push(TurtleCommand::Turn(-angle.into())); self } @@ -118,10 +118,10 @@ pub trait Turnable: WithCommands { /// ``` fn right(&mut self, angle: T) -> &mut Self where - T: Into, + T: Into, { - let degrees: Precision = angle.into(); - self.get_commands_mut().push(TurtleCommand::Turn(degrees)); + self.get_commands_mut() + .push(TurtleCommand::Turn(angle.into())); self } } @@ -160,13 +160,12 @@ pub trait CurvedMovement: WithCommands { fn circle_left(&mut self, radius: R, angle: A, steps: usize) -> &mut Self where R: Into, - A: Into, + A: Into, { let r: Precision = radius.into(); - let a: Precision = angle.into(); self.get_commands_mut().push(TurtleCommand::Circle { radius: r, - angle: a, + angle: angle.into(), steps, direction: crate::circle_geometry::CircleDirection::Left, }); @@ -207,13 +206,12 @@ pub trait CurvedMovement: WithCommands { fn circle_right(&mut self, radius: R, angle: A, steps: usize) -> &mut Self where R: Into, - A: Into, + A: Into, { let r: Precision = radius.into(); - let a: Precision = angle.into(); self.get_commands_mut().push(TurtleCommand::Circle { radius: r, - angle: a, + angle: angle.into(), steps, direction: crate::circle_geometry::CircleDirection::Right, }); @@ -242,14 +240,14 @@ impl TurtlePlan { /// async fn main() { /// let mut turtle = TurtlePlan::new(); /// turtle.forward(100.0).right(90.0).forward(100.0); - /// + /// /// let mut app = TurtleApp::new().with_commands(turtle.build()); - /// + /// /// loop { /// clear_background(WHITE); /// app.update(); /// app.render(); - /// + /// /// if is_key_pressed(KeyCode::Escape) || is_key_pressed(KeyCode::Q) { /// break; /// } @@ -367,9 +365,9 @@ impl TurtlePlan { /// .forward(100.0); /// } /// ``` - pub fn set_heading(&mut self, heading: Precision) -> &mut Self { + pub fn set_heading>(&mut self, heading: T) -> &mut Self { self.queue - .push(TurtleCommand::SetHeading(-heading.to_radians())); + .push(TurtleCommand::SetHeading(-heading.into().as_radians())); self } @@ -699,10 +697,10 @@ impl TurtlePlan { /// fn draw(turtle: &mut TurtlePlan) { /// // Draw something /// turtle.forward(100.0); - /// + /// /// // Reset everything back to default /// turtle.reset(); - /// + /// /// // Start fresh /// turtle.forward(50.0); /// } diff --git a/turtle-lib/src/circle_geometry.rs b/turtle-lib/src/circle_geometry.rs index 0882710..29f700b 100644 --- a/turtle-lib/src/circle_geometry.rs +++ b/turtle-lib/src/circle_geometry.rs @@ -1,5 +1,6 @@ //! Circle geometry calculations - single source of truth for `circle_left` and `circle_right` +use crate::general::Radians; use macroquad::prelude::*; /// Direction of circular motion (in screen coordinates with Y-down) @@ -22,12 +23,15 @@ impl CircleGeometry { #[must_use] pub fn new( turtle_pos: Vec2, - turtle_heading: f32, + turtle_heading: Radians, radius: f32, direction: CircleDirection, ) -> Self { use std::f32::consts::FRAC_PI_2; + // Extract raw f32 once — all arithmetic below is in radians + let heading = turtle_heading.value(); + // Calculate center based on direction // In screen coordinates (Y-down): // - Left turn (counter-clockwise visually): center is perpendicular-left from turtle's perspective @@ -35,8 +39,8 @@ impl CircleGeometry { // - Right turn (clockwise visually): center is perpendicular-right from turtle's perspective // which is heading + π/2 (rotated counter-clockwise from heading vector) let center_offset_angle = match direction { - CircleDirection::Left => turtle_heading - FRAC_PI_2, - CircleDirection::Right => turtle_heading + FRAC_PI_2, + CircleDirection::Left => heading - FRAC_PI_2, + CircleDirection::Right => heading + FRAC_PI_2, }; let center = vec2( @@ -46,8 +50,8 @@ impl CircleGeometry { // Angle from center back to turtle position let start_angle_from_center = match direction { - CircleDirection::Left => turtle_heading + FRAC_PI_2, - CircleDirection::Right => turtle_heading - FRAC_PI_2, + CircleDirection::Left => heading + FRAC_PI_2, + CircleDirection::Right => heading - FRAC_PI_2, }; Self { @@ -151,7 +155,7 @@ mod tests { fn test_circle_left_geometry() { let geom = CircleGeometry::new( vec2(0.0, 0.0), - 0.0, // heading east (0 radians) + Radians::new(0.0), // heading east (0 radians) 100.0, CircleDirection::Left, ); @@ -183,7 +187,7 @@ mod tests { fn test_circle_right_geometry() { let geom = CircleGeometry::new( vec2(0.0, 0.0), - 0.0, // heading east + Radians::new(0.0), // heading east 100.0, CircleDirection::Right, ); diff --git a/turtle-lib/src/command_behavior.rs b/turtle-lib/src/command_behavior.rs index 89c0a37..c27146b 100644 --- a/turtle-lib/src/command_behavior.rs +++ b/turtle-lib/src/command_behavior.rs @@ -9,7 +9,7 @@ use crate::circle_geometry::{CircleDirection, CircleGeometry}; use crate::commands::TurtleCommand; -use crate::general::AnimationSpeed; +use crate::general::{AnimationSpeed, Radians}; use crate::state::TurtleParams; use crate::tweening::normalize_angle; use macroquad::prelude::vec2; @@ -36,7 +36,7 @@ impl TurtleCommand { params.position = vec2(params.position.x + dx, params.position.y + dy); } TurtleCommand::Turn(angle) => { - params.heading = normalize_angle(params.heading + angle.to_radians()); + params.heading = normalize_angle(params.heading + angle.as_radians().value()); } TurtleCommand::Circle { radius, @@ -44,12 +44,17 @@ impl TurtleCommand { direction, .. } => { - let geom = - CircleGeometry::new(params.position, params.heading, *radius, *direction); - params.position = geom.position_at_angle(angle.to_radians()); + let geom = CircleGeometry::new( + params.position, + Radians::new(params.heading), + *radius, + *direction, + ); + let angle_rad = angle.as_radians().value(); + params.position = geom.position_at_angle(angle_rad); params.heading = normalize_angle(match direction { - CircleDirection::Left => params.heading - angle.to_radians(), - CircleDirection::Right => params.heading + angle.to_radians(), + CircleDirection::Left => params.heading - angle_rad, + CircleDirection::Right => params.heading + angle_rad, }); } TurtleCommand::Goto(coord) => { @@ -57,7 +62,7 @@ impl TurtleCommand { params.position = vec2(coord.x, -coord.y); } TurtleCommand::SetHeading(heading) => { - params.heading = normalize_angle(*heading); + params.heading = normalize_angle(heading.value()); } TurtleCommand::SetColor(color) => { params.color = *color; @@ -115,9 +120,9 @@ impl TurtleCommand { let base: f32 = match self { TurtleCommand::Move(dist) => dist.abs() / spd, - TurtleCommand::Turn(angle) => angle.abs() / (spd * 1.8), + TurtleCommand::Turn(angle) => angle.value().abs() / (spd * 1.8), TurtleCommand::Circle { radius, angle, .. } => { - let arc_length = radius * angle.to_radians().abs(); + let arc_length = radius * angle.as_radians().value().abs(); arc_length / spd } TurtleCommand::Goto(target) => { diff --git a/turtle-lib/src/commands.rs b/turtle-lib/src/commands.rs index 6750079..10468bb 100644 --- a/turtle-lib/src/commands.rs +++ b/turtle-lib/src/commands.rs @@ -1,6 +1,6 @@ //! Turtle commands and command queue -use crate::general::{AnimationSpeed, Color, Coordinate, FontSize, Precision}; +use crate::general::{AnimationSpeed, Color, Coordinate, Degrees, FontSize, Precision, Radians}; use crate::shapes::TurtleShape; /// Individual turtle commands @@ -9,13 +9,14 @@ pub enum TurtleCommand { // Movement (positive = forward, negative = backward) Move(Precision), - // Rotation (positive = right/clockwise, negative = left/counter-clockwise in degrees) - Turn(Precision), + // Rotation (positive = right/clockwise, negative = left/counter-clockwise) + // Stored in degrees — the natural unit at the user-facing API boundary. + Turn(Degrees), // Circle drawing Circle { radius: Precision, - angle: Precision, // degrees + angle: Degrees, // sweep angle — degrees, as supplied by the user steps: usize, direction: crate::circle_geometry::CircleDirection, }, @@ -33,7 +34,8 @@ pub enum TurtleCommand { // Position Goto(Coordinate), - SetHeading(Precision), // radians + /// Heading stored as radians — already converted by the builder. + SetHeading(Radians), // Visibility ShowTurtle, diff --git a/turtle-lib/src/drawing.rs b/turtle-lib/src/drawing.rs index 2841014..1ec87c4 100644 --- a/turtle-lib/src/drawing.rs +++ b/turtle-lib/src/drawing.rs @@ -177,9 +177,10 @@ pub(crate) fn render_world_with_tweens(world: &TurtleWorld, zoom_level: f32) { { // Calculate partial arc vertices based on current progress use crate::circle_geometry::CircleGeometry; + use crate::general::Radians; let geom = CircleGeometry::new( tween.start_params.position, - tween.start_params.heading, + Radians::new(tween.start_params.heading), *radius, *direction, ); // Calculate progress @@ -197,11 +198,11 @@ pub(crate) fn render_world_with_tweens(world: &TurtleWorld, zoom_level: f32) { let current_angle = match direction { crate::circle_geometry::CircleDirection::Left => { geom.start_angle_from_center - - angle.to_radians() * sample_progress + - angle.as_radians().value() * sample_progress } crate::circle_geometry::CircleDirection::Right => { geom.start_angle_from_center - + angle.to_radians() * sample_progress + + angle.as_radians().value() * sample_progress } }; @@ -347,13 +348,14 @@ fn draw_text_command( fn draw_tween_arc( tween: &crate::tweening::CommandTween, radius: f32, - total_angle: f32, + total_angle: crate::general::Degrees, steps: usize, direction: CircleDirection, ) { + use crate::general::Radians; let geom = CircleGeometry::new( tween.start_params.position, - tween.start_params.heading, + Radians::new(tween.start_params.heading), radius, direction, ); @@ -375,7 +377,7 @@ fn draw_tween_arc( geom.center, radius, geom.start_angle_from_center.to_degrees(), - total_angle * progress, + total_angle.value() * progress, tween.start_params.color, tween.start_params.pen_width, ((steps as f32 * progress).ceil() as usize).max(1), diff --git a/turtle-lib/src/execution.rs b/turtle-lib/src/execution.rs index c3d1851..62b8476 100644 --- a/turtle-lib/src/execution.rs +++ b/turtle-lib/src/execution.rs @@ -2,7 +2,7 @@ use crate::circle_geometry::{CircleDirection, CircleGeometry}; use crate::commands::TurtleCommand; -use crate::general::Coordinate; +use crate::general::{Coordinate, Radians}; use crate::state::{DrawCommand, FillState, Turtle, TurtleParams, TurtleWorld}; use crate::tessellation; use macroquad::prelude::*; @@ -247,7 +247,7 @@ pub(crate) fn record_fill_vertices_after_movement( } => { let geom = CircleGeometry::new( start_state.position, - start_state.heading, + Radians::new(start_state.heading), *radius, *direction, ); @@ -267,10 +267,10 @@ pub(crate) fn record_fill_vertices_after_movement( let progress = i as f32 / num_samples as f32; let current_angle = match direction { CircleDirection::Left => { - geom.start_angle_from_center - angle.to_radians() * progress + geom.start_angle_from_center - angle.as_radians().value() * progress } CircleDirection::Right => { - geom.start_angle_from_center + angle.to_radians() * progress + geom.start_angle_from_center + angle.as_radians().value() * progress } }; let vertex = Coordinate::new( @@ -362,12 +362,17 @@ pub(crate) fn tessellate_command( direction, } => { use crate::circle_geometry::CircleGeometry; - let geom = CircleGeometry::new(start.position, start.heading, *radius, *direction); + let geom = CircleGeometry::new( + start.position, + Radians::new(start.heading), + *radius, + *direction, + ); let mesh_data = tessellation::tessellate_arc( geom.center, *radius, geom.start_angle_from_center.to_degrees(), - *angle, + angle.value(), start.color, start.pen_width, *steps, @@ -449,6 +454,7 @@ pub(crate) fn execute_command_with_id( mod tests { use super::*; use crate::commands::TurtleCommand; + use crate::general::Degrees; use crate::shapes::TurtleShape; use crate::tweening::TweenController; @@ -512,7 +518,7 @@ mod tests { // Left 90 degrees - should face north (heading decreases by 90°) // In screen coords: north = -90° = -π/2 - execute_command(&TurtleCommand::Turn(-90.0), &mut state); + execute_command(&TurtleCommand::Turn(Degrees::new(-90.0)), &mut state); assert!( (state.params.position.x - 100.0).abs() < 0.01, "After left(90): x = {}", diff --git a/turtle-lib/src/general.rs b/turtle-lib/src/general.rs index c726bc0..4ad3133 100644 --- a/turtle-lib/src/general.rs +++ b/turtle-lib/src/general.rs @@ -6,7 +6,7 @@ pub mod angle; pub mod fontsize; pub mod length; -pub use angle::Angle; +pub use angle::{Degrees, Radians}; pub use fontsize::FontSize; pub use length::Length; diff --git a/turtle-lib/src/general/angle.rs b/turtle-lib/src/general/angle.rs index 4ff9efd..e5814f6 100644 --- a/turtle-lib/src/general/angle.rs +++ b/turtle-lib/src/general/angle.rs @@ -1,205 +1,162 @@ -//! Angle type with degrees and radians support +//! Angle unit newtypes: `Degrees` and `Radians`. +//! +//! ## Design +//! +//! Two separate types instead of a single enum so that function signatures are +//! self-documenting and the compiler rejects wrong-unit arguments. +//! +//! - **`Degrees`** — public API boundary. Builder methods and `TurtleCommand` +//! fields that originate from user input store this type. Convert with +//! `as_radians()` before entering the rendering pipeline. +//! +//! - **`Radians`** — internal pipeline. All geometry functions and +//! `TurtleParams` arithmetic work in radians. Extract the raw `f32` with +//! `value()` only where stdlib trig functions (`sin`, `cos`, …) require it. +//! +//! There is intentionally **no** conversion from `Radians` back to `f32` that +//! strips the unit tag silently — use `.value()` explicitly and at the last +//! possible moment. use super::Precision; -use std::ops::{Add, Div, Mul, Neg, Rem, Sub}; +use std::ops::Neg; -#[derive(Copy, Clone, Debug, PartialEq)] -pub enum AngleUnit { - Degrees(Precision), - Radians(Precision), -} +/// An angle measured in degrees. +/// +/// Used at the public API boundary. Convert to [`Radians`] with `as_radians()` +/// before passing into internal rendering functions. +#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Default)] +pub struct Degrees(pub Precision); -impl Default for AngleUnit { - fn default() -> Self { - Self::Degrees(0.0) +impl Degrees { + /// Construct from a raw degrees value. + #[must_use] + pub fn new(v: Precision) -> Self { + Self(v) + } + + /// Convert to [`Radians`] for use in the rendering pipeline. + /// + /// This is the **only** correct way to enter the internal math layer. + #[must_use] + pub fn as_radians(self) -> Radians { + Radians(self.0.to_radians()) + } + + /// The raw degrees value. + /// + /// Use only for degree-to-degree arithmetic (e.g. negating a turn angle + /// before storing it as a command). Do not pass this to trig functions. + #[must_use] + pub fn value(self) -> Precision { + self.0 } } -#[derive(Copy, Clone, Debug, PartialEq)] -pub struct Angle { - value: AngleUnit, -} - -impl Default for Angle { - fn default() -> Self { - Self { - value: AngleUnit::Degrees(0.0), - } - } -} - -impl From for Angle { - fn from(i: i16) -> Self { - Self { - value: AngleUnit::Degrees(Precision::from(i)), - } - } -} - -impl From for Angle { - fn from(f: f32) -> Self { - Self { - value: AngleUnit::Degrees(f), - } - } -} - -impl Rem for Angle { +impl Neg for Degrees { type Output = Self; - - fn rem(self, rhs: Precision) -> Self::Output { - match self.value { - AngleUnit::Degrees(v) => Self::degrees(v % rhs), - AngleUnit::Radians(v) => Self::radians(v % rhs), - } + fn neg(self) -> Self { + Self(-self.0) } } -impl Mul for Angle { +impl From for Degrees { + fn from(v: f32) -> Self { + Self(v) + } +} + +impl From for Degrees { + fn from(v: i32) -> Self { + Self(v as Precision) + } +} + +impl From for Degrees { + fn from(v: i16) -> Self { + Self(Precision::from(v)) + } +} + +// ───────────────────────────────────────────────────────────────────────────── + +/// An angle measured in radians. +/// +/// Used in all internal function signatures and geometry math. Extract the +/// raw `f32` with [`value()`](Radians::value) only when calling stdlib trig +/// functions (`sin`, `cos`, etc.). +#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Default)] +pub struct Radians(pub Precision); + +impl Radians { + /// Construct from a raw radians value. + #[must_use] + pub fn new(v: Precision) -> Self { + Self(v) + } + + /// Convert to [`Degrees`] for display or user-facing output. + #[must_use] + pub fn as_degrees(self) -> Degrees { + Degrees(self.0.to_degrees()) + } + + /// The raw radians value. + /// + /// Use only when calling stdlib trig functions or other `f32`-based + /// math APIs. Keep `Radians` as the type at all internal function + /// boundaries. + #[must_use] + pub fn value(self) -> Precision { + self.0 + } +} + +impl Neg for Radians { type Output = Self; - - fn mul(self, rhs: Precision) -> Self::Output { - match self.value { - AngleUnit::Degrees(v) => Self::degrees(v * rhs), - AngleUnit::Radians(v) => Self::radians(v * rhs), - } + fn neg(self) -> Self { + Self(-self.0) } } -impl Div for Angle { - type Output = Self; - - fn div(self, rhs: Precision) -> Self::Output { - match self.value { - AngleUnit::Degrees(v) => Self::degrees(v / rhs), - AngleUnit::Radians(v) => Self::radians(v / rhs), - } - } -} - -impl Neg for Angle { - type Output = Self; - - fn neg(self) -> Self::Output { - match self.value { - AngleUnit::Degrees(v) => Self::degrees(-v), - AngleUnit::Radians(v) => Self::radians(-v), - } - } -} - -impl Neg for &Angle { - type Output = Angle; - - fn neg(self) -> Self::Output { - match self.value { - AngleUnit::Degrees(v) => Angle::degrees(-v), - AngleUnit::Radians(v) => Angle::radians(-v), - } - } -} - -impl Add for Angle { - type Output = Angle; - - fn add(self, rhs: Self) -> Self::Output { - match (self.value, rhs.value) { - (AngleUnit::Degrees(v), AngleUnit::Degrees(o)) => Self::degrees(v + o), - (AngleUnit::Degrees(v), AngleUnit::Radians(o)) => Self::radians(v.to_radians() + o), - (AngleUnit::Radians(v), AngleUnit::Degrees(o)) => Self::radians(v + o.to_radians()), - (AngleUnit::Radians(v), AngleUnit::Radians(o)) => Self::radians(v + o), - } - } -} - -impl Sub for Angle { - type Output = Angle; - - fn sub(self, rhs: Self) -> Self::Output { - match (self.value, rhs.value) { - (AngleUnit::Degrees(v), AngleUnit::Degrees(o)) => Self::degrees(v - o), - (AngleUnit::Degrees(v), AngleUnit::Radians(o)) => Self::radians(v.to_radians() - o), - (AngleUnit::Radians(v), AngleUnit::Degrees(o)) => Self::radians(v - o.to_radians()), - (AngleUnit::Radians(v), AngleUnit::Radians(o)) => Self::radians(v - o), - } - } -} - -impl Angle { - #[must_use] - pub fn degrees(value: Precision) -> Self { - Self { - value: AngleUnit::Degrees(value), - } - } - - #[must_use] - pub fn radians(value: Precision) -> Self { - Self { - value: AngleUnit::Radians(value), - } - } - - #[must_use] - pub fn value(&self) -> Precision { - match self.value { - AngleUnit::Degrees(v) | AngleUnit::Radians(v) => v, - } - } - - #[must_use] - pub fn to_radians(self) -> Self { - match self.value { - AngleUnit::Degrees(v) => Self::radians(v.to_radians()), - AngleUnit::Radians(_) => self, - } - } - - #[must_use] - pub fn to_degrees(self) -> Self { - match self.value { - AngleUnit::Degrees(_) => self, - AngleUnit::Radians(v) => Self::degrees(v.to_degrees()), - } - } - - #[must_use] - pub fn limit_smaller_than_full_circle(self) -> Self { - use std::f32::consts::PI; - match self.value { - AngleUnit::Degrees(v) => Self::degrees(v % 360.0), - AngleUnit::Radians(v) => Self::radians(v % (2.0 * PI)), - } +impl From for Radians { + fn from(v: f32) -> Self { + Self(v) } } #[cfg(test)] mod tests { use super::*; + use std::f32::consts::PI; #[test] - fn convert_to_radians() { - let radi = Angle::radians(30f32.to_radians()); - let degr = Angle::degrees(30f32); - let converted = degr.to_radians(); - assert!((radi.value() - converted.value()).abs() < 0.0001); + fn degrees_to_radians_roundtrip() { + let deg = Degrees::new(180.0); + let rad = deg.as_radians(); + assert!( + (rad.value() - PI).abs() < 1e-6, + "expected π, got {}", + rad.value() + ); + let back = rad.as_degrees(); + assert!( + (back.value() - 180.0).abs() < 1e-4, + "expected 180°, got {}", + back.value() + ); } #[test] - fn sum_degrees() { - let fst = Angle::degrees(30f32); - let snd = Angle::degrees(30f32); - let sum = fst + snd; - assert!((sum.value() - 60f32).abs() < 0.0001); - assert!((sum.to_radians().value() - 60f32.to_radians()).abs() < 0.0001); + fn negation() { + assert_eq!(-Degrees::new(90.0), Degrees::new(-90.0)); + assert_eq!(-Radians::new(1.0), Radians::new(-1.0)); } #[test] - fn sum_mixed() { - let fst = Angle::degrees(30f32); - let snd = Angle::radians(30f32.to_radians()); - let sum = fst + snd; - assert!((sum.to_degrees().value() - 60f32).abs() < 0.0001); - assert!((sum.to_radians().value() - 60f32.to_radians()).abs() < 0.0001); + fn from_integer() { + let d: Degrees = 90_i32.into(); + assert_eq!(d, Degrees::new(90.0)); + let d2: Degrees = 45_i16.into(); + assert_eq!(d2, Degrees::new(45.0)); } } diff --git a/turtle-lib/src/lib.rs b/turtle-lib/src/lib.rs index a1228b3..c53620e 100644 --- a/turtle-lib/src/lib.rs +++ b/turtle-lib/src/lib.rs @@ -63,7 +63,7 @@ pub(crate) mod tweening; pub use builders::{CurvedMovement, DirectionalMovement, Turnable, TurtlePlan, WithCommands}; pub use commands::{CommandQueue, TurtleCommand}; pub use commands_channel::TurtleCommandSender; -pub use general::{Angle, AnimationSpeed, Color, Coordinate, Length, Precision}; +pub use general::{Degrees, Radians, AnimationSpeed, Color, Coordinate, Length, Precision}; pub use shapes::{ShapeType, TurtleShape}; pub mod export; diff --git a/turtle-lib/src/state.rs b/turtle-lib/src/state.rs index 6bd594e..f46e077 100644 --- a/turtle-lib/src/state.rs +++ b/turtle-lib/src/state.rs @@ -1,7 +1,7 @@ //! Turtle state and world state management use crate::commands::CommandQueue; -use crate::general::{Angle, AnimationSpeed, Color, Coordinate}; +use crate::general::{AnimationSpeed, Color, Coordinate}; use crate::shapes::TurtleShape; use crate::tweening::TweenController; use macroquad::prelude::*; @@ -88,8 +88,8 @@ impl Turtle { } #[must_use] - pub fn heading_angle(&self) -> Angle { - Angle::radians(self.params.heading) + pub fn heading_angle(&self) -> crate::general::Radians { + crate::general::Radians::new(self.params.heading) } /// Reset turtle to default state (preserves `turtle_id` and queued commands) diff --git a/turtle-lib/src/tweening.rs b/turtle-lib/src/tweening.rs index c1c6f90..d36b936 100644 --- a/turtle-lib/src/tweening.rs +++ b/turtle-lib/src/tweening.rs @@ -2,7 +2,7 @@ use crate::circle_geometry::{CircleDirection, CircleGeometry}; use crate::commands::{CommandQueue, TurtleCommand}; -use crate::general::AnimationSpeed; +use crate::general::{AnimationSpeed, Radians}; use crate::state::{DrawCommand, FillState, TurtleParams}; use macroquad::prelude::*; use tween::{CubicInOut, TweenValue, Tweener}; @@ -180,10 +180,10 @@ impl TweenController { direction, .. } => { - let angle_traveled = angle.to_radians() * progress; + let angle_traveled = angle.as_radians().value() * progress; calculate_circle_position( tween.start_params.position, - tween.start_params.heading, + Radians::new(tween.start_params.heading), *radius, angle_traveled, *direction, @@ -204,14 +204,14 @@ impl TweenController { angle, direction, .. } => match direction { CircleDirection::Left => { - tween.start_params.heading - angle.to_radians() * progress + tween.start_params.heading - angle.as_radians().value() * progress } CircleDirection::Right => { - tween.start_params.heading + angle.to_radians() * progress + tween.start_params.heading + angle.as_radians().value() * progress } }, TurtleCommand::Turn(angle) => { - tween.start_params.heading + angle.to_radians() * progress + tween.start_params.heading + angle.as_radians().value() * progress } _ => { // For other commands that change heading, lerp directly @@ -368,12 +368,17 @@ impl TweenController { } } -/// Calculate position on a circular arc +/// Calculate position on a circular arc. +/// +/// `start_heading` is in radians (typed as `Radians` to make the unit +/// explicit at every call site). `angle_traveled` is already a raw `f32` +/// radians value produced by multiplying `Degrees::as_radians().value()` +/// by a tween progress scalar. fn calculate_circle_position( start_pos: Vec2, - start_heading: f32, + start_heading: Radians, radius: f32, - angle_traveled: f32, // How much of the total angle we've traveled (in radians) + angle_traveled: f32, direction: CircleDirection, ) -> Vec2 { let geom = CircleGeometry::new(start_pos, start_heading, radius, direction);