radian/lib.rs
1#![cfg_attr(not(feature = "std"), no_std)]
2#![deny(clippy::std_instead_of_core)]
3#![doc = include_str!("../README.md")]
4
5#[cfg(all(feature = "std", feature = "libm"))]
6compile_error!("The `std` and `libm` features may not both be enabled simultaneously.");
7
8use core::{f64::consts::PI, fmt::Display};
9
10#[cfg(feature = "serde")]
11use serde::{Deserialize, Serialize};
12
13mod ops;
14
15/// A wrapper around a `f64` angle that is guaranteed to always be normalized.
16#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
17#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
18pub struct Angle {
19 value: f64,
20}
21
22impl Angle {
23 pub const ZERO: Self = Self { value: 0.0 };
24 pub const PI: Self = Self { value: PI };
25 pub const NEG_PI: Self = Self { value: -PI };
26
27 /// Create a new [`Angle`] by normalizing the given angle in radians.
28 ///
29 /// ```
30 /// # use radian::Angle;
31 /// # use core::f64::consts::PI;
32 /// let angle = Angle::new(PI * 5.0);
33 /// assert_eq!(angle.radians(), PI);
34 /// ```
35 pub fn new(radians: f64) -> Self {
36 let two_pi = 2.0 * PI;
37 let mut normalized = radians % two_pi;
38 if normalized > PI {
39 normalized -= two_pi;
40 } else if normalized < -PI {
41 normalized += two_pi;
42 }
43 Angle { value: normalized }
44 }
45
46 /// Create a new [`Angle`] from degrees.
47 ///
48 /// ```
49 /// # use radian::Angle;
50 /// # use core::f64::consts::PI;
51 /// let angle = Angle::from_degrees(180.0);
52 /// assert_eq!(angle.radians(), PI);
53 /// ```
54 pub fn from_degrees(degrees: f64) -> Angle {
55 Angle::new(degrees.to_radians())
56 }
57
58 /// Get the angle in radians.
59 ///
60 /// ```
61 /// # use radian::Angle;
62 /// # use core::f64::consts::PI;
63 /// let angle = Angle::new(PI);
64 /// assert_eq!(angle.radians(), PI);
65 /// ```
66 #[inline(always)]
67 pub fn radians(&self) -> f64 {
68 self.value
69 }
70
71 /// Get the angle in degrees.
72 ///
73 /// ```
74 /// # use radian::Angle;
75 /// # use core::f64::consts::PI;
76 /// let angle = Angle::new(PI);
77 /// assert_eq!(angle.degrees(), 180.0);
78 /// ```
79 #[inline(always)]
80 pub fn degrees(&self) -> f64 {
81 self.value.to_degrees()
82 }
83
84 /// Get the absolute of this angle.
85 ///
86 /// ```
87 /// # use radian::Angle;
88 /// let angle = Angle::new(-1.0);
89 /// assert_eq!(angle.abs().radians(), 1.0);
90 /// ```
91 #[inline(always)]
92 pub fn abs(&self) -> Angle {
93 #[cfg(feature = "std")]
94 return Angle {
95 value: self.value.abs(),
96 };
97
98 #[cfg(feature = "libm")]
99 return Angle {
100 value: libm::fabs(self.value),
101 };
102
103 #[cfg(all(not(feature = "std"), not(feature = "libm")))]
104 if self.value.is_sign_positive() {
105 *self
106 } else {
107 -*self
108 }
109 }
110
111 /// Get the difference between this angle and another one.
112 ///
113 /// ```
114 /// # use radian::Angle;
115 /// # use core::f64::consts::PI;
116 /// let a = Angle::new(-PI / 2.0);
117 /// let b = Angle::new(PI / 2.0);
118 /// assert_eq!(a.difference(&b).radians(), PI);
119 /// ```
120 #[inline(always)]
121 pub fn difference(&self, other: &Self) -> Self {
122 Angle::new(other.value - self.value)
123 }
124
125 /// Get the absolute distance between this angle and another one.
126 ///
127 /// ```
128 /// # use radian::Angle;
129 /// # use core::f64::consts::PI;
130 /// let a = Angle::new(-PI);
131 /// let b = Angle::new(PI / 2.0);
132 /// assert_eq!(a.distance(&b).radians(), PI / 2.0);
133 /// ```
134 #[inline(always)]
135 pub fn distance(&self, other: &Self) -> Self {
136 Angle::new(self.value - other.value).abs()
137 }
138
139 /// Check if this angle is in a clockwise driection from another one.
140 ///
141 /// ```
142 /// # use radian::Angle;
143 /// # use core::f64::consts::PI;
144 /// let a = Angle::new(PI);
145 /// let b = Angle::new(PI / 2.0);
146 /// assert!(a.is_clockwise_to(&b));
147 /// ```
148 pub fn is_clockwise_to(&self, other: &Self) -> bool {
149 self.difference(other).value < 0.0
150 }
151
152 /// Check if this angle is in a counterclockwise driection from another one.
153 ///
154 /// ```
155 /// # use radian::Angle;
156 /// # use core::f64::consts::PI;
157 /// let a = Angle::new(PI / 2.0);
158 /// let b = Angle::new(PI);
159 /// assert!(a.is_counterclockwise_to(&b));
160 /// ```
161 pub fn is_counterclockwise_to(&self, other: &Self) -> bool {
162 self.difference(other).value > 0.0
163 }
164
165 /// Clamp the angle to a given range.
166 ///
167 /// ```
168 /// # use radian::Angle;
169 /// # use core::f64::consts::PI;
170 /// let angle = Angle::new(PI);
171 /// let clamped = angle.clamp(Angle::ZERO, Angle::new(PI / 2.0));
172 /// assert_eq!(clamped.radians(), PI / 2.0);
173 /// ```
174 pub fn clamp(&self, min: Angle, max: Angle) -> Angle {
175 if self.value < min.value {
176 min
177 } else if self.value > max.value {
178 max
179 } else {
180 *self
181 }
182 }
183
184 /// Interpolate between this angle and another by a given factor (t).
185 ///
186 /// ```
187 /// # use radian::Angle;
188 /// # use core::f64::consts::PI;
189 /// let a = Angle::new(0.0);
190 /// let b = Angle::new(PI);
191 /// let interpolated = a.lerp(&b, 0.5);
192 /// assert_eq!(interpolated.radians(), PI / 2.0);
193 /// ```
194 pub fn lerp(&self, other: &Self, t: f64) -> Angle {
195 assert!((0.0..=1.0).contains(&t), "t must be between 0.0 and 1.0");
196 let difference = self.difference(other);
197 Angle::new(self.value + difference.value * t)
198 }
199
200 /// Get the opposite angle (add π).
201 ///
202 /// ```
203 /// # use radian::Angle;
204 /// # use core::f64::consts::PI;
205 /// let angle = Angle::new(-PI / 2.0);
206 /// assert_eq!(angle.opposite().radians(), PI / 2.0);
207 /// ```
208 pub fn opposite(&self) -> Angle {
209 Angle::new(self.value + PI)
210 }
211
212 /// Find the midpoint angle between this angle and another one.
213 ///
214 /// ```
215 /// # use radian::Angle;
216 /// # use core::f64::consts::PI;
217 /// let a = Angle::new(0.0);
218 /// let b = Angle::new(PI);
219 /// assert_eq!(a.midpoint(&b).radians(), PI / 2.0);
220 /// ```
221 pub fn midpoint(&self, other: &Self) -> Self {
222 let diff = self.difference(other).value;
223 Angle::new(self.value + diff / 2.0)
224 }
225
226 /// Get the unit vector of this angle defined as (cos(θ), sin(θ)).
227 ///
228 /// ```
229 /// # use radian::Angle;
230 /// # use core::f64::consts::{PI, SQRT_2};
231 /// # use approx::assert_relative_eq;
232 /// let angle = Angle::new(PI / 4.0);
233 /// let (x, y) = angle.to_unit_vector();
234 /// assert_relative_eq!(x, SQRT_2 / 2.0);
235 /// assert_relative_eq!(y, SQRT_2 / 2.0);
236 /// ```
237 #[cfg(any(feature = "std", feature = "libm"))]
238 pub fn to_unit_vector(&self) -> (f64, f64) {
239 #[cfg(feature = "std")]
240 return (self.value.cos(), self.value.sin());
241 #[cfg(feature = "libm")]
242 return (libm::cos(self.value), libm::sin(self.value));
243 }
244
245 /// Create an angle from a unit vector (x, y).
246 ///
247 /// ```
248 /// # use radian::Angle;
249 /// # use core::f64::consts::{PI, SQRT_2};
250 /// # use approx::assert_relative_eq;
251 /// let angle = Angle::from_unit_vector(SQRT_2 / 2.0, SQRT_2 / 2.0);
252 /// assert_relative_eq!(angle.radians(), PI / 4.0);
253 /// ```
254 #[cfg(any(feature = "std", feature = "libm"))]
255 pub fn from_unit_vector(x: f64, y: f64) -> Angle {
256 #[cfg(feature = "std")]
257 return Angle { value: y.atan2(x) };
258 #[cfg(feature = "libm")]
259 return Angle {
260 value: libm::atan2(y, x),
261 };
262 }
263}
264
265impl Display for Angle {
266 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
267 write!(f, "{} radians", self.value)
268 }
269}
270
271#[cfg(feature = "ufmt")]
272impl ufmt::uDisplay for Angle {
273 fn fmt<W>(&self, f: &mut ufmt::Formatter<'_, W>) -> Result<(), W::Error>
274 where
275 W: ufmt::uWrite + ?Sized,
276 {
277 ufmt::uwrite!(f, "{} radians", ufloat::Uf64(self.value, 4))
278 }
279}
280
281#[cfg(test)]
282mod tests {
283 use super::Angle;
284 use core::f64::consts::PI;
285 use proptest::prelude::*;
286
287 #[test]
288 fn test_in_range() {
289 assert_eq!(Angle::new(PI).radians(), PI);
290 assert_eq!(Angle::new(-PI).radians(), -PI);
291 assert_eq!(Angle::new(PI / 2.0).radians(), PI / 2.0);
292 assert_eq!(Angle::new(-PI / 2.0).radians(), -PI / 2.0);
293 assert_eq!(Angle::new(0.0).radians(), 0.0);
294 }
295
296 #[test]
297 fn test_out_of_range() {
298 assert_eq!(Angle::new(PI + 0.1).radians(), -PI + 0.1);
299 assert_eq!(Angle::new(-PI - 0.1).radians(), PI - 0.1);
300 }
301
302 #[test]
303 fn test_math_ops() {
304 assert_eq!((Angle::PI - Angle::PI / 2.0).radians(), PI / 2.0);
305 assert_eq!((Angle::PI * 2.0).radians(), 0.0);
306 }
307
308 proptest! {
309 #[test]
310 fn prop_test_in_range(angle in -PI..=PI) {
311 assert_eq!(Angle::new(angle).radians(), angle);
312 }
313
314 #[test]
315 fn prop_test_above_range(angle in (PI + f64::EPSILON)..) {
316 assert!(Angle::new(angle).radians() < angle);
317 }
318
319 #[test]
320 fn prop_test_below_range(angle in ..PI) {
321 assert!(Angle::new(angle).radians() > angle);
322 }
323
324 #[test]
325 #[cfg(any(feature = "std", feature = "libm"))]
326 fn prop_test_from_unit_vector(x in -1000.0..1000.0, y in -1000.0..1000.0) {
327 let angle = Angle::from_unit_vector(x, y);
328 assert!(-PI <= angle.radians());
329 assert!(angle.radians() <= PI);
330 }
331 }
332}