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
|
//! 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,
|
||||||
});
|
});
|
||||||
@ -242,14 +240,14 @@ impl TurtlePlan {
|
|||||||
/// async fn main() {
|
/// async fn main() {
|
||||||
/// let mut turtle = TurtlePlan::new();
|
/// let mut turtle = TurtlePlan::new();
|
||||||
/// turtle.forward(100.0).right(90.0).forward(100.0);
|
/// turtle.forward(100.0).right(90.0).forward(100.0);
|
||||||
///
|
///
|
||||||
/// let mut app = TurtleApp::new().with_commands(turtle.build());
|
/// let mut app = TurtleApp::new().with_commands(turtle.build());
|
||||||
///
|
///
|
||||||
/// loop {
|
/// loop {
|
||||||
/// clear_background(WHITE);
|
/// clear_background(WHITE);
|
||||||
/// app.update();
|
/// app.update();
|
||||||
/// app.render();
|
/// app.render();
|
||||||
///
|
///
|
||||||
/// if is_key_pressed(KeyCode::Escape) || is_key_pressed(KeyCode::Q) {
|
/// if is_key_pressed(KeyCode::Escape) || is_key_pressed(KeyCode::Q) {
|
||||||
/// break;
|
/// break;
|
||||||
/// }
|
/// }
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -699,10 +697,10 @@ impl TurtlePlan {
|
|||||||
/// fn draw(turtle: &mut TurtlePlan) {
|
/// fn draw(turtle: &mut TurtlePlan) {
|
||||||
/// // Draw something
|
/// // Draw something
|
||||||
/// turtle.forward(100.0);
|
/// turtle.forward(100.0);
|
||||||
///
|
///
|
||||||
/// // Reset everything back to default
|
/// // Reset everything back to default
|
||||||
/// turtle.reset();
|
/// turtle.reset();
|
||||||
///
|
///
|
||||||
/// // Start fresh
|
/// // Start fresh
|
||||||
/// turtle.forward(50.0);
|
/// turtle.forward(50.0);
|
||||||
/// }
|
/// }
|
||||||
|
|||||||
@ -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,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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) => {
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
@ -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 = {}",
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user