Skip to main content

goud_engine/ecs/components/
global_transform2d.rs

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