use core::fmt;
use nalgebra::UnitQuaternion;
use crate::{Isometry, Point2, Point3, UnitVec2, UnitVec3, Vec2, Vec3};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FrameError {
NonFinite,
ZeroAxis,
DegenerateHint,
NotOrthonormal,
}
impl fmt::Display for FrameError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
FrameError::NonFinite => "frame has a non-finite coordinate",
FrameError::ZeroAxis => "frame axis has zero length",
FrameError::DegenerateHint => "frame x hint is zero or parallel to the axis",
FrameError::NotOrthonormal => "frame axes are not orthonormal",
})
}
}
impl std::error::Error for FrameError {}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(try_from = "FrameRepr", into = "FrameRepr"))]
pub struct Frame {
origin: Point3,
x: UnitVec3,
y: UnitVec3,
z: UnitVec3,
}
#[cfg(feature = "serde")]
#[derive(serde::Serialize, serde::Deserialize)]
struct FrameRepr {
origin: Point3,
x: Vec3,
y: Vec3,
z: Vec3,
}
#[cfg(feature = "serde")]
impl From<Frame> for FrameRepr {
fn from(f: Frame) -> Self {
FrameRepr {
origin: f.origin,
x: f.x.into_inner(),
y: f.y.into_inner(),
z: f.z.into_inner(),
}
}
}
#[cfg(feature = "serde")]
impl TryFrom<FrameRepr> for Frame {
type Error = FrameError;
fn try_from(r: FrameRepr) -> Result<Self, FrameError> {
Frame::from_orthonormal(r.origin, r.x, r.y, r.z)
}
}
impl Frame {
pub fn world() -> Self {
Frame {
origin: Point3::origin(),
x: Vec3::x_axis(),
y: Vec3::y_axis(),
z: Vec3::z_axis(),
}
}
pub fn new(origin: Point3, z: Vec3, x_hint: Vec3) -> Result<Self, FrameError> {
if !(is_finite3(&origin.coords) && is_finite3(&z) && is_finite3(&x_hint)) {
return Err(FrameError::NonFinite);
}
let z = UnitVec3::try_new(z, 0.0).ok_or(FrameError::ZeroAxis)?;
let perpendicular = x_hint - z.dot(&x_hint) * z.into_inner();
let x = UnitVec3::try_new(perpendicular, 0.0).ok_or(FrameError::DegenerateHint)?;
Ok(Self::orthonormalised(origin, x, z))
}
pub fn from_z(origin: Point3, z: Vec3) -> Result<Self, FrameError> {
if !(is_finite3(&origin.coords) && is_finite3(&z)) {
return Err(FrameError::NonFinite);
}
let z = UnitVec3::try_new(z, 0.0).ok_or(FrameError::ZeroAxis)?;
let (a, b, c) = (z.x, z.y, z.z);
let (aa, ba, ca) = (a.abs(), b.abs(), c.abs());
let hint = if ba <= aa && ba <= ca {
if aa > ca {
Vec3::new(-c, 0.0, a)
} else {
Vec3::new(c, 0.0, -a)
}
} else if aa <= ba && aa <= ca {
if ba > ca {
Vec3::new(0.0, -c, b)
} else {
Vec3::new(0.0, c, -b)
}
} else if aa > ba {
Vec3::new(-b, a, 0.0)
} else {
Vec3::new(b, -a, 0.0)
};
let x = UnitVec3::try_new(hint, 0.0).ok_or(FrameError::ZeroAxis)?;
Ok(Self::orthonormalised(origin, x, z))
}
pub fn from_orthonormal(origin: Point3, x: Vec3, y: Vec3, z: Vec3) -> Result<Self, FrameError> {
if !(is_finite3(&origin.coords) && is_finite3(&x) && is_finite3(&y) && is_finite3(&z)) {
return Err(FrameError::NonFinite);
}
let unit = |v: &Vec3| crate::is_negligible(v.norm() - 1.0, 1.0);
let perpendicular = |a: &Vec3, b: &Vec3| crate::is_negligible(a.dot(b), 1.0);
if !(unit(&x) && unit(&y) && unit(&z))
|| !(perpendicular(&x, &y) && perpendicular(&y, &z) && perpendicular(&z, &x))
|| !crate::is_negligible((x.cross(&y) - z).norm(), 1.0)
{
return Err(FrameError::NotOrthonormal);
}
Ok(Frame {
origin,
x: UnitVec3::new_unchecked(x),
y: UnitVec3::new_unchecked(y),
z: UnitVec3::new_unchecked(z),
})
}
pub fn from_rotation(origin: Point3, rotation: &UnitQuaternion<f64>) -> Self {
let x = UnitVec3::new_normalize(rotation.transform_vector(&Vec3::x()));
let z = UnitVec3::new_normalize(rotation.transform_vector(&Vec3::z()));
Self::orthonormalised(origin, x, z)
}
fn orthonormalised(origin: Point3, x: UnitVec3, z: UnitVec3) -> Self {
let y = UnitVec3::new_normalize(z.cross(&x));
let x = UnitVec3::new_normalize(y.cross(&z));
Frame { origin, x, y, z }
}
pub const fn with_origin(&self, origin: Point3) -> Frame {
Frame {
origin,
x: self.x,
y: self.y,
z: self.z,
}
}
pub const fn origin(&self) -> Point3 {
self.origin
}
pub const fn x(&self) -> UnitVec3 {
self.x
}
pub const fn y(&self) -> UnitVec3 {
self.y
}
pub const fn z(&self) -> UnitVec3 {
self.z
}
pub fn to_local(&self, p: Point3) -> Point3 {
Point3::from(self.vec_to_local(p - self.origin))
}
pub fn to_world(&self, p: Point3) -> Point3 {
self.origin + self.vec_to_world(p.coords)
}
pub fn vec_to_local(&self, v: Vec3) -> Vec3 {
Vec3::new(v.dot(&self.x), v.dot(&self.y), v.dot(&self.z))
}
pub fn vec_to_world(&self, v: Vec3) -> Vec3 {
v.x * self.x.into_inner() + v.y * self.y.into_inner() + v.z * self.z.into_inner()
}
pub fn rotation(&self) -> UnitQuaternion<f64> {
UnitQuaternion::from_basis_unchecked(&[
self.x.into_inner(),
self.y.into_inner(),
self.z.into_inner(),
])
}
pub fn as_isometry(&self) -> Isometry {
Isometry::new(self.rotation(), self.origin.coords)
}
pub fn transformed(&self, motion: &Isometry) -> Frame {
Self::orthonormalised(
motion.apply(self.origin),
motion.apply_unit(self.x),
motion.apply_unit(self.z),
)
}
}
fn is_finite3(v: &Vec3) -> bool {
v.iter().all(|c| c.is_finite())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Handedness {
Right,
Left,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(try_from = "Frame2Repr", into = "Frame2Repr"))]
pub struct Frame2 {
origin: Point2,
x: UnitVec2,
y: UnitVec2,
}
#[cfg(feature = "serde")]
#[derive(serde::Serialize, serde::Deserialize)]
struct Frame2Repr {
origin: Point2,
x: Vec2,
y: Vec2,
}
#[cfg(feature = "serde")]
impl From<Frame2> for Frame2Repr {
fn from(f: Frame2) -> Self {
Frame2Repr {
origin: f.origin,
x: f.x.into_inner(),
y: f.y.into_inner(),
}
}
}
#[cfg(feature = "serde")]
impl TryFrom<Frame2Repr> for Frame2 {
type Error = FrameError;
fn try_from(r: Frame2Repr) -> Result<Self, FrameError> {
Frame2::from_orthonormal(r.origin, r.x, r.y)
}
}
impl Frame2 {
pub fn identity() -> Self {
Frame2 {
origin: Point2::origin(),
x: Vec2::x_axis(),
y: Vec2::y_axis(),
}
}
pub fn new(origin: Point2, x: Vec2, handedness: Handedness) -> Result<Self, FrameError> {
if !(origin.coords.iter().all(|c| c.is_finite()) && x.iter().all(|c| c.is_finite())) {
return Err(FrameError::NonFinite);
}
let x = UnitVec2::try_new(x, 0.0).ok_or(FrameError::ZeroAxis)?;
let y = match handedness {
Handedness::Right => Vec2::new(-x.y, x.x),
Handedness::Left => Vec2::new(x.y, -x.x),
};
Ok(Frame2 {
origin,
x,
y: UnitVec2::new_unchecked(y),
})
}
pub fn from_orthonormal(origin: Point2, x: Vec2, y: Vec2) -> Result<Self, FrameError> {
let finite = |v: &Vec2| v.iter().all(|c| c.is_finite());
if !(finite(&origin.coords) && finite(&x) && finite(&y)) {
return Err(FrameError::NonFinite);
}
let unit = |v: &Vec2| crate::is_negligible(v.norm() - 1.0, 1.0);
if !(unit(&x) && unit(&y)) || !crate::is_negligible(x.dot(&y), 1.0) {
return Err(FrameError::NotOrthonormal);
}
Ok(Frame2 {
origin,
x: UnitVec2::new_unchecked(x),
y: UnitVec2::new_unchecked(y),
})
}
pub const fn origin(&self) -> Point2 {
self.origin
}
pub fn translated(&self, by: Vec2) -> Frame2 {
Frame2 {
origin: self.origin + by,
x: self.x,
y: self.y,
}
}
pub const fn x(&self) -> UnitVec2 {
self.x
}
pub const fn y(&self) -> UnitVec2 {
self.y
}
pub fn handedness(&self) -> Handedness {
if self.is_right_handed() {
Handedness::Right
} else {
Handedness::Left
}
}
pub fn is_right_handed(&self) -> bool {
self.x.perp(&self.y) > 0.0
}
pub fn to_local(&self, p: Point2) -> Point2 {
Point2::from(self.vec_to_local(p - self.origin))
}
pub fn to_world(&self, p: Point2) -> Point2 {
self.origin + self.vec_to_world(p.coords)
}
pub fn vec_to_local(&self, v: Vec2) -> Vec2 {
Vec2::new(v.dot(&self.x), v.dot(&self.y))
}
pub fn vec_to_world(&self, v: Vec2) -> Vec2 {
v.x * self.x.into_inner() + v.y * self.y.into_inner()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn world_frame_is_the_identity() {
let w = Frame::world();
let p = Point3::new(1.0, -2.0, 3.0);
assert_eq!(w.to_local(p), p);
assert_eq!(w.to_world(p), p);
assert_eq!(w.rotation(), UnitQuaternion::identity());
}
#[test]
fn from_z_follows_the_axis_rule() {
let f = Frame::from_z(Point3::origin(), Vec3::z()).unwrap();
assert_eq!(f.x().into_inner(), Vec3::x());
assert_eq!(f.y().into_inner(), Vec3::y());
let f = Frame::from_z(Point3::origin(), Vec3::x()).unwrap();
assert_eq!(f.x().into_inner(), Vec3::z());
assert_eq!(f.y().into_inner(), -Vec3::y());
let f = Frame::from_z(Point3::origin(), Vec3::y()).unwrap();
assert_eq!(f.x().into_inner(), Vec3::z());
assert_eq!(f.y().into_inner(), Vec3::x());
}
#[test]
fn errors_name_the_problem() {
let o = Point3::origin();
assert_eq!(
Frame::new(o, Vec3::zeros(), Vec3::x()),
Err(FrameError::ZeroAxis)
);
assert_eq!(
Frame::new(o, Vec3::z(), Vec3::z() * 2.0),
Err(FrameError::DegenerateHint)
);
assert_eq!(
Frame::new(o, Vec3::z(), Vec3::zeros()),
Err(FrameError::DegenerateHint)
);
assert_eq!(
Frame::new(o, Vec3::new(f64::NAN, 0.0, 1.0), Vec3::x()),
Err(FrameError::NonFinite)
);
assert_eq!(Frame::from_z(o, Vec3::zeros()), Err(FrameError::ZeroAxis));
assert_eq!(
Frame2::new(Point2::origin(), Vec2::zeros(), Handedness::Right),
Err(FrameError::ZeroAxis)
);
}
#[test]
fn frame2_handedness_round_trips() {
for h in [Handedness::Right, Handedness::Left] {
let f = Frame2::new(Point2::new(0.5, -0.5), Vec2::new(3.0, 4.0), h).unwrap();
assert_eq!(f.handedness(), h);
let p = Point2::new(0.3, 0.9);
assert!((f.to_local(f.to_world(p)) - p).norm() < 1e-15);
assert!(f.x().dot(&f.y()).abs() < 1e-15);
}
assert!(Frame2::identity().is_right_handed());
}
}