Skip to main content

cu_spatial_payloads/
lib.rs

1use bincode::{Decode, Encode};
2use core::fmt::Debug;
3use core::ops::Mul;
4use cu29::prelude::*;
5use cu29::units::si::angle::radian;
6use cu29::units::si::f32::Angle as Angle32;
7use cu29::units::si::f32::Length as Length32;
8use cu29::units::si::f64::Angle as Angle64;
9use cu29::units::si::f64::Length as Length64;
10use cu29::units::si::length::meter;
11use serde::{Deserialize, Serialize};
12
13#[cfg(feature = "glam")]
14use glam::{Affine3A, DAffine3, DMat4, Mat4};
15
16#[cfg(feature = "rerun")]
17mod rerun_components;
18
19/// Transform3D represents a 3D transformation (rotation + translation)
20/// When the glam feature is enabled, it uses glam's optimized types internally
21#[derive(Debug, Clone, Copy, Reflect)]
22#[reflect(opaque, from_reflect = false)]
23pub struct Transform3D<T: Copy + Debug + 'static> {
24    #[cfg(feature = "glam")]
25    inner: TransformInner<T>,
26    #[cfg(not(feature = "glam"))]
27    pub mat: [[T; 4]; 4],
28}
29
30#[cfg(feature = "glam")]
31#[derive(Debug, Clone, Copy)]
32enum TransformInner<T: Copy + Debug + 'static> {
33    F32(Affine3A),
34    F64(DAffine3),
35    _Phantom(std::marker::PhantomData<T>),
36}
37
38pub type Pose<T> = Transform3D<T>;
39
40macro_rules! impl_transform_accessors {
41    ($ty:ty, $len:ty, $ang:ty) => {
42        impl Transform3D<$ty> {
43            pub fn translation(&self) -> [$len; 3] {
44                let mat = self.to_matrix();
45                [
46                    <$len>::new::<meter>(mat[0][3]),
47                    <$len>::new::<meter>(mat[1][3]),
48                    <$len>::new::<meter>(mat[2][3]),
49                ]
50            }
51
52            pub fn rotation(&self) -> [[$ang; 3]; 3] {
53                let mat = self.to_matrix();
54                [
55                    [
56                        <$ang>::new::<radian>(mat[0][0]),
57                        <$ang>::new::<radian>(mat[0][1]),
58                        <$ang>::new::<radian>(mat[0][2]),
59                    ],
60                    [
61                        <$ang>::new::<radian>(mat[1][0]),
62                        <$ang>::new::<radian>(mat[1][1]),
63                        <$ang>::new::<radian>(mat[1][2]),
64                    ],
65                    [
66                        <$ang>::new::<radian>(mat[2][0]),
67                        <$ang>::new::<radian>(mat[2][1]),
68                        <$ang>::new::<radian>(mat[2][2]),
69                    ],
70                ]
71            }
72        }
73    };
74}
75
76// Manual implementations for serialization
77impl<T: Copy + Debug + Default + 'static> Serialize for Transform3D<T>
78where
79    T: Serialize,
80{
81    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
82    where
83        S: serde::Serializer,
84    {
85        #[cfg(feature = "glam")]
86        {
87            let mat = self.to_matrix();
88            mat.serialize(serializer)
89        }
90        #[cfg(not(feature = "glam"))]
91        {
92            self.mat.serialize(serializer)
93        }
94    }
95}
96
97impl<'de, T: Copy + Debug + 'static> Deserialize<'de> for Transform3D<T>
98where
99    T: Deserialize<'de> + Default,
100{
101    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
102    where
103        D: serde::Deserializer<'de>,
104    {
105        let mat: [[T; 4]; 4] = Deserialize::deserialize(deserializer)?;
106        Ok(Self::from_matrix(mat))
107    }
108}
109
110// Bincode implementations
111impl<T: Copy + Debug + Default + 'static> Encode for Transform3D<T>
112where
113    T: Encode,
114{
115    fn encode<E: bincode::enc::Encoder>(
116        &self,
117        encoder: &mut E,
118    ) -> Result<(), bincode::error::EncodeError> {
119        #[cfg(feature = "glam")]
120        {
121            let mat = self.to_matrix();
122            mat.encode(encoder)
123        }
124        #[cfg(not(feature = "glam"))]
125        {
126            self.mat.encode(encoder)
127        }
128    }
129}
130
131impl<T: Copy + Debug + 'static> Decode<()> for Transform3D<T>
132where
133    T: Decode<()> + Default,
134{
135    fn decode<D: bincode::de::Decoder<Context = ()>>(
136        decoder: &mut D,
137    ) -> Result<Self, bincode::error::DecodeError> {
138        let mat: [[T; 4]; 4] = Decode::decode(decoder)?;
139        Ok(Self::from_matrix(mat))
140    }
141}
142
143impl<T: Copy + Debug + Default + 'static> Transform3D<T> {
144    /// Create a transform from a 4x4 matrix
145    pub fn from_matrix(mat: [[T; 4]; 4]) -> Self {
146        #[cfg(feature = "glam")]
147        {
148            Self {
149                inner: TransformInner::from_matrix(mat),
150            }
151        }
152        #[cfg(not(feature = "glam"))]
153        {
154            Self { mat }
155        }
156    }
157
158    /// Get the transform as a 4x4 matrix
159    pub fn to_matrix(self) -> [[T; 4]; 4] {
160        #[cfg(feature = "glam")]
161        {
162            self.inner.to_matrix()
163        }
164        #[cfg(not(feature = "glam"))]
165        {
166            self.mat
167        }
168    }
169
170    /// Get a mutable reference to the matrix (for compatibility)
171    #[cfg(not(feature = "glam"))]
172    pub fn mat_mut(&mut self) -> &mut [[T; 4]; 4] {
173        &mut self.mat
174    }
175}
176
177#[cfg(feature = "glam")]
178impl<T: Copy + Debug + Default + 'static> TransformInner<T> {
179    fn from_matrix(mat: [[T; 4]; 4]) -> Self {
180        use std::any::TypeId;
181
182        // This is a bit hacky but necessary for type safety
183        // In practice, T will be f32 or f64
184        if TypeId::of::<T>() == TypeId::of::<f32>() {
185            // Convert to f32 matrix
186            // SAFETY: We just verified T == f32, so the layouts match.
187            let mat_f32: [[f32; 4]; 4] = unsafe { std::mem::transmute_copy(&mat) };
188            let glam_mat = Mat4::from_cols_array_2d(&mat_f32);
189            let affine = Affine3A::from_mat4(glam_mat);
190            // SAFETY: We just verified T == f32, so this is the correct enum variant.
191            unsafe { std::mem::transmute_copy(&TransformInner::<T>::F32(affine)) }
192        } else if TypeId::of::<T>() == TypeId::of::<f64>() {
193            // Convert to f64 matrix
194            // SAFETY: We just verified T == f64, so the layouts match.
195            let mat_f64: [[f64; 4]; 4] = unsafe { std::mem::transmute_copy(&mat) };
196            // let m = mat_f64;
197            let glam_mat = DMat4::from_cols_array_2d(&mat_f64);
198            let affine = DAffine3::from_mat4(glam_mat);
199            // SAFETY: We just verified T == f64, so this is the correct enum variant.
200            unsafe { std::mem::transmute_copy(&TransformInner::<T>::F64(affine)) }
201        } else {
202            panic!("Transform3D only supports f32 and f64 types when using glam feature");
203        }
204    }
205
206    fn to_matrix(self) -> [[T; 4]; 4] {
207        match self {
208            TransformInner::F32(affine) => {
209                let mat = Mat4::from(affine);
210                let mat_array = mat.to_cols_array_2d();
211                // SAFETY: We only reach this arm when T == f32.
212                unsafe { std::mem::transmute_copy(&mat_array) }
213            }
214            TransformInner::F64(affine) => {
215                let mat = DMat4::from(affine);
216                let mat_array = mat.to_cols_array_2d();
217                // SAFETY: We only reach this arm when T == f64.
218                unsafe { std::mem::transmute_copy(&mat_array) }
219            }
220            TransformInner::_Phantom(_) => unreachable!(),
221        }
222    }
223}
224
225impl_transform_accessors!(f32, Length32, Angle32);
226impl_transform_accessors!(f64, Length64, Angle64);
227
228impl<T: Copy + Debug + Default + 'static> Default for Transform3D<T> {
229    fn default() -> Self {
230        Self::from_matrix([[T::default(); 4]; 4])
231    }
232}
233
234macro_rules! impl_transform_mul {
235    ($ty:ty, $zero:expr, $variant:ident) => {
236        impl Mul for Transform3D<$ty> {
237            type Output = Self;
238
239            fn mul(self, rhs: Self) -> Self::Output {
240                #[cfg(feature = "glam")]
241                {
242                    match (&self.inner, &rhs.inner) {
243                        (TransformInner::$variant(a), TransformInner::$variant(b)) => Self {
244                            inner: TransformInner::$variant(*a * *b),
245                        },
246                        _ => unreachable!(),
247                    }
248                }
249                #[cfg(not(feature = "glam"))]
250                {
251                    let mut result = [[$zero; 4]; 4];
252                    for i in 0..4 {
253                        for j in 0..4 {
254                            let mut sum = $zero;
255                            for k in 0..4 {
256                                sum += self.mat[i][k] * rhs.mat[k][j];
257                            }
258                            result[i][j] = sum;
259                        }
260                    }
261                    Self { mat: result }
262                }
263            }
264        }
265    };
266}
267
268impl_transform_mul!(f32, 0.0f32, F32);
269impl_transform_mul!(f64, 0.0f64, F64);
270
271/// Reference implementations for f32
272impl Mul for &Transform3D<f32> {
273    type Output = Transform3D<f32>;
274
275    fn mul(self, rhs: Self) -> Self::Output {
276        *self * *rhs
277    }
278}
279
280impl Mul<Transform3D<f32>> for &Transform3D<f32> {
281    type Output = Transform3D<f32>;
282
283    fn mul(self, rhs: Transform3D<f32>) -> Self::Output {
284        *self * rhs
285    }
286}
287
288impl Mul<&Transform3D<f32>> for Transform3D<f32> {
289    type Output = Transform3D<f32>;
290
291    fn mul(self, rhs: &Transform3D<f32>) -> Self::Output {
292        self * *rhs
293    }
294}
295
296/// Reference implementations for f64
297impl Mul for &Transform3D<f64> {
298    type Output = Transform3D<f64>;
299
300    fn mul(self, rhs: Self) -> Self::Output {
301        *self * *rhs
302    }
303}
304
305impl Mul<Transform3D<f64>> for &Transform3D<f64> {
306    type Output = Transform3D<f64>;
307
308    fn mul(self, rhs: Transform3D<f64>) -> Self::Output {
309        *self * rhs
310    }
311}
312
313impl Mul<&Transform3D<f64>> for Transform3D<f64> {
314    type Output = Transform3D<f64>;
315
316    fn mul(self, rhs: &Transform3D<f64>) -> Self::Output {
317        self * *rhs
318    }
319}
320
321macro_rules! impl_transform_inverse {
322    ($ty:ty, $zero:expr, $one:expr, $variant:ident) => {
323        impl Transform3D<$ty> {
324            /// Computes the inverse of this transformation matrix.
325            pub fn inverse(&self) -> Self {
326                #[cfg(feature = "glam")]
327                {
328                    match &self.inner {
329                        TransformInner::$variant(affine) => Self {
330                            inner: TransformInner::$variant(affine.inverse()),
331                        },
332                        _ => unreachable!(),
333                    }
334                }
335                #[cfg(not(feature = "glam"))]
336                {
337                    let mat = self.mat;
338                    // Extract rotation matrix (top-left 3x3)
339                    let r = [
340                        [mat[0][0], mat[0][1], mat[0][2]],
341                        [mat[1][0], mat[1][1], mat[1][2]],
342                        [mat[2][0], mat[2][1], mat[2][2]],
343                    ];
344
345                    // Extract translation (top-right 3x1)
346                    let t = [mat[0][3], mat[1][3], mat[2][3]];
347
348                    // Compute transpose of rotation matrix (which is its inverse for orthogonal matrices)
349                    let r_inv = [
350                        [r[0][0], r[1][0], r[2][0]],
351                        [r[0][1], r[1][1], r[2][1]],
352                        [r[0][2], r[1][2], r[2][2]],
353                    ];
354
355                    // Compute -R^T * t
356                    let t_inv = [
357                        -(r_inv[0][0] * t[0] + r_inv[0][1] * t[1] + r_inv[0][2] * t[2]),
358                        -(r_inv[1][0] * t[0] + r_inv[1][1] * t[1] + r_inv[1][2] * t[2]),
359                        -(r_inv[2][0] * t[0] + r_inv[2][1] * t[1] + r_inv[2][2] * t[2]),
360                    ];
361
362                    // Construct the inverse transformation matrix
363                    let mut inv_mat = [[$zero; 4]; 4];
364
365                    // Copy rotation transpose
366                    for i in 0..3 {
367                        for j in 0..3 {
368                            inv_mat[i][j] = r_inv[i][j];
369                        }
370                    }
371
372                    // Copy translation part
373                    inv_mat[0][3] = t_inv[0];
374                    inv_mat[1][3] = t_inv[1];
375                    inv_mat[2][3] = t_inv[2];
376
377                    // Keep the homogeneous coordinate the same
378                    inv_mat[3][3] = $one;
379
380                    Self { mat: inv_mat }
381                }
382            }
383        }
384    };
385}
386
387impl_transform_inverse!(f32, 0.0f32, 1.0f32, F32);
388impl_transform_inverse!(f64, 0.0f64, 1.0f64, F64);
389
390#[cfg(feature = "faer")]
391mod faer_integration {
392    use super::Transform3D;
393    use faer::prelude::*;
394
395    impl From<&Transform3D<f64>> for Mat<f64> {
396        fn from(p: &Transform3D<f64>) -> Self {
397            let mat_array = p.to_matrix();
398            let mut mat: Mat<f64> = Mat::zeros(4, 4);
399            for (r, row) in mat_array.iter().enumerate() {
400                for (c, item) in row.iter().enumerate() {
401                    *mat.get_mut(r, c) = *item;
402                }
403            }
404            mat
405        }
406    }
407
408    impl From<Mat<f64>> for Transform3D<f64> {
409        fn from(mat: Mat<f64>) -> Self {
410            assert_eq!(mat.nrows(), 4);
411            assert_eq!(mat.ncols(), 4);
412            let mut transform = [[0.0; 4]; 4];
413            for (r, row) in transform.iter_mut().enumerate() {
414                for (c, val) in row.iter_mut().enumerate() {
415                    *val = *mat.get(r, c);
416                }
417            }
418            Self::from_matrix(transform)
419        }
420    }
421}
422
423// Optional Nalgebra integration
424#[cfg(feature = "nalgebra")]
425mod nalgebra_integration {
426    use super::Transform3D;
427    use nalgebra::{Isometry3, Matrix3, Matrix4, Rotation3, Translation3, Vector3};
428
429    impl From<&Transform3D<f64>> for Isometry3<f64> {
430        fn from(pose: &Transform3D<f64>) -> Self {
431            let mat_array = pose.to_matrix();
432            let flat_transform: [f64; 16] = std::array::from_fn(|i| mat_array[i / 4][i % 4]);
433            let matrix = Matrix4::from_row_slice(&flat_transform);
434
435            let rotation_matrix: Matrix3<f64> = matrix.fixed_view::<3, 3>(0, 0).into();
436            let rotation = Rotation3::from_matrix_unchecked(rotation_matrix);
437
438            let translation_vector: Vector3<f64> = matrix.fixed_view::<3, 1>(0, 3).into();
439            let translation = Translation3::from(translation_vector);
440
441            Isometry3::from_parts(translation, rotation.into())
442        }
443    }
444
445    impl From<Isometry3<f64>> for Transform3D<f64> {
446        fn from(iso: Isometry3<f64>) -> Self {
447            let matrix = iso.to_homogeneous();
448            let transform = std::array::from_fn(|r| std::array::from_fn(|c| matrix[(r, c)]));
449            Transform3D::from_matrix(transform)
450        }
451    }
452}
453
454// Keep existing glam integration but update it
455#[cfg(feature = "glam")]
456mod glam_integration {
457    use super::Transform3D;
458    use glam::{Affine3A, DAffine3};
459
460    impl From<Transform3D<f64>> for DAffine3 {
461        fn from(p: Transform3D<f64>) -> Self {
462            let mat = p.to_matrix();
463            let mut aff = DAffine3::IDENTITY;
464            aff.matrix3.x_axis.x = mat[0][0];
465            aff.matrix3.x_axis.y = mat[0][1];
466            aff.matrix3.x_axis.z = mat[0][2];
467
468            aff.matrix3.y_axis.x = mat[1][0];
469            aff.matrix3.y_axis.y = mat[1][1];
470            aff.matrix3.y_axis.z = mat[1][2];
471
472            aff.matrix3.z_axis.x = mat[2][0];
473            aff.matrix3.z_axis.y = mat[2][1];
474            aff.matrix3.z_axis.z = mat[2][2];
475
476            aff.translation.x = mat[3][0];
477            aff.translation.y = mat[3][1];
478            aff.translation.z = mat[3][2];
479
480            aff
481        }
482    }
483
484    impl From<DAffine3> for Transform3D<f64> {
485        fn from(aff: DAffine3) -> Self {
486            let mut transform = [[0.0f64; 4]; 4];
487
488            transform[0][0] = aff.matrix3.x_axis.x;
489            transform[0][1] = aff.matrix3.x_axis.y;
490            transform[0][2] = aff.matrix3.x_axis.z;
491
492            transform[1][0] = aff.matrix3.y_axis.x;
493            transform[1][1] = aff.matrix3.y_axis.y;
494            transform[1][2] = aff.matrix3.y_axis.z;
495
496            transform[2][0] = aff.matrix3.z_axis.x;
497            transform[2][1] = aff.matrix3.z_axis.y;
498            transform[2][2] = aff.matrix3.z_axis.z;
499
500            transform[3][0] = aff.translation.x;
501            transform[3][1] = aff.translation.y;
502            transform[3][2] = aff.translation.z;
503            transform[3][3] = 1.0;
504
505            Transform3D::from_matrix(transform)
506        }
507    }
508
509    impl From<Transform3D<f32>> for Affine3A {
510        fn from(p: Transform3D<f32>) -> Self {
511            let mat = p.to_matrix();
512            let mut aff = Affine3A::IDENTITY;
513            aff.matrix3.x_axis.x = mat[0][0];
514            aff.matrix3.x_axis.y = mat[0][1];
515            aff.matrix3.x_axis.z = mat[0][2];
516
517            aff.matrix3.y_axis.x = mat[1][0];
518            aff.matrix3.y_axis.y = mat[1][1];
519            aff.matrix3.y_axis.z = mat[1][2];
520
521            aff.matrix3.z_axis.x = mat[2][0];
522            aff.matrix3.z_axis.y = mat[2][1];
523            aff.matrix3.z_axis.z = mat[2][2];
524
525            aff.translation.x = mat[0][3];
526            aff.translation.y = mat[1][3];
527            aff.translation.z = mat[2][3];
528
529            aff
530        }
531    }
532
533    impl From<Affine3A> for Transform3D<f32> {
534        fn from(aff: Affine3A) -> Self {
535            let mut transform = [[0.0f32; 4]; 4];
536
537            transform[0][0] = aff.matrix3.x_axis.x;
538            transform[0][1] = aff.matrix3.x_axis.y;
539            transform[0][2] = aff.matrix3.x_axis.z;
540
541            transform[1][0] = aff.matrix3.y_axis.x;
542            transform[1][1] = aff.matrix3.y_axis.y;
543            transform[1][2] = aff.matrix3.y_axis.z;
544
545            transform[2][0] = aff.matrix3.z_axis.x;
546            transform[2][1] = aff.matrix3.z_axis.y;
547            transform[2][2] = aff.matrix3.z_axis.z;
548
549            transform[0][3] = aff.translation.x;
550            transform[1][3] = aff.translation.y;
551            transform[2][3] = aff.translation.z;
552            transform[3][3] = 1.0;
553
554            Transform3D::from_matrix(transform)
555        }
556    }
557}
558
559#[cfg(feature = "nalgebra")]
560#[allow(unused_imports)]
561pub use nalgebra_integration::*;
562
563#[cfg(feature = "faer")]
564#[allow(unused_imports)]
565pub use faer_integration::*;
566
567#[cfg(test)]
568mod tests {
569    use super::*;
570
571    fn assert_matrix_close<const N: usize, T: Copy + Into<f64>>(
572        lhs: [[T; N]; N],
573        rhs: [[T; N]; N],
574        eps: f64,
575    ) {
576        for i in 0..N {
577            for j in 0..N {
578                let lhs = lhs[i][j].into();
579                let rhs = rhs[i][j].into();
580                assert!(
581                    (lhs - rhs).abs() <= eps,
582                    "Element at [{},{}] differs: {} vs expected {}",
583                    i,
584                    j,
585                    lhs,
586                    rhs
587                );
588            }
589        }
590    }
591
592    #[test]
593    fn test_pose_default() {
594        let pose: Transform3D<f32> = Transform3D::default();
595        let mat = pose.to_matrix();
596
597        // With glam feature, the default is created from a zero matrix
598        // but internally glam may adjust it to ensure valid transforms
599        #[cfg(feature = "glam")]
600        {
601            // When we create from all zeros, glam will create a transform
602            // that has zeros except for the homogeneous coordinate
603            let expected = [
604                [0.0, 0.0, 0.0, 0.0],
605                [0.0, 0.0, 0.0, 0.0],
606                [0.0, 0.0, 0.0, 0.0],
607                [0.0, 0.0, 0.0, 1.0], // homogeneous coordinate
608            ];
609            assert_eq!(mat, expected, "Default pose with glam should have w=1");
610        }
611
612        #[cfg(not(feature = "glam"))]
613        {
614            assert_eq!(
615                mat, [[0.0; 4]; 4],
616                "Default pose without glam should be a zero matrix"
617            );
618        }
619    }
620
621    #[test]
622    fn test_transform_inverse_f32() {
623        // Create a test transform with rotation and translation
624        let transform = Transform3D::<f32>::from_matrix([
625            [1.0, 0.0, 0.0, 2.0], // x-axis with 2m translation
626            [0.0, 1.0, 0.0, 3.0], // y-axis with 3m translation
627            [0.0, 0.0, 1.0, 4.0], // z-axis with 4m translation
628            [0.0, 0.0, 0.0, 1.0], // homogeneous coordinate
629        ]);
630
631        // Compute inverse
632        let inverse = transform.inverse();
633
634        // Expected inverse for this transform
635        let expected_inverse = Transform3D::<f32>::from_matrix([
636            [1.0, 0.0, 0.0, -2.0], // Negated translation
637            [0.0, 1.0, 0.0, -3.0],
638            [0.0, 0.0, 1.0, -4.0],
639            [0.0, 0.0, 0.0, 1.0],
640        ]);
641
642        // Check each element with a small epsilon for floating-point comparison
643        let epsilon = 1e-5;
644        let inv_mat = inverse.to_matrix();
645        let exp_mat = expected_inverse.to_matrix();
646        assert_matrix_close(inv_mat, exp_mat, epsilon);
647    }
648
649    #[test]
650    fn test_transform_inverse_f64() {
651        // Create a test transform with rotation and translation
652        let transform = Transform3D::<f64>::from_matrix([
653            [0.0, -1.0, 0.0, 5.0], // 90-degree rotation around z with translation
654            [1.0, 0.0, 0.0, 6.0],
655            [0.0, 0.0, 1.0, 7.0],
656            [0.0, 0.0, 0.0, 1.0],
657        ]);
658
659        // Compute inverse
660        let inverse = transform.inverse();
661
662        // Expected inverse for this transform
663        let expected_inverse = Transform3D::<f64>::from_matrix([
664            [0.0, 1.0, 0.0, -6.0], // Transposed rotation and adjusted translation
665            [-1.0, 0.0, 0.0, 5.0],
666            [0.0, 0.0, 1.0, -7.0],
667            [0.0, 0.0, 0.0, 1.0],
668        ]);
669
670        // Check each element with a small epsilon for floating-point comparison
671        let epsilon = 1e-10;
672        let inv_mat = inverse.to_matrix();
673        let exp_mat = expected_inverse.to_matrix();
674        assert_matrix_close(inv_mat, exp_mat, epsilon);
675    }
676
677    #[test]
678    fn test_transform_inverse_identity() {
679        // Create identity transform
680        let identity = Transform3D::<f32>::from_matrix([
681            [1.0, 0.0, 0.0, 0.0],
682            [0.0, 1.0, 0.0, 0.0],
683            [0.0, 0.0, 1.0, 0.0],
684            [0.0, 0.0, 0.0, 1.0],
685        ]);
686
687        // Inverse of identity should be identity
688        let inverse = identity.inverse();
689
690        // Check if inverse is also identity
691        let epsilon = 1e-5;
692        let inv_mat = inverse.to_matrix();
693        let id_mat = identity.to_matrix();
694        assert_matrix_close(inv_mat, id_mat, epsilon);
695    }
696
697    #[test]
698    fn test_transform_multiplication_f32() {
699        // Create two transforms to multiply
700        let t1 = Transform3D::<f32>::from_matrix([
701            [1.0, 0.0, 0.0, 2.0], // Identity rotation + translation (2,3,4)
702            [0.0, 1.0, 0.0, 3.0],
703            [0.0, 0.0, 1.0, 4.0],
704            [0.0, 0.0, 0.0, 1.0],
705        ]);
706
707        let t2 = Transform3D::<f32>::from_matrix([
708            [0.0, -1.0, 0.0, 5.0], // 90-degree rotation around z + translation (5,6,7)
709            [1.0, 0.0, 0.0, 6.0],
710            [0.0, 0.0, 1.0, 7.0],
711            [0.0, 0.0, 0.0, 1.0],
712        ]);
713
714        // Compute t1 * t2
715        let result = t1 * t2;
716
717        // Expected result: t1 * t2 represents first rotating by t2, then translating by t1
718        let expected = Transform3D::<f32>::from_matrix([
719            [0.0, -1.0, 0.0, 7.0], // Rotation from t2 + combined translation
720            [1.0, 0.0, 0.0, 9.0],
721            [0.0, 0.0, 1.0, 11.0],
722            [0.0, 0.0, 0.0, 1.0],
723        ]);
724
725        // Check results
726        let epsilon = 1e-5;
727        let res_mat = result.to_matrix();
728        let exp_mat = expected.to_matrix();
729        assert_matrix_close(res_mat, exp_mat, epsilon);
730    }
731
732    #[test]
733    fn test_transform_multiplication_f64() {
734        // Create two transforms to multiply
735        let t1 = Transform3D::<f64>::from_matrix([
736            [1.0, 0.0, 0.0, 2.0], // Identity rotation + translation (2,3,4)
737            [0.0, 1.0, 0.0, 3.0],
738            [0.0, 0.0, 1.0, 4.0],
739            [0.0, 0.0, 0.0, 1.0],
740        ]);
741
742        let t2 = Transform3D::<f64>::from_matrix([
743            [0.0, -1.0, 0.0, 5.0], // 90-degree rotation around z + translation (5,6,7)
744            [1.0, 0.0, 0.0, 6.0],
745            [0.0, 0.0, 1.0, 7.0],
746            [0.0, 0.0, 0.0, 1.0],
747        ]);
748
749        // Compute t1 * t2
750        let result = t1 * t2;
751
752        // Expected result
753        let expected = Transform3D::<f64>::from_matrix([
754            [0.0, -1.0, 0.0, 7.0], // Rotation from t2 + combined translation
755            [1.0, 0.0, 0.0, 9.0],
756            [0.0, 0.0, 1.0, 11.0],
757            [0.0, 0.0, 0.0, 1.0],
758        ]);
759
760        // Check results
761        let epsilon = 1e-10;
762        let res_mat = result.to_matrix();
763        let exp_mat = expected.to_matrix();
764        assert_matrix_close(res_mat, exp_mat, epsilon);
765    }
766
767    #[test]
768    fn test_transform_reference_multiplication() {
769        // Test multiplication on references
770        let t1 = Transform3D::<f32>::from_matrix([
771            [1.0, 0.0, 0.0, 2.0],
772            [0.0, 1.0, 0.0, 3.0],
773            [0.0, 0.0, 1.0, 4.0],
774            [0.0, 0.0, 0.0, 1.0],
775        ]);
776
777        let t2 = Transform3D::<f32>::from_matrix([
778            [0.0, -1.0, 0.0, 5.0],
779            [1.0, 0.0, 0.0, 6.0],
780            [0.0, 0.0, 1.0, 7.0],
781            [0.0, 0.0, 0.0, 1.0],
782        ]);
783
784        // Compute &t1 * &t2
785        let result = t1 * t2;
786
787        // Expected result
788        let expected = Transform3D::<f32>::from_matrix([
789            [0.0, -1.0, 0.0, 7.0],
790            [1.0, 0.0, 0.0, 9.0],
791            [0.0, 0.0, 1.0, 11.0],
792            [0.0, 0.0, 0.0, 1.0],
793        ]);
794
795        // Check results
796        let epsilon = 1e-5;
797        let res_mat = result.to_matrix();
798        let exp_mat = expected.to_matrix();
799        assert_matrix_close(res_mat, exp_mat, epsilon);
800    }
801
802    #[cfg(feature = "faer")]
803    #[test]
804    fn test_pose_faer_conversion() {
805        use faer::prelude::*;
806
807        let pose = Transform3D::from_matrix([
808            [1.0, 2.0, 3.0, 4.0],
809            [5.0, 6.0, 7.0, 8.0],
810            [9.0, 10.0, 11.0, 12.0],
811            [13.0, 14.0, 15.0, 16.0],
812        ]);
813
814        let mat: Mat<f64> = (&pose).into();
815        let pose_from_mat = Transform3D::from(mat);
816
817        assert_eq!(
818            pose.to_matrix(),
819            pose_from_mat.to_matrix(),
820            "Faer conversion should be lossless"
821        );
822    }
823
824    #[cfg(feature = "nalgebra")]
825    #[test]
826    fn test_pose_nalgebra_conversion() {
827        use nalgebra::Isometry3;
828
829        let pose = Transform3D::from_matrix([
830            [1.0, 0.0, 0.0, 2.0],
831            [0.0, 1.0, 0.0, 3.0],
832            [0.0, 0.0, 1.0, 4.0],
833            [0.0, 0.0, 0.0, 1.0],
834        ]);
835
836        let iso: Isometry3<f64> = (&pose.clone()).into();
837        let pose_from_iso: Transform3D<f64> = iso.into();
838
839        assert_eq!(
840            pose.to_matrix(),
841            pose_from_iso.to_matrix(),
842            "Nalgebra conversion should be lossless"
843        );
844    }
845
846    #[cfg(feature = "glam")]
847    #[test]
848    fn test_pose_glam_conversion() {
849        use glam::DAffine3;
850
851        let pose = Transform3D::from_matrix([
852            [1.0, 0.0, 0.0, 0.0],
853            [0.0, 1.0, 0.0, 0.0],
854            [0.0, 0.0, 1.0, 0.0],
855            [5.0, 6.0, 7.0, 1.0],
856        ]);
857        let aff: DAffine3 = pose.into();
858        assert_eq!(aff.translation[0], 5.0);
859        let pose_from_aff: Transform3D<f64> = aff.into();
860
861        assert_eq!(
862            pose.to_matrix(),
863            pose_from_aff.to_matrix(),
864            "Glam conversion should be lossless"
865        );
866    }
867
868    #[cfg(feature = "glam")]
869    #[test]
870    fn test_matrix_format_issue() {
871        use glam::Mat4;
872
873        // Test case: row-major matrix with translation in last column
874        let row_major = [
875            [1.0, 0.0, 0.0, 5.0], // row 0: x-axis + x translation
876            [0.0, 1.0, 0.0, 6.0], // row 1: y-axis + y translation
877            [0.0, 0.0, 1.0, 7.0], // row 2: z-axis + z translation
878            [0.0, 0.0, 0.0, 1.0], // row 3: homogeneous
879        ];
880
881        // What glam expects: column-major format
882        // Each inner array is a COLUMN, not a row
883        let col_major = [
884            [1.0, 0.0, 0.0, 0.0], // column 0: x-axis
885            [0.0, 1.0, 0.0, 0.0], // column 1: y-axis
886            [0.0, 0.0, 1.0, 0.0], // column 2: z-axis
887            [5.0, 6.0, 7.0, 1.0], // column 3: translation + w
888        ];
889
890        // Create matrices
891        let mat_from_row = Mat4::from_cols_array_2d(&row_major);
892        let mat_from_col = Mat4::from_cols_array_2d(&col_major);
893
894        // When using row-major data directly, translation ends up in wrong place
895        assert_ne!(mat_from_row.w_axis.x, 5.0); // Translation is NOT where we expect
896
897        // When using column-major data, translation is correct
898        assert_eq!(mat_from_col.w_axis.x, 5.0);
899        assert_eq!(mat_from_col.w_axis.y, 6.0);
900        assert_eq!(mat_from_col.w_axis.z, 7.0);
901
902        // The fix: transpose the row-major matrix
903        let mat_transposed = Mat4::from_cols_array_2d(&row_major).transpose();
904        assert_eq!(mat_transposed.w_axis.x, 5.0);
905        assert_eq!(mat_transposed.w_axis.y, 6.0);
906        assert_eq!(mat_transposed.w_axis.z, 7.0);
907    }
908}