goud_engine/ecs/systems/
transform.rs1use 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#[derive(Debug, Default, Clone)]
37pub struct TransformPropagationSystem {
38 _marker: std::marker::PhantomData<()>,
39}
40
41impl TransformPropagationSystem {
42 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 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 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 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 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 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 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 system.run(&mut world);
148 }
149
150 #[test]
151 fn test_3d_propagation_multi_level_hierarchy() {
152 let mut world = World::new();
153
154 let grandparent = spawn_3d(&mut world, Vec3::new(10.0, 0.0, 0.0));
156
157 let parent = spawn_3d(&mut world, Vec3::new(5.0, 0.0, 0.0));
159 set_parent(&mut world, grandparent, parent);
160
161 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 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 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 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 let parent = spawn_2d(&mut world, Vec2::new(100.0, 50.0));
199
200 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 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 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 let g2d = world.get::<GlobalTransform2D>(child_2d).unwrap();
243 assert!((g2d.translation().x - 15.0).abs() < 0.001);
244
245 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}