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