Skip to main content

euv_engine/physics/
impl.rs

1use super::*;
2
3/// Implements `Default` for `BodyCollider`, returning an AABB collider with default values.
4impl Default for BodyCollider {
5    /// Constructs a default [`BodyCollider`] value.
6    ///
7    /// # Returns
8    ///
9    /// - `BodyCollider` - A default-constructed instance with the documented initial state.
10    fn default() -> BodyCollider {
11        BodyCollider::Aabb(AabbCollider::default())
12    }
13}
14
15/// Implements `Default` for `BodyCollider3D`, returning a 3D AABB collider with default values.
16impl Default for BodyCollider3D {
17    /// Constructs a default [`BodyCollider3D`] value.
18    ///
19    /// # Returns
20    ///
21    /// - `BodyCollider3D` - A default-constructed instance with the documented initial state.
22    fn default() -> BodyCollider3D {
23        BodyCollider3D::Aabb(AabbCollider3D::default())
24    }
25}
26
27/// Implements default configuration for `PhysicsConfig`.
28impl Default for PhysicsConfig {
29    /// Constructs a default [`PhysicsConfig`] value.
30    ///
31    /// # Returns
32    ///
33    /// - `PhysicsConfig` - A default-constructed instance with the documented initial state.
34    fn default() -> PhysicsConfig {
35        PhysicsConfig::new(
36            Vector2D::new(0.0, DEFAULT_GRAVITY),
37            DEFAULT_LINEAR_DAMPING,
38            DEFAULT_ANGULAR_DAMPING,
39        )
40    }
41}
42
43/// Implements body creation and force management for `RigidBody2D`.
44impl RigidBody2D {
45    /// Creates a new dynamic rigid body with default mass and the given position.
46    ///
47    /// # Arguments
48    ///
49    /// - `u64` - The unique ID.
50    /// - `Vector2D` - The initial position.
51    ///
52    /// # Returns
53    ///
54    /// - `RigidBody2D` - The new body.
55    pub fn new_dynamic(id: u64, position: Vector2D) -> RigidBody2D {
56        let mass: f64 = PHYSICS_DEFAULT_MASS;
57        RigidBody2D::new(
58            id,
59            position,
60            mass,
61            1.0 / mass,
62            DEFAULT_RESTITUTION,
63            DEFAULT_FRICTION,
64            BodyType::Dynamic,
65        )
66    }
67
68    /// Creates a new static rigid body at the given position with infinite mass.
69    ///
70    /// # Arguments
71    ///
72    /// - `u64` - The unique ID.
73    /// - `Vector2D` - The position.
74    ///
75    /// # Returns
76    ///
77    /// - `RigidBody2D` - The new static body.
78    pub fn new_static(id: u64, position: Vector2D) -> RigidBody2D {
79        RigidBody2D::new(
80            id,
81            position,
82            PHYSICS_STATIC_MASS,
83            0.0,
84            DEFAULT_RESTITUTION,
85            DEFAULT_FRICTION,
86            BodyType::Static,
87        )
88    }
89
90    /// Applies a force to the body's force accumulator.
91    ///
92    /// # Arguments
93    ///
94    /// - `Vector2D` - The force vector.
95    pub fn apply_force(&mut self, force: Vector2D) {
96        *self.get_mut_force_accumulator() += force;
97    }
98
99    /// Applies an instantaneous impulse, directly changing velocity.
100    ///
101    /// # Arguments
102    ///
103    /// - `Vector2D` - The impulse vector.
104    pub fn apply_impulse(&mut self, impulse: Vector2D) {
105        let inverse_mass: f64 = self.get_inverse_mass();
106        if inverse_mass == 0.0 {
107            return;
108        }
109        *self.get_mut_velocity() += impulse.scaled(inverse_mass);
110    }
111
112    /// Sets the mass of the body, updating the inverse mass.
113    /// A mass of 0 makes the body static (infinite mass).
114    ///
115    /// # Arguments
116    ///
117    /// - `f64` - The new mass.
118    pub fn update_mass(&mut self, mass: f64) {
119        self.set_mass(mass);
120        self.set_inverse_mass(if mass > 0.0 { 1.0 / mass } else { 0.0 });
121    }
122
123    /// Returns `true` if this body is affected by forces and collisions.
124    ///
125    /// # Returns
126    ///
127    /// - `bool` - True if the body is dynamic.
128    pub fn is_dynamic(&self) -> bool {
129        self.get_body_type() == BodyType::Dynamic
130    }
131
132    /// Attaches a collider shape to this body.
133    ///
134    /// # Arguments
135    ///
136    /// - `BodyCollider` - The collider to attach.
137    pub fn update_collider(&mut self, collider: BodyCollider) {
138        self.set_collider(Some(collider));
139    }
140
141    /// Returns the world-space bounding box of the attached collider, if any.
142    ///
143    /// # Returns
144    ///
145    /// - `Option<Rect>` - The bounding box, or `None` if no collider is attached.
146    pub fn bounding_box(&self) -> Option<Rect> {
147        let collider: Option<BodyCollider> = self.get_collider();
148        match collider? {
149            BodyCollider::Aabb(aabb) => {
150                let aabb_rect: Rect = aabb.get_rect();
151                let mut offset_rect: Rect = aabb_rect;
152                offset_rect.set_x(
153                    offset_rect.get_x() + self.get_position().get_x() - aabb_rect.get_width() * 0.5,
154                );
155                offset_rect.set_y(
156                    offset_rect.get_y() + self.get_position().get_y()
157                        - aabb_rect.get_height() * 0.5,
158                );
159                Some(offset_rect)
160            }
161            BodyCollider::Circle(circle) => {
162                let diameter: f64 = circle.get_circle().get_radius() * 2.0;
163                Some(Rect::from_center(self.get_position(), diameter, diameter))
164            }
165        }
166    }
167}
168
169/// Implements body management and simulation for `PhysicsWorld2D`.
170impl PhysicsWorld2D {
171    /// Creates a new physics world with the given configuration.
172    ///
173    /// # Arguments
174    ///
175    /// - `PhysicsConfig` - The simulation configuration.
176    ///
177    /// # Returns
178    ///
179    /// - `PhysicsWorld2D` - The new world.
180    pub fn with_config(config: PhysicsConfig) -> PhysicsWorld2D {
181        let mut world: PhysicsWorld2D = PhysicsWorld2D::new(config);
182        world.set_grid(SpatialHashGrid2D::with_default_size());
183        world
184    }
185
186    /// Adds a rigid body to the world.
187    ///
188    /// # Arguments
189    ///
190    /// - `RigidBody2D` - The body to add.
191    pub fn add_body(&mut self, body: RigidBody2D) {
192        self.get_mut_bodies().push(body);
193    }
194
195    /// Removes the body with the given ID.
196    ///
197    /// # Arguments
198    ///
199    /// - `u64` - The ID of the body to remove.
200    pub fn remove_body(&mut self, id: u64) {
201        self.get_mut_bodies()
202            .retain(|body: &RigidBody2D| body.get_id() != id);
203    }
204
205    /// Returns a reference to the body with the given ID.
206    ///
207    /// # Arguments
208    ///
209    /// - `u64` - The body ID.
210    ///
211    /// # Returns
212    ///
213    /// - `Option<&RigidBody2D>` - The body reference, if found.
214    pub fn get_body(&self, id: u64) -> Option<&RigidBody2D> {
215        self.get_bodies()
216            .iter()
217            .find(|body: &&RigidBody2D| body.get_id() == id)
218    }
219
220    /// Returns a mutable reference to the body with the given ID.
221    ///
222    /// # Arguments
223    ///
224    /// - `u64` - The body ID.
225    ///
226    /// # Returns
227    ///
228    /// - `Option<&mut RigidBody2D>` - The mutable body reference, if found.
229    pub fn get_body_mut(&mut self, id: u64) -> Option<&mut RigidBody2D> {
230        self.get_mut_bodies()
231            .iter_mut()
232            .find(|body: &&mut RigidBody2D| body.get_id() == id)
233    }
234}
235
236/// Implements `Default` for `PhysicsWorld2D` as an empty world.
237impl Default for PhysicsWorld2D {
238    /// Constructs a default [`PhysicsWorld2D`] value.
239    ///
240    /// # Returns
241    ///
242    /// - `PhysicsWorld2D` - A default-constructed instance with the documented initial state.
243    fn default() -> PhysicsWorld2D {
244        PhysicsWorld2D::with_config(PhysicsConfig::default())
245    }
246}
247
248/// Implements collision detection and resolution for `RigidBody2D`.
249impl RigidBody2D {
250    /// Checks collision with another body based on both bodies' collider shapes.
251    ///
252    /// # Arguments
253    ///
254    /// - `&RigidBody2D` - The other body to check against.
255    ///
256    /// # Returns
257    ///
258    /// - `Option<CollisionResult>` - The collision result, or `None`.
259    fn check_collision_with(&self, other: &RigidBody2D) -> Option<CollisionResult> {
260        let a_bbox: Rect = self.bounding_box()?;
261        let b_bbox: Rect = other.bounding_box()?;
262        if !Rect::broad_phase_alias(a_bbox, b_bbox) {
263            return None;
264        }
265        let self_collider: Option<BodyCollider> = self.get_collider();
266        let other_collider: Option<BodyCollider> = other.get_collider();
267        let position_delta: Vector2D = other.get_position() - self.get_position();
268        match (self_collider, other_collider) {
269            (Some(BodyCollider::Aabb(aabb_a)), Some(BodyCollider::Aabb(aabb_b))) => {
270                let aabb_b_rect: Rect = aabb_b.get_rect();
271                let offset_aabb_b: AabbCollider = AabbCollider::new(Rect::new(
272                    aabb_b_rect.get_x() + position_delta.get_x(),
273                    aabb_b_rect.get_y() + position_delta.get_y(),
274                    aabb_b_rect.get_width(),
275                    aabb_b_rect.get_height(),
276                ));
277                aabb_a.collide_with_aabb(&offset_aabb_b)
278            }
279            (Some(BodyCollider::Circle(circle_a)), Some(BodyCollider::Circle(circle_b))) => {
280                let circle_b_inner: Circle = circle_b.get_circle();
281                let offset_circle_b: CircleCollider = CircleCollider::new(Circle::new(
282                    circle_b_inner.get_center() + position_delta,
283                    circle_b_inner.get_radius(),
284                ));
285                circle_a.collide_with_circle(&offset_circle_b)
286            }
287            (Some(BodyCollider::Aabb(aabb)), Some(BodyCollider::Circle(circle))) => {
288                let circle_inner: Circle = circle.get_circle();
289                let offset_circle: CircleCollider = CircleCollider::new(Circle::new(
290                    circle_inner.get_center() + position_delta,
291                    circle_inner.get_radius(),
292                ));
293                aabb.collide_with_circle(&offset_circle)
294            }
295            (Some(BodyCollider::Circle(circle)), Some(BodyCollider::Aabb(aabb))) => {
296                let aabb_rect: Rect = aabb.get_rect();
297                let offset_aabb: AabbCollider = AabbCollider::new(Rect::new(
298                    aabb_rect.get_x() + position_delta.get_x(),
299                    aabb_rect.get_y() + position_delta.get_y(),
300                    aabb_rect.get_width(),
301                    aabb_rect.get_height(),
302                ));
303                offset_aabb
304                    .collide_with_circle(&circle)
305                    .map(|mut result: CollisionResult| {
306                        result.set_normal(-result.get_normal());
307                        result
308                    })
309            }
310            _ => None,
311        }
312    }
313
314    /// Resolves a collision with another body using impulse-based response
315    /// and position correction.
316    ///
317    /// # Arguments
318    ///
319    /// - `&mut RigidBody2D` - The other body involved in the collision.
320    /// - `&CollisionResult` - The collision data.
321    fn resolve_collision_with(&mut self, other: &mut RigidBody2D, result: &CollisionResult) {
322        let self_inverse_mass: f64 = self.get_inverse_mass();
323        let other_inverse_mass: f64 = other.get_inverse_mass();
324        let relative_velocity: Vector2D = other.get_velocity() - self.get_velocity();
325        let velocity_along_normal: f64 = relative_velocity.dot(result.get_normal());
326        if velocity_along_normal > 0.0 {
327            return;
328        }
329        let restitution: f64 = self.get_restitution().min(other.get_restitution());
330        let inverse_mass_sum: f64 = self_inverse_mass + other_inverse_mass;
331        if inverse_mass_sum == 0.0 {
332            return;
333        }
334        let impulse_magnitude: f64 =
335            -(1.0 + restitution) * velocity_along_normal / inverse_mass_sum;
336        let impulse: Vector2D = result.get_normal().scaled(impulse_magnitude);
337        *self.get_mut_velocity() -= impulse.scaled(self_inverse_mass);
338        *other.get_mut_velocity() += impulse.scaled(other_inverse_mass);
339        let correction: Vector2D = result
340            .get_normal()
341            .scaled((result.get_depth() * PHYSICS_POSITION_PERCENT / inverse_mass_sum).max(0.0));
342        *self.get_mut_position() -= correction.scaled(self_inverse_mass);
343        *other.get_mut_position() += correction.scaled(other_inverse_mass);
344    }
345}
346
347/// Implements simulation stepping and collision resolution for `PhysicsWorld2D`.
348impl PhysicsWorld2D {
349    /// Performs one physics simulation step using semi-implicit Euler integration.
350    ///
351    /// Applies gravity to dynamic bodies, integrates velocity from accumulated forces,
352    /// applies damping, integrates position, and resolves collisions.
353    ///
354    /// # Arguments
355    ///
356    /// - `f64` - The fixed delta time in seconds.
357    pub fn step(&mut self, delta_time: f64) {
358        let config: PhysicsConfig = self.get_config();
359        // Hoist loop-invariant damping factors out of the per-body loop.
360        let damping_factor: f64 = (1.0 - config.get_linear_damping() * delta_time).max(0.0);
361        let angular_damping: f64 = (1.0 - config.get_angular_damping() * delta_time).max(0.0);
362        let gravity: Vector2D = config.get_gravity();
363        for body in self.get_mut_bodies() {
364            if !body.is_dynamic() {
365                continue;
366            }
367            let body_mass: f64 = body.get_mass();
368            let body_inverse_mass: f64 = body.get_inverse_mass();
369            *body.get_mut_force_accumulator() += gravity.scaled(body_mass);
370            let force: Vector2D = body.get_force_accumulator();
371            *body.get_mut_velocity() += force.scaled(body_inverse_mass * delta_time);
372            // In-place damping and integration avoid temporary vector copies.
373            *body.get_mut_velocity() *= damping_factor;
374            let current_velocity: Vector2D = body.get_velocity();
375            *body.get_mut_position() += current_velocity.scaled(delta_time);
376            body.set_force_accumulator(Vector2D::zero());
377            *body.get_mut_angular_velocity() *= angular_damping;
378            let current_angular_velocity: f64 = body.get_angular_velocity();
379            *body.get_mut_rotation() += current_angular_velocity * delta_time;
380        }
381        self.resolve_collisions();
382    }
383
384    /// Detects and resolves all collisions between bodies in the world.
385    ///
386    /// Uses a spatial hash grid for broad-phase culling followed by narrow-phase
387    /// shape-specific collision detection, then applies impulse-based resolution.
388    /// This reduces the broad-phase from O(n²) to near O(n) for typical scenes.
389    fn resolve_collisions(&mut self) {
390        let body_count: usize = self.get_bodies().len();
391        if body_count < 2 {
392            return;
393        }
394        // Rebuild the persistent grid once per step and collect the candidate
395        // pair list once; every solver iteration then reuses both (the grid is
396        // unchanged between iterations), eliminating per-iteration re-queries and
397        // per-query allocations.
398        let mut pairs: Vec<(usize, usize)> = Vec::new();
399        {
400            let (bodies, grid, query_buffer, query_seen) = (
401                &self.bodies,
402                &mut self.grid,
403                &mut self.query_buffer,
404                &mut self.query_seen,
405            );
406            grid.clear();
407            for (index, body) in bodies.iter().enumerate() {
408                if let Some(bbox) = body.bounding_box() {
409                    grid.insert(index, bbox.min(), bbox.max());
410                }
411            }
412            for (i, body) in bodies.iter().enumerate() {
413                let Some(bbox) = body.bounding_box() else {
414                    continue;
415                };
416                grid.query_into(bbox.min(), bbox.max(), query_buffer, query_seen);
417                for &j in query_buffer.iter() {
418                    if j > i {
419                        pairs.push((i, j));
420                    }
421                }
422            }
423        }
424        for iteration in 0..PHYSICS_MAX_ITERATIONS {
425            let mut any_collision: bool = false;
426            for &(i, j) in pairs.iter() {
427                let (left, right) = self.get_mut_bodies().split_at_mut(j);
428                let body_a: &mut RigidBody2D = &mut left[i];
429                let body_b: &mut RigidBody2D = &mut right[0];
430                if body_a.get_inverse_mass() == 0.0 && body_b.get_inverse_mass() == 0.0 {
431                    continue;
432                }
433                if let Some(result) = body_a.check_collision_with(body_b) {
434                    body_a.resolve_collision_with(body_b, &result);
435                    any_collision = true;
436                }
437            }
438            if !any_collision {
439                break;
440            }
441            let _: u32 = iteration;
442        }
443    }
444}
445
446/// Forwards `PhysicsWorld2D::step` through the [`Updatable`] trait so that
447/// physics worlds participate in the same update loop as entities, animators,
448/// and scene managers. The inherent [`PhysicsWorld2D::step`] method is the
449/// canonical implementation; this impl exists purely for trait dispatch.
450/// The inherent call resolves first when both are in scope, so there is no
451/// recursion.
452impl Updatable for PhysicsWorld2D {
453    /// Advances the simulation by `delta_time` seconds.
454    ///
455    /// # Arguments
456    ///
457    /// - `f64` - Seconds elapsed since the previous update.
458    fn update(&mut self, delta_time: f64) {
459        PhysicsWorld2D::step(self, delta_time);
460    }
461}
462
463/// Implements default configuration for `PhysicsConfig3D`.
464impl Default for PhysicsConfig3D {
465    /// Constructs a default [`PhysicsConfig3D`] value.
466    ///
467    /// # Returns
468    ///
469    /// - `PhysicsConfig3D` - A default-constructed instance with the documented initial state.
470    fn default() -> PhysicsConfig3D {
471        PhysicsConfig3D::new(
472            Vector3D::new(0.0, DEFAULT_GRAVITY_3D, 0.0),
473            DEFAULT_LINEAR_DAMPING,
474            DEFAULT_ANGULAR_DAMPING,
475        )
476    }
477}
478
479/// Implements body creation and force management for `RigidBody3D`.
480impl RigidBody3D {
481    /// Creates a new dynamic 3D rigid body with default mass and the given position.
482    ///
483    /// # Arguments
484    ///
485    /// - `u64` - The unique ID.
486    /// - `Vector3D` - The initial position.
487    ///
488    /// # Returns
489    ///
490    /// - `RigidBody3D` - The new body.
491    pub fn new_dynamic(id: u64, position: Vector3D) -> RigidBody3D {
492        let mass: f64 = PHYSICS_DEFAULT_MASS;
493        let mut body: RigidBody3D = RigidBody3D::new(
494            id,
495            position,
496            mass,
497            1.0 / mass,
498            DEFAULT_RESTITUTION,
499            DEFAULT_FRICTION,
500            BodyType::Dynamic,
501        );
502        body.update_inertia(mass);
503        body
504    }
505
506    /// Creates a new static 3D rigid body at the given position with infinite mass.
507    ///
508    /// # Arguments
509    ///
510    /// - `u64` - The unique ID.
511    /// - `Vector3D` - The position.
512    ///
513    /// # Returns
514    ///
515    /// - `RigidBody3D` - The new static body.
516    pub fn new_static(id: u64, position: Vector3D) -> RigidBody3D {
517        RigidBody3D::new(
518            id,
519            position,
520            PHYSICS_STATIC_MASS,
521            0.0,
522            DEFAULT_RESTITUTION,
523            DEFAULT_FRICTION,
524            BodyType::Static,
525        )
526    }
527
528    /// Applies a force to the body's force accumulator.
529    ///
530    /// # Arguments
531    ///
532    /// - `Vector3D` - The force vector.
533    pub fn apply_force(&mut self, force: Vector3D) {
534        *self.get_mut_force_accumulator() += force;
535    }
536
537    /// Applies a torque to the body's torque accumulator.
538    ///
539    /// # Arguments
540    ///
541    /// - `Vector3D` - The torque vector.
542    pub fn apply_torque(&mut self, torque: Vector3D) {
543        *self.get_mut_torque_accumulator() += torque;
544    }
545
546    /// Applies an instantaneous impulse, directly changing velocity.
547    ///
548    /// # Arguments
549    ///
550    /// - `Vector3D` - The impulse vector.
551    pub fn apply_impulse(&mut self, impulse: Vector3D) {
552        let inverse_mass: f64 = self.get_inverse_mass();
553        if inverse_mass == 0.0 {
554            return;
555        }
556        *self.get_mut_velocity() += impulse.scaled(inverse_mass);
557    }
558
559    /// Sets the mass of the body, updating the inverse mass.
560    /// A mass of 0 makes the body static (infinite mass).
561    ///
562    /// # Arguments
563    ///
564    /// - `f64` - The new mass.
565    pub fn update_mass(&mut self, mass: f64) {
566        self.set_mass(mass);
567        self.set_inverse_mass(if mass > 0.0 { 1.0 / mass } else { 0.0 });
568    }
569
570    /// Sets the moment of inertia of the body, updating the inverse inertia.
571    /// An inertia of 0 makes the body non-rotatable (used for static bodies).
572    ///
573    /// # Arguments
574    ///
575    /// - `f64` - The new moment of inertia.
576    pub fn update_inertia(&mut self, inertia: f64) {
577        self.set_inverse_inertia(if inertia > 0.0 { 1.0 / inertia } else { 0.0 });
578    }
579
580    /// Returns `true` if this body is affected by forces and collisions.
581    ///
582    /// # Returns
583    ///
584    /// - `bool` - True if this body is dynamic.
585    pub fn is_dynamic(&self) -> bool {
586        self.get_body_type() == BodyType::Dynamic
587    }
588
589    /// Attaches a 3D collider shape to this body.
590    ///
591    /// # Arguments
592    ///
593    /// - `BodyCollider3D` - The collider to attach.
594    pub fn update_collider(&mut self, collider: BodyCollider3D) {
595        self.set_collider(Some(collider));
596    }
597
598    /// Returns the world-space 3D bounding box of the attached collider, if any.
599    ///
600    /// # Returns
601    ///
602    /// - `Option<AABB3D>` - The bounding box, or `None` if no collider is attached.
603    pub fn bounding_box(&self) -> Option<AABB3D> {
604        let collider: Option<BodyCollider3D> = self.get_collider();
605        let position: Vector3D = self.get_position();
606        match collider? {
607            BodyCollider3D::Aabb(aabb) => {
608                let center: Vector3D = aabb.get_aabb().center();
609                let size: Vector3D = aabb.get_aabb().size();
610                Some(AABB3D::from_center(
611                    position + center,
612                    size.get_x(),
613                    size.get_y(),
614                    size.get_z(),
615                ))
616            }
617            BodyCollider3D::Sphere(sphere) => {
618                let sphere_inner: Sphere = sphere.get_sphere();
619                let diameter: f64 = sphere_inner.get_radius() * 2.0;
620                Some(AABB3D::from_center(
621                    position + sphere_inner.get_center(),
622                    diameter,
623                    diameter,
624                    diameter,
625                ))
626            }
627        }
628    }
629}
630
631/// Implements body management and simulation for `PhysicsWorld3D`.
632impl PhysicsWorld3D {
633    /// Creates a new 3D physics world with the given configuration.
634    ///
635    /// # Arguments
636    ///
637    /// - `PhysicsConfig3D` - The simulation configuration.
638    ///
639    /// # Returns
640    ///
641    /// - `PhysicsWorld3D` - The new world.
642    pub fn with_config(config: PhysicsConfig3D) -> PhysicsWorld3D {
643        let mut world: PhysicsWorld3D = PhysicsWorld3D::new(config);
644        world.set_grid(SpatialHashGrid3D::with_default_size());
645        world
646    }
647
648    /// Adds a rigid body to the world.
649    ///
650    /// # Arguments
651    ///
652    /// - `RigidBody3D` - The body to add.
653    pub fn add_body(&mut self, body: RigidBody3D) {
654        self.get_mut_bodies().push(body);
655    }
656
657    /// Removes the body with the given ID.
658    ///
659    /// # Arguments
660    ///
661    /// - `u64` - The ID of the body to remove.
662    pub fn remove_body(&mut self, id: u64) {
663        self.get_mut_bodies()
664            .retain(|body: &RigidBody3D| body.get_id() != id);
665    }
666
667    /// Returns a reference to the body with the given ID.
668    ///
669    /// # Arguments
670    ///
671    /// - `u64` - The body ID.
672    ///
673    /// # Returns
674    ///
675    /// - `Option<&RigidBody3D>` - The body reference, if found.
676    pub fn get_body(&self, id: u64) -> Option<&RigidBody3D> {
677        self.get_bodies()
678            .iter()
679            .find(|body: &&RigidBody3D| body.get_id() == id)
680    }
681
682    /// Returns a mutable reference to the body with the given ID.
683    ///
684    /// # Arguments
685    ///
686    /// - `u64` - The body ID.
687    ///
688    /// # Returns
689    ///
690    /// - `Option<&mut RigidBody3D>` - The mutable body reference, if found.
691    pub fn get_body_mut(&mut self, id: u64) -> Option<&mut RigidBody3D> {
692        self.get_mut_bodies()
693            .iter_mut()
694            .find(|body: &&mut RigidBody3D| body.get_id() == id)
695    }
696
697    /// Performs one physics simulation step using semi-implicit Euler integration.
698    ///
699    /// Applies gravity to dynamic bodies, integrates velocity from accumulated forces,
700    /// applies damping, integrates position, and resolves collisions.
701    ///
702    /// # Arguments
703    ///
704    /// - `f64` - The fixed delta time in seconds.
705    pub fn step(&mut self, delta_time: f64) {
706        let config: PhysicsConfig3D = self.get_config();
707        // Hoist loop-invariant damping factors out of the per-body loop.
708        let damping_factor: f64 = (1.0 - config.get_linear_damping() * delta_time).max(0.0);
709        let angular_damping: f64 = (1.0 - config.get_angular_damping() * delta_time).max(0.0);
710        let gravity: Vector3D = config.get_gravity();
711        for body in self.get_mut_bodies() {
712            if !body.is_dynamic() {
713                continue;
714            }
715            let body_mass: f64 = body.get_mass();
716            let body_inverse_mass: f64 = body.get_inverse_mass();
717            *body.get_mut_force_accumulator() += gravity.scaled(body_mass);
718            let force: Vector3D = body.get_force_accumulator();
719            *body.get_mut_velocity() += force.scaled(body_inverse_mass * delta_time);
720            // In-place damping and integration avoid temporary vector copies.
721            *body.get_mut_velocity() *= damping_factor;
722            let current_velocity: Vector3D = body.get_velocity();
723            *body.get_mut_position() += current_velocity.scaled(delta_time);
724            body.set_force_accumulator(Vector3D::zero());
725            *body.get_mut_angular_velocity() *= angular_damping;
726            let body_inverse_inertia: f64 = body.get_inverse_inertia();
727            let torque: Vector3D = body.get_torque_accumulator();
728            *body.get_mut_angular_velocity() += torque.scaled(body_inverse_inertia * delta_time);
729            let angular_velocity: Vector3D = body.get_angular_velocity();
730            let rotation_delta: Quaternion = Quaternion::new(
731                angular_velocity.get_x() * delta_time * 0.5,
732                angular_velocity.get_y() * delta_time * 0.5,
733                angular_velocity.get_z() * delta_time * 0.5,
734                1.0,
735            );
736            body.set_rotation((rotation_delta * body.get_rotation()).normalized());
737            body.set_torque_accumulator(Vector3D::zero());
738        }
739        self.resolve_collisions();
740    }
741
742    /// Detects and resolves all collisions between bodies in the 3D world.
743    ///
744    /// Uses a spatial hash grid for broad-phase culling followed by narrow-phase
745    /// shape-specific collision detection, then applies impulse-based resolution.
746    /// This reduces the broad-phase from O(n²) to near O(n) for typical scenes.
747    fn resolve_collisions(&mut self) {
748        let body_count: usize = self.get_bodies().len();
749        if body_count < 2 {
750            return;
751        }
752        // Rebuild the persistent grid once per step and collect the candidate
753        // pair list once; every solver iteration then reuses both (the grid is
754        // unchanged between iterations), eliminating per-iteration re-queries and
755        // per-query allocations.
756        let mut pairs: Vec<(usize, usize)> = Vec::new();
757        // Collect bboxes first (immutable borrow of bodies) then drain the
758        // spatial grid (mutable borrow). Splitting avoids the split-borrow
759        // limitation that method-call-based accessors introduce.
760        let bboxes: Vec<(usize, AABB3D)> = self
761            .get_bodies()
762            .iter()
763            .enumerate()
764            .filter_map(|(index, body)| body.bounding_box().map(|bbox| (index, bbox)))
765            .collect();
766        {
767            let Self {
768                grid,
769                query_buffer,
770                query_seen,
771                ..
772            } = self;
773            let grid: &mut SpatialHashGrid3D = grid;
774            let query_buffer: &mut Vec<usize> = query_buffer;
775            let query_seen: &mut HashSet<usize> = query_seen;
776            grid.clear();
777            for (index, bbox) in &bboxes {
778                grid.insert(*index, bbox.get_min(), bbox.get_max());
779            }
780            for (i, (_, bbox)) in bboxes.iter().enumerate() {
781                grid.query_into(bbox.get_min(), bbox.get_max(), query_buffer, query_seen);
782                for &j in query_buffer.iter() {
783                    if j > i {
784                        pairs.push((i, j));
785                    }
786                }
787            }
788        }
789        for iteration in 0..PHYSICS_MAX_ITERATIONS {
790            let mut any_collision: bool = false;
791            for &(i, j) in pairs.iter() {
792                let (left, right) = self.get_mut_bodies().split_at_mut(j);
793                let body_a: &mut RigidBody3D = &mut left[i];
794                let body_b: &mut RigidBody3D = &mut right[0];
795                if body_a.get_inverse_mass() == 0.0 && body_b.get_inverse_mass() == 0.0 {
796                    continue;
797                }
798                if let Some(result) = Self::check_collision_3d(body_a, body_b) {
799                    Self::resolve_collision_3d(body_a, body_b, &result);
800                    any_collision = true;
801                }
802            }
803            if !any_collision {
804                break;
805            }
806            let _: u32 = iteration;
807        }
808    }
809
810    /// Checks collision between two 3D bodies based on both bodies' collider shapes.
811    ///
812    /// # Arguments
813    ///
814    /// - `&RigidBody3D` - The first body.
815    /// - `&RigidBody3D` - The second body.
816    ///
817    /// # Returns
818    ///
819    /// - `Option<CollisionResult3D>` - The collision result, or `None`.
820    fn check_collision_3d(a: &RigidBody3D, b: &RigidBody3D) -> Option<CollisionResult3D> {
821        let a_bbox: AABB3D = a.bounding_box()?;
822        let b_bbox: AABB3D = b.bounding_box()?;
823        if !AABB3D::broad_phase(a_bbox, b_bbox) {
824            return None;
825        }
826        let a_collider: Option<BodyCollider3D> = a.get_collider();
827        let b_collider: Option<BodyCollider3D> = b.get_collider();
828        let position_delta: Vector3D = b.get_position() - a.get_position();
829        match (a_collider, b_collider) {
830            (Some(BodyCollider3D::Aabb(aabb_a)), Some(BodyCollider3D::Aabb(aabb_b))) => {
831                let aabb_b_inner: AABB3D = aabb_b.get_aabb();
832                let offset_aabb: AabbCollider3D = AabbCollider3D::new(AABB3D::new(
833                    aabb_b_inner.get_min() + position_delta,
834                    aabb_b_inner.get_max() + position_delta,
835                ));
836                aabb_a.collide_with_aabb(&offset_aabb)
837            }
838            (Some(BodyCollider3D::Sphere(sphere_a)), Some(BodyCollider3D::Sphere(sphere_b))) => {
839                let sphere_b_inner: Sphere = sphere_b.get_sphere();
840                let offset_sphere: SphereCollider3D = SphereCollider3D::new(Sphere::new(
841                    sphere_b_inner.get_center() + position_delta,
842                    sphere_b_inner.get_radius(),
843                ));
844                sphere_a.collide_with_sphere(&offset_sphere)
845            }
846            (Some(BodyCollider3D::Aabb(aabb)), Some(BodyCollider3D::Sphere(sphere))) => {
847                let sphere_inner: Sphere = sphere.get_sphere();
848                let offset_sphere: SphereCollider3D = SphereCollider3D::new(Sphere::new(
849                    sphere_inner.get_center() + position_delta,
850                    sphere_inner.get_radius(),
851                ));
852                aabb.collide_with_sphere(&offset_sphere)
853            }
854            (Some(BodyCollider3D::Sphere(sphere)), Some(BodyCollider3D::Aabb(aabb))) => {
855                let aabb_inner: AABB3D = aabb.get_aabb();
856                let offset_aabb: AabbCollider3D = AabbCollider3D::new(AABB3D::new(
857                    aabb_inner.get_min() + position_delta,
858                    aabb_inner.get_max() + position_delta,
859                ));
860                offset_aabb
861                    .collide_with_sphere(&sphere)
862                    .map(|mut result: CollisionResult3D| {
863                        result.set_normal(-result.get_normal());
864                        result
865                    })
866            }
867            _ => None,
868        }
869    }
870
871    /// Resolves a collision between two 3D bodies using impulse-based response
872    /// and position correction.
873    ///
874    /// # Arguments
875    ///
876    /// - `&mut RigidBody3D` - The first body.
877    /// - `&mut RigidBody3D` - The second body.
878    /// - `&CollisionResult3D` - The collision data.
879    fn resolve_collision_3d(a: &mut RigidBody3D, b: &mut RigidBody3D, result: &CollisionResult3D) {
880        let a_inverse_mass: f64 = a.get_inverse_mass();
881        let b_inverse_mass: f64 = b.get_inverse_mass();
882        let relative_velocity: Vector3D = b.get_velocity() - a.get_velocity();
883        let velocity_along_normal: f64 = relative_velocity.dot(result.get_normal());
884        if velocity_along_normal > 0.0 {
885            return;
886        }
887        let restitution: f64 = a.get_restitution().min(b.get_restitution());
888        let inverse_mass_sum: f64 = a_inverse_mass + b_inverse_mass;
889        if inverse_mass_sum == 0.0 {
890            return;
891        }
892        let impulse_magnitude: f64 =
893            -(1.0 + restitution) * velocity_along_normal / inverse_mass_sum;
894        let impulse: Vector3D = result.get_normal().scaled(impulse_magnitude);
895        *a.get_mut_velocity() -= impulse.scaled(a_inverse_mass);
896        *b.get_mut_velocity() += impulse.scaled(b_inverse_mass);
897        let correction: Vector3D = result
898            .get_normal()
899            .scaled((result.get_depth() * PHYSICS_POSITION_PERCENT / inverse_mass_sum).max(0.0));
900        *a.get_mut_position() -= correction.scaled(a_inverse_mass);
901        *b.get_mut_position() += correction.scaled(b_inverse_mass);
902    }
903}
904
905/// Forwards `PhysicsWorld3D::step` through the [`Updatable`] trait so that
906/// 3D physics worlds participate in the same update loop as their 2D
907/// counterparts, entities, animators, and scene managers. The inherent
908/// [`PhysicsWorld3D::step`] method is the canonical implementation; this impl
909/// exists purely for trait dispatch. The inherent call resolves first when
910/// both are in scope, so there is no recursion.
911impl Updatable for PhysicsWorld3D {
912    /// Advances the simulation by `delta_time` seconds.
913    ///
914    /// # Arguments
915    ///
916    /// - `f64` - Seconds elapsed since the previous update.
917    fn update(&mut self, delta_time: f64) {
918        PhysicsWorld3D::step(self, delta_time);
919    }
920}
921
922/// Implements `Default` for `PhysicsWorld3D` as an empty world.
923impl Default for PhysicsWorld3D {
924    /// Constructs a default [`PhysicsWorld3D`] value.
925    ///
926    /// # Returns
927    ///
928    /// - `PhysicsWorld3D` - A default-constructed instance with the documented initial state.
929    fn default() -> PhysicsWorld3D {
930        PhysicsWorld3D::with_config(PhysicsConfig3D::default())
931    }
932}
933
934#[cfg(test)]
935mod tests {
936    //! Regression tests for the 3D physics step pipeline.
937    //!
938    //! These tests live inline (rather than under `engine/tests/`) because the
939    //! `physics` module does not yet `pub use r#impl`, so external tests cannot
940    //! reach methods like `step()` or `apply_torque()`. Once the module is
941    //! reorganised to expose impls publicly, these can move to an integration
942    //! test target alongside `input/fn.rs` and `webgpu/fn.rs`.
943    use super::*;
944
945    const EPSILON: f64 = 1e-9;
946
947    /// Regression test for the bug where `RigidBody3D::apply_torque`
948    /// accumulated torque into `torque_accumulator` but
949    /// `PhysicsWorld3D::step()` only zeroed it without ever converting it
950    /// into angular velocity.
951    #[test]
952    fn step_applies_torque_to_3d_angular_velocity() {
953        let mut world: PhysicsWorld3D = PhysicsWorld3D::default();
954        let mut body: RigidBody3D = RigidBody3D::new_dynamic(1, Vector3D::new(0.0, 0.0, 0.0));
955        // Default inertia = mass (1.0) so inverse_inertia == 1.0; applying
956        // torque (0, 0, 2) for a 1 s step should give omega == (0, 0, 2).
957        body.apply_torque(Vector3D::new(0.0, 0.0, 2.0));
958        world.add_body(body);
959
960        world.step(1.0);
961        let omega: Vector3D = world.get_body(1).unwrap().get_angular_velocity();
962        assert!(
963            omega.get_x().abs() < EPSILON,
964            "unexpected x angular velocity: {}",
965            omega.get_x(),
966        );
967        assert!(
968            omega.get_y().abs() < EPSILON,
969            "unexpected y angular velocity: {}",
970            omega.get_y(),
971        );
972        let expected_z: f64 = 2.0;
973        assert!(
974            (omega.get_z() - expected_z).abs() < EPSILON,
975            "expected z angular velocity {}, got {}",
976            expected_z,
977            omega.get_z(),
978        );
979
980        // Accumulator must be cleared after the step so subsequent steps do
981        // not re-apply the same torque.
982        world.step(1.0);
983        let omega_after: Vector3D = world.get_body(1).unwrap().get_angular_velocity();
984        assert!(
985            (omega_after.get_z() - expected_z).abs() < EPSILON,
986            "torque_accumulator leaked into a second step: z = {}",
987            omega_after.get_z(),
988        );
989    }
990
991    /// Static bodies (inverse_inertia == 0) must ignore torque entirely:
992    /// applying torque to a static body must not produce angular velocity.
993    #[test]
994    fn step_static_3d_body_ignores_torque() {
995        let mut world: PhysicsWorld3D = PhysicsWorld3D::default();
996        let mut body: RigidBody3D = RigidBody3D::new_static(1, Vector3D::new(0.0, 0.0, 0.0));
997        body.apply_torque(Vector3D::new(1.0, 0.0, 0.0));
998        world.add_body(body);
999
1000        world.step(1.0);
1001        let omega: Vector3D = world.get_body(1).unwrap().get_angular_velocity();
1002        assert!(
1003            omega.get_x().abs() < EPSILON
1004                && omega.get_y().abs() < EPSILON
1005                && omega.get_z().abs() < EPSILON,
1006            "static body must remain rotationally inert, got ({}, {}, {})",
1007            omega.get_x(),
1008            omega.get_y(),
1009            omega.get_z(),
1010        );
1011    }
1012
1013    /// Torque applied across multiple steps must accumulate into angular
1014    /// velocity. Guards against the original bug returning silently.
1015    #[test]
1016    fn step_torque_accumulates_over_multiple_steps() {
1017        let mut world: PhysicsWorld3D = PhysicsWorld3D::default();
1018        let body: RigidBody3D = RigidBody3D::new_dynamic(1, Vector3D::new(0.0, 0.0, 0.0));
1019        world.add_body(body);
1020
1021        for _ in 0..4 {
1022            world
1023                .get_body_mut(1)
1024                .unwrap()
1025                .apply_torque(Vector3D::new(0.0, 1.0, 0.0));
1026            world.step(1.0);
1027        }
1028        let omega: Vector3D = world.get_body(1).unwrap().get_angular_velocity();
1029        // Each step: omega_y += torque_y * inv_inertia * dt = 1.0.
1030        // After 4 steps: omega_y == 4.0.
1031        assert!(
1032            (omega.get_y() - 4.0).abs() < EPSILON,
1033            "expected cumulative angular velocity 4.0 on y axis, got {}",
1034            omega.get_y(),
1035        );
1036    }
1037
1038    /// `update_inertia(0)` must zero out `inverse_inertia`, making torque
1039    /// application inert for that body until the inertia is restored.
1040    #[test]
1041    fn update_inertia_zeros_inverse_inertia() {
1042        let mut body: RigidBody3D = RigidBody3D::new_dynamic(1, Vector3D::new(0.0, 0.0, 0.0));
1043        assert!(
1044            (body.get_inverse_inertia() - 1.0).abs() < EPSILON,
1045            "default inverse_inertia must equal 1/mass = 1.0, got {}",
1046            body.get_inverse_inertia(),
1047        );
1048        body.update_inertia(0.0);
1049        assert!(
1050            body.get_inverse_inertia().abs() < EPSILON,
1051            "update_inertia(0) must zero out inverse_inertia, got {}",
1052            body.get_inverse_inertia(),
1053        );
1054    }
1055
1056    /// 2D angular integration is unchanged by the 3D torque fix.
1057    #[test]
1058    fn step_2d_angular_velocity_unchanged() {
1059        let mut world: PhysicsWorld2D = PhysicsWorld2D::default();
1060        let body: RigidBody2D = RigidBody2D::new_dynamic(1, Vector2D::new(0.0, 0.0));
1061        world.add_body(body);
1062
1063        world.step(1.0);
1064        let omega: f64 = world.get_body(1).unwrap().get_angular_velocity();
1065        assert!(
1066            omega.abs() < EPSILON,
1067            "2D angular velocity should remain 0 with no input, got {}",
1068            omega,
1069        );
1070    }
1071}