khora_lanes/physics_lane/
mod.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
15//! Physics Lane
16//!
17//! The physics lane is responsible for synchronizing the physics world with the ECS world.
18
19mod native_lanes;
20mod physics_debug_lane;
21
22pub use native_lanes::*;
23pub use physics_debug_lane::*;
24
25use std::collections::{HashMap, HashSet};
26
27use khora_core::ecs::entity::EntityId;
28use khora_core::physics::{ColliderDesc, PhysicsProvider, RigidBodyDesc};
29use khora_data::ecs::{Collider, GlobalTransform, Parent, RigidBody, Transform, World};
30
31/// The standard physics lane for industrial-grade simulation.
32#[derive(Debug, Default)]
33pub struct StandardPhysicsLane;
34
35impl StandardPhysicsLane {
36    /// Creates a new `StandardPhysicsLane`.
37    pub fn new() -> Self {
38        Self
39    }
40
41    /// Synchronizes components from ECS to the physics provider.
42    fn sync_to_world(&self, world: &mut World, provider: &mut dyn PhysicsProvider) {
43        let mut active_bodies = HashSet::new();
44        let mut active_colliders = HashSet::new();
45
46        // 1. Sync RigidBodies
47        let rb_map = self.sync_rigid_bodies(world, provider, &mut active_bodies);
48
49        // 2. Sync Colliders (requires hierarchy search)
50        self.sync_colliders(world, provider, &mut active_colliders, &rb_map);
51
52        // 3. Cleanup Orphaned Handles
53        self.cleanup_orphans(provider, &active_bodies, &active_colliders);
54    }
55
56    fn sync_rigid_bodies(
57        &self,
58        world: &mut World,
59        provider: &mut dyn PhysicsProvider,
60        active_bodies: &mut HashSet<khora_core::physics::RigidBodyHandle>,
61    ) -> HashMap<EntityId, khora_core::physics::RigidBodyHandle> {
62        let mut rb_map = HashMap::new();
63        let query = world.query_mut::<(EntityId, &GlobalTransform, &mut RigidBody)>();
64
65        for (entity_id, transform, rb) in query {
66            let current_pos = transform.0.translation();
67            let current_rot = transform.0.rotation();
68
69            let desc = RigidBodyDesc {
70                position: current_pos,
71                rotation: current_rot,
72                body_type: rb.body_type,
73                linear_velocity: rb.linear_velocity,
74                angular_velocity: rb.angular_velocity,
75                mass: rb.mass,
76                ccd_enabled: rb.ccd_enabled,
77            };
78
79            let handle = if let Some(handle) = rb.handle {
80                // Teleport detection
81                let (phys_pos, phys_rot) = provider.get_body_transform(handle);
82                if (phys_pos - current_pos).length_squared() > 0.0001
83                    || phys_rot.dot(current_rot).abs() < 0.9999
84                {
85                    provider.set_body_transform(handle, current_pos, current_rot);
86                }
87                provider.update_body_properties(handle, desc);
88                handle
89            } else {
90                let h = provider.add_body(desc);
91                rb.handle = Some(h);
92                h
93            };
94
95            rb_map.insert(entity_id, handle);
96            active_bodies.insert(handle);
97        }
98        rb_map
99    }
100
101    fn sync_colliders(
102        &self,
103        world: &mut World,
104        provider: &mut dyn PhysicsProvider,
105        active_colliders: &mut HashSet<khora_core::physics::ColliderHandle>,
106        rb_map: &HashMap<EntityId, khora_core::physics::RigidBodyHandle>,
107    ) {
108        // Collect a hierarchy map for efficient parent body search.
109        let mut parent_map = HashMap::new();
110        for (id, parent) in world.query::<(EntityId, &Parent)>() {
111            parent_map.insert(id, parent.0);
112        }
113
114        // Pre-collect components that might be on OTHER entities than the one carrying the collider.
115        // Specifically, we need the GlobalTransform of any entity that is a parent of a collider.
116        let mut parent_transforms = HashMap::new();
117        for (id, gt) in world.query::<(EntityId, &GlobalTransform)>() {
118            // Optimization: We only really need these for entities that are in the rb_map
119            // or are parents of colliders. For simplicity and correctness with the SoA query,
120            // we collect all, but a future optimization could filter this.
121            parent_transforms.insert(id, *gt);
122        }
123
124        // Collect optional physics state for entities with colliders.
125        let mut active_events = HashSet::new();
126        for (id, _) in world.query::<(EntityId, &khora_data::ecs::ActiveEvents)>() {
127            active_events.insert(id);
128        }
129
130        let mut materials = HashMap::new();
131        for (id, mat) in world.query::<(EntityId, &khora_data::ecs::PhysicsMaterial)>() {
132            materials.insert(id, *mat);
133        }
134
135        let query = world.query_mut::<(EntityId, &mut Collider, &GlobalTransform)>();
136        for (entity_id, collider, transform) in query {
137            let is_active = active_events.contains(&entity_id);
138            let material = materials.get(&entity_id).cloned().unwrap_or_default();
139
140            let desc = self.build_collider_desc(
141                entity_id,
142                transform,
143                collider,
144                &parent_map,
145                &parent_transforms,
146                is_active,
147                &material,
148                rb_map,
149            );
150
151            let handle = if let Some(handle) = collider.handle {
152                provider.update_collider_properties(handle, desc);
153                handle
154            } else {
155                let h = provider.add_collider(desc);
156                collider.handle = Some(h);
157                h
158            };
159
160            active_colliders.insert(handle);
161        }
162    }
163
164    #[allow(clippy::too_many_arguments)]
165    fn build_collider_desc(
166        &self,
167        entity_id: EntityId,
168        transform: &GlobalTransform,
169        collider: &Collider,
170        parent_map: &HashMap<EntityId, EntityId>,
171        parent_transforms: &HashMap<EntityId, GlobalTransform>,
172        active_events: bool,
173        material: &khora_data::ecs::PhysicsMaterial,
174        rb_map: &HashMap<EntityId, khora_core::physics::RigidBodyHandle>,
175    ) -> ColliderDesc {
176        let (parent_handle, parent_id) = self.find_parent_body(entity_id, parent_map, rb_map);
177        let mut pos = transform.0.translation();
178        let mut rot = transform.0.rotation();
179
180        // If attached to a parent body, we must use the relative transform
181        if let Some(p_id) = parent_id {
182            if p_id != entity_id {
183                if let Some(p_global) = parent_transforms.get(&p_id) {
184                    if let Some(inv_p) = p_global.0.inverse() {
185                        let local = inv_p.0 * transform.0 .0;
186                        let local_t = khora_core::math::AffineTransform(local);
187                        pos = local_t.translation();
188                        rot = local_t.rotation();
189                    }
190                }
191            }
192        }
193
194        ColliderDesc {
195            parent_body: parent_handle,
196            position: pos,
197            rotation: rot,
198            shape: collider.shape.clone(),
199            active_events,
200            friction: material.friction,
201            restitution: material.restitution,
202        }
203    }
204
205    fn find_parent_body(
206        &self,
207        entity_id: EntityId,
208        parent_map: &HashMap<EntityId, EntityId>,
209        rb_map: &HashMap<EntityId, khora_core::physics::RigidBodyHandle>,
210    ) -> (
211        Option<khora_core::physics::RigidBodyHandle>,
212        Option<EntityId>,
213    ) {
214        let mut curr = entity_id;
215        loop {
216            if let Some(h) = rb_map.get(&curr) {
217                return (Some(*h), Some(curr));
218            }
219            if let Some(p) = parent_map.get(&curr) {
220                curr = *p;
221            } else {
222                break;
223            }
224        }
225        (None, None)
226    }
227
228    fn cleanup_orphans(
229        &self,
230        provider: &mut dyn PhysicsProvider,
231        active_bodies: &HashSet<khora_core::physics::RigidBodyHandle>,
232        active_colliders: &HashSet<khora_core::physics::ColliderHandle>,
233    ) {
234        for h in provider.get_all_bodies() {
235            if !active_bodies.contains(&h) {
236                provider.remove_body(h);
237            }
238        }
239        for h in provider.get_all_colliders() {
240            if !active_colliders.contains(&h) {
241                provider.remove_collider(h);
242            }
243        }
244    }
245
246    /// Synchronizes components from the physics provider back to ECS.
247    fn sync_from_world(&self, world: &mut World, provider: &dyn PhysicsProvider) {
248        let query = world.query_mut::<(&mut Transform, &mut RigidBody)>();
249        for (transform, rb) in query {
250            if let Some(handle) = rb.handle {
251                let (pos, rot) = provider.get_body_transform(handle);
252                // Update transform
253                transform.translation = pos;
254                transform.rotation = rot;
255            }
256        }
257    }
258
259    fn resolve_characters(&self, world: &mut World, provider: &dyn PhysicsProvider) {
260        let mut results = Vec::new();
261        {
262            let query = world.query_mut::<(
263                EntityId,
264                &mut khora_data::ecs::KinematicCharacterController,
265                &Collider,
266            )>();
267            for (id, kcc, collider) in query {
268                if let Some(h) = collider.handle {
269                    let options = khora_core::physics::CharacterControllerOptions {
270                        autostep_height: kcc.autostep_height,
271                        autostep_min_width: kcc.autostep_min_width,
272                        autostep_enabled: kcc.autostep_enabled,
273                        max_slope_climb_angle: kcc.max_slope_climb_angle,
274                        min_slope_slide_angle: kcc.min_slope_slide_angle,
275                        offset: kcc.offset,
276                    };
277                    let (m, g) = provider.move_character(h, kcc.desired_translation, &options);
278                    results.push((id, m, g));
279                }
280            }
281        }
282
283        for (id, m, g) in results {
284            if let Some(kcc) = world.get_mut::<khora_data::ecs::KinematicCharacterController>(id) {
285                kcc.is_grounded = g;
286                kcc.desired_translation = khora_core::math::Vec3::ZERO;
287            }
288            if let Some(transform) = world.get_mut::<Transform>(id) {
289                transform.translation = transform.translation + m;
290            }
291        }
292    }
293
294    fn dispatch_events(&self, world: &mut World, provider: &dyn PhysicsProvider) {
295        let events = provider.get_collision_events();
296        let query = world.query_mut::<(EntityId, &mut khora_data::ecs::CollisionEvents)>();
297        for (_, buffer) in query {
298            if events.is_empty() {
299                buffer.events.clear();
300            } else {
301                buffer.events = events.clone();
302            }
303        }
304    }
305}
306
307impl khora_core::lane::Lane for StandardPhysicsLane {
308    fn strategy_name(&self) -> &'static str {
309        "StandardPhysics"
310    }
311
312    fn lane_kind(&self) -> khora_core::lane::LaneKind {
313        khora_core::lane::LaneKind::Physics
314    }
315
316    fn execute(
317        &self,
318        ctx: &mut khora_core::lane::LaneContext,
319    ) -> Result<(), khora_core::lane::LaneError> {
320        use khora_core::lane::{LaneError, Slot};
321
322        let dt = ctx
323            .get::<khora_core::lane::PhysicsDeltaTime>()
324            .ok_or(LaneError::missing("PhysicsDeltaTime"))?
325            .0;
326        let world = ctx
327            .get::<Slot<World>>()
328            .ok_or(LaneError::missing("Slot<World>"))?
329            .get();
330        let provider = ctx
331            .get::<Slot<dyn PhysicsProvider>>()
332            .ok_or(LaneError::missing("Slot<dyn PhysicsProvider>"))?
333            .get();
334
335        self.step(world, provider, dt);
336        Ok(())
337    }
338
339    fn as_any(&self) -> &dyn std::any::Any {
340        self
341    }
342
343    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
344        self
345    }
346}
347
348impl StandardPhysicsLane {
349    /// Executes the full physics step: sync, simulate, writeback, characters, events.
350    pub fn step(&self, world: &mut World, provider: &mut dyn PhysicsProvider, dt: f32) {
351        // 1. Sync ECS -> Physics World
352        self.sync_to_world(world, provider);
353
354        // 2. Simulate
355        provider.step(dt);
356
357        // 3. Sync Physics World -> ECS (Transforms)
358        self.sync_from_world(world, provider);
359
360        // 4. Kinematic Character Movement
361        self.resolve_characters(world, provider);
362
363        // 5. Collision Events
364        self.dispatch_events(world, provider);
365    }
366}