khora_data/ecs/systems/
capture_previous_transform.rs1use std::collections::HashSet;
34
35use khora_core::ecs::entity::EntityId;
36use khora_core::interpolation::{SharedTransformInterpolation, TransformInterpolation};
37
38use crate::ecs::{DataSystemRegistration, GlobalTransform, RigidBody, TickPhase, World};
39
40fn capture_previous_transform(world: &World, store: &mut TransformInterpolation) {
41 let mut live: HashSet<EntityId> = HashSet::new();
45 for (id, gt) in world.query::<(EntityId, &GlobalTransform)>() {
46 if world.get::<RigidBody>(id).is_some() {
47 store.record(id, gt.0);
48 live.insert(id);
49 }
50 }
51 store.retain_live(&live);
52}
53
54fn capture_previous_transform_entry(
58 world: &mut World,
59 runtime: &khora_core::Runtime,
60 _deck: &mut khora_core::lane::OutputDeck,
61) {
62 let Some(shared) = runtime.resources.get::<SharedTransformInterpolation>() else {
63 return;
64 };
65 let mut store = match shared.write() {
66 Ok(s) => s,
67 Err(_) => {
68 log::error!("capture_previous_transform: interpolation store lock poisoned");
69 return;
70 }
71 };
72 capture_previous_transform(world, &mut store);
73}
74
75inventory::submit! {
76 DataSystemRegistration {
77 name: "capture_previous_transform",
78 phase: TickPhase::PostSimulation,
79 run: capture_previous_transform_entry,
80 order_hint: -10,
83 runs_after: &[],
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 use crate::ecs::{GlobalTransform, RigidBody, SemanticDomain, Transform, World};
91 use khora_core::math::Vec3;
92
93 fn register(world: &mut World) {
94 world.register_component::<Transform>(SemanticDomain::Spatial);
95 world.register_component::<GlobalTransform>(SemanticDomain::Spatial);
96 world.register_component::<RigidBody>(SemanticDomain::Physics);
97 }
98
99 #[test]
100 fn records_previous_for_rigid_body() {
101 let mut world = World::default();
102 register(&mut world);
103 let mut store = TransformInterpolation::new();
104
105 let body = world.spawn((
106 GlobalTransform::at_position(Vec3::new(1.0, 2.0, 3.0)),
107 RigidBody::default(),
108 ));
109
110 assert!(store.previous(body).is_none());
111 capture_previous_transform(&world, &mut store);
112
113 assert_eq!(
114 store.previous(body).map(|t| t.translation()),
115 Some(Vec3::new(1.0, 2.0, 3.0)),
116 "a simulated body's transform should be snapshotted"
117 );
118 }
119
120 #[test]
121 fn updates_previous_to_current_each_pass() {
122 let mut world = World::default();
123 register(&mut world);
124 let mut store = TransformInterpolation::new();
125
126 let body = world.spawn((
127 GlobalTransform::at_position(Vec3::new(0.0, 0.0, 0.0)),
128 RigidBody::default(),
129 ));
130 capture_previous_transform(&world, &mut store);
131
132 if let Some(gt) = world.get_mut::<GlobalTransform>(body) {
135 *gt = GlobalTransform::at_position(Vec3::new(5.0, 0.0, 0.0));
136 }
137 capture_previous_transform(&world, &mut store);
138
139 assert_eq!(
140 store.previous(body).map(|t| t.translation()),
141 Some(Vec3::new(5.0, 0.0, 0.0))
142 );
143 }
144
145 #[test]
146 fn static_entity_without_rigid_body_is_not_snapshotted() {
147 let mut world = World::default();
148 register(&mut world);
149 let mut store = TransformInterpolation::new();
150
151 let stat = world.spawn(GlobalTransform::at_position(Vec3::new(1.0, 1.0, 1.0)));
152 capture_previous_transform(&world, &mut store);
153
154 assert!(
155 store.previous(stat).is_none(),
156 "a static entity must not be snapshotted"
157 );
158 }
159
160 #[test]
161 fn despawned_body_is_pruned_next_pass() {
162 let mut world = World::default();
163 register(&mut world);
164 let mut store = TransformInterpolation::new();
165
166 let body = world.spawn((
167 GlobalTransform::at_position(Vec3::new(2.0, 0.0, 0.0)),
168 RigidBody::default(),
169 ));
170 capture_previous_transform(&world, &mut store);
171 assert!(store.previous(body).is_some());
172
173 world.despawn(body);
174 capture_previous_transform(&world, &mut store);
175 assert!(
176 store.previous(body).is_none(),
177 "a despawned body's snapshot must be pruned"
178 );
179 assert!(store.is_empty());
180 }
181}