blob: 3890f258964a2a394c145b8ba16da1b4f2bbf9b3 [file] [log] [blame]
#[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<F: Fn(EulerRot, i32, i32, i32)>(order: EulerRot, test: &F) {
const RANGE: RangeInclusive<i32> = -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<F: Fn(EulerRot)>(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);
}
}