Skip to main content

mittens_engine/engine/ecs/system/
transform_system.rs

1use crate::engine::ecs::ComponentId;
2use crate::engine::ecs::World;
3use crate::engine::ecs::component::{
4    Camera2DComponent, Camera3DComponent, CollisionComponent, RenderableComponent,
5    TransformComponent, TransformParentComponent,
6};
7use crate::engine::ecs::system::CollisionSystem;
8use crate::engine::ecs::system::System;
9use crate::engine::ecs::system::TransformStreamSystem;
10use crate::engine::graphics::VisualWorld;
11use crate::engine::graphics::primitives::TransformMatrix;
12use crate::engine::user_input::InputState;
13
14/// System responsible for
15/// syncing `TransformComponent` changes into `VisualWorld`.
16/// applying side effects to direct children of transforms
17/// and calculating world matrices for descendant transform components.
18///
19/// Key points:
20/// - A `TransformComponent` can parent other transforms to form groups.
21/// - Instances in `VisualWorld` are created per `RenderableComponent` under transforms.
22#[derive(Debug, Default)]
23pub struct TransformSystem;
24
25impl TransformSystem {
26    pub fn new() -> Self {
27        Self
28    }
29
30    fn mat4_identity() -> TransformMatrix {
31        [
32            [1.0, 0.0, 0.0, 0.0],
33            [0.0, 1.0, 0.0, 0.0],
34            [0.0, 0.0, 1.0, 0.0],
35            [0.0, 0.0, 0.0, 1.0],
36        ]
37    }
38
39    fn mat4_mul(a: TransformMatrix, b: TransformMatrix) -> TransformMatrix {
40        let mut out = [[0.0f32; 4]; 4];
41        for c in 0..4 {
42            for r in 0..4 {
43                out[c][r] =
44                    a[0][r] * b[c][0] + a[1][r] * b[c][1] + a[2][r] * b[c][2] + a[3][r] * b[c][3];
45            }
46        }
47        out
48    }
49
50    fn is_descendant_of(world: &World, mut node: ComponentId, ancestor: ComponentId) -> bool {
51        while let Some(parent) = world.parent_of(node) {
52            if parent == ancestor {
53                return true;
54            }
55            node = parent;
56        }
57        false
58    }
59
60    fn nearest_transform_self_or_ancestor(world: &World, cid: ComponentId) -> Option<ComponentId> {
61        if world
62            .get_component_by_id_as::<TransformComponent>(cid)
63            .is_some()
64        {
65            return Some(cid);
66        }
67        let mut cur = cid;
68        while let Some(parent) = world.parent_of(cur) {
69            if world
70                .get_component_by_id_as::<TransformComponent>(parent)
71                .is_some()
72            {
73                return Some(parent);
74            }
75            cur = parent;
76        }
77        None
78    }
79
80    fn propagate_subtree(
81        &mut self,
82        world: &mut World,
83        visuals: &mut VisualWorld,
84        root_node: ComponentId,
85        inherited_world: TransformMatrix,
86        transform_stream_system: &mut TransformStreamSystem,
87        camera_system: &mut crate::engine::ecs::system::CameraSystem,
88        collision_system: &mut CollisionSystem,
89    ) {
90        let mut stack: Vec<(ComponentId, TransformMatrix)> = vec![(root_node, inherited_world)];
91        while let Some((node, current_world)) = stack.pop() {
92            let stream_evaluated =
93                transform_stream_system.evaluate_stream_node(world, node, current_world);
94            let (current_world, stream_output_roots) = match stream_evaluated {
95                Some((processed_world, outputs)) => (processed_world, Some(outputs)),
96                None => (current_world, None),
97            };
98            // Camera-specific anchors own the effective cached matrix as well as the
99            // inherited basis supplied to their direct children.
100            if stream_output_roots.is_some() {
101                if let Some(t) = world.get_component_by_id_as_mut::<TransformComponent>(node) {
102                    t.transform.matrix_world = current_world;
103                }
104            }
105
106            let children: Vec<ComponentId> = match stream_output_roots {
107                Some(outputs) if !outputs.is_empty() => outputs,
108                _ => world.children_of(node).to_vec(),
109            };
110            for child in children {
111                let inherited = if let Some(tp) =
112                    world.get_component_by_id_as::<TransformParentComponent>(child)
113                {
114                    let Some(target) = tp.resolve_target_component(world) else {
115                        continue;
116                    };
117                    let Some(target_world) = Self::world_model(world, target) else {
118                        continue;
119                    };
120                    target_world
121                } else {
122                    current_world
123                };
124                let next_world = if let Some(t) =
125                    world.get_component_by_id_as_mut::<TransformComponent>(child)
126                {
127                    let w = Self::mat4_mul(inherited, t.transform.model);
128                    t.transform.matrix_world = w;
129                    w
130                } else {
131                    inherited
132                };
133
134                if world
135                    .get_component_by_id_as::<TransformComponent>(node)
136                    .is_some()
137                {
138                    if world
139                        .get_component_by_id_as::<Camera2DComponent>(child)
140                        .is_some()
141                    {
142                        camera_system
143                            .update_camera_2d_from_parent_transform(world, visuals, child, node);
144                    }
145
146                    if world
147                        .get_component_by_id_as::<Camera3DComponent>(child)
148                        .is_some()
149                    {
150                        camera_system
151                            .update_camera_3d_from_parent_transform(world, visuals, child, node);
152                    }
153
154                    if world
155                        .get_component_by_id_as::<CollisionComponent>(child)
156                        .is_some()
157                    {
158                        collision_system.update_from_transform(world, child, node);
159                    }
160                }
161
162                if let Some(handle) = world
163                    .get_component_by_id_as::<RenderableComponent>(child)
164                    .and_then(|r| r.get_handle())
165                {
166                    visuals.update_model(handle, next_world);
167                }
168
169                stack.push((child, next_world));
170            }
171        }
172    }
173
174    fn update_transform_parent_dependents(
175        &mut self,
176        world: &mut World,
177        visuals: &mut VisualWorld,
178        changed_component: ComponentId,
179        transform_stream_system: &mut TransformStreamSystem,
180        camera_system: &mut crate::engine::ecs::system::CameraSystem,
181        light_system: &mut crate::engine::ecs::system::LightSystem,
182        collision_system: &mut CollisionSystem,
183    ) {
184        let dependents: Vec<ComponentId> = world
185            .all_components()
186            .filter(|&cid| {
187                world
188                    .get_component_by_id_as::<TransformParentComponent>(cid)
189                    .is_some()
190            })
191            .filter(|&cid| !Self::is_descendant_of(world, cid, changed_component))
192            .filter(|&cid| {
193                let target_transform = world
194                    .get_component_by_id_as::<TransformParentComponent>(cid)
195                    .and_then(|tp| tp.resolve_target_component(world))
196                    .and_then(|target| Self::nearest_transform_self_or_ancestor(world, target));
197                target_transform.is_some_and(|target_transform| {
198                    target_transform == changed_component
199                        || Self::is_descendant_of(world, target_transform, changed_component)
200                })
201            })
202            .collect();
203
204        for dependent in dependents {
205            let Some(inherited_world) = world
206                .get_component_by_id_as::<TransformParentComponent>(dependent)
207                .and_then(|tp| tp.resolve_target_component(world))
208                .and_then(|target| Self::world_model(world, target))
209            else {
210                continue;
211            };
212
213            self.propagate_subtree(
214                world,
215                visuals,
216                dependent,
217                inherited_world,
218                transform_stream_system,
219                camera_system,
220                collision_system,
221            );
222
223            let child_transform_roots: Vec<ComponentId> = world
224                .children_of(dependent)
225                .iter()
226                .copied()
227                .filter(|&cid| {
228                    world
229                        .get_component_by_id_as::<TransformComponent>(cid)
230                        .is_some()
231                })
232                .collect();
233            for root in child_transform_roots {
234                light_system.transform_changed(world, visuals, root);
235            }
236        }
237    }
238
239    /// Compute the world-space model matrix for a component by walking up the component tree
240    /// and multiplying all ancestor `TransformComponent` model matrices.
241    ///
242    /// Returns `None` if there are no ancestor transforms.
243    pub fn world_model(world: &World, cid: ComponentId) -> Option<TransformMatrix> {
244        // If this node is a transform, its cached world matrix is the answer.
245        if let Some(t) = world.get_component_by_id_as::<TransformComponent>(cid) {
246            return Some(t.transform.matrix_world);
247        }
248
249        // Otherwise, return the cached world matrix of the nearest ancestor TransformComponent.
250        let mut cur = cid;
251        while let Some(parent) = world.parent_of(cur) {
252            if let Some(t) = world.get_component_by_id_as::<TransformComponent>(parent) {
253                return Some(t.transform.matrix_world);
254            }
255            cur = parent;
256        }
257        None
258    }
259
260    /// Compute the world-space position (translation) for a component.
261    pub fn world_position(world: &World, cid: ComponentId) -> Option<[f32; 3]> {
262        let model = Self::world_model(world, cid)?;
263        // Column-major translation lives in the last column.
264        let p = model[3];
265        Some([p[0], p[1], p[2]])
266    }
267
268    /// Called by TransformComponent when its values change.
269    ///
270    /// This updates camera translation if the transform has a Camera2D child, and updates
271    /// VisualWorld instance model matrices for any `RenderableComponent` descendants.
272    pub fn transform_changed(
273        &mut self,
274        world: &mut World,
275        visuals: &mut VisualWorld,
276        component: ComponentId,
277        transform_stream_system: &mut TransformStreamSystem,
278        camera_system: &mut crate::engine::ecs::system::CameraSystem,
279        light_system: &mut crate::engine::ecs::system::LightSystem,
280        collision_system: &mut CollisionSystem,
281    ) {
282        if let Some(inherited_world) = world
283            .get_component_by_id_as::<TransformParentComponent>(component)
284            .and_then(|tp| tp.resolve_target_component(world))
285            .and_then(|target| Self::world_model(world, target))
286        {
287            self.propagate_subtree(
288                world,
289                visuals,
290                component,
291                inherited_world,
292                transform_stream_system,
293                camera_system,
294                collision_system,
295            );
296            light_system.transform_changed(world, visuals, component);
297        }
298        if world
299            .get_component_by_id_as::<TransformParentComponent>(component)
300            .is_some()
301        {
302            return;
303        }
304        // Recompute cached world matrices for this transform and all descendant transforms.
305        // Then update any dependent renderables/cameras under the subtree.
306
307        // Build the chain of ancestor transforms (including `component`) from root -> leaf,
308        // stopping at any TC whose immediate non-TC ancestors include a transform-stream
309        // boundary node. Such a TC's `matrix_world` is owned by that boundary's computed
310        // basis: walking further up and recomputing from local matrices would bypass the
311        // operator and overwrite its output with incorrect values. Instead we treat that TC
312        // as the chain root and start the chain-world from its cached `matrix_world`.
313        let mut transform_chain: Vec<ComponentId> = Vec::new();
314        let mut stream_boundary = false; // true → transform_chain[0] is stream-operator-managed
315        let mut transform_parent_basis = None;
316        let mut transform_parent_boundary = false;
317        let mut cur = component;
318        'chain: loop {
319            if world
320                .get_component_by_id_as::<TransformComponent>(cur)
321                .is_some()
322            {
323                transform_chain.push(cur);
324                // Check whether this TC sits directly under a transform-stream boundary node
325                // (i.e., any non-TC node on the path to the next TC ancestor changes the
326                // inherited world basis). If so, its world is operator-managed — stop here.
327                let mut probe = cur;
328                while let Some(p) = world.parent_of(probe) {
329                    if let Some(tp) = world.get_component_by_id_as::<TransformParentComponent>(p) {
330                        transform_parent_boundary = true;
331                        transform_parent_basis = tp
332                            .resolve_target_component(world)
333                            .and_then(|target| Self::world_model(world, target));
334                        break 'chain;
335                    }
336                    if transform_stream_system.is_transform_stream_boundary(world, p) {
337                        stream_boundary = true;
338                        break 'chain;
339                    }
340                    if world
341                        .get_component_by_id_as::<TransformComponent>(p)
342                        .is_some()
343                    {
344                        break; // reached next TC ancestor without finding a stream boundary
345                    }
346                    probe = p;
347                }
348            }
349            let Some(parent) = world.parent_of(cur) else {
350                break;
351            };
352            cur = parent;
353        }
354        transform_chain.reverse();
355
356        // An unresolved follower boundary cannot safely fall back to structural ancestry:
357        // retain the follower's last effective world matrix until its target resolves.
358        if transform_parent_boundary && transform_parent_basis.is_none() {
359            return;
360        }
361
362        // Compute world matrices down the chain and write them back.
363        //
364        // If `stream_boundary` is set, transform_chain[0] is under a stream operator.
365        // Its cached `matrix_world` is stream-managed — use it as the starting world and
366        // skip recomputing it from local matrices (which would bypass the operator).
367        let (start_idx, mut chain_world) = if let Some(basis) = transform_parent_basis {
368            (0, basis)
369        } else if stream_boundary && !transform_chain.is_empty() {
370            let cached = world
371                .get_component_by_id_as::<TransformComponent>(transform_chain[0])
372                .map(|t| t.transform.matrix_world)
373                .unwrap_or_else(Self::mat4_identity);
374            (1, cached)
375        } else {
376            (0, Self::mat4_identity())
377        };
378        for tid in transform_chain[start_idx..].iter().copied() {
379            let local = match world
380                .get_component_by_id_as::<TransformComponent>(tid)
381                .map(|t| t.transform.model)
382            {
383                Some(m) => m,
384                None => continue,
385            };
386            chain_world = Self::mat4_mul(chain_world, local);
387            if let Some(t) = world.get_component_by_id_as_mut::<TransformComponent>(tid) {
388                t.transform.matrix_world = chain_world;
389            }
390        }
391
392        // Start propagation from this transform's world matrix.
393        let root_world = match world
394            .get_component_by_id_as::<TransformComponent>(component)
395            .map(|t| t.transform.matrix_world)
396        {
397            Some(m) => m,
398            None => return,
399        };
400
401        self.propagate_subtree(
402            world,
403            visuals,
404            component,
405            root_world,
406            transform_stream_system,
407            camera_system,
408            collision_system,
409        );
410
411        // If any point lights live under this transform, update their world-space position.
412        // LightSystem uses TransformSystem::world_position(), which now reads cached matrices.
413        light_system.transform_changed(world, visuals, component);
414        self.update_transform_parent_dependents(
415            world,
416            visuals,
417            component,
418            transform_stream_system,
419            camera_system,
420            light_system,
421            collision_system,
422        );
423    }
424}
425
426#[cfg(test)]
427mod tests {
428    use super::TransformSystem;
429    use crate::engine::ecs::World;
430    use crate::engine::ecs::component::{TransformComponent, TransformParentComponent};
431    use crate::engine::ecs::system::{
432        CameraSystem, CollisionSystem, LightSystem, TransformStreamSystem,
433    };
434    use crate::engine::graphics::VisualWorld;
435
436    #[test]
437    fn transform_parent_updates_cross_tree_child_when_target_changes() {
438        let mut world = World::default();
439        let mut visuals = VisualWorld::default();
440        let mut transform_system = TransformSystem::new();
441        let mut transform_stream_system = TransformStreamSystem::new();
442        let mut camera_system = CameraSystem::new();
443        let mut light_system = LightSystem::new();
444        let mut collision_system = CollisionSystem::new();
445
446        let source = world.add_component(TransformComponent::new().with_position(1.0, 0.0, 0.0));
447        let dependent_root = world.add_component(TransformComponent::new());
448        let transform_parent =
449            world.add_component(TransformParentComponent::new().with_target_source(
450                crate::engine::ecs::component::ComponentRef::Query("#source".to_string()),
451            ));
452        let child = world.add_component(TransformComponent::new().with_position(0.0, 2.0, 0.0));
453
454        world.get_component_record_mut(source).unwrap().name = "source".to_string();
455        world.add_child(dependent_root, transform_parent).unwrap();
456        world.add_child(transform_parent, child).unwrap();
457
458        transform_system.transform_changed(
459            &mut world,
460            &mut visuals,
461            source,
462            &mut transform_stream_system,
463            &mut camera_system,
464            &mut light_system,
465            &mut collision_system,
466        );
467
468        assert_eq!(
469            TransformSystem::world_position(&world, child),
470            Some([1.0, 2.0, 0.0])
471        );
472    }
473}
474
475impl System for TransformSystem {
476    fn tick(
477        &mut self,
478        _world: &mut World,
479        _visuals: &mut VisualWorld,
480        _input: &InputState,
481        _dt_sec: f32,
482    ) {
483        // No-op. Transform updates are event-driven via `transform_changed`.
484    }
485}