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