Skip to main content

cu_spatial_payloads/
lib.rs

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