cu_transform/
interpolation.rs

1use crate::error::{TransformError, TransformResult};
2use crate::transform::StampedTransform;
3use cu29::clock::CuTime;
4use cu_spatial_payloads::Transform3D;
5use std::fmt::Debug;
6
7/// Trait for numeric types that can be interpolated
8pub trait Interpolate: Copy + Debug + Default + 'static {
9    /// Convert to f64 for intermediate calculations
10    fn to_f64(self) -> f64;
11    /// Create from f64 after interpolation calculations
12    fn from_f64(val: f64) -> Self;
13}
14
15// Implement for common numeric types
16impl Interpolate for f32 {
17    fn to_f64(self) -> f64 {
18        self as f64
19    }
20    fn from_f64(val: f64) -> Self {
21        val as f32
22    }
23}
24
25impl Interpolate for f64 {
26    fn to_f64(self) -> f64 {
27        self
28    }
29    fn from_f64(val: f64) -> Self {
30        val
31    }
32}
33
34impl Interpolate for i32 {
35    fn to_f64(self) -> f64 {
36        self as f64
37    }
38    fn from_f64(val: f64) -> Self {
39        val.round() as i32
40    }
41}
42
43impl Interpolate for i64 {
44    fn to_f64(self) -> f64 {
45        self as f64
46    }
47    fn from_f64(val: f64) -> Self {
48        val.round() as i64
49    }
50}
51
52impl Interpolate for u32 {
53    fn to_f64(self) -> f64 {
54        self as f64
55    }
56    fn from_f64(val: f64) -> Self {
57        val.round() as u32
58    }
59}
60
61impl Interpolate for u64 {
62    fn to_f64(self) -> f64 {
63        self as f64
64    }
65    fn from_f64(val: f64) -> Self {
66        val.round() as u64
67    }
68}
69
70/// Interpolate between two transforms at a specific time point
71///
72/// This function performs linear interpolation between two transforms for the translation
73/// components (last column of the transformation matrix) at the specified time point.
74/// The rotation components are not interpolated and are copied from the first transform.
75///
76/// Works with any numeric type implementing the `Interpolate` trait, including both
77/// floating-point (f32, f64) and integer types (i32, i64, u32, u64).
78///
79/// # Arguments
80/// * `before` - The transform at an earlier time
81/// * `after` - The transform at a later time
82/// * `time` - The time at which to interpolate (must be between before.stamp and after.stamp)
83///
84/// # Returns
85/// * A new interpolated transform at the specified time
86/// * Error if frames don't match or time is outside the valid range
87pub fn interpolate_transforms<T: Interpolate>(
88    before: &StampedTransform<T>,
89    after: &StampedTransform<T>,
90    time: CuTime,
91) -> TransformResult<Transform3D<T>> {
92    if before.parent_frame != after.parent_frame || before.child_frame != after.child_frame {
93        return Err(TransformError::InterpolationError(
94            "Cannot interpolate between different frame pairs".to_string(),
95        ));
96    }
97
98    if time < before.stamp || time > after.stamp {
99        return Err(TransformError::InterpolationError(
100            "Requested time is outside the range of the transforms".to_string(),
101        ));
102    }
103
104    let before_nanos = before.stamp.as_nanos() as f64;
105    let after_nanos = after.stamp.as_nanos() as f64;
106    let time_nanos = time.as_nanos() as f64;
107
108    let ratio = (time_nanos - before_nanos) / (after_nanos - before_nanos);
109
110    // Get matrices from transforms
111    let before_mat = before.transform.to_matrix();
112    let after_mat = after.transform.to_matrix();
113
114    // Initialize result matrix
115    let mut result_mat = [[T::default(); 4]; 4];
116
117    // Copy the entire matrix from before transform first
118    for i in 0..4 {
119        for j in 0..4 {
120            result_mat[i][j] = before_mat[i][j];
121        }
122    }
123
124    // Interpolate translation (in column-major format, translation is in the last row)
125    for i in 0..3 {
126        let before_val = before_mat[3][i].to_f64();
127        let after_val = after_mat[3][i].to_f64();
128        let interpolated = before_val + (after_val - before_val) * ratio;
129        result_mat[3][i] = T::from_f64(interpolated);
130    }
131
132    Ok(Transform3D::from_matrix(result_mat))
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    use crate::frame_id;
139    // Helper function to replace assert_relative_eq - made generic
140    fn assert_approx_eq<T>(actual: T, expected: T, epsilon: T)
141    where
142        T: Copy + std::fmt::Display + std::ops::Sub<Output = T> + PartialOrd,
143        T: num_traits::Signed,
144    {
145        let diff = (actual - expected).abs();
146        assert!(
147            diff <= epsilon,
148            "expected {expected}, got {actual}, difference {diff} exceeds epsilon {epsilon}",
149        );
150    }
151    use cu29::clock::CuDuration;
152
153    #[test]
154    fn test_interpolate_transforms_f32() {
155        let before: StampedTransform<f32> = StampedTransform {
156            transform: Transform3D::from_matrix([
157                [1.0, 0.0, 0.0, 0.0],
158                [0.0, 1.0, 0.0, 0.0],
159                [0.0, 0.0, 1.0, 0.0],
160                [0.0, 0.0, 0.0, 1.0], // Translation: x=0
161            ]),
162            stamp: CuDuration(1000),
163            parent_frame: frame_id!("world"),
164            child_frame: frame_id!("robot"),
165        };
166
167        let after: StampedTransform<f32> = StampedTransform {
168            transform: Transform3D::from_matrix([
169                [1.0, 0.0, 0.0, 0.0],
170                [0.0, 1.0, 0.0, 0.0],
171                [0.0, 0.0, 1.0, 0.0],
172                [10.0, 0.0, 0.0, 1.0], // Translation: x=10
173            ]),
174            stamp: CuDuration(3000),
175            parent_frame: frame_id!("world"),
176            child_frame: frame_id!("robot"),
177        };
178
179        let result = interpolate_transforms(&before, &after, CuDuration(2000));
180        assert!(result.is_ok());
181
182        let transform = result.unwrap();
183        assert_approx_eq(transform.to_matrix()[3][0], 5.0, 1e-5);
184
185        let result = interpolate_transforms(&before, &after, CuDuration(1500));
186        assert!(result.is_ok());
187
188        let transform = result.unwrap();
189        assert_approx_eq(transform.to_matrix()[3][0], 2.5, 1e-5);
190
191        let result = interpolate_transforms(&before, &after, CuDuration(2500));
192        assert!(result.is_ok());
193
194        let transform = result.unwrap();
195        assert_approx_eq(transform.to_matrix()[3][0], 7.5, 1e-5);
196    }
197
198    #[test]
199    fn test_interpolate_transforms_f64() {
200        let before: StampedTransform<f64> = StampedTransform {
201            transform: Transform3D::from_matrix([
202                [1.0, 0.0, 0.0, 0.0],
203                [0.0, 1.0, 0.0, 0.0],
204                [0.0, 0.0, 1.0, 0.0],
205                [0.0, 0.0, 0.0, 1.0], // Translation: x=0
206            ]),
207            stamp: CuDuration(1000),
208            parent_frame: frame_id!("world"),
209            child_frame: frame_id!("robot"),
210        };
211
212        let after: StampedTransform<f64> = StampedTransform {
213            transform: Transform3D::from_matrix([
214                [1.0, 0.0, 0.0, 0.0], // Translation: x=10
215                [0.0, 1.0, 0.0, 0.0],
216                [0.0, 0.0, 1.0, 0.0],
217                [10.0, 0.0, 0.0, 1.0],
218            ]),
219            stamp: CuDuration(3000),
220            parent_frame: frame_id!("world"),
221            child_frame: frame_id!("robot"),
222        };
223
224        // Test at midpoint
225        let result = interpolate_transforms(&before, &after, CuDuration(2000));
226        assert!(result.is_ok());
227        let transform = result.unwrap();
228        assert_approx_eq(transform.to_matrix()[3][0], 5.0, 1e-5);
229    }
230
231    // Disabled: Transform3D only supports f32 and f64 when using glam (default)
232    #[test]
233    #[ignore]
234    fn test_interpolate_transforms_integer() {
235        // Test with i32
236        let mut before: StampedTransform<i32> = StampedTransform {
237            transform: Transform3D::default(),
238            stamp: CuDuration(1000),
239            parent_frame: frame_id!("world"),
240            child_frame: frame_id!("robot"),
241        };
242
243        let mut after: StampedTransform<i32> = StampedTransform {
244            transform: Transform3D::default(),
245            stamp: CuDuration(3000),
246            parent_frame: frame_id!("world"),
247            child_frame: frame_id!("robot"),
248        };
249
250        let mut before_mat = before.transform.to_matrix();
251        before_mat[0][3] = 0;
252        before.transform = Transform3D::from_matrix(before_mat);
253
254        let mut after_mat = after.transform.to_matrix();
255        after_mat[0][3] = 10;
256        after.transform = Transform3D::from_matrix(after_mat);
257
258        // Test at midpoint
259        let result = interpolate_transforms(&before, &after, CuDuration(2000));
260        assert!(result.is_ok());
261        let transform = result.unwrap();
262        assert_eq!(transform.to_matrix()[0][3], 5);
263
264        // Test with non-integer result (should round)
265        let result = interpolate_transforms(&before, &after, CuDuration(1500));
266        assert!(result.is_ok());
267        let transform = result.unwrap();
268        assert_eq!(transform.to_matrix()[0][3], 3); // 2.5 rounds to 3
269    }
270
271    // Disabled: Transform3D only supports f32 and f64 when using glam (default)
272    #[test]
273    #[ignore]
274    fn test_interpolate_transforms_u64() {
275        // Test with u64
276        let mut before: StampedTransform<u64> = StampedTransform {
277            transform: Transform3D::default(),
278            stamp: CuDuration(1000),
279            parent_frame: frame_id!("world"),
280            child_frame: frame_id!("robot"),
281        };
282
283        let mut after: StampedTransform<u64> = StampedTransform {
284            transform: Transform3D::default(),
285            stamp: CuDuration(3000),
286            parent_frame: frame_id!("world"),
287            child_frame: frame_id!("robot"),
288        };
289
290        // Set large values to test u64 range
291        let mut before_mat = before.transform.to_matrix();
292        before_mat[0][3] = 1_000_000_000;
293        before.transform = Transform3D::from_matrix(before_mat);
294
295        let mut after_mat = after.transform.to_matrix();
296        after_mat[0][3] = 2_000_000_000;
297        after.transform = Transform3D::from_matrix(after_mat);
298
299        // Test at 75% point
300        let result = interpolate_transforms(&before, &after, CuDuration(2500));
301        assert!(result.is_ok());
302        let transform = result.unwrap();
303        assert_eq!(transform.to_matrix()[0][3], 1_750_000_000);
304    }
305
306    #[test]
307    fn test_interpolate_transforms_errors() {
308        let before: StampedTransform<f32> = StampedTransform {
309            transform: Transform3D::default(),
310            stamp: CuDuration(1000),
311            parent_frame: frame_id!("world"),
312            child_frame: frame_id!("robot"),
313        };
314
315        let after: StampedTransform<f32> = StampedTransform {
316            transform: Transform3D::default(),
317            stamp: CuDuration(3000),
318            parent_frame: frame_id!("different"),
319            child_frame: frame_id!("robot"),
320        };
321
322        let result = interpolate_transforms(&before, &after, CuDuration(2000));
323        assert!(result.is_err());
324
325        let after: StampedTransform<f32> = StampedTransform {
326            transform: Transform3D::default(),
327            stamp: CuDuration(3000),
328            parent_frame: frame_id!("world"),
329            child_frame: frame_id!("different"),
330        };
331
332        let result = interpolate_transforms(&before, &after, CuDuration(2000));
333        assert!(result.is_err());
334
335        let after: StampedTransform<f32> = StampedTransform {
336            transform: Transform3D::default(),
337            stamp: CuDuration(3000),
338            parent_frame: frame_id!("world"),
339            child_frame: frame_id!("robot"),
340        };
341
342        let result = interpolate_transforms(&before, &after, CuDuration(500));
343        assert!(result.is_err());
344
345        let result = interpolate_transforms(&before, &after, CuDuration(3500));
346        assert!(result.is_err());
347    }
348}