use crate::CartesianTreeError;
use crate::Pose;
use crate::lazy_access::{LazyRotation, LazyTranslation};
use crate::rotation::{MIN_QUATERNION_NORM, Rotation};
use crate::tree::{HasChildren, HasParent, NodeEquality};
use nalgebra::{Isometry3, Quaternion, Translation3, UnitQuaternion, Vector3};
use serde::{Deserialize, Serialize};
use slotmap::SlotMap;
use std::ops::{Add, Mul, Sub};
use std::sync::{Arc, PoisonError, RwLock, RwLockReadGuard, RwLockWriteGuard};
use uuid::Uuid;
slotmap::new_key_type! {
pub(crate) struct NodeKey;
}
#[derive(Debug)]
struct Node {
name: String,
parent: Option<NodeKey>,
children: Vec<NodeKey>,
transform_to_parent: Isometry3<f64>,
}
#[derive(Debug)]
pub(crate) struct TreeInner {
nodes: SlotMap<NodeKey, Node>,
}
pub(crate) type SharedTree = Arc<RwLock<TreeInner>>;
pub(crate) fn read_tree(tree: &SharedTree) -> RwLockReadGuard<'_, TreeInner> {
tree.read().unwrap_or_else(PoisonError::into_inner)
}
pub(crate) fn write_tree(tree: &SharedTree) -> RwLockWriteGuard<'_, TreeInner> {
tree.write().unwrap_or_else(PoisonError::into_inner)
}
impl TreeInner {
fn node(&self, key: NodeKey) -> Result<&Node, CartesianTreeError> {
self.nodes.get(key).ok_or(CartesianTreeError::FrameRemoved)
}
fn node_mut(&mut self, key: NodeKey) -> Result<&mut Node, CartesianTreeError> {
self.nodes
.get_mut(key)
.ok_or(CartesianTreeError::FrameRemoved)
}
pub(crate) fn contains(&self, key: NodeKey) -> bool {
self.nodes.contains_key(key)
}
pub(crate) fn name_of(&self, key: NodeKey) -> String {
self.nodes
.get(key)
.map_or_else(|| "<removed>".to_owned(), |node| node.name.clone())
}
fn add_child_node(
&mut self,
parent: NodeKey,
name: String,
transform: Isometry3<f64>,
) -> Result<NodeKey, CartesianTreeError> {
let children = self.node(parent)?.children.clone();
if children
.iter()
.any(|&child| self.nodes.get(child).is_some_and(|node| node.name == name))
{
return Err(CartesianTreeError::ChildNameConflict(
name,
self.name_of(parent),
));
}
let key = self.nodes.insert(Node {
name,
parent: Some(parent),
children: Vec::new(),
transform_to_parent: transform,
});
self.nodes[parent].children.push(key);
Ok(key)
}
fn remove_subtree(&mut self, key: NodeKey) {
if let Some(node) = self.nodes.remove(key) {
for child in node.children {
self.remove_subtree(child);
}
}
}
fn depth_of(&self, key: NodeKey) -> Result<usize, CartesianTreeError> {
let mut depth = 0;
let mut current = key;
while let Some(parent) = self.node(current)?.parent {
depth += 1;
current = parent;
}
Ok(depth)
}
pub(crate) fn lca(
&self,
a: NodeKey,
b: NodeKey,
) -> Result<Option<NodeKey>, CartesianTreeError> {
let mut own = a;
let mut other = b;
let mut own_depth = self.depth_of(own)?;
let mut other_depth = self.depth_of(other)?;
while own_depth > other_depth {
own = self.node(own)?.parent.expect("depth guarantees a parent");
own_depth -= 1;
}
while other_depth > own_depth {
other = self.node(other)?.parent.expect("depth guarantees a parent");
other_depth -= 1;
}
while own != other {
match (self.node(own)?.parent, self.node(other)?.parent) {
(Some(own_parent), Some(other_parent)) => {
own = own_parent;
other = other_parent;
}
_ => return Ok(None),
}
}
Ok(Some(own))
}
pub(crate) fn transform_up(
&self,
start: NodeKey,
start_offset: Isometry3<f64>,
target: NodeKey,
) -> Result<Isometry3<f64>, CartesianTreeError> {
let mut transform = start_offset;
let mut current = start;
while current != target {
let node = self.node(current)?;
let Some(parent) = node.parent else {
return Err(CartesianTreeError::IsNoAncestor(
self.name_of(target),
self.name_of(start),
));
};
transform = node.transform_to_parent * transform;
current = parent;
}
Ok(transform)
}
}
#[derive(Clone, Debug)]
pub(crate) enum FrameKind {
Node(NodeKey),
Derived {
anchor: NodeKey,
offset: Isometry3<f64>,
name: String,
},
}
impl FrameKind {
pub(crate) const fn anchor(&self) -> NodeKey {
match self {
Self::Node(key) => *key,
Self::Derived { anchor, .. } => *anchor,
}
}
pub(crate) fn offset(&self) -> Isometry3<f64> {
match self {
Self::Node(_) => Isometry3::identity(),
Self::Derived { offset, .. } => *offset,
}
}
}
#[derive(Clone)]
pub struct Frame {
pub(crate) tree: SharedTree,
pub(crate) kind: FrameKind,
}
impl std::fmt::Debug for Frame {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Frame")
.field("kind", &self.kind)
.finish_non_exhaustive()
}
}
impl Frame {
pub fn new_origin(name: impl Into<String>) -> Self {
let mut nodes = SlotMap::with_key();
let root = nodes.insert(Node {
name: name.into(),
parent: None,
children: Vec::new(),
transform_to_parent: Isometry3::identity(),
});
Self {
tree: Arc::new(RwLock::new(TreeInner { nodes })),
kind: FrameKind::Node(root),
}
}
fn read(&self) -> RwLockReadGuard<'_, TreeInner> {
read_tree(&self.tree)
}
fn write(&self) -> RwLockWriteGuard<'_, TreeInner> {
write_tree(&self.tree)
}
fn node_key(&self) -> Result<NodeKey, CartesianTreeError> {
match &self.kind {
FrameKind::Node(key) => Ok(*key),
FrameKind::Derived { name, .. } => {
Err(CartesianTreeError::DerivedFrameUnsupported(name.clone()))
}
}
}
#[must_use]
pub fn name(&self) -> String {
match &self.kind {
FrameKind::Node(key) => self.read().name_of(*key),
FrameKind::Derived { name, .. } => name.clone(),
}
}
pub fn transformation(&self) -> Result<Isometry3<f64>, CartesianTreeError> {
match &self.kind {
FrameKind::Node(key) => {
let guard = self.read();
let node = guard.node(*key)?;
if node.parent.is_none() {
return Err(CartesianTreeError::RootHasNoParent(node.name.clone()));
}
Ok(node.transform_to_parent)
}
FrameKind::Derived { offset, .. } => Ok(*offset),
}
}
fn local_transform(&self) -> Result<Isometry3<f64>, CartesianTreeError> {
match &self.kind {
FrameKind::Node(key) => Ok(self.read().node(*key)?.transform_to_parent),
FrameKind::Derived { offset, .. } => Ok(*offset),
}
}
pub fn position(&self) -> Result<Vector3<f64>, CartesianTreeError> {
Ok(self.local_transform()?.translation.vector)
}
pub fn orientation(&self) -> Result<Rotation, CartesianTreeError> {
Ok(self.local_transform()?.rotation.into())
}
pub fn set(
&self,
position: Vector3<f64>,
orientation: impl Into<Rotation>,
) -> Result<(), CartesianTreeError> {
let transform = Isometry3::from_parts(
Translation3::from(position),
orientation.into().as_quaternion(),
);
self.update_transform(|_| transform)
}
pub fn apply_in_parent_frame(
&self,
isometry: &Isometry3<f64>,
) -> Result<(), CartesianTreeError> {
self.update_transform(|current| isometry * current)
}
pub fn apply_in_local_frame(
&self,
isometry: &Isometry3<f64>,
) -> Result<(), CartesianTreeError> {
self.update_transform(|current| current * isometry)
}
fn update_transform(
&self,
update: impl FnOnce(Isometry3<f64>) -> Isometry3<f64>,
) -> Result<(), CartesianTreeError> {
let key = self.node_key()?;
let mut guard = self.write();
let node = guard.node_mut(key)?;
if node.parent.is_none() {
return Err(CartesianTreeError::CannotUpdateRootTransform(
node.name.clone(),
));
}
node.transform_to_parent = update(node.transform_to_parent);
Ok(())
}
pub fn add_child(
&self,
name: impl Into<String>,
position: Vector3<f64>,
orientation: impl Into<Rotation>,
) -> Result<Self, CartesianTreeError> {
let key = self.node_key()?;
let transform = Isometry3::from_parts(
Translation3::from(position),
orientation.into().as_quaternion(),
);
let child_key = self.write().add_child_node(key, name.into(), transform)?;
Ok(Self {
tree: Arc::clone(&self.tree),
kind: FrameKind::Node(child_key),
})
}
pub fn remove_child(&self, name: &str) -> Result<(), CartesianTreeError> {
let key = self.node_key()?;
let mut guard = self.write();
let children = guard.node(key)?.children.clone();
let child_key = children
.iter()
.copied()
.find(|&child| guard.nodes.get(child).is_some_and(|node| node.name == name))
.ok_or_else(|| {
CartesianTreeError::ChildNotFound(name.to_owned(), guard.name_of(key))
})?;
guard.node_mut(key)?.children.retain(|&c| c != child_key);
guard.remove_subtree(child_key);
Ok(())
}
pub fn calibrate_child(
&self,
name: impl Into<String>,
desired_position: Vector3<f64>,
desired_orientation: impl Into<Rotation>,
reference_pose: &Pose,
) -> Result<Self, CartesianTreeError> {
let key = self.node_key()?;
if !Arc::ptr_eq(&self.tree, &reference_pose.tree) {
return Err(CartesianTreeError::DifferentTrees(
self.name(),
reference_pose
.frame()
.map_or_else(|| "<removed>".to_owned(), |frame| frame.name()),
));
}
let desired_pose = Isometry3::from_parts(
Translation3::from(desired_position),
desired_orientation.into().as_quaternion(),
);
let mut guard = self.write();
let reference_anchor = reference_pose.anchor.anchor();
let ancestor = guard.lca(key, reference_anchor)?.ok_or_else(|| {
CartesianTreeError::NoCommonAncestor(
guard.name_of(key),
guard.name_of(reference_anchor),
)
})?;
let t_pose_to_reference_anchor =
reference_pose.anchor.offset() * reference_pose.transformation();
let t_pose_to_ancestor =
guard.transform_up(reference_anchor, t_pose_to_reference_anchor, ancestor)?;
let t_parent_to_ancestor = guard.transform_up(key, Isometry3::identity(), ancestor)?;
let t_calibrated_to_parent =
t_parent_to_ancestor.inverse() * t_pose_to_ancestor * desired_pose.inverse();
let child_key = guard.add_child_node(key, name.into(), t_calibrated_to_parent)?;
Ok(Self {
tree: Arc::clone(&self.tree),
kind: FrameKind::Node(child_key),
})
}
pub fn add_pose(&self, position: Vector3<f64>, orientation: impl Into<Rotation>) -> Pose {
Pose::new(
Arc::clone(&self.tree),
self.kind.clone(),
position,
orientation,
)
}
pub fn to_json(&self) -> Result<String, CartesianTreeError> {
let key = self.node_key()?;
let guard = self.read();
let serial = to_serial(&guard, key)?;
Ok(serde_json::to_string_pretty(&serial)?)
}
pub fn apply_config(&self, json: &str) -> Result<(), CartesianTreeError> {
let key = self.node_key()?;
let serial: SerialFrame = serde_json::from_str(json)?;
let mut guard = self.write();
apply_serial(&mut guard, key, &serial)
}
fn derive_in_parent_frame(&self, isometry: &Isometry3<f64>) -> Self {
let (anchor, offset) = match &self.kind {
FrameKind::Node(key) => {
let transform = self
.read()
.nodes
.get(*key)
.map_or_else(Isometry3::identity, |node| node.transform_to_parent);
(*key, transform.inverse() * isometry * transform)
}
FrameKind::Derived { anchor, offset, .. } => (*anchor, isometry * offset),
};
Self {
tree: Arc::clone(&self.tree),
kind: FrameKind::Derived {
anchor,
offset,
name: Uuid::new_v4().to_string(),
},
}
}
fn derive_in_local_frame(&self, isometry: &Isometry3<f64>) -> Self {
let (anchor, offset) = match &self.kind {
FrameKind::Node(key) => (*key, *isometry),
FrameKind::Derived { anchor, offset, .. } => (*anchor, offset * isometry),
};
Self {
tree: Arc::clone(&self.tree),
kind: FrameKind::Derived {
anchor,
offset,
name: Uuid::new_v4().to_string(),
},
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
struct SerialFrame {
name: String,
position: Vector3<f64>,
orientation: Quaternion<f64>,
children: Vec<Self>,
}
fn to_serial(inner: &TreeInner, key: NodeKey) -> Result<SerialFrame, CartesianTreeError> {
let node = inner.node(key)?;
let (position, orientation) = if node.parent.is_some() {
(
node.transform_to_parent.translation.vector,
node.transform_to_parent.rotation.into_inner(),
)
} else {
(Vector3::zeros(), Quaternion::identity())
};
Ok(SerialFrame {
name: node.name.clone(),
position,
orientation,
children: node
.children
.iter()
.map(|&child| to_serial(inner, child))
.collect::<Result<_, _>>()?,
})
}
fn apply_serial(
inner: &mut TreeInner,
key: NodeKey,
serial: &SerialFrame,
) -> Result<(), CartesianTreeError> {
let node = inner.node(key)?;
if node.name != serial.name {
return Err(CartesianTreeError::Mismatch(format!(
"Frame names do not match: {} vs {}",
node.name, serial.name
)));
}
if node.parent.is_some() {
let orientation = UnitQuaternion::try_new(serial.orientation, MIN_QUATERNION_NORM)
.ok_or_else(|| {
let q = &serial.orientation;
CartesianTreeError::InvalidQuaternion(q.i, q.j, q.k, q.w)
})?;
inner.node_mut(key)?.transform_to_parent =
Isometry3::from_parts(Translation3::from(serial.position), orientation);
}
for potential_child in &serial.children {
let children = inner.node(key)?.children.clone();
let matching = children.iter().copied().find(|&child| {
inner
.nodes
.get(child)
.is_some_and(|node| node.name == potential_child.name)
});
if let Some(child_key) = matching {
apply_serial(inner, child_key, potential_child)?;
}
}
Ok(())
}
impl Add<LazyTranslation> for &Frame {
type Output = Frame;
fn add(self, rhs: LazyTranslation) -> Self::Output {
self.derive_in_parent_frame(&rhs.inner)
}
}
impl Sub<LazyTranslation> for &Frame {
type Output = Frame;
fn sub(self, rhs: LazyTranslation) -> Self::Output {
self.derive_in_parent_frame(&rhs.inner.inverse())
}
}
impl Mul<LazyRotation> for &Frame {
type Output = Frame;
fn mul(self, rhs: LazyRotation) -> Self::Output {
self.derive_in_local_frame(&rhs.inner)
}
}
impl HasParent for Frame {
type Node = Self;
fn parent(&self) -> Option<Self::Node> {
let guard = self.read();
let parent_key = match &self.kind {
FrameKind::Node(key) => guard.nodes.get(*key)?.parent?,
FrameKind::Derived { anchor, .. } => {
if !guard.contains(*anchor) {
return None;
}
*anchor
}
};
drop(guard);
Some(Self {
tree: Arc::clone(&self.tree),
kind: FrameKind::Node(parent_key),
})
}
}
impl NodeEquality for Frame {
fn is_same(&self, other: &Self) -> bool {
if !Arc::ptr_eq(&self.tree, &other.tree) {
return false;
}
match (&self.kind, &other.kind) {
(FrameKind::Node(own), FrameKind::Node(other)) => own == other,
(FrameKind::Derived { name: own, .. }, FrameKind::Derived { name: other, .. }) => {
own == other
}
_ => false,
}
}
}
impl HasChildren for Frame {
type Node = Self;
fn children(&self) -> Vec<Self> {
let FrameKind::Node(key) = &self.kind else {
return Vec::new();
};
let guard = self.read();
let Some(node) = guard.nodes.get(*key) else {
return Vec::new();
};
node.children
.iter()
.map(|&child| Self {
tree: Arc::clone(&self.tree),
kind: FrameKind::Node(child),
})
.collect()
}
}
#[cfg(test)]
mod tests {
use crate::lazy_access::{rx, ry, rz, x, y, z};
use super::*;
use crate::tree::Walking;
use approx::assert_relative_eq;
use nalgebra::{UnitQuaternion, Vector3};
#[test]
fn frame_and_pose_are_send_and_sync() {
const fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Frame>();
assert_send_sync::<Pose>();
}
#[test]
fn create_origin_frame() {
let root = Frame::new_origin("world");
assert_eq!(root.name(), "world");
assert!(root.parent().is_none());
assert!(root.children().is_empty());
}
#[test]
fn add_child_frame_with_quaternion() {
let root = Frame::new_origin("world");
let child = root
.add_child(
"dummy",
Vector3::new(1.0, 0.0, 0.0),
UnitQuaternion::identity(),
)
.unwrap();
assert_eq!(root.children().len(), 1);
assert_eq!(child.name(), "dummy");
assert_eq!(child.parent().unwrap().name(), "world");
}
#[test]
fn add_child_frame_with_rpy() {
let root = Frame::new_origin("world");
let child = root
.add_child(
"dummy",
Vector3::new(0.0, 1.0, 0.0),
Rotation::from_rpy(0.0, 0.0, std::f64::consts::FRAC_PI_2),
)
.unwrap();
assert_eq!(child.name(), "dummy");
let rotation = child.transformation().unwrap().rotation;
let expected = UnitQuaternion::from_euler_angles(0.0, 0.0, std::f64::consts::FRAC_PI_2);
assert!((rotation.angle() - expected.angle()).abs() < 1e-10);
}
#[test]
fn test_child_frame_transform_to_parent() {
let root = Frame::new_origin("world");
let child = root
.add_child(
"dummy",
Vector3::new(0.0, 0.0, 1.0),
UnitQuaternion::identity(),
)
.unwrap();
let transform = child.transformation().unwrap();
assert_eq!(transform.translation.vector, Vector3::new(0.0, 0.0, 1.0));
assert_eq!(transform.rotation, UnitQuaternion::identity());
assert_eq!(child.position().unwrap(), Vector3::new(0.0, 0.0, 1.0));
assert_eq!(
child.orientation().unwrap().as_quaternion(),
UnitQuaternion::identity()
);
}
#[test]
fn multiple_child_frames() {
let root = Frame::new_origin("world");
let a = root
.add_child("a", Vector3::new(1.0, 0.0, 0.0), UnitQuaternion::identity())
.unwrap();
let b = root
.add_child("b", Vector3::new(0.0, 1.0, 0.0), UnitQuaternion::identity())
.unwrap();
assert_eq!(root.children().len(), 2);
assert_eq!(a.parent().unwrap().name(), "world");
assert_eq!(b.parent().unwrap().name(), "world");
}
#[test]
fn test_remove_child() {
let root = Frame::new_origin("root");
let child = root
.add_child(
"child",
Vector3::new(1.0, 0.0, 0.0),
UnitQuaternion::identity(),
)
.unwrap();
let grandchild = child
.add_child("grandchild", Vector3::zeros(), UnitQuaternion::identity())
.unwrap();
root.remove_child("child").unwrap();
assert!(root.children().is_empty());
assert!(matches!(
child.transformation(),
Err(CartesianTreeError::FrameRemoved)
));
assert!(matches!(
grandchild.transformation(),
Err(CartesianTreeError::FrameRemoved)
));
assert_eq!(child.name(), "<removed>");
assert!(child.parent().is_none());
assert!(
root.add_child("child", Vector3::zeros(), UnitQuaternion::identity())
.is_ok()
);
assert!(matches!(
root.remove_child("unknown"),
Err(CartesianTreeError::ChildNotFound(..))
));
}
#[test]
fn test_tree_stays_alive_through_any_handle() {
let leaf = {
let root = Frame::new_origin("root");
let mid = root
.add_child(
"mid",
Vector3::new(1.0, 0.0, 0.0),
UnitQuaternion::identity(),
)
.unwrap();
mid.add_child(
"leaf",
Vector3::new(0.0, 2.0, 0.0),
UnitQuaternion::identity(),
)
.unwrap()
};
assert_eq!(leaf.root().name(), "root");
assert_eq!(leaf.depth(), 2);
assert!(leaf.transformation().is_ok());
let leaf_in_root = leaf
.add_pose(Vector3::zeros(), UnitQuaternion::identity())
.in_frame(&leaf.root())
.unwrap()
.transformation();
assert_relative_eq!(
leaf_in_root.translation.vector,
Vector3::new(1.0, 2.0, 0.0),
epsilon = 1e-10
);
}
#[test]
fn test_threaded_access() {
let root = Frame::new_origin("root");
let child = root
.add_child("child", Vector3::zeros(), UnitQuaternion::identity())
.unwrap();
let handles: Vec<_> = (0..4)
.map(|i| {
let child = child.clone();
let root = root.clone();
std::thread::spawn(move || {
for j in 0..100 {
child
.set(
Vector3::new(f64::from(j), 0.0, f64::from(i)),
UnitQuaternion::identity(),
)
.unwrap();
let pose = root.add_pose(Vector3::zeros(), UnitQuaternion::identity());
pose.in_frame(&child).unwrap();
}
})
})
.collect();
for handle in handles {
handle.join().unwrap();
}
}
#[test]
fn reject_duplicate_child_name() {
let root = Frame::new_origin("world");
let _ = root
.add_child(
"duplicate",
Vector3::new(1.0, 0.0, 0.0),
UnitQuaternion::identity(),
)
.unwrap();
let result = root.add_child(
"duplicate",
Vector3::new(2.0, 0.0, 0.0),
UnitQuaternion::identity(),
);
assert!(result.is_err());
}
#[test]
fn test_chained_lazy_frames_survive_intermediate_drop() {
let root = Frame::new_origin("root");
let derived = {
let intermediate = &root + z(5.0);
&intermediate - y(3.0)
};
let derived_in_root = derived
.add_pose(Vector3::zeros(), UnitQuaternion::identity())
.in_frame(&root)
.unwrap()
.transformation();
assert_relative_eq!(
derived_in_root.translation.vector,
Vector3::new(0.0, -3.0, 5.0),
epsilon = 1e-10
);
}
#[test]
fn test_derived_frames_are_read_only() {
let root = Frame::new_origin("root");
let derived = &root + z(5.0);
assert!(matches!(
derived.set(Vector3::zeros(), UnitQuaternion::identity()),
Err(CartesianTreeError::DerivedFrameUnsupported(_))
));
assert!(matches!(
derived.add_child("child", Vector3::zeros(), UnitQuaternion::identity()),
Err(CartesianTreeError::DerivedFrameUnsupported(_))
));
assert!(matches!(
derived.to_json(),
Err(CartesianTreeError::DerivedFrameUnsupported(_))
));
}
#[test]
fn test_add_pose_to_frame() {
let frame = Frame::new_origin("dummy");
let pose = frame.add_pose(Vector3::new(1.0, 2.0, 3.0), UnitQuaternion::identity());
assert_eq!(pose.frame().unwrap().name(), "dummy");
}
#[test]
fn test_set_transform() {
let root = Frame::new_origin("root");
let child = root
.add_child(
"dummy",
Vector3::new(0.0, 0.0, 1.0),
UnitQuaternion::identity(),
)
.unwrap();
child
.set(Vector3::new(1.0, 0.0, 0.0), UnitQuaternion::identity())
.unwrap();
assert_eq!(
child.transformation().unwrap().translation.vector,
Vector3::new(1.0, 0.0, 0.0)
);
assert!(
root.set(Vector3::new(1.0, 0.0, 0.0), UnitQuaternion::identity())
.is_err()
);
}
#[test]
fn test_apply_in_parent_frame() {
let root = Frame::new_origin("root");
let child = root
.add_child(
"dummy",
Vector3::new(1.0, 0.0, 1.0),
UnitQuaternion::identity(),
)
.unwrap();
child
.apply_in_parent_frame(&Isometry3::from_parts(
Translation3::identity(),
UnitQuaternion::from_euler_angles(0.0, 0.0, std::f64::consts::FRAC_PI_2),
))
.unwrap();
assert_relative_eq!(
child.transformation().unwrap().translation.vector,
Vector3::new(0.0, 1.0, 1.0),
epsilon = 1e-10
);
child
.apply_in_parent_frame(&Isometry3::from_parts(
Translation3::new(1.0, 0.0, 1.0),
UnitQuaternion::identity(),
))
.unwrap();
assert_relative_eq!(
child.transformation().unwrap().translation.vector,
Vector3::new(1.0, 1.0, 2.0),
epsilon = 1e-10
);
}
#[test]
fn test_apply_in_local_frame() {
let root = Frame::new_origin("root");
let child = root
.add_child(
"dummy",
Vector3::zeros(),
UnitQuaternion::from_euler_angles(0.0, 0.0, std::f64::consts::FRAC_PI_2),
)
.unwrap();
child
.apply_in_local_frame(&Isometry3::from_parts(
Translation3::new(1.0, 0.0, 0.0),
UnitQuaternion::identity(),
))
.unwrap();
assert_relative_eq!(
child.transformation().unwrap().translation.vector,
Vector3::new(0.0, 1.0, 0.0),
epsilon = 1e-10
);
child
.apply_in_local_frame(&Isometry3::from_parts(
Translation3::identity(),
UnitQuaternion::from_euler_angles(0.0, 0.0, std::f64::consts::FRAC_PI_2),
))
.unwrap();
assert_relative_eq!(
child.transformation().unwrap().translation.vector,
Vector3::new(0.0, 1.0, 0.0),
epsilon = 1e-10
);
let (roll, pitch, yaw) = child.transformation().unwrap().rotation.euler_angles();
assert_relative_eq!(roll, 0.0, epsilon = 1e-10);
assert_relative_eq!(pitch, 0.0, epsilon = 1e-10);
assert_relative_eq!(yaw, std::f64::consts::PI, epsilon = 1e-10);
}
#[test]
fn test_pose_apply_in_parent_frame() {
let root = Frame::new_origin("root");
let mut pose = root.add_pose(Vector3::new(1.0, 0.0, 1.0), UnitQuaternion::identity());
pose.apply_in_parent_frame(&Isometry3::from_parts(
Translation3::identity(),
UnitQuaternion::from_euler_angles(0.0, 0.0, std::f64::consts::FRAC_PI_2),
));
assert_relative_eq!(
pose.transformation().translation.vector,
Vector3::new(0.0, 1.0, 1.0),
epsilon = 1e-10
);
pose.apply_in_parent_frame(&Isometry3::from_parts(
Translation3::new(1.0, 0.0, 1.0),
UnitQuaternion::identity(),
));
assert_relative_eq!(
pose.transformation().translation.vector,
Vector3::new(1.0, 1.0, 2.0),
epsilon = 1e-10
);
}
#[test]
fn test_pose_apply_in_local_frame() {
let root = Frame::new_origin("root");
let mut pose = root.add_pose(
Vector3::zeros(),
UnitQuaternion::from_euler_angles(0.0, 0.0, std::f64::consts::FRAC_PI_2),
);
pose.apply_in_local_frame(&Isometry3::from_parts(
Translation3::new(1.0, 0.0, 0.0),
UnitQuaternion::identity(),
));
assert_relative_eq!(
pose.transformation().translation.vector,
Vector3::new(0.0, 1.0, 0.0),
epsilon = 1e-10
);
pose.apply_in_local_frame(&Isometry3::from_parts(
Translation3::identity(),
UnitQuaternion::from_euler_angles(0.0, 0.0, std::f64::consts::FRAC_PI_2),
));
assert_relative_eq!(
pose.transformation().translation.vector,
Vector3::new(0.0, 1.0, 0.0),
epsilon = 1e-10
);
let (roll, pitch, yaw) = pose.transformation().rotation.euler_angles();
assert_relative_eq!(roll, 0.0, epsilon = 1e-10);
assert_relative_eq!(pitch, 0.0, epsilon = 1e-10);
assert_relative_eq!(yaw, std::f64::consts::PI, epsilon = 1e-10);
}
#[test]
fn test_pose_transform_to_parent() {
let root = Frame::new_origin("root");
let pose = root.add_pose(Vector3::new(1.0, 2.0, 3.0), UnitQuaternion::identity());
let transformation = pose.transformation();
assert_eq!(
transformation.translation.vector,
Vector3::new(1.0, 2.0, 3.0)
);
assert_eq!(transformation.rotation, UnitQuaternion::identity());
assert_eq!(pose.position(), Vector3::new(1.0, 2.0, 3.0));
assert_eq!(
pose.orientation().as_quaternion(),
UnitQuaternion::identity()
);
}
#[test]
fn test_pose_transformation_between_frames() {
let root = Frame::new_origin("root");
let f1 = root
.add_child(
"f1",
Vector3::new(1.0, 0.0, 0.0),
UnitQuaternion::identity(),
)
.unwrap();
let f2 = f1
.add_child(
"f2",
Vector3::new(0.0, 2.0, 0.0),
UnitQuaternion::identity(),
)
.unwrap();
let pose_in_f2 = f2.add_pose(Vector3::new(1.0, 1.0, 0.0), UnitQuaternion::identity());
let pose_in_root = pose_in_f2.in_frame(&root).unwrap();
let pos = pose_in_root.transformation().translation.vector;
assert!((pos - Vector3::new(2.0, 3.0, 0.0)).norm() < 1e-6);
}
#[test]
fn test_pose_round_trip_through_deep_tree() {
let root = Frame::new_origin("root");
let a = root
.add_child(
"a",
Vector3::new(0.3, -1.2, 2.5),
UnitQuaternion::from_euler_angles(0.4, -0.3, 1.2),
)
.unwrap();
let b = a
.add_child(
"b",
Vector3::new(-2.0, 0.7, 0.1),
UnitQuaternion::from_euler_angles(-1.0, 0.2, 0.5),
)
.unwrap();
let c = root
.add_child(
"c",
Vector3::new(1.5, 2.0, -0.4),
UnitQuaternion::from_euler_angles(0.1, 1.1, -0.7),
)
.unwrap();
let d = c
.add_child(
"d",
Vector3::new(0.0, -0.5, 1.0),
UnitQuaternion::from_euler_angles(0.9, -0.8, 0.3),
)
.unwrap();
let pose = b.add_pose(
Vector3::new(0.2, 0.4, -0.6),
UnitQuaternion::from_euler_angles(0.5, 0.5, -0.5),
);
let round_tripped = pose.in_frame(&d).unwrap().in_frame(&b).unwrap();
let original = pose.transformation();
let result = round_tripped.transformation();
assert_relative_eq!(
result.translation.vector,
original.translation.vector,
epsilon = 1e-9
);
assert_relative_eq!(
result.rotation.angle_to(&original.rotation),
0.0,
epsilon = 1e-9
);
}
#[test]
fn test_in_frame_across_disjoint_trees_fails() {
let tree_1 = Frame::new_origin("tree_1");
let tree_2 = Frame::new_origin("tree_2");
let pose = tree_1.add_pose(Vector3::zeros(), UnitQuaternion::identity());
assert!(matches!(
pose.in_frame(&tree_2),
Err(CartesianTreeError::DifferentTrees(..))
));
}
#[test]
fn test_apply_config_malformed_json_fails() {
let root = Frame::new_origin("root");
assert!(matches!(
root.apply_config("not json"),
Err(CartesianTreeError::SerdeError(_))
));
}
#[test]
fn test_lazy_helpers_all_axes() {
use nalgebra::UnitQuaternion;
let root = Frame::new_origin("root");
let pose = root.add_pose(Vector3::zeros(), UnitQuaternion::identity());
let moved = &(&(&pose + x(1.0)) + y(2.0)) + z(3.0);
assert_relative_eq!(
moved.transformation().translation.vector,
Vector3::new(1.0, 2.0, 3.0),
epsilon = 1e-10
);
let rotated = &pose * rx(0.3);
assert_relative_eq!(
rotated
.transformation()
.rotation
.angle_to(&UnitQuaternion::from_euler_angles(0.3, 0.0, 0.0)),
0.0,
epsilon = 1e-10
);
let rotated = &pose * ry(0.4);
assert_relative_eq!(
rotated
.transformation()
.rotation
.angle_to(&UnitQuaternion::from_euler_angles(0.0, 0.4, 0.0)),
0.0,
epsilon = 1e-10
);
}
#[test]
fn test_calibrate_child() {
let root = Frame::new_origin("root");
let reference_pose = root.add_pose(
Vector3::new(1.0, 2.0, 3.0),
UnitQuaternion::from_euler_angles(0.0, 0.0, std::f64::consts::FRAC_PI_2),
);
let calibrated_frame = root
.calibrate_child(
"calibrated",
Vector3::zeros(),
UnitQuaternion::identity(),
&reference_pose,
)
.unwrap();
let pose_in_calibrated = reference_pose.in_frame(&calibrated_frame).unwrap();
let transformation = pose_in_calibrated.transformation();
assert!((transformation.translation.vector - Vector3::zeros()).norm() < 1e-6);
assert!((transformation.rotation.angle() - 0.0).abs() < 1e-6);
let calibrated_transformation = calibrated_frame.transformation().unwrap();
assert!(
(calibrated_transformation.translation.vector - Vector3::new(1.0, 2.0, 3.0)).norm()
< 1e-6
);
assert!(
(calibrated_transformation.rotation.angle() - std::f64::consts::FRAC_PI_2).abs() < 1e-6
);
}
#[test]
fn test_calibrate_child_under_non_identity_parent() {
let root = Frame::new_origin("root");
let mount = root
.add_child(
"mount",
Vector3::new(0.0, 0.0, 1.0),
UnitQuaternion::from_euler_angles(0.0, 0.0, std::f64::consts::FRAC_PI_2),
)
.unwrap();
let reference_pose = root.add_pose(
Vector3::new(1.0, 2.0, 3.0),
UnitQuaternion::from_euler_angles(0.0, 0.0, std::f64::consts::FRAC_PI_4),
);
let desired_position = Vector3::new(0.5, 0.0, 0.0);
let desired_orientation =
UnitQuaternion::from_euler_angles(0.0, 0.0, std::f64::consts::FRAC_PI_2);
let calibrated = mount
.calibrate_child(
"calibrated",
desired_position,
desired_orientation,
&reference_pose,
)
.unwrap();
let pose_in_calibrated = reference_pose.in_frame(&calibrated).unwrap();
let transformation = pose_in_calibrated.transformation();
assert_relative_eq!(
transformation.translation.vector,
desired_position,
epsilon = 1e-10
);
assert_relative_eq!(
transformation.rotation.angle_to(&desired_orientation),
0.0,
epsilon = 1e-10
);
}
#[test]
fn test_to_json_and_apply_config() {
let root = Frame::new_origin("root");
let _ = root
.add_child(
"child",
Vector3::new(1.0, 2.0, 3.0),
UnitQuaternion::from_euler_angles(0.1, 0.2, 0.3),
)
.unwrap();
let json = root.to_json().unwrap();
assert!(json.contains(r#""name": "root""#));
assert!(json.contains(r#""name": "child""#));
let default_root = Frame::new_origin("root");
default_root
.add_child(
"child",
Vector3::new(0.0, 0.0, 0.0),
UnitQuaternion::identity(),
)
.unwrap();
default_root.apply_config(&json).unwrap();
let updated_child = default_root
.children()
.into_iter()
.find(|c| c.name() == "child")
.unwrap();
let iso = updated_child.transformation().unwrap();
assert_eq!(iso.translation.vector, Vector3::new(1.0, 2.0, 3.0));
let (r, p, y) = iso.rotation.euler_angles();
assert!((r - 0.1).abs() < 1e-6);
assert!((p - 0.2).abs() < 1e-6);
assert!((y - 0.3).abs() < 1e-6);
let partial_json = r#"
{
"name": "root",
"position": [0.0, 0.0, 0.0],
"orientation": [0.0, 0.0, 0.0, 1.0],
"children": [
{
"name": "child",
"position": [4.0, 5.0, 6.0],
"orientation": [0.0, 0.0, 0.0, 1.0],
"children": []
},
{
"name": "extra",
"position": [0.0, 0.0, 0.0],
"orientation": [0.0, 0.0, 0.0, 1.0],
"children": []
}
]
}
"#;
default_root.apply_config(partial_json).unwrap();
let updated_child = default_root
.children()
.into_iter()
.find(|c| c.name() == "child")
.unwrap();
assert_eq!(
updated_child.transformation().unwrap().translation.vector,
Vector3::new(4.0, 5.0, 6.0)
);
let mismatch_json = r#"
{
"name": "wrong_root",
"position": [0.0, 0.0, 0.0],
"orientation": [0.0, 0.0, 0.0, 1.0],
"children": []
}
"#;
assert!(default_root.apply_config(mismatch_json).is_err());
}
#[test]
fn test_apply_config_validates_quaternions() {
let root = Frame::new_origin("root");
let child = root
.add_child("child", Vector3::zeros(), UnitQuaternion::identity())
.unwrap();
let scaled_json = r#"
{
"name": "root",
"position": [0.0, 0.0, 0.0],
"orientation": [0.0, 0.0, 0.0, 1.0],
"children": [
{
"name": "child",
"position": [0.0, 0.0, 0.0],
"orientation": [0.0, 0.0, 2.0, 0.0],
"children": []
}
]
}
"#;
root.apply_config(scaled_json).unwrap();
let q = child.orientation().unwrap().as_quaternion();
assert_relative_eq!(q.k, 1.0, epsilon = 1e-12);
assert_relative_eq!(q.w, 0.0, epsilon = 1e-12);
let zero_json = scaled_json.replace("2.0", "0.0");
assert!(matches!(
root.apply_config(&zero_json),
Err(CartesianTreeError::InvalidQuaternion(..))
));
}
#[test]
fn test_lazy_translation_frame() {
use nalgebra::UnitQuaternion;
let root = Frame::new_origin("root");
let child = root
.add_child(
"child",
Vector3::new(0.0, 0.0, 0.0),
UnitQuaternion::identity(),
)
.unwrap();
let result = &child + z(5.0);
assert_relative_eq!(
result.transformation().unwrap().translation.vector,
Vector3::new(0.0, 0.0, 5.0),
epsilon = 1e-10
);
assert_relative_eq!(
child.transformation().unwrap().translation.vector,
Vector3::new(0.0, 0.0, 0.0),
epsilon = 1e-10
);
let result = &result - y(3.0);
let result_in_root = result
.add_pose(Vector3::zeros(), UnitQuaternion::identity())
.in_frame(&root)
.unwrap()
.transformation();
assert_relative_eq!(
result_in_root.translation.vector,
Vector3::new(0.0, -3.0, 5.0),
epsilon = 1e-10
);
let (roll, pitch, yaw) = result_in_root.rotation.euler_angles();
assert_relative_eq!(
Vector3::new(roll, pitch, yaw),
Vector3::new(0.0, 0.0, 0.0),
epsilon = 1e-10
);
}
#[test]
fn test_lazy_rotation_frame() {
use nalgebra::UnitQuaternion;
let root = Frame::new_origin("root");
let child = root
.add_child(
"child",
Vector3::new(0.0, 0.0, 0.0),
UnitQuaternion::identity(),
)
.unwrap();
let result = &child * rz(std::f64::consts::FRAC_PI_4);
let (roll, pitch, yaw) = result.transformation().unwrap().rotation.euler_angles();
assert_relative_eq!(
Vector3::new(roll, pitch, yaw),
Vector3::new(0.0, 0.0, std::f64::consts::FRAC_PI_4),
epsilon = 1e-10
);
assert_relative_eq!(
result.transformation().unwrap().translation.vector,
Vector3::new(0.0, 0.0, 0.0),
epsilon = 1e-10
);
let (roll, pitch, yaw) = child.transformation().unwrap().rotation.euler_angles();
assert_relative_eq!(
Vector3::new(roll, pitch, yaw),
Vector3::new(0.0, 0.0, 0.0),
epsilon = 1e-10
);
}
#[test]
fn test_lazy_ops_on_non_identity_frame() {
use nalgebra::UnitQuaternion;
let root = Frame::new_origin("root");
let yaw_90 = UnitQuaternion::from_euler_angles(0.0, 0.0, std::f64::consts::FRAC_PI_2);
let child = root
.add_child("child", Vector3::new(1.0, 0.0, 0.0), yaw_90)
.unwrap();
let shifted = &child + y(3.0);
let shifted_in_root = shifted
.add_pose(Vector3::zeros(), UnitQuaternion::identity())
.in_frame(&root)
.unwrap()
.transformation();
assert_relative_eq!(
shifted_in_root.translation.vector,
Vector3::new(1.0, 3.0, 0.0),
epsilon = 1e-10
);
assert_relative_eq!(
shifted_in_root.rotation.angle_to(&yaw_90),
0.0,
epsilon = 1e-10
);
let rotated = &child * rz(std::f64::consts::FRAC_PI_2);
let rotated_in_root = rotated
.add_pose(Vector3::zeros(), UnitQuaternion::identity())
.in_frame(&root)
.unwrap()
.transformation();
assert_relative_eq!(
rotated_in_root.translation.vector,
Vector3::new(1.0, 0.0, 0.0),
epsilon = 1e-10
);
let yaw_180 = UnitQuaternion::from_euler_angles(0.0, 0.0, std::f64::consts::PI);
assert_relative_eq!(
rotated_in_root.rotation.angle_to(&yaw_180),
0.0,
epsilon = 1e-10
);
}
#[test]
fn test_lazy_ops_do_not_register_children() {
let root = Frame::new_origin("root");
let child = root
.add_child("child", Vector3::zeros(), UnitQuaternion::identity())
.unwrap();
let derived = &child + z(5.0);
let rotated = &child * rz(0.5);
assert!(child.children().is_empty());
assert_eq!(root.children().len(), 1);
let derived_in_root = derived
.add_pose(Vector3::zeros(), UnitQuaternion::identity())
.in_frame(&root)
.unwrap()
.transformation();
assert_relative_eq!(
derived_in_root.translation.vector,
Vector3::new(0.0, 0.0, 5.0),
epsilon = 1e-10
);
drop(rotated);
}
#[test]
fn test_lazy_translation_pose() {
use nalgebra::UnitQuaternion;
let root = Frame::new_origin("root");
let pose = root.add_pose(Vector3::new(0.0, 0.0, 0.0), UnitQuaternion::identity());
let result = &pose + z(5.0);
assert_relative_eq!(
result.transformation().translation.vector,
Vector3::new(0.0, 0.0, 5.0),
epsilon = 1e-10
);
assert_relative_eq!(
pose.transformation().translation.vector,
Vector3::new(0.0, 0.0, 0.0),
epsilon = 1e-10
);
let result = &result - y(3.0);
assert_relative_eq!(
result.transformation().translation.vector,
Vector3::new(0.0, -3.0, 5.0),
epsilon = 1e-10
);
let (roll, pitch, yaw) = result.transformation().rotation.euler_angles();
assert_relative_eq!(
Vector3::new(roll, pitch, yaw),
Vector3::new(0.0, 0.0, 0.0),
epsilon = 1e-10
);
}
#[test]
fn test_lazy_rotation_pose() {
use nalgebra::UnitQuaternion;
let root = Frame::new_origin("root");
let pose = root.add_pose(Vector3::new(0.0, 0.0, 0.0), UnitQuaternion::identity());
let result = &pose * rz(std::f64::consts::FRAC_PI_4);
let (roll, pitch, yaw) = result.transformation().rotation.euler_angles();
assert_relative_eq!(
Vector3::new(roll, pitch, yaw),
Vector3::new(0.0, 0.0, std::f64::consts::FRAC_PI_4),
epsilon = 1e-10
);
assert_relative_eq!(
result.transformation().translation.vector,
Vector3::new(0.0, 0.0, 0.0),
epsilon = 1e-10
);
let (roll, pitch, yaw) = pose.transformation().rotation.euler_angles();
assert_relative_eq!(
Vector3::new(roll, pitch, yaw),
Vector3::new(0.0, 0.0, 0.0),
epsilon = 1e-10
);
}
}