more consistent use of angle types
This commit is contained in:
parent
3c076fdd03
commit
6b558ca8a0
@ -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<T>(&mut self, angle: T) -> &mut Self
|
||||
where
|
||||
T: Into<Precision>,
|
||||
T: Into<Degrees>,
|
||||
{
|
||||
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<T>(&mut self, angle: T) -> &mut Self
|
||||
where
|
||||
T: Into<Precision>,
|
||||
T: Into<Degrees>,
|
||||
{
|
||||
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<R, A>(&mut self, radius: R, angle: A, steps: usize) -> &mut Self
|
||||
where
|
||||
R: Into<Precision>,
|
||||
A: Into<Precision>,
|
||||
A: Into<Degrees>,
|
||||
{
|
||||
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<R, A>(&mut self, radius: R, angle: A, steps: usize) -> &mut Self
|
||||
where
|
||||
R: Into<Precision>,
|
||||
A: Into<Precision>,
|
||||
A: Into<Degrees>,
|
||||
{
|
||||
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,
|
||||
});
|
||||
@ -367,9 +365,9 @@ impl TurtlePlan {
|
||||
/// .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
|
||||
.push(TurtleCommand::SetHeading(-heading.to_radians()));
|
||||
.push(TurtleCommand::SetHeading(-heading.into().as_radians()));
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
);
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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 = {}",
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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<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 {
|
||||
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<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;
|
||||
|
||||
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<Precision> 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<f32> 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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user