Skip to main content

game_gem/math/
angle.rs

1//! Type-safe angle wrapper to prevent radian/degree confusion.
2
3/// A type-safe angle that stores radians internally but provides
4/// construction and conversion in both radians and degrees.
5#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
6pub struct Angle(f32);
7
8impl Angle {
9    /// Create from radians.
10    #[inline]
11    pub const fn from_radians(rad: f32) -> Self {
12        Angle(rad)
13    }
14
15    /// Create from degrees.
16    #[inline]
17    pub fn from_degrees(deg: f32) -> Self {
18        Angle(deg.to_radians())
19    }
20
21    /// Get the angle in radians.
22    #[inline]
23    pub fn as_radians(self) -> f32 {
24        self.0
25    }
26
27    /// Get the angle in degrees.
28    #[inline]
29    pub fn as_degrees(self) -> f32 {
30        self.0.to_degrees()
31    }
32
33    /// Zero angle.
34    pub const fn zero() -> Self {
35        Angle(0.0)
36    }
37
38    /// Normalize to the range (-π, π].
39    pub fn normalized(self) -> Self {
40        let mut a = self.0 % (2.0 * std::f32::consts::PI);
41        if a > std::f32::consts::PI {
42            a -= 2.0 * std::f32::consts::PI;
43        } else if a < -std::f32::consts::PI {
44            a += 2.0 * std::f32::consts::PI;
45        }
46        Angle(a)
47    }
48
49    /// Sine of the angle.
50    #[inline]
51    pub fn sin(self) -> f32 {
52        self.0.sin()
53    }
54
55    /// Cosine of the angle.
56    #[inline]
57    pub fn cos(self) -> f32 {
58        self.0.cos()
59    }
60
61    /// Both sin and cos, returned as a tuple (sin, cos).
62    #[inline]
63    pub fn sin_cos(self) -> (f32, f32) {
64        self.0.sin_cos()
65    }
66
67    /// Tangent of the angle.
68    #[inline]
69    pub fn tan(self) -> f32 {
70        self.0.tan()
71    }
72}
73
74impl std::ops::Add for Angle {
75    type Output = Self;
76    #[inline]
77    fn add(self, rhs: Self) -> Self {
78        Angle(self.0 + rhs.0)
79    }
80}
81
82impl std::ops::Sub for Angle {
83    type Output = Self;
84    #[inline]
85    fn sub(self, rhs: Self) -> Self {
86        Angle(self.0 - rhs.0)
87    }
88}
89
90impl std::ops::Mul<f32> for Angle {
91    type Output = Self;
92    #[inline]
93    fn mul(self, rhs: f32) -> Self {
94        Angle(self.0 * rhs)
95    }
96}
97
98impl std::ops::Div<f32> for Angle {
99    type Output = Self;
100    #[inline]
101    fn div(self, rhs: f32) -> Self {
102        Angle(self.0 / rhs)
103    }
104}
105
106impl std::ops::Neg for Angle {
107    type Output = Self;
108    #[inline]
109    fn neg(self) -> Self {
110        Angle(-self.0)
111    }
112}
113
114impl std::ops::AddAssign for Angle {
115    fn add_assign(&mut self, rhs: Self) {
116        self.0 += rhs.0;
117    }
118}
119
120impl std::ops::SubAssign for Angle {
121    fn sub_assign(&mut self, rhs: Self) {
122        self.0 -= rhs.0;
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn test_degrees_to_radians() {
132        let a = Angle::from_degrees(180.0);
133        assert!((a.as_radians() - std::f32::consts::PI).abs() < 1e-6);
134    }
135
136    #[test]
137    fn test_normalize() {
138        let a = Angle::from_degrees(450.0).normalized();
139        // Use a tolerance that accommodates f32 round-trip error from
140        // degrees -> radians -> modulo -> degrees.
141        assert!((a.as_degrees() - 90.0).abs() < 1e-4);
142    }
143}