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
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
}

View File

@ -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,
);

View File

@ -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) => {

View File

@ -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,

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
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),

View File

@ -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 = {}",

View File

@ -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;

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 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));
}
}

View File

@ -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;

View File

@ -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)

View File

@ -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);