batbox_la/
angle.rs

1use super::*;
2
3/// This struct represents an angle in 2d space,
4#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
5#[serde(transparent)]
6pub struct Angle<T: Float = f32> {
7    radians: T,
8}
9
10impl<T: Float> rand::distributions::Distribution<Angle<T>> for rand::distributions::Standard {
11    fn sample<R: rand::Rng + ?Sized>(&self, rng: &mut R) -> Angle<T> {
12        Angle::from_radians(rng.gen_range(T::ZERO..T::from_f32(2.0 * f32::PI)))
13    }
14}
15
16impl<T: Float> Angle<T> {
17    /// 0 angle is pointing to positive x axis
18    pub const ZERO: Self = Self { radians: T::ZERO };
19
20    /// Map inner value to a different type
21    pub fn map<U: Float>(&self, f: impl Fn(T) -> U) -> Angle<U> {
22        Angle {
23            radians: f(self.radians),
24        }
25    }
26
27    /// Computes the arccosine of a number as an angle.
28    ///
29    /// Return value is in radians in the range [0, pi] or NaN if the number is outside the range [-1, 1].
30    pub fn acos(cos: T) -> Self {
31        Self {
32            radians: cos.acos(),
33        }
34    }
35
36    /// Computes the arcsine of a number as an angle.
37    ///
38    /// Return value is in radians in the range [-pi/2, pi/2] or NaN if the number is outside the range [-1, 1].
39    pub fn asin(sin: T) -> Self {
40        Self {
41            radians: sin.asin(),
42        }
43    }
44
45    /// Computes the arctangent of a number as an angle.
46    ///
47    /// Return value is in radians in the range [-pi/2, pi/2];
48    pub fn atan(tan: T) -> Self {
49        Self {
50            radians: tan.atan(),
51        }
52    }
53
54    /// Computes the four quadrant arctangent of `self` (`y`) and `other` (`x`) as an angle.
55    ///
56    /// * `x = 0`, `y = 0`: `0`
57    /// * `x >= 0`: `arctan(y/x)` -> `[-pi/2, pi/2]`
58    /// * `y >= 0`: `arctan(y/x) + pi` -> `(pi/2, pi]`
59    /// * `y < 0`: `arctan(y/x) - pi` -> `(-pi, -pi/2)`
60    pub fn atan2(y: T, x: T) -> Self {
61        Self {
62            radians: T::atan2(y, x),
63        }
64    }
65
66    /// Compute the sine
67    pub fn sin(&self) -> T {
68        self.radians.sin()
69    }
70
71    /// Compute the cosine
72    pub fn cos(&self) -> T {
73        self.radians.cos()
74    }
75
76    /// Simultaneously computes the sine and cosine of the angle.
77    /// Returns `(sin(self), cos(self))`.
78    pub fn sin_cos(&self) -> (T, T) {
79        self.radians.sin_cos()
80    }
81
82    /// Computes the tangent of the angle.
83    pub fn tan(self) -> T {
84        self.radians.tan()
85    }
86
87    /// Create angle from value in radians
88    pub fn from_radians(radians: T) -> Self {
89        Self { radians }
90    }
91
92    /// Create angle from value in degrees
93    pub fn from_degrees(degrees: T) -> Self {
94        Self {
95            radians: degrees_to_radians(degrees),
96        }
97    }
98
99    /// See angle value as radians
100    pub fn as_radians(&self) -> T {
101        self.radians
102    }
103
104    /// See angle value as degrees
105    pub fn as_degrees(&self) -> T {
106        radians_to_degrees(self.radians)
107    }
108
109    /// Normalize the angle to be in range `0..2*pi`.
110    pub fn normalized_2pi(&self) -> Self {
111        let tau = T::PI + T::PI;
112        let mut norm = (self.radians / tau).fract();
113        if norm < T::ZERO {
114            norm += T::ONE;
115        }
116        Self {
117            radians: norm * tau,
118        }
119    }
120
121    /// Calculate absolute value
122    pub fn abs(&self) -> Self {
123        Self {
124            radians: self.radians.abs(),
125        }
126    }
127
128    /// Normalize the angle to be in range `-pi..pi`.
129    pub fn normalized_pi(&self) -> Self {
130        let pi = T::PI;
131        let mut angle = self.normalized_2pi().radians;
132        if angle > pi {
133            angle -= pi + pi;
134        }
135        Self { radians: angle }
136    }
137
138    /// Calculates the angle between `from` and `self` in range `-pi..pi`.
139    pub fn angle_from(&self, from: Self) -> Self {
140        from.angle_to(*self)
141    }
142
143    /// Calculates the angle between `self` and `target` in range `-pi..pi`.
144    pub fn angle_to(&self, target: Self) -> Self {
145        let pi = T::PI;
146        let mut delta = target.normalized_2pi().radians - self.normalized_2pi().radians;
147        if delta.abs() > pi {
148            delta -= (pi + pi) * delta.signum();
149        }
150        Self { radians: delta }
151    }
152
153    /// Returns a direction vector of unit length.
154    pub fn unit_vec(&self) -> vec2<T> {
155        let (sin, cos) = self.radians.sin_cos();
156        vec2(cos, sin)
157    }
158}
159
160fn degrees_to_radians<T: Float>(degrees: T) -> T {
161    degrees / T::from_f32(180.0) * T::PI
162}
163
164fn radians_to_degrees<T: Float>(radians: T) -> T {
165    radians / T::PI * T::from_f32(180.0)
166}
167
168impl<T: Float> Mul<T> for Angle<T> {
169    type Output = Self;
170    fn mul(self, rhs: T) -> Self::Output {
171        Self {
172            radians: self.radians * rhs,
173        }
174    }
175}
176
177impl<T: Float> Div<T> for Angle<T> {
178    type Output = Self;
179    fn div(self, rhs: T) -> Self::Output {
180        Self {
181            radians: self.radians / rhs,
182        }
183    }
184}
185
186impl<T: Float> Add for Angle<T> {
187    type Output = Self;
188    fn add(self, rhs: Self) -> Self::Output {
189        Self {
190            radians: self.radians + rhs.radians,
191        }
192    }
193}
194
195impl<T: Float> AddAssign for Angle<T> {
196    fn add_assign(&mut self, rhs: Self) {
197        *self = self.add(rhs);
198    }
199}
200
201impl<T: Float> Sub for Angle<T> {
202    type Output = Self;
203    fn sub(self, rhs: Self) -> Self::Output {
204        Self {
205            radians: self.radians - rhs.radians,
206        }
207    }
208}
209
210impl<T: Float> SubAssign for Angle<T> {
211    fn sub_assign(&mut self, rhs: Self) {
212        *self = self.sub(rhs);
213    }
214}
215
216impl<T: Float> Neg for Angle<T> {
217    type Output = Self;
218    fn neg(self) -> Self::Output {
219        Self {
220            radians: -self.radians,
221        }
222    }
223}
224
225#[test]
226fn test_angle_conversion() {
227    const EPSILON: f32 = 1e-3;
228    let tests = [
229        (0.0, 0.0),
230        (90.0, f32::PI / 2.0),
231        (180.0, f32::PI),
232        (270.0, f32::PI * 3.0 / 2.0),
233        (360.0, f32::PI * 2.0),
234    ];
235    for (degrees, radians) in tests {
236        let d = Angle::from_degrees(degrees).as_radians();
237        let r = Angle::from_radians(radians).as_radians();
238        let delta = r - d;
239        assert!(
240            delta.abs() < EPSILON,
241            "{degrees} degrees expected to be converted to {radians} radians, found {d}"
242        )
243    }
244}
245
246#[test]
247fn test_angle_normalize_2pi() {
248    const EPSILON: f32 = 1e-3;
249    let tests = [0.0, f32::PI, f32::PI / 2.0, f32::PI * 3.0 / 2.0];
250    for test in tests {
251        for offset in [0, 1, -1, 2, -2] {
252            let angle = test + f32::PI * 2.0 * offset as f32;
253            let norm = Angle::from_radians(angle).normalized_2pi().as_radians();
254            let delta = test - norm;
255            assert!(
256                delta.abs() < EPSILON,
257                "Normalized {angle} expected to be {test}, found {norm}"
258            );
259        }
260    }
261}
262
263#[test]
264fn test_angle_delta() {
265    const EPSILON: f32 = 1e-3;
266    let tests = [
267        (0.0, f32::PI / 2.0, f32::PI / 2.0),
268        (0.0, f32::PI * 3.0 / 2.0, -f32::PI / 2.0),
269    ];
270    for (from, to, test) in tests {
271        for offset_from in [0, 1, -1, 2, -2] {
272            for offset_to in [0, 1, -1, 2, -2] {
273                for offset in [0.0, 1.0, -1.0, 2.0, -2.0] {
274                    let from = from + f32::PI * 2.0 * offset_from as f32 + offset;
275                    let to = to + f32::PI * 2.0 * offset_to as f32 + offset;
276                    let angle = Angle::from_radians(from)
277                        .angle_to(Angle::from_radians(to))
278                        .as_radians();
279                    let delta = test - angle;
280                    assert!(
281                        delta.abs() < EPSILON,
282                        "Angle from {from} to {to} expected to be {test}, found {angle}"
283                    );
284                }
285            }
286        }
287    }
288}