1use std::collections::{HashMap, HashSet};
37use std::sync::{Arc, Mutex};
38
39use khora_core::ecs::entity::EntityId;
40use khora_core::lane::OutputDeck;
41use khora_core::math::Vec3;
42use khora_core::physics::{
43 ColliderDesc, ColliderHandle, PhysicsProvider, RigidBodyDesc, RigidBodyHandle,
44};
45use khora_core::Runtime;
46
47use crate::ecs::{
48 ActiveEvents, Camera, Collider, DataSystemRegistration, GlobalTransform, Parent,
49 PhysicsMaterial, RigidBody, SemanticDomain, TickPhase, World,
50};
51use crate::flow::{Flow, Selection};
52use crate::register_flow;
53
54#[derive(Debug, Default, Clone)]
58pub struct PhysicsView {
59 pub active_bodies: usize,
61 pub stashed_bodies: usize,
63 pub camera_anchor: Option<Vec3>,
65}
66
67#[derive(Debug, Default, Clone, Copy)]
77pub struct PhysicsStepResult {
78 pub dt: f32,
80}
81
82#[derive(Default)]
89pub struct PhysicsFlow;
90
91impl Flow for PhysicsFlow {
92 type View = PhysicsView;
93
94 const DOMAIN: SemanticDomain = SemanticDomain::Physics;
95 const NAME: &'static str = "physics";
96
97 fn project(&self, world: &World, _sel: &Selection, _runtime: &Runtime) -> Self::View {
98 let active_bodies = world.query::<&RigidBody>().count();
99 PhysicsView {
100 active_bodies,
101 stashed_bodies: 0,
103 camera_anchor: active_camera_position(world),
104 }
105 }
106}
107
108register_flow!(PhysicsFlow);
109
110fn physics_provider_sync(world: &mut World, runtime: &Runtime, _deck: &mut OutputDeck) {
115 let Some(provider_arc) = runtime
116 .backends
117 .get::<Arc<Mutex<Box<dyn PhysicsProvider>>>>()
118 else {
119 return;
120 };
121 let provider_arc = provider_arc.clone();
122 let mut guard = match provider_arc.lock() {
123 Ok(g) => g,
124 Err(e) => {
125 log::error!("physics_provider_sync: provider mutex poisoned: {}", e);
126 return;
127 }
128 };
129 sync_to_provider(world, guard.as_mut());
130}
131
132inventory::submit! {
133 DataSystemRegistration {
134 name: "physics_provider_sync",
135 phase: TickPhase::PreExtract,
136 run: physics_provider_sync,
137 order_hint: 0,
138 runs_after: &[],
139 }
140}
141
142fn active_camera_position(world: &World) -> Option<Vec3> {
143 for (camera, transform) in world.query::<(&Camera, &GlobalTransform)>() {
144 if camera.is_active {
145 return Some(transform.0.translation());
146 }
147 }
148 None
149}
150
151fn sync_to_provider(world: &mut World, provider: &mut dyn PhysicsProvider) {
160 let mut active_bodies = HashSet::new();
161 let mut active_colliders = HashSet::new();
162
163 let rb_map = sync_rigid_bodies(world, provider, &mut active_bodies);
164 sync_colliders(world, provider, &mut active_colliders, &rb_map);
165 cleanup_orphans(provider, &active_bodies, &active_colliders);
166}
167
168fn sync_rigid_bodies(
169 world: &mut World,
170 provider: &mut dyn PhysicsProvider,
171 active_bodies: &mut HashSet<RigidBodyHandle>,
172) -> HashMap<EntityId, RigidBodyHandle> {
173 let mut rb_map = HashMap::new();
174 let query = world.query_mut::<(EntityId, &GlobalTransform, &mut RigidBody)>();
175
176 for (entity_id, transform, rb) in query {
177 let current_pos = transform.0.translation();
178 let current_rot = transform.0.rotation();
179
180 let desc = RigidBodyDesc {
181 position: current_pos,
182 rotation: current_rot,
183 body_type: rb.body_type,
184 linear_velocity: rb.linear_velocity,
185 angular_velocity: rb.angular_velocity,
186 mass: rb.mass,
187 ccd_enabled: rb.ccd_enabled,
188 };
189
190 let handle = if let Some(handle) = rb.handle {
191 let (phys_pos, phys_rot) = provider.get_body_transform(handle);
193 if (phys_pos - current_pos).length_squared() > 0.0001
194 || phys_rot.dot(current_rot).abs() < 0.9999
195 {
196 provider.set_body_transform(handle, current_pos, current_rot);
197 }
198 provider.update_body_properties(handle, desc);
199 handle
200 } else {
201 let h = provider.add_body(desc);
202 rb.handle = Some(h);
203 h
204 };
205
206 rb_map.insert(entity_id, handle);
207 active_bodies.insert(handle);
208 }
209 rb_map
210}
211
212fn sync_colliders(
213 world: &mut World,
214 provider: &mut dyn PhysicsProvider,
215 active_colliders: &mut HashSet<ColliderHandle>,
216 rb_map: &HashMap<EntityId, RigidBodyHandle>,
217) {
218 let mut parent_map = HashMap::new();
219 for (id, parent) in world.query::<(EntityId, &Parent)>() {
220 parent_map.insert(id, parent.0);
221 }
222
223 let mut parent_transforms = HashMap::new();
224 for (id, gt) in world.query::<(EntityId, &GlobalTransform)>() {
225 parent_transforms.insert(id, *gt);
226 }
227
228 let mut active_events = HashSet::new();
229 for (id, _) in world.query::<(EntityId, &ActiveEvents)>() {
230 active_events.insert(id);
231 }
232
233 let mut materials = HashMap::new();
234 for (id, mat) in world.query::<(EntityId, &PhysicsMaterial)>() {
235 materials.insert(id, *mat);
236 }
237
238 let query = world.query_mut::<(EntityId, &mut Collider, &GlobalTransform)>();
239 for (entity_id, collider, transform) in query {
240 let is_active = active_events.contains(&entity_id);
241 let material = materials.get(&entity_id).cloned().unwrap_or_default();
242
243 let desc = build_collider_desc(
244 entity_id,
245 transform,
246 collider,
247 &parent_map,
248 &parent_transforms,
249 is_active,
250 &material,
251 rb_map,
252 );
253
254 let handle = if let Some(handle) = collider.handle {
255 provider.update_collider_properties(handle, desc);
256 handle
257 } else {
258 let h = provider.add_collider(desc);
259 collider.handle = Some(h);
260 h
261 };
262
263 active_colliders.insert(handle);
264 }
265}
266
267#[allow(clippy::too_many_arguments)]
268fn build_collider_desc(
269 entity_id: EntityId,
270 transform: &GlobalTransform,
271 collider: &Collider,
272 parent_map: &HashMap<EntityId, EntityId>,
273 parent_transforms: &HashMap<EntityId, GlobalTransform>,
274 active_events: bool,
275 material: &PhysicsMaterial,
276 rb_map: &HashMap<EntityId, RigidBodyHandle>,
277) -> ColliderDesc {
278 let (parent_handle, parent_id) = find_parent_body(entity_id, parent_map, rb_map);
279 let mut pos = transform.0.translation();
280 let mut rot = transform.0.rotation();
281
282 if let Some(p_id) = parent_id {
283 if p_id != entity_id {
284 if let Some(p_global) = parent_transforms.get(&p_id) {
285 if let Some(inv_p) = p_global.0.inverse() {
286 let local = inv_p.0 * transform.0 .0;
287 let local_t = khora_core::math::AffineTransform(local);
288 pos = local_t.translation();
289 rot = local_t.rotation();
290 }
291 }
292 }
293 }
294
295 ColliderDesc {
296 parent_body: parent_handle,
297 position: pos,
298 rotation: rot,
299 shape: collider.shape.clone(),
300 active_events,
301 friction: material.friction,
302 restitution: material.restitution,
303 }
304}
305
306fn find_parent_body(
307 entity_id: EntityId,
308 parent_map: &HashMap<EntityId, EntityId>,
309 rb_map: &HashMap<EntityId, RigidBodyHandle>,
310) -> (Option<RigidBodyHandle>, Option<EntityId>) {
311 let mut curr = entity_id;
312 loop {
313 if let Some(h) = rb_map.get(&curr) {
314 return (Some(*h), Some(curr));
315 }
316 if let Some(p) = parent_map.get(&curr) {
317 curr = *p;
318 } else {
319 break;
320 }
321 }
322 (None, None)
323}
324
325fn cleanup_orphans(
326 provider: &mut dyn PhysicsProvider,
327 active_bodies: &HashSet<RigidBodyHandle>,
328 active_colliders: &HashSet<ColliderHandle>,
329) {
330 for h in provider.get_all_bodies() {
331 if !active_bodies.contains(&h) {
332 provider.remove_body(h);
333 }
334 }
335 for h in provider.get_all_colliders() {
336 if !active_colliders.contains(&h) {
337 provider.remove_collider(h);
338 }
339 }
340}