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}