From 76cf9022173d5764814544082f2b518a9df1934b Mon Sep 17 00:00:00 2001 From: cc Date: Thu, 22 Oct 2026 15:09:27 -0700 Subject: [PATCH] Support atomic insertion of transform batches --- src/core/buffer/mod.rs | 2 ++ src/core/registry/mod.rs | 18 +++++++++++++- src/core/registry/tests.rs | 50 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) diff --git a/src/core/buffer/mod.rs b/src/core/buffer/mod.rs index fbfb99c..8540ee8 100644 --- a/src/core/buffer/mod.rs +++ b/src/core/buffer/mod.rs @@ -44,6 +44,7 @@ type NearestTransforms<'a, T> = (Option<(&'a T, &'a Isometry)>, Option<(&'a T, & /// than `max_age` relative to the latest inserted timestamp are removed /// automatically on insert. A buffer created with [`Buffer::dynamic`] never /// expires entries; use [`Buffer::remove_before`] for manual cleanup. +#[derive(Clone)] pub(crate) struct Buffer where T: TimePoint, @@ -118,6 +119,7 @@ where /// a time series of dynamic samples. Keeping the kind structural — instead /// of a flag re-derived from the stored data — makes it impossible for a /// buffer to change kind when it is emptied and refilled. +#[derive(Clone)] enum Kind where T: TimePoint, diff --git a/src/core/registry/mod.rs b/src/core/registry/mod.rs index 65d6f5c..9802098 100644 --- a/src/core/registry/mod.rs +++ b/src/core/registry/mod.rs @@ -91,7 +91,7 @@ impl<'a> Mul for Hop<'a> { /// `Registry` is `Send` and `Sync` when `T` is. An external `RwLock` permits /// concurrent readers; writers need exclusive access, and readers may wait for /// writers. Keep dependent queries under one guard. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Registry where T: TimePoint, @@ -159,6 +159,22 @@ where Self::process_add_transform(t, &mut self.data, self.max_age) } + /// Inserts a batch atomically. An invalid transform leaves the registry unchanged. + /// + /// # Errors + /// Returns the first error from [`Registry::add_transform`]. + pub fn add_transforms( + &mut self, + transforms: impl IntoIterator>, + ) -> Result<(), RegistryError> { + let mut next = self.clone(); + for transform in transforms { + next.add_transform(transform)?; + } + *self = next; + Ok(()) + } + /// Maps `source`-frame coordinates into `target` at `timestamp`. /// /// The argument order matches tf2's `lookupTransform(target, source, time)`. diff --git a/src/core/registry/tests.rs b/src/core/registry/tests.rs index 5a0f14a..a73a59b 100644 --- a/src/core/registry/tests.rs +++ b/src/core/registry/tests.rs @@ -3732,4 +3732,54 @@ mod registry_tests { "expected the moved frame to still be static, got {result:?}" ); } + #[test] + fn a_failed_batch_does_not_leave_edges_or_evictions_behind() { + let mut registry = Registry::with_max_age(Duration::from_nanos(10)); + let sample = |parent, child, ns, x| { + Transform::new( + parent, + child, + Vector3::new(x, 0.0, 0.0), + Quaternion::identity(), + Stamp::At(Timestamp::from_nanos(ns)), + ) + .unwrap() + }; + registry + .add_transform(sample("map", "base", 1, 1.0)) + .unwrap(); + let result = registry.add_transforms([ + sample("map", "base", 100, 2.0), + sample("base", "camera", 100, 3.0), + sample("camera", "map", 100, 4.0), + ]); + assert!(matches!(result, Err(RegistryError::CycleDetected))); + assert_eq!( + registry + .get_transform("map", "base", Timestamp::from_nanos(1)) + .unwrap() + .translation() + .x, + 1.0 + ); + assert!( + registry + .get_transform("base", "camera", Timestamp::from_nanos(100)) + .is_err() + ); + registry + .add_transforms([ + sample("map", "base", 2, 2.0), + sample("base", "camera", 2, 3.0), + ]) + .unwrap(); + assert_eq!( + registry + .get_transform("map", "camera", Timestamp::from_nanos(2)) + .unwrap() + .translation() + .x, + 5.0 + ); + } }