Skip to main content

goud_engine/ecs/components/
global_transform.rs

1//! GlobalTransform component for world-space transformations.
2//!
3//! The [`GlobalTransform`] component stores the computed world-space transformation
4//! for entities in a hierarchy. Unlike [`Transform`] which stores local-space data
5//! relative to the parent, `GlobalTransform` stores the absolute world-space result.
6//!
7//! # Purpose
8//!
9//! When entities are arranged in a parent-child hierarchy, each child's [`Transform`]
10//! is relative to its parent. To render, perform physics, or do other world-space
11//! operations, we need the final world-space transformation.
12//!
13//! For example:
14//! - Parent at position (10, 0, 0)
15//! - Child at local position (5, 0, 0)
16//! - Child's world position is (15, 0, 0)
17//!
18//! The transform propagation system computes these world-space values and stores
19//! them in `GlobalTransform`.
20//!
21//! # Usage
22//!
23//! `GlobalTransform` is typically:
24//! 1. Added automatically when spawning entities with `Transform`
25//! 2. Updated by the transform propagation system each frame
26//! 3. Read by rendering systems, physics, etc.
27//!
28//! **Never modify `GlobalTransform` directly.** Always modify `Transform` and let
29//! the propagation system compute the global value.
30//!
31//! ```
32//! use goud_engine::ecs::components::{Transform, GlobalTransform};
33//! use goud_engine::core::math::Vec3;
34//!
35//! // Create local transform
36//! let local = Transform::from_position(Vec3::new(5.0, 0.0, 0.0));
37//!
38//! // GlobalTransform would be computed by the propagation system
39//! // For a root entity, it equals the local transform
40//! let global = GlobalTransform::from(local);
41//!
42//! assert_eq!(global.translation(), Vec3::new(5.0, 0.0, 0.0));
43//! ```
44//!
45//! # Memory Layout
46//!
47//! GlobalTransform stores a pre-computed 4x4 affine transformation matrix (64 bytes).
48//! While this uses more memory than Transform (40 bytes), it provides:
49//!
50//! - **Direct use**: Matrix can be sent to GPU without further computation
51//! - **Composability**: Easy to combine with parent transforms
52//! - **Decomposability**: Can extract position/rotation/scale when needed
53//!
54//! # FFI Safety
55//!
56//! GlobalTransform uses cgmath's `Matrix4<f32>` internally which is column-major.
57//! For FFI, use the `to_cols_array` method to get a flat `[f32; 16]` array.
58
59use crate::core::math::{Matrix4, Vec3};
60use crate::ecs::components::transform::{Quat, Transform};
61use crate::ecs::Component;
62use cgmath::{InnerSpace, SquareMatrix};
63use std::fmt;
64
65/// A world-space transformation component.
66///
67/// This component caches the computed world-space transformation matrix for
68/// entities in a hierarchy. It is computed by the transform propagation system
69/// based on the entity's local [`Transform`] and its parent's `GlobalTransform`.
70///
71/// # When to Use
72///
73/// - Use `Transform` for setting local position/rotation/scale
74/// - Use `GlobalTransform` for reading world-space values (rendering, physics)
75///
76/// # Do Not Modify Directly
77///
78/// This component is managed by the transform propagation system. Modifying it
79/// directly will cause desynchronization with the entity hierarchy.
80///
81/// # Example
82///
83/// ```
84/// use goud_engine::ecs::components::{Transform, GlobalTransform};
85/// use goud_engine::core::math::Vec3;
86///
87/// // For root entities, global equals local
88/// let transform = Transform::from_position(Vec3::new(10.0, 5.0, 0.0));
89/// let global = GlobalTransform::from(transform);
90///
91/// let position = global.translation();
92/// assert!((position - Vec3::new(10.0, 5.0, 0.0)).length() < 0.001);
93/// ```
94#[derive(Clone, Copy, PartialEq)]
95pub struct GlobalTransform {
96    /// The computed world-space transformation matrix.
97    ///
98    /// This is a column-major 4x4 affine transformation matrix.
99    matrix: Matrix4<f32>,
100}
101
102impl GlobalTransform {
103    /// Identity global transform (no transformation).
104    pub const IDENTITY: GlobalTransform = GlobalTransform {
105        matrix: Matrix4::new(
106            1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
107        ),
108    };
109
110    /// Creates a GlobalTransform from a 4x4 transformation matrix.
111    ///
112    /// # Arguments
113    ///
114    /// * `matrix` - The world-space transformation matrix
115    ///
116    /// # Example
117    ///
118    /// ```
119    /// use goud_engine::ecs::components::GlobalTransform;
120    /// use cgmath::Matrix4;
121    ///
122    /// let matrix = Matrix4::from_translation(cgmath::Vector3::new(10.0, 0.0, 0.0));
123    /// let global = GlobalTransform::from_matrix(matrix);
124    /// ```
125    #[inline]
126    pub const fn from_matrix(matrix: Matrix4<f32>) -> Self {
127        Self { matrix }
128    }
129
130    /// Creates a GlobalTransform from translation only.
131    ///
132    /// # Arguments
133    ///
134    /// * `translation` - World-space position
135    ///
136    /// # Example
137    ///
138    /// ```
139    /// use goud_engine::ecs::components::GlobalTransform;
140    /// use goud_engine::core::math::Vec3;
141    ///
142    /// let global = GlobalTransform::from_translation(Vec3::new(10.0, 5.0, 0.0));
143    /// ```
144    #[inline]
145    pub fn from_translation(translation: Vec3) -> Self {
146        let matrix = Matrix4::from_translation(cgmath::Vector3::new(
147            translation.x,
148            translation.y,
149            translation.z,
150        ));
151        Self { matrix }
152    }
153
154    /// Creates a GlobalTransform from translation, rotation, and scale.
155    ///
156    /// # Arguments
157    ///
158    /// * `translation` - World-space position
159    /// * `rotation` - World-space rotation as quaternion
160    /// * `scale` - World-space scale
161    ///
162    /// # Example
163    ///
164    /// ```
165    /// use goud_engine::ecs::components::GlobalTransform;
166    /// use goud_engine::ecs::components::transform::Quat;
167    /// use goud_engine::core::math::Vec3;
168    ///
169    /// let global = GlobalTransform::from_translation_rotation_scale(
170    ///     Vec3::new(10.0, 0.0, 0.0),
171    ///     Quat::IDENTITY,
172    ///     Vec3::one(),
173    /// );
174    /// ```
175    #[inline]
176    pub fn from_translation_rotation_scale(translation: Vec3, rotation: Quat, scale: Vec3) -> Self {
177        // Build transform matrix: T * R * S
178        let transform = Transform::new(translation, rotation, scale);
179        Self {
180            matrix: transform.matrix(),
181        }
182    }
183
184    /// Returns the underlying 4x4 transformation matrix.
185    ///
186    /// This matrix is column-major and can be used directly for rendering.
187    ///
188    /// # Example
189    ///
190    /// ```
191    /// use goud_engine::ecs::components::GlobalTransform;
192    ///
193    /// let global = GlobalTransform::IDENTITY;
194    /// let matrix = global.matrix();
195    /// ```
196    #[inline]
197    pub fn matrix(&self) -> Matrix4<f32> {
198        self.matrix
199    }
200
201    /// Returns a reference to the underlying matrix.
202    #[inline]
203    pub fn matrix_ref(&self) -> &Matrix4<f32> {
204        &self.matrix
205    }
206
207    /// Returns the matrix as a flat column-major array.
208    ///
209    /// This is useful for FFI and sending to GPU shaders.
210    ///
211    /// # Example
212    ///
213    /// ```
214    /// use goud_engine::ecs::components::GlobalTransform;
215    ///
216    /// let global = GlobalTransform::IDENTITY;
217    /// let cols: [f32; 16] = global.to_cols_array();
218    ///
219    /// // First column (x-axis)
220    /// assert_eq!(cols[0], 1.0); // m00
221    /// ```
222    #[inline]
223    pub fn to_cols_array(&self) -> [f32; 16] {
224        [
225            self.matrix.x.x,
226            self.matrix.x.y,
227            self.matrix.x.z,
228            self.matrix.x.w,
229            self.matrix.y.x,
230            self.matrix.y.y,
231            self.matrix.y.z,
232            self.matrix.y.w,
233            self.matrix.z.x,
234            self.matrix.z.y,
235            self.matrix.z.z,
236            self.matrix.z.w,
237            self.matrix.w.x,
238            self.matrix.w.y,
239            self.matrix.w.z,
240            self.matrix.w.w,
241        ]
242    }
243
244    /// Returns the matrix as a flat row-major array.
245    ///
246    /// Some APIs expect row-major ordering.
247    #[inline]
248    pub fn to_rows_array(&self) -> [f32; 16] {
249        [
250            self.matrix.x.x,
251            self.matrix.y.x,
252            self.matrix.z.x,
253            self.matrix.w.x,
254            self.matrix.x.y,
255            self.matrix.y.y,
256            self.matrix.z.y,
257            self.matrix.w.y,
258            self.matrix.x.z,
259            self.matrix.y.z,
260            self.matrix.z.z,
261            self.matrix.w.z,
262            self.matrix.x.w,
263            self.matrix.y.w,
264            self.matrix.z.w,
265            self.matrix.w.w,
266        ]
267    }
268
269    // =========================================================================
270    // Decomposition Methods
271    // =========================================================================
272
273    /// Extracts the translation (position) component.
274    ///
275    /// This is the fourth column of the matrix.
276    ///
277    /// # Example
278    ///
279    /// ```
280    /// use goud_engine::ecs::components::GlobalTransform;
281    /// use goud_engine::core::math::Vec3;
282    ///
283    /// let global = GlobalTransform::from_translation(Vec3::new(10.0, 5.0, 3.0));
284    /// let pos = global.translation();
285    ///
286    /// assert!((pos.x - 10.0).abs() < 0.001);
287    /// ```
288    #[inline]
289    pub fn translation(&self) -> Vec3 {
290        Vec3::new(self.matrix.w.x, self.matrix.w.y, self.matrix.w.z)
291    }
292
293    /// Extracts the scale component.
294    ///
295    /// This is computed from the lengths of the first three column vectors.
296    /// Note: This does not handle negative scales correctly.
297    ///
298    /// # Example
299    ///
300    /// ```
301    /// use goud_engine::ecs::components::GlobalTransform;
302    /// use goud_engine::ecs::components::transform::Quat;
303    /// use goud_engine::core::math::Vec3;
304    ///
305    /// let global = GlobalTransform::from_translation_rotation_scale(
306    ///     Vec3::zero(),
307    ///     Quat::IDENTITY,
308    ///     Vec3::new(2.0, 3.0, 4.0),
309    /// );
310    ///
311    /// let scale = global.scale();
312    /// assert!((scale.x - 2.0).abs() < 0.001);
313    /// ```
314    #[inline]
315    pub fn scale(&self) -> Vec3 {
316        let scale_x =
317            cgmath::Vector3::new(self.matrix.x.x, self.matrix.x.y, self.matrix.x.z).magnitude();
318        let scale_y =
319            cgmath::Vector3::new(self.matrix.y.x, self.matrix.y.y, self.matrix.y.z).magnitude();
320        let scale_z =
321            cgmath::Vector3::new(self.matrix.z.x, self.matrix.z.y, self.matrix.z.z).magnitude();
322        Vec3::new(scale_x, scale_y, scale_z)
323    }
324
325    /// Extracts the rotation component as a quaternion.
326    ///
327    /// This removes scale from the rotation matrix, then converts to quaternion.
328    /// Note: This may have issues with non-uniform scales or negative scales.
329    ///
330    /// # Example
331    ///
332    /// ```
333    /// use goud_engine::ecs::components::GlobalTransform;
334    /// use goud_engine::ecs::components::transform::Quat;
335    /// use goud_engine::core::math::Vec3;
336    /// use std::f32::consts::PI;
337    ///
338    /// let rotation = Quat::from_axis_angle(Vec3::unit_y(), PI / 4.0);
339    /// let global = GlobalTransform::from_translation_rotation_scale(
340    ///     Vec3::zero(),
341    ///     rotation,
342    ///     Vec3::one(),
343    /// );
344    ///
345    /// let extracted = global.rotation();
346    /// // Quaternion comparison (accounting for sign flip equivalence)
347    /// let dot = rotation.x * extracted.x + rotation.y * extracted.y
348    ///         + rotation.z * extracted.z + rotation.w * extracted.w;
349    /// assert!(dot.abs() > 0.999);
350    /// ```
351    #[inline]
352    pub fn rotation(&self) -> Quat {
353        // Extract scale
354        let scale = self.scale();
355
356        // Build rotation matrix by normalizing columns
357        let m00 = if scale.x != 0.0 {
358            self.matrix.x.x / scale.x
359        } else {
360            1.0
361        };
362        let m01 = if scale.y != 0.0 {
363            self.matrix.y.x / scale.y
364        } else {
365            0.0
366        };
367        let m02 = if scale.z != 0.0 {
368            self.matrix.z.x / scale.z
369        } else {
370            0.0
371        };
372
373        let m10 = if scale.x != 0.0 {
374            self.matrix.x.y / scale.x
375        } else {
376            0.0
377        };
378        let m11 = if scale.y != 0.0 {
379            self.matrix.y.y / scale.y
380        } else {
381            1.0
382        };
383        let m12 = if scale.z != 0.0 {
384            self.matrix.z.y / scale.z
385        } else {
386            0.0
387        };
388
389        let m20 = if scale.x != 0.0 {
390            self.matrix.x.z / scale.x
391        } else {
392            0.0
393        };
394        let m21 = if scale.y != 0.0 {
395            self.matrix.y.z / scale.y
396        } else {
397            0.0
398        };
399        let m22 = if scale.z != 0.0 {
400            self.matrix.z.z / scale.z
401        } else {
402            1.0
403        };
404
405        // Convert rotation matrix to quaternion
406        let trace = m00 + m11 + m22;
407        if trace > 0.0 {
408            let s = (trace + 1.0).sqrt() * 2.0;
409            Quat::new((m21 - m12) / s, (m02 - m20) / s, (m10 - m01) / s, 0.25 * s).normalize()
410        } else if m00 > m11 && m00 > m22 {
411            let s = (1.0 + m00 - m11 - m22).sqrt() * 2.0;
412            Quat::new(0.25 * s, (m01 + m10) / s, (m02 + m20) / s, (m21 - m12) / s).normalize()
413        } else if m11 > m22 {
414            let s = (1.0 + m11 - m00 - m22).sqrt() * 2.0;
415            Quat::new((m01 + m10) / s, 0.25 * s, (m12 + m21) / s, (m02 - m20) / s).normalize()
416        } else {
417            let s = (1.0 + m22 - m00 - m11).sqrt() * 2.0;
418            Quat::new((m02 + m20) / s, (m12 + m21) / s, 0.25 * s, (m10 - m01) / s).normalize()
419        }
420    }
421
422    /// Decomposes the transform into translation, rotation, and scale.
423    ///
424    /// Returns `(translation, rotation, scale)`.
425    ///
426    /// # Example
427    ///
428    /// ```
429    /// use goud_engine::ecs::components::GlobalTransform;
430    /// use goud_engine::ecs::components::transform::Quat;
431    /// use goud_engine::core::math::Vec3;
432    ///
433    /// let global = GlobalTransform::from_translation_rotation_scale(
434    ///     Vec3::new(10.0, 5.0, 3.0),
435    ///     Quat::IDENTITY,
436    ///     Vec3::new(2.0, 2.0, 2.0),
437    /// );
438    ///
439    /// let (translation, rotation, scale) = global.decompose();
440    /// assert!((translation.x - 10.0).abs() < 0.001);
441    /// assert!((scale.x - 2.0).abs() < 0.001);
442    /// ```
443    #[inline]
444    pub fn decompose(&self) -> (Vec3, Quat, Vec3) {
445        (self.translation(), self.rotation(), self.scale())
446    }
447
448    /// Converts this GlobalTransform to a local Transform.
449    ///
450    /// This is useful for extracting a Transform that would produce this
451    /// GlobalTransform when applied from the origin.
452    #[inline]
453    pub fn to_transform(&self) -> Transform {
454        let (translation, rotation, scale) = self.decompose();
455        Transform::new(translation, rotation, scale)
456    }
457
458    // =========================================================================
459    // Transform Operations
460    // =========================================================================
461
462    /// Multiplies this transform with another.
463    ///
464    /// This combines two transformations: `self * other` applies `self` first,
465    /// then `other`.
466    ///
467    /// # Example
468    ///
469    /// ```
470    /// use goud_engine::ecs::components::GlobalTransform;
471    /// use goud_engine::core::math::Vec3;
472    ///
473    /// let parent = GlobalTransform::from_translation(Vec3::new(10.0, 0.0, 0.0));
474    /// let child_local = GlobalTransform::from_translation(Vec3::new(5.0, 0.0, 0.0));
475    ///
476    /// let child_global = parent.mul_transform(&child_local);
477    /// let pos = child_global.translation();
478    /// assert!((pos.x - 15.0).abs() < 0.001);
479    /// ```
480    #[inline]
481    pub fn mul_transform(&self, other: &GlobalTransform) -> GlobalTransform {
482        GlobalTransform {
483            matrix: self.matrix * other.matrix,
484        }
485    }
486
487    /// Multiplies this transform by a local Transform.
488    ///
489    /// This is the primary method used by transform propagation.
490    ///
491    /// # Example
492    ///
493    /// ```
494    /// use goud_engine::ecs::components::{GlobalTransform, Transform};
495    /// use goud_engine::core::math::Vec3;
496    ///
497    /// let parent_global = GlobalTransform::from_translation(Vec3::new(10.0, 0.0, 0.0));
498    /// let child_local = Transform::from_position(Vec3::new(5.0, 0.0, 0.0));
499    ///
500    /// let child_global = parent_global.transform_by(&child_local);
501    /// let pos = child_global.translation();
502    /// assert!((pos.x - 15.0).abs() < 0.001);
503    /// ```
504    #[inline]
505    pub fn transform_by(&self, local: &Transform) -> GlobalTransform {
506        GlobalTransform {
507            matrix: self.matrix * local.matrix(),
508        }
509    }
510
511    /// Transforms a point from local space to world space.
512    ///
513    /// # Example
514    ///
515    /// ```
516    /// use goud_engine::ecs::components::GlobalTransform;
517    /// use goud_engine::core::math::Vec3;
518    ///
519    /// let global = GlobalTransform::from_translation(Vec3::new(10.0, 0.0, 0.0));
520    /// let local_point = Vec3::new(5.0, 0.0, 0.0);
521    /// let world_point = global.transform_point(local_point);
522    ///
523    /// assert!((world_point.x - 15.0).abs() < 0.001);
524    /// ```
525    #[inline]
526    pub fn transform_point(&self, point: Vec3) -> Vec3 {
527        let p = cgmath::Vector4::new(point.x, point.y, point.z, 1.0);
528        let result = self.matrix * p;
529        Vec3::new(result.x, result.y, result.z)
530    }
531
532    /// Transforms a direction from local space to world space.
533    ///
534    /// Unlike points, directions are not affected by translation.
535    #[inline]
536    pub fn transform_direction(&self, direction: Vec3) -> Vec3 {
537        let d = cgmath::Vector4::new(direction.x, direction.y, direction.z, 0.0);
538        let result = self.matrix * d;
539        Vec3::new(result.x, result.y, result.z)
540    }
541
542    /// Returns the inverse of this transform.
543    ///
544    /// The inverse transforms from world space back to local space.
545    /// Returns `None` if the matrix is not invertible (e.g., has zero scale).
546    #[inline]
547    pub fn inverse(&self) -> Option<GlobalTransform> {
548        self.matrix.invert().map(|m| GlobalTransform { matrix: m })
549    }
550
551    // =========================================================================
552    // Direction Vectors
553    // =========================================================================
554
555    /// Returns the forward direction vector (negative Z in local space).
556    #[inline]
557    pub fn forward(&self) -> Vec3 {
558        self.transform_direction(Vec3::new(0.0, 0.0, -1.0))
559            .normalize()
560    }
561
562    /// Returns the right direction vector (positive X in local space).
563    #[inline]
564    pub fn right(&self) -> Vec3 {
565        self.transform_direction(Vec3::new(1.0, 0.0, 0.0))
566            .normalize()
567    }
568
569    /// Returns the up direction vector (positive Y in local space).
570    #[inline]
571    pub fn up(&self) -> Vec3 {
572        self.transform_direction(Vec3::new(0.0, 1.0, 0.0))
573            .normalize()
574    }
575
576    /// Returns the back direction vector (positive Z in local space).
577    #[inline]
578    pub fn back(&self) -> Vec3 {
579        self.transform_direction(Vec3::new(0.0, 0.0, 1.0))
580            .normalize()
581    }
582
583    /// Returns the left direction vector (negative X in local space).
584    #[inline]
585    pub fn left(&self) -> Vec3 {
586        self.transform_direction(Vec3::new(-1.0, 0.0, 0.0))
587            .normalize()
588    }
589
590    /// Returns the down direction vector (negative Y in local space).
591    #[inline]
592    pub fn down(&self) -> Vec3 {
593        self.transform_direction(Vec3::new(0.0, -1.0, 0.0))
594            .normalize()
595    }
596
597    // =========================================================================
598    // Interpolation
599    // =========================================================================
600
601    /// Linearly interpolates between two global transforms.
602    ///
603    /// This decomposes both transforms, interpolates components separately
604    /// (slerp for rotation), then recomposes.
605    ///
606    /// # Example
607    ///
608    /// ```
609    /// use goud_engine::ecs::components::GlobalTransform;
610    /// use goud_engine::core::math::Vec3;
611    ///
612    /// let a = GlobalTransform::from_translation(Vec3::zero());
613    /// let b = GlobalTransform::from_translation(Vec3::new(10.0, 0.0, 0.0));
614    ///
615    /// let mid = a.lerp(&b, 0.5);
616    /// let pos = mid.translation();
617    /// assert!((pos.x - 5.0).abs() < 0.001);
618    /// ```
619    #[inline]
620    pub fn lerp(&self, other: &GlobalTransform, t: f32) -> GlobalTransform {
621        let (t1, r1, s1) = self.decompose();
622        let (t2, r2, s2) = other.decompose();
623
624        GlobalTransform::from_translation_rotation_scale(
625            t1.lerp(t2, t),
626            r1.slerp(r2, t),
627            s1.lerp(s2, t),
628        )
629    }
630}
631
632impl Default for GlobalTransform {
633    #[inline]
634    fn default() -> Self {
635        Self::IDENTITY
636    }
637}
638
639impl fmt::Debug for GlobalTransform {
640    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
641        let (t, r, s) = self.decompose();
642        f.debug_struct("GlobalTransform")
643            .field("translation", &(t.x, t.y, t.z))
644            .field(
645                "rotation",
646                &format!("Quat({:.3}, {:.3}, {:.3}, {:.3})", r.x, r.y, r.z, r.w),
647            )
648            .field("scale", &(s.x, s.y, s.z))
649            .finish()
650    }
651}
652
653impl From<Transform> for GlobalTransform {
654    /// Converts a local Transform to a GlobalTransform.
655    ///
656    /// This is used for root entities where local == global.
657    #[inline]
658    fn from(transform: Transform) -> Self {
659        GlobalTransform {
660            matrix: transform.matrix(),
661        }
662    }
663}
664
665impl From<&Transform> for GlobalTransform {
666    #[inline]
667    fn from(transform: &Transform) -> Self {
668        GlobalTransform {
669            matrix: transform.matrix(),
670        }
671    }
672}
673
674impl From<Matrix4<f32>> for GlobalTransform {
675    #[inline]
676    fn from(matrix: Matrix4<f32>) -> Self {
677        GlobalTransform { matrix }
678    }
679}
680
681// Implement Component trait
682impl Component for GlobalTransform {}
683
684// Implement multiplication operators
685impl std::ops::Mul for GlobalTransform {
686    type Output = GlobalTransform;
687
688    #[inline]
689    fn mul(self, rhs: GlobalTransform) -> GlobalTransform {
690        self.mul_transform(&rhs)
691    }
692}
693
694impl std::ops::Mul<&GlobalTransform> for GlobalTransform {
695    type Output = GlobalTransform;
696
697    #[inline]
698    fn mul(self, rhs: &GlobalTransform) -> GlobalTransform {
699        self.mul_transform(rhs)
700    }
701}
702
703impl std::ops::Mul<Transform> for GlobalTransform {
704    type Output = GlobalTransform;
705
706    #[inline]
707    fn mul(self, rhs: Transform) -> GlobalTransform {
708        self.transform_by(&rhs)
709    }
710}
711
712impl std::ops::Mul<&Transform> for GlobalTransform {
713    type Output = GlobalTransform;
714
715    #[inline]
716    fn mul(self, rhs: &Transform) -> GlobalTransform {
717        self.transform_by(rhs)
718    }
719}
720
721// =============================================================================
722// Unit Tests
723// =============================================================================
724
725#[cfg(test)]
726mod tests {
727    use super::*;
728    use std::f32::consts::FRAC_PI_2;
729    use std::f32::consts::FRAC_PI_4;
730
731    mod construction_tests {
732        use super::*;
733
734        #[test]
735        fn test_identity() {
736            let global = GlobalTransform::IDENTITY;
737            let pos = global.translation();
738            let scale = global.scale();
739
740            assert!((pos.x).abs() < 0.0001);
741            assert!((pos.y).abs() < 0.0001);
742            assert!((pos.z).abs() < 0.0001);
743            assert!((scale.x - 1.0).abs() < 0.0001);
744            assert!((scale.y - 1.0).abs() < 0.0001);
745            assert!((scale.z - 1.0).abs() < 0.0001);
746        }
747
748        #[test]
749        fn test_default() {
750            let global: GlobalTransform = Default::default();
751            assert_eq!(global, GlobalTransform::IDENTITY);
752        }
753
754        #[test]
755        fn test_from_translation() {
756            let global = GlobalTransform::from_translation(Vec3::new(10.0, 20.0, 30.0));
757            let pos = global.translation();
758
759            assert!((pos.x - 10.0).abs() < 0.0001);
760            assert!((pos.y - 20.0).abs() < 0.0001);
761            assert!((pos.z - 30.0).abs() < 0.0001);
762        }
763
764        #[test]
765        fn test_from_translation_rotation_scale() {
766            let rotation = Quat::from_axis_angle(Vec3::unit_y(), FRAC_PI_4);
767            let global = GlobalTransform::from_translation_rotation_scale(
768                Vec3::new(5.0, 10.0, 15.0),
769                rotation,
770                Vec3::new(2.0, 3.0, 4.0),
771            );
772
773            let pos = global.translation();
774            let scale = global.scale();
775
776            assert!((pos.x - 5.0).abs() < 0.0001);
777            assert!((pos.y - 10.0).abs() < 0.0001);
778            assert!((pos.z - 15.0).abs() < 0.0001);
779            assert!((scale.x - 2.0).abs() < 0.0001);
780            assert!((scale.y - 3.0).abs() < 0.0001);
781            assert!((scale.z - 4.0).abs() < 0.0001);
782        }
783
784        #[test]
785        fn test_from_transform() {
786            let transform = Transform::new(
787                Vec3::new(1.0, 2.0, 3.0),
788                Quat::from_axis_angle(Vec3::unit_y(), FRAC_PI_4),
789                Vec3::new(2.0, 2.0, 2.0),
790            );
791            let global: GlobalTransform = transform.into();
792
793            let pos = global.translation();
794            assert!((pos.x - 1.0).abs() < 0.0001);
795            assert!((pos.y - 2.0).abs() < 0.0001);
796            assert!((pos.z - 3.0).abs() < 0.0001);
797        }
798
799        #[test]
800        fn test_from_transform_ref() {
801            let transform = Transform::from_position(Vec3::new(5.0, 0.0, 0.0));
802            let global: GlobalTransform = (&transform).into();
803            let pos = global.translation();
804            assert!((pos.x - 5.0).abs() < 0.0001);
805        }
806    }
807
808    mod decomposition_tests {
809        use super::*;
810
811        #[test]
812        fn test_translation_extraction() {
813            let global = GlobalTransform::from_translation(Vec3::new(1.0, 2.0, 3.0));
814            let pos = global.translation();
815            assert!((pos.x - 1.0).abs() < 0.0001);
816            assert!((pos.y - 2.0).abs() < 0.0001);
817            assert!((pos.z - 3.0).abs() < 0.0001);
818        }
819
820        #[test]
821        fn test_scale_extraction() {
822            let global = GlobalTransform::from_translation_rotation_scale(
823                Vec3::zero(),
824                Quat::IDENTITY,
825                Vec3::new(2.0, 3.0, 4.0),
826            );
827            let scale = global.scale();
828            assert!((scale.x - 2.0).abs() < 0.0001);
829            assert!((scale.y - 3.0).abs() < 0.0001);
830            assert!((scale.z - 4.0).abs() < 0.0001);
831        }
832
833        #[test]
834        fn test_rotation_extraction() {
835            let original = Quat::from_axis_angle(Vec3::unit_y(), FRAC_PI_4);
836            let global = GlobalTransform::from_translation_rotation_scale(
837                Vec3::zero(),
838                original,
839                Vec3::one(),
840            );
841            let extracted = global.rotation();
842
843            // Compare quaternions (accounting for sign flip)
844            let dot = original.x * extracted.x
845                + original.y * extracted.y
846                + original.z * extracted.z
847                + original.w * extracted.w;
848            assert!(dot.abs() > 0.999);
849        }
850
851        #[test]
852        fn test_decompose() {
853            let original_t = Vec3::new(10.0, 5.0, 3.0);
854            let original_r = Quat::from_axis_angle(Vec3::unit_x(), FRAC_PI_4);
855            let original_s = Vec3::new(2.0, 3.0, 4.0);
856
857            let global = GlobalTransform::from_translation_rotation_scale(
858                original_t, original_r, original_s,
859            );
860            let (t, r, s) = global.decompose();
861
862            assert!((t - original_t).length() < 0.001);
863            assert!((s - original_s).length() < 0.001);
864
865            let dot =
866                original_r.x * r.x + original_r.y * r.y + original_r.z * r.z + original_r.w * r.w;
867            assert!(dot.abs() > 0.999);
868        }
869
870        #[test]
871        fn test_to_transform() {
872            let global = GlobalTransform::from_translation_rotation_scale(
873                Vec3::new(5.0, 10.0, 15.0),
874                Quat::IDENTITY,
875                Vec3::new(2.0, 2.0, 2.0),
876            );
877
878            let transform = global.to_transform();
879            assert!((transform.position - Vec3::new(5.0, 10.0, 15.0)).length() < 0.001);
880            assert!((transform.scale - Vec3::new(2.0, 2.0, 2.0)).length() < 0.001);
881        }
882    }
883
884    mod transform_tests {
885        use super::*;
886
887        #[test]
888        fn test_mul_transform_translation() {
889            let a = GlobalTransform::from_translation(Vec3::new(10.0, 0.0, 0.0));
890            let b = GlobalTransform::from_translation(Vec3::new(5.0, 0.0, 0.0));
891            let result = a.mul_transform(&b);
892
893            let pos = result.translation();
894            assert!((pos.x - 15.0).abs() < 0.0001);
895        }
896
897        #[test]
898        fn test_mul_transform_scale() {
899            let a = GlobalTransform::from_translation_rotation_scale(
900                Vec3::zero(),
901                Quat::IDENTITY,
902                Vec3::new(2.0, 2.0, 2.0),
903            );
904            let b = GlobalTransform::from_translation(Vec3::new(5.0, 0.0, 0.0));
905            let result = a.mul_transform(&b);
906
907            let pos = result.translation();
908            // Scale affects the child's translation
909            assert!((pos.x - 10.0).abs() < 0.0001);
910        }
911
912        #[test]
913        fn test_transform_by() {
914            let parent = GlobalTransform::from_translation(Vec3::new(10.0, 0.0, 0.0));
915            let child = Transform::from_position(Vec3::new(5.0, 0.0, 0.0));
916            let result = parent.transform_by(&child);
917
918            let pos = result.translation();
919            assert!((pos.x - 15.0).abs() < 0.0001);
920        }
921
922        #[test]
923        fn test_transform_point() {
924            let global = GlobalTransform::from_translation(Vec3::new(10.0, 0.0, 0.0));
925            let local_point = Vec3::new(5.0, 3.0, 0.0);
926            let world_point = global.transform_point(local_point);
927
928            assert!((world_point.x - 15.0).abs() < 0.0001);
929            assert!((world_point.y - 3.0).abs() < 0.0001);
930        }
931
932        #[test]
933        fn test_transform_direction() {
934            let global = GlobalTransform::from_translation(Vec3::new(1000.0, 0.0, 0.0));
935            let direction = Vec3::new(1.0, 0.0, 0.0);
936            let world_dir = global.transform_direction(direction);
937
938            // Direction should not be affected by translation
939            assert!((world_dir.x - 1.0).abs() < 0.0001);
940            assert!(world_dir.y.abs() < 0.0001);
941        }
942
943        #[test]
944        fn test_inverse() {
945            let global = GlobalTransform::from_translation_rotation_scale(
946                Vec3::new(5.0, 10.0, 15.0),
947                Quat::from_axis_angle(Vec3::unit_y(), FRAC_PI_4),
948                Vec3::new(2.0, 2.0, 2.0),
949            );
950
951            let inverse = global.inverse().expect("Should be invertible");
952            let identity = global.mul_transform(&inverse);
953
954            // Should be close to identity
955            let pos = identity.translation();
956            assert!(pos.length() < 0.001);
957
958            let scale = identity.scale();
959            assert!((scale.x - 1.0).abs() < 0.001);
960        }
961
962        #[test]
963        fn test_mul_operator() {
964            let a = GlobalTransform::from_translation(Vec3::new(10.0, 0.0, 0.0));
965            let b = GlobalTransform::from_translation(Vec3::new(5.0, 0.0, 0.0));
966            let result = a * b;
967
968            let pos = result.translation();
969            assert!((pos.x - 15.0).abs() < 0.0001);
970        }
971
972        #[test]
973        fn test_mul_operator_with_transform() {
974            let parent = GlobalTransform::from_translation(Vec3::new(10.0, 0.0, 0.0));
975            let child = Transform::from_position(Vec3::new(5.0, 0.0, 0.0));
976            let result = parent * child;
977
978            let pos = result.translation();
979            assert!((pos.x - 15.0).abs() < 0.0001);
980        }
981    }
982
983    mod direction_tests {
984        use super::*;
985
986        #[test]
987        fn test_directions_identity() {
988            let global = GlobalTransform::IDENTITY;
989
990            assert!((global.forward() - Vec3::new(0.0, 0.0, -1.0)).length() < 0.0001);
991            assert!((global.back() - Vec3::new(0.0, 0.0, 1.0)).length() < 0.0001);
992            assert!((global.right() - Vec3::new(1.0, 0.0, 0.0)).length() < 0.0001);
993            assert!((global.left() - Vec3::new(-1.0, 0.0, 0.0)).length() < 0.0001);
994            assert!((global.up() - Vec3::new(0.0, 1.0, 0.0)).length() < 0.0001);
995            assert!((global.down() - Vec3::new(0.0, -1.0, 0.0)).length() < 0.0001);
996        }
997
998        #[test]
999        fn test_directions_rotated() {
1000            let global = GlobalTransform::from_translation_rotation_scale(
1001                Vec3::zero(),
1002                Quat::from_axis_angle(Vec3::unit_y(), FRAC_PI_2),
1003                Vec3::one(),
1004            );
1005
1006            // After +90 degree Y rotation:
1007            // forward (-Z) -> -X
1008            let fwd = global.forward();
1009            assert!((fwd.x - (-1.0)).abs() < 0.0001);
1010            assert!(fwd.y.abs() < 0.0001);
1011            assert!(fwd.z.abs() < 0.0001);
1012        }
1013    }
1014
1015    mod interpolation_tests {
1016        use super::*;
1017
1018        #[test]
1019        fn test_lerp_translation() {
1020            let a = GlobalTransform::from_translation(Vec3::zero());
1021            let b = GlobalTransform::from_translation(Vec3::new(10.0, 0.0, 0.0));
1022
1023            let mid = a.lerp(&b, 0.5);
1024            let pos = mid.translation();
1025            assert!((pos.x - 5.0).abs() < 0.0001);
1026        }
1027
1028        #[test]
1029        fn test_lerp_endpoints() {
1030            let a = GlobalTransform::from_translation(Vec3::new(0.0, 0.0, 0.0));
1031            let b = GlobalTransform::from_translation(Vec3::new(10.0, 10.0, 10.0));
1032
1033            let start = a.lerp(&b, 0.0);
1034            assert!((start.translation() - a.translation()).length() < 0.0001);
1035
1036            let end = a.lerp(&b, 1.0);
1037            assert!((end.translation() - b.translation()).length() < 0.0001);
1038        }
1039    }
1040
1041    mod array_tests {
1042        use super::*;
1043
1044        #[test]
1045        fn test_to_cols_array() {
1046            let global = GlobalTransform::IDENTITY;
1047            let cols = global.to_cols_array();
1048
1049            // Identity matrix
1050            assert_eq!(cols[0], 1.0); // m00
1051            assert_eq!(cols[5], 1.0); // m11
1052            assert_eq!(cols[10], 1.0); // m22
1053            assert_eq!(cols[15], 1.0); // m33
1054        }
1055
1056        #[test]
1057        fn test_to_rows_array() {
1058            let global = GlobalTransform::from_translation(Vec3::new(10.0, 20.0, 30.0));
1059            let rows = global.to_rows_array();
1060
1061            // Translation is in the last row for row-major
1062            assert!((rows[3] - 10.0).abs() < 0.0001);
1063            assert!((rows[7] - 20.0).abs() < 0.0001);
1064            assert!((rows[11] - 30.0).abs() < 0.0001);
1065        }
1066    }
1067
1068    mod component_tests {
1069        use super::*;
1070
1071        #[test]
1072        fn test_is_component() {
1073            fn assert_component<T: Component>() {}
1074            assert_component::<GlobalTransform>();
1075        }
1076
1077        #[test]
1078        fn test_is_send() {
1079            fn assert_send<T: Send>() {}
1080            assert_send::<GlobalTransform>();
1081        }
1082
1083        #[test]
1084        fn test_is_sync() {
1085            fn assert_sync<T: Sync>() {}
1086            assert_sync::<GlobalTransform>();
1087        }
1088
1089        #[test]
1090        fn test_clone() {
1091            let global = GlobalTransform::from_translation(Vec3::new(1.0, 2.0, 3.0));
1092            let cloned = global.clone();
1093            assert_eq!(global, cloned);
1094        }
1095
1096        #[test]
1097        fn test_copy() {
1098            let global = GlobalTransform::IDENTITY;
1099            let copied = global;
1100            assert_eq!(global, copied);
1101        }
1102
1103        #[test]
1104        fn test_debug() {
1105            let global = GlobalTransform::from_translation(Vec3::new(10.0, 5.0, 0.0));
1106            let debug = format!("{:?}", global);
1107            assert!(debug.contains("GlobalTransform"));
1108            assert!(debug.contains("translation"));
1109        }
1110    }
1111
1112    mod hierarchy_tests {
1113        use super::*;
1114
1115        #[test]
1116        fn test_parent_child_translation() {
1117            // Parent at (10, 0, 0)
1118            let parent_global = GlobalTransform::from_translation(Vec3::new(10.0, 0.0, 0.0));
1119
1120            // Child at local (5, 0, 0)
1121            let child_local = Transform::from_position(Vec3::new(5.0, 0.0, 0.0));
1122
1123            // Child's global should be (15, 0, 0)
1124            let child_global = parent_global.transform_by(&child_local);
1125            let pos = child_global.translation();
1126            assert!((pos.x - 15.0).abs() < 0.0001);
1127        }
1128
1129        #[test]
1130        fn test_parent_child_rotation() {
1131            // Parent rotated 90 degrees around Y
1132            let parent_global = GlobalTransform::from_translation_rotation_scale(
1133                Vec3::zero(),
1134                Quat::from_axis_angle(Vec3::unit_y(), FRAC_PI_2),
1135                Vec3::one(),
1136            );
1137
1138            // Child at local (0, 0, -10) - in front of parent
1139            let child_local = Transform::from_position(Vec3::new(0.0, 0.0, -10.0));
1140
1141            // After parent rotation, child should be at (-10, 0, 0)
1142            let child_global = parent_global.transform_by(&child_local);
1143            let pos = child_global.translation();
1144            assert!((pos.x - (-10.0)).abs() < 0.01);
1145            assert!(pos.y.abs() < 0.01);
1146            assert!(pos.z.abs() < 0.01);
1147        }
1148
1149        #[test]
1150        fn test_parent_child_scale() {
1151            // Parent scaled 2x
1152            let parent_global = GlobalTransform::from_translation_rotation_scale(
1153                Vec3::zero(),
1154                Quat::IDENTITY,
1155                Vec3::new(2.0, 2.0, 2.0),
1156            );
1157
1158            // Child at local (5, 0, 0)
1159            let child_local = Transform::from_position(Vec3::new(5.0, 0.0, 0.0));
1160
1161            // Child's global position should be (10, 0, 0)
1162            let child_global = parent_global.transform_by(&child_local);
1163            let pos = child_global.translation();
1164            assert!((pos.x - 10.0).abs() < 0.0001);
1165        }
1166
1167        #[test]
1168        fn test_three_level_hierarchy() {
1169            // Grandparent at (10, 0, 0)
1170            let grandparent = GlobalTransform::from_translation(Vec3::new(10.0, 0.0, 0.0));
1171
1172            // Parent at local (5, 0, 0)
1173            let parent_local = Transform::from_position(Vec3::new(5.0, 0.0, 0.0));
1174            let parent_global = grandparent.transform_by(&parent_local);
1175
1176            // Child at local (3, 0, 0)
1177            let child_local = Transform::from_position(Vec3::new(3.0, 0.0, 0.0));
1178            let child_global = parent_global.transform_by(&child_local);
1179
1180            // Child's global should be (18, 0, 0)
1181            let pos = child_global.translation();
1182            assert!((pos.x - 18.0).abs() < 0.0001);
1183        }
1184    }
1185}