#[macro_use] mod support; mod euler { use glam::*; use std::ops::RangeInclusive; /// Helper to get the 'canonical' version of a `Quat`. We define the canonical of quat `q` as: /// /// * `q`, if q.w > epsilon /// * `-q`, if q.w < -epsilon /// * `(0, 0, 0, 1)` otherwise /// /// The rationale is that q and -q represent the same rotation, and any (_, _, _, 0) represent no rotation at all. trait CanonicalQuat: Copy { fn canonical(self) -> Self; } /// Helper to set some alternative epsilons based on the floating point type used trait EulerEpsilon { /// epsilon for comparing quaternion round-tripped through eulers (quat -> euler -> quat) const E_EPS: f32; } impl EulerEpsilon for f32 { const E_EPS: f32 = 2e-6; } impl EulerEpsilon for f64 { const E_EPS: f32 = 1e-8; } fn axis_order(order: EulerRot) -> (usize, usize, usize) { match order { EulerRot::XYZ => (0, 1, 2), EulerRot::XYX => (0, 1, 0), EulerRot::XZY => (0, 2, 1), EulerRot::XZX => (0, 2, 0), EulerRot::YZX => (1, 2, 0), EulerRot::YZY => (1, 2, 1), EulerRot::YXZ => (1, 0, 2), EulerRot::YXY => (1, 0, 1), EulerRot::ZXY => (2, 0, 1), EulerRot::ZXZ => (2, 0, 2), EulerRot::ZYX => (2, 1, 0), EulerRot::ZYZ => (2, 1, 2), EulerRot::ZYXEx => (2, 1, 0), EulerRot::XYXEx => (0, 1, 0), EulerRot::YZXEx => (1, 2, 0), EulerRot::XZXEx => (0, 2, 0), EulerRot::XZYEx => (0, 2, 1), EulerRot::YZYEx => (1, 2, 1), EulerRot::ZXYEx => (2, 0, 1), EulerRot::YXYEx => (1, 0, 1), EulerRot::YXZEx => (1, 0, 2), EulerRot::ZXZEx => (2, 0, 2), EulerRot::XYZEx => (0, 1, 2), EulerRot::ZYZEx => (2, 1, 2), } } fn is_intrinsic(order: EulerRot) -> bool { match order { EulerRot::XYZ | EulerRot::XYX | EulerRot::XZY | EulerRot::XZX | EulerRot::YZX | EulerRot::YZY | EulerRot::YXZ | EulerRot::YXY | EulerRot::ZXY | EulerRot::ZXZ | EulerRot::ZYX | EulerRot::ZYZ => true, EulerRot::ZYXEx | EulerRot::XYXEx | EulerRot::YZXEx | EulerRot::XZXEx | EulerRot::XZYEx | EulerRot::YZYEx | EulerRot::ZXYEx | EulerRot::YXYEx | EulerRot::YXZEx | EulerRot::ZXZEx | EulerRot::XYZEx | EulerRot::ZYZEx => false, } } mod f32 { pub fn deg_to_rad(a: i32, b: i32, c: i32) -> (f32, f32, f32) { ( (a as f32).to_radians(), (b as f32).to_radians(), (c as f32).to_radians(), ) } } mod f64 { pub fn deg_to_rad(a: i32, b: i32, c: i32) -> (f64, f64, f64) { ( (a as f64).to_radians(), (b as f64).to_radians(), (c as f64).to_radians(), ) } } fn test_order_angles(order: EulerRot, test: &F) { const RANGE: RangeInclusive = -180..=180; const STEP: usize = 15; for i in RANGE.step_by(STEP) { for j in RANGE.step_by(STEP) { for k in RANGE.step_by(STEP) { test(order, i, j, k); } } } } fn test_all_orders(test: &F) { test(EulerRot::XYZ); test(EulerRot::XZY); test(EulerRot::YZX); test(EulerRot::YXZ); test(EulerRot::ZXY); test(EulerRot::ZYX); test(EulerRot::XZX); test(EulerRot::XYX); test(EulerRot::YXY); test(EulerRot::YZY); test(EulerRot::ZYZ); test(EulerRot::ZXZ); test(EulerRot::XYZEx); test(EulerRot::XZYEx); test(EulerRot::YZXEx); test(EulerRot::YXZEx); test(EulerRot::ZXYEx); test(EulerRot::ZYXEx); test(EulerRot::XZXEx); test(EulerRot::XYXEx); test(EulerRot::YXYEx); test(EulerRot::YZYEx); test(EulerRot::ZYZEx); test(EulerRot::ZXZEx); } macro_rules! impl_quat_euler_test { ($quat:ident, $t:ident) => { use super::{ axis_order, is_intrinsic, test_all_orders, test_order_angles, $t::deg_to_rad, CanonicalQuat, EulerEpsilon, }; use glam::{$quat, EulerRot}; const AXIS_ANGLE: [fn($t) -> $quat; 3] = [ $quat::from_rotation_x, $quat::from_rotation_y, $quat::from_rotation_z, ]; impl CanonicalQuat for $quat { fn canonical(self) -> Self { match self { _ if self.w >= 1e-5 => self, _ if self.w <= -1e-5 => -self, _ => $quat::from_xyzw(0.0, 0.0, 0.0, 1.0), } } } fn test_euler(order: EulerRot, a: i32, b: i32, c: i32) { println!( "test_euler: {} {order:?} ({a}, {b}, {c})", stringify!($quat) ); let (a, b, c) = deg_to_rad(a, b, c); let m = $quat::from_euler(order, a, b, c); let n = { let (i, j, k) = m.to_euler(order); $quat::from_euler(order, i, j, k) }; assert_approx_eq!(m.canonical(), n.canonical(), $t::E_EPS); let o = { let (i, j, k) = axis_order(order); if is_intrinsic(order) { AXIS_ANGLE[i](a) * AXIS_ANGLE[j](b) * AXIS_ANGLE[k](c) } else { AXIS_ANGLE[k](c) * AXIS_ANGLE[j](b) * AXIS_ANGLE[i](a) } }; assert_approx_eq!(m.canonical(), o.canonical(), $t::E_EPS); } #[test] fn test_all_euler_orders() { let test = |order| test_order_angles(order, &test_euler); test_all_orders(&test); } }; } macro_rules! impl_mat_euler_test { ($mat:ident, $t:ident) => { use super::{ axis_order, is_intrinsic, test_all_orders, test_order_angles, $t::deg_to_rad, EulerEpsilon, }; use glam::{$mat, EulerRot}; const AXIS_ANGLE: [fn($t) -> $mat; 3] = [ $mat::from_rotation_x, $mat::from_rotation_y, $mat::from_rotation_z, ]; fn test_euler(order: EulerRot, a: i32, b: i32, c: i32) { println!("test_euler: {} {order:?} ({a}, {b}, {c})", stringify!($mat)); let (a, b, c) = deg_to_rad(a, b, c); let m = $mat::from_euler(order, a, b, c); let n = { let (i, j, k) = m.to_euler(order); $mat::from_euler(order, i, j, k) }; assert_approx_eq!(m, n, $t::E_EPS); let o = { let (i, j, k) = axis_order(order); if is_intrinsic(order) { AXIS_ANGLE[i](a) * AXIS_ANGLE[j](b) * AXIS_ANGLE[k](c) } else { AXIS_ANGLE[k](c) * AXIS_ANGLE[j](b) * AXIS_ANGLE[i](a) } }; assert_approx_eq!(m, o, $t::E_EPS); } #[test] fn test_all_euler_orders() { let test = |order| test_order_angles(order, &test_euler); test_all_orders(&test); } }; } #[test] fn test_euler_default() { assert_eq!(EulerRot::YXZ, EulerRot::default()); } mod quat { impl_quat_euler_test!(Quat, f32); } mod mat3 { impl_mat_euler_test!(Mat3, f32); } mod mat3a { impl_mat_euler_test!(Mat3A, f32); } mod mat4 { impl_mat_euler_test!(Mat4, f32); } mod dquat { impl_quat_euler_test!(DQuat, f64); } mod dmat3 { impl_mat_euler_test!(DMat3, f64); } mod dmat4 { impl_mat_euler_test!(DMat4, f64); } }