Skip to main content

goud_engine/ecs/components/
transform2d.rs

1//! Transform2D component for 2D spatial transformations.
2//!
3//! The [`Transform2D`] component represents an entity's position, rotation, and scale
4//! in 2D space. It is optimized for 2D games where entities exist on a flat plane.
5//!
6//! # Design Philosophy
7//!
8//! Transform2D uses a simpler representation than the 3D [`Transform`](crate::ecs::components::Transform):
9//!
10//! - **Position**: 2D vector (x, y)
11//! - **Rotation**: Single angle in radians (counter-clockwise)
12//! - **Scale**: 2D vector for non-uniform scaling
13//!
14//! This provides:
15//! - **Simplicity**: No quaternions, just a rotation angle
16//! - **Memory efficiency**: 20 bytes vs 40 bytes for Transform
17//! - **Intuitive**: Rotation is a single value in radians
18//! - **Performance**: Simpler matrix calculations for 2D
19//!
20//! # Usage
21//!
22//! ```
23//! use goud_engine::ecs::components::Transform2D;
24//! use goud_engine::core::math::Vec2;
25//! use std::f32::consts::PI;
26//!
27//! // Create a transform at position (100, 50)
28//! let mut transform = Transform2D::from_position(Vec2::new(100.0, 50.0));
29//!
30//! // Modify the transform
31//! transform.translate(Vec2::new(10.0, 0.0));
32//! transform.rotate(PI / 4.0); // 45 degrees
33//! transform.set_scale(Vec2::new(2.0, 2.0));
34//!
35//! // Get the transformation matrix for rendering
36//! let matrix = transform.matrix();
37//! ```
38//!
39//! # Coordinate System
40//!
41//! GoudEngine 2D uses a standard screen-space coordinate system:
42//! - X axis: Right (positive)
43//! - Y axis: Down (positive) or Up (positive) depending on camera
44//!
45//! Rotation is counter-clockwise when viewed from above (standard mathematical convention).
46//!
47//! # FFI Safety
48//!
49//! Transform2D is `#[repr(C)]` and can be safely passed across FFI boundaries.
50
51use crate::core::math::Vec2;
52use crate::ecs::Component;
53use std::f32::consts::PI;
54
55/// A 2D spatial transformation component.
56///
57/// Represents position, rotation, and scale in 2D space. This is the primary
58/// component for positioning entities in 2D games.
59///
60/// # Memory Layout
61///
62/// The component is laid out as:
63/// - `position`: 2 x f32 (8 bytes)
64/// - `rotation`: f32 (4 bytes)
65/// - `scale`: 2 x f32 (8 bytes)
66/// - Total: 20 bytes
67///
68/// # Example
69///
70/// ```
71/// use goud_engine::ecs::components::Transform2D;
72/// use goud_engine::core::math::Vec2;
73///
74/// // Create at origin with default rotation and scale
75/// let mut transform = Transform2D::default();
76///
77/// // Or create with specific position
78/// let transform = Transform2D::from_position(Vec2::new(100.0, 50.0));
79///
80/// // Or with full control
81/// let transform = Transform2D::new(
82///     Vec2::new(100.0, 50.0),  // position
83///     0.0,                      // rotation (radians)
84///     Vec2::one(),              // scale
85/// );
86/// ```
87#[repr(C)]
88#[derive(Clone, Copy, Debug, PartialEq)]
89pub struct Transform2D {
90    /// Position in world space (or local space if entity has a parent).
91    pub position: Vec2,
92    /// Rotation angle in radians (counter-clockwise).
93    pub rotation: f32,
94    /// Scale along each axis.
95    ///
96    /// A scale of (1, 1) means no scaling. Negative values flip the object
97    /// along that axis.
98    pub scale: Vec2,
99}
100
101/// A 3x3 transformation matrix for 2D transforms.
102///
103/// Stored in column-major order for OpenGL compatibility.
104/// The bottom row is always [0, 0, 1].
105///
106/// Layout:
107/// ```text
108/// | m[0] m[3] m[6] |   | cos*sx  -sin*sy  tx |
109/// | m[1] m[4] m[7] | = | sin*sx   cos*sy  ty |
110/// | m[2] m[5] m[8] |   |   0        0      1 |
111/// ```
112#[repr(C)]
113#[derive(Clone, Copy, Debug, PartialEq)]
114pub struct Mat3x3 {
115    /// Matrix elements in column-major order.
116    pub m: [f32; 9],
117}
118
119impl Mat3x3 {
120    /// Identity matrix.
121    pub const IDENTITY: Mat3x3 = Mat3x3 {
122        m: [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0],
123    };
124
125    /// Creates a new matrix from column-major elements.
126    #[inline]
127    pub const fn new(m: [f32; 9]) -> Self {
128        Self { m }
129    }
130
131    /// Creates a matrix from individual components.
132    ///
133    /// Arguments are in row-major order for readability:
134    /// ```text
135    /// | m00 m01 m02 |
136    /// | m10 m11 m12 |
137    /// | m20 m21 m22 |
138    /// ```
139    #[inline]
140    pub const fn from_rows(
141        m00: f32,
142        m01: f32,
143        m02: f32,
144        m10: f32,
145        m11: f32,
146        m12: f32,
147        m20: f32,
148        m21: f32,
149        m22: f32,
150    ) -> Self {
151        // Convert to column-major storage
152        Self {
153            m: [m00, m10, m20, m01, m11, m21, m02, m12, m22],
154        }
155    }
156
157    /// Creates a translation matrix.
158    #[inline]
159    pub fn translation(tx: f32, ty: f32) -> Self {
160        Self {
161            m: [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, tx, ty, 1.0],
162        }
163    }
164
165    /// Creates a rotation matrix from an angle in radians.
166    #[inline]
167    pub fn rotation(angle: f32) -> Self {
168        let (sin, cos) = angle.sin_cos();
169        Self {
170            m: [cos, sin, 0.0, -sin, cos, 0.0, 0.0, 0.0, 1.0],
171        }
172    }
173
174    /// Creates a scale matrix.
175    #[inline]
176    pub fn scale(sx: f32, sy: f32) -> Self {
177        Self {
178            m: [sx, 0.0, 0.0, 0.0, sy, 0.0, 0.0, 0.0, 1.0],
179        }
180    }
181
182    /// Returns the translation component.
183    #[inline]
184    pub fn get_translation(&self) -> Vec2 {
185        Vec2::new(self.m[6], self.m[7])
186    }
187
188    /// Multiplies two matrices.
189    #[inline]
190    pub fn multiply(&self, other: &Self) -> Self {
191        let a = &self.m;
192        let b = &other.m;
193
194        Self {
195            m: [
196                a[0] * b[0] + a[3] * b[1] + a[6] * b[2],
197                a[1] * b[0] + a[4] * b[1] + a[7] * b[2],
198                a[2] * b[0] + a[5] * b[1] + a[8] * b[2],
199                a[0] * b[3] + a[3] * b[4] + a[6] * b[5],
200                a[1] * b[3] + a[4] * b[4] + a[7] * b[5],
201                a[2] * b[3] + a[5] * b[4] + a[8] * b[5],
202                a[0] * b[6] + a[3] * b[7] + a[6] * b[8],
203                a[1] * b[6] + a[4] * b[7] + a[7] * b[8],
204                a[2] * b[6] + a[5] * b[7] + a[8] * b[8],
205            ],
206        }
207    }
208
209    /// Transforms a point by this matrix.
210    #[inline]
211    pub fn transform_point(&self, point: Vec2) -> Vec2 {
212        Vec2::new(
213            self.m[0] * point.x + self.m[3] * point.y + self.m[6],
214            self.m[1] * point.x + self.m[4] * point.y + self.m[7],
215        )
216    }
217
218    /// Transforms a direction by this matrix (ignores translation).
219    #[inline]
220    pub fn transform_direction(&self, direction: Vec2) -> Vec2 {
221        Vec2::new(
222            self.m[0] * direction.x + self.m[3] * direction.y,
223            self.m[1] * direction.x + self.m[4] * direction.y,
224        )
225    }
226
227    /// Computes the determinant of the matrix.
228    #[inline]
229    pub fn determinant(&self) -> f32 {
230        let m = &self.m;
231        m[0] * (m[4] * m[8] - m[7] * m[5]) - m[3] * (m[1] * m[8] - m[7] * m[2])
232            + m[6] * (m[1] * m[5] - m[4] * m[2])
233    }
234
235    /// Computes the inverse of the matrix.
236    ///
237    /// Returns None if the matrix is not invertible (determinant is zero).
238    pub fn inverse(&self) -> Option<Self> {
239        let det = self.determinant();
240        if det.abs() < f32::EPSILON {
241            return None;
242        }
243
244        let m = &self.m;
245        let inv_det = 1.0 / det;
246
247        Some(Self {
248            m: [
249                (m[4] * m[8] - m[7] * m[5]) * inv_det,
250                (m[7] * m[2] - m[1] * m[8]) * inv_det,
251                (m[1] * m[5] - m[4] * m[2]) * inv_det,
252                (m[6] * m[5] - m[3] * m[8]) * inv_det,
253                (m[0] * m[8] - m[6] * m[2]) * inv_det,
254                (m[3] * m[2] - m[0] * m[5]) * inv_det,
255                (m[3] * m[7] - m[6] * m[4]) * inv_det,
256                (m[6] * m[1] - m[0] * m[7]) * inv_det,
257                (m[0] * m[4] - m[3] * m[1]) * inv_det,
258            ],
259        })
260    }
261
262    /// Converts to a 4x4 matrix for 3D rendering.
263    ///
264    /// The result is a 4x4 matrix with the 2D transform in the XY plane:
265    /// ```text
266    /// | m[0] m[3]  0  m[6] |
267    /// | m[1] m[4]  0  m[7] |
268    /// |  0    0    1   0   |
269    /// |  0    0    0   1   |
270    /// ```
271    #[inline]
272    pub fn to_mat4(&self) -> [f32; 16] {
273        [
274            self.m[0], self.m[1], 0.0, 0.0, // column 0
275            self.m[3], self.m[4], 0.0, 0.0, // column 1
276            0.0, 0.0, 1.0, 0.0, // column 2
277            self.m[6], self.m[7], 0.0, 1.0, // column 3
278        ]
279    }
280}
281
282impl Default for Mat3x3 {
283    #[inline]
284    fn default() -> Self {
285        Self::IDENTITY
286    }
287}
288
289impl std::ops::Mul for Mat3x3 {
290    type Output = Self;
291
292    #[inline]
293    fn mul(self, other: Self) -> Self {
294        self.multiply(&other)
295    }
296}
297
298impl Transform2D {
299    /// Creates a new Transform2D with the specified position, rotation, and scale.
300    #[inline]
301    pub const fn new(position: Vec2, rotation: f32, scale: Vec2) -> Self {
302        Self {
303            position,
304            rotation,
305            scale,
306        }
307    }
308
309    /// Creates a Transform2D at the specified position with default rotation and scale.
310    #[inline]
311    pub fn from_position(position: Vec2) -> Self {
312        Self {
313            position,
314            rotation: 0.0,
315            scale: Vec2::one(),
316        }
317    }
318
319    /// Creates a Transform2D at the origin with the specified rotation.
320    #[inline]
321    pub fn from_rotation(rotation: f32) -> Self {
322        Self {
323            position: Vec2::zero(),
324            rotation,
325            scale: Vec2::one(),
326        }
327    }
328
329    /// Creates a Transform2D with specified rotation in degrees.
330    #[inline]
331    pub fn from_rotation_degrees(degrees: f32) -> Self {
332        Self::from_rotation(degrees.to_radians())
333    }
334
335    /// Creates a Transform2D at the origin with the specified scale.
336    #[inline]
337    pub fn from_scale(scale: Vec2) -> Self {
338        Self {
339            position: Vec2::zero(),
340            rotation: 0.0,
341            scale,
342        }
343    }
344
345    /// Creates a Transform2D with uniform scale.
346    #[inline]
347    pub fn from_scale_uniform(scale: f32) -> Self {
348        Self::from_scale(Vec2::new(scale, scale))
349    }
350
351    /// Creates a Transform2D with position and rotation.
352    #[inline]
353    pub fn from_position_rotation(position: Vec2, rotation: f32) -> Self {
354        Self {
355            position,
356            rotation,
357            scale: Vec2::one(),
358        }
359    }
360
361    /// Creates a Transform2D looking at a target position.
362    ///
363    /// The transform's forward direction (positive X after rotation)
364    /// will point towards the target.
365    #[inline]
366    pub fn look_at(position: Vec2, target: Vec2) -> Self {
367        let direction = target - position;
368        let rotation = direction.y.atan2(direction.x);
369        Self {
370            position,
371            rotation,
372            scale: Vec2::one(),
373        }
374    }
375
376    // =========================================================================
377    // Position Methods
378    // =========================================================================
379
380    /// Translates the transform by the given offset in world space.
381    #[inline]
382    pub fn translate(&mut self, offset: Vec2) {
383        self.position = self.position + offset;
384    }
385
386    /// Translates the transform in local space.
387    ///
388    /// The offset is rotated by the transform's rotation before being applied.
389    #[inline]
390    pub fn translate_local(&mut self, offset: Vec2) {
391        let (sin, cos) = self.rotation.sin_cos();
392        let rotated = Vec2::new(
393            offset.x * cos - offset.y * sin,
394            offset.x * sin + offset.y * cos,
395        );
396        self.position = self.position + rotated;
397    }
398
399    /// Sets the position of the transform.
400    #[inline]
401    pub fn set_position(&mut self, position: Vec2) {
402        self.position = position;
403    }
404
405    // =========================================================================
406    // Rotation Methods
407    // =========================================================================
408
409    /// Rotates the transform by the given angle in radians.
410    #[inline]
411    pub fn rotate(&mut self, angle: f32) {
412        self.rotation = normalize_angle(self.rotation + angle);
413    }
414
415    /// Rotates the transform by the given angle in degrees.
416    #[inline]
417    pub fn rotate_degrees(&mut self, degrees: f32) {
418        self.rotate(degrees.to_radians());
419    }
420
421    /// Sets the rotation angle in radians.
422    #[inline]
423    pub fn set_rotation(&mut self, rotation: f32) {
424        self.rotation = normalize_angle(rotation);
425    }
426
427    /// Sets the rotation angle in degrees.
428    #[inline]
429    pub fn set_rotation_degrees(&mut self, degrees: f32) {
430        self.set_rotation(degrees.to_radians());
431    }
432
433    /// Returns the rotation angle in degrees.
434    #[inline]
435    pub fn rotation_degrees(&self) -> f32 {
436        self.rotation.to_degrees()
437    }
438
439    /// Makes the transform look at a target position.
440    #[inline]
441    pub fn look_at_target(&mut self, target: Vec2) {
442        let direction = target - self.position;
443        self.rotation = direction.y.atan2(direction.x);
444    }
445
446    // =========================================================================
447    // Scale Methods
448    // =========================================================================
449
450    /// Sets the scale of the transform.
451    #[inline]
452    pub fn set_scale(&mut self, scale: Vec2) {
453        self.scale = scale;
454    }
455
456    /// Sets uniform scale on both axes.
457    #[inline]
458    pub fn set_scale_uniform(&mut self, scale: f32) {
459        self.scale = Vec2::new(scale, scale);
460    }
461
462    /// Multiplies the current scale by the given factors.
463    #[inline]
464    pub fn scale_by(&mut self, factors: Vec2) {
465        self.scale = Vec2::new(self.scale.x * factors.x, self.scale.y * factors.y);
466    }
467
468    // =========================================================================
469    // Direction Vectors
470    // =========================================================================
471
472    /// Returns the forward direction vector (positive X axis after rotation).
473    #[inline]
474    pub fn forward(&self) -> Vec2 {
475        let (sin, cos) = self.rotation.sin_cos();
476        Vec2::new(cos, sin)
477    }
478
479    /// Returns the right direction vector (positive Y axis after rotation).
480    ///
481    /// This is perpendicular to forward, rotated 90 degrees counter-clockwise.
482    #[inline]
483    pub fn right(&self) -> Vec2 {
484        let (sin, cos) = self.rotation.sin_cos();
485        Vec2::new(-sin, cos)
486    }
487
488    /// Returns the backward direction vector (negative X axis after rotation).
489    #[inline]
490    pub fn backward(&self) -> Vec2 {
491        -self.forward()
492    }
493
494    /// Returns the left direction vector (negative Y axis after rotation).
495    #[inline]
496    pub fn left(&self) -> Vec2 {
497        -self.right()
498    }
499
500    // =========================================================================
501    // Matrix Generation
502    // =========================================================================
503
504    /// Computes the 3x3 transformation matrix.
505    ///
506    /// The matrix represents the combined transformation: Scale * Rotation * Translation
507    /// (applied in that order when transforming points).
508    #[inline]
509    pub fn matrix(&self) -> Mat3x3 {
510        let (sin, cos) = self.rotation.sin_cos();
511        let sx = self.scale.x;
512        let sy = self.scale.y;
513
514        // Combined SRT matrix in column-major order:
515        // | cos*sx  -sin*sy  tx |
516        // | sin*sx   cos*sy  ty |
517        // |   0        0      1 |
518        Mat3x3 {
519            m: [
520                cos * sx,
521                sin * sx,
522                0.0,
523                -sin * sy,
524                cos * sy,
525                0.0,
526                self.position.x,
527                self.position.y,
528                1.0,
529            ],
530        }
531    }
532
533    /// Computes the inverse transformation matrix.
534    ///
535    /// Useful for converting world-space to local-space.
536    #[inline]
537    pub fn matrix_inverse(&self) -> Mat3x3 {
538        let (sin, cos) = self.rotation.sin_cos();
539        let inv_sx = 1.0 / self.scale.x;
540        let inv_sy = 1.0 / self.scale.y;
541
542        // Inverse of TRS = S^-1 * R^-1 * T^-1
543        // R^-1 is rotation by -angle, which has cos unchanged and sin negated
544
545        // First, the inverse rotation matrix (transpose for orthogonal matrix)
546        // R^-1 = [[cos, sin], [-sin, cos]]  (note: sin is now positive in top row)
547
548        // The combined S^-1 * R^-1 matrix:
549        // | cos/sx   sin/sx |
550        // | -sin/sy  cos/sy |
551
552        // Translation part: -(S^-1 * R^-1 * t)
553        let inv_tx = -(cos * self.position.x + sin * self.position.y) * inv_sx;
554        let inv_ty = -(-sin * self.position.x + cos * self.position.y) * inv_sy;
555
556        Mat3x3 {
557            m: [
558                cos * inv_sx,  // m[0]
559                -sin * inv_sy, // m[1]
560                0.0,           // m[2]
561                sin * inv_sx,  // m[3]
562                cos * inv_sy,  // m[4]
563                0.0,           // m[5]
564                inv_tx,        // m[6]
565                inv_ty,        // m[7]
566                1.0,           // m[8]
567            ],
568        }
569    }
570
571    /// Converts to a 4x4 matrix for use with 3D rendering APIs.
572    ///
573    /// The result places the 2D transform in the XY plane at Z=0.
574    #[inline]
575    pub fn to_mat4(&self) -> [f32; 16] {
576        self.matrix().to_mat4()
577    }
578
579    // =========================================================================
580    // Point Transformation
581    // =========================================================================
582
583    /// Transforms a point from local space to world space.
584    #[inline]
585    pub fn transform_point(&self, point: Vec2) -> Vec2 {
586        let (sin, cos) = self.rotation.sin_cos();
587        let scaled = Vec2::new(point.x * self.scale.x, point.y * self.scale.y);
588        let rotated = Vec2::new(
589            scaled.x * cos - scaled.y * sin,
590            scaled.x * sin + scaled.y * cos,
591        );
592        rotated + self.position
593    }
594
595    /// Transforms a direction from local space to world space.
596    ///
597    /// Unlike points, directions are not affected by translation.
598    #[inline]
599    pub fn transform_direction(&self, direction: Vec2) -> Vec2 {
600        let (sin, cos) = self.rotation.sin_cos();
601        Vec2::new(
602            direction.x * cos - direction.y * sin,
603            direction.x * sin + direction.y * cos,
604        )
605    }
606
607    /// Transforms a point from world space to local space.
608    #[inline]
609    pub fn inverse_transform_point(&self, point: Vec2) -> Vec2 {
610        let translated = point - self.position;
611        let (sin, cos) = self.rotation.sin_cos();
612        let rotated = Vec2::new(
613            translated.x * cos + translated.y * sin,
614            -translated.x * sin + translated.y * cos,
615        );
616        Vec2::new(rotated.x / self.scale.x, rotated.y / self.scale.y)
617    }
618
619    /// Transforms a direction from world space to local space.
620    #[inline]
621    pub fn inverse_transform_direction(&self, direction: Vec2) -> Vec2 {
622        let (sin, cos) = self.rotation.sin_cos();
623        Vec2::new(
624            direction.x * cos + direction.y * sin,
625            -direction.x * sin + direction.y * cos,
626        )
627    }
628
629    // =========================================================================
630    // Interpolation
631    // =========================================================================
632
633    /// Linearly interpolates between two transforms.
634    ///
635    /// Position and scale are linearly interpolated, rotation uses
636    /// shortest-path angle interpolation.
637    #[inline]
638    pub fn lerp(self, other: Self, t: f32) -> Self {
639        Self {
640            position: self.position.lerp(other.position, t),
641            rotation: lerp_angle(self.rotation, other.rotation, t),
642            scale: self.scale.lerp(other.scale, t),
643        }
644    }
645}
646
647impl Default for Transform2D {
648    /// Returns a Transform2D at the origin with no rotation and unit scale.
649    #[inline]
650    fn default() -> Self {
651        Self {
652            position: Vec2::zero(),
653            rotation: 0.0,
654            scale: Vec2::one(),
655        }
656    }
657}
658
659// Implement Component trait for Transform2D
660impl Component for Transform2D {}
661
662/// Normalizes an angle to the range [-PI, PI).
663#[inline]
664fn normalize_angle(angle: f32) -> f32 {
665    let mut result = angle % (2.0 * PI);
666    if result >= PI {
667        result -= 2.0 * PI;
668    } else if result < -PI {
669        result += 2.0 * PI;
670    }
671    result
672}
673
674/// Linearly interpolates between two angles using shortest path.
675#[inline]
676fn lerp_angle(from: f32, to: f32, t: f32) -> f32 {
677    let mut diff = to - from;
678
679    // Wrap to [-PI, PI]
680    while diff > PI {
681        diff -= 2.0 * PI;
682    }
683    while diff < -PI {
684        diff += 2.0 * PI;
685    }
686
687    normalize_angle(from + diff * t)
688}
689
690#[cfg(test)]
691mod tests {
692    use super::*;
693    use std::f32::consts::{FRAC_PI_2, FRAC_PI_4, PI};
694
695    // =========================================================================
696    // Mat3x3 Tests
697    // =========================================================================
698
699    mod mat3x3_tests {
700        use super::*;
701
702        #[test]
703        fn test_identity() {
704            let m = Mat3x3::IDENTITY;
705            assert_eq!(m.m[0], 1.0);
706            assert_eq!(m.m[4], 1.0);
707            assert_eq!(m.m[8], 1.0);
708        }
709
710        #[test]
711        fn test_translation() {
712            let m = Mat3x3::translation(10.0, 20.0);
713            let p = m.transform_point(Vec2::zero());
714            assert!((p.x - 10.0).abs() < 0.0001);
715            assert!((p.y - 20.0).abs() < 0.0001);
716        }
717
718        #[test]
719        fn test_rotation() {
720            let m = Mat3x3::rotation(FRAC_PI_2);
721            let p = m.transform_point(Vec2::unit_x());
722            // 90 degree rotation: (1, 0) -> (0, 1)
723            assert!(p.x.abs() < 0.0001);
724            assert!((p.y - 1.0).abs() < 0.0001);
725        }
726
727        #[test]
728        fn test_scale() {
729            let m = Mat3x3::scale(2.0, 3.0);
730            let p = m.transform_point(Vec2::new(1.0, 1.0));
731            assert!((p.x - 2.0).abs() < 0.0001);
732            assert!((p.y - 3.0).abs() < 0.0001);
733        }
734
735        #[test]
736        fn test_multiply() {
737            let t = Mat3x3::translation(10.0, 0.0);
738            let r = Mat3x3::rotation(FRAC_PI_2);
739            let combined = t * r;
740
741            let p = combined.transform_point(Vec2::unit_x());
742            // First rotate: (1, 0) -> (0, 1)
743            // Then translate: (0, 1) -> (10, 1)
744            assert!((p.x - 10.0).abs() < 0.0001);
745            assert!((p.y - 1.0).abs() < 0.0001);
746        }
747
748        #[test]
749        fn test_inverse() {
750            let m = Mat3x3::translation(10.0, 20.0);
751            let inv = m.inverse().unwrap();
752            let result = m * inv;
753
754            // Should be close to identity
755            assert!((result.m[0] - 1.0).abs() < 0.0001);
756            assert!((result.m[4] - 1.0).abs() < 0.0001);
757            assert!((result.m[8] - 1.0).abs() < 0.0001);
758        }
759
760        #[test]
761        fn test_inverse_rotation() {
762            let m = Mat3x3::rotation(FRAC_PI_4);
763            let inv = m.inverse().unwrap();
764            let result = m * inv;
765
766            assert!((result.m[0] - 1.0).abs() < 0.0001);
767            assert!((result.m[4] - 1.0).abs() < 0.0001);
768        }
769
770        #[test]
771        fn test_determinant() {
772            let m = Mat3x3::IDENTITY;
773            assert!((m.determinant() - 1.0).abs() < 0.0001);
774
775            let s = Mat3x3::scale(2.0, 3.0);
776            assert!((s.determinant() - 6.0).abs() < 0.0001);
777        }
778
779        #[test]
780        fn test_transform_direction() {
781            let m = Mat3x3::translation(100.0, 100.0);
782            let d = m.transform_direction(Vec2::unit_x());
783            // Direction should not be affected by translation
784            assert!((d.x - 1.0).abs() < 0.0001);
785            assert!(d.y.abs() < 0.0001);
786        }
787
788        #[test]
789        fn test_to_mat4() {
790            let m = Mat3x3::translation(10.0, 20.0);
791            let m4 = m.to_mat4();
792
793            // Check diagonal
794            assert_eq!(m4[0], 1.0);
795            assert_eq!(m4[5], 1.0);
796            assert_eq!(m4[10], 1.0);
797            assert_eq!(m4[15], 1.0);
798
799            // Check translation
800            assert_eq!(m4[12], 10.0);
801            assert_eq!(m4[13], 20.0);
802            assert_eq!(m4[14], 0.0);
803        }
804
805        #[test]
806        fn test_default() {
807            assert_eq!(Mat3x3::default(), Mat3x3::IDENTITY);
808        }
809    }
810
811    // =========================================================================
812    // Transform2D Construction Tests
813    // =========================================================================
814
815    mod construction_tests {
816        use super::*;
817
818        #[test]
819        fn test_default() {
820            let t = Transform2D::default();
821            assert_eq!(t.position, Vec2::zero());
822            assert_eq!(t.rotation, 0.0);
823            assert_eq!(t.scale, Vec2::one());
824        }
825
826        #[test]
827        fn test_new() {
828            let pos = Vec2::new(10.0, 20.0);
829            let rot = FRAC_PI_4;
830            let scale = Vec2::new(2.0, 3.0);
831
832            let t = Transform2D::new(pos, rot, scale);
833            assert_eq!(t.position, pos);
834            assert_eq!(t.rotation, rot);
835            assert_eq!(t.scale, scale);
836        }
837
838        #[test]
839        fn test_from_position() {
840            let pos = Vec2::new(100.0, 50.0);
841            let t = Transform2D::from_position(pos);
842            assert_eq!(t.position, pos);
843            assert_eq!(t.rotation, 0.0);
844            assert_eq!(t.scale, Vec2::one());
845        }
846
847        #[test]
848        fn test_from_rotation() {
849            let t = Transform2D::from_rotation(FRAC_PI_2);
850            assert_eq!(t.position, Vec2::zero());
851            assert_eq!(t.rotation, FRAC_PI_2);
852            assert_eq!(t.scale, Vec2::one());
853        }
854
855        #[test]
856        fn test_from_rotation_degrees() {
857            let t = Transform2D::from_rotation_degrees(90.0);
858            assert!((t.rotation - FRAC_PI_2).abs() < 0.0001);
859        }
860
861        #[test]
862        fn test_from_scale() {
863            let scale = Vec2::new(2.0, 3.0);
864            let t = Transform2D::from_scale(scale);
865            assert_eq!(t.position, Vec2::zero());
866            assert_eq!(t.rotation, 0.0);
867            assert_eq!(t.scale, scale);
868        }
869
870        #[test]
871        fn test_from_scale_uniform() {
872            let t = Transform2D::from_scale_uniform(2.0);
873            assert_eq!(t.scale, Vec2::new(2.0, 2.0));
874        }
875
876        #[test]
877        fn test_from_position_rotation() {
878            let pos = Vec2::new(10.0, 20.0);
879            let t = Transform2D::from_position_rotation(pos, FRAC_PI_4);
880            assert_eq!(t.position, pos);
881            assert_eq!(t.rotation, FRAC_PI_4);
882            assert_eq!(t.scale, Vec2::one());
883        }
884
885        #[test]
886        fn test_look_at() {
887            let t = Transform2D::look_at(Vec2::zero(), Vec2::new(1.0, 0.0));
888            assert!(t.rotation.abs() < 0.0001); // Should be 0 (looking right)
889
890            let t2 = Transform2D::look_at(Vec2::zero(), Vec2::new(0.0, 1.0));
891            assert!((t2.rotation - FRAC_PI_2).abs() < 0.0001); // Should be 90 degrees
892        }
893    }
894
895    // =========================================================================
896    // Transform2D Mutation Tests
897    // =========================================================================
898
899    mod mutation_tests {
900        use super::*;
901
902        #[test]
903        fn test_translate() {
904            let mut t = Transform2D::default();
905            t.translate(Vec2::new(5.0, 10.0));
906            assert_eq!(t.position, Vec2::new(5.0, 10.0));
907
908            t.translate(Vec2::new(3.0, 2.0));
909            assert_eq!(t.position, Vec2::new(8.0, 12.0));
910        }
911
912        #[test]
913        fn test_translate_local() {
914            let mut t = Transform2D::from_rotation(FRAC_PI_2);
915            t.translate_local(Vec2::new(1.0, 0.0));
916
917            // 90 degree rotation: local X (1, 0) -> world (0, 1)
918            assert!(t.position.x.abs() < 0.0001);
919            assert!((t.position.y - 1.0).abs() < 0.0001);
920        }
921
922        #[test]
923        fn test_set_position() {
924            let mut t = Transform2D::from_position(Vec2::new(10.0, 20.0));
925            t.set_position(Vec2::new(100.0, 200.0));
926            assert_eq!(t.position, Vec2::new(100.0, 200.0));
927        }
928
929        #[test]
930        fn test_rotate() {
931            let mut t = Transform2D::default();
932            t.rotate(FRAC_PI_4);
933            assert!((t.rotation - FRAC_PI_4).abs() < 0.0001);
934
935            t.rotate(FRAC_PI_4);
936            assert!((t.rotation - FRAC_PI_2).abs() < 0.0001);
937        }
938
939        #[test]
940        fn test_rotate_degrees() {
941            let mut t = Transform2D::default();
942            t.rotate_degrees(45.0);
943            assert!((t.rotation - FRAC_PI_4).abs() < 0.0001);
944        }
945
946        #[test]
947        fn test_set_rotation() {
948            let mut t = Transform2D::default();
949            t.set_rotation(FRAC_PI_2);
950            assert!((t.rotation - FRAC_PI_2).abs() < 0.0001);
951        }
952
953        #[test]
954        fn test_set_rotation_degrees() {
955            let mut t = Transform2D::default();
956            t.set_rotation_degrees(90.0);
957            assert!((t.rotation - FRAC_PI_2).abs() < 0.0001);
958        }
959
960        #[test]
961        fn test_rotation_degrees() {
962            let t = Transform2D::from_rotation(FRAC_PI_2);
963            assert!((t.rotation_degrees() - 90.0).abs() < 0.01);
964        }
965
966        #[test]
967        fn test_look_at_target() {
968            let mut t = Transform2D::from_position(Vec2::new(10.0, 10.0));
969            t.look_at_target(Vec2::new(20.0, 10.0));
970            assert!(t.rotation.abs() < 0.0001); // Looking right = 0 degrees
971        }
972
973        #[test]
974        fn test_set_scale() {
975            let mut t = Transform2D::default();
976            t.set_scale(Vec2::new(2.0, 3.0));
977            assert_eq!(t.scale, Vec2::new(2.0, 3.0));
978        }
979
980        #[test]
981        fn test_set_scale_uniform() {
982            let mut t = Transform2D::default();
983            t.set_scale_uniform(5.0);
984            assert_eq!(t.scale, Vec2::new(5.0, 5.0));
985        }
986
987        #[test]
988        fn test_scale_by() {
989            let mut t = Transform2D::from_scale(Vec2::new(2.0, 3.0));
990            t.scale_by(Vec2::new(2.0, 2.0));
991            assert_eq!(t.scale, Vec2::new(4.0, 6.0));
992        }
993    }
994
995    // =========================================================================
996    // Direction Tests
997    // =========================================================================
998
999    mod direction_tests {
1000        use super::*;
1001
1002        #[test]
1003        fn test_directions_identity() {
1004            let t = Transform2D::default();
1005
1006            let fwd = t.forward();
1007            assert!((fwd.x - 1.0).abs() < 0.0001);
1008            assert!(fwd.y.abs() < 0.0001);
1009
1010            let right = t.right();
1011            assert!(right.x.abs() < 0.0001);
1012            assert!((right.y - 1.0).abs() < 0.0001);
1013        }
1014
1015        #[test]
1016        fn test_directions_rotated() {
1017            let t = Transform2D::from_rotation(FRAC_PI_2);
1018
1019            // After 90 degree rotation:
1020            // forward (1, 0) -> (0, 1)
1021            let fwd = t.forward();
1022            assert!(fwd.x.abs() < 0.0001);
1023            assert!((fwd.y - 1.0).abs() < 0.0001);
1024
1025            // right (0, 1) -> (-1, 0)
1026            let right = t.right();
1027            assert!((right.x - (-1.0)).abs() < 0.0001);
1028            assert!(right.y.abs() < 0.0001);
1029        }
1030
1031        #[test]
1032        fn test_backward_and_left() {
1033            let t = Transform2D::default();
1034
1035            let back = t.backward();
1036            assert!((back.x - (-1.0)).abs() < 0.0001);
1037
1038            let left = t.left();
1039            assert!((left.y - (-1.0)).abs() < 0.0001);
1040        }
1041    }
1042
1043    // =========================================================================
1044    // Matrix Tests
1045    // =========================================================================
1046
1047    mod matrix_tests {
1048        use super::*;
1049
1050        #[test]
1051        fn test_matrix_identity() {
1052            let t = Transform2D::default();
1053            let m = t.matrix();
1054
1055            // Should be close to identity
1056            assert!((m.m[0] - 1.0).abs() < 0.0001);
1057            assert!((m.m[4] - 1.0).abs() < 0.0001);
1058            assert!((m.m[8] - 1.0).abs() < 0.0001);
1059        }
1060
1061        #[test]
1062        fn test_matrix_translation() {
1063            let t = Transform2D::from_position(Vec2::new(10.0, 20.0));
1064            let m = t.matrix();
1065
1066            assert!((m.m[6] - 10.0).abs() < 0.0001);
1067            assert!((m.m[7] - 20.0).abs() < 0.0001);
1068        }
1069
1070        #[test]
1071        fn test_matrix_scale() {
1072            let t = Transform2D::from_scale(Vec2::new(2.0, 3.0));
1073            let m = t.matrix();
1074
1075            assert!((m.m[0] - 2.0).abs() < 0.0001);
1076            assert!((m.m[4] - 3.0).abs() < 0.0001);
1077        }
1078
1079        #[test]
1080        fn test_matrix_rotation() {
1081            let t = Transform2D::from_rotation(FRAC_PI_2);
1082            let m = t.matrix();
1083
1084            let p = m.transform_point(Vec2::new(1.0, 0.0));
1085            // 90 degree rotation: (1, 0) -> (0, 1)
1086            assert!(p.x.abs() < 0.0001);
1087            assert!((p.y - 1.0).abs() < 0.0001);
1088        }
1089
1090        #[test]
1091        fn test_matrix_inverse() {
1092            let t = Transform2D::new(Vec2::new(10.0, 20.0), FRAC_PI_4, Vec2::new(2.0, 3.0));
1093
1094            let m = t.matrix();
1095            let m_inv = t.matrix_inverse();
1096
1097            let result = m * m_inv;
1098
1099            // Should be close to identity
1100            assert!((result.m[0] - 1.0).abs() < 0.001);
1101            assert!((result.m[4] - 1.0).abs() < 0.001);
1102            assert!((result.m[8] - 1.0).abs() < 0.001);
1103            assert!(result.m[6].abs() < 0.001);
1104            assert!(result.m[7].abs() < 0.001);
1105        }
1106
1107        #[test]
1108        fn test_to_mat4() {
1109            let t = Transform2D::from_position(Vec2::new(5.0, 10.0));
1110            let m4 = t.to_mat4();
1111
1112            // Check translation
1113            assert_eq!(m4[12], 5.0);
1114            assert_eq!(m4[13], 10.0);
1115            assert_eq!(m4[14], 0.0);
1116
1117            // Check diagonal
1118            assert_eq!(m4[0], 1.0);
1119            assert_eq!(m4[5], 1.0);
1120            assert_eq!(m4[10], 1.0);
1121            assert_eq!(m4[15], 1.0);
1122        }
1123    }
1124
1125    // =========================================================================
1126    // Point Transformation Tests
1127    // =========================================================================
1128
1129    mod point_transform_tests {
1130        use super::*;
1131
1132        #[test]
1133        fn test_transform_point_translation() {
1134            let t = Transform2D::from_position(Vec2::new(10.0, 20.0));
1135            let p = t.transform_point(Vec2::zero());
1136            assert_eq!(p, Vec2::new(10.0, 20.0));
1137        }
1138
1139        #[test]
1140        fn test_transform_point_scale() {
1141            let t = Transform2D::from_scale(Vec2::new(2.0, 3.0));
1142            let p = t.transform_point(Vec2::new(5.0, 5.0));
1143            assert_eq!(p, Vec2::new(10.0, 15.0));
1144        }
1145
1146        #[test]
1147        fn test_transform_point_rotation() {
1148            let t = Transform2D::from_rotation(FRAC_PI_2);
1149            let p = t.transform_point(Vec2::new(1.0, 0.0));
1150            // 90 degree rotation: (1, 0) -> (0, 1)
1151            assert!(p.x.abs() < 0.0001);
1152            assert!((p.y - 1.0).abs() < 0.0001);
1153        }
1154
1155        #[test]
1156        fn test_transform_direction() {
1157            let t = Transform2D::new(
1158                Vec2::new(100.0, 100.0), // Translation should not affect direction
1159                FRAC_PI_2,
1160                Vec2::one(),
1161            );
1162
1163            let dir = Vec2::new(1.0, 0.0);
1164            let transformed = t.transform_direction(dir);
1165
1166            // 90 degree rotation: (1, 0) -> (0, 1)
1167            assert!(transformed.x.abs() < 0.0001);
1168            assert!((transformed.y - 1.0).abs() < 0.0001);
1169        }
1170
1171        #[test]
1172        fn test_inverse_transform_point() {
1173            let t = Transform2D::new(Vec2::new(10.0, 20.0), FRAC_PI_4, Vec2::new(2.0, 2.0));
1174
1175            let world_point = Vec2::new(5.0, 5.0);
1176            let local = t.inverse_transform_point(world_point);
1177            let back_to_world = t.transform_point(local);
1178
1179            assert!((back_to_world.x - world_point.x).abs() < 0.001);
1180            assert!((back_to_world.y - world_point.y).abs() < 0.001);
1181        }
1182
1183        #[test]
1184        fn test_inverse_transform_direction() {
1185            let t = Transform2D::from_rotation(FRAC_PI_2);
1186
1187            let world_dir = Vec2::new(1.0, 0.0);
1188            let local = t.inverse_transform_direction(world_dir);
1189            let back = t.transform_direction(local);
1190
1191            assert!((back.x - world_dir.x).abs() < 0.0001);
1192            assert!((back.y - world_dir.y).abs() < 0.0001);
1193        }
1194    }
1195
1196    // =========================================================================
1197    // Interpolation Tests
1198    // =========================================================================
1199
1200    mod interpolation_tests {
1201        use super::*;
1202
1203        #[test]
1204        fn test_lerp_position() {
1205            let a = Transform2D::from_position(Vec2::zero());
1206            let b = Transform2D::from_position(Vec2::new(10.0, 20.0));
1207
1208            let mid = a.lerp(b, 0.5);
1209            assert_eq!(mid.position, Vec2::new(5.0, 10.0));
1210        }
1211
1212        #[test]
1213        fn test_lerp_scale() {
1214            let a = Transform2D::from_scale(Vec2::one());
1215            let b = Transform2D::from_scale(Vec2::new(3.0, 3.0));
1216
1217            let mid = a.lerp(b, 0.5);
1218            assert_eq!(mid.scale, Vec2::new(2.0, 2.0));
1219        }
1220
1221        #[test]
1222        fn test_lerp_rotation() {
1223            let a = Transform2D::from_rotation(0.0);
1224            let b = Transform2D::from_rotation(FRAC_PI_2);
1225
1226            let mid = a.lerp(b, 0.5);
1227            assert!((mid.rotation - FRAC_PI_4).abs() < 0.0001);
1228        }
1229
1230        #[test]
1231        fn test_lerp_rotation_shortest_path() {
1232            // From -170 degrees to 170 degrees should go through 180, not through 0
1233            let a = Transform2D::from_rotation(-170.0_f32.to_radians());
1234            let b = Transform2D::from_rotation(170.0_f32.to_radians());
1235
1236            let mid = a.lerp(b, 0.5);
1237            // Should be close to 180 degrees (PI or -PI)
1238            assert!(mid.rotation.abs() > 3.0); // Close to PI
1239        }
1240
1241        #[test]
1242        fn test_lerp_endpoints() {
1243            let a = Transform2D::new(Vec2::zero(), 0.0, Vec2::one());
1244            let b = Transform2D::new(Vec2::new(10.0, 10.0), PI, Vec2::new(2.0, 2.0));
1245
1246            let start = a.lerp(b, 0.0);
1247            assert_eq!(start.position, a.position);
1248            assert_eq!(start.scale, a.scale);
1249
1250            let end = a.lerp(b, 1.0);
1251            assert_eq!(end.position, b.position);
1252            assert_eq!(end.scale, b.scale);
1253        }
1254    }
1255
1256    // =========================================================================
1257    // Component Trait Tests
1258    // =========================================================================
1259
1260    mod component_tests {
1261        use super::*;
1262
1263        #[test]
1264        fn test_transform2d_is_component() {
1265            fn assert_component<T: Component>() {}
1266            assert_component::<Transform2D>();
1267        }
1268
1269        #[test]
1270        fn test_transform2d_is_send() {
1271            fn assert_send<T: Send>() {}
1272            assert_send::<Transform2D>();
1273        }
1274
1275        #[test]
1276        fn test_transform2d_is_sync() {
1277            fn assert_sync<T: Sync>() {}
1278            assert_sync::<Transform2D>();
1279        }
1280
1281        #[test]
1282        fn test_transform2d_clone() {
1283            let t = Transform2D::new(Vec2::new(1.0, 2.0), FRAC_PI_4, Vec2::new(2.0, 3.0));
1284            let cloned = t.clone();
1285            assert_eq!(t, cloned);
1286        }
1287
1288        #[test]
1289        fn test_transform2d_copy() {
1290            let t = Transform2D::default();
1291            let copied = t;
1292            assert_eq!(t, copied);
1293        }
1294    }
1295
1296    // =========================================================================
1297    // FFI Layout Tests
1298    // =========================================================================
1299
1300    mod ffi_tests {
1301        use super::*;
1302        use std::mem::{align_of, size_of};
1303
1304        #[test]
1305        fn test_transform2d_size() {
1306            // Vec2 (8) + f32 (4) + Vec2 (8) = 20 bytes
1307            assert_eq!(size_of::<Transform2D>(), 20);
1308        }
1309
1310        #[test]
1311        fn test_transform2d_align() {
1312            assert_eq!(align_of::<Transform2D>(), 4); // f32 alignment
1313        }
1314
1315        #[test]
1316        fn test_mat3x3_size() {
1317            // 9 * f32 = 36 bytes
1318            assert_eq!(size_of::<Mat3x3>(), 36);
1319        }
1320
1321        #[test]
1322        fn test_mat3x3_align() {
1323            assert_eq!(align_of::<Mat3x3>(), 4);
1324        }
1325
1326        #[test]
1327        fn test_transform2d_field_layout() {
1328            let t = Transform2D::new(Vec2::new(1.0, 2.0), 3.0, Vec2::new(4.0, 5.0));
1329            let ptr = &t as *const Transform2D as *const f32;
1330            unsafe {
1331                assert_eq!(*ptr, 1.0); // position.x
1332                assert_eq!(*ptr.add(1), 2.0); // position.y
1333                assert_eq!(*ptr.add(2), 3.0); // rotation
1334                assert_eq!(*ptr.add(3), 4.0); // scale.x
1335                assert_eq!(*ptr.add(4), 5.0); // scale.y
1336            }
1337        }
1338    }
1339
1340    // =========================================================================
1341    // Utility Function Tests
1342    // =========================================================================
1343
1344    mod utility_tests {
1345        use super::*;
1346
1347        #[test]
1348        fn test_normalize_angle() {
1349            // Within range
1350            assert!((normalize_angle(0.0) - 0.0).abs() < 0.0001);
1351            assert!((normalize_angle(1.0) - 1.0).abs() < 0.0001);
1352
1353            // Above PI
1354            assert!((normalize_angle(PI + 0.5) - (-PI + 0.5)).abs() < 0.0001);
1355
1356            // Below -PI
1357            assert!((normalize_angle(-PI - 0.5) - (PI - 0.5)).abs() < 0.0001);
1358
1359            // Large positive
1360            let result = normalize_angle(3.0 * PI);
1361            assert!(result >= -PI && result < PI);
1362
1363            // Large negative
1364            let result = normalize_angle(-3.0 * PI);
1365            assert!(result >= -PI && result < PI);
1366        }
1367
1368        #[test]
1369        fn test_lerp_angle_same_direction() {
1370            let result = lerp_angle(0.0, FRAC_PI_2, 0.5);
1371            assert!((result - FRAC_PI_4).abs() < 0.0001);
1372        }
1373
1374        #[test]
1375        fn test_lerp_angle_across_boundary() {
1376            // From -170 to 170 should go through 180
1377            let from = -170.0_f32.to_radians();
1378            let to = 170.0_f32.to_radians();
1379            let mid = lerp_angle(from, to, 0.5);
1380
1381            // Should be close to 180 degrees (PI or -PI)
1382            assert!(mid.abs() > 3.0);
1383        }
1384
1385        #[test]
1386        fn test_lerp_angle_endpoints() {
1387            let from = 0.5;
1388            let to = 1.5;
1389
1390            assert!((lerp_angle(from, to, 0.0) - from).abs() < 0.0001);
1391            assert!((lerp_angle(from, to, 1.0) - to).abs() < 0.0001);
1392        }
1393    }
1394}