more consistent use of angle types

This commit is contained in:
Franz Dietrich 2026-05-21 15:02:57 +02:00
parent 3c076fdd03
commit 6b558ca8a0
11 changed files with 222 additions and 243 deletions

View File

@ -1,7 +1,7 @@
//! Builder pattern traits for creating turtle command sequences //! Builder pattern traits for creating turtle command sequences
use crate::commands::{CommandQueue, TurtleCommand}; 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}; use crate::shapes::{ShapeType, TurtleShape};
/// Trait for adding commands to a queue /// Trait for adding commands to a queue
@ -91,10 +91,10 @@ pub trait Turnable: WithCommands {
/// ``` /// ```
fn left<T>(&mut self, angle: T) -> &mut Self fn left<T>(&mut self, angle: T) -> &mut Self
where where
T: Into<Precision>, T: Into<Degrees>,
{ {
let degrees: Precision = angle.into(); self.get_commands_mut()
self.get_commands_mut().push(TurtleCommand::Turn(-degrees)); .push(TurtleCommand::Turn(-angle.into()));
self self
} }
@ -118,10 +118,10 @@ pub trait Turnable: WithCommands {
/// ``` /// ```
fn right<T>(&mut self, angle: T) -> &mut Self fn right<T>(&mut self, angle: T) -> &mut Self
where where
T: Into<Precision>, T: Into<Degrees>,
{ {
let degrees: Precision = angle.into(); self.get_commands_mut()
self.get_commands_mut().push(TurtleCommand::Turn(degrees)); .push(TurtleCommand::Turn(angle.into()));
self self
} }
} }
@ -160,13 +160,12 @@ pub trait CurvedMovement: WithCommands {
fn circle_left<R, A>(&mut self, radius: R, angle: A, steps: usize) -> &mut Self fn circle_left<R, A>(&mut self, radius: R, angle: A, steps: usize) -> &mut Self
where where
R: Into<Precision>, R: Into<Precision>,
A: Into<Precision>, A: Into<Degrees>,
{ {
let r: Precision = radius.into(); let r: Precision = radius.into();
let a: Precision = angle.into();
self.get_commands_mut().push(TurtleCommand::Circle { self.get_commands_mut().push(TurtleCommand::Circle {
radius: r, radius: r,
angle: a, angle: angle.into(),
steps, steps,
direction: crate::circle_geometry::CircleDirection::Left, direction: crate::circle_geometry::CircleDirection::Left,
}); });
@ -207,13 +206,12 @@ pub trait CurvedMovement: WithCommands {
fn circle_right<R, A>(&mut self, radius: R, angle: A, steps: usize) -> &mut Self fn circle_right<R, A>(&mut self, radius: R, angle: A, steps: usize) -> &mut Self
where where
R: Into<Precision>, R: Into<Precision>,
A: Into<Precision>, A: Into<Degrees>,
{ {
let r: Precision = radius.into(); let r: Precision = radius.into();
let a: Precision = angle.into();
self.get_commands_mut().push(TurtleCommand::Circle { self.get_commands_mut().push(TurtleCommand::Circle {
radius: r, radius: r,
angle: a, angle: angle.into(),
steps, steps,
direction: crate::circle_geometry::CircleDirection::Right, direction: crate::circle_geometry::CircleDirection::Right,
}); });
@ -367,9 +365,9 @@ impl TurtlePlan {
/// .forward(100.0); /// .forward(100.0);
/// } /// }
/// ``` /// ```
pub fn set_heading(&mut self, heading: Precision) -> &mut Self { pub fn set_heading<T: Into<Degrees>>(&mut self, heading: T) -> &mut Self {
self.queue self.queue
.push(TurtleCommand::SetHeading(-heading.to_radians())); .push(TurtleCommand::SetHeading(-heading.into().as_radians()));
self self
} }

View File

@ -1,5 +1,6 @@
//! Circle geometry calculations - single source of truth for `circle_left` and `circle_right` //! Circle geometry calculations - single source of truth for `circle_left` and `circle_right`
use crate::general::Radians;
use macroquad::prelude::*; use macroquad::prelude::*;
/// Direction of circular motion (in screen coordinates with Y-down) /// Direction of circular motion (in screen coordinates with Y-down)
@ -22,12 +23,15 @@ impl CircleGeometry {
#[must_use] #[must_use]
pub fn new( pub fn new(
turtle_pos: Vec2, turtle_pos: Vec2,
turtle_heading: f32, turtle_heading: Radians,
radius: f32, radius: f32,
direction: CircleDirection, direction: CircleDirection,
) -> Self { ) -> Self {
use std::f32::consts::FRAC_PI_2; 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 // Calculate center based on direction
// In screen coordinates (Y-down): // In screen coordinates (Y-down):
// - Left turn (counter-clockwise visually): center is perpendicular-left from turtle's perspective // - 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 // - Right turn (clockwise visually): center is perpendicular-right from turtle's perspective
// which is heading + π/2 (rotated counter-clockwise from heading vector) // which is heading + π/2 (rotated counter-clockwise from heading vector)
let center_offset_angle = match direction { let center_offset_angle = match direction {
CircleDirection::Left => turtle_heading - FRAC_PI_2, CircleDirection::Left => heading - FRAC_PI_2,
CircleDirection::Right => turtle_heading + FRAC_PI_2, CircleDirection::Right => heading + FRAC_PI_2,
}; };
let center = vec2( let center = vec2(
@ -46,8 +50,8 @@ impl CircleGeometry {
// Angle from center back to turtle position // Angle from center back to turtle position
let start_angle_from_center = match direction { let start_angle_from_center = match direction {
CircleDirection::Left => turtle_heading + FRAC_PI_2, CircleDirection::Left => heading + FRAC_PI_2,
CircleDirection::Right => turtle_heading - FRAC_PI_2, CircleDirection::Right => heading - FRAC_PI_2,
}; };
Self { Self {
@ -151,7 +155,7 @@ mod tests {
fn test_circle_left_geometry() { fn test_circle_left_geometry() {
let geom = CircleGeometry::new( let geom = CircleGeometry::new(
vec2(0.0, 0.0), vec2(0.0, 0.0),
0.0, // heading east (0 radians) Radians::new(0.0), // heading east (0 radians)
100.0, 100.0,
CircleDirection::Left, CircleDirection::Left,
); );
@ -183,7 +187,7 @@ mod tests {
fn test_circle_right_geometry() { fn test_circle_right_geometry() {
let geom = CircleGeometry::new( let geom = CircleGeometry::new(
vec2(0.0, 0.0), vec2(0.0, 0.0),
0.0, // heading east Radians::new(0.0), // heading east
100.0, 100.0,
CircleDirection::Right, CircleDirection::Right,
); );

View File

@ -9,7 +9,7 @@
use crate::circle_geometry::{CircleDirection, CircleGeometry}; use crate::circle_geometry::{CircleDirection, CircleGeometry};
use crate::commands::TurtleCommand; use crate::commands::TurtleCommand;
use crate::general::AnimationSpeed; use crate::general::{AnimationSpeed, Radians};
use crate::state::TurtleParams; use crate::state::TurtleParams;
use crate::tweening::normalize_angle; use crate::tweening::normalize_angle;
use macroquad::prelude::vec2; use macroquad::prelude::vec2;
@ -36,7 +36,7 @@ impl TurtleCommand {
params.position = vec2(params.position.x + dx, params.position.y + dy); params.position = vec2(params.position.x + dx, params.position.y + dy);
} }
TurtleCommand::Turn(angle) => { TurtleCommand::Turn(angle) => {
params.heading = normalize_angle(params.heading + angle.to_radians()); params.heading = normalize_angle(params.heading + angle.as_radians().value());
} }
TurtleCommand::Circle { TurtleCommand::Circle {
radius, radius,
@ -44,12 +44,17 @@ impl TurtleCommand {
direction, direction,
.. ..
} => { } => {
let geom = let geom = CircleGeometry::new(
CircleGeometry::new(params.position, params.heading, *radius, *direction); params.position,
params.position = geom.position_at_angle(angle.to_radians()); 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 { params.heading = normalize_angle(match direction {
CircleDirection::Left => params.heading - angle.to_radians(), CircleDirection::Left => params.heading - angle_rad,
CircleDirection::Right => params.heading + angle.to_radians(), CircleDirection::Right => params.heading + angle_rad,
}); });
} }
TurtleCommand::Goto(coord) => { TurtleCommand::Goto(coord) => {
@ -57,7 +62,7 @@ impl TurtleCommand {
params.position = vec2(coord.x, -coord.y); params.position = vec2(coord.x, -coord.y);
} }
TurtleCommand::SetHeading(heading) => { TurtleCommand::SetHeading(heading) => {
params.heading = normalize_angle(*heading); params.heading = normalize_angle(heading.value());
} }
TurtleCommand::SetColor(color) => { TurtleCommand::SetColor(color) => {
params.color = *color; params.color = *color;
@ -115,9 +120,9 @@ impl TurtleCommand {
let base: f32 = match self { let base: f32 = match self {
TurtleCommand::Move(dist) => dist.abs() / spd, 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, .. } => { TurtleCommand::Circle { radius, angle, .. } => {
let arc_length = radius * angle.to_radians().abs(); let arc_length = radius * angle.as_radians().value().abs();
arc_length / spd arc_length / spd
} }
TurtleCommand::Goto(target) => { TurtleCommand::Goto(target) => {

View File

@ -1,6 +1,6 @@
//! Turtle commands and command queue //! 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; use crate::shapes::TurtleShape;
/// Individual turtle commands /// Individual turtle commands
@ -9,13 +9,14 @@ pub enum TurtleCommand {
// Movement (positive = forward, negative = backward) // Movement (positive = forward, negative = backward)
Move(Precision), Move(Precision),
// Rotation (positive = right/clockwise, negative = left/counter-clockwise in degrees) // Rotation (positive = right/clockwise, negative = left/counter-clockwise)
Turn(Precision), // Stored in degrees — the natural unit at the user-facing API boundary.
Turn(Degrees),
// Circle drawing // Circle drawing
Circle { Circle {
radius: Precision, radius: Precision,
angle: Precision, // degrees angle: Degrees, // sweep angle — degrees, as supplied by the user
steps: usize, steps: usize,
direction: crate::circle_geometry::CircleDirection, direction: crate::circle_geometry::CircleDirection,
}, },
@ -33,7 +34,8 @@ pub enum TurtleCommand {
// Position // Position
Goto(Coordinate), Goto(Coordinate),
SetHeading(Precision), // radians /// Heading stored as radians — already converted by the builder.
SetHeading(Radians),
// Visibility // Visibility
ShowTurtle, ShowTurtle,

View File

@ -177,9 +177,10 @@ pub(crate) fn render_world_with_tweens(world: &TurtleWorld, zoom_level: f32) {
{ {
// Calculate partial arc vertices based on current progress // Calculate partial arc vertices based on current progress
use crate::circle_geometry::CircleGeometry; use crate::circle_geometry::CircleGeometry;
use crate::general::Radians;
let geom = CircleGeometry::new( let geom = CircleGeometry::new(
tween.start_params.position, tween.start_params.position,
tween.start_params.heading, Radians::new(tween.start_params.heading),
*radius, *radius,
*direction, *direction,
); // Calculate progress ); // Calculate progress
@ -197,11 +198,11 @@ pub(crate) fn render_world_with_tweens(world: &TurtleWorld, zoom_level: f32) {
let current_angle = match direction { let current_angle = match direction {
crate::circle_geometry::CircleDirection::Left => { crate::circle_geometry::CircleDirection::Left => {
geom.start_angle_from_center geom.start_angle_from_center
- angle.to_radians() * sample_progress - angle.as_radians().value() * sample_progress
} }
crate::circle_geometry::CircleDirection::Right => { crate::circle_geometry::CircleDirection::Right => {
geom.start_angle_from_center 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( fn draw_tween_arc(
tween: &crate::tweening::CommandTween, tween: &crate::tweening::CommandTween,
radius: f32, radius: f32,
total_angle: f32, total_angle: crate::general::Degrees,
steps: usize, steps: usize,
direction: CircleDirection, direction: CircleDirection,
) { ) {
use crate::general::Radians;
let geom = CircleGeometry::new( let geom = CircleGeometry::new(
tween.start_params.position, tween.start_params.position,
tween.start_params.heading, Radians::new(tween.start_params.heading),
radius, radius,
direction, direction,
); );
@ -375,7 +377,7 @@ fn draw_tween_arc(
geom.center, geom.center,
radius, radius,
geom.start_angle_from_center.to_degrees(), geom.start_angle_from_center.to_degrees(),
total_angle * progress, total_angle.value() * progress,
tween.start_params.color, tween.start_params.color,
tween.start_params.pen_width, tween.start_params.pen_width,
((steps as f32 * progress).ceil() as usize).max(1), ((steps as f32 * progress).ceil() as usize).max(1),

View File

@ -2,7 +2,7 @@
use crate::circle_geometry::{CircleDirection, CircleGeometry}; use crate::circle_geometry::{CircleDirection, CircleGeometry};
use crate::commands::TurtleCommand; use crate::commands::TurtleCommand;
use crate::general::Coordinate; use crate::general::{Coordinate, Radians};
use crate::state::{DrawCommand, FillState, Turtle, TurtleParams, TurtleWorld}; use crate::state::{DrawCommand, FillState, Turtle, TurtleParams, TurtleWorld};
use crate::tessellation; use crate::tessellation;
use macroquad::prelude::*; use macroquad::prelude::*;
@ -247,7 +247,7 @@ pub(crate) fn record_fill_vertices_after_movement(
} => { } => {
let geom = CircleGeometry::new( let geom = CircleGeometry::new(
start_state.position, start_state.position,
start_state.heading, Radians::new(start_state.heading),
*radius, *radius,
*direction, *direction,
); );
@ -267,10 +267,10 @@ pub(crate) fn record_fill_vertices_after_movement(
let progress = i as f32 / num_samples as f32; let progress = i as f32 / num_samples as f32;
let current_angle = match direction { let current_angle = match direction {
CircleDirection::Left => { CircleDirection::Left => {
geom.start_angle_from_center - angle.to_radians() * progress geom.start_angle_from_center - angle.as_radians().value() * progress
} }
CircleDirection::Right => { 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( let vertex = Coordinate::new(
@ -362,12 +362,17 @@ pub(crate) fn tessellate_command(
direction, direction,
} => { } => {
use crate::circle_geometry::CircleGeometry; 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( let mesh_data = tessellation::tessellate_arc(
geom.center, geom.center,
*radius, *radius,
geom.start_angle_from_center.to_degrees(), geom.start_angle_from_center.to_degrees(),
*angle, angle.value(),
start.color, start.color,
start.pen_width, start.pen_width,
*steps, *steps,
@ -449,6 +454,7 @@ pub(crate) fn execute_command_with_id(
mod tests { mod tests {
use super::*; use super::*;
use crate::commands::TurtleCommand; use crate::commands::TurtleCommand;
use crate::general::Degrees;
use crate::shapes::TurtleShape; use crate::shapes::TurtleShape;
use crate::tweening::TweenController; use crate::tweening::TweenController;
@ -512,7 +518,7 @@ mod tests {
// Left 90 degrees - should face north (heading decreases by 90°) // Left 90 degrees - should face north (heading decreases by 90°)
// In screen coords: north = -90° = -π/2 // 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!( assert!(
(state.params.position.x - 100.0).abs() < 0.01, (state.params.position.x - 100.0).abs() < 0.01,
"After left(90): x = {}", "After left(90): x = {}",

View File

@ -6,7 +6,7 @@ pub mod angle;
pub mod fontsize; pub mod fontsize;
pub mod length; pub mod length;
pub use angle::Angle; pub use angle::{Degrees, Radians};
pub use fontsize::FontSize; pub use fontsize::FontSize;
pub use length::Length; pub use length::Length;

View File

@ -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 super::Precision;
use std::ops::{Add, Div, Mul, Neg, Rem, Sub}; use std::ops::Neg;
#[derive(Copy, Clone, Debug, PartialEq)] /// An angle measured in degrees.
pub enum AngleUnit { ///
Degrees(Precision), /// Used at the public API boundary. Convert to [`Radians`] with `as_radians()`
Radians(Precision), /// before passing into internal rendering functions.
} #[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Default)]
pub struct Degrees(pub Precision);
impl Default for AngleUnit { impl Degrees {
fn default() -> Self { /// Construct from a raw degrees value.
Self::Degrees(0.0) #[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)] impl Neg for Degrees {
pub struct Angle {
value: AngleUnit,
}
impl Default for Angle {
fn default() -> Self {
Self {
value: AngleUnit::Degrees(0.0),
}
}
}
impl From<i16> for Angle {
fn from(i: i16) -> Self {
Self {
value: AngleUnit::Degrees(Precision::from(i)),
}
}
}
impl From<f32> for Angle {
fn from(f: f32) -> Self {
Self {
value: AngleUnit::Degrees(f),
}
}
}
impl Rem<Precision> for Angle {
type Output = Self; type Output = Self;
fn neg(self) -> Self {
fn rem(self, rhs: Precision) -> Self::Output { Self(-self.0)
match self.value {
AngleUnit::Degrees(v) => Self::degrees(v % rhs),
AngleUnit::Radians(v) => Self::radians(v % rhs),
}
} }
} }
impl Mul<Precision> for Angle { impl From<f32> for Degrees {
fn from(v: f32) -> Self {
Self(v)
}
}
impl From<i32> for Degrees {
fn from(v: i32) -> Self {
Self(v as Precision)
}
}
impl From<i16> 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; type Output = Self;
fn neg(self) -> Self {
fn mul(self, rhs: Precision) -> Self::Output { Self(-self.0)
match self.value {
AngleUnit::Degrees(v) => Self::degrees(v * rhs),
AngleUnit::Radians(v) => Self::radians(v * rhs),
}
} }
} }
impl Div<Precision> for Angle { impl From<f32> for Radians {
type Output = Self; fn from(v: f32) -> Self {
Self(v)
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)),
}
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use std::f32::consts::PI;
#[test] #[test]
fn convert_to_radians() { fn degrees_to_radians_roundtrip() {
let radi = Angle::radians(30f32.to_radians()); let deg = Degrees::new(180.0);
let degr = Angle::degrees(30f32); let rad = deg.as_radians();
let converted = degr.to_radians(); assert!(
assert!((radi.value() - converted.value()).abs() < 0.0001); (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] #[test]
fn sum_degrees() { fn negation() {
let fst = Angle::degrees(30f32); assert_eq!(-Degrees::new(90.0), Degrees::new(-90.0));
let snd = Angle::degrees(30f32); assert_eq!(-Radians::new(1.0), Radians::new(-1.0));
let sum = fst + snd;
assert!((sum.value() - 60f32).abs() < 0.0001);
assert!((sum.to_radians().value() - 60f32.to_radians()).abs() < 0.0001);
} }
#[test] #[test]
fn sum_mixed() { fn from_integer() {
let fst = Angle::degrees(30f32); let d: Degrees = 90_i32.into();
let snd = Angle::radians(30f32.to_radians()); assert_eq!(d, Degrees::new(90.0));
let sum = fst + snd; let d2: Degrees = 45_i16.into();
assert!((sum.to_degrees().value() - 60f32).abs() < 0.0001); assert_eq!(d2, Degrees::new(45.0));
assert!((sum.to_radians().value() - 60f32.to_radians()).abs() < 0.0001);
} }
} }

View File

@ -63,7 +63,7 @@ pub(crate) mod tweening;
pub use builders::{CurvedMovement, DirectionalMovement, Turnable, TurtlePlan, WithCommands}; pub use builders::{CurvedMovement, DirectionalMovement, Turnable, TurtlePlan, WithCommands};
pub use commands::{CommandQueue, TurtleCommand}; pub use commands::{CommandQueue, TurtleCommand};
pub use commands_channel::TurtleCommandSender; 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 use shapes::{ShapeType, TurtleShape};
pub mod export; pub mod export;

View File

@ -1,7 +1,7 @@
//! Turtle state and world state management //! Turtle state and world state management
use crate::commands::CommandQueue; use crate::commands::CommandQueue;
use crate::general::{Angle, AnimationSpeed, Color, Coordinate}; use crate::general::{AnimationSpeed, Color, Coordinate};
use crate::shapes::TurtleShape; use crate::shapes::TurtleShape;
use crate::tweening::TweenController; use crate::tweening::TweenController;
use macroquad::prelude::*; use macroquad::prelude::*;
@ -88,8 +88,8 @@ impl Turtle {
} }
#[must_use] #[must_use]
pub fn heading_angle(&self) -> Angle { pub fn heading_angle(&self) -> crate::general::Radians {
Angle::radians(self.params.heading) crate::general::Radians::new(self.params.heading)
} }
/// Reset turtle to default state (preserves `turtle_id` and queued commands) /// Reset turtle to default state (preserves `turtle_id` and queued commands)

View File

@ -2,7 +2,7 @@
use crate::circle_geometry::{CircleDirection, CircleGeometry}; use crate::circle_geometry::{CircleDirection, CircleGeometry};
use crate::commands::{CommandQueue, TurtleCommand}; use crate::commands::{CommandQueue, TurtleCommand};
use crate::general::AnimationSpeed; use crate::general::{AnimationSpeed, Radians};
use crate::state::{DrawCommand, FillState, TurtleParams}; use crate::state::{DrawCommand, FillState, TurtleParams};
use macroquad::prelude::*; use macroquad::prelude::*;
use tween::{CubicInOut, TweenValue, Tweener}; use tween::{CubicInOut, TweenValue, Tweener};
@ -180,10 +180,10 @@ impl TweenController {
direction, direction,
.. ..
} => { } => {
let angle_traveled = angle.to_radians() * progress; let angle_traveled = angle.as_radians().value() * progress;
calculate_circle_position( calculate_circle_position(
tween.start_params.position, tween.start_params.position,
tween.start_params.heading, Radians::new(tween.start_params.heading),
*radius, *radius,
angle_traveled, angle_traveled,
*direction, *direction,
@ -204,14 +204,14 @@ impl TweenController {
angle, direction, .. angle, direction, ..
} => match direction { } => match direction {
CircleDirection::Left => { CircleDirection::Left => {
tween.start_params.heading - angle.to_radians() * progress tween.start_params.heading - angle.as_radians().value() * progress
} }
CircleDirection::Right => { CircleDirection::Right => {
tween.start_params.heading + angle.to_radians() * progress tween.start_params.heading + angle.as_radians().value() * progress
} }
}, },
TurtleCommand::Turn(angle) => { 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 // 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( fn calculate_circle_position(
start_pos: Vec2, start_pos: Vec2,
start_heading: f32, start_heading: Radians,
radius: f32, radius: f32,
angle_traveled: f32, // How much of the total angle we've traveled (in radians) angle_traveled: f32,
direction: CircleDirection, direction: CircleDirection,
) -> Vec2 { ) -> Vec2 {
let geom = CircleGeometry::new(start_pos, start_heading, radius, direction); let geom = CircleGeometry::new(start_pos, start_heading, radius, direction);