firefly_rust/graphics/
angle.rs

1use crate::*;
2use core::f32::consts::{FRAC_PI_2, PI, TAU};
3use core::ops::*;
4
5/// An angle between two vectors.
6///
7/// Used by [`draw_arc`] and [`draw_sector`].
8#[derive(Copy, Clone, PartialEq, PartialOrd, Debug, Default)]
9pub struct Angle(pub(crate) f32);
10
11impl Angle {
12    /// The 360° angle.
13    pub const FULL_CIRCLE: Angle = Angle(TAU);
14    /// The 180° angle.
15    pub const HALF_CIRCLE: Angle = Angle(PI);
16    /// The 90° angle.
17    pub const QUARTER_CIRCLE: Angle = Angle(FRAC_PI_2);
18    /// The 0° angle.
19    pub const ZERO: Angle = Angle(0.);
20
21    /// An angle in degrees where 360.0 is the full circle.
22    #[must_use]
23    pub fn from_degrees(d: f32) -> Self {
24        Self(d * PI / 180.0)
25    }
26
27    /// An angle in radians where [TAU] (doubled [PI]) is the full circle.
28    #[must_use]
29    pub const fn from_radians(r: f32) -> Self {
30        Self(r)
31    }
32
33    /// Convert the angle to an absolute (non-negative) value.
34    #[must_use]
35    pub fn abs(self) -> Self {
36        Self(math::abs(self.0))
37    }
38
39    /// Normalize the angle to less than one full rotation (in the range 0°..360°).
40    #[must_use]
41    pub fn normalize(self) -> Self {
42        Self(math::rem_euclid(self.0, TAU))
43    }
44
45    /// Get the angle value in degrees where 360.0 is the full circle..
46    #[must_use]
47    pub fn to_degrees(self) -> f32 {
48        180. * self.0 / PI
49    }
50
51    /// Get the angle value in radians where [TAU] (doubled [PI]) is the full circle.
52    #[must_use]
53    pub const fn to_radians(self) -> f32 {
54        self.0
55    }
56
57    /// Approximates `sin(x)` of the angle with a maximum error of `0.002`.
58    #[must_use]
59    pub fn sin(&self) -> f32 {
60        math::sin(self.0)
61    }
62
63    /// Approximates `cos(x)` of the angle with a maximum error of `0.002`.
64    #[must_use]
65    pub fn cos(&self) -> f32 {
66        math::cos(self.0)
67    }
68
69    /// Approximates `tan(x)` of the angle with a maximum error of `0.6`.
70    #[must_use]
71    pub fn tan(&self) -> f32 {
72        math::tan(self.0)
73    }
74}
75
76impl Add for Angle {
77    type Output = Self;
78
79    fn add(self, other: Angle) -> Self {
80        Angle(self.0 + other.0)
81    }
82}
83
84impl AddAssign for Angle {
85    fn add_assign(&mut self, other: Angle) {
86        self.0 += other.0;
87    }
88}
89
90impl Sub for Angle {
91    type Output = Self;
92
93    fn sub(self, other: Angle) -> Self {
94        Angle(self.0 - other.0)
95    }
96}
97
98impl SubAssign for Angle {
99    fn sub_assign(&mut self, other: Angle) {
100        self.0 -= other.0;
101    }
102}
103
104impl Neg for Angle {
105    type Output = Self;
106
107    fn neg(self) -> Self {
108        Angle(-self.0)
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn test_from_degrees() {
118        let d = Angle::from_degrees;
119        let r = Angle::from_radians;
120        assert_eq!(d(0.), r(0.));
121        assert_eq!(d(180.), r(PI));
122        assert_eq!(d(360.), r(TAU));
123        assert_eq!(d(90.), r(PI / 2.));
124    }
125
126    #[test]
127    fn test_abs() {
128        let a = Angle::from_degrees;
129        assert_eq!(a(90.), a(90.));
130        assert_ne!(a(-90.), a(90.));
131        assert_eq!(a(-90.).abs(), a(90.));
132    }
133
134    #[test]
135    fn test_normalize() {
136        let a = Angle::from_degrees;
137        assert_eq!(a(90.).normalize(), a(90.));
138        assert_eq!(a(360.).normalize(), a(0.));
139        assert_eq!(a(-90.).normalize(), a(270.));
140    }
141
142    #[test]
143    #[expect(clippy::float_cmp)]
144    fn test_to_degrees() {
145        let a = Angle::from_degrees;
146        assert_eq!(a(47.).to_degrees(), 47.);
147        assert_eq!(a(0.).to_degrees(), 0.);
148        assert_eq!(a(90.).to_degrees(), 90.);
149        assert_eq!(a(370.).to_degrees(), 370.);
150    }
151}