angulus/
unbounded.rs

1use core::fmt::Debug;
2use core::iter::Sum;
3use core::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign};
4
5use crate::float::Float;
6use crate::macros::{forward_ref_binop, forward_ref_op_assign, forward_ref_unop};
7use crate::Angle;
8
9/// Represents a point on the circle as a unit-agnostic angle.
10///
11/// The parameter `F` is the floating-point type used to store the value.
12///
13/// # Behaviour
14///
15/// Unlike [`Angle`], two different values of the same point on the circle are
16/// two different [`AngleUnbounded`].
17///
18/// ```
19/// # use angulus::AngleUnbounded;
20/// let a = AngleUnbounded::from_degrees(90.0);
21/// let b = AngleUnbounded::from_degrees(450.0);
22///
23/// assert_ne!(a, b);
24/// ```
25
26#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Hash)]
27#[repr(transparent)]
28pub struct AngleUnbounded<F> {
29    radians: F,
30}
31
32//-------------------------------------------------------------------
33// Const
34//-------------------------------------------------------------------
35
36impl<F: Float> AngleUnbounded<F> {
37    /// The angle of value zero.
38    pub const ZERO: Self = AngleUnbounded::from_radians(F::ZERO);
39
40    /// [Machine epsilon] value for [`AngleUnbounded`].
41    ///
42    /// [Machine epsilon]: https://en.wikipedia.org/wiki/Machine_epsilon
43    pub const EPSILON: Self = AngleUnbounded::from_radians(F::DOUBLE_EPSILON);
44}
45
46impl<F: Float> AngleUnbounded<F> {
47    /// The angle of π radians.
48    pub const RAD_PI: Self = AngleUnbounded::from_radians(F::PI);
49    /// The angle of π/2 radians.
50    pub const RAD_FRAC_PI_2: Self = AngleUnbounded::from_radians(F::FRAC_PI_2);
51    /// The angle of π/3 radians.
52    pub const RAD_FRAC_PI_3: Self = AngleUnbounded::from_radians(F::FRAC_PI_3);
53    /// The angle of π/4 radians.
54    pub const RAD_FRAC_PI_4: Self = AngleUnbounded::from_radians(F::FRAC_PI_4);
55    /// The angle of π/6 radians.
56    pub const RAD_FRAC_PI_6: Self = AngleUnbounded::from_radians(F::FRAC_PI_6);
57    /// The angle of π/8 radians.
58    pub const RAD_FRAC_PI_8: Self = AngleUnbounded::from_radians(F::FRAC_PI_8);
59}
60
61impl<F: Float> AngleUnbounded<F> {
62    /// The angle of 180°.
63    pub const DEG_180: Self = Self::RAD_PI;
64    /// The angle of 90°.
65    pub const DEG_90: Self = Self::RAD_FRAC_PI_2;
66    /// The angle of 60°.
67    pub const DEG_60: Self = Self::RAD_FRAC_PI_3;
68    /// The angle of 45°.
69    pub const DEG_45: Self = Self::RAD_FRAC_PI_4;
70    /// The angle of 30°.
71    pub const DEG_30: Self = Self::RAD_FRAC_PI_6;
72    /// The angle of 22.5°.
73    pub const DEG_22_5: Self = Self::RAD_FRAC_PI_8;
74}
75
76impl<F: Float> AngleUnbounded<F> {
77    /// The angle of a half of a circle (1/2 turns).
78    pub const HALF: Self = Self::RAD_PI;
79    /// The angle of a quarter of a circle (1/4 turns).
80    pub const QUARTER: Self = Self::RAD_FRAC_PI_2;
81    /// The angle of a sixth of a circle (1/6 turns).
82    pub const SIXTH: Self = Self::RAD_FRAC_PI_3;
83    ///  The angle of a eighth of a circle (1/8 turns).
84    pub const EIGHTH: Self = Self::RAD_FRAC_PI_4;
85    ///  The angle of a twelfth of a circle (1/12 turns).
86    pub const TWELFTH: Self = Self::RAD_FRAC_PI_6;
87    ///  The angle of a sixteenth of a circle (1/16 turns).
88    pub const SIXTEENTH: Self = Self::RAD_FRAC_PI_8;
89}
90
91impl<F: Float> AngleUnbounded<F> {
92    /// The angle of 200g.
93    pub const GRAD_200: Self = Self::RAD_PI;
94    /// The angle of 100g.
95    pub const GRAD_100: Self = Self::RAD_FRAC_PI_2;
96    /// The angle of 66.6g.
97    pub const GRAD_66_6: Self = Self::RAD_FRAC_PI_3;
98    ///  The angle of 50g.
99    pub const GRAD_50: Self = Self::RAD_FRAC_PI_4;
100    ///  The angle of 33.3g.
101    pub const GRAD_33_3: Self = Self::RAD_FRAC_PI_6;
102    ///  The angle of 25g.
103    pub const GRAD_25: Self = Self::RAD_FRAC_PI_8;
104}
105
106//-------------------------------------------------------------------
107// Standard traits
108//-------------------------------------------------------------------
109
110impl<F: Debug> Debug for AngleUnbounded<F> {
111    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
112        f.debug_tuple("AngleUnbounded")
113            .field(&self.radians)
114            .finish()
115    }
116}
117
118impl<F: Float> Default for AngleUnbounded<F> {
119    #[inline]
120    fn default() -> Self {
121        Self::ZERO
122    }
123}
124
125//-------------------------------------------------------------------
126// Ctor
127//-------------------------------------------------------------------
128
129impl<F> AngleUnbounded<F> {
130    /// Creates a new unbounded angle from a value in radians.
131    #[inline]
132    pub const fn from_radians(radians: F) -> Self {
133        Self { radians }
134    }
135}
136
137impl<F: Float> AngleUnbounded<F> {
138    /// Creates a new unbounded angle from a value in degrees.
139    #[inline]
140    pub fn from_degrees(degrees: F) -> Self {
141        Self::from_radians(degrees * F::DEG_TO_RAD)
142    }
143
144    /// Creates a new unbounded angle from a value in turns.
145    #[inline]
146    pub fn from_turns(turns: F) -> Self {
147        Self::from_radians(turns * F::TURNS_TO_RAD)
148    }
149
150    /// Creates a new unbounded angle from a value in gradians.
151    #[inline]
152    pub fn from_gradians(gradians: F) -> Self {
153        Self::from_radians(gradians * F::GRAD_TO_RAD)
154    }
155}
156
157//-------------------------------------------------------------------
158// Getters
159//-------------------------------------------------------------------
160
161impl<F: Copy> AngleUnbounded<F> {
162    /// The value of the unbounded angle in radians.
163    #[must_use = "this returns the result of the operation, without modifying the original"]
164    #[inline]
165    pub const fn to_radians(self) -> F {
166        self.radians
167    }
168}
169
170impl<F: Float> AngleUnbounded<F> {
171    /// The value of the unbounded angle in degrees.
172    #[must_use = "this returns the result of the operation, without modifying the original"]
173    #[inline]
174    pub fn to_degrees(self) -> F {
175        self.radians * F::RAD_TO_DEG
176    }
177
178    /// The value of the unbounded angle in turns.
179    #[must_use = "this returns the result of the operation, without modifying the original"]
180    #[inline]
181    pub fn to_turns(self) -> F {
182        self.radians * F::RAD_TO_TURNS
183    }
184
185    /// The value of the unbounded angle in gradians.
186    #[must_use = "this returns the result of the operation, without modifying the original"]
187    #[inline]
188    pub fn to_gradians(self) -> F {
189        self.radians * F::RAD_TO_GRAD
190    }
191}
192
193//-------------------------------------------------------------------
194// MainAngle conversion
195//-------------------------------------------------------------------
196
197impl<F: Float> AngleUnbounded<F> {
198    /// Converts this angle into a bounded angle.
199    #[must_use = "this returns the result of the operation, without modifying the original"]
200    #[inline]
201    pub fn to_bounded(self) -> Angle<F> {
202        Angle::from_radians(self.radians)
203    }
204}
205
206impl<F: Copy> From<Angle<F>> for AngleUnbounded<F> {
207    #[inline]
208    fn from(angle: Angle<F>) -> Self {
209        Self::from_radians(angle.to_radians())
210    }
211}
212
213//-------------------------------------------------------------------
214// Floating point type conversion
215//-------------------------------------------------------------------
216
217impl AngleUnbounded<f32> {
218    /// Converts the floating point type to [`f64`].
219    #[must_use = "this returns the result of the operation, without modifying the original"]
220    #[inline]
221    pub fn to_f64(self) -> AngleUnbounded<f64> {
222        let radians = f64::from(self.radians);
223        AngleUnbounded::from_radians(radians)
224    }
225}
226
227impl AngleUnbounded<f64> {
228    /// Converts the floating point type to [`f32`].
229    #[must_use = "this returns the result of the operation, without modifying the original"]
230    #[inline]
231    pub fn to_f32(self) -> AngleUnbounded<f32> {
232        #[allow(clippy::cast_possible_truncation)]
233        let radians = self.radians as f32;
234        AngleUnbounded::from_radians(radians)
235    }
236}
237
238impl From<AngleUnbounded<f64>> for AngleUnbounded<f32> {
239    #[inline]
240    fn from(value: AngleUnbounded<f64>) -> Self {
241        value.to_f32()
242    }
243}
244
245impl From<AngleUnbounded<f32>> for AngleUnbounded<f64> {
246    #[inline]
247    fn from(value: AngleUnbounded<f32>) -> Self {
248        value.to_f64()
249    }
250}
251
252//-------------------------------------------------------------------
253// Maths
254//-------------------------------------------------------------------
255
256#[cfg(any(feature = "std", feature = "libm"))]
257impl<F: crate::float::FloatMath> AngleUnbounded<F> {
258    /// Computes the sine.
259    #[must_use = "this returns the result of the operation, without modifying the original"]
260    #[inline]
261    pub fn sin(self) -> F {
262        self.radians.sin()
263    }
264
265    /// Computes the cosine.
266    #[must_use = "this returns the result of the operation, without modifying the original"]
267    #[inline]
268    pub fn cos(self) -> F {
269        self.radians.cos()
270    }
271
272    /// Computes the tangent.
273    #[must_use = "this returns the result of the operation, without modifying the original"]
274    #[inline]
275    pub fn tan(self) -> F {
276        self.radians.tan()
277    }
278
279    /// Simultaneously computes the sine and cosine. Returns `(sin(x), cos(x))`.
280    #[must_use = "this returns the result of the operation, without modifying the original"]
281    #[inline]
282    pub fn sin_cos(self) -> (F, F) {
283        self.radians.sin_cos()
284    }
285}
286
287//-------------------------------------------------------------------
288// Ops
289//-------------------------------------------------------------------
290
291impl<F: Float> Add for AngleUnbounded<F> {
292    type Output = Self;
293
294    #[inline]
295    fn add(self, rhs: Self) -> Self::Output {
296        Self::from_radians(self.radians + rhs.radians)
297    }
298}
299
300forward_ref_binop!(impl<F: Float> Add, add for AngleUnbounded<F>, AngleUnbounded<F>);
301
302impl<F: Float> AddAssign for AngleUnbounded<F> {
303    #[inline]
304    fn add_assign(&mut self, rhs: Self) {
305        self.radians += rhs.radians;
306    }
307}
308
309forward_ref_op_assign!(impl<F: Float> AddAssign, add_assign for AngleUnbounded<F>, AngleUnbounded<F>);
310
311impl<F: Float> Sub for AngleUnbounded<F> {
312    type Output = Self;
313
314    #[inline]
315    fn sub(self, rhs: Self) -> Self::Output {
316        Self::from_radians(self.radians - rhs.radians)
317    }
318}
319
320forward_ref_binop!(impl<F: Float> Sub, sub for AngleUnbounded<F>, AngleUnbounded<F>);
321
322impl<F: Float> SubAssign for AngleUnbounded<F> {
323    #[inline]
324    fn sub_assign(&mut self, rhs: Self) {
325        self.radians -= rhs.radians;
326    }
327}
328
329forward_ref_op_assign!(impl<F: Float> SubAssign, sub_assign for AngleUnbounded<F>, AngleUnbounded<F>);
330
331impl<F: Float> Mul<F> for AngleUnbounded<F> {
332    type Output = Self;
333
334    #[inline]
335    fn mul(self, rhs: F) -> Self::Output {
336        Self::from_radians(self.radians * rhs)
337    }
338}
339
340forward_ref_binop!(impl<F: Float> Mul, mul for AngleUnbounded<F>, F);
341
342impl Mul<AngleUnbounded<f32>> for f32 {
343    type Output = AngleUnbounded<f32>;
344
345    #[inline]
346    fn mul(self, rhs: AngleUnbounded<f32>) -> Self::Output {
347        rhs * self
348    }
349}
350
351forward_ref_binop!(impl Mul, mul for f32, AngleUnbounded<f32>);
352
353impl Mul<AngleUnbounded<f64>> for f64 {
354    type Output = AngleUnbounded<f64>;
355
356    #[inline]
357    fn mul(self, rhs: AngleUnbounded<f64>) -> Self::Output {
358        rhs * self
359    }
360}
361
362forward_ref_binop!(impl Mul, mul for f64, AngleUnbounded<f64>);
363
364impl<F: Float> MulAssign<F> for AngleUnbounded<F> {
365    #[inline]
366    fn mul_assign(&mut self, rhs: F) {
367        self.radians *= rhs;
368    }
369}
370
371forward_ref_op_assign!(impl<F: Float> MulAssign, mul_assign for AngleUnbounded<F>, F);
372
373impl<F: Float> Div<F> for AngleUnbounded<F> {
374    type Output = Self;
375
376    #[inline]
377    fn div(self, rhs: F) -> Self::Output {
378        Self::from_radians(self.radians / rhs)
379    }
380}
381
382forward_ref_binop!(impl<F: Float> Div, div for AngleUnbounded<F>, F);
383
384impl<F: Float> DivAssign<F> for AngleUnbounded<F> {
385    #[inline]
386    fn div_assign(&mut self, rhs: F) {
387        self.radians /= rhs;
388    }
389}
390
391forward_ref_op_assign!(impl<F: Float> DivAssign, div_assign for AngleUnbounded<F>, F);
392
393impl<F: Float> Neg for AngleUnbounded<F> {
394    type Output = Self;
395
396    #[inline]
397    fn neg(self) -> Self::Output {
398        Self::from_radians(-self.radians)
399    }
400}
401
402forward_ref_unop!(impl<F: Float> Neg, neg for AngleUnbounded<F>);
403
404impl<F: Sum> Sum for AngleUnbounded<F> {
405    fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
406        AngleUnbounded::from_radians(iter.map(|x| x.radians).sum())
407    }
408}
409
410#[cfg(test)]
411mod tests {
412    use float_eq::assert_float_eq;
413
414    use crate::AngleUnbounded32;
415
416    #[test]
417    fn angle_unbounded_sum_is_accurate() {
418        const ANGLES: [f32; 20] = [
419            -1.093_766_9,
420            -2.507_797_2,
421            -1.995_534_5,
422            -0.704_018_65,
423            0.601_837_7,
424            -1.887_757_9,
425            0.630_587_64,
426            -0.860_579_43,
427            2.683_119,
428            0.664_140_76,
429            0.018_360_304,
430            0.041_261_05,
431            2.733_847_6,
432            2.532_730_3,
433            -3.082_243_2,
434            -1.973_592_4,
435            2.883_761_2,
436            0.876_528_8,
437            -1.492_470_1,
438            -1.600_921_4,
439        ];
440
441        let angles = ANGLES.map(AngleUnbounded32::from_radians);
442
443        let sum: AngleUnbounded32 = angles.iter().copied().sum();
444        let add = angles.iter().fold(AngleUnbounded32::ZERO, |a, b| a + b);
445
446        assert_float_eq!(sum.to_radians(), add.to_radians(), abs <= 1e-5);
447    }
448}