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    /// Creates a homogeneous 4x4 transform indexed as `mat[row][column]`.
375    /// Each inner array is a row; points are column vectors (`p_out = mat * p_in`).
376    /// Translation is `[mat[0][3], mat[1][3], mat[2][3]]`; the last row must be `[0, 0, 0, 1]`.
377    /// The convention is backend-independent: callers must not transpose for glam.
378    /// Glam/Rerun adapters repack into columns internally; the fallback stores rows directly.
379    /// Nalgebra/faer adapters copy by `(row, column)`, independent of storage order.
380    pub fn from_matrix(mat: [[T; 4]; 4]) -> Self {
381        #[cfg(feature = "glam")]
382        {
383            Self {
384                inner: TransformInner::from_matrix(mat),
385            }
386        }
387        #[cfg(not(feature = "glam"))]
388        {
389            Self { mat }
390        }
391    }
392
393    /// Returns a homogeneous 4x4 matrix indexed as `mat[row][column]`.
394    /// Each inner array is a row, with translation in the last column, regardless of backend.
395    /// Uses the same column-vector convention as [`Self::from_matrix`].
396    pub fn to_matrix(self) -> [[T; 4]; 4] {
397        #[cfg(feature = "glam")]
398        {
399            self.inner.to_matrix()
400        }
401        #[cfg(not(feature = "glam"))]
402        {
403            self.mat
404        }
405    }
406
407    /// Get a mutable reference to the matrix (for compatibility)
408    #[cfg(not(feature = "glam"))]
409    pub fn mat_mut(&mut self) -> &mut [[T; 4]; 4] {
410        &mut self.mat
411    }
412}
413
414#[cfg(feature = "glam")]
415impl<T: Copy + Debug + Default + 'static> TransformInner<T> {
416    fn from_matrix(mat: [[T; 4]; 4]) -> Self {
417        use core::any::TypeId;
418
419        // This is a bit hacky but necessary for type safety
420        // In practice, T will be f32 or f64
421        if TypeId::of::<T>() == TypeId::of::<f32>() {
422            // Convert to f32 matrix
423            // SAFETY: We just verified T == f32, so the layouts match.
424            let mat_f32: [[f32; 4]; 4] = unsafe { core::mem::transmute_copy(&mat) };
425            // `mat` is row-major; glam stores column-major, so transpose at the boundary.
426            let glam_mat = Mat4::from_cols_array_2d(&mat_f32).transpose();
427            let affine = Affine3A::from_mat4(glam_mat);
428            // SAFETY: We just verified T == f32, so this is the correct enum variant.
429            unsafe { core::mem::transmute_copy(&TransformInner::<T>::F32(affine)) }
430        } else if TypeId::of::<T>() == TypeId::of::<f64>() {
431            // Convert to f64 matrix
432            // SAFETY: We just verified T == f64, so the layouts match.
433            let mat_f64: [[f64; 4]; 4] = unsafe { core::mem::transmute_copy(&mat) };
434            // `mat` is row-major; glam stores column-major, so transpose at the boundary.
435            let glam_mat = DMat4::from_cols_array_2d(&mat_f64).transpose();
436            let affine = DAffine3::from_mat4(glam_mat);
437            // SAFETY: We just verified T == f64, so this is the correct enum variant.
438            unsafe { core::mem::transmute_copy(&TransformInner::<T>::F64(affine)) }
439        } else {
440            panic!("Transform3D only supports f32 and f64 types when using glam feature");
441        }
442    }
443
444    fn to_matrix(self) -> [[T; 4]; 4] {
445        match self {
446            TransformInner::F32(affine) => {
447                let mat = Mat4::from(affine);
448                // glam is column-major; transposing then taking columns yields row-major.
449                let mat_array = mat.transpose().to_cols_array_2d();
450                // SAFETY: We only reach this arm when T == f32.
451                unsafe { core::mem::transmute_copy(&mat_array) }
452            }
453            TransformInner::F64(affine) => {
454                let mat = DMat4::from(affine);
455                // glam is column-major; transposing then taking columns yields row-major.
456                let mat_array = mat.transpose().to_cols_array_2d();
457                // SAFETY: We only reach this arm when T == f64.
458                unsafe { core::mem::transmute_copy(&mat_array) }
459            }
460            TransformInner::_Phantom(_) => unreachable!(),
461        }
462    }
463}
464
465impl_transform_accessors!(f32, Length32, F32);
466impl_transform_accessors!(f64, Length64, F64);
467
468macro_rules! impl_transform_points {
469    ($ty:ty, $len:ty, $variant:ident, $vec:ty) => {
470        impl Transform3D<$ty> {
471            /// The translation as a point: where the pose sits in its parent
472            /// frame.
473            pub fn position(&self) -> Point3<$len> {
474                #[cfg(feature = "glam")]
475                {
476                    match &self.inner {
477                        TransformInner::$variant(affine) => {
478                            let t = affine.translation;
479                            Point3::new(
480                                <$len>::new::<meter>(t.x as $ty),
481                                <$len>::new::<meter>(t.y as $ty),
482                                <$len>::new::<meter>(t.z as $ty),
483                            )
484                        }
485                        _ => unreachable!(),
486                    }
487                }
488                #[cfg(not(feature = "glam"))]
489                {
490                    Point3::new(
491                        <$len>::new::<meter>(self.mat[0][3]),
492                        <$len>::new::<meter>(self.mat[1][3]),
493                        <$len>::new::<meter>(self.mat[2][3]),
494                    )
495                }
496            }
497
498            /// `p` mapped through the transform: rotation, then translation.
499            pub fn transform_point(&self, p: Point3<$len>) -> Point3<$len> {
500                #[cfg(feature = "glam")]
501                {
502                    match &self.inner {
503                        TransformInner::$variant(affine) => {
504                            let out = affine.transform_point3(<$vec>::new(
505                                p.x.raw(),
506                                p.y.raw(),
507                                p.z.raw(),
508                            ));
509                            Point3::new(
510                                <$len>::new::<meter>(out.x),
511                                <$len>::new::<meter>(out.y),
512                                <$len>::new::<meter>(out.z),
513                            )
514                        }
515                        _ => unreachable!(),
516                    }
517                }
518                #[cfg(not(feature = "glam"))]
519                {
520                    let m = &self.mat;
521                    let (x, y, z) = (p.x.raw(), p.y.raw(), p.z.raw());
522                    Point3::new(
523                        <$len>::new::<meter>(m[0][0] * x + m[0][1] * y + m[0][2] * z + m[0][3]),
524                        <$len>::new::<meter>(m[1][0] * x + m[1][1] * y + m[1][2] * z + m[1][3]),
525                        <$len>::new::<meter>(m[2][0] * x + m[2][1] * y + m[2][2] * z + m[2][3]),
526                    )
527                }
528            }
529
530            /// `v` mapped through the rotation only, for directions and
531            /// offsets.
532            pub fn transform_vector(&self, v: Point3<$len>) -> Point3<$len> {
533                #[cfg(feature = "glam")]
534                {
535                    match &self.inner {
536                        TransformInner::$variant(affine) => {
537                            let out = affine.transform_vector3(<$vec>::new(
538                                v.x.raw(),
539                                v.y.raw(),
540                                v.z.raw(),
541                            ));
542                            Point3::new(
543                                <$len>::new::<meter>(out.x),
544                                <$len>::new::<meter>(out.y),
545                                <$len>::new::<meter>(out.z),
546                            )
547                        }
548                        _ => unreachable!(),
549                    }
550                }
551                #[cfg(not(feature = "glam"))]
552                {
553                    let m = &self.mat;
554                    let (x, y, z) = (v.x.raw(), v.y.raw(), v.z.raw());
555                    Point3::new(
556                        <$len>::new::<meter>(m[0][0] * x + m[0][1] * y + m[0][2] * z),
557                        <$len>::new::<meter>(m[1][0] * x + m[1][1] * y + m[1][2] * z),
558                        <$len>::new::<meter>(m[2][0] * x + m[2][1] * y + m[2][2] * z),
559                    )
560                }
561            }
562
563            /// Every point in the set mapped through the transform, in place.
564            ///
565            /// May differ from [`Self::transform_point`] by a rounding step.
566            pub fn transform_points<const N: usize>(&self, points: &mut Point3Soa<$len, N>) {
567                #[cfg(feature = "glam")]
568                let m = match &self.inner {
569                    TransformInner::$variant(affine) => {
570                        let r = &affine.matrix3;
571                        let t = affine.translation;
572                        [
573                            [r.x_axis.x, r.y_axis.x, r.z_axis.x, t.x],
574                            [r.x_axis.y, r.y_axis.y, r.z_axis.y, t.y],
575                            [r.x_axis.z, r.y_axis.z, r.z_axis.z, t.z],
576                        ]
577                    }
578                    _ => unreachable!(),
579                };
580                #[cfg(not(feature = "glam"))]
581                let m = [self.mat[0], self.mat[1], self.mat[2]];
582
583                let n = points.len();
584                for i in 0..n {
585                    let (x, y, z) = (points.x[i].raw(), points.y[i].raw(), points.z[i].raw());
586                    points.x[i] =
587                        <$len>::new::<meter>(m[0][0] * x + m[0][1] * y + m[0][2] * z + m[0][3]);
588                    points.y[i] =
589                        <$len>::new::<meter>(m[1][0] * x + m[1][1] * y + m[1][2] * z + m[1][3]);
590                    points.z[i] =
591                        <$len>::new::<meter>(m[2][0] * x + m[2][1] * y + m[2][2] * z + m[2][3]);
592                }
593            }
594        }
595    };
596}
597
598impl_transform_points!(f32, Length32, F32, Vec3);
599impl_transform_points!(f64, Length64, F64, DVec3);
600
601impl<T: Copy + Debug + Default + 'static> Default for Transform3D<T> {
602    fn default() -> Self {
603        Self::from_matrix([[T::default(); 4]; 4])
604    }
605}
606
607macro_rules! impl_transform_mul {
608    ($ty:ty, $zero:expr, $variant:ident) => {
609        impl Mul for Transform3D<$ty> {
610            type Output = Self;
611
612            fn mul(self, rhs: Self) -> Self::Output {
613                #[cfg(feature = "glam")]
614                {
615                    match (&self.inner, &rhs.inner) {
616                        (TransformInner::$variant(a), TransformInner::$variant(b)) => Self {
617                            inner: TransformInner::$variant(*a * *b),
618                        },
619                        _ => unreachable!(),
620                    }
621                }
622                #[cfg(not(feature = "glam"))]
623                {
624                    let mut result = [[$zero; 4]; 4];
625                    for i in 0..4 {
626                        for j in 0..4 {
627                            let mut sum = $zero;
628                            for k in 0..4 {
629                                sum += self.mat[i][k] * rhs.mat[k][j];
630                            }
631                            result[i][j] = sum;
632                        }
633                    }
634                    Self { mat: result }
635                }
636            }
637        }
638    };
639}
640
641impl_transform_mul!(f32, 0.0f32, F32);
642impl_transform_mul!(f64, 0.0f64, F64);
643
644/// Reference implementations for f32
645impl Mul for &Transform3D<f32> {
646    type Output = Transform3D<f32>;
647
648    fn mul(self, rhs: Self) -> Self::Output {
649        *self * *rhs
650    }
651}
652
653impl Mul<Transform3D<f32>> for &Transform3D<f32> {
654    type Output = Transform3D<f32>;
655
656    fn mul(self, rhs: Transform3D<f32>) -> Self::Output {
657        *self * rhs
658    }
659}
660
661impl Mul<&Transform3D<f32>> for Transform3D<f32> {
662    type Output = Transform3D<f32>;
663
664    fn mul(self, rhs: &Transform3D<f32>) -> Self::Output {
665        self * *rhs
666    }
667}
668
669/// Reference implementations for f64
670impl Mul for &Transform3D<f64> {
671    type Output = Transform3D<f64>;
672
673    fn mul(self, rhs: Self) -> Self::Output {
674        *self * *rhs
675    }
676}
677
678impl Mul<Transform3D<f64>> for &Transform3D<f64> {
679    type Output = Transform3D<f64>;
680
681    fn mul(self, rhs: Transform3D<f64>) -> Self::Output {
682        *self * rhs
683    }
684}
685
686impl Mul<&Transform3D<f64>> for Transform3D<f64> {
687    type Output = Transform3D<f64>;
688
689    fn mul(self, rhs: &Transform3D<f64>) -> Self::Output {
690        self * *rhs
691    }
692}
693
694macro_rules! impl_transform_inverse {
695    ($ty:ty, $zero:expr, $one:expr, $variant:ident) => {
696        impl Transform3D<$ty> {
697            /// Computes the inverse of this transformation matrix.
698            pub fn inverse(&self) -> Self {
699                #[cfg(feature = "glam")]
700                {
701                    match &self.inner {
702                        TransformInner::$variant(affine) => Self {
703                            inner: TransformInner::$variant(affine.inverse()),
704                        },
705                        _ => unreachable!(),
706                    }
707                }
708                #[cfg(not(feature = "glam"))]
709                {
710                    let mat = self.mat;
711                    // Extract rotation matrix (top-left 3x3)
712                    let r = [
713                        [mat[0][0], mat[0][1], mat[0][2]],
714                        [mat[1][0], mat[1][1], mat[1][2]],
715                        [mat[2][0], mat[2][1], mat[2][2]],
716                    ];
717
718                    // Extract translation (top-right 3x1)
719                    let t = [mat[0][3], mat[1][3], mat[2][3]];
720
721                    // Compute transpose of rotation matrix (which is its inverse for orthogonal matrices)
722                    let r_inv = [
723                        [r[0][0], r[1][0], r[2][0]],
724                        [r[0][1], r[1][1], r[2][1]],
725                        [r[0][2], r[1][2], r[2][2]],
726                    ];
727
728                    // Compute -R^T * t
729                    let t_inv = [
730                        -(r_inv[0][0] * t[0] + r_inv[0][1] * t[1] + r_inv[0][2] * t[2]),
731                        -(r_inv[1][0] * t[0] + r_inv[1][1] * t[1] + r_inv[1][2] * t[2]),
732                        -(r_inv[2][0] * t[0] + r_inv[2][1] * t[1] + r_inv[2][2] * t[2]),
733                    ];
734
735                    // Construct the inverse transformation matrix
736                    let mut inv_mat = [[$zero; 4]; 4];
737
738                    // Copy rotation transpose
739                    for i in 0..3 {
740                        for j in 0..3 {
741                            inv_mat[i][j] = r_inv[i][j];
742                        }
743                    }
744
745                    // Copy translation part
746                    inv_mat[0][3] = t_inv[0];
747                    inv_mat[1][3] = t_inv[1];
748                    inv_mat[2][3] = t_inv[2];
749
750                    // Keep the homogeneous coordinate the same
751                    inv_mat[3][3] = $one;
752
753                    Self { mat: inv_mat }
754                }
755            }
756        }
757    };
758}
759
760impl_transform_inverse!(f32, 0.0f32, 1.0f32, F32);
761impl_transform_inverse!(f64, 0.0f64, 1.0f64, F64);
762
763#[cfg(feature = "faer")]
764mod faer_integration {
765    use super::Transform3D;
766    use faer::prelude::*;
767
768    impl From<&Transform3D<f64>> for Mat<f64> {
769        fn from(p: &Transform3D<f64>) -> Self {
770            let mat_array = p.to_matrix();
771            let mut mat: Mat<f64> = Mat::zeros(4, 4);
772            for (r, row) in mat_array.iter().enumerate() {
773                for (c, item) in row.iter().enumerate() {
774                    *mat.get_mut(r, c) = *item;
775                }
776            }
777            mat
778        }
779    }
780
781    impl From<Mat<f64>> for Transform3D<f64> {
782        fn from(mat: Mat<f64>) -> Self {
783            assert_eq!(mat.nrows(), 4);
784            assert_eq!(mat.ncols(), 4);
785            let mut transform = [[0.0; 4]; 4];
786            for (r, row) in transform.iter_mut().enumerate() {
787                for (c, val) in row.iter_mut().enumerate() {
788                    *val = *mat.get(r, c);
789                }
790            }
791            Self::from_matrix(transform)
792        }
793    }
794}
795
796// Optional Nalgebra integration
797#[cfg(feature = "nalgebra")]
798mod nalgebra_integration {
799    use super::Transform3D;
800    use nalgebra::{Isometry3, Matrix3, Matrix4, Rotation3, Translation3, Vector3};
801
802    impl From<&Transform3D<f64>> for Isometry3<f64> {
803        fn from(pose: &Transform3D<f64>) -> Self {
804            let mat_array = pose.to_matrix();
805            let flat_transform: [f64; 16] = core::array::from_fn(|i| mat_array[i / 4][i % 4]);
806            let matrix = Matrix4::from_row_slice(&flat_transform);
807
808            let rotation_matrix: Matrix3<f64> = matrix.fixed_view::<3, 3>(0, 0).into();
809            let rotation = Rotation3::from_matrix_unchecked(rotation_matrix);
810
811            let translation_vector: Vector3<f64> = matrix.fixed_view::<3, 1>(0, 3).into();
812            let translation = Translation3::from(translation_vector);
813
814            Isometry3::from_parts(translation, rotation.into())
815        }
816    }
817
818    impl From<Isometry3<f64>> for Transform3D<f64> {
819        fn from(iso: Isometry3<f64>) -> Self {
820            let matrix = iso.to_homogeneous();
821            let transform = core::array::from_fn(|r| core::array::from_fn(|c| matrix[(r, c)]));
822            Transform3D::from_matrix(transform)
823        }
824    }
825}
826
827// Keep existing glam integration but update it
828#[cfg(feature = "glam")]
829mod glam_integration {
830    use super::Transform3D;
831    use glam::{Affine3A, DAffine3, DMat4, Mat4};
832
833    impl From<Transform3D<f64>> for DAffine3 {
834        fn from(p: Transform3D<f64>) -> Self {
835            // `to_matrix` is row-major; glam stores column-major, so transpose.
836            let mat = p.to_matrix();
837            DAffine3::from_mat4(DMat4::from_cols_array_2d(&mat).transpose())
838        }
839    }
840
841    impl From<DAffine3> for Transform3D<f64> {
842        fn from(aff: DAffine3) -> Self {
843            // glam is column-major; transposing then taking columns yields row-major.
844            Transform3D::from_matrix(DMat4::from(aff).transpose().to_cols_array_2d())
845        }
846    }
847
848    impl From<Transform3D<f32>> for Affine3A {
849        fn from(p: Transform3D<f32>) -> Self {
850            // `to_matrix` is row-major; glam stores column-major, so transpose.
851            let mat = p.to_matrix();
852            Affine3A::from_mat4(Mat4::from_cols_array_2d(&mat).transpose())
853        }
854    }
855
856    impl From<Affine3A> for Transform3D<f32> {
857        fn from(aff: Affine3A) -> Self {
858            // glam is column-major; transposing then taking columns yields row-major.
859            Transform3D::from_matrix(Mat4::from(aff).transpose().to_cols_array_2d())
860        }
861    }
862}
863
864#[cfg(feature = "nalgebra")]
865#[allow(unused_imports)]
866pub use nalgebra_integration::*;
867
868#[cfg(feature = "faer")]
869#[allow(unused_imports)]
870pub use faer_integration::*;
871
872#[cfg(test)]
873mod tests {
874    use super::*;
875
876    const CONST_PARENT_TO_INTERMEDIATE: Transform3D<f32> =
877        Transform3D::<f32>::from_translation_euler_xyz(
878            [
879                Length32 { value: 1.0 },
880                Length32 { value: 2.0 },
881                Length32 { value: 3.0 },
882            ],
883            [
884                Angle32 { value: 0.0 },
885                Angle32 { value: 0.0 },
886                Angle32 {
887                    value: core::f32::consts::FRAC_PI_2,
888                },
889            ],
890        );
891    const CONST_INTERMEDIATE_TO_CHILD: Transform3D<f32> =
892        Transform3D::<f32>::from_translation_euler_xyz(
893            [
894                Length32 { value: 1.0 },
895                Length32 { value: 0.0 },
896                Length32 { value: 0.0 },
897            ],
898            [Angle32 { value: 0.0 }; 3],
899        );
900    const CONST_PARENT_TO_CHILD: Transform3D<f32> =
901        CONST_PARENT_TO_INTERMEDIATE.compose(CONST_INTERMEDIATE_TO_CHILD);
902    const CONST_TRANSFORM_F64: Transform3D<f64> = Transform3D::<f64>::from_translation_euler_xyz(
903        [Length64 { value: 0.0 }; 3],
904        [
905            Angle64 {
906                value: core::f64::consts::PI / 6.0,
907            },
908            Angle64 {
909                value: -core::f64::consts::PI / 9.0,
910            },
911            Angle64 {
912                value: core::f64::consts::PI / 18.0,
913            },
914        ],
915    );
916
917    fn assert_matrix_close<const N: usize, T: Copy + Into<f64>>(
918        lhs: [[T; N]; N],
919        rhs: [[T; N]; N],
920        eps: f64,
921    ) {
922        for i in 0..N {
923            for j in 0..N {
924                let lhs = lhs[i][j].into();
925                let rhs = rhs[i][j].into();
926                assert!(
927                    (lhs - rhs).abs() <= eps,
928                    "Element at [{},{}] differs: {} vs expected {}",
929                    i,
930                    j,
931                    lhs,
932                    rhs
933                );
934            }
935        }
936    }
937
938    #[test]
939    fn const_sin_cos_matches_runtime_trigonometry() {
940        for step in -64..=64 {
941            let angle = f64::from(step) * core::f64::consts::PI / 8.0;
942            let (actual_sin, actual_cos) = const_sin_cos(angle);
943            let (expected_sin, expected_cos) = angle.sin_cos();
944            assert!((actual_sin - expected_sin).abs() <= 1e-14);
945            assert!((actual_cos - expected_cos).abs() <= 1e-14);
946        }
947    }
948
949    #[test]
950    fn const_transform_construction_and_composition_are_semantic() {
951        assert_point_close(
952            CONST_PARENT_TO_CHILD.position(),
953            Point3f::from_meters(1.0, 3.0, 3.0),
954            1e-5,
955        );
956        assert_point_close(
957            CONST_PARENT_TO_CHILD.transform_vector(Point3f::from_meters(1.0, 0.0, 0.0)),
958            Point3f::from_meters(0.0, 1.0, 0.0),
959            1e-5,
960        );
961
962        let point = Point3::new(
963            Length64::new::<meter>(1.0),
964            Length64::new::<meter>(2.0),
965            Length64::new::<meter>(3.0),
966        );
967        let transformed = CONST_TRANSFORM_F64.transform_vector(point);
968        assert!(transformed.x.raw().is_finite());
969        assert!(transformed.y.raw().is_finite());
970        assert!(transformed.z.raw().is_finite());
971    }
972
973    #[test]
974    fn test_pose_default() {
975        let pose: Transform3D<f32> = Transform3D::default();
976        let mat = pose.to_matrix();
977
978        // With glam feature, the default is created from a zero matrix
979        // but internally glam may adjust it to ensure valid transforms
980        #[cfg(feature = "glam")]
981        {
982            // When we create from all zeros, glam will create a transform
983            // that has zeros except for the homogeneous coordinate
984            let expected = [
985                [0.0, 0.0, 0.0, 0.0],
986                [0.0, 0.0, 0.0, 0.0],
987                [0.0, 0.0, 0.0, 0.0],
988                [0.0, 0.0, 0.0, 1.0], // homogeneous coordinate
989            ];
990            assert_eq!(mat, expected, "Default pose with glam should have w=1");
991        }
992
993        #[cfg(not(feature = "glam"))]
994        {
995            assert_eq!(
996                mat, [[0.0; 4]; 4],
997                "Default pose without glam should be a zero matrix"
998            );
999        }
1000    }
1001
1002    #[test]
1003    fn test_transform_inverse_f32() {
1004        // Create a test transform with rotation and translation
1005        let transform = Transform3D::<f32>::from_matrix([
1006            [1.0, 0.0, 0.0, 2.0], // x-axis with 2m translation
1007            [0.0, 1.0, 0.0, 3.0], // y-axis with 3m translation
1008            [0.0, 0.0, 1.0, 4.0], // z-axis with 4m translation
1009            [0.0, 0.0, 0.0, 1.0], // homogeneous coordinate
1010        ]);
1011
1012        // Compute inverse
1013        let inverse = transform.inverse();
1014
1015        // Expected inverse for this transform
1016        let expected_inverse = Transform3D::<f32>::from_matrix([
1017            [1.0, 0.0, 0.0, -2.0], // Negated translation
1018            [0.0, 1.0, 0.0, -3.0],
1019            [0.0, 0.0, 1.0, -4.0],
1020            [0.0, 0.0, 0.0, 1.0],
1021        ]);
1022
1023        // Check each element with a small epsilon for floating-point comparison
1024        let epsilon = 1e-5;
1025        let inv_mat = inverse.to_matrix();
1026        let exp_mat = expected_inverse.to_matrix();
1027        assert_matrix_close(inv_mat, exp_mat, epsilon);
1028    }
1029
1030    #[test]
1031    fn test_transform_inverse_f64() {
1032        // Create a test transform with rotation and translation
1033        let transform = Transform3D::<f64>::from_matrix([
1034            [0.0, -1.0, 0.0, 5.0], // 90-degree rotation around z with translation
1035            [1.0, 0.0, 0.0, 6.0],
1036            [0.0, 0.0, 1.0, 7.0],
1037            [0.0, 0.0, 0.0, 1.0],
1038        ]);
1039
1040        // Compute inverse
1041        let inverse = transform.inverse();
1042
1043        // Expected inverse for this transform
1044        let expected_inverse = Transform3D::<f64>::from_matrix([
1045            [0.0, 1.0, 0.0, -6.0], // Transposed rotation and adjusted translation
1046            [-1.0, 0.0, 0.0, 5.0],
1047            [0.0, 0.0, 1.0, -7.0],
1048            [0.0, 0.0, 0.0, 1.0],
1049        ]);
1050
1051        // Check each element with a small epsilon for floating-point comparison
1052        let epsilon = 1e-10;
1053        let inv_mat = inverse.to_matrix();
1054        let exp_mat = expected_inverse.to_matrix();
1055        assert_matrix_close(inv_mat, exp_mat, epsilon);
1056    }
1057
1058    #[test]
1059    fn test_transform_inverse_identity() {
1060        // Create identity transform
1061        let identity = Transform3D::<f32>::from_matrix([
1062            [1.0, 0.0, 0.0, 0.0],
1063            [0.0, 1.0, 0.0, 0.0],
1064            [0.0, 0.0, 1.0, 0.0],
1065            [0.0, 0.0, 0.0, 1.0],
1066        ]);
1067
1068        // Inverse of identity should be identity
1069        let inverse = identity.inverse();
1070
1071        // Check if inverse is also identity
1072        let epsilon = 1e-5;
1073        let inv_mat = inverse.to_matrix();
1074        let id_mat = identity.to_matrix();
1075        assert_matrix_close(inv_mat, id_mat, epsilon);
1076    }
1077
1078    #[test]
1079    fn test_transform_multiplication_f32() {
1080        // Create two transforms to multiply
1081        let t1 = Transform3D::<f32>::from_matrix([
1082            [1.0, 0.0, 0.0, 2.0], // Identity rotation + translation (2,3,4)
1083            [0.0, 1.0, 0.0, 3.0],
1084            [0.0, 0.0, 1.0, 4.0],
1085            [0.0, 0.0, 0.0, 1.0],
1086        ]);
1087
1088        let t2 = Transform3D::<f32>::from_matrix([
1089            [0.0, -1.0, 0.0, 5.0], // 90-degree rotation around z + translation (5,6,7)
1090            [1.0, 0.0, 0.0, 6.0],
1091            [0.0, 0.0, 1.0, 7.0],
1092            [0.0, 0.0, 0.0, 1.0],
1093        ]);
1094
1095        // Compute t1 * t2
1096        let result = t1 * t2;
1097
1098        // Expected result: t1 * t2 represents first rotating by t2, then translating by t1
1099        let expected = Transform3D::<f32>::from_matrix([
1100            [0.0, -1.0, 0.0, 7.0], // Rotation from t2 + combined translation
1101            [1.0, 0.0, 0.0, 9.0],
1102            [0.0, 0.0, 1.0, 11.0],
1103            [0.0, 0.0, 0.0, 1.0],
1104        ]);
1105
1106        // Check results
1107        let epsilon = 1e-5;
1108        let res_mat = result.to_matrix();
1109        let exp_mat = expected.to_matrix();
1110        assert_matrix_close(res_mat, exp_mat, epsilon);
1111    }
1112
1113    #[test]
1114    fn test_transform_multiplication_f64() {
1115        // Create two transforms to multiply
1116        let t1 = Transform3D::<f64>::from_matrix([
1117            [1.0, 0.0, 0.0, 2.0], // Identity rotation + translation (2,3,4)
1118            [0.0, 1.0, 0.0, 3.0],
1119            [0.0, 0.0, 1.0, 4.0],
1120            [0.0, 0.0, 0.0, 1.0],
1121        ]);
1122
1123        let t2 = Transform3D::<f64>::from_matrix([
1124            [0.0, -1.0, 0.0, 5.0], // 90-degree rotation around z + translation (5,6,7)
1125            [1.0, 0.0, 0.0, 6.0],
1126            [0.0, 0.0, 1.0, 7.0],
1127            [0.0, 0.0, 0.0, 1.0],
1128        ]);
1129
1130        // Compute t1 * t2
1131        let result = t1 * t2;
1132
1133        // Expected result
1134        let expected = Transform3D::<f64>::from_matrix([
1135            [0.0, -1.0, 0.0, 7.0], // Rotation from t2 + combined translation
1136            [1.0, 0.0, 0.0, 9.0],
1137            [0.0, 0.0, 1.0, 11.0],
1138            [0.0, 0.0, 0.0, 1.0],
1139        ]);
1140
1141        // Check results
1142        let epsilon = 1e-10;
1143        let res_mat = result.to_matrix();
1144        let exp_mat = expected.to_matrix();
1145        assert_matrix_close(res_mat, exp_mat, epsilon);
1146    }
1147
1148    #[test]
1149    fn test_transform_reference_multiplication() {
1150        // Test multiplication on references
1151        let t1 = Transform3D::<f32>::from_matrix([
1152            [1.0, 0.0, 0.0, 2.0],
1153            [0.0, 1.0, 0.0, 3.0],
1154            [0.0, 0.0, 1.0, 4.0],
1155            [0.0, 0.0, 0.0, 1.0],
1156        ]);
1157
1158        let t2 = Transform3D::<f32>::from_matrix([
1159            [0.0, -1.0, 0.0, 5.0],
1160            [1.0, 0.0, 0.0, 6.0],
1161            [0.0, 0.0, 1.0, 7.0],
1162            [0.0, 0.0, 0.0, 1.0],
1163        ]);
1164
1165        // Compute &t1 * &t2
1166        let result = t1 * t2;
1167
1168        // Expected result
1169        let expected = Transform3D::<f32>::from_matrix([
1170            [0.0, -1.0, 0.0, 7.0],
1171            [1.0, 0.0, 0.0, 9.0],
1172            [0.0, 0.0, 1.0, 11.0],
1173            [0.0, 0.0, 0.0, 1.0],
1174        ]);
1175
1176        // Check results
1177        let epsilon = 1e-5;
1178        let res_mat = result.to_matrix();
1179        let exp_mat = expected.to_matrix();
1180        assert_matrix_close(res_mat, exp_mat, epsilon);
1181    }
1182
1183    #[cfg(feature = "faer")]
1184    #[test]
1185    fn test_pose_faer_conversion() {
1186        use faer::prelude::*;
1187
1188        let pose = Transform3D::from_matrix([
1189            [1.0, 2.0, 3.0, 4.0],
1190            [5.0, 6.0, 7.0, 8.0],
1191            [9.0, 10.0, 11.0, 12.0],
1192            [13.0, 14.0, 15.0, 16.0],
1193        ]);
1194
1195        let mat: Mat<f64> = (&pose).into();
1196        let pose_from_mat = Transform3D::from(mat);
1197
1198        assert_eq!(
1199            pose.to_matrix(),
1200            pose_from_mat.to_matrix(),
1201            "Faer conversion should be lossless"
1202        );
1203    }
1204
1205    #[cfg(feature = "nalgebra")]
1206    #[test]
1207    fn test_pose_nalgebra_conversion() {
1208        use nalgebra::Isometry3;
1209
1210        let pose = Transform3D::from_matrix([
1211            [1.0, 0.0, 0.0, 2.0],
1212            [0.0, 1.0, 0.0, 3.0],
1213            [0.0, 0.0, 1.0, 4.0],
1214            [0.0, 0.0, 0.0, 1.0],
1215        ]);
1216
1217        let iso: Isometry3<f64> = (&pose.clone()).into();
1218        let pose_from_iso: Transform3D<f64> = iso.into();
1219
1220        assert_eq!(
1221            pose.to_matrix(),
1222            pose_from_iso.to_matrix(),
1223            "Nalgebra conversion should be lossless"
1224        );
1225    }
1226
1227    #[cfg(feature = "glam")]
1228    #[test]
1229    fn test_pose_glam_conversion() {
1230        use glam::DAffine3;
1231
1232        let pose = Transform3D::from_matrix([
1233            [1.0, 0.0, 0.0, 5.0],
1234            [0.0, 1.0, 0.0, 6.0],
1235            [0.0, 0.0, 1.0, 7.0],
1236            [0.0, 0.0, 0.0, 1.0],
1237        ]);
1238        let aff: DAffine3 = pose.into();
1239        assert_eq!(aff.translation[0], 5.0);
1240        assert_eq!(aff.translation[1], 6.0);
1241        assert_eq!(aff.translation[2], 7.0);
1242        let pose_from_aff: Transform3D<f64> = aff.into();
1243
1244        assert_eq!(
1245            pose.to_matrix(),
1246            pose_from_aff.to_matrix(),
1247            "Glam conversion should be lossless"
1248        );
1249    }
1250
1251    #[cfg(feature = "glam")]
1252    #[test]
1253    fn test_matrix_format_issue() {
1254        use glam::Mat4;
1255
1256        // Test case: row-major matrix with translation in last column
1257        let row_major = [
1258            [1.0, 0.0, 0.0, 5.0], // row 0: x-axis + x translation
1259            [0.0, 1.0, 0.0, 6.0], // row 1: y-axis + y translation
1260            [0.0, 0.0, 1.0, 7.0], // row 2: z-axis + z translation
1261            [0.0, 0.0, 0.0, 1.0], // row 3: homogeneous
1262        ];
1263
1264        // What glam expects: column-major format
1265        // Each inner array is a COLUMN, not a row
1266        let col_major = [
1267            [1.0, 0.0, 0.0, 0.0], // column 0: x-axis
1268            [0.0, 1.0, 0.0, 0.0], // column 1: y-axis
1269            [0.0, 0.0, 1.0, 0.0], // column 2: z-axis
1270            [5.0, 6.0, 7.0, 1.0], // column 3: translation + w
1271        ];
1272
1273        // Create matrices
1274        let mat_from_row = Mat4::from_cols_array_2d(&row_major);
1275        let mat_from_col = Mat4::from_cols_array_2d(&col_major);
1276
1277        // When using row-major data directly, translation ends up in wrong place
1278        assert_ne!(mat_from_row.w_axis.x, 5.0); // Translation is NOT where we expect
1279
1280        // When using column-major data, translation is correct
1281        assert_eq!(mat_from_col.w_axis.x, 5.0);
1282        assert_eq!(mat_from_col.w_axis.y, 6.0);
1283        assert_eq!(mat_from_col.w_axis.z, 7.0);
1284
1285        // The fix: transpose the row-major matrix
1286        let mat_transposed = Mat4::from_cols_array_2d(&row_major).transpose();
1287        assert_eq!(mat_transposed.w_axis.x, 5.0);
1288        assert_eq!(mat_transposed.w_axis.y, 6.0);
1289        assert_eq!(mat_transposed.w_axis.z, 7.0);
1290    }
1291
1292    /// 90 degrees around z plus a (2, 3, 4) translation, in row-major layout
1293    /// (the canonical layout for both the glam and non-glam backends).
1294    fn quarter_turn_and_shift() -> Transform3D<f32> {
1295        Transform3D::from_matrix([
1296            [0.0, -1.0, 0.0, 2.0],
1297            [1.0, 0.0, 0.0, 3.0],
1298            [0.0, 0.0, 1.0, 4.0],
1299            [0.0, 0.0, 0.0, 1.0],
1300        ])
1301    }
1302
1303    fn assert_point_close(lhs: Point3f, rhs: Point3f, eps: f32) {
1304        assert!(
1305            (lhs.x - rhs.x).raw().abs() <= eps
1306                && (lhs.y - rhs.y).raw().abs() <= eps
1307                && (lhs.z - rhs.z).raw().abs() <= eps,
1308            "{lhs:?} differs from {rhs:?}"
1309        );
1310    }
1311
1312    #[test]
1313    fn transform_point_rotates_and_translates() {
1314        let t = quarter_turn_and_shift();
1315        let p = Point3f::from_meters(1.0, 0.0, 0.0);
1316        assert_point_close(
1317            t.transform_point(p),
1318            Point3f::from_meters(2.0, 4.0, 4.0),
1319            1e-5,
1320        );
1321        assert_point_close(
1322            t.transform_vector(p),
1323            Point3f::from_meters(0.0, 1.0, 0.0),
1324            1e-5,
1325        );
1326        assert_point_close(t.position(), Point3f::from_meters(2.0, 3.0, 4.0), 1e-5);
1327    }
1328
1329    #[test]
1330    fn transform_accessors_are_backend_independent() {
1331        let t = quarter_turn_and_shift();
1332
1333        assert_eq!(
1334            t.translation(),
1335            [
1336                Length32::new::<meter>(2.0),
1337                Length32::new::<meter>(3.0),
1338                Length32::new::<meter>(4.0),
1339            ]
1340        );
1341        assert_eq!(
1342            t.rotation(),
1343            [[0.0, -1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]]
1344        );
1345    }
1346
1347    #[test]
1348    fn from_matrix_is_row_major_and_round_trips() {
1349        // The canonical layout is row-major: translation lives in the last
1350        // column, matching the non-glam backend and the doc examples. The glam
1351        // backend must transpose at its boundary so both feature configs agree.
1352        let mat = [
1353            [0.0, -1.0, 0.0, 2.0],
1354            [1.0, 0.0, 0.0, 3.0],
1355            [0.0, 0.0, 1.0, 4.0],
1356            [0.0, 0.0, 0.0, 1.0],
1357        ];
1358        let t = Transform3D::<f32>::from_matrix(mat);
1359
1360        // Lossless round-trip in the canonical layout.
1361        assert_eq!(t.to_matrix(), mat);
1362
1363        // Accessors agree with the matrix.
1364        assert_eq!(
1365            t.translation(),
1366            [
1367                Length32::new::<meter>(2.0),
1368                Length32::new::<meter>(3.0),
1369                Length32::new::<meter>(4.0),
1370            ]
1371        );
1372        assert_eq!(
1373            t.rotation(),
1374            [[0.0, -1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]]
1375        );
1376    }
1377
1378    #[test]
1379    fn transform_point_identity_is_a_fixed_point() {
1380        let identity = Transform3D::<f32>::from_matrix([
1381            [1.0, 0.0, 0.0, 0.0],
1382            [0.0, 1.0, 0.0, 0.0],
1383            [0.0, 0.0, 1.0, 0.0],
1384            [0.0, 0.0, 0.0, 1.0],
1385        ]);
1386        let p = Point3f::from_meters(1.5, -2.0, 0.25);
1387        assert_eq!(identity.transform_point(p), p);
1388        assert_eq!(identity.transform_vector(p), p);
1389        assert_eq!(identity.position(), Point3f::default());
1390    }
1391
1392    #[test]
1393    fn transform_points_matches_transform_point() {
1394        let t = quarter_turn_and_shift();
1395        let points = [
1396            Point3f::from_meters(1.0, 0.0, 0.0),
1397            Point3f::from_meters(-1.5, 2.0, 0.25),
1398            Point3f::from_meters(0.0, 0.0, 0.0),
1399        ];
1400        let mut set = Point3fSoa::<4>::default();
1401        for p in points {
1402            set.push(p);
1403        }
1404
1405        t.transform_points(&mut set);
1406        for (i, p) in points.iter().enumerate() {
1407            assert_point_close(set.get(i), t.transform_point(*p), 1e-5);
1408        }
1409    }
1410
1411    #[test]
1412    fn geodetic_position_converts_to_and_from_degrees() {
1413        let position = GeodeticPosition::from_degrees(59.319_221, 18.075_631);
1414
1415        assert_eq!(position.latitude.get::<degree>(), 59.319_221);
1416        assert_eq!(position.longitude.get::<degree>(), 18.075_631);
1417        assert_eq!(position.latitude_degrees(), 59.319_221);
1418        assert_eq!(position.longitude_degrees(), 18.075_631);
1419    }
1420}