use dualis_units::LengthVec;
use glam::{DQuat, DVec3};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Pose {
translation: LengthVec,
rotation: DQuat,
}
impl Default for Pose {
fn default() -> Pose {
Pose::IDENTITY
}
}
impl Pose {
pub const IDENTITY: Pose = Pose {
translation: LengthVec::ZERO,
rotation: DQuat::IDENTITY,
};
pub fn new(translation: LengthVec, rotation: DQuat) -> Pose {
Pose {
translation,
rotation: rotation.normalize(),
}
}
pub fn at(translation: LengthVec) -> Pose {
Pose::new(translation, DQuat::IDENTITY)
}
pub fn turned(rotation: DQuat) -> Pose {
Pose::new(LengthVec::ZERO, rotation)
}
pub fn translation(&self) -> LengthVec {
self.translation
}
pub fn rotation(&self) -> DQuat {
self.rotation
}
pub fn point_to_world(&self, local: LengthVec) -> LengthVec {
LengthVec::from_si(self.rotation * local.to_si() + self.translation.to_si())
}
pub fn point_to_local(&self, world: LengthVec) -> LengthVec {
LengthVec::from_si(
self.rotation
.conjugate()
.mul_vec3(world.to_si() - self.translation.to_si()),
)
}
pub fn direction_to_world(&self, local: DVec3) -> DVec3 {
self.rotation * local
}
pub fn direction_to_local(&self, world: DVec3) -> DVec3 {
self.rotation.conjugate().mul_vec3(world)
}
pub fn then(&self, outer: Pose) -> Pose {
Pose {
translation: LengthVec::from_si(
outer.rotation * self.translation.to_si() + outer.translation.to_si(),
),
rotation: (outer.rotation * self.rotation).normalize(),
}
}
pub fn inverse(&self) -> Pose {
let r = self.rotation.conjugate();
Pose {
translation: LengthVec::from_si(-(r.mul_vec3(self.translation.to_si()))),
rotation: r,
}
}
pub fn is_identity(&self, tol: f64) -> bool {
self.translation.to_si().length() <= tol && (self.rotation.w.abs() - 1.0).abs() <= tol
}
}
#[cfg(test)]
mod tests {
use super::*;
use dualis_units::Length;
use std::f64::consts::{FRAC_PI_2, PI};
fn wild() -> Pose {
Pose::new(
LengthVec::m(1.5, -0.25, 3.0),
DQuat::from_euler(glam::EulerRot::XYZ, 0.3, -1.1, 2.2),
)
}
#[test]
fn placing_something_cannot_change_a_distance() {
let p = wild();
let pairs = [
(LengthVec::m(0.0, 0.0, 0.0), LengthVec::m(1.0, 0.0, 0.0)),
(LengthVec::m(-2.0, 5.0, 0.5), LengthVec::m(3.0, -1.0, 4.0)),
(LengthVec::m(1e-6, 0.0, 0.0), LengthVec::m(-1e-6, 0.0, 0.0)),
];
for (a, b) in pairs {
let before = (a.to_si() - b.to_si()).length();
let after = (p.point_to_world(a).to_si() - p.point_to_world(b).to_si()).length();
assert!(
(after - before).abs() <= 1e-15 * before.max(1.0),
"a {before} m separation became {after} m"
);
}
}
#[test]
fn local_to_world_and_back_is_the_identity() {
let p = wild();
for v in [
LengthVec::m(0.0, 0.0, 0.0),
LengthVec::m(1.0, 2.0, 3.0),
LengthVec::m(-7.5, 0.0, 1e3),
] {
let back = p.point_to_local(p.point_to_world(v));
assert!(
(back.to_si() - v.to_si()).length() < 1e-12,
"{:?} came back as {:?}",
v.to_si(),
back.to_si()
);
}
for d in [
DVec3::X,
DVec3::Y,
DVec3::Z,
DVec3::new(1.0, -2.0, 0.5).normalize(),
] {
let back = p.direction_to_local(p.direction_to_world(d));
assert!((back - d).length() < 1e-12);
}
}
#[test]
fn a_direction_ignores_the_translation() {
let far = Pose::at(LengthVec::m(1e4, -1e4, 1e4));
for d in [DVec3::X, DVec3::Y, DVec3::Z] {
assert_eq!(
far.direction_to_world(d),
d,
"translation rotated a direction"
);
}
let p = wild();
let (u, v) = (DVec3::X, DVec3::Y);
let n = u.cross(v);
let (u2, v2, n2) = (
p.direction_to_world(u),
p.direction_to_world(v),
p.direction_to_world(n),
);
assert!(n2.dot(u2).abs() < 1e-15 && n2.dot(v2).abs() < 1e-15);
assert!((u2.cross(v2) - n2).length() < 1e-15, "handedness flipped");
}
#[test]
fn composing_is_associative_and_in_the_order_it_reads() {
let (a, b, c) = (
Pose::at(LengthVec::m(1.0, 0.0, 0.0)),
Pose::turned(DQuat::from_rotation_z(FRAC_PI_2)),
Pose::at(LengthVec::m(0.0, 0.0, 2.0)),
);
let left = a.then(b).then(c);
let right = a.then(b.then(c));
assert!((left.translation.to_si() - right.translation.to_si()).length() < 1e-14);
assert!(left.rotation.abs_diff_eq(right.rotation, 1e-14));
let origin = a.then(b).point_to_world(LengthVec::ZERO);
assert!(
(origin.to_si() - DVec3::new(0.0, 1.0, 0.0)).length() < 1e-14,
"a then b put the origin at {:?}",
origin.to_si()
);
let swapped = b.then(a).point_to_world(LengthVec::ZERO);
assert!((swapped.to_si() - DVec3::new(1.0, 0.0, 0.0)).length() < 1e-14);
}
#[test]
fn a_pose_undoes_itself() {
let p = wild();
let both = p.then(p.inverse());
assert!(both.is_identity(1e-12), "{both:?}");
assert!(p.inverse().then(p).is_identity(1e-12));
assert!(Pose::IDENTITY.inverse().is_identity(0.0));
let half = Pose::turned(DQuat::from_rotation_y(PI));
assert!(half.then(half).is_identity(1e-14));
}
#[test]
fn the_identity_is_free() {
let v = LengthVec::m(3.0, -1.0, 0.25);
assert_eq!(Pose::IDENTITY.point_to_world(v).to_si(), v.to_si());
assert_eq!(Pose::default(), Pose::IDENTITY);
assert!(Pose::IDENTITY.is_identity(0.0));
assert!(!Pose::at(LengthVec::m(1e-3, 0.0, 0.0)).is_identity(1e-9));
assert_eq!(
Pose::at(LengthVec::m(5.0, 0.0, 0.0))
.point_to_world(LengthVec::m(1.0, 0.0, 0.0))
.to_si()
.x,
Length::m(6.0).to_si()
);
}
}