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                Some(Rect::from_center(
152                    aabb_rect.center() + self.get_position(),
153                    aabb_rect.get_width(),
154                    aabb_rect.get_height(),
155                ))
156            }
157            BodyCollider::Circle(circle) => {
158                let diameter: f64 = circle.get_circle().get_radius() * 2.0;
159                Some(Rect::from_center(self.get_position(), diameter, diameter))
160            }
161        }
162    }
163}
164
165/// Implements body management and simulation for `PhysicsWorld2D`.
166impl PhysicsWorld2D {
167    /// Creates a new physics world with the given configuration.
168    ///
169    /// # Arguments
170    ///
171    /// - `PhysicsConfig` - The simulation configuration.
172    ///
173    /// # Returns
174    ///
175    /// - `PhysicsWorld2D` - The new world.
176    pub fn with_config(config: PhysicsConfig) -> PhysicsWorld2D {
177        let mut world: PhysicsWorld2D = PhysicsWorld2D::new(config);
178        world.set_grid(SpatialHashGrid2D::with_default_size());
179        world
180    }
181
182    /// Adds a rigid body to the world.
183    ///
184    /// # Arguments
185    ///
186    /// - `RigidBody2D` - The body to add.
187    pub fn add_body(&mut self, body: RigidBody2D) {
188        self.get_mut_bodies().push(body);
189    }
190
191    /// Removes the body with the given ID.
192    ///
193    /// # Arguments
194    ///
195    /// - `u64` - The ID of the body to remove.
196    pub fn remove_body(&mut self, id: u64) {
197        self.get_mut_bodies()
198            .retain(|body: &RigidBody2D| body.get_id() != id);
199    }
200
201    /// Returns a reference to the body with the given ID.
202    ///
203    /// # Arguments
204    ///
205    /// - `u64` - The body ID.
206    ///
207    /// # Returns
208    ///
209    /// - `Option<&RigidBody2D>` - The body reference, if found.
210    pub fn get_body(&self, id: u64) -> Option<&RigidBody2D> {
211        self.get_bodies()
212            .iter()
213            .find(|body: &&RigidBody2D| body.get_id() == id)
214    }
215
216    /// Returns a mutable reference to the body with the given ID.
217    ///
218    /// # Arguments
219    ///
220    /// - `u64` - The body ID.
221    ///
222    /// # Returns
223    ///
224    /// - `Option<&mut RigidBody2D>` - The mutable body reference, if found.
225    pub fn get_body_mut(&mut self, id: u64) -> Option<&mut RigidBody2D> {
226        self.get_mut_bodies()
227            .iter_mut()
228            .find(|body: &&mut RigidBody2D| body.get_id() == id)
229    }
230}
231
232/// Implements `Default` for `PhysicsWorld2D` as an empty world.
233impl Default for PhysicsWorld2D {
234    /// Constructs a default [`PhysicsWorld2D`] value.
235    ///
236    /// # Returns
237    ///
238    /// - `PhysicsWorld2D` - A default-constructed instance with the documented initial state.
239    fn default() -> PhysicsWorld2D {
240        PhysicsWorld2D::with_config(PhysicsConfig::default())
241    }
242}
243
244/// Implements collision detection and resolution for `RigidBody2D`.
245impl RigidBody2D {
246    /// Checks collision with another body based on both bodies' collider shapes.
247    ///
248    /// # Arguments
249    ///
250    /// - `&RigidBody2D` - The other body to check against.
251    ///
252    /// # Returns
253    ///
254    /// - `Option<CollisionResult>` - The collision result, or `None`.
255    fn check_collision_with(&self, other: &RigidBody2D) -> Option<CollisionResult> {
256        let a_bbox: Rect = self.bounding_box()?;
257        let b_bbox: Rect = other.bounding_box()?;
258        if !Rect::broad_phase_alias(a_bbox, b_bbox) {
259            return None;
260        }
261        let self_collider: Option<BodyCollider> = self.get_collider();
262        let other_collider: Option<BodyCollider> = other.get_collider();
263        let position_delta: Vector2D = other.get_position() - self.get_position();
264        match (self_collider, other_collider) {
265            (Some(BodyCollider::Aabb(aabb_a)), Some(BodyCollider::Aabb(aabb_b))) => {
266                let aabb_b_rect: Rect = aabb_b.get_rect();
267                let offset_aabb_b: AabbCollider = AabbCollider::new(Rect::new(
268                    aabb_b_rect.get_x() + position_delta.get_x(),
269                    aabb_b_rect.get_y() + position_delta.get_y(),
270                    aabb_b_rect.get_width(),
271                    aabb_b_rect.get_height(),
272                ));
273                aabb_a.collide_with_aabb(&offset_aabb_b)
274            }
275            (Some(BodyCollider::Circle(circle_a)), Some(BodyCollider::Circle(circle_b))) => {
276                let circle_b_inner: Circle = circle_b.get_circle();
277                let offset_circle_b: CircleCollider = CircleCollider::new(Circle::new(
278                    circle_b_inner.get_center() + position_delta,
279                    circle_b_inner.get_radius(),
280                ));
281                circle_a.collide_with_circle(&offset_circle_b)
282            }
283            (Some(BodyCollider::Aabb(aabb)), Some(BodyCollider::Circle(circle))) => {
284                let circle_inner: Circle = circle.get_circle();
285                let offset_circle: CircleCollider = CircleCollider::new(Circle::new(
286                    circle_inner.get_center() + position_delta,
287                    circle_inner.get_radius(),
288                ));
289                aabb.collide_with_circle(&offset_circle)
290            }
291            (Some(BodyCollider::Circle(circle)), Some(BodyCollider::Aabb(aabb))) => {
292                let aabb_rect: Rect = aabb.get_rect();
293                let offset_aabb: AabbCollider = AabbCollider::new(Rect::new(
294                    aabb_rect.get_x() + position_delta.get_x(),
295                    aabb_rect.get_y() + position_delta.get_y(),
296                    aabb_rect.get_width(),
297                    aabb_rect.get_height(),
298                ));
299                offset_aabb
300                    .collide_with_circle(&circle)
301                    .map(|mut result: CollisionResult| {
302                        result.set_normal(-result.get_normal());
303                        result
304                    })
305            }
306            _ => None,
307        }
308    }
309
310    /// Resolves a collision with another body using impulse-based response,
311    /// Coulomb friction, and position correction.
312    ///
313    /// Friction is resolved first and independently of the normal impulse:
314    /// a body sliding across a surface is typically *separating* along the
315    /// contact normal, so the normal-impulse early return would otherwise
316    /// skip friction entirely and the body would slide forever.
317    ///
318    /// # Arguments
319    ///
320    /// - `&mut RigidBody2D` - The other body involved in the collision.
321    /// - `&CollisionResult` - The collision data.
322    fn resolve_collision_with(&mut self, other: &mut RigidBody2D, result: &CollisionResult) {
323        let self_inverse_mass: f64 = self.get_inverse_mass();
324        let other_inverse_mass: f64 = other.get_inverse_mass();
325        let inverse_mass_sum: f64 = self_inverse_mass + other_inverse_mass;
326        if inverse_mass_sum == 0.0 {
327            return;
328        }
329        let relative_velocity: Vector2D = other.get_velocity() - self.get_velocity();
330        let velocity_along_normal: f64 = relative_velocity.dot(result.get_normal());
331        let restitution: f64 = self.get_restitution().min(other.get_restitution());
332        let impulse_magnitude: f64 = if velocity_along_normal > 0.0 {
333            0.0
334        } else {
335            -(1.0 + restitution) * velocity_along_normal / inverse_mass_sum
336        };
337        // Coulomb cone. The normal load is the penetration-driven correction
338        // impulse rather than the velocity impulse: a body sliding on a
339        // surface is *separating* along the normal (so the velocity impulse is
340        // zero) yet still carries real load through the contact, and clamping
341        // to the velocity impulse would zero out friction and let the body
342        // slide forever. Clamping to the load also keeps friction from
343        // reversing tangential motion, so a resting body settles instead of
344        // jittering.
345        let normal_load: f64 = (result.get_depth() * PHYSICS_POSITION_PERCENT / inverse_mass_sum)
346            .max(0.0)
347            + impulse_magnitude.abs();
348        let max_friction_impulse: f64 = self.get_friction().min(other.get_friction()) * normal_load;
349        let tangent_velocity: Vector2D =
350            relative_velocity - result.get_normal().scaled(velocity_along_normal);
351        let tangent_speed: f64 = tangent_velocity.magnitude();
352        if max_friction_impulse > 0.0 && tangent_speed > 0.0 {
353            let friction_impulse: Vector2D = tangent_velocity
354                .normalized()
355                .scaled(-(tangent_speed / inverse_mass_sum).min(max_friction_impulse));
356            *self.get_mut_velocity() -= friction_impulse.scaled(self_inverse_mass);
357            *other.get_mut_velocity() += friction_impulse.scaled(other_inverse_mass);
358        }
359        if velocity_along_normal > 0.0 {
360            return;
361        }
362        let impulse: Vector2D = result.get_normal().scaled(impulse_magnitude);
363        *self.get_mut_velocity() -= impulse.scaled(self_inverse_mass);
364        *other.get_mut_velocity() += impulse.scaled(other_inverse_mass);
365        let correction: Vector2D = result
366            .get_normal()
367            .scaled((result.get_depth() * PHYSICS_POSITION_PERCENT / inverse_mass_sum).max(0.0));
368        *self.get_mut_position() -= correction.scaled(self_inverse_mass);
369        *other.get_mut_position() += correction.scaled(other_inverse_mass);
370    }
371}
372
373/// Implements simulation stepping and collision resolution for `PhysicsWorld2D`.
374impl PhysicsWorld2D {
375    /// Performs one physics simulation step using semi-implicit Euler integration.
376    ///
377    /// Applies gravity to dynamic bodies, integrates velocity from accumulated forces,
378    /// applies damping, integrates position, and resolves collisions.
379    ///
380    /// # Arguments
381    ///
382    /// - `f64` - The fixed delta time in seconds.
383    pub fn step(&mut self, delta_time: f64) {
384        let config: PhysicsConfig = self.get_config();
385        // Hoist loop-invariant damping factors out of the per-body loop.
386        let damping_factor: f64 = (1.0 - config.get_linear_damping() * delta_time).max(0.0);
387        let angular_damping: f64 = (1.0 - config.get_angular_damping() * delta_time).max(0.0);
388        let gravity: Vector2D = config.get_gravity();
389        for body in self.get_mut_bodies() {
390            if !body.is_dynamic() {
391                continue;
392            }
393            let body_mass: f64 = body.get_mass();
394            let body_inverse_mass: f64 = body.get_inverse_mass();
395            *body.get_mut_force_accumulator() += gravity.scaled(body_mass);
396            let force: Vector2D = body.get_force_accumulator();
397            *body.get_mut_velocity() += force.scaled(body_inverse_mass * delta_time);
398            // In-place damping and integration avoid temporary vector copies.
399            *body.get_mut_velocity() *= damping_factor;
400            let current_velocity: Vector2D = body.get_velocity();
401            *body.get_mut_position() += current_velocity.scaled(delta_time);
402            body.set_force_accumulator(Vector2D::zero());
403            *body.get_mut_angular_velocity() *= angular_damping;
404            let current_angular_velocity: f64 = body.get_angular_velocity();
405            *body.get_mut_rotation() += current_angular_velocity * delta_time;
406        }
407        self.resolve_collisions();
408    }
409
410    /// Detects and resolves all collisions between bodies in the world.
411    ///
412    /// Uses a spatial hash grid for broad-phase culling followed by narrow-phase
413    /// shape-specific collision detection, then applies impulse-based resolution.
414    /// This reduces the broad-phase from O(n²) to near O(n) for typical scenes.
415    fn resolve_collisions(&mut self) {
416        let body_count: usize = self.get_bodies().len();
417        if body_count < 2 {
418            return;
419        }
420        // Rebuild the persistent grid once per step and collect the candidate
421        // pair list once; every solver iteration then reuses both (the grid is
422        // unchanged between iterations), eliminating per-iteration re-queries and
423        // per-query allocations.
424        // OPT 33: the candidate pair list is now backed by `self.pair_buffer`,
425        // a persistent `Vec<(usize, usize)>` field on `PhysicsWorld2D`. Cleared
426        // at the top of each step instead of allocating a fresh `Vec` — across
427        // thousands of physics steps per app run the heap churn adds up.
428        self.get_mut_pair_buffer().clear();
429        {
430            let Self {
431                bodies,
432                grid,
433                query_buffer,
434                query_seen,
435                pair_buffer,
436                ..
437            } = self;
438            let bodies: &Vec<RigidBody2D> = bodies;
439            let grid: &mut SpatialHashGrid2D = grid;
440            let query_buffer: &mut Vec<usize> = query_buffer;
441            let query_seen: &mut HashSet<usize> = query_seen;
442            grid.clear();
443            for (index, body) in bodies.iter().enumerate() {
444                if let Some(bbox) = body.bounding_box() {
445                    grid.insert(index, bbox.min(), bbox.max());
446                }
447            }
448            let pairs: &mut Vec<(usize, usize)> = pair_buffer;
449            for (i, body) in bodies.iter().enumerate() {
450                let Some(bbox) = body.bounding_box() else {
451                    continue;
452                };
453                grid.query_into(bbox.min(), bbox.max(), query_buffer, query_seen);
454                for &j in query_buffer.iter() {
455                    if j > i {
456                        pairs.push((i, j));
457                    }
458                }
459            }
460        }
461        // OPT 33: `mem::take` moves the pairs out (leaving an empty Vec
462        // behind) so the immutable borrow on `self.pair_buffer` ends before
463        // the `self.get_mut_bodies()` mutable borrow below — the buffer keeps
464        // its allocation across steps instead of paying one Vec clone
465        // (alloc + memcpy) per step per world. It is restored after the
466        // iteration loop.
467        let pairs_snapshot: Vec<(usize, usize)> = take(self.get_mut_pair_buffer());
468        for iteration in 0..PHYSICS_MAX_ITERATIONS {
469            let mut any_collision: bool = false;
470            for &(i, j) in pairs_snapshot.iter() {
471                let (left, right) = self.get_mut_bodies().split_at_mut(j);
472                let body_a: &mut RigidBody2D = &mut left[i];
473                let body_b: &mut RigidBody2D = &mut right[0];
474                if body_a.get_inverse_mass() == 0.0 && body_b.get_inverse_mass() == 0.0 {
475                    continue;
476                }
477                if let Some(result) = body_a.check_collision_with(body_b) {
478                    body_a.resolve_collision_with(body_b, &result);
479                    any_collision = true;
480                }
481            }
482            if !any_collision {
483                break;
484            }
485            let _: u32 = iteration;
486        }
487        self.set_pair_buffer(pairs_snapshot);
488    }
489}
490
491/// Forwards `PhysicsWorld2D::step` through the [`Updatable`] trait so that
492/// physics worlds participate in the same update loop as entities, animators,
493/// and scene managers. The inherent [`PhysicsWorld2D::step`] method is the
494/// canonical implementation; this impl exists purely for trait dispatch.
495/// The inherent call resolves first when both are in scope, so there is no
496/// recursion.
497impl Updatable for PhysicsWorld2D {
498    /// Advances the simulation by `delta_time` seconds.
499    ///
500    /// # Arguments
501    ///
502    /// - `f64` - Seconds elapsed since the previous update.
503    fn update(&mut self, delta_time: f64) {
504        PhysicsWorld2D::step(self, delta_time);
505    }
506}
507
508/// Implements default configuration for `PhysicsConfig3D`.
509impl Default for PhysicsConfig3D {
510    /// Constructs a default [`PhysicsConfig3D`] value.
511    ///
512    /// # Returns
513    ///
514    /// - `PhysicsConfig3D` - A default-constructed instance with the documented initial state.
515    fn default() -> PhysicsConfig3D {
516        PhysicsConfig3D::new(
517            Vector3D::new(0.0, DEFAULT_GRAVITY_3D, 0.0),
518            DEFAULT_LINEAR_DAMPING,
519            DEFAULT_ANGULAR_DAMPING,
520        )
521    }
522}
523
524/// Implements body creation and force management for `RigidBody3D`.
525impl RigidBody3D {
526    /// Creates a new dynamic 3D rigid body with default mass and the given position.
527    ///
528    /// # Arguments
529    ///
530    /// - `u64` - The unique ID.
531    /// - `Vector3D` - The initial position.
532    ///
533    /// # Returns
534    ///
535    /// - `RigidBody3D` - The new body.
536    pub fn new_dynamic(id: u64, position: Vector3D) -> RigidBody3D {
537        let mass: f64 = PHYSICS_DEFAULT_MASS;
538        let mut body: RigidBody3D = RigidBody3D::new(
539            id,
540            position,
541            mass,
542            1.0 / mass,
543            DEFAULT_RESTITUTION,
544            DEFAULT_FRICTION,
545            BodyType::Dynamic,
546        );
547        body.update_inertia(mass);
548        body
549    }
550
551    /// Creates a new static 3D rigid body at the given position with infinite mass.
552    ///
553    /// # Arguments
554    ///
555    /// - `u64` - The unique ID.
556    /// - `Vector3D` - The position.
557    ///
558    /// # Returns
559    ///
560    /// - `RigidBody3D` - The new static body.
561    pub fn new_static(id: u64, position: Vector3D) -> RigidBody3D {
562        RigidBody3D::new(
563            id,
564            position,
565            PHYSICS_STATIC_MASS,
566            0.0,
567            DEFAULT_RESTITUTION,
568            DEFAULT_FRICTION,
569            BodyType::Static,
570        )
571    }
572
573    /// Applies a force to the body's force accumulator.
574    ///
575    /// # Arguments
576    ///
577    /// - `Vector3D` - The force vector.
578    pub fn apply_force(&mut self, force: Vector3D) {
579        *self.get_mut_force_accumulator() += force;
580    }
581
582    /// Applies a torque to the body's torque accumulator.
583    ///
584    /// # Arguments
585    ///
586    /// - `Vector3D` - The torque vector.
587    pub fn apply_torque(&mut self, torque: Vector3D) {
588        *self.get_mut_torque_accumulator() += torque;
589    }
590
591    /// Applies an instantaneous impulse, directly changing velocity.
592    ///
593    /// # Arguments
594    ///
595    /// - `Vector3D` - The impulse vector.
596    pub fn apply_impulse(&mut self, impulse: Vector3D) {
597        let inverse_mass: f64 = self.get_inverse_mass();
598        if inverse_mass == 0.0 {
599            return;
600        }
601        *self.get_mut_velocity() += impulse.scaled(inverse_mass);
602    }
603
604    /// Sets the mass of the body, updating the inverse mass.
605    /// A mass of 0 makes the body static (infinite mass).
606    ///
607    /// # Arguments
608    ///
609    /// - `f64` - The new mass.
610    pub fn update_mass(&mut self, mass: f64) {
611        self.set_mass(mass);
612        self.set_inverse_mass(if mass > 0.0 { 1.0 / mass } else { 0.0 });
613    }
614
615    /// Sets the moment of inertia of the body, updating the inverse inertia.
616    /// An inertia of 0 makes the body non-rotatable (used for static bodies).
617    ///
618    /// # Arguments
619    ///
620    /// - `f64` - The new moment of inertia.
621    pub fn update_inertia(&mut self, inertia: f64) {
622        self.set_inverse_inertia(if inertia > 0.0 { 1.0 / inertia } else { 0.0 });
623    }
624
625    /// Returns `true` if this body is affected by forces and collisions.
626    ///
627    /// # Returns
628    ///
629    /// - `bool` - True if this body is dynamic.
630    pub fn is_dynamic(&self) -> bool {
631        self.get_body_type() == BodyType::Dynamic
632    }
633
634    /// Attaches a 3D collider shape to this body.
635    ///
636    /// # Arguments
637    ///
638    /// - `BodyCollider3D` - The collider to attach.
639    pub fn update_collider(&mut self, collider: BodyCollider3D) {
640        self.set_collider(Some(collider));
641    }
642
643    /// Returns the world-space 3D bounding box of the attached collider, if any.
644    ///
645    /// # Returns
646    ///
647    /// - `Option<AABB3D>` - The bounding box, or `None` if no collider is attached.
648    pub fn bounding_box(&self) -> Option<AABB3D> {
649        let collider: Option<BodyCollider3D> = self.get_collider();
650        let position: Vector3D = self.get_position();
651        match collider? {
652            BodyCollider3D::Aabb(aabb) => {
653                let center: Vector3D = aabb.get_aabb().center();
654                let size: Vector3D = aabb.get_aabb().size();
655                Some(AABB3D::from_center(
656                    position + center,
657                    size.get_x(),
658                    size.get_y(),
659                    size.get_z(),
660                ))
661            }
662            BodyCollider3D::Sphere(sphere) => {
663                let sphere_inner: Sphere = sphere.get_sphere();
664                let diameter: f64 = sphere_inner.get_radius() * 2.0;
665                Some(AABB3D::from_center(
666                    position + sphere_inner.get_center(),
667                    diameter,
668                    diameter,
669                    diameter,
670                ))
671            }
672        }
673    }
674}
675
676/// Implements body management and simulation for `PhysicsWorld3D`.
677impl PhysicsWorld3D {
678    /// Creates a new 3D physics world with the given configuration.
679    ///
680    /// # Arguments
681    ///
682    /// - `PhysicsConfig3D` - The simulation configuration.
683    ///
684    /// # Returns
685    ///
686    /// - `PhysicsWorld3D` - The new world.
687    pub fn with_config(config: PhysicsConfig3D) -> PhysicsWorld3D {
688        let mut world: PhysicsWorld3D = PhysicsWorld3D::new(config);
689        world.set_grid(SpatialHashGrid3D::with_default_size());
690        world
691    }
692
693    /// Adds a rigid body to the world.
694    ///
695    /// # Arguments
696    ///
697    /// - `RigidBody3D` - The body to add.
698    pub fn add_body(&mut self, body: RigidBody3D) {
699        self.get_mut_bodies().push(body);
700    }
701
702    /// Removes the body with the given ID.
703    ///
704    /// # Arguments
705    ///
706    /// - `u64` - The ID of the body to remove.
707    pub fn remove_body(&mut self, id: u64) {
708        self.get_mut_bodies()
709            .retain(|body: &RigidBody3D| body.get_id() != id);
710    }
711
712    /// Returns a reference to the body with the given ID.
713    ///
714    /// # Arguments
715    ///
716    /// - `u64` - The body ID.
717    ///
718    /// # Returns
719    ///
720    /// - `Option<&RigidBody3D>` - The body reference, if found.
721    pub fn get_body(&self, id: u64) -> Option<&RigidBody3D> {
722        self.get_bodies()
723            .iter()
724            .find(|body: &&RigidBody3D| body.get_id() == id)
725    }
726
727    /// Returns a mutable reference to the body with the given ID.
728    ///
729    /// # Arguments
730    ///
731    /// - `u64` - The body ID.
732    ///
733    /// # Returns
734    ///
735    /// - `Option<&mut RigidBody3D>` - The mutable body reference, if found.
736    pub fn get_body_mut(&mut self, id: u64) -> Option<&mut RigidBody3D> {
737        self.get_mut_bodies()
738            .iter_mut()
739            .find(|body: &&mut RigidBody3D| body.get_id() == id)
740    }
741
742    /// Performs one physics simulation step using semi-implicit Euler integration.
743    ///
744    /// Applies gravity to dynamic bodies, integrates velocity from accumulated forces,
745    /// applies damping, integrates position, and resolves collisions.
746    ///
747    /// # Arguments
748    ///
749    /// - `f64` - The fixed delta time in seconds.
750    pub fn step(&mut self, delta_time: f64) {
751        let config: PhysicsConfig3D = self.get_config();
752        // Hoist loop-invariant damping factors out of the per-body loop.
753        let damping_factor: f64 = (1.0 - config.get_linear_damping() * delta_time).max(0.0);
754        let angular_damping: f64 = (1.0 - config.get_angular_damping() * delta_time).max(0.0);
755        let gravity: Vector3D = config.get_gravity();
756        for body in self.get_mut_bodies() {
757            if !body.is_dynamic() {
758                continue;
759            }
760            let body_mass: f64 = body.get_mass();
761            let body_inverse_mass: f64 = body.get_inverse_mass();
762            *body.get_mut_force_accumulator() += gravity.scaled(body_mass);
763            let force: Vector3D = body.get_force_accumulator();
764            *body.get_mut_velocity() += force.scaled(body_inverse_mass * delta_time);
765            // In-place damping and integration avoid temporary vector copies.
766            *body.get_mut_velocity() *= damping_factor;
767            let current_velocity: Vector3D = body.get_velocity();
768            *body.get_mut_position() += current_velocity.scaled(delta_time);
769            body.set_force_accumulator(Vector3D::zero());
770            *body.get_mut_angular_velocity() *= angular_damping;
771            let body_inverse_inertia: f64 = body.get_inverse_inertia();
772            let torque: Vector3D = body.get_torque_accumulator();
773            *body.get_mut_angular_velocity() += torque.scaled(body_inverse_inertia * delta_time);
774            let angular_velocity: Vector3D = body.get_angular_velocity();
775            let rotation_delta: Quaternion = Quaternion::new(
776                angular_velocity.get_x() * delta_time * 0.5,
777                angular_velocity.get_y() * delta_time * 0.5,
778                angular_velocity.get_z() * delta_time * 0.5,
779                1.0,
780            );
781            body.set_rotation((rotation_delta * body.get_rotation()).normalized());
782            body.set_torque_accumulator(Vector3D::zero());
783        }
784        self.resolve_collisions();
785    }
786
787    /// Detects and resolves all collisions between bodies in the 3D world.
788    ///
789    /// Uses a spatial hash grid for broad-phase culling followed by narrow-phase
790    /// shape-specific collision detection, then applies impulse-based resolution.
791    /// This reduces the broad-phase from O(n²) to near O(n) for typical scenes.
792    fn resolve_collisions(&mut self) {
793        let body_count: usize = self.get_bodies().len();
794        if body_count < 2 {
795            return;
796        }
797        // Rebuild the persistent grid once per step and collect the candidate
798        // pair list once; every solver iteration then reuses both (the grid is
799        // unchanged between iterations), eliminating per-iteration re-queries and
800        // per-query allocations.
801        // OPT 33: candidate pair list backed by `self.pair_buffer`, a persistent
802        // field on `PhysicsWorld3D`. See `PhysicsWorld2D::resolve_collisions`
803        // for the rationale.
804        self.get_mut_pair_buffer().clear();
805        // Collect bboxes first (immutable borrow of bodies) then drain the
806        // spatial grid (mutable borrow). Splitting avoids the split-borrow
807        // limitation that method-call-based accessors introduce.
808        // OPT 33: pre-size the bboxes scratch Vec to the current body count
809        // so the first allocation does not double-grow on subsequent frames
810        // (each body's bbox is `O(1)` and the Vec is rebuilt every step).
811        // The actual allocation still happens here — the persistent
812        // `bbox_buffer` field idea was rejected because the immutable-then-
813        // mutable borrow split on `self.bodies` cannot hold both a `&mut`
814        // borrow on `bbox_buffer` and the source `iter()` simultaneously
815        // even with edition 2024 split-borrow rules.
816        let body_count: usize = self.get_bodies().len();
817        let mut bboxes: Vec<(usize, AABB3D)> = Vec::with_capacity(body_count);
818        bboxes.extend(self.get_bodies().iter().enumerate().filter_map(
819            |(index, body): (usize, &RigidBody3D)| {
820                body.bounding_box().map(|bbox: AABB3D| (index, bbox))
821            },
822        ));
823        {
824            let Self {
825                grid,
826                query_buffer,
827                query_seen,
828                pair_buffer,
829                ..
830            } = self;
831            let grid: &mut SpatialHashGrid3D = grid;
832            let query_buffer: &mut Vec<usize> = query_buffer;
833            let query_seen: &mut HashSet<usize> = query_seen;
834            grid.clear();
835            let pairs: &mut Vec<(usize, usize)> = pair_buffer;
836            for (index, bbox) in bboxes.iter() {
837                grid.insert(*index, bbox.get_min(), bbox.get_max());
838            }
839            for (i, (_, bbox)) in bboxes.iter().enumerate() {
840                grid.query_into(bbox.get_min(), bbox.get_max(), query_buffer, query_seen);
841                for &j in query_buffer.iter() {
842                    if j > i {
843                        pairs.push((i, j));
844                    }
845                }
846            }
847        }
848        // `mem::take` moves the pairs out (leaving an empty Vec behind) so
849        // the immutable borrow on `self.pair_buffer` ends before the
850        // `self.get_mut_bodies()` mutable borrow below — the buffer keeps
851        // its allocation across steps instead of paying one Vec clone
852        // (alloc + memcpy) per step per world. It is restored after the
853        // iteration loop.
854        let pairs_snapshot: Vec<(usize, usize)> = take(self.get_mut_pair_buffer());
855        for iteration in 0..PHYSICS_MAX_ITERATIONS {
856            let mut any_collision: bool = false;
857            for &(i, j) in pairs_snapshot.iter() {
858                let (left, right) = self.get_mut_bodies().split_at_mut(j);
859                let body_a: &mut RigidBody3D = &mut left[i];
860                let body_b: &mut RigidBody3D = &mut right[0];
861                if body_a.get_inverse_mass() == 0.0 && body_b.get_inverse_mass() == 0.0 {
862                    continue;
863                }
864                if let Some(result) = Self::check_collision_3d(body_a, body_b) {
865                    Self::resolve_collision_3d(body_a, body_b, &result);
866                    any_collision = true;
867                }
868            }
869            if !any_collision {
870                break;
871            }
872            let _: u32 = iteration;
873        }
874        self.set_pair_buffer(pairs_snapshot);
875    }
876
877    /// Checks collision between two 3D bodies based on both bodies' collider shapes.
878    ///
879    /// # Arguments
880    ///
881    /// - `&RigidBody3D` - The first body.
882    /// - `&RigidBody3D` - The second body.
883    ///
884    /// # Returns
885    ///
886    /// - `Option<CollisionResult3D>` - The collision result, or `None`.
887    fn check_collision_3d(a: &RigidBody3D, b: &RigidBody3D) -> Option<CollisionResult3D> {
888        let a_bbox: AABB3D = a.bounding_box()?;
889        let b_bbox: AABB3D = b.bounding_box()?;
890        if !AABB3D::broad_phase(a_bbox, b_bbox) {
891            return None;
892        }
893        let a_collider: Option<BodyCollider3D> = a.get_collider();
894        let b_collider: Option<BodyCollider3D> = b.get_collider();
895        let position_delta: Vector3D = b.get_position() - a.get_position();
896        match (a_collider, b_collider) {
897            (Some(BodyCollider3D::Aabb(aabb_a)), Some(BodyCollider3D::Aabb(aabb_b))) => {
898                let aabb_b_inner: AABB3D = aabb_b.get_aabb();
899                let offset_aabb: AabbCollider3D = AabbCollider3D::new(AABB3D::new(
900                    aabb_b_inner.get_min() + position_delta,
901                    aabb_b_inner.get_max() + position_delta,
902                ));
903                aabb_a.collide_with_aabb(&offset_aabb)
904            }
905            (Some(BodyCollider3D::Sphere(sphere_a)), Some(BodyCollider3D::Sphere(sphere_b))) => {
906                let sphere_b_inner: Sphere = sphere_b.get_sphere();
907                let offset_sphere: SphereCollider3D = SphereCollider3D::new(Sphere::new(
908                    sphere_b_inner.get_center() + position_delta,
909                    sphere_b_inner.get_radius(),
910                ));
911                sphere_a.collide_with_sphere(&offset_sphere)
912            }
913            (Some(BodyCollider3D::Aabb(aabb)), Some(BodyCollider3D::Sphere(sphere))) => {
914                let sphere_inner: Sphere = sphere.get_sphere();
915                let offset_sphere: SphereCollider3D = SphereCollider3D::new(Sphere::new(
916                    sphere_inner.get_center() + position_delta,
917                    sphere_inner.get_radius(),
918                ));
919                aabb.collide_with_sphere(&offset_sphere)
920            }
921            (Some(BodyCollider3D::Sphere(sphere)), Some(BodyCollider3D::Aabb(aabb))) => {
922                let aabb_inner: AABB3D = aabb.get_aabb();
923                let offset_aabb: AabbCollider3D = AabbCollider3D::new(AABB3D::new(
924                    aabb_inner.get_min() + position_delta,
925                    aabb_inner.get_max() + position_delta,
926                ));
927                offset_aabb
928                    .collide_with_sphere(&sphere)
929                    .map(|mut result: CollisionResult3D| {
930                        result.set_normal(-result.get_normal());
931                        result
932                    })
933            }
934            _ => None,
935        }
936    }
937
938    /// Resolves a collision between two 3D bodies using impulse-based
939    /// response, Coulomb friction, and position correction.
940    ///
941    /// Friction is resolved first and independently of the normal impulse:
942    /// a body sliding across a surface is typically *separating* along the
943    /// contact normal, so the normal-impulse early return would otherwise
944    /// skip friction entirely and the body would slide forever. The tangent
945    /// is the in-plane projection of the relative velocity, so this is
946    /// dimension-agnostic and needs no per-axis tangent basis.
947    ///
948    /// # Arguments
949    ///
950    /// - `&mut RigidBody3D` - The first body.
951    /// - `&mut RigidBody3D` - The second body.
952    /// - `&CollisionResult3D` - The collision data.
953    fn resolve_collision_3d(a: &mut RigidBody3D, b: &mut RigidBody3D, result: &CollisionResult3D) {
954        let a_inverse_mass: f64 = a.get_inverse_mass();
955        let b_inverse_mass: f64 = b.get_inverse_mass();
956        let inverse_mass_sum: f64 = a_inverse_mass + b_inverse_mass;
957        if inverse_mass_sum == 0.0 {
958            return;
959        }
960        let relative_velocity: Vector3D = b.get_velocity() - a.get_velocity();
961        let velocity_along_normal: f64 = relative_velocity.dot(result.get_normal());
962        let restitution: f64 = a.get_restitution().min(b.get_restitution());
963        let impulse_magnitude: f64 = if velocity_along_normal > 0.0 {
964            0.0
965        } else {
966            -(1.0 + restitution) * velocity_along_normal / inverse_mass_sum
967        };
968        let normal_load: f64 = (result.get_depth() * PHYSICS_POSITION_PERCENT / inverse_mass_sum)
969            .max(0.0)
970            + impulse_magnitude.abs();
971        let max_friction_impulse: f64 = a.get_friction().min(b.get_friction()) * normal_load;
972        let tangent_velocity: Vector3D =
973            relative_velocity - result.get_normal().scaled(velocity_along_normal);
974        let tangent_speed: f64 = tangent_velocity.magnitude();
975        if max_friction_impulse > 0.0 && tangent_speed > 0.0 {
976            let friction_impulse: Vector3D = tangent_velocity
977                .normalized()
978                .scaled(-(tangent_speed / inverse_mass_sum).min(max_friction_impulse));
979            *a.get_mut_velocity() -= friction_impulse.scaled(a_inverse_mass);
980            *b.get_mut_velocity() += friction_impulse.scaled(b_inverse_mass);
981        }
982        if velocity_along_normal > 0.0 {
983            return;
984        }
985        let impulse: Vector3D = result.get_normal().scaled(impulse_magnitude);
986        *a.get_mut_velocity() -= impulse.scaled(a_inverse_mass);
987        *b.get_mut_velocity() += impulse.scaled(b_inverse_mass);
988        let correction: Vector3D = result
989            .get_normal()
990            .scaled((result.get_depth() * PHYSICS_POSITION_PERCENT / inverse_mass_sum).max(0.0));
991        *a.get_mut_position() -= correction.scaled(a_inverse_mass);
992        *b.get_mut_position() += correction.scaled(b_inverse_mass);
993    }
994}
995
996/// Forwards `PhysicsWorld3D::step` through the [`Updatable`] trait so that
997/// 3D physics worlds participate in the same update loop as their 2D
998/// counterparts, entities, animators, and scene managers. The inherent
999/// [`PhysicsWorld3D::step`] method is the canonical implementation; this impl
1000/// exists purely for trait dispatch. The inherent call resolves first when
1001/// both are in scope, so there is no recursion.
1002impl Updatable for PhysicsWorld3D {
1003    /// Advances the simulation by `delta_time` seconds.
1004    ///
1005    /// # Arguments
1006    ///
1007    /// - `f64` - Seconds elapsed since the previous update.
1008    fn update(&mut self, delta_time: f64) {
1009        PhysicsWorld3D::step(self, delta_time);
1010    }
1011}
1012
1013/// Implements `Default` for `PhysicsWorld3D` as an empty world.
1014impl Default for PhysicsWorld3D {
1015    /// Constructs a default [`PhysicsWorld3D`] value.
1016    ///
1017    /// # Returns
1018    ///
1019    /// - `PhysicsWorld3D` - A default-constructed instance with the documented initial state.
1020    fn default() -> PhysicsWorld3D {
1021        PhysicsWorld3D::with_config(PhysicsConfig3D::default())
1022    }
1023}