use super::{Rotation3, Translation3};
use crate::cartesian::xyz::XYZ;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Isometry3 {
pub rotation: Rotation3,
pub translation: Translation3,
}
impl Isometry3 {
pub const IDENTITY: Self = Self {
rotation: Rotation3::IDENTITY,
translation: Translation3::ZERO,
};
#[inline]
#[must_use]
pub const fn new(rotation: Rotation3, translation: Translation3) -> Self {
Self {
rotation,
translation,
}
}
#[inline]
#[must_use]
pub const fn from_rotation(rotation: Rotation3) -> Self {
Self {
rotation,
translation: Translation3::ZERO,
}
}
#[inline]
#[must_use]
pub const fn from_translation(translation: Translation3) -> Self {
Self {
rotation: Rotation3::IDENTITY,
translation,
}
}
#[inline]
#[must_use]
pub fn inverse(&self) -> Self {
let r_inv = self.rotation.inverse();
let t_inv = r_inv.apply_array(self.translation.v);
Self {
rotation: r_inv,
translation: Translation3::new(-t_inv[0], -t_inv[1], -t_inv[2]),
}
}
#[inline]
#[must_use]
pub fn compose(&self, other: &Self) -> Self {
let rotation = self.rotation.compose(&other.rotation);
let rotated_t = self.rotation.apply_array(other.translation.v);
let translation = Translation3::new(
rotated_t[0] + self.translation.v[0],
rotated_t[1] + self.translation.v[1],
rotated_t[2] + self.translation.v[2],
);
Self {
rotation,
translation,
}
}
#[inline]
pub fn apply_point(&self, p: [f64; 3]) -> [f64; 3] {
let rotated = self.rotation.apply_array(p);
self.translation.apply_array(rotated)
}
#[inline]
pub fn apply_xyz(&self, xyz: XYZ<f64>) -> XYZ<f64> {
let [x, y, z] = self.apply_point([xyz.x(), xyz.y(), xyz.z()]);
XYZ::new(x, y, z)
}
#[inline]
pub fn apply_vector(&self, v: [f64; 3]) -> [f64; 3] {
self.rotation.apply_array(v)
}
#[inline]
pub fn apply_vector_xyz(&self, xyz: XYZ<f64>) -> XYZ<f64> {
self.rotation.apply_xyz(xyz)
}
}
impl Default for Isometry3 {
fn default() -> Self {
Self::IDENTITY
}
}
impl std::ops::Mul for Isometry3 {
type Output = Self;
#[inline]
fn mul(self, rhs: Self) -> Self::Output {
self.compose(&rhs)
}
}
impl std::ops::Mul<[f64; 3]> for Isometry3 {
type Output = [f64; 3];
#[inline]
fn mul(self, rhs: [f64; 3]) -> [f64; 3] {
self.apply_point(rhs)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cartesian::xyz::XYZ;
use qtty::Radians;
use std::f64::consts::FRAC_PI_2;
const EPSILON: f64 = 1e-12;
fn assert_array_eq(a: [f64; 3], b: [f64; 3], msg: &str) {
assert!(
(a[0] - b[0]).abs() < EPSILON
&& (a[1] - b[1]).abs() < EPSILON
&& (a[2] - b[2]).abs() < EPSILON,
"{}: {:?} != {:?}",
msg,
a,
b
);
}
#[test]
fn test_isometry_identity() {
let iso = Isometry3::IDENTITY;
let p = [1.0, 2.0, 3.0];
assert_array_eq(
iso.apply_point(p),
p,
"Identity isometry should not change point",
);
}
#[test]
fn test_isometry_rotation_only() {
let rot = Rotation3::rz(Radians::new(FRAC_PI_2));
let iso = Isometry3::from_rotation(rot);
let p = [1.0, 0.0, 0.0];
let result = iso.apply_point(p);
assert_array_eq(result, [0.0, 1.0, 0.0], "Rotation-only isometry");
}
#[test]
fn test_isometry_translation_only() {
let trans = Translation3::new(1.0, 2.0, 3.0);
let iso = Isometry3::from_translation(trans);
let p = [0.0, 0.0, 0.0];
let result = iso.apply_point(p);
assert_array_eq(result, [1.0, 2.0, 3.0], "Translation-only isometry");
}
#[test]
fn test_isometry_combined() {
let rot = Rotation3::rz(Radians::new(FRAC_PI_2));
let trans = Translation3::new(1.0, 0.0, 0.0);
let iso = Isometry3::new(rot, trans);
let p = [1.0, 0.0, 0.0];
let result = iso.apply_point(p);
assert_array_eq(result, [1.0, 1.0, 0.0], "Combined isometry");
}
#[test]
fn test_isometry_inverse() {
let rot = Rotation3::rz(Radians::new(0.5));
let trans = Translation3::new(1.0, 2.0, 3.0);
let iso = Isometry3::new(rot, trans);
let iso_inv = iso.inverse();
let p = [4.0, 5.0, 6.0];
let transformed = iso.apply_point(p);
let recovered = iso_inv.apply_point(transformed);
assert_array_eq(recovered, p, "Isometry inverse should recover original");
}
#[test]
fn test_isometry_composition() {
let iso1 = Isometry3::new(
Rotation3::rz(Radians::new(FRAC_PI_2)),
Translation3::new(1.0, 0.0, 0.0),
);
let iso2 = Isometry3::new(
Rotation3::rx(Radians::new(FRAC_PI_2)),
Translation3::new(0.0, 1.0, 0.0),
);
let composed = iso1.compose(&iso2);
let p = [0.0, 0.0, 0.0];
let result = composed.apply_point(p);
assert_array_eq(result, [0.0, 0.0, 0.0], "Composed isometry");
}
#[test]
fn test_isometry_apply_vector() {
let rot = Rotation3::rz(Radians::new(FRAC_PI_2));
let trans = Translation3::new(100.0, 200.0, 300.0);
let iso = Isometry3::new(rot, trans);
let v = [1.0, 0.0, 0.0];
let result = iso.apply_vector(v);
assert_array_eq(result, [0.0, 1.0, 0.0], "Vector ignores translation");
}
#[test]
fn test_isometry_helpers_and_ops() {
let iso = Isometry3::default();
assert_eq!(iso, Isometry3::IDENTITY);
let iso1 = Isometry3::new(
Rotation3::rz(Radians::new(FRAC_PI_2)),
Translation3::new(1.0, 0.0, 0.0),
);
let iso2 = Isometry3::from_translation(Translation3::new(0.0, 1.0, 0.0));
let composed = iso1 * iso2;
let p = XYZ::new(1.0, 0.0, 0.0);
let applied = composed.apply_xyz(p);
assert_array_eq(
[applied.x(), applied.y(), applied.z()],
[0.0, 1.0, 0.0],
"apply_xyz",
);
let v = XYZ::new(1.0, 0.0, 0.0);
let vec_only = composed.apply_vector_xyz(v);
assert_array_eq(
[vec_only.x(), vec_only.y(), vec_only.z()],
[0.0, 1.0, 0.0],
"apply_vector_xyz",
);
}
#[test]
fn test_isometry_mul_f64_array() {
let rot = Rotation3::rz(Radians::new(FRAC_PI_2));
let trans = Translation3::new(1.0, 0.0, 0.0);
let iso = Isometry3::new(rot, trans);
let result = iso * [1.0, 0.0, 0.0];
assert_array_eq(result, [1.0, 1.0, 0.0], "Isometry * [f64; 3]");
}
#[test]
fn test_isometry_mul_quantity_array() {
use qtty::{Meter, Quantity};
let rot = Rotation3::rz(Radians::new(FRAC_PI_2));
let trans = Translation3::new(1.0, 0.0, 0.0);
let iso = Isometry3::new(rot, trans);
let v = [
Quantity::<Meter>::new(1.0),
Quantity::<Meter>::new(0.0),
Quantity::<Meter>::new(0.0),
];
let result = iso * v;
assert!((result[0].value() - 1.0).abs() < EPSILON);
assert!((result[1].value() - 1.0).abs() < EPSILON);
assert!((result[2].value()).abs() < EPSILON);
}
}