#![cfg_attr(not(feature = "std"), no_std)]
#![deny(clippy::std_instead_of_core)]
#![doc = include_str!("../README.md")]
#[cfg(all(feature = "std", feature = "libm"))]
compile_error!("The `std` and `libm` features may not both be enabled simultaneously.");
use core::{f64::consts::PI, fmt::Display};
mod ops;
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Angle {
value: f64,
}
impl Angle {
pub const ZERO: Self = Self { value: 0.0 };
pub const PI: Self = Self { value: PI };
pub const NEG_PI: Self = Self { value: -PI };
pub fn new(radians: f64) -> Self {
let two_pi = 2.0 * PI;
let mut normalized = radians % two_pi;
if normalized > PI {
normalized -= two_pi;
} else if normalized < -PI {
normalized += two_pi;
}
Angle { value: normalized }
}
pub fn from_degrees(degrees: f64) -> Angle {
Angle::new(degrees.to_radians())
}
#[inline(always)]
pub fn radians(&self) -> f64 {
self.value
}
#[inline(always)]
pub fn degrees(&self) -> f64 {
self.value.to_degrees()
}
#[inline(always)]
pub fn abs(&self) -> Angle {
#[cfg(feature = "std")]
return Angle {
value: self.value.abs(),
};
#[cfg(feature = "libm")]
return Angle {
value: libm::fabs(self.value),
};
#[cfg(all(not(feature = "std"), not(feature = "libm")))]
if self.value.is_sign_positive() {
*self
} else {
-*self
}
}
#[inline(always)]
pub fn difference(&self, other: &Self) -> Self {
Angle::new(other.value - self.value)
}
#[inline(always)]
pub fn distance(&self, other: &Self) -> Self {
Angle::new(self.value - other.value).abs()
}
pub fn is_clockwise_to(&self, other: &Self) -> bool {
self.difference(other).value < 0.0
}
pub fn is_counterclockwise_to(&self, other: &Self) -> bool {
self.difference(other).value > 0.0
}
pub fn clamp(&self, min: Angle, max: Angle) -> Angle {
if self.value < min.value {
min
} else if self.value > max.value {
max
} else {
*self
}
}
pub fn lerp(&self, other: &Self, t: f64) -> Angle {
assert!((0.0..=1.0).contains(&t), "t must be between 0.0 and 1.0");
let difference = self.difference(other);
Angle::new(self.value + difference.value * t)
}
pub fn opposite(&self) -> Angle {
Angle::new(self.value + PI)
}
pub fn midpoint(&self, other: &Self) -> Self {
let diff = self.difference(other).value;
Angle::new(self.value + diff / 2.0)
}
#[cfg(any(feature = "std", feature = "libm"))]
pub fn to_unit_vector(&self) -> (f64, f64) {
#[cfg(feature = "std")]
return (self.value.cos(), self.value.sin());
#[cfg(feature = "libm")]
return (libm::cos(self.value), libm::sin(self.value));
}
#[cfg(any(feature = "std", feature = "libm"))]
pub fn from_unit_vector(x: f64, y: f64) -> Angle {
#[cfg(feature = "std")]
return Angle::new(y.atan2(x));
#[cfg(feature = "libm")]
return Angle::new(libm::atan2(y, x));
}
}
impl Display for Angle {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{} radians", self.value)
}
}
#[cfg(feature = "ufmt")]
impl ufmt::uDisplay for Angle {
fn fmt<W>(&self, f: &mut ufmt::Formatter<'_, W>) -> Result<(), W::Error>
where
W: ufmt::uWrite + ?Sized,
{
ufmt::uwrite!(f, "{} radians", ufmt_float::uFmt_f64::Five(self.value))
}
}
#[cfg(test)]
mod tests {
use super::Angle;
use core::f64::consts::PI;
#[test]
fn test_in_range() {
assert_eq!(Angle::new(PI).radians(), PI);
assert_eq!(Angle::new(-PI).radians(), -PI);
assert_eq!(Angle::new(PI / 2.0).radians(), PI / 2.0);
assert_eq!(Angle::new(-PI / 2.0).radians(), -PI / 2.0);
assert_eq!(Angle::new(0.0).radians(), 0.0);
}
#[test]
fn test_out_of_range() {
assert_eq!(Angle::new(PI + 0.1).radians(), -PI + 0.1);
assert_eq!(Angle::new(-PI - 0.1).radians(), PI - 0.1);
}
#[test]
fn test_math_ops() {
assert_eq!((Angle::PI - Angle::PI / 2.0).radians(), PI / 2.0);
assert_eq!((Angle::PI * 2.0).radians(), 0.0);
}
}