use serde::{Deserialize, Serialize};
use std::{f64::consts::PI, fmt};
use approxim::approx_derive::RelativeEq;
use rand::{
Rng,
distr::{Distribution, StandardUniform, Uniform},
};
use crate::{Cartesian, Rotate, Rotation, RotationMatrix};
#[derive(Clone, Copy, Debug, Default, PartialEq, RelativeEq, Serialize, Deserialize)]
pub struct Angle {
pub theta: f64,
}
impl Angle {
#[inline]
#[must_use]
pub fn to_reduced(self) -> Self {
Self {
theta: self.theta.rem_euclid(2.0 * PI),
}
}
}
impl From<Angle> for RotationMatrix<2> {
#[inline]
fn from(angle: Angle) -> RotationMatrix<2> {
let sin_theta = angle.theta.sin();
let cos_theta = angle.theta.cos();
RotationMatrix {
rows: [
[cos_theta, -sin_theta].into(),
[sin_theta, cos_theta].into(),
],
}
}
}
impl From<f64> for Angle {
#[inline]
fn from(theta: f64) -> Self {
Self { theta }
}
}
impl Rotate<Cartesian<2>> for Angle {
type Matrix = RotationMatrix<2>;
#[inline]
fn rotate(&self, vector: &Cartesian<2>) -> Cartesian<2> {
let sin_theta = self.theta.sin();
let cos_theta = self.theta.cos();
Cartesian::from([
vector.coordinates[0] * cos_theta - vector.coordinates[1] * sin_theta,
vector.coordinates[0] * sin_theta + vector.coordinates[1] * cos_theta,
])
}
}
impl Rotation for Angle {
#[inline]
fn identity() -> Self {
Self::default()
}
#[inline]
fn inverted(self) -> Self {
Self::from(-self.theta)
}
#[inline]
fn combine(&self, other: &Self) -> Self {
Self::from(self.theta + other.theta)
}
}
impl fmt::Display for Angle {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "<{}>", self.theta)
}
}
impl Distribution<Angle> for StandardUniform {
#[inline]
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Angle {
let uniform = Uniform::new(0.0, 2.0 * PI).expect("hard-coded distribution should be valid");
Angle::from(uniform.sample(rng))
}
}
#[cfg(test)]
mod tests {
use super::*;
use approxim::assert_relative_eq;
use rand::{RngExt, SeedableRng, rngs::StdRng};
use rstest::*;
use std::f64::consts::PI;
#[rstest]
#[case::pi_halves(PI / 2.0, (1.0, -0.5), (0.5, 1.0))]
#[case::negative_pi_thirds(-PI / 3.0, (1.0, 0.0), (0.5, -f64::sqrt(3.0) / 2.0))]
#[case::negative_pi(-PI, (3.1, -0.2), (-3.1, 0.2))]
#[case::two_pi(PI*2.0, (3.1, -0.2), (3.1, -0.2))]
#[case::zero(0.0, (3.1, -0.2), (3.1, -0.2))]
#[case::negative_zero(-0.0, (3.1, -0.2), (3.1, -0.2))]
fn rotate_2d(#[case] angle: f64, #[case] vec: (f64, f64), #[case] ans: (f64, f64)) {
let angle = Angle::from(angle);
let vec = Cartesian::from(vec);
let ans = Cartesian::from(ans);
assert_relative_eq!(angle.rotate(&vec), ans, epsilon = 4.0 * f64::EPSILON);
assert_relative_eq!(
RotationMatrix::from(angle).rotate(&vec),
ans,
epsilon = 4.0 * f64::EPSILON
);
}
#[rstest(
ang1 => [0.0, PI / 2.0, 1e-12 * PI, -3.0, 12345.6],
ang2 => [-0.0, -PI / 3.0, PI, 2.0 * PI]
)]
fn combine_2d(ang1: f64, ang2: f64) {
let (angle1, angle2) = (Angle::from(ang1), Angle::from(ang2));
assert_relative_eq!(angle1.combine(&angle2).theta, ang1 + ang2);
}
#[test]
fn default() {
let a = Angle::default();
assert!(a.theta == 0.0);
}
#[test]
fn identity() {
let a = Angle::identity();
assert!(a.theta == 0.0);
}
#[rstest(theta => [0.0, 1.0, 2.125, 14.875, -4.5])]
fn inverted(theta: f64) {
let angle1 = Angle::from(theta);
let angle2 = angle1.inverted();
assert!(angle2.theta == -theta);
assert_relative_eq!(angle1.combine(&angle2), Angle::identity());
}
#[test]
fn display() {
let a = Angle::from(1.5);
let s = format!("{a}");
assert_eq!(s, "<1.5>");
}
#[test]
fn reduced() {
let two_pi = 2.0 * PI;
assert_relative_eq!(Angle::from(0.125).to_reduced(), (0.125).into());
assert_relative_eq!(Angle::from(2.0 * PI + 0.125).to_reduced(), (0.125).into());
assert_relative_eq!(Angle::from(2.0 * 2.0 * PI + 0.5).to_reduced(), (0.5).into());
assert_relative_eq!(Angle::from(3.0 * 2.0 * PI + 3.0).to_reduced(), (3.0).into());
assert_relative_eq!(
Angle::from(2.0 * PI - 0.125).to_reduced(),
(2.0 * PI - 0.125).into()
);
assert_relative_eq!(Angle::from(two_pi).to_reduced(), (0.0).into());
assert_relative_eq!(Angle::from(-0.125).to_reduced(), (2.0 * PI - 0.125).into());
assert_relative_eq!(Angle::from(-3.0).to_reduced(), (2.0 * PI - 3.0).into());
assert_relative_eq!(
Angle::from(-2.0 * PI - 0.125).to_reduced(),
(2.0 * PI - 0.125).into()
);
assert_relative_eq!(
Angle::from(10.0 * -2.0 * PI - 0.125).to_reduced(),
(2.0 * PI - 0.125).into()
);
}
#[test]
fn random() {
let mut rng = StdRng::seed_from_u64(1);
for _ in 0..10000 {
let a: Angle = rng.random();
assert!(a.theta >= 0.0 && a.theta < 2.0 * PI);
}
}
}