cu_spatial_payloads/
lib.rs

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