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}