use crate::{
geometry::{Quaternion, Vector3},
time::{Stamp, TimePoint, Timestamp},
};
use alloc::string::String;
use approx::{AbsDiffEq, RelativeEq};
use core::ops::Mul;
pub use error::TransformError;
pub use traits::{Localized, Transformable};
mod error;
mod traits;
pub const UNIT_NORM_TOLERANCE: f64 = 1e-6;
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "serde",
serde(
try_from = "TransformRepr<T>",
bound(deserialize = "T: TimePoint + serde::Deserialize<'de>")
)
)]
#[non_exhaustive]
pub struct Transform<T = Timestamp>
where
T: TimePoint,
{
translation: Vector3,
rotation: Quaternion,
timestamp: Stamp<T>,
parent: String,
child: String,
}
impl<T> Transform<T>
where
T: TimePoint,
{
pub fn new(
parent: &str,
child: &str,
translation: Vector3,
rotation: Quaternion,
timestamp: Stamp<T>,
) -> Result<Self, TransformError> {
let transform = Self::unvalidated(
parent.into(),
child.into(),
translation,
rotation,
timestamp,
);
transform.validate()?;
Ok(transform)
}
pub fn static_between(
parent: &str,
child: &str,
translation: Vector3,
rotation: Quaternion,
) -> Result<Self, TransformError> {
Self::new(parent, child, translation, rotation, Stamp::Static)
}
pub(crate) fn unvalidated(
parent: String,
child: String,
translation: Vector3,
rotation: Quaternion,
timestamp: Stamp<T>,
) -> Self {
Self {
translation,
rotation,
timestamp,
parent,
child,
}
}
#[must_use]
pub fn translation(&self) -> Vector3 {
self.translation
}
#[must_use]
pub fn rotation(&self) -> Quaternion {
self.rotation
}
#[must_use]
pub fn timestamp(&self) -> Stamp<T> {
self.timestamp
}
#[must_use]
pub fn parent(&self) -> &str {
&self.parent
}
#[must_use]
pub fn child(&self) -> &str {
&self.child
}
pub fn validate(&self) -> Result<(), TransformError> {
let t = self.translation;
let q = self.rotation;
let finite = t.x.is_finite()
&& t.y.is_finite()
&& t.z.is_finite()
&& q.w.is_finite()
&& q.x.is_finite()
&& q.y.is_finite()
&& q.z.is_finite();
if !finite {
return Err(TransformError::NonFiniteValues);
}
let norm = q.norm();
if (norm - 1.0).abs() > UNIT_NORM_TOLERANCE {
return Err(TransformError::NonUnitRotation(norm));
}
Ok(())
}
pub fn interpolate(
from: &Transform<T>,
to: &Transform<T>,
timestamp: T,
) -> Result<Transform<T>, TransformError> {
let (Stamp::At(from_time), Stamp::At(to_time)) = (from.timestamp, to.timestamp) else {
return Err(TransformError::StaticInterpolation);
};
if from_time > to_time {
return Err(TransformError::TimestampMismatch {
lhs: from_time.as_seconds_lossy(),
rhs: to_time.as_seconds_lossy(),
});
}
if timestamp < from_time || timestamp > to_time {
return Err(TransformError::TimestampOutOfRange {
requested: timestamp.as_seconds_lossy(),
start: from_time.as_seconds_lossy(),
end: to_time.as_seconds_lossy(),
});
}
if from.child != to.child || from.parent != to.parent {
return Err(TransformError::IncompatibleFrames {
expected: alloc::format!("{} -> {}", from.parent, from.child),
found: alloc::format!("{} -> {}", to.parent, to.child),
});
}
let range = to_time.duration_since(from_time)?;
if range.is_zero() {
return Ok(from.clone());
}
let diff = timestamp.duration_since(from_time)?;
let ratio = diff.as_secs_f64() / range.as_secs_f64();
Ok(Self::unvalidated(
from.parent.clone(),
from.child.clone(),
(1.0 - ratio) * from.translation + ratio * to.translation,
from.rotation.slerp(to.rotation, ratio),
Stamp::At(timestamp),
))
}
pub fn inverse(&self) -> Result<Self, TransformError> {
let q = self.rotation.normalize()?;
let inverse_rotation = q.conjugate();
let inverse_translation = -1.0 * (inverse_rotation.rotate_vector(self.translation));
if !inverse_translation.x.is_finite()
|| !inverse_translation.y.is_finite()
|| !inverse_translation.z.is_finite()
{
return Err(TransformError::NonFiniteValues);
}
Ok(Self::unvalidated(
self.child.clone(),
self.parent.clone(),
inverse_translation,
inverse_rotation,
self.timestamp,
))
}
#[must_use]
pub(crate) fn restamped(
mut self,
timestamp: Stamp<T>,
) -> Self {
self.timestamp = timestamp;
self
}
pub(crate) fn compose_ignoring_time(
self,
rhs: Transform<T>,
) -> Result<Transform<T>, TransformError> {
if self.child == rhs.child {
return Err(TransformError::SameFrameMultiplication { frame: rhs.child });
}
if self.child != rhs.parent {
return Err(TransformError::IncompatibleFrames {
expected: self.child,
found: rhs.parent,
});
}
let rotation = self.rotation * rhs.rotation;
let translation = self.rotation.rotate_vector(rhs.translation) + self.translation;
Ok(Self::unvalidated(
self.parent,
rhs.child,
translation,
rotation,
self.timestamp,
))
}
}
impl<T> Mul for Transform<T>
where
T: TimePoint,
{
type Output = Result<Transform<T>, TransformError>;
#[inline]
fn mul(
self,
rhs: Transform<T>,
) -> Self::Output {
let timestamp = match (self.timestamp, rhs.timestamp) {
(Stamp::Static, rhs_stamp) => rhs_stamp,
(self_stamp, Stamp::Static) => self_stamp,
(Stamp::At(lhs), Stamp::At(rhs_time)) => {
if lhs != rhs_time {
return Err(TransformError::TimestampMismatch {
lhs: lhs.as_seconds_lossy(),
rhs: rhs_time.as_seconds_lossy(),
});
}
Stamp::At(lhs)
}
};
let mut result = self.compose_ignoring_time(rhs)?;
result.timestamp = timestamp;
Ok(result)
}
}
#[cfg(feature = "serde")]
#[derive(serde::Deserialize)]
#[serde(rename = "Transform")]
#[serde(bound(deserialize = "T: TimePoint + serde::Deserialize<'de>"))]
struct TransformRepr<T>
where
T: TimePoint,
{
translation: Vector3,
rotation: Quaternion,
timestamp: Stamp<T>,
parent: String,
child: String,
}
#[cfg(feature = "serde")]
impl<T> TryFrom<TransformRepr<T>> for Transform<T>
where
T: TimePoint,
{
type Error = TransformError;
fn try_from(repr: TransformRepr<T>) -> Result<Self, Self::Error> {
let transform = Self::unvalidated(
repr.parent,
repr.child,
repr.translation,
repr.rotation,
repr.timestamp,
);
transform.validate()?;
Ok(transform)
}
}
impl<T> AbsDiffEq for Transform<T>
where
T: TimePoint,
{
type Epsilon = f64;
fn default_epsilon() -> Self::Epsilon {
f64::EPSILON
}
fn abs_diff_eq(
&self,
other: &Self,
epsilon: Self::Epsilon,
) -> bool {
self.translation.abs_diff_eq(&other.translation, epsilon)
&& self.rotation.abs_diff_eq(&other.rotation, epsilon)
&& self.timestamp == other.timestamp
&& self.parent == other.parent
&& self.child == other.child
}
}
impl<T> RelativeEq for Transform<T>
where
T: TimePoint,
{
fn default_max_relative() -> Self::Epsilon {
f64::EPSILON
}
fn relative_eq(
&self,
other: &Self,
epsilon: Self::Epsilon,
max_relative: Self::Epsilon,
) -> bool {
self.translation
.relative_eq(&other.translation, epsilon, max_relative)
&& self
.rotation
.relative_eq(&other.rotation, epsilon, max_relative)
&& self.timestamp == other.timestamp
&& self.parent == other.parent
&& self.child == other.child
}
}
#[cfg(test)]
mod tests;