use num_traits::{Float, FloatConst, Signed};
use crate::{cartesian::Cartesian, radian::Radian};
use super::Transform;
#[derive(Debug, Clone, Copy)]
pub struct Rotation<T> {
pub axis: Cartesian<T>,
pub theta: Radian<T>,
}
impl<T> Transform<Cartesian<T>> for Rotation<T>
where
T: Float,
{
fn transform(&self, coords: Cartesian<T>) -> Cartesian<T> {
coords * self.theta.into_inner().cos()
+ self.axis.cross(&coords) * self.theta.into_inner().sin()
+ self.axis * self.axis.dot(&coords) * (T::one() - self.theta.into_inner().cos())
}
}
impl<T> Rotation<T>
where
T: Signed + Float + FloatConst,
{
pub fn noop() -> Self {
Self {
axis: Cartesian::origin(),
theta: T::zero().into(),
}
}
}
impl<T> Rotation<T> {
pub fn with_axis(self, axis: Cartesian<T>) -> Self {
Self { axis, ..self }
}
pub fn with_theta(self, theta: Radian<T>) -> Self {
Self { theta, ..self }
}
}
#[cfg(test)]
mod tests {
use std::f64::consts::{FRAC_PI_2, PI};
use crate::{
radian::Radian,
transform::{Rotation, Transform},
Cartesian,
};
#[test]
fn cartesian_rotation() {
struct Test {
name: &'static str,
theta: Radian<f64>,
axis: Cartesian<f64>,
input: Cartesian<f64>,
output: Cartesian<f64>,
}
vec![
Test {
name: "noop rotation must not change the point",
theta: Radian::from(0.),
axis: Cartesian::origin(),
input: Cartesian::origin().with_x(1.).with_y(2.).with_z(3.),
output: Cartesian::origin().with_x(1.).with_y(2.).with_z(3.),
},
Test {
name: "full rotation on the x axis must not change the y point",
theta: Radian::from(2. * PI),
axis: Cartesian::origin().with_x(1.),
input: Cartesian::origin().with_y(1.),
output: Cartesian::origin().with_y(1.),
},
Test {
name: "half of a whole rotation on the x axis must change the y point",
theta: Radian::from(PI),
axis: Cartesian::origin().with_x(1.),
input: Cartesian::origin().with_y(1.),
output: Cartesian::origin().with_y(-1.),
},
Test {
name: "a quarter of a whole rotation on the x axis must change the y point",
theta: Radian::from(FRAC_PI_2),
axis: Cartesian::origin().with_x(1.),
input: Cartesian::origin().with_y(1.),
output: Cartesian::origin().with_z(1.),
},
Test {
name: "full rotation on the z axis must not change the y point",
theta: Radian::from(2. * PI),
axis: Cartesian::origin().with_z(1.),
input: Cartesian::origin().with_y(1.),
output: Cartesian::origin().with_y(1.),
},
Test {
name: "half of a whole rotation on the z axis must change the y point",
theta: Radian::from(PI),
axis: Cartesian::origin().with_z(1.),
input: Cartesian::origin().with_y(1.),
output: Cartesian::origin().with_y(-1.),
},
Test {
name: "a quarter of a whole rotation on the z axis must change the y point",
theta: Radian::from(FRAC_PI_2),
axis: Cartesian::origin().with_z(1.),
input: Cartesian::origin().with_y(1.),
output: Cartesian::origin().with_x(-1.),
},
Test {
name: "rotate over itself must not change the point",
theta: Radian::from(FRAC_PI_2),
axis: Cartesian::origin().with_y(1.),
input: Cartesian::origin().with_y(1.),
output: Cartesian::origin().with_y(1.),
},
]
.into_iter()
.for_each(|test| {
let rotated = Rotation::noop()
.with_axis(test.axis)
.with_theta(test.theta)
.transform(test.input);
let tolerance = 1e-09;
rotated
.into_iter()
.zip(test.output)
.for_each(|(got, want)| {
assert!(
(got - want).abs() < tolerance,
"{}: got rotated = {:?}, want {:?}",
test.name,
rotated,
test.output
);
});
});
}
}