more consistent use of angle types

This commit is contained in:
2026-05-21 15:02:57 +02:00
parent 3c076fdd03
commit 6b558ca8a0
11 changed files with 222 additions and 243 deletions
+131 -174
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));
}
}