foundation: move apsis-math tests into tests directory
This commit is contained in:
parent
1adbbe3b64
commit
7b09b5fbdf
|
|
@ -322,211 +322,3 @@ impl fmt::Display for Mat3 {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::Mat3;
|
|
||||||
use crate::quat::Quat;
|
|
||||||
use crate::vec3::Vec3;
|
|
||||||
use core::f64::consts::FRAC_PI_2;
|
|
||||||
|
|
||||||
const EPS: f64 = 1e-12;
|
|
||||||
|
|
||||||
fn assert_mat_close(lhs: Mat3, rhs: Mat3) {
|
|
||||||
for row in 0..3 {
|
|
||||||
for column in 0..3 {
|
|
||||||
let (lhs, rhs) = (
|
|
||||||
lhs.get(row, column).expect("indices are in range"),
|
|
||||||
rhs.get(row, column).expect("indices are in range"),
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
(lhs - rhs).abs() < EPS,
|
|
||||||
"at ({row}, {column}): {lhs} vs {rhs}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn constructors_place_entries_in_the_right_slots() {
|
|
||||||
let m = Mat3::from_rows([1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]);
|
|
||||||
assert_eq!(m.get(0, 0), Some(1.0));
|
|
||||||
assert_eq!(m.get(0, 1), Some(2.0));
|
|
||||||
assert_eq!(m.get(0, 2), Some(3.0));
|
|
||||||
assert_eq!(m.get(1, 0), Some(4.0));
|
|
||||||
assert_eq!(m.get(2, 2), Some(9.0));
|
|
||||||
assert_eq!(m.get(3, 0), None);
|
|
||||||
assert_eq!(m.get(0, 3), None);
|
|
||||||
|
|
||||||
assert_eq!(m.row(1), Some(Vec3::new(4.0, 5.0, 6.0)));
|
|
||||||
assert_eq!(m.column(1), Some(Vec3::new(2.0, 5.0, 8.0)));
|
|
||||||
assert_eq!(m.row(3), None);
|
|
||||||
assert_eq!(m.column(3), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn from_columns_matches_from_rows() {
|
|
||||||
let c0 = Vec3::new(1.0, 4.0, 7.0);
|
|
||||||
let c1 = Vec3::new(2.0, 5.0, 8.0);
|
|
||||||
let c2 = Vec3::new(3.0, 6.0, 9.0);
|
|
||||||
let expected = Mat3::from_rows([1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]);
|
|
||||||
assert_eq!(Mat3::from_columns(c0, c1, c2), expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn identity_is_the_multiplicative_unit() {
|
|
||||||
let m = Mat3::from_rotation_z(FRAC_PI_2);
|
|
||||||
assert_eq!(Mat3::IDENTITY * m, m);
|
|
||||||
assert_eq!(m * Mat3::IDENTITY, m);
|
|
||||||
assert_eq!(Mat3::IDENTITY.determinant(), 1.0);
|
|
||||||
assert_eq!(Mat3::default(), Mat3::ZERO);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn zero_annihilates() {
|
|
||||||
assert_eq!(Mat3::ZERO * Vec3::new(1.0, 2.0, 3.0), Vec3::ZERO);
|
|
||||||
assert_eq!(Mat3::ZERO * Mat3::IDENTITY, Mat3::ZERO);
|
|
||||||
assert_eq!(Mat3::ZERO.determinant(), 0.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn transpose_swaps_rows_and_columns() {
|
|
||||||
let m = Mat3::from_rows([1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]);
|
|
||||||
let t = m.transpose();
|
|
||||||
assert_eq!(t.row(0), m.column(0));
|
|
||||||
assert_eq!(t.row(1), m.column(1));
|
|
||||||
assert_eq!(t.row(2), m.column(2));
|
|
||||||
assert_eq!(t.transpose(), m);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn determinant_matches_hand_computation() {
|
|
||||||
let m = Mat3::from_rows([1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 10.0]);
|
|
||||||
assert!((m.determinant() + 3.0).abs() < EPS);
|
|
||||||
assert_eq!(
|
|
||||||
Mat3::from_diagonal(Vec3::new(2.0, 3.0, 4.0)).determinant(),
|
|
||||||
24.0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn trace_sums_the_diagonal() {
|
|
||||||
let m = Mat3::from_rows([1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]);
|
|
||||||
assert_eq!(m.trace(), 15.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn matrix_vector_product_uses_rows() {
|
|
||||||
let m = Mat3::from_rows([1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]);
|
|
||||||
let v = Vec3::new(1.0, 1.0, 1.0);
|
|
||||||
assert_eq!(m * v, Vec3::new(6.0, 15.0, 24.0));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn matrix_product_is_associative() {
|
|
||||||
let a = Mat3::from_rows([1.0, 2.0, 0.0], [0.0, 1.0, 3.0], [1.0, 0.0, 1.0]);
|
|
||||||
let b = Mat3::from_rows([0.0, 1.0, 2.0], [1.0, 0.0, 1.0], [2.0, 1.0, 0.0]);
|
|
||||||
let c = Mat3::from_rows([1.0, 1.0, 1.0], [2.0, 2.0, 2.0], [3.0, 3.0, 3.0]);
|
|
||||||
assert_eq!((a * b) * c, a * (b * c));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn matrix_product_preserves_the_determinant_product() {
|
|
||||||
let a = Mat3::from_rows([1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 10.0]);
|
|
||||||
let b = Mat3::from_rows([2.0, 0.0, 1.0], [1.0, 3.0, 2.0], [0.0, 1.0, 4.0]);
|
|
||||||
let expected = a.determinant() * b.determinant();
|
|
||||||
assert!(((a * b).determinant() - expected).abs() < 1e-9);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn scalar_product_scales_every_entry() {
|
|
||||||
let m = Mat3::from_rows([1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]);
|
|
||||||
let scaled = m * 2.0;
|
|
||||||
assert_eq!(scaled.get(0, 0), Some(2.0));
|
|
||||||
assert_eq!(scaled.get(2, 2), Some(18.0));
|
|
||||||
assert_eq!(2.0 * m, scaled);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn rotation_matrices_satisfy_the_rotation_predicate() {
|
|
||||||
let m = Mat3::from_rotation_z(FRAC_PI_2);
|
|
||||||
assert!(m.is_rotation(1e-12));
|
|
||||||
assert!((m.determinant() - 1.0).abs() < EPS);
|
|
||||||
assert_mat_close(m * m.transpose(), Mat3::IDENTITY);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn is_rotation_rejects_non_rotations() {
|
|
||||||
assert!(!Mat3::ZERO.is_rotation(1e-9));
|
|
||||||
assert!(!Mat3::from_diagonal(Vec3::splat(2.0)).is_rotation(1e-9));
|
|
||||||
// A reflection has determinant minus one.
|
|
||||||
assert!(!Mat3::from_diagonal(Vec3::new(1.0, 1.0, -1.0)).is_rotation(1e-9));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn axis_angle_matches_the_quaternion_rotation() {
|
|
||||||
let axis = Vec3::new(0.5, -2.0, 1.0);
|
|
||||||
let m = Mat3::from_axis_angle(axis, 0.9).expect("non-zero axis");
|
|
||||||
let q = Quat::from_axis_angle(axis, 0.9).expect("non-zero axis");
|
|
||||||
let v = Vec3::new(1.0, -2.0, 3.0);
|
|
||||||
assert!((m * v - q.rotate(v)).norm() < EPS);
|
|
||||||
assert_eq!(Mat3::from_axis_angle(Vec3::ZERO, 1.0), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn rotation_maps_axes_as_expected() {
|
|
||||||
let m = Mat3::from_rotation_z(FRAC_PI_2);
|
|
||||||
assert!((m * Vec3::X - Vec3::Y).norm() < EPS);
|
|
||||||
assert!((m * Vec3::Y + Vec3::X).norm() < EPS);
|
|
||||||
assert!((m * Vec3::Z - Vec3::Z).norm() < EPS);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn per_axis_constructors_agree_with_the_axis_angle_form() {
|
|
||||||
let angle = 0.6;
|
|
||||||
let about_x = Mat3::from_rotation_x(angle);
|
|
||||||
assert_eq!(
|
|
||||||
about_x,
|
|
||||||
Mat3::from_axis_angle(Vec3::X, angle).expect("unit axis")
|
|
||||||
);
|
|
||||||
assert_eq!(about_x * Vec3::X, Vec3::X);
|
|
||||||
|
|
||||||
let about_y = Mat3::from_rotation_y(angle);
|
|
||||||
assert_eq!(
|
|
||||||
about_y,
|
|
||||||
Mat3::from_axis_angle(Vec3::Y, angle).expect("unit axis")
|
|
||||||
);
|
|
||||||
assert_eq!(about_y * Vec3::Y, Vec3::Y);
|
|
||||||
|
|
||||||
let about_z = Mat3::from_rotation_z(angle);
|
|
||||||
assert_eq!(
|
|
||||||
about_z,
|
|
||||||
Mat3::from_axis_angle(Vec3::Z, angle).expect("unit axis")
|
|
||||||
);
|
|
||||||
assert_eq!(about_z * Vec3::Z, Vec3::Z);
|
|
||||||
assert!((about_z.determinant() - 1.0).abs() < EPS);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn finiteness_is_reported() {
|
|
||||||
assert!(Mat3::IDENTITY.is_finite());
|
|
||||||
let mut m = Mat3::IDENTITY;
|
|
||||||
m.m01 = f64::NAN;
|
|
||||||
assert!(!m.is_finite());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn display_uses_row_major_groups() {
|
|
||||||
let m = Mat3::from_diagonal(Vec3::new(1.0, 2.0, 3.0));
|
|
||||||
assert_eq!(m.to_string(), "[1, 0, 0; 0, 2, 0; 0, 0, 3]");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn from_quat_is_consistent_with_quaternion_rotation() {
|
|
||||||
let q = Quat::from_axis_angle(Vec3::new(1.0, 1.0, 0.0), 1.3).expect("non-zero axis");
|
|
||||||
let m = Mat3::from_quat(q);
|
|
||||||
let v = Vec3::new(2.0, -1.0, 0.5);
|
|
||||||
assert!((m * v - q.rotate(v)).norm() < EPS);
|
|
||||||
assert!(m.is_rotation(1e-12));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -272,225 +272,3 @@ impl fmt::Display for Quat {
|
||||||
write!(f, "({}, {}, {}, {})", self.w, self.x, self.y, self.z)
|
write!(f, "({}, {}, {}, {})", self.w, self.x, self.y, self.z)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::Quat;
|
|
||||||
use crate::mat3::Mat3;
|
|
||||||
use crate::vec3::Vec3;
|
|
||||||
use core::f64::consts::{FRAC_PI_2, PI};
|
|
||||||
|
|
||||||
const EPS: f64 = 1e-12;
|
|
||||||
|
|
||||||
fn assert_close(lhs: Vec3, rhs: Vec3) {
|
|
||||||
assert!((lhs - rhs).norm() < EPS, "left {lhs}, right {rhs}");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn assert_quat_close(lhs: Quat, rhs: Quat) {
|
|
||||||
let difference = Quat::new(lhs.w - rhs.w, lhs.x - rhs.x, lhs.y - rhs.y, lhs.z - rhs.z);
|
|
||||||
assert!(difference.norm() < EPS, "left {lhs}, right {rhs}");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn identity_rotates_nothing() {
|
|
||||||
let v = Vec3::new(1.0, 2.0, 3.0);
|
|
||||||
assert_close(Quat::IDENTITY.rotate(v), v);
|
|
||||||
assert_eq!(Quat::IDENTITY.angle(), 0.0);
|
|
||||||
assert_eq!(Quat::IDENTITY.axis(), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn axis_angle_round_trips() {
|
|
||||||
let axis = Vec3::new(1.0, 2.0, -2.0)
|
|
||||||
.normalize()
|
|
||||||
.expect("non-zero axis");
|
|
||||||
let q = Quat::from_axis_angle(axis, 0.7).expect("non-zero axis");
|
|
||||||
assert!((q.angle() - 0.7).abs() < EPS);
|
|
||||||
assert_close(q.axis().expect("rotation has an axis"), axis);
|
|
||||||
assert!(q.is_normalized());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn axis_angle_rejects_undefined_axes() {
|
|
||||||
assert_eq!(Quat::from_axis_angle(Vec3::ZERO, 1.0), None);
|
|
||||||
assert_eq!(Quat::from_axis_angle(Vec3::splat(f64::NAN), 1.0), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn quarter_turn_about_z_maps_x_to_y() {
|
|
||||||
let q = Quat::from_rotation_z(FRAC_PI_2);
|
|
||||||
assert_close(q.rotate(Vec3::X), Vec3::Y);
|
|
||||||
assert_close(q.rotate(Vec3::Y), -Vec3::X);
|
|
||||||
assert_close(q.rotate(Vec3::Z), Vec3::Z);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn quarter_turn_about_x_maps_y_to_z() {
|
|
||||||
let q = Quat::from_rotation_x(FRAC_PI_2);
|
|
||||||
assert_close(q.rotate(Vec3::Y), Vec3::Z);
|
|
||||||
assert_close(q.rotate(Vec3::Z), -Vec3::Y);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn quarter_turn_about_y_maps_z_to_x() {
|
|
||||||
let q = Quat::from_rotation_y(FRAC_PI_2);
|
|
||||||
assert_close(q.rotate(Vec3::Z), Vec3::X);
|
|
||||||
assert_close(q.rotate(Vec3::X), -Vec3::Z);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn rotation_preserves_length() {
|
|
||||||
let q = Quat::from_axis_angle(Vec3::new(1.0, -1.0, 0.5), 1.234).expect("non-zero axis");
|
|
||||||
let v = Vec3::new(3.0, -4.0, 12.0);
|
|
||||||
assert!((q.rotate(v).norm() - v.norm()).abs() < EPS);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn conjugate_undoes_the_rotation() {
|
|
||||||
let q = Quat::from_axis_angle(Vec3::new(0.0, 1.0, 1.0), 0.9).expect("non-zero axis");
|
|
||||||
let v = Vec3::new(1.0, -2.0, 0.5);
|
|
||||||
assert_close(q.conjugate().rotate(q.rotate(v)), v);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn inverse_undoes_the_rotation() {
|
|
||||||
let q = Quat::from_axis_angle(Vec3::new(-0.5, 1.0, 0.25), 2.0).expect("non-zero axis");
|
|
||||||
let v = Vec3::new(2.0, 0.0, -1.0);
|
|
||||||
let inverse = q.inverse().expect("unit quaternion is invertible");
|
|
||||||
assert_close(inverse.rotate(q.rotate(v)), v);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn inverse_rejects_degenerate_quaternions() {
|
|
||||||
assert_eq!(Quat::new(0.0, 0.0, 0.0, 0.0).inverse(), None);
|
|
||||||
assert_eq!(Quat::new(f64::NAN, 0.0, 0.0, 0.0).inverse(), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn multiplication_composes_right_to_left() {
|
|
||||||
let first = Quat::from_rotation_z(FRAC_PI_2);
|
|
||||||
let second = Quat::from_rotation_x(FRAC_PI_2);
|
|
||||||
let v = Vec3::new(1.0, 0.0, 0.0);
|
|
||||||
// `second * first` applies `first` first, then `second`.
|
|
||||||
assert_close((second * first).rotate(v), second.rotate(first.rotate(v)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn identity_is_the_multiplicative_unit() {
|
|
||||||
let q = Quat::from_rotation_y(0.4);
|
|
||||||
assert_quat_close(q * Quat::IDENTITY, q);
|
|
||||||
assert_quat_close(Quat::IDENTITY * q, q);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn multiplication_matches_matrix_composition() {
|
|
||||||
let a = Quat::from_axis_angle(Vec3::new(1.0, 0.5, -0.25), 0.8).expect("non-zero axis");
|
|
||||||
let b = Quat::from_axis_angle(Vec3::new(-0.5, 1.0, 0.75), 1.6).expect("non-zero axis");
|
|
||||||
let composed = a * b;
|
|
||||||
let v = Vec3::new(1.0, 2.0, 3.0);
|
|
||||||
let via_matrices = Mat3::from_quat(a) * (Mat3::from_quat(b) * v);
|
|
||||||
assert_close(composed.rotate(v), via_matrices);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn normalize_produces_unit_length() {
|
|
||||||
let q = Quat::new(2.0, 0.0, 0.0, 0.0);
|
|
||||||
let unit = q.normalize().expect("non-zero quaternion");
|
|
||||||
assert!(unit.is_normalized());
|
|
||||||
assert!((unit.w - 1.0).abs() < EPS);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn normalize_rejects_degenerate_quaternions() {
|
|
||||||
assert_eq!(Quat::new(0.0, 0.0, 0.0, 0.0).normalize(), None);
|
|
||||||
assert_eq!(Quat::new(f64::NAN, 0.0, 0.0, 0.0).normalize(), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn from_mat3_recovers_the_original_rotation() {
|
|
||||||
let q = Quat::from_axis_angle(Vec3::new(0.3, -0.4, 0.5), 1.1).expect("non-zero axis");
|
|
||||||
let recovered = Quat::from_mat3(Mat3::from_quat(q)).expect("rotation matrix is accepted");
|
|
||||||
// `q` and `-q` are the same rotation, so compare through their effect.
|
|
||||||
let v = Vec3::new(1.0, -2.0, 3.0);
|
|
||||||
assert_close(recovered.rotate(v), q.rotate(v));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn from_mat3_covers_every_branch() {
|
|
||||||
// These angles exercise the trace branch and each of the three
|
|
||||||
// largest-diagonal branches.
|
|
||||||
for (axis, angle) in [
|
|
||||||
(Vec3::new(1.0, 0.0, 0.0), 0.2),
|
|
||||||
(Vec3::new(1.0, 0.0, 0.0), PI - 0.1),
|
|
||||||
(Vec3::new(0.0, 1.0, 0.0), PI - 0.1),
|
|
||||||
(Vec3::new(0.0, 0.0, 1.0), PI - 0.1),
|
|
||||||
] {
|
|
||||||
let q = Quat::from_axis_angle(axis, angle).expect("non-zero axis");
|
|
||||||
let recovered =
|
|
||||||
Quat::from_mat3(Mat3::from_quat(q)).expect("rotation matrix is accepted");
|
|
||||||
let v = Vec3::new(0.5, 1.5, -2.5);
|
|
||||||
assert_close(recovered.rotate(v), q.rotate(v));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn from_mat3_rejects_non_rotations() {
|
|
||||||
let scaled = Mat3::from_diagonal(Vec3::splat(2.0));
|
|
||||||
assert_eq!(Quat::from_mat3(scaled), None);
|
|
||||||
assert_eq!(Quat::from_mat3(Mat3::ZERO), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn slerp_walks_the_short_arc_uniformly() {
|
|
||||||
let start = Quat::IDENTITY;
|
|
||||||
let end = Quat::from_rotation_z(PI);
|
|
||||||
let half = start.slerp(end, 0.5);
|
|
||||||
assert!((half.angle() - FRAC_PI_2).abs() < 1e-9);
|
|
||||||
assert_close(half.rotate(Vec3::X), Vec3::Y);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn slerp_selects_the_short_arc_when_endpoints_are_negated() {
|
|
||||||
let start = Quat::IDENTITY;
|
|
||||||
let end = Quat::new(-1.0, 0.0, 0.0, 0.0);
|
|
||||||
// The two quaternions describe the same rotation, so any
|
|
||||||
// interpolation between them must stay at the identity.
|
|
||||||
let middle = start.slerp(end, 0.5);
|
|
||||||
assert_close(middle.rotate(Vec3::X), Vec3::X);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn slerp_hits_both_endpoints() {
|
|
||||||
let start = Quat::from_rotation_x(0.3);
|
|
||||||
let end = Quat::from_rotation_y(1.2);
|
|
||||||
assert_quat_close(start.slerp(end, 0.0), start);
|
|
||||||
assert_quat_close(start.slerp(end, 1.0), end);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn slerp_handles_nearly_parallel_inputs() {
|
|
||||||
let start = Quat::IDENTITY;
|
|
||||||
let end = Quat::from_rotation_z(1e-9);
|
|
||||||
let middle = start.slerp(end, 0.5);
|
|
||||||
assert!(middle.is_normalized());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn display_lists_the_components() {
|
|
||||||
assert_eq!(Quat::new(1.0, 0.0, 0.0, 0.0).to_string(), "(1, 0, 0, 0)");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn finiteness_is_reported() {
|
|
||||||
assert!(Quat::IDENTITY.is_finite());
|
|
||||||
assert!(!Quat::new(f64::INFINITY, 0.0, 0.0, 0.0).is_finite());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn named_turns_behave_as_documented() {
|
|
||||||
assert_close(Quat::quarter_turn_x().rotate(Vec3::Y), Vec3::Z);
|
|
||||||
assert_close(Quat::half_turn_z().rotate(Vec3::X), -Vec3::X);
|
|
||||||
assert!(Quat::quarter_turn_x().is_normalized());
|
|
||||||
assert!(Quat::half_turn_z().is_normalized());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -244,173 +244,3 @@ impl fmt::Display for Vec3 {
|
||||||
write!(f, "({}, {}, {})", self.x, self.y, self.z)
|
write!(f, "({}, {}, {})", self.x, self.y, self.z)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::Vec3;
|
|
||||||
|
|
||||||
const EPS: f64 = 1e-12;
|
|
||||||
|
|
||||||
fn assert_close(lhs: Vec3, rhs: Vec3) {
|
|
||||||
assert!((lhs - rhs).norm() < EPS, "left {lhs}, right {rhs}");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn constructors_set_the_components() {
|
|
||||||
assert_eq!(
|
|
||||||
Vec3::new(1.0, 2.0, 3.0),
|
|
||||||
Vec3 {
|
|
||||||
x: 1.0,
|
|
||||||
y: 2.0,
|
|
||||||
z: 3.0
|
|
||||||
}
|
|
||||||
);
|
|
||||||
assert_eq!(Vec3::splat(2.0), Vec3::new(2.0, 2.0, 2.0));
|
|
||||||
assert_eq!(Vec3::default(), Vec3::ZERO);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn get_selects_by_index_and_rejects_out_of_range() {
|
|
||||||
let v = Vec3::new(1.0, 2.0, 3.0);
|
|
||||||
assert_eq!(v.get(0), Some(1.0));
|
|
||||||
assert_eq!(v.get(1), Some(2.0));
|
|
||||||
assert_eq!(v.get(2), Some(3.0));
|
|
||||||
assert_eq!(v.get(3), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn dot_product_is_symmetric() {
|
|
||||||
let a = Vec3::new(1.0, 2.0, 3.0);
|
|
||||||
let b = Vec3::new(-4.0, 5.0, 0.5);
|
|
||||||
assert!((a.dot(b) - b.dot(a)).abs() < EPS);
|
|
||||||
assert!((a.dot(b) - 7.5).abs() < EPS);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn cross_product_follows_the_right_hand_rule() {
|
|
||||||
assert_eq!(Vec3::X.cross(Vec3::Y), Vec3::Z);
|
|
||||||
assert_eq!(Vec3::Y.cross(Vec3::Z), Vec3::X);
|
|
||||||
assert_eq!(Vec3::Z.cross(Vec3::X), Vec3::Y);
|
|
||||||
// The cross product of parallel vectors vanishes.
|
|
||||||
assert_eq!(Vec3::X.cross(Vec3::X), Vec3::ZERO);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn cross_product_is_orthogonal_to_its_inputs() {
|
|
||||||
let a = Vec3::new(1.0, -2.0, 3.5);
|
|
||||||
let b = Vec3::new(0.5, 4.0, -1.0);
|
|
||||||
let c = a.cross(b);
|
|
||||||
assert!(c.dot(a).abs() < EPS);
|
|
||||||
assert!(c.dot(b).abs() < EPS);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn cross_product_is_antisymmetric() {
|
|
||||||
let a = Vec3::new(1.0, 2.0, 3.0);
|
|
||||||
let b = Vec3::new(4.0, 5.0, 6.0);
|
|
||||||
assert_eq!(a.cross(b), -b.cross(a));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn norm_matches_the_pythagorean_length() {
|
|
||||||
let v = Vec3::new(3.0, 4.0, 12.0);
|
|
||||||
assert!((v.norm_squared() - 169.0).abs() < EPS);
|
|
||||||
assert!((v.norm() - 13.0).abs() < EPS);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn normalize_produces_a_unit_vector() {
|
|
||||||
let v = Vec3::new(-3.0, 0.0, 4.0);
|
|
||||||
let unit = v.normalize().expect("non-zero vector has a direction");
|
|
||||||
assert!((unit.norm() - 1.0).abs() < EPS);
|
|
||||||
assert_close(unit, Vec3::new(-0.6, 0.0, 0.8));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn normalize_rejects_undefined_directions() {
|
|
||||||
assert_eq!(Vec3::ZERO.normalize(), None);
|
|
||||||
assert_eq!(Vec3::splat(f64::INFINITY).normalize(), None);
|
|
||||||
assert_eq!(Vec3::splat(f64::NAN).normalize(), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn distance_measures_separation() {
|
|
||||||
let a = Vec3::new(1.0, 1.0, 1.0);
|
|
||||||
let b = Vec3::new(1.0, 1.0, 4.0);
|
|
||||||
assert!((a.distance(b) - 3.0).abs() < EPS);
|
|
||||||
assert!((a.distance_squared(b) - 9.0).abs() < EPS);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn angle_between_uses_the_right_hand_geometry() {
|
|
||||||
let angle = Vec3::X
|
|
||||||
.angle_between(Vec3::Y)
|
|
||||||
.expect("both axes have unit length");
|
|
||||||
assert!((angle - core::f64::consts::FRAC_PI_2).abs() < EPS);
|
|
||||||
let opposite = Vec3::X
|
|
||||||
.angle_between(-Vec3::X)
|
|
||||||
.expect("both vectors have unit length");
|
|
||||||
assert!((opposite - core::f64::consts::PI).abs() < EPS);
|
|
||||||
let parallel = Vec3::X
|
|
||||||
.angle_between(Vec3::X)
|
|
||||||
.expect("both vectors have unit length");
|
|
||||||
assert!(parallel.abs() < EPS);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn angle_between_rejects_undefined_directions() {
|
|
||||||
assert_eq!(Vec3::ZERO.angle_between(Vec3::X), None);
|
|
||||||
assert_eq!(Vec3::X.angle_between(Vec3::ZERO), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn lerp_hits_both_endpoints() {
|
|
||||||
let a = Vec3::new(0.0, 0.0, 0.0);
|
|
||||||
let b = Vec3::new(2.0, 4.0, 6.0);
|
|
||||||
assert_eq!(a.lerp(b, 0.0), a);
|
|
||||||
assert_eq!(a.lerp(b, 1.0), b);
|
|
||||||
assert_eq!(a.lerp(b, 0.5), Vec3::new(1.0, 2.0, 3.0));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn component_reductions_report_extremes() {
|
|
||||||
let v = Vec3::new(-2.0, 5.0, 1.0);
|
|
||||||
assert!((v.max_component() - 5.0).abs() < EPS);
|
|
||||||
assert!((v.min_component() + 2.0).abs() < EPS);
|
|
||||||
assert_close(v.abs(), Vec3::new(2.0, 5.0, 1.0));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn finiteness_is_reported() {
|
|
||||||
assert!(Vec3::new(1.0, 2.0, 3.0).is_finite());
|
|
||||||
assert!(!Vec3::new(1.0, f64::NAN, 3.0).is_finite());
|
|
||||||
assert!(!Vec3::splat(f64::INFINITY).is_finite());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn arithmetic_operators_match_component_wise_results() {
|
|
||||||
let a = Vec3::new(1.0, 2.0, 3.0);
|
|
||||||
let b = Vec3::new(4.0, 5.0, 6.0);
|
|
||||||
assert_eq!(a + b, Vec3::new(5.0, 7.0, 9.0));
|
|
||||||
assert_eq!(b - a, Vec3::new(3.0, 3.0, 3.0));
|
|
||||||
assert_eq!(-a, Vec3::new(-1.0, -2.0, -3.0));
|
|
||||||
assert_eq!(a * 2.0, Vec3::new(2.0, 4.0, 6.0));
|
|
||||||
assert_eq!(2.0 * a, a * 2.0);
|
|
||||||
assert_eq!(b / 2.0, Vec3::new(2.0, 2.5, 3.0));
|
|
||||||
|
|
||||||
let mut acc = a;
|
|
||||||
acc += b;
|
|
||||||
assert_eq!(acc, a + b);
|
|
||||||
acc -= b;
|
|
||||||
assert_eq!(acc, a);
|
|
||||||
acc *= 3.0;
|
|
||||||
assert_eq!(acc, a * 3.0);
|
|
||||||
acc /= 3.0;
|
|
||||||
assert_eq!(acc, a);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn display_lists_the_components() {
|
|
||||||
assert_eq!(Vec3::new(1.0, 2.5, -3.0).to_string(), "(1, 2.5, -3)");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,204 @@
|
||||||
|
//! Tests for three by three matrices.
|
||||||
|
|
||||||
|
use apsis_math::{Mat3, Quat, Vec3};
|
||||||
|
use core::f64::consts::FRAC_PI_2;
|
||||||
|
|
||||||
|
const EPS: f64 = 1e-12;
|
||||||
|
|
||||||
|
fn assert_mat_close(lhs: Mat3, rhs: Mat3) {
|
||||||
|
for row in 0..3 {
|
||||||
|
for column in 0..3 {
|
||||||
|
let (lhs, rhs) = (
|
||||||
|
lhs.get(row, column).expect("indices are in range"),
|
||||||
|
rhs.get(row, column).expect("indices are in range"),
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
(lhs - rhs).abs() < EPS,
|
||||||
|
"at ({row}, {column}): {lhs} vs {rhs}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn constructors_place_entries_in_the_right_slots() {
|
||||||
|
let m = Mat3::from_rows([1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]);
|
||||||
|
assert_eq!(m.get(0, 0), Some(1.0));
|
||||||
|
assert_eq!(m.get(0, 1), Some(2.0));
|
||||||
|
assert_eq!(m.get(0, 2), Some(3.0));
|
||||||
|
assert_eq!(m.get(1, 0), Some(4.0));
|
||||||
|
assert_eq!(m.get(2, 2), Some(9.0));
|
||||||
|
assert_eq!(m.get(3, 0), None);
|
||||||
|
assert_eq!(m.get(0, 3), None);
|
||||||
|
|
||||||
|
assert_eq!(m.row(1), Some(Vec3::new(4.0, 5.0, 6.0)));
|
||||||
|
assert_eq!(m.column(1), Some(Vec3::new(2.0, 5.0, 8.0)));
|
||||||
|
assert_eq!(m.row(3), None);
|
||||||
|
assert_eq!(m.column(3), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_columns_matches_from_rows() {
|
||||||
|
let c0 = Vec3::new(1.0, 4.0, 7.0);
|
||||||
|
let c1 = Vec3::new(2.0, 5.0, 8.0);
|
||||||
|
let c2 = Vec3::new(3.0, 6.0, 9.0);
|
||||||
|
let expected = Mat3::from_rows([1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]);
|
||||||
|
assert_eq!(Mat3::from_columns(c0, c1, c2), expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn identity_is_the_multiplicative_unit() {
|
||||||
|
let m = Mat3::from_rotation_z(FRAC_PI_2);
|
||||||
|
assert_eq!(Mat3::IDENTITY * m, m);
|
||||||
|
assert_eq!(m * Mat3::IDENTITY, m);
|
||||||
|
assert_eq!(Mat3::IDENTITY.determinant(), 1.0);
|
||||||
|
assert_eq!(Mat3::default(), Mat3::ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zero_annihilates() {
|
||||||
|
assert_eq!(Mat3::ZERO * Vec3::new(1.0, 2.0, 3.0), Vec3::ZERO);
|
||||||
|
assert_eq!(Mat3::ZERO * Mat3::IDENTITY, Mat3::ZERO);
|
||||||
|
assert_eq!(Mat3::ZERO.determinant(), 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn transpose_swaps_rows_and_columns() {
|
||||||
|
let m = Mat3::from_rows([1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]);
|
||||||
|
let t = m.transpose();
|
||||||
|
assert_eq!(t.row(0), m.column(0));
|
||||||
|
assert_eq!(t.row(1), m.column(1));
|
||||||
|
assert_eq!(t.row(2), m.column(2));
|
||||||
|
assert_eq!(t.transpose(), m);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn determinant_matches_hand_computation() {
|
||||||
|
let m = Mat3::from_rows([1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 10.0]);
|
||||||
|
assert!((m.determinant() + 3.0).abs() < EPS);
|
||||||
|
assert_eq!(
|
||||||
|
Mat3::from_diagonal(Vec3::new(2.0, 3.0, 4.0)).determinant(),
|
||||||
|
24.0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn trace_sums_the_diagonal() {
|
||||||
|
let m = Mat3::from_rows([1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]);
|
||||||
|
assert_eq!(m.trace(), 15.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn matrix_vector_product_uses_rows() {
|
||||||
|
let m = Mat3::from_rows([1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]);
|
||||||
|
let v = Vec3::new(1.0, 1.0, 1.0);
|
||||||
|
assert_eq!(m * v, Vec3::new(6.0, 15.0, 24.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn matrix_product_is_associative() {
|
||||||
|
let a = Mat3::from_rows([1.0, 2.0, 0.0], [0.0, 1.0, 3.0], [1.0, 0.0, 1.0]);
|
||||||
|
let b = Mat3::from_rows([0.0, 1.0, 2.0], [1.0, 0.0, 1.0], [2.0, 1.0, 0.0]);
|
||||||
|
let c = Mat3::from_rows([1.0, 1.0, 1.0], [2.0, 2.0, 2.0], [3.0, 3.0, 3.0]);
|
||||||
|
assert_eq!((a * b) * c, a * (b * c));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn matrix_product_preserves_the_determinant_product() {
|
||||||
|
let a = Mat3::from_rows([1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 10.0]);
|
||||||
|
let b = Mat3::from_rows([2.0, 0.0, 1.0], [1.0, 3.0, 2.0], [0.0, 1.0, 4.0]);
|
||||||
|
let expected = a.determinant() * b.determinant();
|
||||||
|
assert!(((a * b).determinant() - expected).abs() < 1e-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scalar_product_scales_every_entry() {
|
||||||
|
let m = Mat3::from_rows([1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]);
|
||||||
|
let scaled = m * 2.0;
|
||||||
|
assert_eq!(scaled.get(0, 0), Some(2.0));
|
||||||
|
assert_eq!(scaled.get(2, 2), Some(18.0));
|
||||||
|
assert_eq!(2.0 * m, scaled);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rotation_matrices_satisfy_the_rotation_predicate() {
|
||||||
|
let m = Mat3::from_rotation_z(FRAC_PI_2);
|
||||||
|
assert!(m.is_rotation(1e-12));
|
||||||
|
assert!((m.determinant() - 1.0).abs() < EPS);
|
||||||
|
assert_mat_close(m * m.transpose(), Mat3::IDENTITY);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn is_rotation_rejects_non_rotations() {
|
||||||
|
assert!(!Mat3::ZERO.is_rotation(1e-9));
|
||||||
|
assert!(!Mat3::from_diagonal(Vec3::splat(2.0)).is_rotation(1e-9));
|
||||||
|
// A reflection has determinant minus one.
|
||||||
|
assert!(!Mat3::from_diagonal(Vec3::new(1.0, 1.0, -1.0)).is_rotation(1e-9));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn axis_angle_matches_the_quaternion_rotation() {
|
||||||
|
let axis = Vec3::new(0.5, -2.0, 1.0);
|
||||||
|
let m = Mat3::from_axis_angle(axis, 0.9).expect("non-zero axis");
|
||||||
|
let q = Quat::from_axis_angle(axis, 0.9).expect("non-zero axis");
|
||||||
|
let v = Vec3::new(1.0, -2.0, 3.0);
|
||||||
|
assert!((m * v - q.rotate(v)).norm() < EPS);
|
||||||
|
assert_eq!(Mat3::from_axis_angle(Vec3::ZERO, 1.0), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rotation_maps_axes_as_expected() {
|
||||||
|
let m = Mat3::from_rotation_z(FRAC_PI_2);
|
||||||
|
assert!((m * Vec3::X - Vec3::Y).norm() < EPS);
|
||||||
|
assert!((m * Vec3::Y + Vec3::X).norm() < EPS);
|
||||||
|
assert!((m * Vec3::Z - Vec3::Z).norm() < EPS);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn per_axis_constructors_agree_with_the_axis_angle_form() {
|
||||||
|
let angle = 0.6;
|
||||||
|
let about_x = Mat3::from_rotation_x(angle);
|
||||||
|
assert_eq!(
|
||||||
|
about_x,
|
||||||
|
Mat3::from_axis_angle(Vec3::X, angle).expect("unit axis")
|
||||||
|
);
|
||||||
|
assert_eq!(about_x * Vec3::X, Vec3::X);
|
||||||
|
|
||||||
|
let about_y = Mat3::from_rotation_y(angle);
|
||||||
|
assert_eq!(
|
||||||
|
about_y,
|
||||||
|
Mat3::from_axis_angle(Vec3::Y, angle).expect("unit axis")
|
||||||
|
);
|
||||||
|
assert_eq!(about_y * Vec3::Y, Vec3::Y);
|
||||||
|
|
||||||
|
let about_z = Mat3::from_rotation_z(angle);
|
||||||
|
assert_eq!(
|
||||||
|
about_z,
|
||||||
|
Mat3::from_axis_angle(Vec3::Z, angle).expect("unit axis")
|
||||||
|
);
|
||||||
|
assert_eq!(about_z * Vec3::Z, Vec3::Z);
|
||||||
|
assert!((about_z.determinant() - 1.0).abs() < EPS);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn finiteness_is_reported() {
|
||||||
|
assert!(Mat3::IDENTITY.is_finite());
|
||||||
|
let mut m = Mat3::IDENTITY;
|
||||||
|
m.m01 = f64::NAN;
|
||||||
|
assert!(!m.is_finite());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn display_uses_row_major_groups() {
|
||||||
|
let m = Mat3::from_diagonal(Vec3::new(1.0, 2.0, 3.0));
|
||||||
|
assert_eq!(m.to_string(), "[1, 0, 0; 0, 2, 0; 0, 0, 3]");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_quat_is_consistent_with_quaternion_rotation() {
|
||||||
|
let q = Quat::from_axis_angle(Vec3::new(1.0, 1.0, 0.0), 1.3).expect("non-zero axis");
|
||||||
|
let m = Mat3::from_quat(q);
|
||||||
|
let v = Vec3::new(2.0, -1.0, 0.5);
|
||||||
|
assert!((m * v - q.rotate(v)).norm() < EPS);
|
||||||
|
assert!(m.is_rotation(1e-12));
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,217 @@
|
||||||
|
//! Tests for unit quaternion rotations.
|
||||||
|
|
||||||
|
use apsis_math::{Mat3, Quat, Vec3};
|
||||||
|
use core::f64::consts::{FRAC_PI_2, PI};
|
||||||
|
|
||||||
|
const EPS: f64 = 1e-12;
|
||||||
|
|
||||||
|
fn assert_close(lhs: Vec3, rhs: Vec3) {
|
||||||
|
assert!((lhs - rhs).norm() < EPS, "left {lhs}, right {rhs}");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_quat_close(lhs: Quat, rhs: Quat) {
|
||||||
|
let difference = Quat::new(lhs.w - rhs.w, lhs.x - rhs.x, lhs.y - rhs.y, lhs.z - rhs.z);
|
||||||
|
assert!(difference.norm() < EPS, "left {lhs}, right {rhs}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn identity_rotates_nothing() {
|
||||||
|
let v = Vec3::new(1.0, 2.0, 3.0);
|
||||||
|
assert_close(Quat::IDENTITY.rotate(v), v);
|
||||||
|
assert_eq!(Quat::IDENTITY.angle(), 0.0);
|
||||||
|
assert_eq!(Quat::IDENTITY.axis(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn axis_angle_round_trips() {
|
||||||
|
let axis = Vec3::new(1.0, 2.0, -2.0)
|
||||||
|
.normalize()
|
||||||
|
.expect("non-zero axis");
|
||||||
|
let q = Quat::from_axis_angle(axis, 0.7).expect("non-zero axis");
|
||||||
|
assert!((q.angle() - 0.7).abs() < EPS);
|
||||||
|
assert_close(q.axis().expect("rotation has an axis"), axis);
|
||||||
|
assert!(q.is_normalized());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn axis_angle_rejects_undefined_axes() {
|
||||||
|
assert_eq!(Quat::from_axis_angle(Vec3::ZERO, 1.0), None);
|
||||||
|
assert_eq!(Quat::from_axis_angle(Vec3::splat(f64::NAN), 1.0), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn quarter_turn_about_z_maps_x_to_y() {
|
||||||
|
let q = Quat::from_rotation_z(FRAC_PI_2);
|
||||||
|
assert_close(q.rotate(Vec3::X), Vec3::Y);
|
||||||
|
assert_close(q.rotate(Vec3::Y), -Vec3::X);
|
||||||
|
assert_close(q.rotate(Vec3::Z), Vec3::Z);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn quarter_turn_about_x_maps_y_to_z() {
|
||||||
|
let q = Quat::from_rotation_x(FRAC_PI_2);
|
||||||
|
assert_close(q.rotate(Vec3::Y), Vec3::Z);
|
||||||
|
assert_close(q.rotate(Vec3::Z), -Vec3::Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn quarter_turn_about_y_maps_z_to_x() {
|
||||||
|
let q = Quat::from_rotation_y(FRAC_PI_2);
|
||||||
|
assert_close(q.rotate(Vec3::Z), Vec3::X);
|
||||||
|
assert_close(q.rotate(Vec3::X), -Vec3::Z);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rotation_preserves_length() {
|
||||||
|
let q = Quat::from_axis_angle(Vec3::new(1.0, -1.0, 0.5), 1.234).expect("non-zero axis");
|
||||||
|
let v = Vec3::new(3.0, -4.0, 12.0);
|
||||||
|
assert!((q.rotate(v).norm() - v.norm()).abs() < EPS);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn conjugate_undoes_the_rotation() {
|
||||||
|
let q = Quat::from_axis_angle(Vec3::new(0.0, 1.0, 1.0), 0.9).expect("non-zero axis");
|
||||||
|
let v = Vec3::new(1.0, -2.0, 0.5);
|
||||||
|
assert_close(q.conjugate().rotate(q.rotate(v)), v);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn inverse_undoes_the_rotation() {
|
||||||
|
let q = Quat::from_axis_angle(Vec3::new(-0.5, 1.0, 0.25), 2.0).expect("non-zero axis");
|
||||||
|
let v = Vec3::new(2.0, 0.0, -1.0);
|
||||||
|
let inverse = q.inverse().expect("unit quaternion is invertible");
|
||||||
|
assert_close(inverse.rotate(q.rotate(v)), v);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn inverse_rejects_degenerate_quaternions() {
|
||||||
|
assert_eq!(Quat::new(0.0, 0.0, 0.0, 0.0).inverse(), None);
|
||||||
|
assert_eq!(Quat::new(f64::NAN, 0.0, 0.0, 0.0).inverse(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multiplication_composes_right_to_left() {
|
||||||
|
let first = Quat::from_rotation_z(FRAC_PI_2);
|
||||||
|
let second = Quat::from_rotation_x(FRAC_PI_2);
|
||||||
|
let v = Vec3::new(1.0, 0.0, 0.0);
|
||||||
|
// `second * first` applies `first` first, then `second`.
|
||||||
|
assert_close((second * first).rotate(v), second.rotate(first.rotate(v)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn identity_is_the_multiplicative_unit() {
|
||||||
|
let q = Quat::from_rotation_y(0.4);
|
||||||
|
assert_quat_close(q * Quat::IDENTITY, q);
|
||||||
|
assert_quat_close(Quat::IDENTITY * q, q);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multiplication_matches_matrix_composition() {
|
||||||
|
let a = Quat::from_axis_angle(Vec3::new(1.0, 0.5, -0.25), 0.8).expect("non-zero axis");
|
||||||
|
let b = Quat::from_axis_angle(Vec3::new(-0.5, 1.0, 0.75), 1.6).expect("non-zero axis");
|
||||||
|
let composed = a * b;
|
||||||
|
let v = Vec3::new(1.0, 2.0, 3.0);
|
||||||
|
let via_matrices = Mat3::from_quat(a) * (Mat3::from_quat(b) * v);
|
||||||
|
assert_close(composed.rotate(v), via_matrices);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normalize_produces_unit_length() {
|
||||||
|
let q = Quat::new(2.0, 0.0, 0.0, 0.0);
|
||||||
|
let unit = q.normalize().expect("non-zero quaternion");
|
||||||
|
assert!(unit.is_normalized());
|
||||||
|
assert!((unit.w - 1.0).abs() < EPS);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normalize_rejects_degenerate_quaternions() {
|
||||||
|
assert_eq!(Quat::new(0.0, 0.0, 0.0, 0.0).normalize(), None);
|
||||||
|
assert_eq!(Quat::new(f64::NAN, 0.0, 0.0, 0.0).normalize(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_mat3_recovers_the_original_rotation() {
|
||||||
|
let q = Quat::from_axis_angle(Vec3::new(0.3, -0.4, 0.5), 1.1).expect("non-zero axis");
|
||||||
|
let recovered = Quat::from_mat3(Mat3::from_quat(q)).expect("rotation matrix is accepted");
|
||||||
|
// `q` and `-q` are the same rotation, so compare through their effect.
|
||||||
|
let v = Vec3::new(1.0, -2.0, 3.0);
|
||||||
|
assert_close(recovered.rotate(v), q.rotate(v));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_mat3_covers_every_branch() {
|
||||||
|
// These angles exercise the trace branch and each of the three
|
||||||
|
// largest-diagonal branches.
|
||||||
|
for (axis, angle) in [
|
||||||
|
(Vec3::new(1.0, 0.0, 0.0), 0.2),
|
||||||
|
(Vec3::new(1.0, 0.0, 0.0), PI - 0.1),
|
||||||
|
(Vec3::new(0.0, 1.0, 0.0), PI - 0.1),
|
||||||
|
(Vec3::new(0.0, 0.0, 1.0), PI - 0.1),
|
||||||
|
] {
|
||||||
|
let q = Quat::from_axis_angle(axis, angle).expect("non-zero axis");
|
||||||
|
let recovered = Quat::from_mat3(Mat3::from_quat(q)).expect("rotation matrix is accepted");
|
||||||
|
let v = Vec3::new(0.5, 1.5, -2.5);
|
||||||
|
assert_close(recovered.rotate(v), q.rotate(v));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_mat3_rejects_non_rotations() {
|
||||||
|
let scaled = Mat3::from_diagonal(Vec3::splat(2.0));
|
||||||
|
assert_eq!(Quat::from_mat3(scaled), None);
|
||||||
|
assert_eq!(Quat::from_mat3(Mat3::ZERO), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn slerp_walks_the_short_arc_uniformly() {
|
||||||
|
let start = Quat::IDENTITY;
|
||||||
|
let end = Quat::from_rotation_z(PI);
|
||||||
|
let half = start.slerp(end, 0.5);
|
||||||
|
assert!((half.angle() - FRAC_PI_2).abs() < 1e-9);
|
||||||
|
assert_close(half.rotate(Vec3::X), Vec3::Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn slerp_selects_the_short_arc_when_endpoints_are_negated() {
|
||||||
|
let start = Quat::IDENTITY;
|
||||||
|
let end = Quat::new(-1.0, 0.0, 0.0, 0.0);
|
||||||
|
// The two quaternions describe the same rotation, so any
|
||||||
|
// interpolation between them must stay at the identity.
|
||||||
|
let middle = start.slerp(end, 0.5);
|
||||||
|
assert_close(middle.rotate(Vec3::X), Vec3::X);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn slerp_hits_both_endpoints() {
|
||||||
|
let start = Quat::from_rotation_x(0.3);
|
||||||
|
let end = Quat::from_rotation_y(1.2);
|
||||||
|
assert_quat_close(start.slerp(end, 0.0), start);
|
||||||
|
assert_quat_close(start.slerp(end, 1.0), end);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn slerp_handles_nearly_parallel_inputs() {
|
||||||
|
let start = Quat::IDENTITY;
|
||||||
|
let end = Quat::from_rotation_z(1e-9);
|
||||||
|
let middle = start.slerp(end, 0.5);
|
||||||
|
assert!(middle.is_normalized());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn display_lists_the_components() {
|
||||||
|
assert_eq!(Quat::new(1.0, 0.0, 0.0, 0.0).to_string(), "(1, 0, 0, 0)");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn finiteness_is_reported() {
|
||||||
|
assert!(Quat::IDENTITY.is_finite());
|
||||||
|
assert!(!Quat::new(f64::INFINITY, 0.0, 0.0, 0.0).is_finite());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn named_turns_behave_as_documented() {
|
||||||
|
assert_close(Quat::quarter_turn_x().rotate(Vec3::Y), Vec3::Z);
|
||||||
|
assert_close(Quat::half_turn_z().rotate(Vec3::X), -Vec3::X);
|
||||||
|
assert!(Quat::quarter_turn_x().is_normalized());
|
||||||
|
assert!(Quat::half_turn_z().is_normalized());
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,168 @@
|
||||||
|
//! Tests for three component vectors.
|
||||||
|
|
||||||
|
use apsis_math::Vec3;
|
||||||
|
|
||||||
|
const EPS: f64 = 1e-12;
|
||||||
|
|
||||||
|
fn assert_close(lhs: Vec3, rhs: Vec3) {
|
||||||
|
assert!((lhs - rhs).norm() < EPS, "left {lhs}, right {rhs}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn constructors_set_the_components() {
|
||||||
|
assert_eq!(
|
||||||
|
Vec3::new(1.0, 2.0, 3.0),
|
||||||
|
Vec3 {
|
||||||
|
x: 1.0,
|
||||||
|
y: 2.0,
|
||||||
|
z: 3.0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(Vec3::splat(2.0), Vec3::new(2.0, 2.0, 2.0));
|
||||||
|
assert_eq!(Vec3::default(), Vec3::ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_selects_by_index_and_rejects_out_of_range() {
|
||||||
|
let v = Vec3::new(1.0, 2.0, 3.0);
|
||||||
|
assert_eq!(v.get(0), Some(1.0));
|
||||||
|
assert_eq!(v.get(1), Some(2.0));
|
||||||
|
assert_eq!(v.get(2), Some(3.0));
|
||||||
|
assert_eq!(v.get(3), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dot_product_is_symmetric() {
|
||||||
|
let a = Vec3::new(1.0, 2.0, 3.0);
|
||||||
|
let b = Vec3::new(-4.0, 5.0, 0.5);
|
||||||
|
assert!((a.dot(b) - b.dot(a)).abs() < EPS);
|
||||||
|
assert!((a.dot(b) - 7.5).abs() < EPS);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cross_product_follows_the_right_hand_rule() {
|
||||||
|
assert_eq!(Vec3::X.cross(Vec3::Y), Vec3::Z);
|
||||||
|
assert_eq!(Vec3::Y.cross(Vec3::Z), Vec3::X);
|
||||||
|
assert_eq!(Vec3::Z.cross(Vec3::X), Vec3::Y);
|
||||||
|
// The cross product of parallel vectors vanishes.
|
||||||
|
assert_eq!(Vec3::X.cross(Vec3::X), Vec3::ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cross_product_is_orthogonal_to_its_inputs() {
|
||||||
|
let a = Vec3::new(1.0, -2.0, 3.5);
|
||||||
|
let b = Vec3::new(0.5, 4.0, -1.0);
|
||||||
|
let c = a.cross(b);
|
||||||
|
assert!(c.dot(a).abs() < EPS);
|
||||||
|
assert!(c.dot(b).abs() < EPS);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cross_product_is_antisymmetric() {
|
||||||
|
let a = Vec3::new(1.0, 2.0, 3.0);
|
||||||
|
let b = Vec3::new(4.0, 5.0, 6.0);
|
||||||
|
assert_eq!(a.cross(b), -b.cross(a));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn norm_matches_the_pythagorean_length() {
|
||||||
|
let v = Vec3::new(3.0, 4.0, 12.0);
|
||||||
|
assert!((v.norm_squared() - 169.0).abs() < EPS);
|
||||||
|
assert!((v.norm() - 13.0).abs() < EPS);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normalize_produces_a_unit_vector() {
|
||||||
|
let v = Vec3::new(-3.0, 0.0, 4.0);
|
||||||
|
let unit = v.normalize().expect("non-zero vector has a direction");
|
||||||
|
assert!((unit.norm() - 1.0).abs() < EPS);
|
||||||
|
assert_close(unit, Vec3::new(-0.6, 0.0, 0.8));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normalize_rejects_undefined_directions() {
|
||||||
|
assert_eq!(Vec3::ZERO.normalize(), None);
|
||||||
|
assert_eq!(Vec3::splat(f64::INFINITY).normalize(), None);
|
||||||
|
assert_eq!(Vec3::splat(f64::NAN).normalize(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn distance_measures_separation() {
|
||||||
|
let a = Vec3::new(1.0, 1.0, 1.0);
|
||||||
|
let b = Vec3::new(1.0, 1.0, 4.0);
|
||||||
|
assert!((a.distance(b) - 3.0).abs() < EPS);
|
||||||
|
assert!((a.distance_squared(b) - 9.0).abs() < EPS);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn angle_between_uses_the_right_hand_geometry() {
|
||||||
|
let angle = Vec3::X
|
||||||
|
.angle_between(Vec3::Y)
|
||||||
|
.expect("both axes have unit length");
|
||||||
|
assert!((angle - core::f64::consts::FRAC_PI_2).abs() < EPS);
|
||||||
|
let opposite = Vec3::X
|
||||||
|
.angle_between(-Vec3::X)
|
||||||
|
.expect("both vectors have unit length");
|
||||||
|
assert!((opposite - core::f64::consts::PI).abs() < EPS);
|
||||||
|
let parallel = Vec3::X
|
||||||
|
.angle_between(Vec3::X)
|
||||||
|
.expect("both vectors have unit length");
|
||||||
|
assert!(parallel.abs() < EPS);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn angle_between_rejects_undefined_directions() {
|
||||||
|
assert_eq!(Vec3::ZERO.angle_between(Vec3::X), None);
|
||||||
|
assert_eq!(Vec3::X.angle_between(Vec3::ZERO), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lerp_hits_both_endpoints() {
|
||||||
|
let a = Vec3::new(0.0, 0.0, 0.0);
|
||||||
|
let b = Vec3::new(2.0, 4.0, 6.0);
|
||||||
|
assert_eq!(a.lerp(b, 0.0), a);
|
||||||
|
assert_eq!(a.lerp(b, 1.0), b);
|
||||||
|
assert_eq!(a.lerp(b, 0.5), Vec3::new(1.0, 2.0, 3.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn component_reductions_report_extremes() {
|
||||||
|
let v = Vec3::new(-2.0, 5.0, 1.0);
|
||||||
|
assert!((v.max_component() - 5.0).abs() < EPS);
|
||||||
|
assert!((v.min_component() + 2.0).abs() < EPS);
|
||||||
|
assert_close(v.abs(), Vec3::new(2.0, 5.0, 1.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn finiteness_is_reported() {
|
||||||
|
assert!(Vec3::new(1.0, 2.0, 3.0).is_finite());
|
||||||
|
assert!(!Vec3::new(1.0, f64::NAN, 3.0).is_finite());
|
||||||
|
assert!(!Vec3::splat(f64::INFINITY).is_finite());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arithmetic_operators_match_component_wise_results() {
|
||||||
|
let a = Vec3::new(1.0, 2.0, 3.0);
|
||||||
|
let b = Vec3::new(4.0, 5.0, 6.0);
|
||||||
|
assert_eq!(a + b, Vec3::new(5.0, 7.0, 9.0));
|
||||||
|
assert_eq!(b - a, Vec3::new(3.0, 3.0, 3.0));
|
||||||
|
assert_eq!(-a, Vec3::new(-1.0, -2.0, -3.0));
|
||||||
|
assert_eq!(a * 2.0, Vec3::new(2.0, 4.0, 6.0));
|
||||||
|
assert_eq!(2.0 * a, a * 2.0);
|
||||||
|
assert_eq!(b / 2.0, Vec3::new(2.0, 2.5, 3.0));
|
||||||
|
|
||||||
|
let mut acc = a;
|
||||||
|
acc += b;
|
||||||
|
assert_eq!(acc, a + b);
|
||||||
|
acc -= b;
|
||||||
|
assert_eq!(acc, a);
|
||||||
|
acc *= 3.0;
|
||||||
|
assert_eq!(acc, a * 3.0);
|
||||||
|
acc /= 3.0;
|
||||||
|
assert_eq!(acc, a);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn display_lists_the_components() {
|
||||||
|
assert_eq!(Vec3::new(1.0, 2.5, -3.0).to_string(), "(1, 2.5, -3)");
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue