khora_data/ecs/systems/
transform_propagation.rs1use std::collections::{HashMap, VecDeque};
20
21use khora_core::{ecs::entity::EntityId, math::Mat4};
22
23use crate::ecs::{
24 DataSystemRegistration, GlobalTransform, Parent, TickPhase, Transform, Without, World,
25};
26
27pub fn transform_propagation_system(world: &mut World) {
33 let mut queue: VecDeque<EntityId> = VecDeque::new();
36 for (id, transform, global_transform, _) in
37 world.query::<(EntityId, &Transform, &mut GlobalTransform, Without<Parent>)>()
38 {
39 global_transform.0 = transform.to_mat4().into();
40 queue.push_back(id);
41 }
42
43 let mut children_map: HashMap<EntityId, Vec<EntityId>> = HashMap::new();
45 for (child_id, parent) in world.query::<(EntityId, &Parent)>() {
46 children_map.entry(parent.0).or_default().push(child_id);
47 }
48
49 let mut head = 0;
55 while let Some(&parent_id) = queue.get(head) {
56 head += 1;
57
58 let Some(children) = children_map.get(&parent_id) else {
59 continue;
60 };
61 let Some(parent_global) = world.get::<GlobalTransform>(parent_id) else {
62 continue;
63 };
64 let parent_matrix = parent_global.0;
65
66 for &child_id in children {
67 let Some(local_transform) = world.get::<Transform>(child_id) else {
68 continue;
69 };
70 let child_matrix = Mat4::from(parent_matrix) * local_transform.to_mat4();
71 if let Some(global_transform) = world.get_mut::<GlobalTransform>(child_id) {
75 global_transform.0 = child_matrix.into();
76 queue.push_back(child_id);
77 }
78 }
79 }
80}
81
82fn transform_propagation_entry(
87 world: &mut World,
88 _runtime: &khora_core::Runtime,
89 _deck: &mut khora_core::lane::OutputDeck,
90) {
91 transform_propagation_system(world);
92}
93
94inventory::submit! {
95 DataSystemRegistration {
96 name: "transform_propagation",
97 phase: TickPhase::PostSimulation,
98 run: transform_propagation_entry,
99 order_hint: 0,
100 runs_after: &[],
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 use crate::ecs::{Children, GlobalTransform, Parent, SemanticDomain, Transform, World};
108 use khora_core::math::{Mat4, Vec3, EPSILON};
109
110 fn assert_matrix_approx_eq(a: Mat4, b: Mat4) {
111 for i in 0..4 {
112 for j in 0..4 {
113 let val_a = a.cols[i][j];
114 let val_b = b.cols[i][j];
115 assert!(
116 (val_a - val_b).abs() < EPSILON,
117 "Matrix mismatch at col {}, row {}: {} != {}",
118 i,
119 j,
120 val_a,
121 val_b
122 );
123 }
124 }
125 }
126
127 #[test]
128 fn test_transform_propagation_simple_hierarchy() {
129 let mut world = World::default();
130
131 world.register_component::<Parent>(SemanticDomain::Spatial);
132 world.register_component::<Children>(SemanticDomain::Spatial);
133 world.register_component::<Transform>(SemanticDomain::Spatial);
134 world.register_component::<GlobalTransform>(SemanticDomain::Spatial);
135
136 let parent_transform = Transform {
137 translation: Vec3::new(10.0, 0.0, 0.0),
138 ..Default::default()
139 };
140 let parent_id = world.spawn((parent_transform, GlobalTransform::identity()));
141
142 let child_transform = Transform {
143 translation: Vec3::new(0.0, 2.0, 0.0),
144 ..Default::default()
145 };
146 let child_id = world.spawn((
147 child_transform,
148 GlobalTransform::identity(),
149 Parent(parent_id),
150 ));
151
152 transform_propagation_system(&mut world);
153
154 let child_global_transform = world
155 .get::<GlobalTransform>(child_id)
156 .expect("Child should have a GlobalTransform component");
157
158 let expected_matrix = Mat4::from_translation(Vec3::new(10.0, 2.0, 0.0));
159 assert_matrix_approx_eq(child_global_transform.0.into(), expected_matrix);
160 }
161}