Skip to main content

embedded_3dgfx/
physics.rs

1//! Physics engine for embedded 3D graphics.
2//!
3//! Provides rigid body dynamics with linear and angular motion, gravity,
4//! collision detection, impulse-based response with Coulomb friction,
5//! body lifecycle management, and constraint joints.
6//!
7//! Designed for `no_std` environments using fixed-capacity `heapless` collections.
8//!
9//! # Collider shapes
10//! Each body can optionally have a [`Collider`] attached. Supported shapes:
11//! - [`Collider::Sphere`] — radius-based, cheapest intersection test
12//! - [`Collider::Aabb`] — axis-aligned bounding box, defined by half-extents
13//!
14//! # Example
15//! ```
16//! use embedded_3dgfx::physics::{PhysicsWorld, RigidBody, Collider};
17//! use nalgebra::Vector3;
18//!
19//! let mut world = PhysicsWorld::<16>::new();
20//! world.set_gravity(Vector3::new(0.0, -9.81, 0.0));
21//!
22//! let ball = RigidBody::new(1.0)
23//!     .with_position(Vector3::new(0.0, 10.0, 0.0))
24//!     .with_collider(Collider::Sphere { radius: 0.5 });
25//! let id = world.add_body(ball).unwrap();
26//!
27//! let floor = RigidBody::new_static()
28//!     .with_collider(Collider::Aabb { half_extents: Vector3::new(10.0, 0.1, 10.0) });
29//! world.add_body(floor).unwrap();
30//!
31//! // Advance simulation — collision detection + response happen automatically
32//! // The const generic `8` sets the max number of contacts per step.
33//! world.step::<8>(0.016);
34//! ```
35
36use nalgebra::{Matrix3, UnitQuaternion, Vector3};
37
38#[cfg(not(feature = "std"))]
39use micromath::F32Ext;
40
41/// Unique identifier for a rigid body within a [`PhysicsWorld`].
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub struct BodyId(usize);
44
45/// Determines how a body participates in the simulation.
46#[derive(Debug, Clone, Copy, PartialEq)]
47pub enum BodyType {
48    /// Fully simulated: affected by forces, gravity, and velocity.
49    Dynamic,
50    /// Not affected by forces or gravity. Useful for floors, walls, and platforms.
51    /// Can still be repositioned manually.
52    Static,
53}
54
55/// A collision shape attached to a [`RigidBody`].
56///
57/// The collider is centered on the body's position. For AABBs the half-extents
58/// define the box dimensions along each axis from the center.
59#[derive(Debug, Clone, Copy, PartialEq)]
60pub enum Collider {
61    /// A sphere defined by its radius.
62    Sphere { radius: f32 },
63    /// An axis-aligned bounding box defined by half-extents along each axis.
64    Aabb { half_extents: Vector3<f32> },
65    /// A capsule defined by height and radius.
66    /// The capsule is oriented along the Y axis, centered on the body position.
67    /// Height is the distance between the two hemisphere centers (excluding the caps).
68    Capsule { height: f32, radius: f32 },
69}
70
71/// A detected contact between two bodies.
72///
73/// Contains the information needed for collision response.
74#[derive(Debug, Clone)]
75pub struct Contact {
76    /// ID of the first body.
77    pub body_a: BodyId,
78    /// ID of the second body.
79    pub body_b: BodyId,
80    /// Contact normal pointing from body A toward body B.
81    pub normal: Vector3<f32>,
82    /// Penetration depth (positive when overlapping).
83    pub penetration: f32,
84}
85
86/// A rigid body with linear and angular dynamics.
87///
88/// Tracks position, velocity, orientation, angular velocity, accumulated
89/// forces/torques, and mass/inertia.
90#[derive(Debug, Clone)]
91pub struct RigidBody {
92    // -- Linear state --
93    pub position: Vector3<f32>,
94    pub velocity: Vector3<f32>,
95    pub mass: f32,
96    pub inv_mass: f32,
97    pub body_type: BodyType,
98    pub restitution: f32,
99    pub collider: Option<Collider>,
100
101    /// Coulomb friction coefficient (0.0 = frictionless ice, 1.0 = very rough).
102    /// During collision response the effective friction is the geometric mean
103    /// of both bodies' coefficients: `sqrt(mu_a * mu_b)`.
104    pub friction: f32,
105
106    /// Accumulated forces applied this frame. Cleared after each `step()`.
107    force_accumulator: Vector3<f32>,
108
109    /// Linear damping factor (0.0 = no damping, 1.0 = full stop).
110    /// Applied each step as `velocity *= (1.0 - damping)`.
111    pub damping: f32,
112
113    // -- Angular state --
114    /// Orientation quaternion. Defaults to identity (no rotation).
115    pub orientation: UnitQuaternion<f32>,
116
117    /// Angular velocity in world-space (radians per second).
118    pub angular_velocity: Vector3<f32>,
119
120    /// Inverse of the inertia tensor in body-local space.
121    /// For static bodies this is the zero matrix.
122    pub inv_inertia_local: Matrix3<f32>,
123
124    /// Accumulated torques applied this frame. Cleared after each `step()`.
125    torque_accumulator: Vector3<f32>,
126
127    /// Angular damping factor (0.0 = no damping, 1.0 = full stop).
128    /// Applied each step as `angular_velocity *= (1.0 - angular_damping)`.
129    pub angular_damping: f32,
130
131    /// Whether this body is active. Inactive bodies are skipped during
132    /// integration and collision detection. Set to `false` by [`PhysicsWorld::remove_body`].
133    pub active: bool,
134}
135
136impl RigidBody {
137    /// Create a new dynamic rigid body with the given mass (in kg).
138    ///
139    /// The body starts with a default point-mass inertia tensor (identity scaled
140    /// by mass). Use [`with_inertia_sphere`] or [`with_inertia_box`] after
141    /// attaching a collider for physically accurate rotation.
142    ///
143    /// # Panics
144    /// Panics if `mass` is not positive and finite.
145    pub fn new(mass: f32) -> Self {
146        assert!(
147            mass > 0.0 && mass.is_finite(),
148            "mass must be positive and finite"
149        );
150        // Default inertia: treat as unit sphere (I = 2/5 * m * 1^2)
151        let i = 0.4 * mass;
152        let inv_i = 1.0 / i;
153        Self {
154            position: Vector3::zeros(),
155            velocity: Vector3::zeros(),
156            mass,
157            inv_mass: 1.0 / mass,
158            body_type: BodyType::Dynamic,
159            restitution: 0.5,
160            collider: None,
161            friction: 0.3,
162            force_accumulator: Vector3::zeros(),
163            damping: 0.01,
164            orientation: UnitQuaternion::identity(),
165            angular_velocity: Vector3::zeros(),
166            inv_inertia_local: Matrix3::from_diagonal(&Vector3::new(inv_i, inv_i, inv_i)),
167            torque_accumulator: Vector3::zeros(),
168            angular_damping: 0.01,
169            active: true,
170        }
171    }
172
173    /// Create a new static rigid body (infinite mass, unaffected by forces).
174    pub fn new_static() -> Self {
175        Self {
176            position: Vector3::zeros(),
177            velocity: Vector3::zeros(),
178            mass: f32::INFINITY,
179            inv_mass: 0.0,
180            body_type: BodyType::Static,
181            restitution: 0.5,
182            collider: None,
183            friction: 0.5,
184            force_accumulator: Vector3::zeros(),
185            damping: 0.0,
186            orientation: UnitQuaternion::identity(),
187            angular_velocity: Vector3::zeros(),
188            inv_inertia_local: Matrix3::zeros(),
189            torque_accumulator: Vector3::zeros(),
190            angular_damping: 0.0,
191            active: true,
192        }
193    }
194
195    /// Builder: set initial position.
196    pub fn with_position(mut self, position: Vector3<f32>) -> Self {
197        self.position = position;
198        self
199    }
200
201    /// Builder: set initial velocity.
202    pub fn with_velocity(mut self, velocity: Vector3<f32>) -> Self {
203        self.velocity = velocity;
204        self
205    }
206
207    /// Builder: set restitution (bounciness, 0.0..=1.0).
208    pub fn with_restitution(mut self, restitution: f32) -> Self {
209        self.restitution = restitution.clamp(0.0, 1.0);
210        self
211    }
212
213    /// Builder: set linear damping (0.0..=1.0).
214    pub fn with_damping(mut self, damping: f32) -> Self {
215        self.damping = damping.clamp(0.0, 1.0);
216        self
217    }
218
219    /// Builder: attach a collider for collision detection.
220    pub fn with_collider(mut self, collider: Collider) -> Self {
221        self.collider = Some(collider);
222        self
223    }
224
225    /// Builder: set friction coefficient (0.0..=1.0).
226    pub fn with_friction(mut self, friction: f32) -> Self {
227        self.friction = friction.clamp(0.0, 1.0);
228        self
229    }
230
231    /// Builder: set initial angular velocity (in radians per second).
232    pub fn with_angular_velocity(mut self, angular_velocity: Vector3<f32>) -> Self {
233        self.angular_velocity = angular_velocity;
234        self
235    }
236
237    /// Builder: set angular damping (0.0..=1.0).
238    pub fn with_angular_damping(mut self, damping: f32) -> Self {
239        self.angular_damping = damping.clamp(0.0, 1.0);
240        self
241    }
242
243    /// Builder: set the inertia tensor for a solid sphere of given radius.
244    ///
245    /// Inertia: `I = (2/5) * m * r²` (uniform along all axes).
246    pub fn with_inertia_sphere(mut self, radius: f32) -> Self {
247        if self.body_type == BodyType::Dynamic {
248            let i = 0.4 * self.mass * radius * radius;
249            let inv_i = 1.0 / i;
250            self.inv_inertia_local = Matrix3::from_diagonal(&Vector3::new(inv_i, inv_i, inv_i));
251        }
252        self
253    }
254
255    /// Builder: set the inertia tensor for a solid box with given half-extents.
256    ///
257    /// For a box of full dimensions `(2*hx, 2*hy, 2*hz)`:
258    /// - `Ixx = (1/12) * m * (4*hy² + 4*hz²)`
259    /// - `Iyy = (1/12) * m * (4*hx² + 4*hz²)`
260    /// - `Izz = (1/12) * m * (4*hx² + 4*hy²)`
261    pub fn with_inertia_box(mut self, half_extents: Vector3<f32>) -> Self {
262        if self.body_type == BodyType::Dynamic {
263            let hx2 = 4.0 * half_extents.x * half_extents.x;
264            let hy2 = 4.0 * half_extents.y * half_extents.y;
265            let hz2 = 4.0 * half_extents.z * half_extents.z;
266            let k = self.mass / 12.0;
267            let ix = k * (hy2 + hz2);
268            let iy = k * (hx2 + hz2);
269            let iz = k * (hx2 + hy2);
270            self.inv_inertia_local =
271                Matrix3::from_diagonal(&Vector3::new(1.0 / ix, 1.0 / iy, 1.0 / iz));
272        }
273        self
274    }
275
276    /// Apply a force (in Newtons) to this body. Forces accumulate until the next `step()`.
277    #[inline]
278    pub fn apply_force(&mut self, force: Vector3<f32>) {
279        self.force_accumulator += force;
280    }
281
282    /// Apply an instantaneous impulse (change in momentum) to this body.
283    /// Directly modifies velocity: `delta_v = impulse / mass`.
284    #[inline]
285    pub fn apply_impulse(&mut self, impulse: Vector3<f32>) {
286        if self.body_type == BodyType::Dynamic {
287            self.velocity += impulse * self.inv_mass;
288        }
289    }
290
291    /// Apply a torque (in N·m) to this body. Torques accumulate until the next `step()`.
292    #[inline]
293    pub fn apply_torque(&mut self, torque: Vector3<f32>) {
294        self.torque_accumulator += torque;
295    }
296
297    /// Apply an instantaneous angular impulse (change in angular momentum).
298    /// Directly modifies angular velocity: `delta_ω = I⁻¹ * impulse`.
299    #[inline]
300    pub fn apply_angular_impulse(&mut self, impulse: Vector3<f32>) {
301        if self.body_type == BodyType::Dynamic {
302            let inv_inertia_world = self.inv_inertia_world();
303            self.angular_velocity += inv_inertia_world * impulse;
304        }
305    }
306
307    /// Compute the world-space inverse inertia tensor from the local one and
308    /// the current orientation: `I⁻¹_world = R * I⁻¹_local * Rᵀ`.
309    #[inline]
310    pub fn inv_inertia_world(&self) -> Matrix3<f32> {
311        let r = self.orientation.to_rotation_matrix();
312        r.matrix() * self.inv_inertia_local * r.matrix().transpose()
313    }
314
315    /// Returns the current speed (magnitude of velocity).
316    #[inline]
317    pub fn speed(&self) -> f32 {
318        self.velocity.norm()
319    }
320
321    /// Returns the kinetic energy of this body: `0.5 * m * v^2`.
322    #[inline]
323    pub fn kinetic_energy(&self) -> f32 {
324        0.5 * self.mass * self.velocity.norm_squared()
325    }
326
327    /// Integrate this body forward by `dt` seconds using semi-implicit Euler.
328    ///
329    /// Semi-implicit Euler updates velocity first, then position/orientation,
330    /// which provides better energy conservation than explicit (forward) Euler.
331    fn integrate(&mut self, dt: f32, gravity: Vector3<f32>) {
332        if self.body_type != BodyType::Dynamic {
333            self.force_accumulator = Vector3::zeros();
334            self.torque_accumulator = Vector3::zeros();
335            return;
336        }
337
338        // --- Linear ---
339        let acceleration = gravity + self.force_accumulator * self.inv_mass;
340        self.velocity += acceleration * dt;
341        self.velocity *= 1.0 - self.damping;
342        self.position += self.velocity * dt;
343
344        // --- Angular ---
345        let inv_inertia_world = self.inv_inertia_world();
346        let angular_acceleration = inv_inertia_world * self.torque_accumulator;
347        self.angular_velocity += angular_acceleration * dt;
348        self.angular_velocity *= 1.0 - self.angular_damping;
349
350        // Integrate orientation: q' = q + 0.5 * dt * ω * q
351        // where ω * q is the quaternion product with ω encoded as (0, ωx, ωy, ωz)
352        let w = &self.angular_velocity;
353        let half_dt = 0.5 * dt;
354        let dq = nalgebra::Quaternion::new(0.0, w.x * half_dt, w.y * half_dt, w.z * half_dt);
355        let q = self.orientation.into_inner();
356        let new_q = q + dq * q;
357        // Renormalize to prevent drift
358        self.orientation = UnitQuaternion::new_normalize(new_q);
359
360        // Clear accumulators
361        self.force_accumulator = Vector3::zeros();
362        self.torque_accumulator = Vector3::zeros();
363    }
364}
365
366// ---------------------------------------------------------------------------
367// Collision detection
368// ---------------------------------------------------------------------------
369
370/// Test two colliders for intersection and generate a contact if overlapping.
371///
372/// Returns `None` if the shapes are not overlapping.
373/// The returned normal points from A toward B.
374fn collide(
375    pos_a: &Vector3<f32>,
376    col_a: &Collider,
377    pos_b: &Vector3<f32>,
378    col_b: &Collider,
379) -> Option<(Vector3<f32>, f32)> {
380    match (col_a, col_b) {
381        (Collider::Sphere { radius: ra }, Collider::Sphere { radius: rb }) => {
382            collide_sphere_sphere(pos_a, *ra, pos_b, *rb)
383        }
384        (Collider::Aabb { half_extents: ha }, Collider::Aabb { half_extents: hb }) => {
385            collide_aabb_aabb(pos_a, ha, pos_b, hb)
386        }
387        (Collider::Sphere { radius }, Collider::Aabb { half_extents }) => {
388            collide_sphere_aabb(pos_a, *radius, pos_b, half_extents)
389        }
390        (Collider::Aabb { half_extents }, Collider::Sphere { radius }) => {
391            // Flip: run sphere-aabb with swapped order, negate normal
392            let result = collide_sphere_aabb(pos_b, *radius, pos_a, half_extents);
393            result.map(|(normal, pen)| (-normal, pen))
394        }
395        (
396            Collider::Capsule {
397                height: ha,
398                radius: ra,
399            },
400            Collider::Capsule {
401                height: hb,
402                radius: rb,
403            },
404        ) => collide_capsule_capsule(pos_a, *ha, *ra, pos_b, *hb, *rb),
405        (
406            Collider::Sphere { radius },
407            Collider::Capsule {
408                height,
409                radius: cap_radius,
410            },
411        ) => collide_sphere_capsule(pos_a, *radius, pos_b, *height, *cap_radius),
412        (
413            Collider::Capsule {
414                height,
415                radius: cap_radius,
416            },
417            Collider::Sphere { radius },
418        ) => {
419            let result = collide_sphere_capsule(pos_b, *radius, pos_a, *height, *cap_radius);
420            result.map(|(normal, pen)| (-normal, pen))
421        }
422        (Collider::Capsule { height, radius }, Collider::Aabb { half_extents }) => {
423            collide_capsule_aabb(pos_a, *height, *radius, pos_b, half_extents)
424        }
425        (Collider::Aabb { half_extents }, Collider::Capsule { height, radius }) => {
426            let result = collide_capsule_aabb(pos_b, *height, *radius, pos_a, half_extents);
427            result.map(|(normal, pen)| (-normal, pen))
428        }
429    }
430}
431
432/// Sphere vs Sphere intersection test.
433///
434/// Returns `(normal_a_to_b, penetration_depth)` or `None`.
435fn collide_sphere_sphere(
436    pos_a: &Vector3<f32>,
437    radius_a: f32,
438    pos_b: &Vector3<f32>,
439    radius_b: f32,
440) -> Option<(Vector3<f32>, f32)> {
441    let diff = pos_b - pos_a;
442    let dist_sq = diff.norm_squared();
443    let sum_r = radius_a + radius_b;
444
445    if dist_sq >= sum_r * sum_r {
446        return None;
447    }
448
449    let dist = dist_sq.sqrt();
450    let penetration = sum_r - dist;
451
452    let normal = if dist > 1e-6 {
453        diff / dist
454    } else {
455        // Perfectly overlapping — pick an arbitrary separation axis
456        Vector3::new(0.0, 1.0, 0.0)
457    };
458
459    Some((normal, penetration))
460}
461
462/// AABB vs AABB intersection test using the Separating Axis Theorem.
463///
464/// Returns `(normal_a_to_b, penetration_depth)` or `None`.
465fn collide_aabb_aabb(
466    pos_a: &Vector3<f32>,
467    half_a: &Vector3<f32>,
468    pos_b: &Vector3<f32>,
469    half_b: &Vector3<f32>,
470) -> Option<(Vector3<f32>, f32)> {
471    let diff = pos_b - pos_a;
472
473    let overlap_x = half_a.x + half_b.x - diff.x.abs();
474    if overlap_x <= 0.0 {
475        return None;
476    }
477    let overlap_y = half_a.y + half_b.y - diff.y.abs();
478    if overlap_y <= 0.0 {
479        return None;
480    }
481    let overlap_z = half_a.z + half_b.z - diff.z.abs();
482    if overlap_z <= 0.0 {
483        return None;
484    }
485
486    // Pick the axis with minimum penetration (least overlap)
487    if overlap_x <= overlap_y && overlap_x <= overlap_z {
488        let sign = if diff.x >= 0.0 { 1.0 } else { -1.0 };
489        Some((Vector3::new(sign, 0.0, 0.0), overlap_x))
490    } else if overlap_y <= overlap_z {
491        let sign = if diff.y >= 0.0 { 1.0 } else { -1.0 };
492        Some((Vector3::new(0.0, sign, 0.0), overlap_y))
493    } else {
494        let sign = if diff.z >= 0.0 { 1.0 } else { -1.0 };
495        Some((Vector3::new(0.0, 0.0, sign), overlap_z))
496    }
497}
498
499/// Sphere vs AABB intersection test.
500///
501/// Finds the closest point on the AABB to the sphere center, then checks distance.
502/// Returns `(normal_sphere_to_aabb, penetration_depth)` or `None`.
503fn collide_sphere_aabb(
504    sphere_pos: &Vector3<f32>,
505    sphere_radius: f32,
506    aabb_pos: &Vector3<f32>,
507    aabb_half: &Vector3<f32>,
508) -> Option<(Vector3<f32>, f32)> {
509    // Find the closest point on the AABB to the sphere center
510    let aabb_min = aabb_pos - aabb_half;
511    let aabb_max = aabb_pos + aabb_half;
512
513    let closest = Vector3::new(
514        sphere_pos.x.clamp(aabb_min.x, aabb_max.x),
515        sphere_pos.y.clamp(aabb_min.y, aabb_max.y),
516        sphere_pos.z.clamp(aabb_min.z, aabb_max.z),
517    );
518
519    let diff = sphere_pos - closest;
520    let dist_sq = diff.norm_squared();
521
522    if dist_sq >= sphere_radius * sphere_radius {
523        return None;
524    }
525
526    let dist = dist_sq.sqrt();
527
528    if dist > 1e-6 {
529        // Sphere center is outside the AABB — normal points from closest to sphere center
530        // We want normal from sphere toward AABB, so negate
531        let normal = -diff / dist;
532        let penetration = sphere_radius - dist;
533        Some((normal, penetration))
534    } else {
535        // Sphere center is inside the AABB — find the axis of least penetration
536        let dx_pos = aabb_max.x - sphere_pos.x;
537        let dx_neg = sphere_pos.x - aabb_min.x;
538        let dy_pos = aabb_max.y - sphere_pos.y;
539        let dy_neg = sphere_pos.y - aabb_min.y;
540        let dz_pos = aabb_max.z - sphere_pos.z;
541        let dz_neg = sphere_pos.z - aabb_min.z;
542
543        let mut min_dist = dx_pos;
544        let mut normal = Vector3::new(1.0, 0.0, 0.0);
545
546        if dx_neg < min_dist {
547            min_dist = dx_neg;
548            normal = Vector3::new(-1.0, 0.0, 0.0);
549        }
550        if dy_pos < min_dist {
551            min_dist = dy_pos;
552            normal = Vector3::new(0.0, 1.0, 0.0);
553        }
554        if dy_neg < min_dist {
555            min_dist = dy_neg;
556            normal = Vector3::new(0.0, -1.0, 0.0);
557        }
558        if dz_pos < min_dist {
559            min_dist = dz_pos;
560            normal = Vector3::new(0.0, 0.0, 1.0);
561        }
562        if dz_neg < min_dist {
563            min_dist = dz_neg;
564            normal = Vector3::new(0.0, 0.0, -1.0);
565        }
566
567        let penetration = sphere_radius + min_dist;
568        Some((normal, penetration))
569    }
570}
571
572/// Capsule vs Capsule intersection test.
573///
574/// Capsules are oriented along Y axis, centered at body position.
575fn collide_capsule_capsule(
576    pos_a: &Vector3<f32>,
577    height_a: f32,
578    radius_a: f32,
579    pos_b: &Vector3<f32>,
580    height_b: f32,
581    radius_b: f32,
582) -> Option<(Vector3<f32>, f32)> {
583    // Get capsule endpoints (top and bottom sphere centers)
584    let half_height_a = height_a * 0.5;
585    let half_height_b = height_b * 0.5;
586
587    let a_bottom = *pos_a + Vector3::new(0.0, -half_height_a, 0.0);
588    let a_top = *pos_a + Vector3::new(0.0, half_height_a, 0.0);
589    let b_bottom = *pos_b + Vector3::new(0.0, -half_height_b, 0.0);
590    let b_top = *pos_b + Vector3::new(0.0, half_height_b, 0.0);
591
592    // Find closest points on the two line segments
593    let (closest_a, closest_b) = closest_points_on_segments(a_bottom, a_top, b_bottom, b_top);
594
595    // Treat as sphere-sphere collision between closest points
596    collide_sphere_sphere(&closest_a, radius_a, &closest_b, radius_b)
597}
598
599/// Sphere vs Capsule intersection test.
600fn collide_sphere_capsule(
601    sphere_pos: &Vector3<f32>,
602    sphere_radius: f32,
603    capsule_pos: &Vector3<f32>,
604    capsule_height: f32,
605    capsule_radius: f32,
606) -> Option<(Vector3<f32>, f32)> {
607    // Get capsule endpoints
608    let half_height = capsule_height * 0.5;
609    let cap_bottom = *capsule_pos + Vector3::new(0.0, -half_height, 0.0);
610    let cap_top = *capsule_pos + Vector3::new(0.0, half_height, 0.0);
611
612    // Find closest point on capsule axis to sphere
613    let closest = closest_point_on_segment(*sphere_pos, cap_bottom, cap_top);
614
615    // Treat as sphere-sphere collision
616    collide_sphere_sphere(sphere_pos, sphere_radius, &closest, capsule_radius)
617}
618
619/// Capsule vs AABB intersection test.
620fn collide_capsule_aabb(
621    capsule_pos: &Vector3<f32>,
622    capsule_height: f32,
623    capsule_radius: f32,
624    aabb_pos: &Vector3<f32>,
625    aabb_half_extents: &Vector3<f32>,
626) -> Option<(Vector3<f32>, f32)> {
627    // Get capsule endpoints
628    let half_height = capsule_height * 0.5;
629    let cap_bottom = *capsule_pos + Vector3::new(0.0, -half_height, 0.0);
630    let cap_top = *capsule_pos + Vector3::new(0.0, half_height, 0.0);
631
632    // Find closest point on capsule axis to AABB
633    let aabb_min = aabb_pos - aabb_half_extents;
634    let aabb_max = aabb_pos + aabb_half_extents;
635
636    // Clamp capsule endpoints to find closest segment to AABB
637    let closest_on_capsule =
638        closest_point_on_segment_to_aabb(cap_bottom, cap_top, &aabb_min, &aabb_max);
639
640    // Treat as sphere-AABB collision from the closest point on capsule
641    collide_sphere_aabb(
642        &closest_on_capsule,
643        capsule_radius,
644        aabb_pos,
645        aabb_half_extents,
646    )
647}
648
649/// Find closest point on line segment to a point.
650fn closest_point_on_segment(
651    point: Vector3<f32>,
652    seg_start: Vector3<f32>,
653    seg_end: Vector3<f32>,
654) -> Vector3<f32> {
655    let segment = seg_end - seg_start;
656    let segment_len_sq = segment.norm_squared();
657
658    if segment_len_sq < 1e-6 {
659        return seg_start;
660    }
661
662    let t = ((point - seg_start).dot(&segment) / segment_len_sq).clamp(0.0, 1.0);
663    seg_start + segment * t
664}
665
666/// Find closest points on two line segments.
667fn closest_points_on_segments(
668    a_start: Vector3<f32>,
669    a_end: Vector3<f32>,
670    b_start: Vector3<f32>,
671    b_end: Vector3<f32>,
672) -> (Vector3<f32>, Vector3<f32>) {
673    let d1 = a_end - a_start;
674    let d2 = b_end - b_start;
675    let r = a_start - b_start;
676
677    let a = d1.dot(&d1);
678    let e = d2.dot(&d2);
679    let f = d2.dot(&r);
680
681    let epsilon = 1e-6;
682
683    if a < epsilon && e < epsilon {
684        // Both segments are points
685        return (a_start, b_start);
686    }
687
688    let mut s: f32;
689    let mut t: f32;
690
691    if a < epsilon {
692        // First segment is a point
693        s = 0.0;
694        t = (f / e).clamp(0.0, 1.0);
695    } else {
696        let c = d1.dot(&r);
697        if e < epsilon {
698            // Second segment is a point
699            t = 0.0;
700            s = (-c / a).clamp(0.0, 1.0);
701        } else {
702            // General case
703            let b = d1.dot(&d2);
704            let denom = a * e - b * b;
705
706            s = if denom.abs() > epsilon {
707                ((b * f - c * e) / denom).clamp(0.0, 1.0)
708            } else {
709                0.0
710            };
711
712            t = (b * s + f) / e;
713
714            if t < 0.0 {
715                t = 0.0;
716                s = (-c / a).clamp(0.0, 1.0);
717            } else if t > 1.0 {
718                t = 1.0;
719                s = ((b - c) / a).clamp(0.0, 1.0);
720            }
721        }
722    }
723
724    (a_start + d1 * s, b_start + d2 * t)
725}
726
727/// Find closest point on line segment to an AABB.
728fn closest_point_on_segment_to_aabb(
729    seg_start: Vector3<f32>,
730    seg_end: Vector3<f32>,
731    aabb_min: &Vector3<f32>,
732    aabb_max: &Vector3<f32>,
733) -> Vector3<f32> {
734    // Sample points along segment and find closest to AABB center
735    let aabb_center = (aabb_min + aabb_max) * 0.5;
736    closest_point_on_segment(aabb_center, seg_start, seg_end)
737}
738
739// ---------------------------------------------------------------------------
740// Ray Casting
741// ---------------------------------------------------------------------------
742
743/// A ray for intersection testing.
744#[derive(Debug, Clone, Copy)]
745pub struct Ray {
746    /// Ray origin point
747    pub origin: Vector3<f32>,
748    /// Ray direction (should be normalized)
749    pub direction: Vector3<f32>,
750}
751
752impl Ray {
753    /// Create a new ray with the given origin and direction.
754    ///
755    /// The direction will be normalized automatically.
756    pub fn new(origin: Vector3<f32>, direction: Vector3<f32>) -> Self {
757        Self {
758            origin,
759            direction: direction.normalize(),
760        }
761    }
762
763    /// Get a point along the ray at distance t.
764    pub fn point_at(&self, t: f32) -> Vector3<f32> {
765        self.origin + self.direction * t
766    }
767}
768
769/// Result of a ray cast intersection.
770#[derive(Debug, Clone, Copy)]
771pub struct RayCastHit {
772    /// ID of the body that was hit
773    pub body_id: BodyId,
774    /// Point of intersection in world space
775    pub point: Vector3<f32>,
776    /// Surface normal at the hit point
777    pub normal: Vector3<f32>,
778    /// Distance along the ray to the hit point
779    pub distance: f32,
780    /// UV texture coordinates at the hit point, if computable for this collider type
781    pub uv: Option<[f32; 2]>,
782}
783
784/// Ray vs Sphere intersection test.
785///
786/// Returns the distance to the nearest intersection point, or None if no hit.
787fn ray_intersect_sphere(ray: &Ray, center: &Vector3<f32>, radius: f32) -> Option<f32> {
788    let oc = ray.origin - center;
789    let a = ray.direction.dot(&ray.direction);
790    let b = 2.0 * oc.dot(&ray.direction);
791    let c = oc.dot(&oc) - radius * radius;
792    let discriminant = b * b - 4.0 * a * c;
793
794    if discriminant < 0.0 {
795        return None;
796    }
797
798    let sqrt_disc = discriminant.sqrt();
799    let t1 = (-b - sqrt_disc) / (2.0 * a);
800    let t2 = (-b + sqrt_disc) / (2.0 * a);
801
802    // Return nearest positive intersection
803    if t1 > 0.0 {
804        Some(t1)
805    } else if t2 > 0.0 {
806        Some(t2)
807    } else {
808        None
809    }
810}
811
812/// Ray vs AABB intersection test.
813///
814/// Returns the distance to the nearest intersection point, or None if no hit.
815fn ray_intersect_aabb(
816    ray: &Ray,
817    aabb_pos: &Vector3<f32>,
818    half_extents: &Vector3<f32>,
819) -> Option<f32> {
820    let aabb_min = aabb_pos - half_extents;
821    let aabb_max = aabb_pos + half_extents;
822
823    let mut tmin = (aabb_min.x - ray.origin.x) / ray.direction.x;
824    let mut tmax = (aabb_max.x - ray.origin.x) / ray.direction.x;
825
826    if tmin > tmax {
827        core::mem::swap(&mut tmin, &mut tmax);
828    }
829
830    let mut tymin = (aabb_min.y - ray.origin.y) / ray.direction.y;
831    let mut tymax = (aabb_max.y - ray.origin.y) / ray.direction.y;
832
833    if tymin > tymax {
834        core::mem::swap(&mut tymin, &mut tymax);
835    }
836
837    if (tmin > tymax) || (tymin > tmax) {
838        return None;
839    }
840
841    tmin = tmin.max(tymin);
842    tmax = tmax.min(tymax);
843
844    let mut tzmin = (aabb_min.z - ray.origin.z) / ray.direction.z;
845    let mut tzmax = (aabb_max.z - ray.origin.z) / ray.direction.z;
846
847    if tzmin > tzmax {
848        core::mem::swap(&mut tzmin, &mut tzmax);
849    }
850
851    if (tmin > tzmax) || (tzmin > tmax) {
852        return None;
853    }
854
855    tmin = tmin.max(tzmin);
856
857    if tmin > 0.0 {
858        Some(tmin)
859    } else if tmax > 0.0 {
860        Some(tmax)
861    } else {
862        None
863    }
864}
865
866/// Ray vs Capsule intersection test.
867///
868/// Returns the distance to the nearest intersection point, or None if no hit.
869fn ray_intersect_capsule(
870    ray: &Ray,
871    capsule_pos: &Vector3<f32>,
872    height: f32,
873    radius: f32,
874) -> Option<f32> {
875    // Get capsule endpoints
876    let half_height = height * 0.5;
877    let cap_bottom = *capsule_pos + Vector3::new(0.0, -half_height, 0.0);
878    let cap_top = *capsule_pos + Vector3::new(0.0, half_height, 0.0);
879
880    // Find closest point on capsule axis to ray
881    // Simplified: test ray against spheres at endpoints and cylinder in between
882    let mut min_t = f32::MAX;
883    let mut hit = false;
884
885    // Test bottom sphere
886    if let Some(t) = ray_intersect_sphere(ray, &cap_bottom, radius) {
887        min_t = min_t.min(t);
888        hit = true;
889    }
890
891    // Test top sphere
892    if let Some(t) = ray_intersect_sphere(ray, &cap_top, radius) {
893        min_t = min_t.min(t);
894        hit = true;
895    }
896
897    // Test cylinder body (simplified - treats as expanded line segment)
898    // For more accuracy, would need proper ray-cylinder intersection
899
900    if hit { Some(min_t) } else { None }
901}
902
903// ---------------------------------------------------------------------------
904// Constraints & Joints
905// ---------------------------------------------------------------------------
906
907/// Unique identifier for a constraint within a [`PhysicsWorld`].
908#[derive(Debug, Clone, Copy, PartialEq, Eq)]
909pub struct ConstraintId(usize);
910
911/// A constraint (joint) between two bodies.
912///
913/// Constraints restrict the relative motion of bodies. Each variant stores
914/// body-local anchor points so the constraint follows the bodies as they move
915/// and rotate.
916#[derive(Debug, Clone)]
917pub enum Constraint {
918    /// Keeps two anchor points at a fixed distance (like a rigid rod).
919    ///
920    /// - `rest_length = 0`: the anchors are pinned together (point-to-point).
921    /// - `compliance > 0`: soft spring-like behaviour (higher = softer).
922    Distance {
923        body_a: BodyId,
924        body_b: BodyId,
925        /// Anchor offset in body A's local space.
926        anchor_a: Vector3<f32>,
927        /// Anchor offset in body B's local space.
928        anchor_b: Vector3<f32>,
929        /// Target distance between the two anchor points.
930        rest_length: f32,
931        /// Compliance (inverse stiffness). 0.0 = perfectly rigid.
932        compliance: f32,
933    },
934    /// Ball-socket joint: constrains two body-local points to coincide.
935    ///
936    /// Equivalent to a distance constraint with `rest_length = 0`, but
937    /// named for clarity.
938    BallSocket {
939        body_a: BodyId,
940        body_b: BodyId,
941        /// Anchor offset in body A's local space.
942        anchor_a: Vector3<f32>,
943        /// Anchor offset in body B's local space.
944        anchor_b: Vector3<f32>,
945        /// Compliance (inverse stiffness). 0.0 = perfectly rigid.
946        compliance: f32,
947    },
948    /// Fixed (weld) joint: locks relative position and orientation.
949    ///
950    /// Stores the initial relative orientation so it can be enforced.
951    Fixed {
952        body_a: BodyId,
953        body_b: BodyId,
954        /// Anchor offset in body A's local space.
955        anchor_a: Vector3<f32>,
956        /// Anchor offset in body B's local space.
957        anchor_b: Vector3<f32>,
958        /// Target relative orientation: `q_b * q_a⁻¹` at rest.
959        relative_orientation: UnitQuaternion<f32>,
960        /// Compliance (inverse stiffness). 0.0 = perfectly rigid.
961        compliance: f32,
962    },
963}
964
965/// The physics simulation world.
966///
967/// Manages a fixed-capacity set of rigid bodies and constraints, and steps
968/// the simulation forward.
969///
970/// # Type Parameters
971/// * `N` - Maximum number of bodies (compile-time capacity).
972/// * `M` - Maximum number of constraints/joints (compile-time capacity).
973///
974/// # Example
975/// ```
976/// use embedded_3dgfx::physics::{PhysicsWorld, RigidBody, Constraint};
977/// use nalgebra::Vector3;
978///
979/// let mut world = PhysicsWorld::<8, 4>::new();
980/// world.set_gravity(Vector3::new(0.0, -9.81, 0.0));
981///
982/// let body = RigidBody::new(2.0)
983///     .with_position(Vector3::new(0.0, 5.0, 0.0));
984/// let id = world.add_body(body).unwrap();
985///
986/// world.step::<8>(1.0 / 60.0);
987/// ```
988pub struct PhysicsWorld<const N: usize, const M: usize = 0> {
989    bodies: heapless::Vec<RigidBody, N>,
990    constraints: heapless::Vec<Constraint, M>,
991    gravity: Vector3<f32>,
992    /// Number of iterations for the constraint solver per substep.
993    pub solver_iterations: u32,
994}
995
996impl<const N: usize, const M: usize> PhysicsWorld<N, M> {
997    /// Create a new physics world with no gravity.
998    pub fn new() -> Self {
999        Self {
1000            bodies: heapless::Vec::new(),
1001            constraints: heapless::Vec::new(),
1002            gravity: Vector3::zeros(),
1003            solver_iterations: 4,
1004        }
1005    }
1006
1007    /// Set the gravity vector (e.g., `Vector3::new(0.0, -9.81, 0.0)`).
1008    pub fn set_gravity(&mut self, gravity: Vector3<f32>) {
1009        self.gravity = gravity;
1010    }
1011
1012    /// Returns the current gravity vector.
1013    pub fn gravity(&self) -> Vector3<f32> {
1014        self.gravity
1015    }
1016
1017    /// Add a body to the world. Returns its [`BodyId`], or `None` if at capacity.
1018    pub fn add_body(&mut self, body: RigidBody) -> Option<BodyId> {
1019        let id = BodyId(self.bodies.len());
1020        self.bodies.push(body).ok()?;
1021        Some(id)
1022    }
1023
1024    /// Get an immutable reference to a body by its ID.
1025    pub fn body(&self, id: BodyId) -> Option<&RigidBody> {
1026        self.bodies.get(id.0)
1027    }
1028
1029    /// Get a mutable reference to a body by its ID.
1030    pub fn body_mut(&mut self, id: BodyId) -> Option<&mut RigidBody> {
1031        self.bodies.get_mut(id.0)
1032    }
1033
1034    /// Returns the total number of bodies in the world (including inactive).
1035    pub fn body_count(&self) -> usize {
1036        self.bodies.len()
1037    }
1038
1039    /// Returns the number of active bodies in the world.
1040    pub fn active_body_count(&self) -> usize {
1041        self.bodies.iter().filter(|b| b.active).count()
1042    }
1043
1044    /// Deactivate a body, effectively removing it from the simulation.
1045    ///
1046    /// The body remains in the world (its slot is preserved) but it is skipped
1047    /// during integration and collision detection. This avoids invalidating
1048    /// existing [`BodyId`]s.
1049    ///
1050    /// Returns `true` if the body was found and deactivated, `false` if the ID
1051    /// was out of bounds or the body was already inactive.
1052    pub fn remove_body(&mut self, id: BodyId) -> bool {
1053        if let Some(body) = self.bodies.get_mut(id.0) {
1054            if body.active {
1055                body.active = false;
1056                body.velocity = Vector3::zeros();
1057                body.angular_velocity = Vector3::zeros();
1058                body.force_accumulator = Vector3::zeros();
1059                body.torque_accumulator = Vector3::zeros();
1060                return true;
1061            }
1062        }
1063        false
1064    }
1065
1066    /// Set whether a body is active. Inactive bodies are skipped during
1067    /// integration and collision detection.
1068    ///
1069    /// Returns `true` if the body exists, `false` otherwise.
1070    pub fn set_active(&mut self, id: BodyId, active: bool) -> bool {
1071        if let Some(body) = self.bodies.get_mut(id.0) {
1072            body.active = active;
1073            true
1074        } else {
1075            false
1076        }
1077    }
1078
1079    /// Iterate over all bodies immutably.
1080    pub fn bodies(&self) -> impl Iterator<Item = (BodyId, &RigidBody)> {
1081        self.bodies.iter().enumerate().map(|(i, b)| (BodyId(i), b))
1082    }
1083
1084    /// Iterate over all bodies mutably.
1085    pub fn bodies_mut(&mut self) -> impl Iterator<Item = (BodyId, &mut RigidBody)> {
1086        self.bodies
1087            .iter_mut()
1088            .enumerate()
1089            .map(|(i, b)| (BodyId(i), b))
1090    }
1091
1092    // -- Ray casting --
1093
1094    /// Cast a ray through the world and find the nearest intersection.
1095    ///
1096    /// Returns information about the hit, or None if no bodies were hit.
1097    ///
1098    /// # Arguments
1099    /// * `ray` - The ray to cast
1100    /// * `max_distance` - Maximum distance to check (use f32::MAX for infinite)
1101    ///
1102    /// # Example
1103    /// ```
1104    /// use embedded_3dgfx::physics::{PhysicsWorld, Ray};
1105    /// use nalgebra::Vector3;
1106    ///
1107    /// let world = PhysicsWorld::<16>::new();
1108    /// let ray = Ray::new(
1109    ///     Vector3::new(0.0, 10.0, 0.0),
1110    ///     Vector3::new(0.0, -1.0, 0.0)
1111    /// );
1112    ///
1113    /// if let Some(hit) = world.ray_cast(&ray, 100.0) {
1114    ///     println!("Hit body at distance: {}", hit.distance);
1115    /// }
1116    /// ```
1117    pub fn ray_cast(&self, ray: &Ray, max_distance: f32) -> Option<RayCastHit> {
1118        let mut nearest_hit: Option<RayCastHit> = None;
1119        let mut min_distance = max_distance;
1120
1121        for (i, body) in self.bodies.iter().enumerate() {
1122            if !body.active {
1123                continue;
1124            }
1125
1126            let collider = match &body.collider {
1127                Some(c) => c,
1128                None => continue,
1129            };
1130
1131            let distance = match collider {
1132                Collider::Sphere { radius } => ray_intersect_sphere(ray, &body.position, *radius),
1133                Collider::Aabb { half_extents } => {
1134                    ray_intersect_aabb(ray, &body.position, half_extents)
1135                }
1136                Collider::Capsule { height, radius } => {
1137                    ray_intersect_capsule(ray, &body.position, *height, *radius)
1138                }
1139            };
1140
1141            if let Some(dist) = distance {
1142                if dist < min_distance {
1143                    let point = ray.point_at(dist);
1144                    let normal = match collider {
1145                        Collider::Sphere { .. } => (point - body.position).normalize(),
1146                        Collider::Aabb { .. } => {
1147                            // Approximate normal from hit point to AABB center
1148                            (point - body.position).normalize()
1149                        }
1150                        Collider::Capsule { height, .. } => {
1151                            // Get closest point on capsule axis
1152                            let half_height = height * 0.5;
1153                            let cap_bottom = body.position + Vector3::new(0.0, -half_height, 0.0);
1154                            let cap_top = body.position + Vector3::new(0.0, half_height, 0.0);
1155                            let closest = closest_point_on_segment(point, cap_bottom, cap_top);
1156                            (point - closest).normalize()
1157                        }
1158                    };
1159
1160                    let uv = match collider {
1161                        Collider::Sphere { .. } => {
1162                            // Spherical UV from surface normal
1163                            let theta = normal.z.atan2(normal.x); // -PI..PI
1164                            let phi = normal.y.clamp(-1.0, 1.0).asin(); // -PI/2..PI/2
1165                            let u = theta / (2.0 * core::f32::consts::PI) + 0.5;
1166                            let v = 0.5 - phi / core::f32::consts::PI;
1167                            Some([u, v])
1168                        }
1169                        Collider::Aabb { half_extents } => {
1170                            // Box UV: determine face from dominant normal axis, then planar project
1171                            let abs_n = [normal.x.abs(), normal.y.abs(), normal.z.abs()];
1172                            let (u, v) = if abs_n[0] >= abs_n[1] && abs_n[0] >= abs_n[2] {
1173                                // X face: project on YZ
1174                                (
1175                                    0.5 + (point.y - body.position.y) / (2.0 * half_extents.y),
1176                                    0.5 + (point.z - body.position.z) / (2.0 * half_extents.z),
1177                                )
1178                            } else if abs_n[1] >= abs_n[0] && abs_n[1] >= abs_n[2] {
1179                                // Y face: project on XZ
1180                                (
1181                                    0.5 + (point.x - body.position.x) / (2.0 * half_extents.x),
1182                                    0.5 + (point.z - body.position.z) / (2.0 * half_extents.z),
1183                                )
1184                            } else {
1185                                // Z face: project on XY
1186                                (
1187                                    0.5 + (point.x - body.position.x) / (2.0 * half_extents.x),
1188                                    0.5 + (point.y - body.position.y) / (2.0 * half_extents.y),
1189                                )
1190                            };
1191                            Some([u.clamp(0.0, 1.0), v.clamp(0.0, 1.0)])
1192                        }
1193                        Collider::Capsule { height, .. } => {
1194                            // Cylindrical UV: theta for U, normalized height for V
1195                            let half_height = height * 0.5;
1196                            let local_y =
1197                                (point.y - body.position.y).clamp(-half_height, half_height);
1198                            let theta = normal.z.atan2(normal.x);
1199                            let u = theta / (2.0 * core::f32::consts::PI) + 0.5;
1200                            let v = (local_y + half_height) / height;
1201                            Some([u, v])
1202                        }
1203                    };
1204
1205                    min_distance = dist;
1206                    nearest_hit = Some(RayCastHit {
1207                        body_id: BodyId(i),
1208                        point,
1209                        normal,
1210                        distance: dist,
1211                        uv,
1212                    });
1213                }
1214            }
1215        }
1216
1217        nearest_hit
1218    }
1219
1220    // -- Constraint management --
1221
1222    /// Add a constraint to the world. Returns its [`ConstraintId`], or `None`
1223    /// if at capacity.
1224    pub fn add_constraint(&mut self, constraint: Constraint) -> Option<ConstraintId> {
1225        let id = ConstraintId(self.constraints.len());
1226        self.constraints.push(constraint).ok()?;
1227        Some(id)
1228    }
1229
1230    /// Get an immutable reference to a constraint by its ID.
1231    pub fn constraint(&self, id: ConstraintId) -> Option<&Constraint> {
1232        self.constraints.get(id.0)
1233    }
1234
1235    /// Get a mutable reference to a constraint by its ID.
1236    pub fn constraint_mut(&mut self, id: ConstraintId) -> Option<&mut Constraint> {
1237        self.constraints.get_mut(id.0)
1238    }
1239
1240    /// Remove a constraint by ID (swap-removes; invalidates the last ID).
1241    ///
1242    /// Returns `true` if a constraint was removed, `false` if the ID was
1243    /// out of bounds.
1244    pub fn remove_constraint(&mut self, id: ConstraintId) -> bool {
1245        if id.0 < self.constraints.len() {
1246            self.constraints.swap_remove(id.0);
1247            true
1248        } else {
1249            false
1250        }
1251    }
1252
1253    /// Returns the number of constraints in the world.
1254    pub fn constraint_count(&self) -> usize {
1255        self.constraints.len()
1256    }
1257
1258    /// Helper: create a distance constraint between two bodies.
1259    ///
1260    /// Anchors are in body-local space. The rest length is computed
1261    /// automatically from the current body positions.
1262    pub fn add_distance_constraint(
1263        &mut self,
1264        body_a: BodyId,
1265        anchor_a: Vector3<f32>,
1266        body_b: BodyId,
1267        anchor_b: Vector3<f32>,
1268        compliance: f32,
1269    ) -> Option<ConstraintId> {
1270        let world_a = self.body_to_world(body_a, &anchor_a)?;
1271        let world_b = self.body_to_world(body_b, &anchor_b)?;
1272        let rest_length = (world_b - world_a).norm();
1273
1274        self.add_constraint(Constraint::Distance {
1275            body_a,
1276            body_b,
1277            anchor_a,
1278            anchor_b,
1279            rest_length,
1280            compliance,
1281        })
1282    }
1283
1284    /// Helper: create a ball-socket joint between two bodies.
1285    ///
1286    /// Anchors are in body-local space. The two anchor points will be
1287    /// constrained to coincide (distance = 0).
1288    pub fn add_ball_socket(
1289        &mut self,
1290        body_a: BodyId,
1291        anchor_a: Vector3<f32>,
1292        body_b: BodyId,
1293        anchor_b: Vector3<f32>,
1294        compliance: f32,
1295    ) -> Option<ConstraintId> {
1296        self.add_constraint(Constraint::BallSocket {
1297            body_a,
1298            body_b,
1299            anchor_a,
1300            anchor_b,
1301            compliance,
1302        })
1303    }
1304
1305    /// Helper: create a fixed (weld) joint between two bodies.
1306    ///
1307    /// Stores the current relative orientation as the target.
1308    pub fn add_fixed_joint(
1309        &mut self,
1310        body_a: BodyId,
1311        anchor_a: Vector3<f32>,
1312        body_b: BodyId,
1313        anchor_b: Vector3<f32>,
1314        compliance: f32,
1315    ) -> Option<ConstraintId> {
1316        let qa = self.bodies.get(body_a.0)?.orientation;
1317        let qb = self.bodies.get(body_b.0)?.orientation;
1318        let relative_orientation = qb * qa.inverse();
1319
1320        self.add_constraint(Constraint::Fixed {
1321            body_a,
1322            body_b,
1323            anchor_a,
1324            anchor_b,
1325            relative_orientation,
1326            compliance,
1327        })
1328    }
1329
1330    /// Convert a body-local point to world space.
1331    fn body_to_world(&self, id: BodyId, local_point: &Vector3<f32>) -> Option<Vector3<f32>> {
1332        let body = self.bodies.get(id.0)?;
1333        Some(body.position + body.orientation * local_point)
1334    }
1335
1336    /// Compute the world-space AABB `(min_x, max_x)` along the X axis for a body.
1337    #[inline]
1338    fn body_x_extent(body: &RigidBody, collider: &Collider) -> (f32, f32) {
1339        let px = body.position.x;
1340        match collider {
1341            Collider::Sphere { radius } => (px - radius, px + radius),
1342            Collider::Aabb { half_extents } => (px - half_extents.x, px + half_extents.x),
1343            Collider::Capsule { radius, .. } => (px - radius, px + radius),
1344        }
1345    }
1346
1347    /// Detect all collisions between bodies with colliders.
1348    ///
1349    /// Uses a sort-and-sweep broad phase on the X axis to prune pairs that
1350    /// cannot possibly overlap, then runs the narrow-phase `collide()` test
1351    /// only on candidate pairs.
1352    ///
1353    /// Returns contacts in a fixed-capacity buffer. The `C` const generic sets the
1354    /// maximum number of contacts per detection pass. For `N` bodies, worst case is
1355    /// `N*(N-1)/2` pairs, so choose `C` accordingly.
1356    ///
1357    /// Pairs where both bodies lack a collider are skipped.
1358    pub fn detect_collisions<const C: usize>(&self) -> heapless::Vec<Contact, C> {
1359        let mut contacts = heapless::Vec::new();
1360
1361        // Build list of (min_x, max_x, body_index) for active bodies with colliders
1362        let mut entries = heapless::Vec::<(f32, f32, usize), N>::new();
1363        for (i, body) in self.bodies.iter().enumerate() {
1364            if !body.active {
1365                continue;
1366            }
1367            if let Some(ref col) = body.collider {
1368                let (min_x, max_x) = Self::body_x_extent(body, col);
1369                let _ = entries.push((min_x, max_x, i));
1370            }
1371        }
1372
1373        // Sort by min_x (insertion sort — good for small N and nearly-sorted data)
1374        for i in 1..entries.len() {
1375            let mut j = i;
1376            while j > 0 && entries[j].0 < entries[j - 1].0 {
1377                entries.swap(j, j - 1);
1378                j -= 1;
1379            }
1380        }
1381
1382        // Sweep: for each entry, check overlapping entries on X axis
1383        let len = entries.len();
1384        for i in 0..len {
1385            let (_, max_x_i, idx_a) = entries[i];
1386            let body_a = &self.bodies[idx_a];
1387            let col_a = body_a.collider.as_ref().unwrap();
1388
1389            for j in (i + 1)..len {
1390                let (min_x_j, _, idx_b) = entries[j];
1391
1392                // Since entries are sorted by min_x, once min_x_j > max_x_i
1393                // all subsequent entries are also non-overlapping on X
1394                if min_x_j > max_x_i {
1395                    break;
1396                }
1397
1398                let body_b = &self.bodies[idx_b];
1399                let col_b = body_b.collider.as_ref().unwrap();
1400
1401                // Skip pairs where both are static
1402                if body_a.body_type == BodyType::Static && body_b.body_type == BodyType::Static {
1403                    continue;
1404                }
1405
1406                if let Some((normal, penetration)) =
1407                    collide(&body_a.position, col_a, &body_b.position, col_b)
1408                {
1409                    let _ = contacts.push(Contact {
1410                        body_a: BodyId(idx_a),
1411                        body_b: BodyId(idx_b),
1412                        normal,
1413                        penetration,
1414                    });
1415                }
1416            }
1417        }
1418
1419        contacts
1420    }
1421
1422    /// Resolve a set of contacts by applying positional correction, impulse response
1423    /// (linear + angular), and Coulomb friction.
1424    ///
1425    /// Contact-point velocity includes both linear and angular contributions:
1426    /// `v_contact = v_linear + ω × r`, where `r` is the vector from the body
1427    /// center to the contact point. Impulses produce both linear velocity changes
1428    /// and torques via `τ = r × J`.
1429    pub fn resolve_contacts(&mut self, contacts: &[Contact]) {
1430        for contact in contacts {
1431            let a = contact.body_a.0;
1432            let b = contact.body_b.0;
1433
1434            let inv_mass_a = self.bodies[a].inv_mass;
1435            let inv_mass_b = self.bodies[b].inv_mass;
1436
1437            // Compute contact point: on the surface of each body along the normal.
1438            // For simplicity, use the midpoint of the overlap region along the normal.
1439            let contact_point =
1440                self.bodies[a].position + contact.normal * (contact.penetration * 0.5);
1441
1442            let ra = contact_point - self.bodies[a].position;
1443            let rb = contact_point - self.bodies[b].position;
1444
1445            let inv_inertia_a = self.bodies[a].inv_inertia_world();
1446            let inv_inertia_b = self.bodies[b].inv_inertia_world();
1447
1448            // --- Positional correction (push bodies apart) ---
1449            let inv_mass_sum = inv_mass_a + inv_mass_b;
1450            if inv_mass_sum == 0.0 {
1451                continue; // Both static
1452            }
1453            let correction = contact.normal * (contact.penetration / inv_mass_sum);
1454            self.bodies[a].position -= correction * inv_mass_a;
1455            self.bodies[b].position += correction * inv_mass_b;
1456
1457            // --- Contact-point velocity (linear + angular) ---
1458            let vel_a = self.bodies[a].velocity + self.bodies[a].angular_velocity.cross(&ra);
1459            let vel_b = self.bodies[b].velocity + self.bodies[b].angular_velocity.cross(&rb);
1460            let relative_vel = vel_b - vel_a;
1461            let vel_along_normal = relative_vel.dot(&contact.normal);
1462
1463            // Only resolve if bodies are moving toward each other
1464            if vel_along_normal >= 0.0 {
1465                continue;
1466            }
1467
1468            let restitution = self.bodies[a].restitution.min(self.bodies[b].restitution);
1469
1470            // Effective mass including angular contribution:
1471            // 1/m_eff = 1/m_a + 1/m_b + (I_a⁻¹(ra×n))×ra·n + (I_b⁻¹(rb×n))×rb·n
1472            let ra_cross_n = ra.cross(&contact.normal);
1473            let rb_cross_n = rb.cross(&contact.normal);
1474            let angular_term_a = (inv_inertia_a * ra_cross_n).cross(&ra).dot(&contact.normal);
1475            let angular_term_b = (inv_inertia_b * rb_cross_n).cross(&rb).dot(&contact.normal);
1476            let eff_mass_inv = inv_mass_a + inv_mass_b + angular_term_a + angular_term_b;
1477
1478            if eff_mass_inv <= 0.0 {
1479                continue;
1480            }
1481
1482            // Normal impulse magnitude
1483            let j = -(1.0 + restitution) * vel_along_normal / eff_mass_inv;
1484
1485            let impulse = contact.normal * j;
1486            self.bodies[a].velocity -= impulse * inv_mass_a;
1487            self.bodies[b].velocity += impulse * inv_mass_b;
1488            self.bodies[a].angular_velocity -= inv_inertia_a * ra.cross(&impulse);
1489            self.bodies[b].angular_velocity += inv_inertia_b * rb.cross(&impulse);
1490
1491            // --- Friction impulse (Coulomb model) ---
1492            let mu_a = self.bodies[a].friction;
1493            let mu_b = self.bodies[b].friction;
1494            let mu = (mu_a * mu_b).sqrt();
1495
1496            if mu > 1e-6 {
1497                // Recompute relative velocity at contact point after normal impulse
1498                let vel_a = self.bodies[a].velocity + self.bodies[a].angular_velocity.cross(&ra);
1499                let vel_b = self.bodies[b].velocity + self.bodies[b].angular_velocity.cross(&rb);
1500                let relative_vel = vel_b - vel_a;
1501
1502                let vn = relative_vel.dot(&contact.normal);
1503                let tangent_vel = relative_vel - contact.normal * vn;
1504                let tangent_speed = tangent_vel.norm();
1505
1506                if tangent_speed > 1e-6 {
1507                    let tangent = tangent_vel / tangent_speed;
1508
1509                    // Effective mass in the tangent direction (with angular terms)
1510                    let ra_cross_t = ra.cross(&tangent);
1511                    let rb_cross_t = rb.cross(&tangent);
1512                    let ang_t_a = (inv_inertia_a * ra_cross_t).cross(&ra).dot(&tangent);
1513                    let ang_t_b = (inv_inertia_b * rb_cross_t).cross(&rb).dot(&tangent);
1514                    let eff_mass_t_inv = inv_mass_a + inv_mass_b + ang_t_a + ang_t_b;
1515
1516                    if eff_mass_t_inv > 0.0 {
1517                        // Clamp by Coulomb cone: |jt| <= mu * |jn|
1518                        let jt = (-tangent_speed / eff_mass_t_inv).max(-mu * j);
1519
1520                        let friction_impulse = tangent * jt;
1521                        self.bodies[a].velocity -= friction_impulse * inv_mass_a;
1522                        self.bodies[b].velocity += friction_impulse * inv_mass_b;
1523                        self.bodies[a].angular_velocity -=
1524                            inv_inertia_a * ra.cross(&friction_impulse);
1525                        self.bodies[b].angular_velocity +=
1526                            inv_inertia_b * rb.cross(&friction_impulse);
1527                    }
1528                }
1529            }
1530        }
1531    }
1532
1533    // -- Constraint solver --
1534
1535    /// Solve all constraints using position-based correction.
1536    ///
1537    /// Uses Extended Position-Based Dynamics (XPBD) style positional correction
1538    /// with compliance for soft constraints. Each constraint is solved
1539    /// `solver_iterations` times per call for convergence.
1540    pub fn solve_constraints(&mut self, dt: f32) {
1541        if self.constraints.is_empty() || dt <= 0.0 {
1542            return;
1543        }
1544
1545        let iterations = self.solver_iterations;
1546        for _ in 0..iterations {
1547            // Iterate constraints by index to satisfy the borrow checker
1548            for ci in 0..self.constraints.len() {
1549                // Clone the constraint to avoid borrow conflict
1550                let constraint = self.constraints[ci].clone();
1551                match &constraint {
1552                    Constraint::Distance {
1553                        body_a,
1554                        body_b,
1555                        anchor_a,
1556                        anchor_b,
1557                        rest_length,
1558                        compliance,
1559                    } => {
1560                        self.solve_distance(
1561                            body_a.0,
1562                            anchor_a,
1563                            body_b.0,
1564                            anchor_b,
1565                            *rest_length,
1566                            *compliance,
1567                            dt,
1568                        );
1569                    }
1570                    Constraint::BallSocket {
1571                        body_a,
1572                        body_b,
1573                        anchor_a,
1574                        anchor_b,
1575                        compliance,
1576                    } => {
1577                        self.solve_distance(
1578                            body_a.0,
1579                            anchor_a,
1580                            body_b.0,
1581                            anchor_b,
1582                            0.0,
1583                            *compliance,
1584                            dt,
1585                        );
1586                    }
1587                    Constraint::Fixed {
1588                        body_a,
1589                        body_b,
1590                        anchor_a,
1591                        anchor_b,
1592                        relative_orientation,
1593                        compliance,
1594                    } => {
1595                        // Positional part (same as ball-socket)
1596                        self.solve_distance(
1597                            body_a.0,
1598                            anchor_a,
1599                            body_b.0,
1600                            anchor_b,
1601                            0.0,
1602                            *compliance,
1603                            dt,
1604                        );
1605                        // Rotational part
1606                        self.solve_orientation(
1607                            body_a.0,
1608                            body_b.0,
1609                            relative_orientation,
1610                            *compliance,
1611                            dt,
1612                        );
1613                    }
1614                }
1615            }
1616        }
1617    }
1618
1619    /// Solve a single distance constraint between two anchor points.
1620    ///
1621    /// Uses XPBD positional correction: applies positional deltas and
1622    /// corresponding velocity updates to both bodies.
1623    fn solve_distance(
1624        &mut self,
1625        a: usize,
1626        anchor_a: &Vector3<f32>,
1627        b: usize,
1628        anchor_b: &Vector3<f32>,
1629        rest_length: f32,
1630        compliance: f32,
1631        dt: f32,
1632    ) {
1633        if !self.bodies[a].active && !self.bodies[b].active {
1634            return;
1635        }
1636
1637        // World-space anchor positions
1638        let ra = self.bodies[a].orientation * anchor_a;
1639        let rb = self.bodies[b].orientation * anchor_b;
1640        let world_a = self.bodies[a].position + ra;
1641        let world_b = self.bodies[b].position + rb;
1642
1643        let diff = world_b - world_a;
1644        let dist = diff.norm();
1645
1646        let error = dist - rest_length;
1647        if error.abs() < 1e-6 {
1648            return;
1649        }
1650
1651        // Direction of correction
1652        let n = if dist > 1e-6 {
1653            diff / dist
1654        } else {
1655            Vector3::new(0.0, 1.0, 0.0) // Arbitrary axis for zero-length
1656        };
1657
1658        // Generalized inverse masses (linear + angular contribution)
1659        let inv_mass_a = self.bodies[a].inv_mass;
1660        let inv_mass_b = self.bodies[b].inv_mass;
1661        let inv_inertia_a = self.bodies[a].inv_inertia_world();
1662        let inv_inertia_b = self.bodies[b].inv_inertia_world();
1663
1664        let ra_cross_n = ra.cross(&n);
1665        let rb_cross_n = rb.cross(&n);
1666        let w_a = inv_mass_a + ra_cross_n.dot(&(inv_inertia_a * ra_cross_n));
1667        let w_b = inv_mass_b + rb_cross_n.dot(&(inv_inertia_b * rb_cross_n));
1668        let w_sum = w_a + w_b;
1669
1670        if w_sum <= 0.0 {
1671            return;
1672        }
1673
1674        // XPBD: compliance term scaled by dt²
1675        let alpha = compliance / (dt * dt);
1676        let delta_lambda = -error / (w_sum + alpha);
1677        let correction = n * delta_lambda;
1678
1679        // Apply positional correction
1680        self.bodies[a].position -= correction * inv_mass_a;
1681        self.bodies[b].position += correction * inv_mass_b;
1682
1683        // Apply angular correction
1684        self.bodies[a].angular_velocity -= inv_inertia_a * ra.cross(&correction) / dt;
1685        self.bodies[b].angular_velocity += inv_inertia_b * rb.cross(&correction) / dt;
1686
1687        // Update linear velocities from positional correction
1688        self.bodies[a].velocity -= correction * inv_mass_a / dt;
1689        self.bodies[b].velocity += correction * inv_mass_b / dt;
1690    }
1691
1692    /// Solve the rotational part of a fixed (weld) joint.
1693    ///
1694    /// Applies angular corrections to enforce the target relative orientation.
1695    fn solve_orientation(
1696        &mut self,
1697        a: usize,
1698        b: usize,
1699        target_rel: &UnitQuaternion<f32>,
1700        compliance: f32,
1701        dt: f32,
1702    ) {
1703        if !self.bodies[a].active && !self.bodies[b].active {
1704            return;
1705        }
1706
1707        let qa = self.bodies[a].orientation;
1708        let qb = self.bodies[b].orientation;
1709
1710        // Current relative orientation vs target
1711        let current_rel = qb * qa.inverse();
1712        let error_q = current_rel * target_rel.inverse();
1713
1714        // Extract the rotation axis and angle from the error quaternion
1715        let error_inner = error_q.into_inner();
1716        let error_vec = Vector3::new(error_inner.i, error_inner.j, error_inner.k);
1717
1718        // For small angles: rotation vector ≈ 2 * [i, j, k] (when w > 0)
1719        let error = if error_inner.w >= 0.0 {
1720            error_vec * 2.0
1721        } else {
1722            -error_vec * 2.0
1723        };
1724
1725        if error.norm_squared() < 1e-10 {
1726            return;
1727        }
1728
1729        let inv_inertia_a = self.bodies[a].inv_inertia_world();
1730        let inv_inertia_b = self.bodies[b].inv_inertia_world();
1731
1732        // Generalized inverse masses for rotation
1733        // For each axis, the angular "mass" contribution is n · (I⁻¹ * n)
1734        // Simplified: use the sum of diagonal elements as effective inverse mass
1735        let w_a = if self.bodies[a].body_type == BodyType::Dynamic {
1736            inv_inertia_a[(0, 0)] + inv_inertia_a[(1, 1)] + inv_inertia_a[(2, 2)]
1737        } else {
1738            0.0
1739        };
1740        let w_b = if self.bodies[b].body_type == BodyType::Dynamic {
1741            inv_inertia_b[(0, 0)] + inv_inertia_b[(1, 1)] + inv_inertia_b[(2, 2)]
1742        } else {
1743            0.0
1744        };
1745        let w_sum = w_a + w_b;
1746        if w_sum <= 0.0 {
1747            return;
1748        }
1749
1750        let alpha = compliance / (dt * dt);
1751        let delta_lambda = -error / (w_sum + alpha);
1752
1753        // Apply angular correction
1754        if self.bodies[a].body_type == BodyType::Dynamic {
1755            self.bodies[a].angular_velocity -= inv_inertia_a * delta_lambda / dt;
1756        }
1757        if self.bodies[b].body_type == BodyType::Dynamic {
1758            self.bodies[b].angular_velocity += inv_inertia_b * delta_lambda / dt;
1759        }
1760    }
1761
1762    /// Advance the simulation by `dt` seconds.
1763    ///
1764    /// Integrates all dynamic bodies, detects and resolves collisions, then
1765    /// solves constraints.
1766    ///
1767    /// The `C` const generic sets the maximum number of collision contacts per step.
1768    pub fn step<const C: usize>(&mut self, dt: f32) {
1769        let gravity = self.gravity;
1770        for body in self.bodies.iter_mut() {
1771            if body.active {
1772                body.integrate(dt, gravity);
1773            }
1774        }
1775
1776        let contacts = self.detect_collisions::<C>();
1777        self.resolve_contacts(&contacts);
1778        self.solve_constraints(dt);
1779    }
1780
1781    /// Advance the simulation using fixed-size substeps for stability.
1782    ///
1783    /// Divides `dt` into `substeps` equal intervals. Each substep runs integration,
1784    /// collision detection/response, and constraint solving.
1785    pub fn step_fixed<const C: usize>(&mut self, dt: f32, substeps: u32) {
1786        let sub_dt = dt / substeps as f32;
1787        for _ in 0..substeps {
1788            self.step::<C>(sub_dt);
1789        }
1790    }
1791}
1792
1793/// Helper to sync a [`RigidBody`]'s position and orientation back to a
1794/// [`K3dMesh`](crate::mesh::K3dMesh).
1795///
1796/// Call this after `PhysicsWorld::step()` to update mesh transforms.
1797///
1798/// # Example
1799/// ```ignore
1800/// physics_world.step::<16>(dt);
1801/// for (id, body) in physics_world.bodies() {
1802///     sync_body_to_mesh(body, &mut meshes[id]);
1803/// }
1804/// ```
1805pub fn sync_body_to_mesh(body: &RigidBody, mesh: &mut crate::mesh::K3dMesh<'_>) {
1806    mesh.set_position(body.position.x, body.position.y, body.position.z);
1807    mesh.set_rotation(body.orientation);
1808}
1809
1810#[cfg(test)]
1811mod tests {
1812    extern crate std;
1813    use super::*;
1814
1815    const EPSILON: f32 = 1e-4;
1816
1817    fn approx_eq(a: f32, b: f32) -> bool {
1818        (a - b).abs() < EPSILON
1819    }
1820
1821    fn approx_vec_eq(a: &Vector3<f32>, b: &Vector3<f32>) -> bool {
1822        approx_eq(a.x, b.x) && approx_eq(a.y, b.y) && approx_eq(a.z, b.z)
1823    }
1824
1825    // -- RigidBody tests --
1826
1827    #[test]
1828    fn test_body_creation() {
1829        let body = RigidBody::new(5.0);
1830        assert_eq!(body.mass, 5.0);
1831        assert!(approx_eq(body.inv_mass, 0.2));
1832        assert_eq!(body.body_type, BodyType::Dynamic);
1833        assert!(approx_vec_eq(&body.position, &Vector3::zeros()));
1834        assert!(approx_vec_eq(&body.velocity, &Vector3::zeros()));
1835    }
1836
1837    #[test]
1838    fn test_static_body() {
1839        let body = RigidBody::new_static();
1840        assert_eq!(body.body_type, BodyType::Static);
1841        assert_eq!(body.inv_mass, 0.0);
1842        assert!(body.mass.is_infinite());
1843    }
1844
1845    #[test]
1846    #[should_panic]
1847    fn test_body_zero_mass_panics() {
1848        RigidBody::new(0.0);
1849    }
1850
1851    #[test]
1852    #[should_panic]
1853    fn test_body_negative_mass_panics() {
1854        RigidBody::new(-1.0);
1855    }
1856
1857    #[test]
1858    fn test_builder_pattern() {
1859        let body = RigidBody::new(1.0)
1860            .with_position(Vector3::new(1.0, 2.0, 3.0))
1861            .with_velocity(Vector3::new(0.0, 5.0, 0.0))
1862            .with_restitution(0.8)
1863            .with_damping(0.05);
1864
1865        assert!(approx_vec_eq(&body.position, &Vector3::new(1.0, 2.0, 3.0)));
1866        assert!(approx_vec_eq(&body.velocity, &Vector3::new(0.0, 5.0, 0.0)));
1867        assert!(approx_eq(body.restitution, 0.8));
1868        assert!(approx_eq(body.damping, 0.05));
1869    }
1870
1871    #[test]
1872    fn test_apply_force() {
1873        let mut body = RigidBody::new(1.0);
1874        body.apply_force(Vector3::new(10.0, 0.0, 0.0));
1875        body.apply_force(Vector3::new(0.0, 5.0, 0.0));
1876
1877        // Forces accumulate
1878        assert!(approx_vec_eq(
1879            &body.force_accumulator,
1880            &Vector3::new(10.0, 5.0, 0.0)
1881        ));
1882    }
1883
1884    #[test]
1885    fn test_apply_impulse() {
1886        let mut body = RigidBody::new(2.0);
1887        body.apply_impulse(Vector3::new(10.0, 0.0, 0.0));
1888
1889        // delta_v = impulse / mass = 10 / 2 = 5
1890        assert!(approx_vec_eq(&body.velocity, &Vector3::new(5.0, 0.0, 0.0)));
1891    }
1892
1893    #[test]
1894    fn test_impulse_on_static_body_ignored() {
1895        let mut body = RigidBody::new_static();
1896        body.apply_impulse(Vector3::new(100.0, 0.0, 0.0));
1897        assert!(approx_vec_eq(&body.velocity, &Vector3::zeros()));
1898    }
1899
1900    #[test]
1901    fn test_speed() {
1902        let body = RigidBody::new(1.0).with_velocity(Vector3::new(3.0, 4.0, 0.0));
1903        assert!(approx_eq(body.speed(), 5.0));
1904    }
1905
1906    #[test]
1907    fn test_kinetic_energy() {
1908        let body = RigidBody::new(2.0).with_velocity(Vector3::new(3.0, 0.0, 0.0));
1909        // KE = 0.5 * 2 * 9 = 9
1910        assert!(approx_eq(body.kinetic_energy(), 9.0));
1911    }
1912
1913    // -- PhysicsWorld tests --
1914
1915    #[test]
1916    fn test_world_creation() {
1917        let world = PhysicsWorld::<8>::new();
1918        assert_eq!(world.body_count(), 0);
1919        assert!(approx_vec_eq(&world.gravity(), &Vector3::zeros()));
1920    }
1921
1922    #[test]
1923    fn test_add_and_get_body() {
1924        let mut world = PhysicsWorld::<8>::new();
1925        let body = RigidBody::new(1.0).with_position(Vector3::new(1.0, 2.0, 3.0));
1926        let id = world.add_body(body).unwrap();
1927
1928        assert_eq!(world.body_count(), 1);
1929        let b = world.body(id).unwrap();
1930        assert!(approx_vec_eq(&b.position, &Vector3::new(1.0, 2.0, 3.0)));
1931    }
1932
1933    #[test]
1934    fn test_add_body_at_capacity() {
1935        let mut world = PhysicsWorld::<2>::new();
1936        assert!(world.add_body(RigidBody::new(1.0)).is_some());
1937        assert!(world.add_body(RigidBody::new(1.0)).is_some());
1938        assert!(world.add_body(RigidBody::new(1.0)).is_none()); // at capacity
1939    }
1940
1941    #[test]
1942    fn test_gravity_freefall() {
1943        let mut world = PhysicsWorld::<4>::new();
1944        world.set_gravity(Vector3::new(0.0, -10.0, 0.0));
1945
1946        let body = RigidBody::new(1.0)
1947            .with_position(Vector3::new(0.0, 100.0, 0.0))
1948            .with_damping(0.0);
1949        let id = world.add_body(body).unwrap();
1950
1951        // Step 1 second
1952        world.step::<4>(1.0);
1953
1954        let b = world.body(id).unwrap();
1955        // After 1s of freefall at -10 m/s²:
1956        // v = 0 + (-10)*1 = -10 m/s
1957        // p = 100 + (-10)*1 = 90 m  (semi-implicit: velocity updated first, then position)
1958        assert!(approx_eq(b.velocity.y, -10.0));
1959        assert!(approx_eq(b.position.y, 90.0));
1960    }
1961
1962    #[test]
1963    fn test_static_body_not_affected_by_gravity() {
1964        let mut world = PhysicsWorld::<4>::new();
1965        world.set_gravity(Vector3::new(0.0, -9.81, 0.0));
1966
1967        let body = RigidBody::new_static().with_position(Vector3::new(0.0, 0.0, 0.0));
1968        let id = world.add_body(body).unwrap();
1969
1970        world.step::<4>(1.0);
1971
1972        let b = world.body(id).unwrap();
1973        assert!(approx_vec_eq(&b.position, &Vector3::zeros()));
1974        assert!(approx_vec_eq(&b.velocity, &Vector3::zeros()));
1975    }
1976
1977    #[test]
1978    fn test_force_accumulation_and_clearing() {
1979        let mut world = PhysicsWorld::<4>::new();
1980
1981        let body = RigidBody::new(1.0).with_damping(0.0);
1982        let id = world.add_body(body).unwrap();
1983
1984        // Apply a force and step
1985        world
1986            .body_mut(id)
1987            .unwrap()
1988            .apply_force(Vector3::new(10.0, 0.0, 0.0));
1989        world.step::<4>(1.0);
1990
1991        let b = world.body(id).unwrap();
1992        assert!(approx_eq(b.velocity.x, 10.0));
1993
1994        // Forces should be cleared — another step with no forces should not accelerate further
1995        world.step::<4>(1.0);
1996        let b = world.body(id).unwrap();
1997        assert!(approx_eq(b.velocity.x, 10.0)); // unchanged (no damping, no forces)
1998    }
1999
2000    #[test]
2001    fn test_damping_reduces_velocity() {
2002        let mut world = PhysicsWorld::<4>::new();
2003
2004        let body = RigidBody::new(1.0)
2005            .with_velocity(Vector3::new(10.0, 0.0, 0.0))
2006            .with_damping(0.1);
2007        let id = world.add_body(body).unwrap();
2008
2009        world.step::<4>(1.0);
2010
2011        let b = world.body(id).unwrap();
2012        // velocity should be reduced by damping
2013        assert!(b.velocity.x < 10.0);
2014        assert!(b.velocity.x > 0.0);
2015    }
2016
2017    #[test]
2018    fn test_step_fixed_substeps() {
2019        let mut world_single = PhysicsWorld::<4>::new();
2020        let mut world_sub = PhysicsWorld::<4>::new();
2021
2022        world_single.set_gravity(Vector3::new(0.0, -10.0, 0.0));
2023        world_sub.set_gravity(Vector3::new(0.0, -10.0, 0.0));
2024
2025        let body = RigidBody::new(1.0)
2026            .with_position(Vector3::new(0.0, 100.0, 0.0))
2027            .with_damping(0.0);
2028
2029        let id1 = world_single.add_body(body.clone()).unwrap();
2030        let id2 = world_sub.add_body(body).unwrap();
2031
2032        // Single large step vs 10 substeps
2033        world_single.step::<4>(1.0);
2034        world_sub.step_fixed::<4>(1.0, 10);
2035
2036        // Both should give similar results (substeps are more accurate)
2037        let b1 = world_single.body(id1).unwrap();
2038        let b2 = world_sub.body(id2).unwrap();
2039
2040        // Velocities should be the same (gravity is constant)
2041        assert!(approx_eq(b1.velocity.y, b2.velocity.y));
2042    }
2043
2044    #[test]
2045    fn test_bodies_iterator() {
2046        let mut world = PhysicsWorld::<4>::new();
2047        world
2048            .add_body(RigidBody::new(1.0).with_position(Vector3::new(1.0, 0.0, 0.0)))
2049            .unwrap();
2050        world
2051            .add_body(RigidBody::new(2.0).with_position(Vector3::new(2.0, 0.0, 0.0)))
2052            .unwrap();
2053
2054        let positions: std::vec::Vec<f32> = world.bodies().map(|(_, b)| b.position.x).collect();
2055        assert_eq!(positions, std::vec![1.0, 2.0]);
2056    }
2057
2058    #[test]
2059    fn test_projectile_motion() {
2060        let mut world = PhysicsWorld::<4>::new();
2061        world.set_gravity(Vector3::new(0.0, -10.0, 0.0));
2062
2063        // Launch at 45 degrees: vx=10, vy=10
2064        let body = RigidBody::new(1.0)
2065            .with_velocity(Vector3::new(10.0, 10.0, 0.0))
2066            .with_damping(0.0);
2067        let id = world.add_body(body).unwrap();
2068
2069        // Step for 2 seconds (at t=2, vy should be 10 - 20 = -10)
2070        for _ in 0..200 {
2071            world.step::<4>(0.01);
2072        }
2073
2074        let b = world.body(id).unwrap();
2075        // x position: ~20m (10 m/s * 2s)
2076        assert!((b.position.x - 20.0).abs() < 0.5);
2077        // y velocity: ~-10 m/s
2078        assert!((b.velocity.y - (-10.0)).abs() < 0.5);
2079    }
2080
2081    // -- Collision detection tests --
2082
2083    #[test]
2084    fn test_sphere_sphere_no_collision() {
2085        let result = collide_sphere_sphere(
2086            &Vector3::new(0.0, 0.0, 0.0),
2087            1.0,
2088            &Vector3::new(3.0, 0.0, 0.0),
2089            1.0,
2090        );
2091        assert!(result.is_none());
2092    }
2093
2094    #[test]
2095    fn test_sphere_sphere_touching() {
2096        let result = collide_sphere_sphere(
2097            &Vector3::new(0.0, 0.0, 0.0),
2098            1.0,
2099            &Vector3::new(2.0, 0.0, 0.0),
2100            1.0,
2101        );
2102        // Exactly touching — no penetration
2103        assert!(result.is_none());
2104    }
2105
2106    #[test]
2107    fn test_sphere_sphere_overlapping() {
2108        let result = collide_sphere_sphere(
2109            &Vector3::new(0.0, 0.0, 0.0),
2110            1.0,
2111            &Vector3::new(1.5, 0.0, 0.0),
2112            1.0,
2113        );
2114        let (normal, penetration) = result.unwrap();
2115        assert!(approx_eq(penetration, 0.5));
2116        // Normal should point from A to B (positive X)
2117        assert!(normal.x > 0.0);
2118        assert!(approx_eq(normal.y, 0.0));
2119    }
2120
2121    #[test]
2122    fn test_sphere_sphere_coincident() {
2123        let result = collide_sphere_sphere(
2124            &Vector3::new(0.0, 0.0, 0.0),
2125            1.0,
2126            &Vector3::new(0.0, 0.0, 0.0),
2127            1.0,
2128        );
2129        let (normal, penetration) = result.unwrap();
2130        assert!(approx_eq(penetration, 2.0));
2131        // Should get an arbitrary valid normal
2132        assert!(approx_eq(normal.norm(), 1.0));
2133    }
2134
2135    #[test]
2136    fn test_aabb_aabb_no_collision() {
2137        let result = collide_aabb_aabb(
2138            &Vector3::new(0.0, 0.0, 0.0),
2139            &Vector3::new(1.0, 1.0, 1.0),
2140            &Vector3::new(3.0, 0.0, 0.0),
2141            &Vector3::new(1.0, 1.0, 1.0),
2142        );
2143        assert!(result.is_none());
2144    }
2145
2146    #[test]
2147    fn test_aabb_aabb_overlapping_x() {
2148        let result = collide_aabb_aabb(
2149            &Vector3::new(0.0, 0.0, 0.0),
2150            &Vector3::new(1.0, 1.0, 1.0),
2151            &Vector3::new(1.5, 0.0, 0.0),
2152            &Vector3::new(1.0, 1.0, 1.0),
2153        );
2154        let (normal, penetration) = result.unwrap();
2155        // X axis has least penetration (0.5) vs Y (2.0) and Z (2.0)
2156        assert!(approx_eq(penetration, 0.5));
2157        assert!(approx_eq(normal.x, 1.0));
2158    }
2159
2160    #[test]
2161    fn test_aabb_aabb_overlapping_y() {
2162        let result = collide_aabb_aabb(
2163            &Vector3::new(0.0, 0.0, 0.0),
2164            &Vector3::new(1.0, 1.0, 1.0),
2165            &Vector3::new(0.0, 1.5, 0.0),
2166            &Vector3::new(1.0, 1.0, 1.0),
2167        );
2168        let (normal, penetration) = result.unwrap();
2169        assert!(approx_eq(penetration, 0.5));
2170        assert!(approx_eq(normal.y, 1.0));
2171    }
2172
2173    #[test]
2174    fn test_aabb_aabb_negative_direction() {
2175        let result = collide_aabb_aabb(
2176            &Vector3::new(0.0, 0.0, 0.0),
2177            &Vector3::new(1.0, 1.0, 1.0),
2178            &Vector3::new(-1.5, 0.0, 0.0),
2179            &Vector3::new(1.0, 1.0, 1.0),
2180        );
2181        let (normal, _) = result.unwrap();
2182        // Normal should point negative X (from A toward B)
2183        assert!(approx_eq(normal.x, -1.0));
2184    }
2185
2186    #[test]
2187    fn test_sphere_aabb_no_collision() {
2188        let result = collide_sphere_aabb(
2189            &Vector3::new(5.0, 0.0, 0.0),
2190            1.0,
2191            &Vector3::new(0.0, 0.0, 0.0),
2192            &Vector3::new(1.0, 1.0, 1.0),
2193        );
2194        assert!(result.is_none());
2195    }
2196
2197    #[test]
2198    fn test_sphere_aabb_overlapping() {
2199        // Sphere at x=1.5 with radius 1, AABB from -1 to 1
2200        // Closest point on AABB to sphere center is (1, 0, 0)
2201        // Distance = 0.5, penetration = 1.0 - 0.5 = 0.5
2202        let result = collide_sphere_aabb(
2203            &Vector3::new(1.5, 0.0, 0.0),
2204            1.0,
2205            &Vector3::new(0.0, 0.0, 0.0),
2206            &Vector3::new(1.0, 1.0, 1.0),
2207        );
2208        let (normal, penetration) = result.unwrap();
2209        assert!(approx_eq(penetration, 0.5));
2210        // Normal should point from sphere toward AABB (negative X)
2211        assert!(normal.x < 0.0);
2212    }
2213
2214    #[test]
2215    fn test_sphere_aabb_sphere_inside() {
2216        // Sphere center is inside the AABB
2217        let result = collide_sphere_aabb(
2218            &Vector3::new(0.0, 0.0, 0.0),
2219            0.5,
2220            &Vector3::new(0.0, 0.0, 0.0),
2221            &Vector3::new(2.0, 1.0, 3.0),
2222        );
2223        let (normal, penetration) = result.unwrap();
2224        // Least penetration is Y axis (1.0 to face), plus radius
2225        assert!(penetration > 0.0);
2226        assert!(approx_eq(normal.norm(), 1.0));
2227    }
2228
2229    #[test]
2230    fn test_sphere_aabb_from_below() {
2231        // Sphere below AABB, overlapping on Y axis
2232        let result = collide_sphere_aabb(
2233            &Vector3::new(0.0, -1.5, 0.0),
2234            1.0,
2235            &Vector3::new(0.0, 0.0, 0.0),
2236            &Vector3::new(2.0, 1.0, 2.0),
2237        );
2238        let (normal, penetration) = result.unwrap();
2239        assert!(penetration > 0.0);
2240        // Normal should point from sphere toward AABB (positive Y)
2241        assert!(normal.y > 0.0);
2242    }
2243
2244    // -- World-level collision tests --
2245
2246    #[test]
2247    fn test_detect_collisions_no_colliders() {
2248        let mut world = PhysicsWorld::<4>::new();
2249        world.add_body(RigidBody::new(1.0)).unwrap();
2250        world.add_body(RigidBody::new(1.0)).unwrap();
2251
2252        let contacts = world.detect_collisions::<4>();
2253        assert_eq!(contacts.len(), 0);
2254    }
2255
2256    #[test]
2257    fn test_detect_collisions_sphere_sphere() {
2258        let mut world = PhysicsWorld::<4>::new();
2259        world
2260            .add_body(
2261                RigidBody::new(1.0)
2262                    .with_position(Vector3::new(0.0, 0.0, 0.0))
2263                    .with_collider(Collider::Sphere { radius: 1.0 }),
2264            )
2265            .unwrap();
2266        world
2267            .add_body(
2268                RigidBody::new(1.0)
2269                    .with_position(Vector3::new(1.5, 0.0, 0.0))
2270                    .with_collider(Collider::Sphere { radius: 1.0 }),
2271            )
2272            .unwrap();
2273
2274        let contacts = world.detect_collisions::<4>();
2275        assert_eq!(contacts.len(), 1);
2276        assert!(approx_eq(contacts[0].penetration, 0.5));
2277    }
2278
2279    #[test]
2280    fn test_detect_collisions_skips_static_pairs() {
2281        let mut world = PhysicsWorld::<4>::new();
2282        world
2283            .add_body(
2284                RigidBody::new_static()
2285                    .with_position(Vector3::new(0.0, 0.0, 0.0))
2286                    .with_collider(Collider::Sphere { radius: 1.0 }),
2287            )
2288            .unwrap();
2289        world
2290            .add_body(
2291                RigidBody::new_static()
2292                    .with_position(Vector3::new(0.5, 0.0, 0.0))
2293                    .with_collider(Collider::Sphere { radius: 1.0 }),
2294            )
2295            .unwrap();
2296
2297        let contacts = world.detect_collisions::<4>();
2298        assert_eq!(contacts.len(), 0);
2299    }
2300
2301    #[test]
2302    fn test_collision_response_separates_bodies() {
2303        let mut world = PhysicsWorld::<4>::new();
2304        world
2305            .add_body(
2306                RigidBody::new(1.0)
2307                    .with_position(Vector3::new(0.0, 0.0, 0.0))
2308                    .with_collider(Collider::Sphere { radius: 1.0 })
2309                    .with_restitution(0.0)
2310                    .with_damping(0.0),
2311            )
2312            .unwrap();
2313        world
2314            .add_body(
2315                RigidBody::new(1.0)
2316                    .with_position(Vector3::new(1.0, 0.0, 0.0))
2317                    .with_collider(Collider::Sphere { radius: 1.0 })
2318                    .with_restitution(0.0)
2319                    .with_damping(0.0),
2320            )
2321            .unwrap();
2322
2323        let contacts = world.detect_collisions::<4>();
2324        assert_eq!(contacts.len(), 1);
2325
2326        world.resolve_contacts(&contacts);
2327
2328        // After resolution, bodies should be separated (no overlap)
2329        let a = &world.body(BodyId(0)).unwrap().position;
2330        let b = &world.body(BodyId(1)).unwrap().position;
2331        let dist = (b - a).norm();
2332        assert!(dist >= 2.0 - EPSILON);
2333    }
2334
2335    #[test]
2336    fn test_collision_response_bounce() {
2337        let mut world = PhysicsWorld::<4>::new();
2338        // Body A moving right
2339        world
2340            .add_body(
2341                RigidBody::new(1.0)
2342                    .with_position(Vector3::new(0.0, 0.0, 0.0))
2343                    .with_velocity(Vector3::new(5.0, 0.0, 0.0))
2344                    .with_collider(Collider::Sphere { radius: 1.0 })
2345                    .with_restitution(1.0)
2346                    .with_damping(0.0),
2347            )
2348            .unwrap();
2349        // Body B stationary
2350        world
2351            .add_body(
2352                RigidBody::new(1.0)
2353                    .with_position(Vector3::new(1.5, 0.0, 0.0))
2354                    .with_velocity(Vector3::zeros())
2355                    .with_collider(Collider::Sphere { radius: 1.0 })
2356                    .with_restitution(1.0)
2357                    .with_damping(0.0),
2358            )
2359            .unwrap();
2360
2361        let contacts = world.detect_collisions::<4>();
2362        world.resolve_contacts(&contacts);
2363
2364        let vel_a = world.body(BodyId(0)).unwrap().velocity;
2365        let vel_b = world.body(BodyId(1)).unwrap().velocity;
2366
2367        // With equal masses and restitution=1.0 (perfectly elastic):
2368        // A should stop, B should take A's velocity
2369        assert!(approx_eq(vel_a.x, 0.0));
2370        assert!(approx_eq(vel_b.x, 5.0));
2371    }
2372
2373    #[test]
2374    fn test_collision_dynamic_vs_static() {
2375        let mut world = PhysicsWorld::<4>::new();
2376        // Dynamic ball falling into static floor
2377        world
2378            .add_body(
2379                RigidBody::new(1.0)
2380                    .with_position(Vector3::new(0.0, 0.5, 0.0))
2381                    .with_velocity(Vector3::new(0.0, -5.0, 0.0))
2382                    .with_collider(Collider::Sphere { radius: 1.0 })
2383                    .with_restitution(0.5)
2384                    .with_damping(0.0),
2385            )
2386            .unwrap();
2387        // Static floor
2388        world
2389            .add_body(
2390                RigidBody::new_static()
2391                    .with_position(Vector3::new(0.0, -1.0, 0.0))
2392                    .with_collider(Collider::Aabb {
2393                        half_extents: Vector3::new(10.0, 1.0, 10.0),
2394                    })
2395                    .with_restitution(1.0),
2396            )
2397            .unwrap();
2398
2399        let contacts = world.detect_collisions::<4>();
2400        assert_eq!(contacts.len(), 1);
2401
2402        world.resolve_contacts(&contacts);
2403
2404        // Ball should bounce upward
2405        let ball = world.body(BodyId(0)).unwrap();
2406        assert!(ball.velocity.y > 0.0);
2407
2408        // Static floor should not move
2409        let floor = world.body(BodyId(1)).unwrap();
2410        assert!(approx_vec_eq(&floor.velocity, &Vector3::zeros()));
2411        assert!(approx_eq(floor.position.y, -1.0));
2412    }
2413
2414    #[test]
2415    fn test_step_with_collisions() {
2416        let mut world = PhysicsWorld::<4>::new();
2417        world.set_gravity(Vector3::new(0.0, -10.0, 0.0));
2418
2419        // Ball above a static floor
2420        world
2421            .add_body(
2422                RigidBody::new(1.0)
2423                    .with_position(Vector3::new(0.0, 2.0, 0.0))
2424                    .with_collider(Collider::Sphere { radius: 0.5 })
2425                    .with_restitution(0.5)
2426                    .with_damping(0.0),
2427            )
2428            .unwrap();
2429        world
2430            .add_body(
2431                RigidBody::new_static()
2432                    .with_position(Vector3::new(0.0, 0.0, 0.0))
2433                    .with_collider(Collider::Aabb {
2434                        half_extents: Vector3::new(10.0, 0.1, 10.0),
2435                    }),
2436            )
2437            .unwrap();
2438
2439        // Run for several steps — ball should not fall through the floor
2440        for _ in 0..600 {
2441            world.step::<4>(1.0 / 60.0);
2442        }
2443
2444        let ball = world.body(BodyId(0)).unwrap();
2445        // Ball center should be above the floor surface (floor top at y=0.1, ball radius=0.5)
2446        assert!(ball.position.y >= 0.5);
2447    }
2448
2449    #[test]
2450    fn test_collider_builder() {
2451        let body = RigidBody::new(1.0).with_collider(Collider::Sphere { radius: 2.0 });
2452        assert_eq!(body.collider, Some(Collider::Sphere { radius: 2.0 }));
2453
2454        let body2 = RigidBody::new(1.0).with_collider(Collider::Aabb {
2455            half_extents: Vector3::new(1.0, 2.0, 3.0),
2456        });
2457        assert!(matches!(body2.collider, Some(Collider::Aabb { .. })));
2458    }
2459
2460    #[test]
2461    fn test_no_collider_body_ignored() {
2462        let mut world = PhysicsWorld::<4>::new();
2463        // Body without collider
2464        world.add_body(RigidBody::new(1.0)).unwrap();
2465        // Body with collider overlapping the first
2466        world
2467            .add_body(RigidBody::new(1.0).with_collider(Collider::Sphere { radius: 100.0 }))
2468            .unwrap();
2469
2470        let contacts = world.detect_collisions::<4>();
2471        assert_eq!(contacts.len(), 0); // No collision because first body has no collider
2472    }
2473
2474    #[test]
2475    fn test_aabb_sphere_collision_via_world() {
2476        let mut world = PhysicsWorld::<4>::new();
2477        world
2478            .add_body(
2479                RigidBody::new(1.0)
2480                    .with_position(Vector3::new(0.0, 0.0, 0.0))
2481                    .with_collider(Collider::Aabb {
2482                        half_extents: Vector3::new(1.0, 1.0, 1.0),
2483                    }),
2484            )
2485            .unwrap();
2486        world
2487            .add_body(
2488                RigidBody::new(1.0)
2489                    .with_position(Vector3::new(1.5, 0.0, 0.0))
2490                    .with_collider(Collider::Sphere { radius: 1.0 }),
2491            )
2492            .unwrap();
2493
2494        let contacts = world.detect_collisions::<4>();
2495        assert_eq!(contacts.len(), 1);
2496        assert!(contacts[0].penetration > 0.0);
2497    }
2498
2499    // -- Phase 3: Friction tests --
2500
2501    #[test]
2502    fn test_friction_builder() {
2503        let body = RigidBody::new(1.0).with_friction(0.7);
2504        assert!(approx_eq(body.friction, 0.7));
2505    }
2506
2507    #[test]
2508    fn test_friction_clamped() {
2509        let body = RigidBody::new(1.0).with_friction(2.0);
2510        assert!(approx_eq(body.friction, 1.0));
2511        let body2 = RigidBody::new(1.0).with_friction(-0.5);
2512        assert!(approx_eq(body2.friction, 0.0));
2513    }
2514
2515    #[test]
2516    fn test_default_friction_values() {
2517        let dynamic = RigidBody::new(1.0);
2518        assert!(approx_eq(dynamic.friction, 0.3));
2519        let static_body = RigidBody::new_static();
2520        assert!(approx_eq(static_body.friction, 0.5));
2521    }
2522
2523    #[test]
2524    fn test_friction_reduces_tangential_velocity() {
2525        // A ball sliding along a static floor should slow down due to friction
2526        let mut world = PhysicsWorld::<4>::new();
2527
2528        // Ball moving horizontally, resting on floor
2529        world
2530            .add_body(
2531                RigidBody::new(1.0)
2532                    .with_position(Vector3::new(0.0, 0.55, 0.0))
2533                    .with_velocity(Vector3::new(10.0, -0.1, 0.0))
2534                    .with_collider(Collider::Sphere { radius: 0.5 })
2535                    .with_restitution(0.0)
2536                    .with_friction(0.8)
2537                    .with_damping(0.0),
2538            )
2539            .unwrap();
2540        // Static floor
2541        world
2542            .add_body(
2543                RigidBody::new_static()
2544                    .with_position(Vector3::new(0.0, 0.0, 0.0))
2545                    .with_collider(Collider::Aabb {
2546                        half_extents: Vector3::new(50.0, 0.1, 50.0),
2547                    })
2548                    .with_friction(0.8),
2549            )
2550            .unwrap();
2551
2552        let initial_vx = 10.0_f32;
2553
2554        // Step and detect collision
2555        let contacts = world.detect_collisions::<4>();
2556        assert_eq!(contacts.len(), 1);
2557        world.resolve_contacts(&contacts);
2558
2559        let ball = world.body(BodyId(0)).unwrap();
2560        // Tangential (X) velocity should be reduced by friction
2561        assert!(ball.velocity.x < initial_vx);
2562        assert!(ball.velocity.x >= 0.0); // Should not reverse
2563    }
2564
2565    #[test]
2566    fn test_zero_friction_preserves_tangential_velocity() {
2567        let mut world = PhysicsWorld::<4>::new();
2568
2569        // Ball sliding on frictionless surface
2570        world
2571            .add_body(
2572                RigidBody::new(1.0)
2573                    .with_position(Vector3::new(0.0, 0.55, 0.0))
2574                    .with_velocity(Vector3::new(10.0, -1.0, 0.0))
2575                    .with_collider(Collider::Sphere { radius: 0.5 })
2576                    .with_restitution(0.0)
2577                    .with_friction(0.0)
2578                    .with_damping(0.0),
2579            )
2580            .unwrap();
2581        world
2582            .add_body(
2583                RigidBody::new_static()
2584                    .with_position(Vector3::new(0.0, 0.0, 0.0))
2585                    .with_collider(Collider::Aabb {
2586                        half_extents: Vector3::new(50.0, 0.1, 50.0),
2587                    })
2588                    .with_friction(0.0),
2589            )
2590            .unwrap();
2591
2592        let contacts = world.detect_collisions::<4>();
2593        world.resolve_contacts(&contacts);
2594
2595        let ball = world.body(BodyId(0)).unwrap();
2596        // With zero friction, tangential velocity should be preserved
2597        assert!(approx_eq(ball.velocity.x, 10.0));
2598    }
2599
2600    #[test]
2601    fn test_high_friction_vs_low_friction() {
2602        // Compare two identical scenarios with different friction values
2603        let make_world = |friction: f32| -> PhysicsWorld<4> {
2604            let mut world = PhysicsWorld::<4>::new();
2605            world
2606                .add_body(
2607                    RigidBody::new(1.0)
2608                        .with_position(Vector3::new(0.0, 0.55, 0.0))
2609                        .with_velocity(Vector3::new(10.0, -1.0, 0.0))
2610                        .with_collider(Collider::Sphere { radius: 0.5 })
2611                        .with_restitution(0.0)
2612                        .with_friction(friction)
2613                        .with_damping(0.0),
2614                )
2615                .unwrap();
2616            world
2617                .add_body(
2618                    RigidBody::new_static()
2619                        .with_position(Vector3::new(0.0, 0.0, 0.0))
2620                        .with_collider(Collider::Aabb {
2621                            half_extents: Vector3::new(50.0, 0.1, 50.0),
2622                        })
2623                        .with_friction(friction),
2624                )
2625                .unwrap();
2626            world
2627        };
2628
2629        let mut world_low = make_world(0.1);
2630        let mut world_high = make_world(1.0);
2631
2632        let contacts_low = world_low.detect_collisions::<4>();
2633        world_low.resolve_contacts(&contacts_low);
2634        let contacts_high = world_high.detect_collisions::<4>();
2635        world_high.resolve_contacts(&contacts_high);
2636
2637        let vx_low = world_low.body(BodyId(0)).unwrap().velocity.x;
2638        let vx_high = world_high.body(BodyId(0)).unwrap().velocity.x;
2639
2640        // High friction should slow tangential velocity more
2641        assert!(vx_high < vx_low);
2642    }
2643
2644    // -- Phase 3: Body lifecycle tests --
2645
2646    #[test]
2647    fn test_body_active_by_default() {
2648        let body = RigidBody::new(1.0);
2649        assert!(body.active);
2650        let static_body = RigidBody::new_static();
2651        assert!(static_body.active);
2652    }
2653
2654    #[test]
2655    fn test_remove_body() {
2656        let mut world = PhysicsWorld::<4>::new();
2657        let id = world
2658            .add_body(RigidBody::new(1.0).with_velocity(Vector3::new(5.0, 0.0, 0.0)))
2659            .unwrap();
2660
2661        assert_eq!(world.active_body_count(), 1);
2662        assert!(world.remove_body(id));
2663        assert_eq!(world.active_body_count(), 0);
2664        assert_eq!(world.body_count(), 1); // Still in the world, just inactive
2665
2666        // Body velocity should be zeroed
2667        let body = world.body(id).unwrap();
2668        assert!(!body.active);
2669        assert!(approx_vec_eq(&body.velocity, &Vector3::zeros()));
2670    }
2671
2672    #[test]
2673    fn test_remove_body_twice_returns_false() {
2674        let mut world = PhysicsWorld::<4>::new();
2675        let id = world.add_body(RigidBody::new(1.0)).unwrap();
2676
2677        assert!(world.remove_body(id));
2678        assert!(!world.remove_body(id)); // Already inactive
2679    }
2680
2681    #[test]
2682    fn test_remove_body_invalid_id() {
2683        let mut world = PhysicsWorld::<4>::new();
2684        assert!(!world.remove_body(BodyId(99)));
2685    }
2686
2687    #[test]
2688    fn test_inactive_body_not_integrated() {
2689        let mut world = PhysicsWorld::<4>::new();
2690        world.set_gravity(Vector3::new(0.0, -10.0, 0.0));
2691
2692        let id = world
2693            .add_body(
2694                RigidBody::new(1.0)
2695                    .with_position(Vector3::new(0.0, 10.0, 0.0))
2696                    .with_damping(0.0),
2697            )
2698            .unwrap();
2699
2700        world.remove_body(id);
2701        world.step::<4>(1.0);
2702
2703        // Position should not change since body is inactive
2704        let body = world.body(id).unwrap();
2705        assert!(approx_eq(body.position.y, 10.0));
2706    }
2707
2708    #[test]
2709    fn test_inactive_body_not_collided() {
2710        let mut world = PhysicsWorld::<4>::new();
2711
2712        // Two overlapping bodies
2713        let id_a = world
2714            .add_body(
2715                RigidBody::new(1.0)
2716                    .with_position(Vector3::zeros())
2717                    .with_collider(Collider::Sphere { radius: 1.0 }),
2718            )
2719            .unwrap();
2720        world
2721            .add_body(
2722                RigidBody::new(1.0)
2723                    .with_position(Vector3::new(0.5, 0.0, 0.0))
2724                    .with_collider(Collider::Sphere { radius: 1.0 }),
2725            )
2726            .unwrap();
2727
2728        // Should detect collision when both active
2729        let contacts = world.detect_collisions::<4>();
2730        assert_eq!(contacts.len(), 1);
2731
2732        // Deactivate first body — no more collisions
2733        world.remove_body(id_a);
2734        let contacts = world.detect_collisions::<4>();
2735        assert_eq!(contacts.len(), 0);
2736    }
2737
2738    #[test]
2739    fn test_set_active_reactivates_body() {
2740        let mut world = PhysicsWorld::<4>::new();
2741        world.set_gravity(Vector3::new(0.0, -10.0, 0.0));
2742
2743        let id = world
2744            .add_body(
2745                RigidBody::new(1.0)
2746                    .with_position(Vector3::new(0.0, 10.0, 0.0))
2747                    .with_damping(0.0),
2748            )
2749            .unwrap();
2750
2751        // Deactivate, step (should not move)
2752        world.remove_body(id);
2753        world.step::<4>(1.0);
2754        assert!(approx_eq(world.body(id).unwrap().position.y, 10.0));
2755
2756        // Reactivate, step (should fall)
2757        world.set_active(id, true);
2758        assert_eq!(world.active_body_count(), 1);
2759        world.step::<4>(1.0);
2760        assert!(world.body(id).unwrap().position.y < 10.0);
2761    }
2762
2763    #[test]
2764    fn test_active_body_count() {
2765        let mut world = PhysicsWorld::<8>::new();
2766        let id0 = world.add_body(RigidBody::new(1.0)).unwrap();
2767        let id1 = world.add_body(RigidBody::new(1.0)).unwrap();
2768        let _id2 = world.add_body(RigidBody::new(1.0)).unwrap();
2769
2770        assert_eq!(world.active_body_count(), 3);
2771
2772        world.remove_body(id0);
2773        assert_eq!(world.active_body_count(), 2);
2774
2775        world.remove_body(id1);
2776        assert_eq!(world.active_body_count(), 1);
2777    }
2778
2779    #[test]
2780    fn test_remove_preserves_other_body_ids() {
2781        let mut world = PhysicsWorld::<4>::new();
2782        world.set_gravity(Vector3::new(0.0, -10.0, 0.0));
2783
2784        let id0 = world
2785            .add_body(
2786                RigidBody::new(1.0)
2787                    .with_position(Vector3::new(0.0, 10.0, 0.0))
2788                    .with_damping(0.0),
2789            )
2790            .unwrap();
2791        let id1 = world
2792            .add_body(
2793                RigidBody::new(1.0)
2794                    .with_position(Vector3::new(5.0, 10.0, 0.0))
2795                    .with_damping(0.0),
2796            )
2797            .unwrap();
2798
2799        // Remove first, step
2800        world.remove_body(id0);
2801        world.step::<4>(1.0);
2802
2803        // First body should not have moved, second should have fallen
2804        assert!(approx_eq(world.body(id0).unwrap().position.y, 10.0));
2805        assert!(world.body(id1).unwrap().position.y < 10.0);
2806    }
2807
2808    // -- Phase 4: Angular dynamics tests --
2809
2810    #[test]
2811    fn test_default_orientation_is_identity() {
2812        let body = RigidBody::new(1.0);
2813        assert!(approx_eq(body.orientation.w, 1.0));
2814        assert!(approx_vec_eq(&body.angular_velocity, &Vector3::zeros()));
2815    }
2816
2817    #[test]
2818    fn test_angular_velocity_builder() {
2819        let body = RigidBody::new(1.0).with_angular_velocity(Vector3::new(0.0, 5.0, 0.0));
2820        assert!(approx_eq(body.angular_velocity.y, 5.0));
2821    }
2822
2823    #[test]
2824    fn test_angular_damping_builder() {
2825        let body = RigidBody::new(1.0).with_angular_damping(0.05);
2826        assert!(approx_eq(body.angular_damping, 0.05));
2827    }
2828
2829    #[test]
2830    fn test_angular_damping_clamped() {
2831        let body = RigidBody::new(1.0).with_angular_damping(2.0);
2832        assert!(approx_eq(body.angular_damping, 1.0));
2833        let body2 = RigidBody::new(1.0).with_angular_damping(-1.0);
2834        assert!(approx_eq(body2.angular_damping, 0.0));
2835    }
2836
2837    #[test]
2838    fn test_inertia_sphere() {
2839        let body = RigidBody::new(2.0).with_inertia_sphere(0.5);
2840        // I = 2/5 * 2 * 0.25 = 0.2, inv = 5.0
2841        let inv_i = body.inv_inertia_local[(0, 0)];
2842        assert!(approx_eq(inv_i, 5.0));
2843        // Should be diagonal and uniform
2844        assert!(approx_eq(body.inv_inertia_local[(1, 1)], 5.0));
2845        assert!(approx_eq(body.inv_inertia_local[(2, 2)], 5.0));
2846        assert!(approx_eq(body.inv_inertia_local[(0, 1)], 0.0));
2847    }
2848
2849    #[test]
2850    fn test_inertia_box() {
2851        let body = RigidBody::new(12.0).with_inertia_box(Vector3::new(1.0, 2.0, 3.0));
2852        // Full dims: 2x4x6
2853        // Ixx = (12/12) * (16 + 36) = 52, inv = 1/52
2854        // Iyy = (12/12) * (4 + 36) = 40, inv = 1/40
2855        // Izz = (12/12) * (4 + 16) = 20, inv = 1/20
2856        assert!(approx_eq(body.inv_inertia_local[(0, 0)], 1.0 / 52.0));
2857        assert!(approx_eq(body.inv_inertia_local[(1, 1)], 1.0 / 40.0));
2858        assert!(approx_eq(body.inv_inertia_local[(2, 2)], 1.0 / 20.0));
2859    }
2860
2861    #[test]
2862    fn test_inertia_static_body_unchanged() {
2863        let body = RigidBody::new_static().with_inertia_sphere(1.0);
2864        // Static bodies should keep zero inverse inertia
2865        assert!(approx_eq(body.inv_inertia_local[(0, 0)], 0.0));
2866    }
2867
2868    #[test]
2869    fn test_apply_torque() {
2870        let mut body = RigidBody::new(1.0);
2871        body.apply_torque(Vector3::new(1.0, 0.0, 0.0));
2872        body.apply_torque(Vector3::new(0.0, 2.0, 0.0));
2873        assert!(approx_vec_eq(
2874            &body.torque_accumulator,
2875            &Vector3::new(1.0, 2.0, 0.0)
2876        ));
2877    }
2878
2879    #[test]
2880    fn test_apply_angular_impulse() {
2881        let mut body = RigidBody::new(1.0).with_inertia_sphere(1.0);
2882        // I = 2/5 * 1 * 1 = 0.4, inv = 2.5
2883        body.apply_angular_impulse(Vector3::new(1.0, 0.0, 0.0));
2884        // delta_omega = inv_I * impulse = 2.5 * 1.0 = 2.5
2885        assert!(approx_eq(body.angular_velocity.x, 2.5));
2886    }
2887
2888    #[test]
2889    fn test_angular_impulse_on_static_body_ignored() {
2890        let mut body = RigidBody::new_static();
2891        body.apply_angular_impulse(Vector3::new(100.0, 0.0, 0.0));
2892        assert!(approx_vec_eq(&body.angular_velocity, &Vector3::zeros()));
2893    }
2894
2895    #[test]
2896    fn test_constant_angular_velocity_changes_orientation() {
2897        let mut world = PhysicsWorld::<4>::new();
2898        let id = world
2899            .add_body(
2900                RigidBody::new(1.0)
2901                    .with_angular_velocity(Vector3::new(0.0, core::f32::consts::PI, 0.0))
2902                    .with_angular_damping(0.0)
2903                    .with_damping(0.0),
2904            )
2905            .unwrap();
2906
2907        // After 1 second at PI rad/s around Y, orientation should have rotated ~180 degrees
2908        for _ in 0..100 {
2909            world.step::<4>(0.01);
2910        }
2911
2912        let body = world.body(id).unwrap();
2913        // Check quaternion is no longer identity
2914        assert!(body.orientation.w.abs() < 0.9); // Rotated significantly
2915        // Angular velocity should be preserved (no damping)
2916        assert!(approx_eq(body.angular_velocity.y, core::f32::consts::PI));
2917    }
2918
2919    #[test]
2920    fn test_torque_produces_angular_acceleration() {
2921        let mut world = PhysicsWorld::<4>::new();
2922        let id = world
2923            .add_body(
2924                RigidBody::new(1.0)
2925                    .with_inertia_sphere(1.0)
2926                    .with_angular_damping(0.0)
2927                    .with_damping(0.0),
2928            )
2929            .unwrap();
2930
2931        // Apply torque around Z axis
2932        world
2933            .body_mut(id)
2934            .unwrap()
2935            .apply_torque(Vector3::new(0.0, 0.0, 1.0));
2936        world.step::<4>(1.0);
2937
2938        let body = world.body(id).unwrap();
2939        // I = 0.4, alpha = torque * inv_I = 1.0 * 2.5 = 2.5
2940        // After 1s: omega = 2.5 rad/s
2941        assert!(approx_eq(body.angular_velocity.z, 2.5));
2942    }
2943
2944    #[test]
2945    fn test_torque_cleared_after_step() {
2946        let mut world = PhysicsWorld::<4>::new();
2947        let id = world
2948            .add_body(
2949                RigidBody::new(1.0)
2950                    .with_inertia_sphere(1.0)
2951                    .with_angular_damping(0.0)
2952                    .with_damping(0.0),
2953            )
2954            .unwrap();
2955
2956        world
2957            .body_mut(id)
2958            .unwrap()
2959            .apply_torque(Vector3::new(0.0, 0.0, 1.0));
2960        world.step::<4>(1.0);
2961
2962        // No new torque — angular velocity should stay constant
2963        let omega_before = world.body(id).unwrap().angular_velocity.z;
2964        world.step::<4>(1.0);
2965        let omega_after = world.body(id).unwrap().angular_velocity.z;
2966        assert!(approx_eq(omega_before, omega_after));
2967    }
2968
2969    #[test]
2970    fn test_angular_damping_reduces_spin() {
2971        let mut world = PhysicsWorld::<4>::new();
2972        let id = world
2973            .add_body(
2974                RigidBody::new(1.0)
2975                    .with_angular_velocity(Vector3::new(10.0, 0.0, 0.0))
2976                    .with_angular_damping(0.1)
2977                    .with_damping(0.0),
2978            )
2979            .unwrap();
2980
2981        world.step::<4>(1.0);
2982
2983        let body = world.body(id).unwrap();
2984        assert!(body.angular_velocity.x < 10.0);
2985        assert!(body.angular_velocity.x > 0.0);
2986    }
2987
2988    #[test]
2989    fn test_orientation_stays_normalized() {
2990        let mut world = PhysicsWorld::<4>::new();
2991        let id = world
2992            .add_body(
2993                RigidBody::new(1.0)
2994                    .with_angular_velocity(Vector3::new(3.0, 5.0, 7.0))
2995                    .with_angular_damping(0.0)
2996                    .with_damping(0.0),
2997            )
2998            .unwrap();
2999
3000        // Many steps to check quaternion doesn't drift
3001        for _ in 0..1000 {
3002            world.step::<4>(0.01);
3003        }
3004
3005        let body = world.body(id).unwrap();
3006        let q = body.orientation.into_inner();
3007        let norm = (q.w * q.w + q.i * q.i + q.j * q.j + q.k * q.k).sqrt();
3008        assert!((norm - 1.0).abs() < 1e-3);
3009    }
3010
3011    #[test]
3012    fn test_collision_imparts_angular_velocity() {
3013        // A ball hitting a static floor off-center should start spinning
3014        let mut world = PhysicsWorld::<4>::new();
3015
3016        // Ball with some horizontal velocity hitting a floor
3017        world
3018            .add_body(
3019                RigidBody::new(1.0)
3020                    .with_position(Vector3::new(0.0, 0.55, 0.0))
3021                    .with_velocity(Vector3::new(5.0, -1.0, 0.0))
3022                    .with_collider(Collider::Sphere { radius: 0.5 })
3023                    .with_inertia_sphere(0.5)
3024                    .with_restitution(0.0)
3025                    .with_friction(0.8)
3026                    .with_damping(0.0)
3027                    .with_angular_damping(0.0),
3028            )
3029            .unwrap();
3030
3031        // Static floor
3032        world
3033            .add_body(
3034                RigidBody::new_static()
3035                    .with_position(Vector3::new(0.0, 0.0, 0.0))
3036                    .with_collider(Collider::Aabb {
3037                        half_extents: Vector3::new(50.0, 0.1, 50.0),
3038                    })
3039                    .with_friction(0.8),
3040            )
3041            .unwrap();
3042
3043        let contacts = world.detect_collisions::<4>();
3044        assert_eq!(contacts.len(), 1);
3045        world.resolve_contacts(&contacts);
3046
3047        let ball = world.body(BodyId(0)).unwrap();
3048        // Friction at the contact should have induced angular velocity (spinning)
3049        let omega_magnitude = ball.angular_velocity.norm();
3050        assert!(
3051            omega_magnitude > 0.01,
3052            "Expected angular velocity from friction, got {}",
3053            omega_magnitude
3054        );
3055    }
3056
3057    #[test]
3058    fn test_remove_body_clears_angular_state() {
3059        let mut world = PhysicsWorld::<4>::new();
3060        let id = world
3061            .add_body(RigidBody::new(1.0).with_angular_velocity(Vector3::new(5.0, 5.0, 5.0)))
3062            .unwrap();
3063
3064        world.remove_body(id);
3065        let body = world.body(id).unwrap();
3066        assert!(approx_vec_eq(&body.angular_velocity, &Vector3::zeros()));
3067    }
3068
3069    #[test]
3070    fn test_inv_inertia_world_rotated() {
3071        // For a non-uniform inertia tensor, rotating the body should change
3072        // the world-space inverse inertia
3073        let mut body = RigidBody::new(1.0).with_inertia_box(Vector3::new(1.0, 2.0, 3.0));
3074
3075        let inv_i_local_00 = body.inv_inertia_local[(0, 0)];
3076        let inv_i_world_00 = body.inv_inertia_world()[(0, 0)];
3077        // At identity orientation, local = world
3078        assert!(approx_eq(inv_i_local_00, inv_i_world_00));
3079
3080        // Rotate 90 degrees around Y
3081        body.orientation = UnitQuaternion::from_axis_angle(
3082            &nalgebra::Unit::new_normalize(Vector3::new(0.0, 1.0, 0.0)),
3083            core::f32::consts::FRAC_PI_2,
3084        );
3085        let inv_i_rotated = body.inv_inertia_world();
3086        // After 90-degree Y rotation, Ixx and Izz should swap
3087        assert!(approx_eq(
3088            inv_i_rotated[(0, 0)],
3089            body.inv_inertia_local[(2, 2)]
3090        ));
3091        assert!(approx_eq(
3092            inv_i_rotated[(2, 2)],
3093            body.inv_inertia_local[(0, 0)]
3094        ));
3095    }
3096
3097    #[test]
3098    fn test_elastic_collision_angular_conservation() {
3099        // For a head-on elastic collision between equal spheres,
3100        // total angular momentum should be conserved (both start at zero)
3101        let mut world = PhysicsWorld::<4>::new();
3102        world
3103            .add_body(
3104                RigidBody::new(1.0)
3105                    .with_position(Vector3::new(0.0, 0.0, 0.0))
3106                    .with_velocity(Vector3::new(5.0, 0.0, 0.0))
3107                    .with_collider(Collider::Sphere { radius: 1.0 })
3108                    .with_inertia_sphere(1.0)
3109                    .with_restitution(1.0)
3110                    .with_friction(0.0)
3111                    .with_damping(0.0)
3112                    .with_angular_damping(0.0),
3113            )
3114            .unwrap();
3115        world
3116            .add_body(
3117                RigidBody::new(1.0)
3118                    .with_position(Vector3::new(1.5, 0.0, 0.0))
3119                    .with_velocity(Vector3::zeros())
3120                    .with_collider(Collider::Sphere { radius: 1.0 })
3121                    .with_inertia_sphere(1.0)
3122                    .with_restitution(1.0)
3123                    .with_friction(0.0)
3124                    .with_damping(0.0)
3125                    .with_angular_damping(0.0),
3126            )
3127            .unwrap();
3128
3129        let contacts = world.detect_collisions::<4>();
3130        world.resolve_contacts(&contacts);
3131
3132        let omega_a = world.body(BodyId(0)).unwrap().angular_velocity;
3133        let omega_b = world.body(BodyId(1)).unwrap().angular_velocity;
3134        // With zero friction and head-on collision, no angular velocity should be generated
3135        assert!(omega_a.norm() < EPSILON);
3136        assert!(omega_b.norm() < EPSILON);
3137    }
3138
3139    // -- Phase 5: Constraint & joint tests --
3140
3141    #[test]
3142    fn test_add_constraint() {
3143        let mut world = PhysicsWorld::<4, 4>::new();
3144        let a = world.add_body(RigidBody::new(1.0)).unwrap();
3145        let b = world.add_body(RigidBody::new(1.0)).unwrap();
3146
3147        let cid = world
3148            .add_constraint(Constraint::Distance {
3149                body_a: a,
3150                body_b: b,
3151                anchor_a: Vector3::zeros(),
3152                anchor_b: Vector3::zeros(),
3153                rest_length: 2.0,
3154                compliance: 0.0,
3155            })
3156            .unwrap();
3157
3158        assert_eq!(world.constraint_count(), 1);
3159        assert!(world.constraint(cid).is_some());
3160    }
3161
3162    #[test]
3163    fn test_add_constraint_at_capacity() {
3164        let mut world = PhysicsWorld::<4, 1>::new();
3165        let a = world.add_body(RigidBody::new(1.0)).unwrap();
3166        let b = world.add_body(RigidBody::new(1.0)).unwrap();
3167
3168        let c = Constraint::BallSocket {
3169            body_a: a,
3170            body_b: b,
3171            anchor_a: Vector3::zeros(),
3172            anchor_b: Vector3::zeros(),
3173            compliance: 0.0,
3174        };
3175        assert!(world.add_constraint(c.clone()).is_some());
3176        assert!(world.add_constraint(c).is_none()); // At capacity
3177    }
3178
3179    #[test]
3180    fn test_remove_constraint() {
3181        let mut world = PhysicsWorld::<4, 4>::new();
3182        let a = world.add_body(RigidBody::new(1.0)).unwrap();
3183        let b = world.add_body(RigidBody::new(1.0)).unwrap();
3184
3185        let cid = world
3186            .add_ball_socket(a, Vector3::zeros(), b, Vector3::zeros(), 0.0)
3187            .unwrap();
3188        assert_eq!(world.constraint_count(), 1);
3189
3190        assert!(world.remove_constraint(cid));
3191        assert_eq!(world.constraint_count(), 0);
3192    }
3193
3194    #[test]
3195    fn test_remove_constraint_invalid_id() {
3196        let mut world = PhysicsWorld::<4, 4>::new();
3197        assert!(!world.remove_constraint(ConstraintId(99)));
3198    }
3199
3200    #[test]
3201    fn test_distance_constraint_maintains_length() {
3202        let mut world = PhysicsWorld::<4, 4>::new();
3203        world.set_gravity(Vector3::new(0.0, -10.0, 0.0));
3204
3205        // Two bodies connected by a rigid rod of length 3
3206        let a = world
3207            .add_body(
3208                RigidBody::new(1.0)
3209                    .with_position(Vector3::new(0.0, 5.0, 0.0))
3210                    .with_damping(0.0)
3211                    .with_angular_damping(0.0),
3212            )
3213            .unwrap();
3214        let b = world
3215            .add_body(
3216                RigidBody::new(1.0)
3217                    .with_position(Vector3::new(3.0, 5.0, 0.0))
3218                    .with_damping(0.0)
3219                    .with_angular_damping(0.0),
3220            )
3221            .unwrap();
3222
3223        world
3224            .add_distance_constraint(a, Vector3::zeros(), b, Vector3::zeros(), 0.0)
3225            .unwrap();
3226
3227        // Run simulation — gravity pulls both down, constraint keeps distance
3228        for _ in 0..200 {
3229            world.step::<4>(1.0 / 60.0);
3230        }
3231
3232        let pa = world.body(a).unwrap().position;
3233        let pb = world.body(b).unwrap().position;
3234        let dist = (pb - pa).norm();
3235        // Distance should stay close to 3.0
3236        assert!(
3237            (dist - 3.0).abs() < 0.3,
3238            "Expected distance ~3.0, got {}",
3239            dist
3240        );
3241    }
3242
3243    #[test]
3244    fn test_ball_socket_keeps_points_together() {
3245        // Two bodies at the same position with a ball-socket joint (zero anchors)
3246        // should stay together under gravity
3247        let mut world = PhysicsWorld::<4, 4>::new();
3248        world.set_gravity(Vector3::new(0.0, -10.0, 0.0));
3249        world.solver_iterations = 8;
3250
3251        let a = world
3252            .add_body(RigidBody::new_static().with_position(Vector3::new(0.0, 10.0, 0.0)))
3253            .unwrap();
3254
3255        let b = world
3256            .add_body(
3257                RigidBody::new(1.0)
3258                    .with_position(Vector3::new(0.0, 10.0, 0.0))
3259                    .with_damping(0.01)
3260                    .with_angular_damping(0.01),
3261            )
3262            .unwrap();
3263
3264        // Ball-socket: pin body B's center to body A's center
3265        world
3266            .add_ball_socket(a, Vector3::zeros(), b, Vector3::zeros(), 0.0)
3267            .unwrap();
3268
3269        for _ in 0..120 {
3270            world.step::<4>(1.0 / 60.0);
3271        }
3272
3273        // Body B should stay close to A (gravity pulls it down but joint holds)
3274        let pa = world.body(a).unwrap().position;
3275        let pb = world.body(b).unwrap().position;
3276        let gap = (pb - pa).norm();
3277        assert!(gap < 1.0, "Ball-socket gap too large: {}", gap);
3278    }
3279
3280    #[test]
3281    fn test_pendulum_swings_under_gravity() {
3282        let mut world = PhysicsWorld::<4, 4>::new();
3283        world.set_gravity(Vector3::new(0.0, -10.0, 0.0));
3284
3285        let anchor = world
3286            .add_body(RigidBody::new_static().with_position(Vector3::new(0.0, 10.0, 0.0)))
3287            .unwrap();
3288
3289        let bob = world
3290            .add_body(
3291                RigidBody::new(1.0)
3292                    .with_position(Vector3::new(3.0, 10.0, 0.0))
3293                    .with_damping(0.0)
3294                    .with_angular_damping(0.0),
3295            )
3296            .unwrap();
3297
3298        // Distance constraint (rod of length 3)
3299        world
3300            .add_distance_constraint(anchor, Vector3::zeros(), bob, Vector3::zeros(), 0.0)
3301            .unwrap();
3302
3303        // Bob starts at same height as anchor, should swing down
3304        let initial_y = 10.0;
3305
3306        for _ in 0..120 {
3307            world.step::<4>(1.0 / 60.0);
3308        }
3309
3310        let bob_pos = world.body(bob).unwrap().position;
3311        // Bob should have swung below the anchor
3312        assert!(
3313            bob_pos.y < initial_y,
3314            "Bob should swing down, y={}",
3315            bob_pos.y
3316        );
3317    }
3318
3319    #[test]
3320    fn test_soft_distance_constraint() {
3321        // A soft spring should allow the bodies to overshoot the rest length
3322        let mut world = PhysicsWorld::<4, 4>::new();
3323        world.set_gravity(Vector3::zeros());
3324
3325        let a = world
3326            .add_body(
3327                RigidBody::new(1.0)
3328                    .with_position(Vector3::new(0.0, 0.0, 0.0))
3329                    .with_damping(0.0)
3330                    .with_angular_damping(0.0),
3331            )
3332            .unwrap();
3333        let b = world
3334            .add_body(
3335                RigidBody::new(1.0)
3336                    .with_position(Vector3::new(5.0, 0.0, 0.0))
3337                    .with_damping(0.0)
3338                    .with_angular_damping(0.0),
3339            )
3340            .unwrap();
3341
3342        // Soft spring with rest_length=2, compliance=0.01 (springy)
3343        world
3344            .add_constraint(Constraint::Distance {
3345                body_a: a,
3346                body_b: b,
3347                anchor_a: Vector3::zeros(),
3348                anchor_b: Vector3::zeros(),
3349                rest_length: 2.0,
3350                compliance: 0.01,
3351            })
3352            .unwrap();
3353
3354        // Step a few times
3355        for _ in 0..60 {
3356            world.step::<4>(1.0 / 60.0);
3357        }
3358
3359        let pa = world.body(a).unwrap().position;
3360        let pb = world.body(b).unwrap().position;
3361        let dist = (pb - pa).norm();
3362        // With compliance, it should have moved toward rest_length but may not be exact
3363        assert!(dist < 5.0, "Spring should have contracted, dist={}", dist);
3364    }
3365
3366    #[test]
3367    fn test_fixed_joint_preserves_relative_orientation() {
3368        let mut world = PhysicsWorld::<4, 4>::new();
3369        world.set_gravity(Vector3::new(0.0, -10.0, 0.0));
3370
3371        let a = world
3372            .add_body(RigidBody::new_static().with_position(Vector3::new(0.0, 10.0, 0.0)))
3373            .unwrap();
3374        let b = world
3375            .add_body(
3376                RigidBody::new(1.0)
3377                    .with_position(Vector3::new(2.0, 10.0, 0.0))
3378                    .with_inertia_box(Vector3::new(0.5, 0.5, 0.5))
3379                    .with_damping(0.0)
3380                    .with_angular_damping(0.0),
3381            )
3382            .unwrap();
3383
3384        // Fixed joint — should preserve relative orientation
3385        world
3386            .add_fixed_joint(a, Vector3::new(2.0, 0.0, 0.0), b, Vector3::zeros(), 0.0)
3387            .unwrap();
3388
3389        // Store initial relative orientation
3390        let initial_qa = world.body(a).unwrap().orientation;
3391        let initial_qb = world.body(b).unwrap().orientation;
3392        let initial_rel = initial_qb * initial_qa.inverse();
3393
3394        for _ in 0..200 {
3395            world.step::<4>(1.0 / 60.0);
3396        }
3397
3398        // Relative orientation should be similar to initial
3399        let qa = world.body(a).unwrap().orientation;
3400        let qb = world.body(b).unwrap().orientation;
3401        let current_rel = qb * qa.inverse();
3402        let error = current_rel * initial_rel.inverse();
3403        let angle = error.angle();
3404        assert!(angle < 0.5, "Fixed joint orientation drift: {} rad", angle);
3405    }
3406
3407    #[test]
3408    fn test_chain_of_distance_constraints() {
3409        // Three bodies connected by two distance constraints forming a chain
3410        let mut world = PhysicsWorld::<8, 8>::new();
3411        world.set_gravity(Vector3::new(0.0, -10.0, 0.0));
3412        world.solver_iterations = 8;
3413
3414        let anchor = world
3415            .add_body(RigidBody::new_static().with_position(Vector3::new(0.0, 10.0, 0.0)))
3416            .unwrap();
3417        let link1 = world
3418            .add_body(
3419                RigidBody::new(1.0)
3420                    .with_position(Vector3::new(0.0, 8.0, 0.0))
3421                    .with_damping(0.01)
3422                    .with_angular_damping(0.01),
3423            )
3424            .unwrap();
3425        let link2 = world
3426            .add_body(
3427                RigidBody::new(1.0)
3428                    .with_position(Vector3::new(0.0, 6.0, 0.0))
3429                    .with_damping(0.01)
3430                    .with_angular_damping(0.01),
3431            )
3432            .unwrap();
3433
3434        world
3435            .add_distance_constraint(anchor, Vector3::zeros(), link1, Vector3::zeros(), 0.0)
3436            .unwrap();
3437        world
3438            .add_distance_constraint(link1, Vector3::zeros(), link2, Vector3::zeros(), 0.0)
3439            .unwrap();
3440
3441        for _ in 0..600 {
3442            world.step::<4>(1.0 / 60.0);
3443        }
3444
3445        // Verify chain lengths are approximately maintained
3446        let pa = world.body(anchor).unwrap().position;
3447        let p1 = world.body(link1).unwrap().position;
3448        let p2 = world.body(link2).unwrap().position;
3449
3450        let d1 = (p1 - pa).norm();
3451        let d2 = (p2 - p1).norm();
3452
3453        assert!((d1 - 2.0).abs() < 0.5, "Link 1 distance: {}", d1);
3454        assert!((d2 - 2.0).abs() < 0.5, "Link 2 distance: {}", d2);
3455
3456        // Everything should hang below the anchor
3457        assert!(p1.y < pa.y, "Link 1 should be below anchor");
3458        assert!(p2.y < p1.y, "Link 2 should be below link 1");
3459    }
3460
3461    #[test]
3462    fn test_constraint_between_equal_bodies_symmetric() {
3463        // Two equal-mass bodies connected by a distance constraint in zero gravity
3464        // should move symmetrically toward each other
3465        let mut world = PhysicsWorld::<4, 4>::new();
3466        world.set_gravity(Vector3::zeros());
3467
3468        let a = world
3469            .add_body(
3470                RigidBody::new(1.0)
3471                    .with_position(Vector3::new(-3.0, 0.0, 0.0))
3472                    .with_damping(0.0)
3473                    .with_angular_damping(0.0),
3474            )
3475            .unwrap();
3476        let b = world
3477            .add_body(
3478                RigidBody::new(1.0)
3479                    .with_position(Vector3::new(3.0, 0.0, 0.0))
3480                    .with_damping(0.0)
3481                    .with_angular_damping(0.0),
3482            )
3483            .unwrap();
3484
3485        // Distance constraint with rest_length=2 (bodies start 6 apart)
3486        world
3487            .add_constraint(Constraint::Distance {
3488                body_a: a,
3489                body_b: b,
3490                anchor_a: Vector3::zeros(),
3491                anchor_b: Vector3::zeros(),
3492                rest_length: 2.0,
3493                compliance: 0.0,
3494            })
3495            .unwrap();
3496
3497        for _ in 0..120 {
3498            world.step::<4>(1.0 / 60.0);
3499        }
3500
3501        let pa = world.body(a).unwrap().position;
3502        let pb = world.body(b).unwrap().position;
3503
3504        // Center of mass should remain at origin (symmetric motion)
3505        let com = (pa + pb) / 2.0;
3506        assert!(
3507            com.norm() < 0.5,
3508            "COM should stay near origin, got {:?}",
3509            com
3510        );
3511    }
3512
3513    #[test]
3514    fn test_constraint_with_static_body() {
3515        // Constraint between static and dynamic body — only dynamic should move
3516        let mut world = PhysicsWorld::<4, 4>::new();
3517        world.set_gravity(Vector3::zeros());
3518
3519        let fixed = world
3520            .add_body(RigidBody::new_static().with_position(Vector3::new(0.0, 0.0, 0.0)))
3521            .unwrap();
3522        let dynamic = world
3523            .add_body(
3524                RigidBody::new(1.0)
3525                    .with_position(Vector3::new(5.0, 0.0, 0.0))
3526                    .with_damping(0.0)
3527                    .with_angular_damping(0.0),
3528            )
3529            .unwrap();
3530
3531        // Distance constraint rest_length=2
3532        world
3533            .add_constraint(Constraint::Distance {
3534                body_a: fixed,
3535                body_b: dynamic,
3536                anchor_a: Vector3::zeros(),
3537                anchor_b: Vector3::zeros(),
3538                rest_length: 2.0,
3539                compliance: 0.0,
3540            })
3541            .unwrap();
3542
3543        for _ in 0..120 {
3544            world.step::<4>(1.0 / 60.0);
3545        }
3546
3547        // Static body should not move
3548        let p_fixed = world.body(fixed).unwrap().position;
3549        assert!(approx_vec_eq(&p_fixed, &Vector3::zeros()));
3550
3551        // Dynamic body should have moved toward rest_length distance
3552        let p_dyn = world.body(dynamic).unwrap().position;
3553        let dist = p_dyn.norm();
3554        assert!((dist - 2.0).abs() < 0.5, "Expected ~2.0, got {}", dist);
3555    }
3556
3557    #[test]
3558    fn test_solver_iterations_affect_convergence() {
3559        // More solver iterations should give tighter constraint enforcement
3560        let make_world = |iterations: u32| -> PhysicsWorld<4, 4> {
3561            let mut world = PhysicsWorld::<4, 4>::new();
3562            world.set_gravity(Vector3::new(0.0, -10.0, 0.0));
3563            world.solver_iterations = iterations;
3564
3565            let a = world
3566                .add_body(RigidBody::new_static().with_position(Vector3::new(0.0, 10.0, 0.0)))
3567                .unwrap();
3568            let b = world
3569                .add_body(
3570                    RigidBody::new(1.0)
3571                        .with_position(Vector3::new(3.0, 10.0, 0.0))
3572                        .with_damping(0.0)
3573                        .with_angular_damping(0.0),
3574                )
3575                .unwrap();
3576
3577            world
3578                .add_distance_constraint(a, Vector3::zeros(), b, Vector3::zeros(), 0.0)
3579                .unwrap();
3580            world
3581        };
3582
3583        let mut world_low = make_world(1);
3584        let mut world_high = make_world(16);
3585
3586        for _ in 0..120 {
3587            world_low.step::<4>(1.0 / 60.0);
3588            world_high.step::<4>(1.0 / 60.0);
3589        }
3590
3591        let pa_low = world_low.body(BodyId(0)).unwrap().position;
3592        let pb_low = world_low.body(BodyId(1)).unwrap().position;
3593        let error_low = ((pb_low - pa_low).norm() - 3.0).abs();
3594
3595        let pa_high = world_high.body(BodyId(0)).unwrap().position;
3596        let pb_high = world_high.body(BodyId(1)).unwrap().position;
3597        let error_high = ((pb_high - pa_high).norm() - 3.0).abs();
3598
3599        // Higher iterations should give less error (or equal)
3600        assert!(
3601            error_high <= error_low + EPSILON,
3602            "High iterations error {} should be <= low iterations error {}",
3603            error_high,
3604            error_low
3605        );
3606    }
3607
3608    #[test]
3609    fn test_no_constraints_no_effect() {
3610        // World with no constraints should behave identically
3611        let mut world = PhysicsWorld::<4, 4>::new();
3612        world.set_gravity(Vector3::new(0.0, -10.0, 0.0));
3613
3614        let id = world
3615            .add_body(
3616                RigidBody::new(1.0)
3617                    .with_position(Vector3::new(0.0, 10.0, 0.0))
3618                    .with_damping(0.0),
3619            )
3620            .unwrap();
3621
3622        world.step::<4>(1.0);
3623
3624        let body = world.body(id).unwrap();
3625        assert!(approx_eq(body.velocity.y, -10.0));
3626    }
3627
3628    #[test]
3629    fn test_add_distance_constraint_helper() {
3630        let mut world = PhysicsWorld::<4, 4>::new();
3631        let a = world
3632            .add_body(RigidBody::new(1.0).with_position(Vector3::new(0.0, 0.0, 0.0)))
3633            .unwrap();
3634        let b = world
3635            .add_body(RigidBody::new(1.0).with_position(Vector3::new(5.0, 0.0, 0.0)))
3636            .unwrap();
3637
3638        let cid = world
3639            .add_distance_constraint(a, Vector3::zeros(), b, Vector3::zeros(), 0.0)
3640            .unwrap();
3641
3642        // Rest length should be auto-computed from positions
3643        if let Constraint::Distance { rest_length, .. } = world.constraint(cid).unwrap() {
3644            assert!(approx_eq(*rest_length, 5.0));
3645        } else {
3646            panic!("Expected Distance constraint");
3647        }
3648    }
3649
3650    #[test]
3651    fn test_broad_phase_skips_distant_bodies() {
3652        // Two spheres far apart on the X axis — broad phase should prune them
3653        let mut world = PhysicsWorld::<4>::new();
3654        world
3655            .add_body(
3656                RigidBody::new(1.0)
3657                    .with_position(Vector3::new(0.0, 0.0, 0.0))
3658                    .with_collider(Collider::Sphere { radius: 1.0 }),
3659            )
3660            .unwrap();
3661        world
3662            .add_body(
3663                RigidBody::new(1.0)
3664                    .with_position(Vector3::new(100.0, 0.0, 0.0))
3665                    .with_collider(Collider::Sphere { radius: 1.0 }),
3666            )
3667            .unwrap();
3668
3669        let contacts = world.detect_collisions::<4>();
3670        assert!(contacts.is_empty(), "Distant bodies should not collide");
3671    }
3672
3673    #[test]
3674    fn test_broad_phase_regression() {
3675        // Same setup as test_detect_collisions_sphere_sphere — results must match
3676        let mut world = PhysicsWorld::<4>::new();
3677        world
3678            .add_body(
3679                RigidBody::new(1.0)
3680                    .with_position(Vector3::new(0.0, 0.0, 0.0))
3681                    .with_collider(Collider::Sphere { radius: 1.0 }),
3682            )
3683            .unwrap();
3684        world
3685            .add_body(
3686                RigidBody::new(1.0)
3687                    .with_position(Vector3::new(1.5, 0.0, 0.0))
3688                    .with_collider(Collider::Sphere { radius: 1.0 }),
3689            )
3690            .unwrap();
3691
3692        let contacts = world.detect_collisions::<4>();
3693        assert_eq!(
3694            contacts.len(),
3695            1,
3696            "Overlapping spheres should produce one contact"
3697        );
3698        assert!(contacts[0].penetration > 0.0);
3699    }
3700}