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::f32::Angle as Angle32;
12use cu29::units::si::f32::Length as Length32;
13use cu29::units::si::f64::Angle as Angle64;
14use cu29::units::si::f64::Length as Length64;
15use cu29::units::si::length::meter;
16use serde::{Deserialize, Serialize};
17
18#[cfg(feature = "glam")]
19use glam::{Affine3A, DAffine3, DMat4, DVec3, Mat4, Vec3, Vec3A};
20
21mod geometry;
22pub use geometry::{
23    BBox, BBox2d, BBox2f, BBox2i, BBox2u, BBox3d, BBox3f, Point2, Point2Iterator, Point2Soa,
24    Point2d, Point2dSoa, Point2f, Point2fSoa, Point2i, Point2iSoa, Point2u, Point2uSoa, Point3,
25    Point3Iterator, Point3Soa, Point3d, Point3dSoa, Point3f, Point3fSoa,
26};
27
28#[cfg(feature = "rerun")]
29mod rerun_components;
30
31/// Geodetic position expressed in [EPSG:4326](https://epsg.io/4326) latitude and longitude.
32///
33/// Use this for absolute Earth-referenced positions such as GNSS fixes. Keep it distinct from
34/// local engineering coordinates like ENU/NED/cartesian meters.
35#[derive(
36    Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize, Encode, Decode, Reflect,
37)]
38pub struct GeodeticPosition {
39    pub latitude: Angle64,
40    pub longitude: Angle64,
41}
42
43impl GeodeticPosition {
44    pub fn new(latitude: Angle64, longitude: Angle64) -> Self {
45        Self {
46            latitude,
47            longitude,
48        }
49    }
50
51    pub fn from_degrees(latitude_deg: f64, longitude_deg: f64) -> Self {
52        Self {
53            latitude: Angle64::new::<degree>(latitude_deg),
54            longitude: Angle64::new::<degree>(longitude_deg),
55        }
56    }
57
58    pub fn latitude_degrees(&self) -> f64 {
59        self.latitude.get::<degree>()
60    }
61
62    pub fn longitude_degrees(&self) -> f64 {
63        self.longitude.get::<degree>()
64    }
65}
66
67/// Transform3D represents a 3D transformation (rotation + translation)
68/// When the glam feature is enabled, it uses glam's optimized types internally
69#[derive(Debug, Clone, Copy, Reflect)]
70#[reflect(opaque, from_reflect = false)]
71pub struct Transform3D<T: Copy + Debug + 'static> {
72    #[cfg(feature = "glam")]
73    inner: TransformInner<T>,
74    #[cfg(not(feature = "glam"))]
75    pub mat: [[T; 4]; 4],
76}
77
78#[cfg(feature = "glam")]
79#[derive(Debug, Clone, Copy)]
80enum TransformInner<T: Copy + Debug + 'static> {
81    F32(Affine3A),
82    F64(DAffine3),
83    _Phantom(core::marker::PhantomData<T>),
84}
85
86const fn const_sin_cos(mut angle: f64) -> (f64, f64) {
87    const PI: f64 = core::f64::consts::PI;
88    const FRAC_PI_2: f64 = core::f64::consts::FRAC_PI_2;
89    const TAU: f64 = core::f64::consts::TAU;
90
91    angle %= TAU;
92    if angle > PI {
93        angle -= TAU;
94    } else if angle < -PI {
95        angle += TAU;
96    }
97
98    let mut cos_sign = 1.0;
99    if angle > FRAC_PI_2 {
100        angle = PI - angle;
101        cos_sign = -1.0;
102    } else if angle < -FRAC_PI_2 {
103        angle = -PI - angle;
104        cos_sign = -1.0;
105    }
106
107    // After range reduction to [-pi/2, pi/2], Taylor terms through x^21 for sine and x^20 for
108    // cosine keep the approximation below the f64 regression tolerance used by this crate.
109    let x2 = angle * angle;
110    let mut sin_poly = 1.0 / 51_090_942_171_709_440_000.0;
111    sin_poly = -1.0 / 121_645_100_408_832_000.0 + x2 * sin_poly;
112    sin_poly = 1.0 / 355_687_428_096_000.0 + x2 * sin_poly;
113    sin_poly = -1.0 / 1_307_674_368_000.0 + x2 * sin_poly;
114    sin_poly = 1.0 / 6_227_020_800.0 + x2 * sin_poly;
115    sin_poly = -1.0 / 39_916_800.0 + x2 * sin_poly;
116    sin_poly = 1.0 / 362_880.0 + x2 * sin_poly;
117    sin_poly = -1.0 / 5_040.0 + x2 * sin_poly;
118    sin_poly = 1.0 / 120.0 + x2 * sin_poly;
119    sin_poly = -1.0 / 6.0 + x2 * sin_poly;
120    let sin = angle * (1.0 + x2 * sin_poly);
121
122    let mut cos_poly = 1.0 / 2_432_902_008_176_640_000.0;
123    cos_poly = -1.0 / 6_402_373_705_728_000.0 + x2 * cos_poly;
124    cos_poly = 1.0 / 20_922_789_888_000.0 + x2 * cos_poly;
125    cos_poly = -1.0 / 87_178_291_200.0 + x2 * cos_poly;
126    cos_poly = 1.0 / 479_001_600.0 + x2 * cos_poly;
127    cos_poly = -1.0 / 3_628_800.0 + x2 * cos_poly;
128    cos_poly = 1.0 / 40_320.0 + x2 * cos_poly;
129    cos_poly = -1.0 / 720.0 + x2 * cos_poly;
130    cos_poly = 1.0 / 24.0 + x2 * cos_poly;
131    cos_poly = -1.0 / 2.0 + x2 * cos_poly;
132    let cos = 1.0 + x2 * cos_poly;
133
134    (sin, cos_sign * cos)
135}
136
137macro_rules! impl_const_transform {
138    ($ty:ty, $len:ty, $ang:ty, $variant:ident, $affine:ty, $vec:ty) => {
139        impl Transform3D<$ty> {
140            const fn from_rows(rows: [[$ty; 4]; 3]) -> Self {
141                #[cfg(feature = "glam")]
142                {
143                    Self {
144                        inner: TransformInner::$variant(<$affine>::from_cols(
145                            <$vec>::new(rows[0][0], rows[1][0], rows[2][0]),
146                            <$vec>::new(rows[0][1], rows[1][1], rows[2][1]),
147                            <$vec>::new(rows[0][2], rows[1][2], rows[2][2]),
148                            <$vec>::new(rows[0][3], rows[1][3], rows[2][3]),
149                        )),
150                    }
151                }
152                #[cfg(not(feature = "glam"))]
153                {
154                    Self {
155                        mat: [
156                            rows[0],
157                            rows[1],
158                            rows[2],
159                            [0.0 as $ty, 0.0 as $ty, 0.0 as $ty, 1.0 as $ty],
160                        ],
161                    }
162                }
163            }
164
165            const fn rows(self) -> [[$ty; 4]; 3] {
166                #[cfg(feature = "glam")]
167                {
168                    match self.inner {
169                        TransformInner::$variant(affine) => {
170                            let r = affine.matrix3;
171                            let x = r.x_axis.to_array();
172                            let y = r.y_axis.to_array();
173                            let z = r.z_axis.to_array();
174                            let t = affine.translation.to_array();
175                            [
176                                [x[0], y[0], z[0], t[0]],
177                                [x[1], y[1], z[1], t[1]],
178                                [x[2], y[2], z[2], t[2]],
179                            ]
180                        }
181                        _ => panic!("invalid Transform3D storage variant"),
182                    }
183                }
184                #[cfg(not(feature = "glam"))]
185                {
186                    [self.mat[0], self.mat[1], self.mat[2]]
187                }
188            }
189
190            /// Creates the identity transform during const evaluation.
191            pub const fn identity() -> Self {
192                Self::from_rows([
193                    [1.0 as $ty, 0.0 as $ty, 0.0 as $ty, 0.0 as $ty],
194                    [0.0 as $ty, 1.0 as $ty, 0.0 as $ty, 0.0 as $ty],
195                    [0.0 as $ty, 0.0 as $ty, 1.0 as $ty, 0.0 as $ty],
196                ])
197            }
198
199            /// Creates a transform from unit-typed translation and XYZ Euler angles.
200            ///
201            /// `rotation` is `[roll_x, pitch_y, yaw_z]`. Rotations are applied X, then Y, then Z,
202            /// producing `Rz * Ry * Rx` for column vectors. Translation is applied last.
203            /// Const unit values use their SI base representation: meters and radians.
204            pub const fn from_translation_euler_xyz(
205                translation: [$len; 3],
206                rotation: [$ang; 3],
207            ) -> Self {
208                let (sx, cx) = const_sin_cos(rotation[0].value as f64);
209                let (sy, cy) = const_sin_cos(rotation[1].value as f64);
210                let (sz, cz) = const_sin_cos(rotation[2].value as f64);
211                let sx = sx as $ty;
212                let cx = cx as $ty;
213                let sy = sy as $ty;
214                let cy = cy as $ty;
215                let sz = sz as $ty;
216                let cz = cz as $ty;
217
218                Self::from_rows([
219                    [
220                        cy * cz,
221                        cz * sx * sy - cx * sz,
222                        sx * sz + cx * cz * sy,
223                        translation[0].value,
224                    ],
225                    [
226                        cy * sz,
227                        cx * cz + sx * sy * sz,
228                        cx * sy * sz - cz * sx,
229                        translation[1].value,
230                    ],
231                    [-sy, cy * sx, cx * cy, translation[2].value],
232                ])
233            }
234
235            /// Composes `self` with `rhs` during const evaluation.
236            ///
237            /// The returned transform applies `rhs` first and then `self`, matching `self * rhs`.
238            pub const fn compose(self, rhs: Self) -> Self {
239                let lhs = self.rows();
240                let rhs = rhs.rows();
241                let mut result = [[0.0 as $ty; 4]; 3];
242                let mut row = 0;
243                while row < 3 {
244                    let mut column = 0;
245                    while column < 3 {
246                        result[row][column] = lhs[row][0] * rhs[0][column]
247                            + lhs[row][1] * rhs[1][column]
248                            + lhs[row][2] * rhs[2][column];
249                        column += 1;
250                    }
251                    result[row][3] = lhs[row][0] * rhs[0][3]
252                        + lhs[row][1] * rhs[1][3]
253                        + lhs[row][2] * rhs[2][3]
254                        + lhs[row][3];
255                    row += 1;
256                }
257                Self::from_rows(result)
258            }
259        }
260    };
261}
262
263impl_const_transform!(f32, Length32, Angle32, F32, Affine3A, Vec3A);
264impl_const_transform!(f64, Length64, Angle64, F64, DAffine3, DVec3);
265
266pub type Pose<T> = Transform3D<T>;
267
268macro_rules! impl_transform_accessors {
269    ($ty:ty, $len:ty, $variant:ident) => {
270        impl Transform3D<$ty> {
271            /// The translation component, in meters.
272            pub fn translation(&self) -> [$len; 3] {
273                let position = self.position();
274                [position.x, position.y, position.z]
275            }
276
277            /// The dimensionless rotation matrix, represented as rows.
278            pub fn rotation(&self) -> [[$ty; 3]; 3] {
279                #[cfg(feature = "glam")]
280                {
281                    match &self.inner {
282                        TransformInner::$variant(affine) => {
283                            let r = &affine.matrix3;
284                            [
285                                [r.x_axis.x, r.y_axis.x, r.z_axis.x],
286                                [r.x_axis.y, r.y_axis.y, r.z_axis.y],
287                                [r.x_axis.z, r.y_axis.z, r.z_axis.z],
288                            ]
289                        }
290                        _ => unreachable!(),
291                    }
292                }
293                #[cfg(not(feature = "glam"))]
294                {
295                    [
296                        [self.mat[0][0], self.mat[0][1], self.mat[0][2]],
297                        [self.mat[1][0], self.mat[1][1], self.mat[1][2]],
298                        [self.mat[2][0], self.mat[2][1], self.mat[2][2]],
299                    ]
300                }
301            }
302        }
303    };
304}
305
306// Manual implementations for serialization
307impl<T: Copy + Debug + Default + 'static> Serialize for Transform3D<T>
308where
309    T: Serialize,
310{
311    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
312    where
313        S: serde::Serializer,
314    {
315        #[cfg(feature = "glam")]
316        {
317            let mat = self.to_matrix();
318            mat.serialize(serializer)
319        }
320        #[cfg(not(feature = "glam"))]
321        {
322            self.mat.serialize(serializer)
323        }
324    }
325}
326
327impl<'de, T: Copy + Debug + 'static> Deserialize<'de> for Transform3D<T>
328where
329    T: Deserialize<'de> + Default,
330{
331    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
332    where
333        D: serde::Deserializer<'de>,
334    {
335        let mat: [[T; 4]; 4] = Deserialize::deserialize(deserializer)?;
336        Ok(Self::from_matrix(mat))
337    }
338}
339
340// Bincode implementations
341impl<T: Copy + Debug + Default + 'static> Encode for Transform3D<T>
342where
343    T: Encode,
344{
345    fn encode<E: bincode::enc::Encoder>(
346        &self,
347        encoder: &mut E,
348    ) -> Result<(), bincode::error::EncodeError> {
349        #[cfg(feature = "glam")]
350        {
351            let mat = self.to_matrix();
352            mat.encode(encoder)
353        }
354        #[cfg(not(feature = "glam"))]
355        {
356            self.mat.encode(encoder)
357        }
358    }
359}
360
361impl<T: Copy + Debug + 'static> Decode<()> for Transform3D<T>
362where
363    T: Decode<()> + Default,
364{
365    fn decode<D: bincode::de::Decoder<Context = ()>>(
366        decoder: &mut D,
367    ) -> Result<Self, bincode::error::DecodeError> {
368        let mat: [[T; 4]; 4] = Decode::decode(decoder)?;
369        Ok(Self::from_matrix(mat))
370    }
371}
372
373impl<T: Copy + Debug + Default + 'static> Transform3D<T> {
374    /// Create a transform from a 4x4 matrix
375    pub fn from_matrix(mat: [[T; 4]; 4]) -> Self {
376        #[cfg(feature = "glam")]
377        {
378            Self {
379                inner: TransformInner::from_matrix(mat),
380            }
381        }
382        #[cfg(not(feature = "glam"))]
383        {
384            Self { mat }
385        }
386    }
387
388    /// Get the transform as a 4x4 matrix
389    pub fn to_matrix(self) -> [[T; 4]; 4] {
390        #[cfg(feature = "glam")]
391        {
392            self.inner.to_matrix()
393        }
394        #[cfg(not(feature = "glam"))]
395        {
396            self.mat
397        }
398    }
399
400    /// Get a mutable reference to the matrix (for compatibility)
401    #[cfg(not(feature = "glam"))]
402    pub fn mat_mut(&mut self) -> &mut [[T; 4]; 4] {
403        &mut self.mat
404    }
405}
406
407#[cfg(feature = "glam")]
408impl<T: Copy + Debug + Default + 'static> TransformInner<T> {
409    fn from_matrix(mat: [[T; 4]; 4]) -> Self {
410        use core::any::TypeId;
411
412        // This is a bit hacky but necessary for type safety
413        // In practice, T will be f32 or f64
414        if TypeId::of::<T>() == TypeId::of::<f32>() {
415            // Convert to f32 matrix
416            // SAFETY: We just verified T == f32, so the layouts match.
417            let mat_f32: [[f32; 4]; 4] = unsafe { core::mem::transmute_copy(&mat) };
418            let glam_mat = Mat4::from_cols_array_2d(&mat_f32);
419            let affine = Affine3A::from_mat4(glam_mat);
420            // SAFETY: We just verified T == f32, so this is the correct enum variant.
421            unsafe { core::mem::transmute_copy(&TransformInner::<T>::F32(affine)) }
422        } else if TypeId::of::<T>() == TypeId::of::<f64>() {
423            // Convert to f64 matrix
424            // SAFETY: We just verified T == f64, so the layouts match.
425            let mat_f64: [[f64; 4]; 4] = unsafe { core::mem::transmute_copy(&mat) };
426            // let m = mat_f64;
427            let glam_mat = DMat4::from_cols_array_2d(&mat_f64);
428            let affine = DAffine3::from_mat4(glam_mat);
429            // SAFETY: We just verified T == f64, so this is the correct enum variant.
430            unsafe { core::mem::transmute_copy(&TransformInner::<T>::F64(affine)) }
431        } else {
432            panic!("Transform3D only supports f32 and f64 types when using glam feature");
433        }
434    }
435
436    fn to_matrix(self) -> [[T; 4]; 4] {
437        match self {
438            TransformInner::F32(affine) => {
439                let mat = Mat4::from(affine);
440                let mat_array = mat.to_cols_array_2d();
441                // SAFETY: We only reach this arm when T == f32.
442                unsafe { core::mem::transmute_copy(&mat_array) }
443            }
444            TransformInner::F64(affine) => {
445                let mat = DMat4::from(affine);
446                let mat_array = mat.to_cols_array_2d();
447                // SAFETY: We only reach this arm when T == f64.
448                unsafe { core::mem::transmute_copy(&mat_array) }
449            }
450            TransformInner::_Phantom(_) => unreachable!(),
451        }
452    }
453}
454
455impl_transform_accessors!(f32, Length32, F32);
456impl_transform_accessors!(f64, Length64, F64);
457
458macro_rules! impl_transform_points {
459    ($ty:ty, $len:ty, $variant:ident, $vec:ty) => {
460        impl Transform3D<$ty> {
461            /// The translation as a point: where the pose sits in its parent
462            /// frame.
463            pub fn position(&self) -> Point3<$len> {
464                #[cfg(feature = "glam")]
465                {
466                    match &self.inner {
467                        TransformInner::$variant(affine) => {
468                            let t = affine.translation;
469                            Point3::new(
470                                <$len>::new::<meter>(t.x as $ty),
471                                <$len>::new::<meter>(t.y as $ty),
472                                <$len>::new::<meter>(t.z as $ty),
473                            )
474                        }
475                        _ => unreachable!(),
476                    }
477                }
478                #[cfg(not(feature = "glam"))]
479                {
480                    Point3::new(
481                        <$len>::new::<meter>(self.mat[0][3]),
482                        <$len>::new::<meter>(self.mat[1][3]),
483                        <$len>::new::<meter>(self.mat[2][3]),
484                    )
485                }
486            }
487
488            /// `p` mapped through the transform: rotation, then translation.
489            pub fn transform_point(&self, p: Point3<$len>) -> Point3<$len> {
490                #[cfg(feature = "glam")]
491                {
492                    match &self.inner {
493                        TransformInner::$variant(affine) => {
494                            let out = affine.transform_point3(<$vec>::new(
495                                p.x.raw(),
496                                p.y.raw(),
497                                p.z.raw(),
498                            ));
499                            Point3::new(
500                                <$len>::new::<meter>(out.x),
501                                <$len>::new::<meter>(out.y),
502                                <$len>::new::<meter>(out.z),
503                            )
504                        }
505                        _ => unreachable!(),
506                    }
507                }
508                #[cfg(not(feature = "glam"))]
509                {
510                    let m = &self.mat;
511                    let (x, y, z) = (p.x.raw(), p.y.raw(), p.z.raw());
512                    Point3::new(
513                        <$len>::new::<meter>(m[0][0] * x + m[0][1] * y + m[0][2] * z + m[0][3]),
514                        <$len>::new::<meter>(m[1][0] * x + m[1][1] * y + m[1][2] * z + m[1][3]),
515                        <$len>::new::<meter>(m[2][0] * x + m[2][1] * y + m[2][2] * z + m[2][3]),
516                    )
517                }
518            }
519
520            /// `v` mapped through the rotation only, for directions and
521            /// offsets.
522            pub fn transform_vector(&self, v: Point3<$len>) -> Point3<$len> {
523                #[cfg(feature = "glam")]
524                {
525                    match &self.inner {
526                        TransformInner::$variant(affine) => {
527                            let out = affine.transform_vector3(<$vec>::new(
528                                v.x.raw(),
529                                v.y.raw(),
530                                v.z.raw(),
531                            ));
532                            Point3::new(
533                                <$len>::new::<meter>(out.x),
534                                <$len>::new::<meter>(out.y),
535                                <$len>::new::<meter>(out.z),
536                            )
537                        }
538                        _ => unreachable!(),
539                    }
540                }
541                #[cfg(not(feature = "glam"))]
542                {
543                    let m = &self.mat;
544                    let (x, y, z) = (v.x.raw(), v.y.raw(), v.z.raw());
545                    Point3::new(
546                        <$len>::new::<meter>(m[0][0] * x + m[0][1] * y + m[0][2] * z),
547                        <$len>::new::<meter>(m[1][0] * x + m[1][1] * y + m[1][2] * z),
548                        <$len>::new::<meter>(m[2][0] * x + m[2][1] * y + m[2][2] * z),
549                    )
550                }
551            }
552
553            /// Every point in the set mapped through the transform, in place.
554            ///
555            /// May differ from [`Self::transform_point`] by a rounding step.
556            pub fn transform_points<const N: usize>(&self, points: &mut Point3Soa<$len, N>) {
557                #[cfg(feature = "glam")]
558                let m = match &self.inner {
559                    TransformInner::$variant(affine) => {
560                        let r = &affine.matrix3;
561                        let t = affine.translation;
562                        [
563                            [r.x_axis.x, r.y_axis.x, r.z_axis.x, t.x],
564                            [r.x_axis.y, r.y_axis.y, r.z_axis.y, t.y],
565                            [r.x_axis.z, r.y_axis.z, r.z_axis.z, t.z],
566                        ]
567                    }
568                    _ => unreachable!(),
569                };
570                #[cfg(not(feature = "glam"))]
571                let m = [self.mat[0], self.mat[1], self.mat[2]];
572
573                let n = points.len();
574                for i in 0..n {
575                    let (x, y, z) = (points.x[i].raw(), points.y[i].raw(), points.z[i].raw());
576                    points.x[i] =
577                        <$len>::new::<meter>(m[0][0] * x + m[0][1] * y + m[0][2] * z + m[0][3]);
578                    points.y[i] =
579                        <$len>::new::<meter>(m[1][0] * x + m[1][1] * y + m[1][2] * z + m[1][3]);
580                    points.z[i] =
581                        <$len>::new::<meter>(m[2][0] * x + m[2][1] * y + m[2][2] * z + m[2][3]);
582                }
583            }
584        }
585    };
586}
587
588impl_transform_points!(f32, Length32, F32, Vec3);
589impl_transform_points!(f64, Length64, F64, DVec3);
590
591impl<T: Copy + Debug + Default + 'static> Default for Transform3D<T> {
592    fn default() -> Self {
593        Self::from_matrix([[T::default(); 4]; 4])
594    }
595}
596
597macro_rules! impl_transform_mul {
598    ($ty:ty, $zero:expr, $variant:ident) => {
599        impl Mul for Transform3D<$ty> {
600            type Output = Self;
601
602            fn mul(self, rhs: Self) -> Self::Output {
603                #[cfg(feature = "glam")]
604                {
605                    match (&self.inner, &rhs.inner) {
606                        (TransformInner::$variant(a), TransformInner::$variant(b)) => Self {
607                            inner: TransformInner::$variant(*a * *b),
608                        },
609                        _ => unreachable!(),
610                    }
611                }
612                #[cfg(not(feature = "glam"))]
613                {
614                    let mut result = [[$zero; 4]; 4];
615                    for i in 0..4 {
616                        for j in 0..4 {
617                            let mut sum = $zero;
618                            for k in 0..4 {
619                                sum += self.mat[i][k] * rhs.mat[k][j];
620                            }
621                            result[i][j] = sum;
622                        }
623                    }
624                    Self { mat: result }
625                }
626            }
627        }
628    };
629}
630
631impl_transform_mul!(f32, 0.0f32, F32);
632impl_transform_mul!(f64, 0.0f64, F64);
633
634/// Reference implementations for f32
635impl Mul for &Transform3D<f32> {
636    type Output = Transform3D<f32>;
637
638    fn mul(self, rhs: Self) -> Self::Output {
639        *self * *rhs
640    }
641}
642
643impl Mul<Transform3D<f32>> for &Transform3D<f32> {
644    type Output = Transform3D<f32>;
645
646    fn mul(self, rhs: Transform3D<f32>) -> Self::Output {
647        *self * rhs
648    }
649}
650
651impl Mul<&Transform3D<f32>> for Transform3D<f32> {
652    type Output = Transform3D<f32>;
653
654    fn mul(self, rhs: &Transform3D<f32>) -> Self::Output {
655        self * *rhs
656    }
657}
658
659/// Reference implementations for f64
660impl Mul for &Transform3D<f64> {
661    type Output = Transform3D<f64>;
662
663    fn mul(self, rhs: Self) -> Self::Output {
664        *self * *rhs
665    }
666}
667
668impl Mul<Transform3D<f64>> for &Transform3D<f64> {
669    type Output = Transform3D<f64>;
670
671    fn mul(self, rhs: Transform3D<f64>) -> Self::Output {
672        *self * rhs
673    }
674}
675
676impl Mul<&Transform3D<f64>> for Transform3D<f64> {
677    type Output = Transform3D<f64>;
678
679    fn mul(self, rhs: &Transform3D<f64>) -> Self::Output {
680        self * *rhs
681    }
682}
683
684macro_rules! impl_transform_inverse {
685    ($ty:ty, $zero:expr, $one:expr, $variant:ident) => {
686        impl Transform3D<$ty> {
687            /// Computes the inverse of this transformation matrix.
688            pub fn inverse(&self) -> Self {
689                #[cfg(feature = "glam")]
690                {
691                    match &self.inner {
692                        TransformInner::$variant(affine) => Self {
693                            inner: TransformInner::$variant(affine.inverse()),
694                        },
695                        _ => unreachable!(),
696                    }
697                }
698                #[cfg(not(feature = "glam"))]
699                {
700                    let mat = self.mat;
701                    // Extract rotation matrix (top-left 3x3)
702                    let r = [
703                        [mat[0][0], mat[0][1], mat[0][2]],
704                        [mat[1][0], mat[1][1], mat[1][2]],
705                        [mat[2][0], mat[2][1], mat[2][2]],
706                    ];
707
708                    // Extract translation (top-right 3x1)
709                    let t = [mat[0][3], mat[1][3], mat[2][3]];
710
711                    // Compute transpose of rotation matrix (which is its inverse for orthogonal matrices)
712                    let r_inv = [
713                        [r[0][0], r[1][0], r[2][0]],
714                        [r[0][1], r[1][1], r[2][1]],
715                        [r[0][2], r[1][2], r[2][2]],
716                    ];
717
718                    // Compute -R^T * t
719                    let t_inv = [
720                        -(r_inv[0][0] * t[0] + r_inv[0][1] * t[1] + r_inv[0][2] * t[2]),
721                        -(r_inv[1][0] * t[0] + r_inv[1][1] * t[1] + r_inv[1][2] * t[2]),
722                        -(r_inv[2][0] * t[0] + r_inv[2][1] * t[1] + r_inv[2][2] * t[2]),
723                    ];
724
725                    // Construct the inverse transformation matrix
726                    let mut inv_mat = [[$zero; 4]; 4];
727
728                    // Copy rotation transpose
729                    for i in 0..3 {
730                        for j in 0..3 {
731                            inv_mat[i][j] = r_inv[i][j];
732                        }
733                    }
734
735                    // Copy translation part
736                    inv_mat[0][3] = t_inv[0];
737                    inv_mat[1][3] = t_inv[1];
738                    inv_mat[2][3] = t_inv[2];
739
740                    // Keep the homogeneous coordinate the same
741                    inv_mat[3][3] = $one;
742
743                    Self { mat: inv_mat }
744                }
745            }
746        }
747    };
748}
749
750impl_transform_inverse!(f32, 0.0f32, 1.0f32, F32);
751impl_transform_inverse!(f64, 0.0f64, 1.0f64, F64);
752
753#[cfg(feature = "faer")]
754mod faer_integration {
755    use super::Transform3D;
756    use faer::prelude::*;
757
758    impl From<&Transform3D<f64>> for Mat<f64> {
759        fn from(p: &Transform3D<f64>) -> Self {
760            let mat_array = p.to_matrix();
761            let mut mat: Mat<f64> = Mat::zeros(4, 4);
762            for (r, row) in mat_array.iter().enumerate() {
763                for (c, item) in row.iter().enumerate() {
764                    *mat.get_mut(r, c) = *item;
765                }
766            }
767            mat
768        }
769    }
770
771    impl From<Mat<f64>> for Transform3D<f64> {
772        fn from(mat: Mat<f64>) -> Self {
773            assert_eq!(mat.nrows(), 4);
774            assert_eq!(mat.ncols(), 4);
775            let mut transform = [[0.0; 4]; 4];
776            for (r, row) in transform.iter_mut().enumerate() {
777                for (c, val) in row.iter_mut().enumerate() {
778                    *val = *mat.get(r, c);
779                }
780            }
781            Self::from_matrix(transform)
782        }
783    }
784}
785
786// Optional Nalgebra integration
787#[cfg(feature = "nalgebra")]
788mod nalgebra_integration {
789    use super::Transform3D;
790    use nalgebra::{Isometry3, Matrix3, Matrix4, Rotation3, Translation3, Vector3};
791
792    impl From<&Transform3D<f64>> for Isometry3<f64> {
793        fn from(pose: &Transform3D<f64>) -> Self {
794            let mat_array = pose.to_matrix();
795            let flat_transform: [f64; 16] = core::array::from_fn(|i| mat_array[i / 4][i % 4]);
796            let matrix = Matrix4::from_row_slice(&flat_transform);
797
798            let rotation_matrix: Matrix3<f64> = matrix.fixed_view::<3, 3>(0, 0).into();
799            let rotation = Rotation3::from_matrix_unchecked(rotation_matrix);
800
801            let translation_vector: Vector3<f64> = matrix.fixed_view::<3, 1>(0, 3).into();
802            let translation = Translation3::from(translation_vector);
803
804            Isometry3::from_parts(translation, rotation.into())
805        }
806    }
807
808    impl From<Isometry3<f64>> for Transform3D<f64> {
809        fn from(iso: Isometry3<f64>) -> Self {
810            let matrix = iso.to_homogeneous();
811            let transform = core::array::from_fn(|r| core::array::from_fn(|c| matrix[(r, c)]));
812            Transform3D::from_matrix(transform)
813        }
814    }
815}
816
817// Keep existing glam integration but update it
818#[cfg(feature = "glam")]
819mod glam_integration {
820    use super::Transform3D;
821    use glam::{Affine3A, DAffine3};
822
823    impl From<Transform3D<f64>> for DAffine3 {
824        fn from(p: Transform3D<f64>) -> Self {
825            let mat = p.to_matrix();
826            let mut aff = DAffine3::IDENTITY;
827            aff.matrix3.x_axis.x = mat[0][0];
828            aff.matrix3.x_axis.y = mat[0][1];
829            aff.matrix3.x_axis.z = mat[0][2];
830
831            aff.matrix3.y_axis.x = mat[1][0];
832            aff.matrix3.y_axis.y = mat[1][1];
833            aff.matrix3.y_axis.z = mat[1][2];
834
835            aff.matrix3.z_axis.x = mat[2][0];
836            aff.matrix3.z_axis.y = mat[2][1];
837            aff.matrix3.z_axis.z = mat[2][2];
838
839            aff.translation.x = mat[3][0];
840            aff.translation.y = mat[3][1];
841            aff.translation.z = mat[3][2];
842
843            aff
844        }
845    }
846
847    impl From<DAffine3> for Transform3D<f64> {
848        fn from(aff: DAffine3) -> Self {
849            let mut transform = [[0.0f64; 4]; 4];
850
851            transform[0][0] = aff.matrix3.x_axis.x;
852            transform[0][1] = aff.matrix3.x_axis.y;
853            transform[0][2] = aff.matrix3.x_axis.z;
854
855            transform[1][0] = aff.matrix3.y_axis.x;
856            transform[1][1] = aff.matrix3.y_axis.y;
857            transform[1][2] = aff.matrix3.y_axis.z;
858
859            transform[2][0] = aff.matrix3.z_axis.x;
860            transform[2][1] = aff.matrix3.z_axis.y;
861            transform[2][2] = aff.matrix3.z_axis.z;
862
863            transform[3][0] = aff.translation.x;
864            transform[3][1] = aff.translation.y;
865            transform[3][2] = aff.translation.z;
866            transform[3][3] = 1.0;
867
868            Transform3D::from_matrix(transform)
869        }
870    }
871
872    impl From<Transform3D<f32>> for Affine3A {
873        fn from(p: Transform3D<f32>) -> Self {
874            let mat = p.to_matrix();
875            let mut aff = Affine3A::IDENTITY;
876            aff.matrix3.x_axis.x = mat[0][0];
877            aff.matrix3.x_axis.y = mat[0][1];
878            aff.matrix3.x_axis.z = mat[0][2];
879
880            aff.matrix3.y_axis.x = mat[1][0];
881            aff.matrix3.y_axis.y = mat[1][1];
882            aff.matrix3.y_axis.z = mat[1][2];
883
884            aff.matrix3.z_axis.x = mat[2][0];
885            aff.matrix3.z_axis.y = mat[2][1];
886            aff.matrix3.z_axis.z = mat[2][2];
887
888            aff.translation.x = mat[0][3];
889            aff.translation.y = mat[1][3];
890            aff.translation.z = mat[2][3];
891
892            aff
893        }
894    }
895
896    impl From<Affine3A> for Transform3D<f32> {
897        fn from(aff: Affine3A) -> Self {
898            let mut transform = [[0.0f32; 4]; 4];
899
900            transform[0][0] = aff.matrix3.x_axis.x;
901            transform[0][1] = aff.matrix3.x_axis.y;
902            transform[0][2] = aff.matrix3.x_axis.z;
903
904            transform[1][0] = aff.matrix3.y_axis.x;
905            transform[1][1] = aff.matrix3.y_axis.y;
906            transform[1][2] = aff.matrix3.y_axis.z;
907
908            transform[2][0] = aff.matrix3.z_axis.x;
909            transform[2][1] = aff.matrix3.z_axis.y;
910            transform[2][2] = aff.matrix3.z_axis.z;
911
912            transform[0][3] = aff.translation.x;
913            transform[1][3] = aff.translation.y;
914            transform[2][3] = aff.translation.z;
915            transform[3][3] = 1.0;
916
917            Transform3D::from_matrix(transform)
918        }
919    }
920}
921
922#[cfg(feature = "nalgebra")]
923#[allow(unused_imports)]
924pub use nalgebra_integration::*;
925
926#[cfg(feature = "faer")]
927#[allow(unused_imports)]
928pub use faer_integration::*;
929
930#[cfg(test)]
931mod tests {
932    use super::*;
933
934    const CONST_PARENT_TO_INTERMEDIATE: Transform3D<f32> =
935        Transform3D::<f32>::from_translation_euler_xyz(
936            [
937                Length32 { value: 1.0 },
938                Length32 { value: 2.0 },
939                Length32 { value: 3.0 },
940            ],
941            [
942                Angle32 { value: 0.0 },
943                Angle32 { value: 0.0 },
944                Angle32 {
945                    value: core::f32::consts::FRAC_PI_2,
946                },
947            ],
948        );
949    const CONST_INTERMEDIATE_TO_CHILD: Transform3D<f32> =
950        Transform3D::<f32>::from_translation_euler_xyz(
951            [
952                Length32 { value: 1.0 },
953                Length32 { value: 0.0 },
954                Length32 { value: 0.0 },
955            ],
956            [Angle32 { value: 0.0 }; 3],
957        );
958    const CONST_PARENT_TO_CHILD: Transform3D<f32> =
959        CONST_PARENT_TO_INTERMEDIATE.compose(CONST_INTERMEDIATE_TO_CHILD);
960    const CONST_TRANSFORM_F64: Transform3D<f64> = Transform3D::<f64>::from_translation_euler_xyz(
961        [Length64 { value: 0.0 }; 3],
962        [
963            Angle64 {
964                value: core::f64::consts::PI / 6.0,
965            },
966            Angle64 {
967                value: -core::f64::consts::PI / 9.0,
968            },
969            Angle64 {
970                value: core::f64::consts::PI / 18.0,
971            },
972        ],
973    );
974
975    fn assert_matrix_close<const N: usize, T: Copy + Into<f64>>(
976        lhs: [[T; N]; N],
977        rhs: [[T; N]; N],
978        eps: f64,
979    ) {
980        for i in 0..N {
981            for j in 0..N {
982                let lhs = lhs[i][j].into();
983                let rhs = rhs[i][j].into();
984                assert!(
985                    (lhs - rhs).abs() <= eps,
986                    "Element at [{},{}] differs: {} vs expected {}",
987                    i,
988                    j,
989                    lhs,
990                    rhs
991                );
992            }
993        }
994    }
995
996    #[test]
997    fn const_sin_cos_matches_runtime_trigonometry() {
998        for step in -64..=64 {
999            let angle = f64::from(step) * core::f64::consts::PI / 8.0;
1000            let (actual_sin, actual_cos) = const_sin_cos(angle);
1001            let (expected_sin, expected_cos) = angle.sin_cos();
1002            assert!((actual_sin - expected_sin).abs() <= 1e-14);
1003            assert!((actual_cos - expected_cos).abs() <= 1e-14);
1004        }
1005    }
1006
1007    #[test]
1008    fn const_transform_construction_and_composition_are_semantic() {
1009        assert_point_close(
1010            CONST_PARENT_TO_CHILD.position(),
1011            Point3f::from_meters(1.0, 3.0, 3.0),
1012            1e-5,
1013        );
1014        assert_point_close(
1015            CONST_PARENT_TO_CHILD.transform_vector(Point3f::from_meters(1.0, 0.0, 0.0)),
1016            Point3f::from_meters(0.0, 1.0, 0.0),
1017            1e-5,
1018        );
1019
1020        let point = Point3::new(
1021            Length64::new::<meter>(1.0),
1022            Length64::new::<meter>(2.0),
1023            Length64::new::<meter>(3.0),
1024        );
1025        let transformed = CONST_TRANSFORM_F64.transform_vector(point);
1026        assert!(transformed.x.raw().is_finite());
1027        assert!(transformed.y.raw().is_finite());
1028        assert!(transformed.z.raw().is_finite());
1029    }
1030
1031    #[test]
1032    fn test_pose_default() {
1033        let pose: Transform3D<f32> = Transform3D::default();
1034        let mat = pose.to_matrix();
1035
1036        // With glam feature, the default is created from a zero matrix
1037        // but internally glam may adjust it to ensure valid transforms
1038        #[cfg(feature = "glam")]
1039        {
1040            // When we create from all zeros, glam will create a transform
1041            // that has zeros except for the homogeneous coordinate
1042            let expected = [
1043                [0.0, 0.0, 0.0, 0.0],
1044                [0.0, 0.0, 0.0, 0.0],
1045                [0.0, 0.0, 0.0, 0.0],
1046                [0.0, 0.0, 0.0, 1.0], // homogeneous coordinate
1047            ];
1048            assert_eq!(mat, expected, "Default pose with glam should have w=1");
1049        }
1050
1051        #[cfg(not(feature = "glam"))]
1052        {
1053            assert_eq!(
1054                mat, [[0.0; 4]; 4],
1055                "Default pose without glam should be a zero matrix"
1056            );
1057        }
1058    }
1059
1060    #[test]
1061    fn test_transform_inverse_f32() {
1062        // Create a test transform with rotation and translation
1063        let transform = Transform3D::<f32>::from_matrix([
1064            [1.0, 0.0, 0.0, 2.0], // x-axis with 2m translation
1065            [0.0, 1.0, 0.0, 3.0], // y-axis with 3m translation
1066            [0.0, 0.0, 1.0, 4.0], // z-axis with 4m translation
1067            [0.0, 0.0, 0.0, 1.0], // homogeneous coordinate
1068        ]);
1069
1070        // Compute inverse
1071        let inverse = transform.inverse();
1072
1073        // Expected inverse for this transform
1074        let expected_inverse = Transform3D::<f32>::from_matrix([
1075            [1.0, 0.0, 0.0, -2.0], // Negated translation
1076            [0.0, 1.0, 0.0, -3.0],
1077            [0.0, 0.0, 1.0, -4.0],
1078            [0.0, 0.0, 0.0, 1.0],
1079        ]);
1080
1081        // Check each element with a small epsilon for floating-point comparison
1082        let epsilon = 1e-5;
1083        let inv_mat = inverse.to_matrix();
1084        let exp_mat = expected_inverse.to_matrix();
1085        assert_matrix_close(inv_mat, exp_mat, epsilon);
1086    }
1087
1088    #[test]
1089    fn test_transform_inverse_f64() {
1090        // Create a test transform with rotation and translation
1091        let transform = Transform3D::<f64>::from_matrix([
1092            [0.0, -1.0, 0.0, 5.0], // 90-degree rotation around z with translation
1093            [1.0, 0.0, 0.0, 6.0],
1094            [0.0, 0.0, 1.0, 7.0],
1095            [0.0, 0.0, 0.0, 1.0],
1096        ]);
1097
1098        // Compute inverse
1099        let inverse = transform.inverse();
1100
1101        // Expected inverse for this transform
1102        let expected_inverse = Transform3D::<f64>::from_matrix([
1103            [0.0, 1.0, 0.0, -6.0], // Transposed rotation and adjusted translation
1104            [-1.0, 0.0, 0.0, 5.0],
1105            [0.0, 0.0, 1.0, -7.0],
1106            [0.0, 0.0, 0.0, 1.0],
1107        ]);
1108
1109        // Check each element with a small epsilon for floating-point comparison
1110        let epsilon = 1e-10;
1111        let inv_mat = inverse.to_matrix();
1112        let exp_mat = expected_inverse.to_matrix();
1113        assert_matrix_close(inv_mat, exp_mat, epsilon);
1114    }
1115
1116    #[test]
1117    fn test_transform_inverse_identity() {
1118        // Create identity transform
1119        let identity = Transform3D::<f32>::from_matrix([
1120            [1.0, 0.0, 0.0, 0.0],
1121            [0.0, 1.0, 0.0, 0.0],
1122            [0.0, 0.0, 1.0, 0.0],
1123            [0.0, 0.0, 0.0, 1.0],
1124        ]);
1125
1126        // Inverse of identity should be identity
1127        let inverse = identity.inverse();
1128
1129        // Check if inverse is also identity
1130        let epsilon = 1e-5;
1131        let inv_mat = inverse.to_matrix();
1132        let id_mat = identity.to_matrix();
1133        assert_matrix_close(inv_mat, id_mat, epsilon);
1134    }
1135
1136    #[test]
1137    fn test_transform_multiplication_f32() {
1138        // Create two transforms to multiply
1139        let t1 = Transform3D::<f32>::from_matrix([
1140            [1.0, 0.0, 0.0, 2.0], // Identity rotation + translation (2,3,4)
1141            [0.0, 1.0, 0.0, 3.0],
1142            [0.0, 0.0, 1.0, 4.0],
1143            [0.0, 0.0, 0.0, 1.0],
1144        ]);
1145
1146        let t2 = Transform3D::<f32>::from_matrix([
1147            [0.0, -1.0, 0.0, 5.0], // 90-degree rotation around z + translation (5,6,7)
1148            [1.0, 0.0, 0.0, 6.0],
1149            [0.0, 0.0, 1.0, 7.0],
1150            [0.0, 0.0, 0.0, 1.0],
1151        ]);
1152
1153        // Compute t1 * t2
1154        let result = t1 * t2;
1155
1156        // Expected result: t1 * t2 represents first rotating by t2, then translating by t1
1157        let expected = Transform3D::<f32>::from_matrix([
1158            [0.0, -1.0, 0.0, 7.0], // Rotation from t2 + combined translation
1159            [1.0, 0.0, 0.0, 9.0],
1160            [0.0, 0.0, 1.0, 11.0],
1161            [0.0, 0.0, 0.0, 1.0],
1162        ]);
1163
1164        // Check results
1165        let epsilon = 1e-5;
1166        let res_mat = result.to_matrix();
1167        let exp_mat = expected.to_matrix();
1168        assert_matrix_close(res_mat, exp_mat, epsilon);
1169    }
1170
1171    #[test]
1172    fn test_transform_multiplication_f64() {
1173        // Create two transforms to multiply
1174        let t1 = Transform3D::<f64>::from_matrix([
1175            [1.0, 0.0, 0.0, 2.0], // Identity rotation + translation (2,3,4)
1176            [0.0, 1.0, 0.0, 3.0],
1177            [0.0, 0.0, 1.0, 4.0],
1178            [0.0, 0.0, 0.0, 1.0],
1179        ]);
1180
1181        let t2 = Transform3D::<f64>::from_matrix([
1182            [0.0, -1.0, 0.0, 5.0], // 90-degree rotation around z + translation (5,6,7)
1183            [1.0, 0.0, 0.0, 6.0],
1184            [0.0, 0.0, 1.0, 7.0],
1185            [0.0, 0.0, 0.0, 1.0],
1186        ]);
1187
1188        // Compute t1 * t2
1189        let result = t1 * t2;
1190
1191        // Expected result
1192        let expected = Transform3D::<f64>::from_matrix([
1193            [0.0, -1.0, 0.0, 7.0], // Rotation from t2 + combined translation
1194            [1.0, 0.0, 0.0, 9.0],
1195            [0.0, 0.0, 1.0, 11.0],
1196            [0.0, 0.0, 0.0, 1.0],
1197        ]);
1198
1199        // Check results
1200        let epsilon = 1e-10;
1201        let res_mat = result.to_matrix();
1202        let exp_mat = expected.to_matrix();
1203        assert_matrix_close(res_mat, exp_mat, epsilon);
1204    }
1205
1206    #[test]
1207    fn test_transform_reference_multiplication() {
1208        // Test multiplication on references
1209        let t1 = Transform3D::<f32>::from_matrix([
1210            [1.0, 0.0, 0.0, 2.0],
1211            [0.0, 1.0, 0.0, 3.0],
1212            [0.0, 0.0, 1.0, 4.0],
1213            [0.0, 0.0, 0.0, 1.0],
1214        ]);
1215
1216        let t2 = Transform3D::<f32>::from_matrix([
1217            [0.0, -1.0, 0.0, 5.0],
1218            [1.0, 0.0, 0.0, 6.0],
1219            [0.0, 0.0, 1.0, 7.0],
1220            [0.0, 0.0, 0.0, 1.0],
1221        ]);
1222
1223        // Compute &t1 * &t2
1224        let result = t1 * t2;
1225
1226        // Expected result
1227        let expected = Transform3D::<f32>::from_matrix([
1228            [0.0, -1.0, 0.0, 7.0],
1229            [1.0, 0.0, 0.0, 9.0],
1230            [0.0, 0.0, 1.0, 11.0],
1231            [0.0, 0.0, 0.0, 1.0],
1232        ]);
1233
1234        // Check results
1235        let epsilon = 1e-5;
1236        let res_mat = result.to_matrix();
1237        let exp_mat = expected.to_matrix();
1238        assert_matrix_close(res_mat, exp_mat, epsilon);
1239    }
1240
1241    #[cfg(feature = "faer")]
1242    #[test]
1243    fn test_pose_faer_conversion() {
1244        use faer::prelude::*;
1245
1246        let pose = Transform3D::from_matrix([
1247            [1.0, 2.0, 3.0, 4.0],
1248            [5.0, 6.0, 7.0, 8.0],
1249            [9.0, 10.0, 11.0, 12.0],
1250            [13.0, 14.0, 15.0, 16.0],
1251        ]);
1252
1253        let mat: Mat<f64> = (&pose).into();
1254        let pose_from_mat = Transform3D::from(mat);
1255
1256        assert_eq!(
1257            pose.to_matrix(),
1258            pose_from_mat.to_matrix(),
1259            "Faer conversion should be lossless"
1260        );
1261    }
1262
1263    #[cfg(feature = "nalgebra")]
1264    #[test]
1265    fn test_pose_nalgebra_conversion() {
1266        use nalgebra::Isometry3;
1267
1268        let pose = Transform3D::from_matrix([
1269            [1.0, 0.0, 0.0, 2.0],
1270            [0.0, 1.0, 0.0, 3.0],
1271            [0.0, 0.0, 1.0, 4.0],
1272            [0.0, 0.0, 0.0, 1.0],
1273        ]);
1274
1275        let iso: Isometry3<f64> = (&pose.clone()).into();
1276        let pose_from_iso: Transform3D<f64> = iso.into();
1277
1278        assert_eq!(
1279            pose.to_matrix(),
1280            pose_from_iso.to_matrix(),
1281            "Nalgebra conversion should be lossless"
1282        );
1283    }
1284
1285    #[cfg(feature = "glam")]
1286    #[test]
1287    fn test_pose_glam_conversion() {
1288        use glam::DAffine3;
1289
1290        let pose = Transform3D::from_matrix([
1291            [1.0, 0.0, 0.0, 0.0],
1292            [0.0, 1.0, 0.0, 0.0],
1293            [0.0, 0.0, 1.0, 0.0],
1294            [5.0, 6.0, 7.0, 1.0],
1295        ]);
1296        let aff: DAffine3 = pose.into();
1297        assert_eq!(aff.translation[0], 5.0);
1298        let pose_from_aff: Transform3D<f64> = aff.into();
1299
1300        assert_eq!(
1301            pose.to_matrix(),
1302            pose_from_aff.to_matrix(),
1303            "Glam conversion should be lossless"
1304        );
1305    }
1306
1307    #[cfg(feature = "glam")]
1308    #[test]
1309    fn test_matrix_format_issue() {
1310        use glam::Mat4;
1311
1312        // Test case: row-major matrix with translation in last column
1313        let row_major = [
1314            [1.0, 0.0, 0.0, 5.0], // row 0: x-axis + x translation
1315            [0.0, 1.0, 0.0, 6.0], // row 1: y-axis + y translation
1316            [0.0, 0.0, 1.0, 7.0], // row 2: z-axis + z translation
1317            [0.0, 0.0, 0.0, 1.0], // row 3: homogeneous
1318        ];
1319
1320        // What glam expects: column-major format
1321        // Each inner array is a COLUMN, not a row
1322        let col_major = [
1323            [1.0, 0.0, 0.0, 0.0], // column 0: x-axis
1324            [0.0, 1.0, 0.0, 0.0], // column 1: y-axis
1325            [0.0, 0.0, 1.0, 0.0], // column 2: z-axis
1326            [5.0, 6.0, 7.0, 1.0], // column 3: translation + w
1327        ];
1328
1329        // Create matrices
1330        let mat_from_row = Mat4::from_cols_array_2d(&row_major);
1331        let mat_from_col = Mat4::from_cols_array_2d(&col_major);
1332
1333        // When using row-major data directly, translation ends up in wrong place
1334        assert_ne!(mat_from_row.w_axis.x, 5.0); // Translation is NOT where we expect
1335
1336        // When using column-major data, translation is correct
1337        assert_eq!(mat_from_col.w_axis.x, 5.0);
1338        assert_eq!(mat_from_col.w_axis.y, 6.0);
1339        assert_eq!(mat_from_col.w_axis.z, 7.0);
1340
1341        // The fix: transpose the row-major matrix
1342        let mat_transposed = Mat4::from_cols_array_2d(&row_major).transpose();
1343        assert_eq!(mat_transposed.w_axis.x, 5.0);
1344        assert_eq!(mat_transposed.w_axis.y, 6.0);
1345        assert_eq!(mat_transposed.w_axis.z, 7.0);
1346    }
1347
1348    /// 90 degrees around z plus a (2, 3, 4) translation, in the layout the
1349    /// active backend expects: glam stores column-major, the fallback
1350    /// row-major.
1351    fn quarter_turn_and_shift() -> Transform3D<f32> {
1352        #[cfg(feature = "glam")]
1353        {
1354            Transform3D::from_matrix([
1355                [0.0, 1.0, 0.0, 0.0],
1356                [-1.0, 0.0, 0.0, 0.0],
1357                [0.0, 0.0, 1.0, 0.0],
1358                [2.0, 3.0, 4.0, 1.0],
1359            ])
1360        }
1361        #[cfg(not(feature = "glam"))]
1362        {
1363            Transform3D::from_matrix([
1364                [0.0, -1.0, 0.0, 2.0],
1365                [1.0, 0.0, 0.0, 3.0],
1366                [0.0, 0.0, 1.0, 4.0],
1367                [0.0, 0.0, 0.0, 1.0],
1368            ])
1369        }
1370    }
1371
1372    fn assert_point_close(lhs: Point3f, rhs: Point3f, eps: f32) {
1373        assert!(
1374            (lhs.x - rhs.x).raw().abs() <= eps
1375                && (lhs.y - rhs.y).raw().abs() <= eps
1376                && (lhs.z - rhs.z).raw().abs() <= eps,
1377            "{lhs:?} differs from {rhs:?}"
1378        );
1379    }
1380
1381    #[test]
1382    fn transform_point_rotates_and_translates() {
1383        let t = quarter_turn_and_shift();
1384        let p = Point3f::from_meters(1.0, 0.0, 0.0);
1385        assert_point_close(
1386            t.transform_point(p),
1387            Point3f::from_meters(2.0, 4.0, 4.0),
1388            1e-5,
1389        );
1390        assert_point_close(
1391            t.transform_vector(p),
1392            Point3f::from_meters(0.0, 1.0, 0.0),
1393            1e-5,
1394        );
1395        assert_point_close(t.position(), Point3f::from_meters(2.0, 3.0, 4.0), 1e-5);
1396    }
1397
1398    #[test]
1399    fn transform_accessors_are_backend_independent() {
1400        let t = quarter_turn_and_shift();
1401
1402        assert_eq!(
1403            t.translation(),
1404            [
1405                Length32::new::<meter>(2.0),
1406                Length32::new::<meter>(3.0),
1407                Length32::new::<meter>(4.0),
1408            ]
1409        );
1410        assert_eq!(
1411            t.rotation(),
1412            [[0.0, -1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]]
1413        );
1414    }
1415
1416    #[test]
1417    fn transform_point_identity_is_a_fixed_point() {
1418        let identity = Transform3D::<f32>::from_matrix([
1419            [1.0, 0.0, 0.0, 0.0],
1420            [0.0, 1.0, 0.0, 0.0],
1421            [0.0, 0.0, 1.0, 0.0],
1422            [0.0, 0.0, 0.0, 1.0],
1423        ]);
1424        let p = Point3f::from_meters(1.5, -2.0, 0.25);
1425        assert_eq!(identity.transform_point(p), p);
1426        assert_eq!(identity.transform_vector(p), p);
1427        assert_eq!(identity.position(), Point3f::default());
1428    }
1429
1430    #[test]
1431    fn transform_points_matches_transform_point() {
1432        let t = quarter_turn_and_shift();
1433        let points = [
1434            Point3f::from_meters(1.0, 0.0, 0.0),
1435            Point3f::from_meters(-1.5, 2.0, 0.25),
1436            Point3f::from_meters(0.0, 0.0, 0.0),
1437        ];
1438        let mut set = Point3fSoa::<4>::default();
1439        for p in points {
1440            set.push(p);
1441        }
1442
1443        t.transform_points(&mut set);
1444        for (i, p) in points.iter().enumerate() {
1445            assert_point_close(set.get(i), t.transform_point(*p), 1e-5);
1446        }
1447    }
1448
1449    #[test]
1450    fn geodetic_position_converts_to_and_from_degrees() {
1451        let position = GeodeticPosition::from_degrees(59.319_221, 18.075_631);
1452
1453        assert_eq!(position.latitude.get::<degree>(), 59.319_221);
1454        assert_eq!(position.longitude.get::<degree>(), 18.075_631);
1455        assert_eq!(position.latitude_degrees(), 59.319_221);
1456        assert_eq!(position.longitude_degrees(), 18.075_631);
1457    }
1458}