khora_lanes/scene_lane/
transform_propagation.rs

1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::collections::{HashMap, VecDeque};
16
17use khora_data::ecs::{EntityId, GlobalTransform, Parent, Transform, Without, World};
18
19/// A system that propagates local `Transform` changes through the scene hierarchy
20/// to compute the final `GlobalTransform` for each entity.
21///
22/// This system performs a Breadth-First Search (BFS) traversal of the scene hierarchy.
23/// It guarantees that parent transforms are computed before their children, ensuring
24/// correctness in a single pass.
25///
26/// It is designed to run once per frame, before the rendering and physics systems.
27pub fn transform_propagation_system(world: &mut World) {
28    // Stage 1: Initialize the work queue with all root entities.
29    // A root is an entity with a `Transform` and `GlobalTransform` but no `Parent`.
30    // Their `GlobalTransform` is calculated directly from their local `Transform`.
31    let mut queue: VecDeque<EntityId> = VecDeque::new();
32    for (id, transform, global_transform, _) in
33        world.query::<(EntityId, &Transform, &mut GlobalTransform, Without<Parent>)>()
34    {
35        global_transform.0 = transform.to_mat4();
36        queue.push_back(id);
37    }
38
39    // Stage 2: Build a map of parent -> children relationships for efficient traversal.
40    // This is done once at the beginning for performance.
41    let mut children_map: HashMap<EntityId, Vec<EntityId>> = HashMap::new();
42    for (child_id, parent) in world.query::<(EntityId, &Parent)>() {
43        children_map.entry(parent.0).or_default().push(child_id);
44    }
45
46    // Stage 3: Process the queue in a Breadth-First manner.
47    // We use a `head` index to iterate through the queue without complex borrowing issues,
48    // as we are both reading from and writing to the queue.
49    let mut head = 0;
50    while let Some(&parent_id) = queue.get(head) {
51        head += 1; // Move to the next item for the next iteration.
52
53        // If this parent has any children...
54        if let Some(children) = children_map.get(&parent_id) {
55            // Get the parent's global transform, which we know is now up-to-date.
56            // We can `unwrap` because we know the entity is valid and has this component.
57            let parent_matrix = world.get::<GlobalTransform>(parent_id).unwrap().0;
58
59            for &child_id in children {
60                // Read the child's local transform.
61                if let Some(local_transform) = world.get::<Transform>(child_id) {
62                    let child_matrix = parent_matrix * local_transform.to_mat4();
63
64                    // Update the child's `GlobalTransform` component directly.
65                    if let Some(global_transform) = world.get_mut::<GlobalTransform>(child_id) {
66                        global_transform.0 = child_matrix;
67                    }
68
69                    // Add the child to the end of the queue. Its own children will be processed
70                    // in a future iteration, after all entities at the current depth are done.
71                    queue.push_back(child_id);
72                }
73            }
74        }
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81    use khora_core::math::{Mat4, Vec3, EPSILON};
82    use khora_data::ecs::{Children, GlobalTransform, Parent, SemanticDomain, Transform, World};
83
84    /// A helper function to compare two `Mat4` matrices for approximate equality.
85    fn assert_matrix_approx_eq(a: Mat4, b: Mat4) {
86        for i in 0..4 {
87            for j in 0..4 {
88                let val_a = a.cols[i][j];
89                let val_b = b.cols[i][j];
90                assert!(
91                    (val_a - val_b).abs() < EPSILON,
92                    "Matrix mismatch at col {}, row {}: {} != {}",
93                    i,
94                    j,
95                    val_a,
96                    val_b
97                );
98            }
99        }
100    }
101
102    #[test]
103    fn test_transform_propagation_simple_hierarchy() {
104        // --- 1. ARRANGE ---
105        let mut world = World::default();
106
107        // Register all the components we will use.
108        world.register_component::<Parent>(SemanticDomain::Spatial);
109        world.register_component::<Children>(SemanticDomain::Spatial);
110        world.register_component::<Transform>(SemanticDomain::Spatial);
111        world.register_component::<GlobalTransform>(SemanticDomain::Spatial);
112
113        // Create a root entity (the parent).
114        // It's translated by 10 units on the X axis.
115        let parent_transform = Transform {
116            translation: Vec3::new(10.0, 0.0, 0.0),
117            ..Default::default()
118        };
119        let parent_id = world.spawn((parent_transform, GlobalTransform::identity()));
120
121        // Create a child entity.
122        // It's translated by 2 units on the Y axis *relative to its parent*.
123        let child_transform = Transform {
124            translation: Vec3::new(0.0, 2.0, 0.0),
125            ..Default::default()
126        };
127        let child_id = world.spawn((
128            child_transform,
129            GlobalTransform::identity(),
130            Parent(parent_id),
131        ));
132
133        // --- 2. ACT ---
134        // Run the system we want to test.
135        transform_propagation_system(&mut world);
136
137        // --- 3. ASSERT ---
138        // Get the final GlobalTransform of the child entity.
139        let child_global_transform = world
140            .get::<GlobalTransform>(child_id)
141            .expect("Child should have a GlobalTransform component");
142
143        // The expected result is the parent's translation combined with the child's.
144        let expected_matrix = Mat4::from_translation(Vec3::new(10.0, 2.0, 0.0));
145
146        assert_matrix_approx_eq(child_global_transform.0, expected_matrix);
147    }
148}