use super::{MIN_VECTOR_LENGTH, Quatd, Vec3d};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Rotation {
axis: Vec3d,
angle: f64,
}
impl Rotation {
pub const IDENTITY: Rotation = Rotation {
axis: Vec3d { x: 1.0, y: 0.0, z: 0.0 },
angle: 0.0,
};
pub fn new(axis: Vec3d, degrees: f64) -> Self {
let length_sq = axis.dot(axis);
let axis = match (length_sq - 1.0).abs() < 1e-10 {
true => axis,
false => axis * (1.0 / length_sq.sqrt().max(MIN_VECTOR_LENGTH)),
};
Self { axis, angle: degrees }
}
pub fn from_quat(q: Quatd) -> Self {
let imaginary = Vec3d { x: q.x, y: q.y, z: q.z };
let length = imaginary.length();
if length > MIN_VECTOR_LENGTH {
let half_angle = q.w.clamp(-1.0, 1.0).acos();
Self::new(imaginary * (1.0 / length), 2.0 * half_angle.to_degrees())
} else {
Self::IDENTITY
}
}
pub fn axis(self) -> Vec3d {
self.axis
}
pub fn angle(self) -> f64 {
self.angle
}
pub fn inverse(self) -> Self {
Self::new(self.axis, -self.angle)
}
pub fn quat(self) -> Quatd {
let (sin, cos) = (self.angle.to_radians() / 2.0).sin_cos();
let axis = self.axis * sin;
Quatd {
w: cos,
x: axis.x,
y: axis.y,
z: axis.z,
}
.normalize()
}
}
impl Default for Rotation {
fn default() -> Self {
Self::IDENTITY
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gf;
#[test]
fn quat_round_trip() {
let q = gf::quatd(0.5, 0.5, 0.5, 0.5);
let r = Rotation::from_quat(q);
assert!((r.angle() - 120.0).abs() < 1e-12);
let back = r.quat();
for (a, b) in [(back.w, q.w), (back.x, q.x), (back.y, q.y), (back.z, q.z)] {
assert!((a - b).abs() < 1e-15);
}
}
#[test]
fn short_imaginary_is_identity() {
assert_eq!(Rotation::from_quat(gf::quatd(1.0, 0.0, 0.0, 0.0)), Rotation::IDENTITY);
assert_eq!(Rotation::from_quat(gf::quatd(0.5, 1e-11, 0.0, 0.0)), Rotation::IDENTITY);
}
#[test]
fn imaginary_length_threshold() {
let at = Rotation::from_quat(gf::quatd(0.5, MIN_VECTOR_LENGTH, 0.0, 0.0));
assert_eq!(at, Rotation::IDENTITY);
let above = Rotation::from_quat(gf::quatd(0.5, 1.0000001e-10, 0.0, 0.0));
assert_eq!(above.axis(), gf::vec3d(1.0, 0.0, 0.0));
assert!((above.angle() - 120.0).abs() < 1e-12);
}
#[test]
fn nan_components() {
for q in [
gf::quatd(0.5, f64::NAN, 0.0, 0.0),
gf::quatd(0.5, 0.0, f64::NAN, 1.0),
gf::quatd(0.5, 1.0, 0.0, f64::NAN),
] {
assert_eq!(Rotation::from_quat(q), Rotation::IDENTITY);
}
let r = Rotation::from_quat(gf::quatd(f64::NAN, 1.0, 0.0, 0.0));
assert_eq!(r.axis(), gf::vec3d(1.0, 0.0, 0.0));
assert!(r.angle().is_nan());
}
#[test]
fn axis_normalized() {
let r = Rotation::new(gf::vec3d(0.0, 0.0, 2.0), 90.0);
assert_eq!(r.axis(), gf::vec3d(0.0, 0.0, 1.0));
assert_eq!(r.inverse().angle(), -90.0);
}
#[test]
fn angle_from_real_part() {
let r = Rotation::from_quat(gf::quatd(0.70710677_f32 as f64, 0.70710677_f32 as f64, 0.0, 0.0));
assert!(r.angle() > 90.0 && r.angle() - 90.0 < 3e-6);
assert_eq!(r.axis(), gf::vec3d(1.0, 0.0, 0.0));
}
}