Skip to main content

goud_engine/ecs/systems/
transform.rs

1//! Transform propagation system.
2//!
3//! This module provides systems for propagating transform changes through entity hierarchies.
4//! It handles both 2D and 3D transform propagation in a single system.
5//!
6//! # Example
7//!
8//! ```rust,ignore
9//! use goud_engine::ecs::systems::TransformPropagationSystem;
10//! use goud_engine::ecs::system::System;
11//! use goud_engine::ecs::World;
12//!
13//! let mut system = TransformPropagationSystem::new();
14//! system.run(&mut world);
15//! ```
16
17use crate::ecs::component::ComponentId;
18use crate::ecs::components::global_transform::GlobalTransform;
19use crate::ecs::components::global_transform2d::GlobalTransform2D;
20use crate::ecs::components::hierarchy::{Children, Parent};
21use crate::ecs::components::propagation::{propagate_transforms, propagate_transforms_2d};
22use crate::ecs::components::transform::Transform;
23use crate::ecs::components::transform2d::Transform2D;
24use crate::ecs::query::Access;
25use crate::ecs::system::System;
26use crate::ecs::World;
27
28/// System for propagating transform changes through entity hierarchies.
29///
30/// This system:
31/// - Runs 2D transform propagation (Transform2D -> GlobalTransform2D)
32/// - Runs 3D transform propagation (Transform -> GlobalTransform)
33/// - Traverses parent-child relationships to compute world-space transforms
34///
35/// Both 2D and 3D propagation run independently in each execution.
36#[derive(Debug, Default, Clone)]
37pub struct TransformPropagationSystem {
38    _marker: std::marker::PhantomData<()>,
39}
40
41impl TransformPropagationSystem {
42    /// Creates a new transform propagation system.
43    pub fn new() -> Self {
44        Self::default()
45    }
46}
47
48impl System for TransformPropagationSystem {
49    fn name(&self) -> &'static str {
50        "TransformPropagationSystem"
51    }
52
53    fn component_access(&self) -> Access {
54        let mut access = Access::new();
55
56        // Reads: local transforms and hierarchy
57        access.add_read(ComponentId::of::<Transform>());
58        access.add_read(ComponentId::of::<Transform2D>());
59        access.add_read(ComponentId::of::<Parent>());
60        access.add_read(ComponentId::of::<Children>());
61
62        // Writes: global transforms
63        access.add_write(ComponentId::of::<GlobalTransform>());
64        access.add_write(ComponentId::of::<GlobalTransform2D>());
65
66        access
67    }
68
69    fn run(&mut self, world: &mut World) {
70        propagate_transforms_2d(world);
71        propagate_transforms(world);
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use crate::core::math::{Vec2, Vec3};
79
80    /// Helper: set up a 3D entity with Transform + GlobalTransform.
81    fn spawn_3d(world: &mut World, pos: Vec3) -> crate::ecs::entity::Entity {
82        let entity = world.spawn_empty();
83        world.insert(entity, Transform::from_position(pos));
84        world.insert(entity, GlobalTransform::IDENTITY);
85        entity
86    }
87
88    /// Helper: set up a 2D entity with Transform2D + GlobalTransform2D.
89    fn spawn_2d(world: &mut World, pos: Vec2) -> crate::ecs::entity::Entity {
90        let entity = world.spawn_empty();
91        world.insert(entity, Transform2D::from_position(pos));
92        world.insert(entity, GlobalTransform2D::IDENTITY);
93        entity
94    }
95
96    /// Helper: establish parent-child relationship.
97    fn set_parent(
98        world: &mut World,
99        parent: crate::ecs::entity::Entity,
100        child: crate::ecs::entity::Entity,
101    ) {
102        world.insert(child, Parent::new(parent));
103        if let Some(existing) = world.get::<Children>(parent) {
104            let mut children = existing.clone();
105            children.push(child);
106            world.insert(parent, children);
107        } else {
108            let mut children = Children::new();
109            children.push(child);
110            world.insert(parent, children);
111        }
112    }
113
114    #[test]
115    fn test_system_name() {
116        let system = TransformPropagationSystem::new();
117        assert_eq!(system.name(), "TransformPropagationSystem");
118    }
119
120    #[test]
121    fn test_component_access_declares_reads() {
122        let system = TransformPropagationSystem::new();
123        let access = system.component_access();
124
125        // Verify we're not read-only (we write global transforms)
126        assert!(
127            !access.is_read_only(),
128            "System writes GlobalTransform/GlobalTransform2D, should not be read-only"
129        );
130    }
131
132    #[test]
133    fn test_component_access_is_not_empty() {
134        let system = TransformPropagationSystem::new();
135        let access = system.component_access();
136        assert!(
137            !access.is_empty(),
138            "System should declare non-empty component access"
139        );
140    }
141
142    #[test]
143    fn test_run_on_empty_world() {
144        let mut world = World::new();
145        let mut system = TransformPropagationSystem::new();
146        // Should not panic
147        system.run(&mut world);
148    }
149
150    #[test]
151    fn test_3d_propagation_multi_level_hierarchy() {
152        let mut world = World::new();
153
154        // grandparent at (10, 0, 0)
155        let grandparent = spawn_3d(&mut world, Vec3::new(10.0, 0.0, 0.0));
156
157        // parent at local (5, 0, 0) -> global (15, 0, 0)
158        let parent = spawn_3d(&mut world, Vec3::new(5.0, 0.0, 0.0));
159        set_parent(&mut world, grandparent, parent);
160
161        // child at local (3, 0, 0) -> global (18, 0, 0)
162        let child = spawn_3d(&mut world, Vec3::new(3.0, 0.0, 0.0));
163        set_parent(&mut world, parent, child);
164
165        let mut system = TransformPropagationSystem::new();
166        system.run(&mut world);
167
168        // Verify grandparent global = local
169        let gp_global = world.get::<GlobalTransform>(grandparent).unwrap();
170        assert!(
171            (gp_global.translation().x - 10.0).abs() < 0.001,
172            "Grandparent global x should be 10.0, got {}",
173            gp_global.translation().x
174        );
175
176        // Verify parent global = grandparent + local
177        let p_global = world.get::<GlobalTransform>(parent).unwrap();
178        assert!(
179            (p_global.translation().x - 15.0).abs() < 0.001,
180            "Parent global x should be 15.0, got {}",
181            p_global.translation().x
182        );
183
184        // Verify child global = grandparent + parent + local
185        let c_global = world.get::<GlobalTransform>(child).unwrap();
186        assert!(
187            (c_global.translation().x - 18.0).abs() < 0.001,
188            "Child global x should be 18.0, got {}",
189            c_global.translation().x
190        );
191    }
192
193    #[test]
194    fn test_2d_propagation_parent_child() {
195        let mut world = World::new();
196
197        // parent at (100, 50)
198        let parent = spawn_2d(&mut world, Vec2::new(100.0, 50.0));
199
200        // child at local (20, 10) -> global (120, 60)
201        let child = spawn_2d(&mut world, Vec2::new(20.0, 10.0));
202        set_parent(&mut world, parent, child);
203
204        let mut system = TransformPropagationSystem::new();
205        system.run(&mut world);
206
207        let parent_global = world.get::<GlobalTransform2D>(parent).unwrap();
208        assert!((parent_global.translation().x - 100.0).abs() < 0.001);
209        assert!((parent_global.translation().y - 50.0).abs() < 0.001);
210
211        let child_global = world.get::<GlobalTransform2D>(child).unwrap();
212        assert!(
213            (child_global.translation().x - 120.0).abs() < 0.001,
214            "Child 2D global x should be 120.0, got {}",
215            child_global.translation().x
216        );
217        assert!(
218            (child_global.translation().y - 60.0).abs() < 0.001,
219            "Child 2D global y should be 60.0, got {}",
220            child_global.translation().y
221        );
222    }
223
224    #[test]
225    fn test_both_2d_and_3d_propagate_in_same_run() {
226        let mut world = World::new();
227
228        // 2D hierarchy
229        let parent_2d = spawn_2d(&mut world, Vec2::new(10.0, 0.0));
230        let child_2d = spawn_2d(&mut world, Vec2::new(5.0, 0.0));
231        set_parent(&mut world, parent_2d, child_2d);
232
233        // 3D hierarchy
234        let parent_3d = spawn_3d(&mut world, Vec3::new(20.0, 0.0, 0.0));
235        let child_3d = spawn_3d(&mut world, Vec3::new(7.0, 0.0, 0.0));
236        set_parent(&mut world, parent_3d, child_3d);
237
238        let mut system = TransformPropagationSystem::new();
239        system.run(&mut world);
240
241        // 2D child should be (15, 0)
242        let g2d = world.get::<GlobalTransform2D>(child_2d).unwrap();
243        assert!((g2d.translation().x - 15.0).abs() < 0.001);
244
245        // 3D child should be (27, 0, 0)
246        let g3d = world.get::<GlobalTransform>(child_3d).unwrap();
247        assert!((g3d.translation().x - 27.0).abs() < 0.001);
248    }
249
250    #[test]
251    fn test_should_run_returns_true() {
252        let system = TransformPropagationSystem::new();
253        let world = World::new();
254        assert!(system.should_run(&world));
255    }
256
257    #[test]
258    fn test_system_is_not_read_only() {
259        let system = TransformPropagationSystem::new();
260        assert!(
261            !system.is_read_only(),
262            "TransformPropagationSystem writes global transforms"
263        );
264    }
265
266    #[test]
267    fn test_3d_root_entity_global_equals_local() {
268        let mut world = World::new();
269        let root = spawn_3d(&mut world, Vec3::new(42.0, 13.0, 7.0));
270
271        let mut system = TransformPropagationSystem::new();
272        system.run(&mut world);
273
274        let global = world.get::<GlobalTransform>(root).unwrap();
275        assert!((global.translation().x - 42.0).abs() < 0.001);
276        assert!((global.translation().y - 13.0).abs() < 0.001);
277        assert!((global.translation().z - 7.0).abs() < 0.001);
278    }
279}