Skip to main content

alice_physics/
solver.rs

1//! XPBD (Extended Position Based Dynamics) Solver - Batched Edition
2//!
3//! Unified solver for rigid bodies, cloth, and ropes.
4//!
5//! # Key Features
6//!
7//! - Unconditionally stable (no stiffness-induced explosions)
8//! - Supports substeps for stiff constraints
9//! - Deterministic with fixed iteration counts
10//! - **Constraint Batching**: Graph-colored constraint groups for parallel solving
11//! - **Optional Parallelism**: Enable `parallel` feature for Rayon-based solving
12//!
13//! # Batching Strategy
14//!
15//! Constraints are grouped by "color" where constraints of the same color
16//! share no bodies (independent). This allows:
17//! - Sequential processing within each color (data dependency)
18//! - Parallel processing across colors (no conflicts)
19
20use crate::bvh::{BroadphaseHybrid, BvhPrimitive, LinearBvh};
21use crate::collider::{Contact, AABB};
22use crate::event::EventCollector;
23use crate::filter::CollisionFilter;
24use crate::force::{apply_force_fields, ForceFieldInstance};
25use crate::joint::{solve_joints, Joint};
26use crate::math::{select_vec3, Fix128, QuatFix, Vec3Fix};
27use crate::sdf_collider::SdfCollider;
28use crate::sleeping::{IslandManager, SleepConfig, SleepData, SleepState};
29
30/// Minimum effective inverse-mass sum below which constraint solving is skipped.
31/// Prevents division explosion when two near-static bodies are in contact.
32/// Value: 2^-40 ≈ 9.1e-13 in Fix128 (`lo = 1 << 24`). Before 1.2.0 the raw
33/// value was `1 << 40` = 2^-24 ≈ 6e-8 while the doc said 2^-40, so a dynamic
34/// body heavier than 2^24 kg against a static one (or two bodies above
35/// 2^25 kg) had its contacts, distance constraints and restitution
36/// silently skipped.
37const W_SUM_EPSILON: Fix128 = Fix128 {
38    hi: 0,
39    lo: 0x0000000001000000,
40};
41
42#[cfg(not(feature = "std"))]
43use alloc::vec;
44#[cfg(not(feature = "std"))]
45use alloc::vec::Vec;
46
47// Rayon parallel iterator support
48#[cfg(feature = "parallel")]
49use rayon::prelude::*;
50
51mod participants;
52mod world_ccd;
53mod world_snapshot;
54pub use participants::DeclareFieldError;
55pub use world_ccd::WorldCcdConfig;
56pub use world_snapshot::{LawCheck, WorldSnapshotError};
57
58// ============================================================================
59// Body Type
60// ============================================================================
61
62/// Type of rigid body
63#[derive(Clone, Copy, Debug, PartialEq, Eq)]
64#[repr(u8)]
65pub enum BodyType {
66    /// Moved by physics (gravity, constraints, impulses)
67    Dynamic = 0,
68    /// Never moves
69    Static = 1,
70    /// Moved by user code, pushes dynamic bodies but is not affected by them
71    Kinematic = 2,
72}
73
74// ============================================================================
75// Rigid Body
76// ============================================================================
77
78/// Default [`RigidBody::friction`] of a body built by `new`, `new_dynamic`,
79/// `new_static` and `new_kinematic`: 0.5, the `dynamic_friction` of the default
80/// material, so a body that keeps the defaults contacts like one with no body value.
81const DEFAULT_BODY_FRICTION: Fix128 = Fix128::from_raw(0, 9_223_372_036_854_775_808);
82
83/// Default [`RigidBody::restitution`] of a body built by `new`, `new_dynamic`,
84/// `new_static` and `new_kinematic`: 0.3, the `restitution` of the default material.
85const DEFAULT_BODY_RESTITUTION: Fix128 = Fix128::from_raw(0, 5_534_023_222_112_865_484);
86
87/// Rigid body state
88///
89/// Field layout is optimized for cache performance (Gap 1.1):
90/// - HOT fields (accessed every solver iteration) are placed first.
91/// - COLD fields (accessed occasionally) follow after.
92///
93/// `#[repr(C, align(64))]` ensures the struct starts on a cache-line boundary
94/// so the hot fields are always in the first 64-byte cache line.
95#[derive(Clone, Copy, Debug, PartialEq, Eq)]
96#[repr(C, align(64))]
97pub struct RigidBody {
98    // --- HOT fields (accessed every iteration) ---
99    /// Position (center of mass)
100    pub position: Vec3Fix,
101    /// Linear velocity
102    pub velocity: Vec3Fix,
103    /// Inverse mass (0 = static/infinite mass)
104    pub inv_mass: Fix128,
105    /// Inverse inertia tensor (diagonal, in local space)
106    pub inv_inertia: Vec3Fix,
107    /// Previous position (for XPBD)
108    pub prev_position: Vec3Fix,
109
110    // --- COLD fields (accessed occasionally) ---
111    /// Orientation, a unit quaternion
112    ///
113    /// [`Self::set_rotation`], [`PhysicsWorld::add_body`] and the start of
114    /// every [`PhysicsWorld::step`] store it at unit length. A non-unit value
115    /// written to this field directly (or through
116    /// [`PhysicsWorld::get_body_mut`], or restored from a snapshot) is used as
117    /// written until the next step, so what it rotates is scaled by `|q|^2`
118    /// in the queries made before that step.
119    pub rotation: QuatFix,
120    /// Angular velocity
121    pub angular_velocity: Vec3Fix,
122    /// Previous rotation (for XPBD)
123    pub prev_rotation: QuatFix,
124    /// Coefficient of restitution (bounciness), default 0.3
125    ///
126    /// Read by contacts only while the body has the default material
127    /// (see [`PhysicsWorld::combined_material`]).
128    pub restitution: Fix128,
129    /// Friction coefficient, default 0.5
130    ///
131    /// Read by contacts only while the body has the default material
132    /// (see [`PhysicsWorld::combined_material`]).
133    pub friction: Fix128,
134    /// Gravity scale multiplier (1.0 = normal, 0.0 = no gravity, 2.0 = double)
135    pub gravity_scale: Fix128,
136    /// Per-body linear damping factor (1.0 = no damping, 0.0 = full damping)
137    /// Multiplied with the global damping, applied once per frame (`step()`).
138    pub linear_damping: Fix128,
139    /// Per-body angular damping factor (1.0 = no damping, 0.0 = full damping)
140    /// Multiplied with the global damping, applied once per frame (`step()`).
141    pub angular_damping: Fix128,
142    /// Whether this body is a sensor/trigger (detects overlap but no physics response)
143    pub is_sensor: bool,
144    /// Body type (Dynamic, Static, Kinematic)
145    pub body_type: BodyType,
146    /// Kinematic target position and rotation
147    pub kinematic_target: Option<(Vec3Fix, QuatFix)>,
148}
149
150/// `a - b`, or `None` when the difference leaves the range of [`Fix128`];
151/// when it is `Some` it is bit for bit `a - b`.
152fn checked_fix_sub(a: Fix128, b: Fix128) -> Option<Fix128> {
153    let s = fix_wide(a).checked_sub(fix_wide(b))?;
154    Some(fix_narrow(s))
155}
156
157/// The Q64.64 bits of `f` as one `i128`.
158fn fix_wide(f: Fix128) -> i128 {
159    (i128::from(f.hi) << 64) | i128::from(f.lo)
160}
161
162/// The inverse of [`fix_wide`].
163fn fix_narrow(s: i128) -> Fix128 {
164    Fix128::from_raw((s >> 64) as i64, s as u64)
165}
166
167/// [`QuatFix::mul`] with every product, sum and difference checked, in the
168/// same order.
169fn checked_quat_mul(a: QuatFix, b: QuatFix) -> Option<QuatFix> {
170    let m = |x: Fix128, y: Fix128| x.checked_mul(y);
171    let x = checked_fix_sub(
172        Fix128::checked_add(
173            Fix128::checked_add(m(a.w, b.x)?, m(a.x, b.w)?)?,
174            m(a.y, b.z)?,
175        )?,
176        m(a.z, b.y)?,
177    )?;
178    let y = Fix128::checked_add(
179        Fix128::checked_add(checked_fix_sub(m(a.w, b.y)?, m(a.x, b.z)?)?, m(a.y, b.w)?)?,
180        m(a.z, b.x)?,
181    )?;
182    let z = Fix128::checked_add(
183        checked_fix_sub(
184            Fix128::checked_add(m(a.w, b.z)?, m(a.x, b.y)?)?,
185            m(a.y, b.x)?,
186        )?,
187        m(a.z, b.w)?,
188    )?;
189    let w = checked_fix_sub(
190        checked_fix_sub(checked_fix_sub(m(a.w, b.w)?, m(a.x, b.x)?)?, m(a.y, b.y)?)?,
191        m(a.z, b.z)?,
192    )?;
193    Some(QuatFix::new(x, y, z, w))
194}
195
196/// Whether a body-frame diagonal inverse inertia is isotropic (`x = y = z`).
197///
198/// For such a body `R · diag(c, c, c) · Rᵀ = c · E` for every rotation `R`,
199/// so a world-frame inverse inertia applies as the plain product with `c`.
200#[inline]
201#[must_use]
202pub(crate) fn inv_inertia_is_isotropic(inv_inertia: Vec3Fix) -> bool {
203    inv_inertia.x == inv_inertia.y && inv_inertia.y == inv_inertia.z
204}
205
206/// World-frame `I⁻¹ v` for the body-frame diagonal `inv_inertia` and the
207/// body orientation `rotation`: `R · diag(inv_inertia) · R⁻¹ v`.
208///
209/// # Claims
210/// - an isotropic `inv_inertia = (c, c, c)` gives the component-wise product
211///   `(v.x·c, v.y·c, v.z·c)` without rotating, so the result does not depend
212///   on `rotation` (the rotate, scale, rotate-back evaluation rounds
213///   differently for every non-identity rotation)
214/// - any other `inv_inertia` rotates `v` into the body frame by `R⁻¹`,
215///   scales it component-wise and rotates it back by `R`
216#[inline]
217#[must_use]
218pub(crate) fn inv_inertia_world_apply(
219    rotation: QuatFix,
220    inv_inertia: Vec3Fix,
221    v: Vec3Fix,
222) -> Vec3Fix {
223    if inv_inertia_is_isotropic(inv_inertia) {
224        let c = inv_inertia.x;
225        return Vec3Fix::new(v.x * c, v.y * c, v.z * c);
226    }
227    let local = rotation.conjugate().rotate_vec(v);
228    let scaled = Vec3Fix::new(
229        local.x * inv_inertia.x,
230        local.y * inv_inertia.y,
231        local.z * inv_inertia.z,
232    );
233    rotation.rotate_vec(scaled)
234}
235
236/// [`QuatFix::rotate_vec`] (`q v q*`) with every operation checked.
237fn checked_rotate_vec(q: QuatFix, v: Vec3Fix) -> Option<Vec3Fix> {
238    let qv = QuatFix::new(v.x, v.y, v.z, Fix128::ZERO);
239    let r = checked_quat_mul(checked_quat_mul(q, qv)?, q.conjugate())?;
240    Some(Vec3Fix::new(r.x, r.y, r.z))
241}
242
243impl RigidBody {
244    /// Create a new dynamic rigid body at the given position with the given mass.
245    ///
246    /// The inverse mass is computed automatically. A unit-sphere inertia tensor
247    /// is used as the default. Use `new_dynamic` as a more descriptive alias.
248    ///
249    /// # Examples
250    ///
251    /// ```
252    /// use alice_physics::{Fix128, RigidBody, Vec3Fix};
253    ///
254    /// let body = RigidBody::new(Vec3Fix::from_int(0, 10, 0), Fix128::ONE);
255    /// // inv_mass should be 1 / 1 = 1
256    /// assert_eq!(body.inv_mass.hi, 1);
257    /// // Position should match
258    /// assert_eq!(body.position.y.hi, 10);
259    /// // Dynamic body is not static
260    /// assert!(!body.is_static());
261    /// ```
262    #[must_use]
263    pub fn new(position: Vec3Fix, mass: Fix128) -> Self {
264        let inv_mass = if mass.is_zero() {
265            Fix128::ZERO
266        } else {
267            Fix128::ONE / mass
268        };
269
270        // Default inertia (unit sphere)
271        let inertia = mass * Fix128::from_ratio(2, 5);
272        let inv_inertia = if inertia.is_zero() {
273            Vec3Fix::ZERO
274        } else {
275            let inv_i = Fix128::ONE / inertia;
276            Vec3Fix::new(inv_i, inv_i, inv_i)
277        };
278
279        Self {
280            position,
281            velocity: Vec3Fix::ZERO,
282            inv_mass,
283            inv_inertia,
284            prev_position: position,
285            rotation: QuatFix::IDENTITY,
286            angular_velocity: Vec3Fix::ZERO,
287            prev_rotation: QuatFix::IDENTITY,
288            restitution: DEFAULT_BODY_RESTITUTION,
289            friction: DEFAULT_BODY_FRICTION,
290            gravity_scale: Fix128::ONE,
291            linear_damping: Fix128::ONE,
292            angular_damping: Fix128::ONE,
293            is_sensor: false,
294            body_type: BodyType::Dynamic,
295            kinematic_target: None,
296        }
297    }
298
299    /// Create new dynamic rigid body (alias for new)
300    #[inline]
301    #[must_use]
302    pub fn new_dynamic(position: Vec3Fix, mass: Fix128) -> Self {
303        Self::new(position, mass)
304    }
305
306    /// Create static (immovable) rigid body
307    #[must_use]
308    pub const fn new_static(position: Vec3Fix) -> Self {
309        Self {
310            position,
311            velocity: Vec3Fix::ZERO,
312            inv_mass: Fix128::ZERO,
313            inv_inertia: Vec3Fix::ZERO,
314            prev_position: position,
315            rotation: QuatFix::IDENTITY,
316            angular_velocity: Vec3Fix::ZERO,
317            prev_rotation: QuatFix::IDENTITY,
318            restitution: DEFAULT_BODY_RESTITUTION,
319            friction: DEFAULT_BODY_FRICTION,
320            gravity_scale: Fix128::ZERO,
321            linear_damping: Fix128::ONE,
322            angular_damping: Fix128::ONE,
323            is_sensor: false,
324            body_type: BodyType::Static,
325            kinematic_target: None,
326        }
327    }
328
329    /// Create a sensor (trigger) body - detects overlap but no physics response
330    #[must_use]
331    pub const fn new_sensor(position: Vec3Fix) -> Self {
332        Self {
333            position,
334            velocity: Vec3Fix::ZERO,
335            inv_mass: Fix128::ZERO,
336            inv_inertia: Vec3Fix::ZERO,
337            prev_position: position,
338            rotation: QuatFix::IDENTITY,
339            angular_velocity: Vec3Fix::ZERO,
340            prev_rotation: QuatFix::IDENTITY,
341            restitution: Fix128::ZERO,
342            friction: Fix128::ZERO,
343            gravity_scale: Fix128::ZERO,
344            linear_damping: Fix128::ONE,
345            angular_damping: Fix128::ONE,
346            is_sensor: true,
347            body_type: BodyType::Static,
348            kinematic_target: None,
349        }
350    }
351
352    /// Create a kinematic body (moved by user code, not physics)
353    ///
354    /// Kinematic bodies have infinite mass and are unaffected by forces,
355    /// but can push dynamic bodies. Set target via `set_kinematic_target`.
356    #[must_use]
357    pub const fn new_kinematic(position: Vec3Fix) -> Self {
358        Self {
359            position,
360            velocity: Vec3Fix::ZERO,
361            inv_mass: Fix128::ZERO,
362            inv_inertia: Vec3Fix::ZERO,
363            prev_position: position,
364            rotation: QuatFix::IDENTITY,
365            angular_velocity: Vec3Fix::ZERO,
366            prev_rotation: QuatFix::IDENTITY,
367            restitution: DEFAULT_BODY_RESTITUTION,
368            friction: DEFAULT_BODY_FRICTION,
369            gravity_scale: Fix128::ZERO,
370            linear_damping: Fix128::ONE,
371            angular_damping: Fix128::ONE,
372            is_sensor: false,
373            body_type: BodyType::Kinematic,
374            kinematic_target: None,
375        }
376    }
377
378    /// Check if body is static
379    #[inline]
380    #[must_use]
381    pub const fn is_static(&self) -> bool {
382        self.inv_mass.is_zero()
383    }
384
385    /// Check if body is kinematic
386    #[inline]
387    #[must_use]
388    pub fn is_kinematic(&self) -> bool {
389        self.body_type == BodyType::Kinematic
390    }
391
392    /// Check if body is dynamic
393    #[inline]
394    #[must_use]
395    pub fn is_dynamic(&self) -> bool {
396        self.body_type == BodyType::Dynamic
397    }
398
399    /// Set kinematic target position and rotation
400    ///
401    /// The body will be moved to this target during the next simulation step.
402    /// Velocity is automatically computed from the position change.
403    ///
404    /// `rotation` is stored as a unit quaternion (see [`Self::set_rotation`]).
405    pub fn set_kinematic_target(&mut self, position: Vec3Fix, rotation: QuatFix) {
406        self.kinematic_target = Some((position, rotation.unit_rotation()));
407    }
408
409    /// Apply impulse at center of mass
410    pub fn apply_impulse(&mut self, impulse: Vec3Fix) {
411        if !self.is_static() {
412            self.velocity = self.velocity + impulse * self.inv_mass;
413        }
414    }
415
416    /// World-frame `I⁻¹ τ` for a body whose `inv_inertia` is diagonal in the body frame.
417    ///
418    /// # Claims
419    /// - `τ` is a world-frame vector; the result is a world-frame vector
420    /// - computed as `R · diag(inv_inertia) · R⁻¹ τ` with `R = self.rotation`
421    /// - for an identity rotation it equals the component-wise product with `inv_inertia`
422    /// - a zero `inv_inertia` (or a zero `τ`) gives the zero vector
423    /// - an isotropic `inv_inertia = (c, c, c)` gives the component-wise
424    ///   product `c · τ` for every rotation (see [`inv_inertia_world_apply`])
425    #[inline]
426    pub(crate) fn world_inv_inertia_apply(&self, torque: Vec3Fix) -> Vec3Fix {
427        inv_inertia_world_apply(self.rotation, self.inv_inertia, torque)
428    }
429
430    /// [`Self::world_inv_inertia_apply`] with every product, sum and
431    /// difference checked: `None` when one of them leaves the range of
432    /// [`Fix128`].
433    ///
434    /// # Claims
435    /// - when it is `Some`, the value is bit for bit the one of
436    ///   [`Self::world_inv_inertia_apply`] (the same operations in the same
437    ///   order; a wrapping sum whose every partial sum is in range is exact)
438    /// - a product goes through [`Fix128::checked_mul`]; an intermediate
439    ///   partial sum out of range is `None` even if the final value would be
440    ///   in range (the rotation of a unit quaternion keeps the partial sums
441    ///   within a small factor of `|τ|`, so this only matters near the edge
442    ///   of the range)
443    #[must_use]
444    pub(crate) fn checked_world_inv_inertia_apply(&self, torque: Vec3Fix) -> Option<Vec3Fix> {
445        if inv_inertia_is_isotropic(self.inv_inertia) {
446            let c = self.inv_inertia.x;
447            return Some(Vec3Fix::new(
448                torque.x.checked_mul(c)?,
449                torque.y.checked_mul(c)?,
450                torque.z.checked_mul(c)?,
451            ));
452        }
453        let q = self.rotation;
454        let local = checked_rotate_vec(q.conjugate(), torque)?;
455        let scaled = Vec3Fix::new(
456            local.x.checked_mul(self.inv_inertia.x)?,
457            local.y.checked_mul(self.inv_inertia.y)?,
458            local.z.checked_mul(self.inv_inertia.z)?,
459        );
460        checked_rotate_vec(q, scaled)
461    }
462
463    /// Apply impulse at world-space point
464    ///
465    /// # Claims
466    /// - the linear part is `impulse * inv_mass`
467    /// - the angular part is `I_world⁻¹ ((point - position) × impulse)`, with the body-frame
468    ///   diagonal `inv_inertia` rotated by `rotation` into the world frame
469    /// - a static body is unchanged
470    pub fn apply_impulse_at(&mut self, impulse: Vec3Fix, point: Vec3Fix) {
471        if !self.is_static() {
472            self.velocity = self.velocity + impulse * self.inv_mass;
473
474            let r = point - self.position;
475            let torque = r.cross(impulse);
476            self.angular_velocity = self.angular_velocity + self.world_inv_inertia_apply(torque);
477        }
478    }
479
480    /// Apply a continuous force (accumulated, applied during next step)
481    ///
482    /// Force is converted to velocity change: v += (F * `inv_mass`) * dt
483    /// For one-shot velocity changes, use `apply_impulse` instead.
484    #[inline]
485    pub fn add_force(&mut self, force: Vec3Fix, dt: Fix128) {
486        if !self.is_static() {
487            self.velocity = self.velocity + force * self.inv_mass * dt;
488        }
489    }
490
491    /// Apply a continuous torque
492    ///
493    /// # Claims
494    /// - `torque` is a world-frame vector; `angular_velocity += I_world⁻¹ torque * dt`
495    /// - `I_world⁻¹ = R diag(inv_inertia) R⁻¹` with `R = rotation`
496    /// - a static body is unchanged
497    #[inline]
498    pub fn add_torque(&mut self, torque: Vec3Fix, dt: Fix128) {
499        if !self.is_static() {
500            self.angular_velocity =
501                self.angular_velocity + self.world_inv_inertia_apply(torque) * dt;
502        }
503    }
504
505    /// Set the linear velocity directly
506    #[inline]
507    pub fn set_velocity(&mut self, velocity: Vec3Fix) {
508        self.velocity = velocity;
509    }
510
511    /// Set the angular velocity directly
512    #[inline]
513    pub fn set_angular_velocity(&mut self, angular_velocity: Vec3Fix) {
514        self.angular_velocity = angular_velocity;
515    }
516
517    /// Set position directly (teleport)
518    #[inline]
519    pub fn set_position(&mut self, position: Vec3Fix) {
520        self.position = position;
521        self.prev_position = position;
522    }
523
524    /// Set rotation directly
525    ///
526    /// The stored rotation is `rotation` as a unit quaternion: one whose
527    /// squared length is within `2^-32` of one is kept bit for bit, any other
528    /// is normalized (the zero quaternion becomes the identity). Applying a
529    /// quaternion `q` scales what it rotates by `|q|^2`.
530    #[inline]
531    pub fn set_rotation(&mut self, rotation: QuatFix) {
532        let rotation = rotation.unit_rotation();
533        self.rotation = rotation;
534        self.prev_rotation = rotation;
535    }
536
537    /// The stored rotations (`rotation`, `prev_rotation` and the rotation of
538    /// `kinematic_target`) as unit quaternions, see
539    /// [`QuatFix::unit_rotation`]: a rotation already of unit length (within
540    /// `2^-32` in squared length) is left bit for bit.
541    #[inline]
542    pub(crate) fn make_rotations_unit(&mut self) {
543        self.rotation = self.rotation.unit_rotation();
544        self.prev_rotation = self.prev_rotation.unit_rotation();
545        if let Some((position, rotation)) = self.kinematic_target {
546            self.kinematic_target = Some((position, rotation.unit_rotation()));
547        }
548    }
549
550    /// Get mass (inverse of `inv_mass`, returns infinity for static bodies)
551    #[inline]
552    #[must_use]
553    pub fn mass(&self) -> Fix128 {
554        if self.inv_mass.is_zero() {
555            Fix128::ZERO // Represents infinite mass
556        } else {
557            Fix128::ONE / self.inv_mass
558        }
559    }
560
561    /// Get linear speed (magnitude of velocity)
562    #[inline]
563    #[must_use]
564    pub fn speed(&self) -> Fix128 {
565        self.velocity.length()
566    }
567
568    /// Set per-body linear damping (1.0 = no extra damping)
569    #[inline]
570    #[must_use]
571    pub const fn with_linear_damping(mut self, damping: Fix128) -> Self {
572        self.linear_damping = damping;
573        self
574    }
575
576    /// Set per-body angular damping (1.0 = no extra damping)
577    #[inline]
578    #[must_use]
579    pub const fn with_angular_damping(mut self, damping: Fix128) -> Self {
580        self.angular_damping = damping;
581        self
582    }
583
584    /// Builder: set restitution (bounciness)
585    #[inline]
586    #[must_use]
587    pub const fn with_restitution(mut self, restitution: Fix128) -> Self {
588        self.restitution = restitution;
589        self
590    }
591
592    /// Builder: set friction coefficient
593    #[inline]
594    #[must_use]
595    pub const fn with_friction(mut self, friction: Fix128) -> Self {
596        self.friction = friction;
597        self
598    }
599
600    /// Builder: set gravity scale
601    #[inline]
602    #[must_use]
603    pub const fn with_gravity_scale(mut self, scale: Fix128) -> Self {
604        self.gravity_scale = scale;
605        self
606    }
607
608    /// Builder: set as sensor (trigger)
609    #[inline]
610    #[must_use]
611    pub const fn with_sensor(mut self, is_sensor: bool) -> Self {
612        self.is_sensor = is_sensor;
613        self
614    }
615
616    /// Builder: set initial velocity
617    #[inline]
618    #[must_use]
619    pub const fn with_velocity(mut self, velocity: Vec3Fix) -> Self {
620        self.velocity = velocity;
621        self
622    }
623
624    /// Builder: set initial rotation
625    #[inline]
626    #[must_use]
627    pub const fn with_rotation(mut self, rotation: QuatFix) -> Self {
628        self.rotation = rotation;
629        self.prev_rotation = rotation;
630        self
631    }
632}
633
634impl Default for RigidBody {
635    /// Default creates a 1kg dynamic body at the origin.
636    fn default() -> Self {
637        Self::new(Vec3Fix::ZERO, Fix128::ONE)
638    }
639}
640
641// ============================================================================
642// Constraints
643// ============================================================================
644
645/// Distance constraint between two bodies
646#[derive(Clone, Copy, Debug, PartialEq, Eq)]
647pub struct DistanceConstraint {
648    /// Index of the first body
649    pub body_a: usize,
650    /// Index of the second body
651    pub body_b: usize,
652    /// Anchor point in body A's local space
653    pub local_anchor_a: Vec3Fix,
654    /// Anchor point in body B's local space
655    pub local_anchor_b: Vec3Fix,
656    /// Target distance between anchors
657    pub target_distance: Fix128,
658    /// Inverse stiffness (0 = infinitely stiff)
659    pub compliance: Fix128,
660    /// XPBD Lagrange multiplier accumulated over the current substep.
661    ///
662    /// Zeroed at the start of every substep and incremented by `dlambda` in each
663    /// solve iteration (Macklin 2016), so the compliance term `alpha~ * lambda`
664    /// sees the total force applied so far and the effective stiffness is
665    /// independent of `iterations`. Before 1.2.0 it held only the last
666    /// iteration's increment and was carried across substeps.
667    pub cached_lambda: Fix128,
668}
669
670impl DistanceConstraint {
671    /// Create a new distance constraint
672    #[must_use]
673    pub const fn new(
674        body_a: usize,
675        body_b: usize,
676        anchor_a: Vec3Fix,
677        anchor_b: Vec3Fix,
678        distance: Fix128,
679    ) -> Self {
680        Self {
681            body_a,
682            body_b,
683            local_anchor_a: anchor_a,
684            local_anchor_b: anchor_b,
685            target_distance: distance,
686            compliance: Fix128::ZERO,
687            cached_lambda: Fix128::ZERO,
688        }
689    }
690
691    /// Set compliance (inverse stiffness)
692    #[must_use]
693    pub const fn with_compliance(mut self, compliance: Fix128) -> Self {
694        self.compliance = compliance;
695        self
696    }
697}
698
699/// Contact constraint (from collision detection)
700#[derive(Clone, Copy, Debug, PartialEq, Eq)]
701pub struct ContactConstraint {
702    /// Index of the first body
703    pub body_a: usize,
704    /// Index of the second body
705    pub body_b: usize,
706    /// Contact information from collision detection
707    pub contact: Contact,
708    /// Friction coefficient
709    pub friction: Fix128,
710    /// Restitution (bounciness) coefficient
711    pub restitution: Fix128,
712    /// Contact multiplier accumulated over the substep the contact lives in
713    /// (contacts are re-detected every substep since 1.2.0): the separation
714    /// along `contact.normal` already applied by earlier solver iterations.
715    pub cached_lambda: Fix128,
716}
717
718impl ContactConstraint {
719    /// Create a new contact constraint with default friction and restitution
720    #[must_use]
721    pub fn new(body_a: usize, body_b: usize, contact: Contact) -> Self {
722        Self {
723            body_a,
724            body_b,
725            contact,
726            friction: Fix128::from_ratio(3, 10), // 0.3 friction
727            restitution: Fix128::from_ratio(2, 10), // 0.2 restitution
728            cached_lambda: Fix128::ZERO,
729        }
730    }
731}
732
733// ============================================================================
734// XPBD Solver
735// ============================================================================
736
737/// Selects which rigid-body integrator [`PhysicsWorld::step`] dispatches to.
738///
739/// Both variants read and write the same [`RigidBody`] fields, so a world
740/// can be migrated between backends between steps without changing any
741/// other configuration. They are **not** numerically interchangeable —
742/// XPBD is position-based (Macklin/Müller) and TGS is a sub-stepping
743/// impulse-based projected Gauss-Seidel integrator (Müller et al. 2020,
744/// see the crate-internal `solver_tgs` module) — so the same scene produces different
745/// (both physically valid) trajectories under each backend. Pick one and
746/// stay on it for a given simulation; do not expect bit-identical replay
747/// across a backend switch.
748///
749/// # Notes on the `Tgs` path
750///
751/// [`DistanceConstraint`]s are enforced as bilateral velocity-level impulse
752/// joints (`JointOriented` in `solver_tgs_hooks_6dof_oriented`), solved
753/// every TGS inner sub-step alongside contacts, plus a Baumgarte position
754/// correction for residual drift — a genuinely different algorithm from
755/// XPBD's position-based compliance solve, so the two backends converge to
756/// slightly different equilibria for the same scene (not a bug; see
757/// `tests/analytic_tgs_wiring.rs`'s joint tests for both backends' measured
758/// bands).
759///
760/// Pre-solve hooks and contact modifiers run once over the
761/// tick's contacts before the solve (`tests/analytic_tgs_backend_coverage.rs`).
762///
763/// The world's [`Joint`]s are solved inside the TGS substep loop: after each
764/// substep's impulse solve, the same position-level joint projection the XPBD
765/// substep runs is applied and its correction is added to the bodies' linear
766/// and angular velocities (`tests/analytic_tgs_joints_in_substep.rs`).
767///
768/// * Requires the `std` feature (`solver_tgs` is `std`-gated, same as the
769///   rest of the TGS family). Selecting `Tgs` in a build without `std`
770///   is **not** silently ignored at the type level — the variant still
771///   exists — but [`PhysicsWorld::step`] falls back to running `Xpbd`
772///   for that build, since there is no TGS implementation to dispatch to.
773///   This is a documented, defined fallback (a different already-correct
774///   solver runs), not state corruption.
775#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
776#[non_exhaustive]
777pub enum SolverBackend {
778    /// Extended Position-Based Dynamics — the solver this crate has shipped
779    /// since 0.1.0. Default.
780    #[default]
781    Xpbd,
782    /// Sub-stepping impulse-based temporal Gauss-Seidel. See the gaps listed
783    /// on [`SolverBackend`] itself before using this in production.
784    Tgs,
785}
786
787/// The broad-phase [`PhysicsWorld`] uses to find the pairs of bodies whose
788/// colliders may touch.
789///
790/// A body's box is the closed-form world box of its collider when it carries a
791/// shape or a compound, and the cube of its collision sphere otherwise. Every
792/// kind hands its candidates (pairs whose boxes may overlap) to the
793/// same exact narrow-phase in ascending pair order, and a pair whose boxes do not
794/// meet has no contact, so a world steps to **bit-identical** results whichever
795/// it uses; they differ in cost. Set with [`PhysicsWorld::set_broadphase`].
796#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
797#[non_exhaustive]
798pub enum Broadphase {
799    /// A linear BVH over Morton codes, rebuilt from scratch every step. Default.
800    #[default]
801    Bvh,
802    /// A persistent dynamic AABB tree ([`crate::dynamic_bvh::DynamicAabbTree`]):
803    /// each body keeps a fattened proxy that is only re-inserted when the body
804    /// leaves it, so a world where most bodies move little pays for the few that do.
805    DynamicTree,
806    /// Static bodies in a BVH rebuilt only when the static set changes, small
807    /// dynamic bodies in a sparse hash grid whose cell size follows the median
808    /// body, and large dynamic bodies in a per-step BVH; reports exactly the
809    /// pairs whose boxes overlap. Fastest when many bodies are static or bodies
810    /// are of similar size. The sleep skip ([`PhysicsWorld::set_sleep_skip`])
811    /// parks bodies only with [`Broadphase::Bvh`]; with this kind every body is
812    /// staged every step.
813    Hybrid,
814}
815
816/// What the [`Broadphase::DynamicTree`] broad-phase currently holds, returned by
817/// [`PhysicsWorld::broadphase_stats`].
818#[derive(Clone, Copy, Debug, PartialEq, Eq)]
819pub struct BroadphaseStats {
820    /// Bodies with a proxy in the tree.
821    pub proxies: usize,
822    /// Height of the tree (0 when empty).
823    pub height: i32,
824}
825
826/// How much work the last [`PhysicsWorld::step`] did, stage by stage.
827///
828/// Every counter is a number of items a stage visited, summed over the substeps
829/// of that one call (reset when `step` starts). They count work, not time, so a
830/// test can assert on them without measuring a clock. `step_parallel`,
831/// `step_with_bridge`, the TGS backend and the public substep API add to the
832/// same counters but do not reset them.
833///
834/// With the sleep skip on ([`PhysicsWorld::set_sleep_skip`], the default) a
835/// sleeping body that is at rest and not attached to a joint or distance
836/// constraint is *parked*: every stage below except `sleep_scanned` and
837/// `parked_sleep_updates` leaves it out, and the broad-phase finds its contacts
838/// through a persistent tree instead of rebuilding it. The counters then depend
839/// on the awake bodies only, except those two, which stay one per sleeping body
840/// per step (a check that the parked state was not edited between steps, and
841/// the `idle_frames` count the snapshot carries).
842#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
843#[non_exhaustive]
844pub struct StageWork {
845    /// Bodies examined at the start of the step to decide which are parked.
846    pub sleep_scanned: u64,
847    /// Bodies parked for this step (not counting ones woken during it).
848    pub parked: u64,
849    /// Parked bodies woken by a contact during the step.
850    pub unparked: u64,
851    /// Bodies the force fields were applied to.
852    pub force_field_bodies: u64,
853    /// Bodies visited by position integration.
854    pub integrated: u64,
855    /// Bodies put into the per-substep broad-phase BVH.
856    pub broadphase_primitives: u64,
857    /// Candidate pairs the broad-phase handed to the narrow-phase.
858    pub broadphase_pairs: u64,
859    /// Pairs of bodies whose exact boxes the [`Broadphase::Hybrid`] broad-phase
860    /// compared to find `broadphase_pairs` (0 with the other broad-phases).
861    pub broadphase_box_tests: u64,
862    /// Bodies tested against the static and SDF colliders.
863    pub resolution_bodies: u64,
864    /// Bodies whose velocity was derived from their position change.
865    pub velocity_bodies: u64,
866    /// Bodies visited by the frame damping.
867    pub damping_bodies: u64,
868    /// Bodies whose sleep state was evaluated from their velocity.
869    pub sleep_evaluated: u64,
870    /// Parked bodies whose sleep bookkeeping was advanced without evaluation.
871    pub parked_sleep_updates: u64,
872    /// Proxies inserted into the parked-body tree.
873    pub tree_inserts: u64,
874    /// Proxies removed from the parked-body tree.
875    pub tree_removes: u64,
876}
877
878/// What a parked body looked like when it was last found at rest, so the next
879/// step can tell with a few compares that nothing has edited it since.
880#[derive(Clone, Copy, Debug, PartialEq, Eq)]
881struct ParkCache {
882    position: Vec3Fix,
883    rotation: QuatFix,
884    /// The angular velocity the velocity derivation gives a body whose rotation
885    /// did not change (not always exactly zero: `x·(−y)` and `−(x·y)` can differ
886    /// by one ulp in fixed point).
887    angular_velocity: Vec3Fix,
888    radius: Option<Fix128>,
889    /// Recorded for a `BodyType::Static` body (only position and radius matter,
890    /// and the rotation when the body carries a collider: it turns the box).
891    static_kind: bool,
892}
893
894/// Everything a parked body's "a step changes nothing but `idle_frames`" verdict
895/// depends on besides its own state. Any change drops every [`ParkCache`].
896#[derive(Clone, Debug, PartialEq, Eq)]
897struct ParkKey {
898    substep_dt: Fix128,
899    sleep: SleepConfig,
900    generation: u64,
901    sdf_radius: Fix128,
902    /// Pose, scale, attachment and field address of each SDF collider (the
903    /// `sdf_colliders` field is public, so it is compared every step).
904    sdf: Vec<(Vec3Fix, QuatFix, Fix128, usize, usize)>,
905}
906
907/// Book-keeping of the sleep skip (see [`StageWork`]).
908#[derive(Default)]
909struct ParkState {
910    /// Parking is in effect for the step being run.
911    active: bool,
912    /// Per body: parked for the rest of the current step.
913    parked: Vec<bool>,
914    /// The bodies parked at the start of the step, ascending.
915    parked_list: Vec<usize>,
916    /// The bodies that are not parked, ascending; the stages iterate these.
917    awake: Vec<usize>,
918    cache: Vec<Option<ParkCache>>,
919    proxies: Vec<Option<u32>>,
920    /// Number of `Some` in `proxies` (`DynamicAabbTree::proxy_count` walks
921    /// every node, which would put the parked bodies back into each substep).
922    proxy_live: usize,
923    tree: crate::dynamic_bvh::DynamicAabbTree,
924    key: Option<ParkKey>,
925    /// Scratch: body referenced by a joint or a distance constraint.
926    constrained: Vec<bool>,
927    /// The substep being run is the first of the step.
928    first_substep: bool,
929    /// The full `dt` of the step (force fields act on it).
930    frame_dt: Fix128,
931}
932
933impl ParkState {
934    /// [`Self::clear`], counting the dropped proxies as tree removals.
935    fn clear_counted(&mut self, stats: &mut StageWork) {
936        stats.tree_removes += self.proxy_live as u64;
937        self.clear();
938    }
939
940    /// Drop the parked-body tree and every cache.
941    fn clear(&mut self) {
942        self.tree = crate::dynamic_bvh::DynamicAabbTree::new();
943        self.proxies.clear();
944        self.proxy_live = 0;
945        self.cache.clear();
946        self.parked.clear();
947        self.parked_list.clear();
948        self.awake.clear();
949        self.key = None;
950        self.active = false;
951    }
952
953    /// Remove body `i`'s proxy from the tree, if it has one.
954    fn remove_proxy(&mut self, i: usize, stats: &mut StageWork) {
955        if let Some(proxy) = self.proxies.get_mut(i).and_then(Option::take) {
956            self.tree.remove(proxy);
957            self.proxy_live -= 1;
958            stats.tree_removes += 1;
959        }
960    }
961
962    /// Body `i` was woken during the step: from now on every stage sees it.
963    fn unpark(&mut self, i: usize, stats: &mut StageWork) {
964        if !self.active || !self.parked.get(i).copied().unwrap_or(false) {
965            return;
966        }
967        self.parked[i] = false;
968        self.remove_proxy(i, stats);
969        if let Err(pos) = self.awake.binary_search(&i) {
970            self.awake.insert(pos, i);
971        }
972        stats.unparked += 1;
973    }
974
975    /// Whether body `i` is parked in the current step.
976    #[inline]
977    fn is_parked(&self, i: usize) -> bool {
978        self.active && self.parked.get(i).copied().unwrap_or(false)
979    }
980}
981
982/// Below this `|v|²` (`|v| < 2⁻⁸`) the rotation angle `2·asin|v|` is taken
983/// from its series, whose first omitted term is below `2⁻⁶⁴` relative.
984const LOG_SERIES_LIMIT_SQ: Fix128 = Fix128::from_raw(0, 1 << 48);
985
986/// Angular velocity derived from a rotation change by the exact logarithm of
987/// the rotation: with `dq = q·p⁻¹ = (v, w)`, its sign folded so that `w ≥ 0`
988/// (the shorter of the two equivalent rotations), the rotation is
989/// `θ = 2·atan2(|v|, w)` about `v / |v|`, so `ω = v̂ θ / dt`.
990///
991/// The angle is evaluated as the factor `θ / |v| = 2·asin(|v|)/|v|`
992/// multiplying `v`: from the series `2 (1 + s²/6 + 3s⁴/40 + 5s⁶/112)`
993/// (`s² = |v|²`) below `|v| = 2⁻⁸`, which needs no square root and keeps
994/// small rotations exact to rounding, and from `2·atan2(|v|, w)/|v|` above.
995/// A rotation predicted as `from_axis_angle(ω̂, |ω| dt)` therefore gives back
996/// `ω` (the earlier chord `2 v / dt` gave `(2/dt) sin(|ω| dt / 2)`, which
997/// lost `|ω|³ dt² / 24` per call).
998#[inline]
999fn angular_from_rotations(rotation: QuatFix, prev_rotation: QuatFix, inv_dt: Fix128) -> Vec3Fix {
1000    let dq = rotation.mul(prev_rotation.conjugate());
1001    let (v, w) = if dq.w < Fix128::ZERO {
1002        (Vec3Fix::new(-dq.x, -dq.y, -dq.z), -dq.w)
1003    } else {
1004        (Vec3Fix::new(dq.x, dq.y, dq.z), dq.w)
1005    };
1006    let s2 = v.dot(v);
1007    let factor = if s2 < LOG_SERIES_LIMIT_SQ {
1008        let series = Fix128::from_ratio(1, 6)
1009            + s2 * (Fix128::from_ratio(3, 40) + s2 * Fix128::from_ratio(5, 112));
1010        (Fix128::ONE + s2 * series).double()
1011    } else {
1012        let s = s2.sqrt();
1013        Fix128::atan2(s, w).double() / s
1014    };
1015    v * (factor * inv_dt)
1016}
1017
1018/// Kind tags for [`tgs_cache_key`]: contacts and distance constraints share
1019/// one warm-start cache.
1020#[cfg(feature = "std")]
1021const TGS_KEY_CONTACT: u64 = 0;
1022#[cfg(feature = "std")]
1023const TGS_KEY_DISTANCE: u64 = 1;
1024
1025/// Warm-start cache key for the `ordinal`-th entry of kind `tag` joining the
1026/// bodies with stable ids `a` → `b` this tick (`ordinals` counts per pair and
1027/// is reset every tick by the caller creating it afresh).
1028///
1029/// The four words are mixed with the SplitMix64 finaliser, so the key is a
1030/// deterministic function of what the entry joins — never of where it sits
1031/// in a vector. Two distinct entries collide with probability about
1032/// `n² / 2⁶⁵` per tick for `n` entries.
1033#[cfg(feature = "std")]
1034fn tgs_cache_key(
1035    tag: u64,
1036    a: u64,
1037    b: u64,
1038    ordinals: &mut std::collections::BTreeMap<(u64, u64), u64>,
1039) -> u64 {
1040    let slot = ordinals.entry((a, b)).or_insert(0);
1041    let ordinal = *slot;
1042    *slot += 1;
1043    let mut h: u64 = 0x9E37_79B9_7F4A_7C15;
1044    for w in [tag, a, b, ordinal] {
1045        h ^= w;
1046        h = h.wrapping_add(0x9E37_79B9_7F4A_7C15);
1047        h = (h ^ (h >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
1048        h = (h ^ (h >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
1049        h ^= h >> 31;
1050    }
1051    h
1052}
1053
1054/// Predict a dynamic body's rotation over one substep `dt`.
1055///
1056/// A body with a gyroscopic response is advanced by the symplectic splitting
1057/// of [`crate::gyroscopic::split_free_rotation`]: its rotation and angular
1058/// velocity become the split's end state, and the split is returned so that
1059/// `update_velocities` can rebuild `ω` from it. Any other body (isotropic,
1060/// at rest, infinite moments) turns about `ω` by `|ω| dt`, as before.
1061///
1062/// `|ω|² ≥ 2⁶³` (`|ω| ≥ 2³¹·⁵ ≈ 3.04e9` rad/s) では `normalize_with_length` の
1063/// 長さが wrap して 0 になり、回転が止まって速度導出で ω まで 0 に消える
1064/// その範囲だけ、2 乗を経ない長さと向き ([`Vec3Fix::checked_length_scaled`] /
1065/// [`Vec3Fix::try_normalize_scaled`]) で回す 長さ自体か `|ω|·dt` が表せない
1066/// 時は回さずに `overflow` を立てる 範囲内は従来の式のまま (bit 不変)
1067#[inline]
1068fn predict_rotation(
1069    body: &mut RigidBody,
1070    dt: Fix128,
1071    overflow: &mut bool,
1072) -> Option<crate::gyroscopic::FreeRotation> {
1073    let split = crate::gyroscopic::split_free_rotation(
1074        body.angular_velocity,
1075        body.rotation,
1076        body.inv_inertia,
1077        dt,
1078    );
1079    match split {
1080        Some(f) => {
1081            body.angular_velocity = f.omega;
1082            body.rotation = f.rotation;
1083        }
1084        None => {
1085            let omega = body.angular_velocity;
1086            if let Some(speed_sq) = omega.checked_length_squared() {
1087                // `normalize_with_length` の式を展開したもの (single sqrt、
1088                // `Some` の 2 乗は `length_squared` と bit 一致)
1089                let ang_speed = speed_sq.sqrt();
1090                if !ang_speed.is_zero() {
1091                    let axis = omega * (Fix128::ONE / ang_speed);
1092                    let delta_rot = QuatFix::from_axis_angle(axis, ang_speed * dt);
1093                    body.rotation = delta_rot.mul(body.rotation).normalize();
1094                }
1095            } else {
1096                // |ω|² が範囲外: ω は非零 (零なら 2 乗は 0) なので向きは必ずある
1097                let turn = omega
1098                    .checked_length_scaled()
1099                    .and_then(|s| s.checked_mul(dt))
1100                    .zip(omega.try_normalize_scaled());
1101                match turn {
1102                    Some((angle, axis)) => {
1103                        let delta_rot = QuatFix::from_axis_angle(axis, angle);
1104                        body.rotation = delta_rot.mul(body.rotation).normalize();
1105                    }
1106                    None => *overflow = true,
1107                }
1108            }
1109        }
1110    }
1111    split
1112}
1113
1114/// Linear velocity at the end of a substep, written into `body.velocity`.
1115///
1116/// A body predicted this substep (`predicted = Some(x_pred)`, with
1117/// `body.velocity` still the predicted velocity `v_pred` the prediction
1118/// `x_pred = x_prev + v_pred·h` used) gets `v = v_pred + (x − x_pred) / h`:
1119/// the predicted velocity plus the correction the solve applied. A body no
1120/// constraint or contact moved keeps `v_pred` bit for bit. In exact
1121/// arithmetic this equals `(x − x_prev) / h`; re-deriving from the position
1122/// difference instead dropped the low bits that the truncating product
1123/// `v_pred·h` lost, every substep, so free bodies did not conserve momentum.
1124/// Any other body (`None`) derives `v = (x − x_prev) / h`, as before.
1125/// A substep whose position add left the range keeps the in-range predicted
1126/// velocity: the position stays put, so it equals `x_pred` and the
1127/// correction is zero.
1128///
1129/// Returns `false` when the scaled difference leaves the `Fix128` range; the
1130/// velocity is then left unchanged.
1131#[inline]
1132fn derive_linear_velocity(
1133    body: &mut RigidBody,
1134    predicted: Option<Vec3Fix>,
1135    inv_dt: Fix128,
1136) -> bool {
1137    match predicted {
1138        Some(x_pred) if body.position == x_pred => true,
1139        Some(x_pred) => match (body.position - x_pred).checked_scale(inv_dt) {
1140            Some(dv) => {
1141                body.velocity = body.velocity + dv;
1142                true
1143            }
1144            None => false,
1145        },
1146        None => match (body.position - body.prev_position).checked_scale(inv_dt) {
1147            Some(v) => {
1148                body.velocity = v;
1149                true
1150            }
1151            None => false,
1152        },
1153    }
1154}
1155
1156/// Angular velocity at the end of a substep. A body predicted by the
1157/// splitting keeps the split's end velocity plus the rotation the position
1158/// solve added beyond the split's end orientation (zero when no constraint
1159/// turned it). Any other body predicted this substep (`predicted =
1160/// Some(q_pred)`, `body.angular_velocity` still the `ω_pred` that predicted
1161/// `q_pred`) does the same with `ω_pred` and `q_pred`: a body the solve did
1162/// not turn keeps `ω_pred` bit for bit (re-deriving it from
1163/// `log(q · q_prev⁻¹)` rounds through the axis-angle, the normalisation and
1164/// the logarithm). A body not predicted (`None`: static, kinematic,
1165/// sleeping, parked) derives `ω` from its rotation change, as
1166/// [`angular_from_rotations`] always did.
1167#[inline]
1168fn derived_angular_velocity(
1169    body: &RigidBody,
1170    free: Option<crate::gyroscopic::FreeRotation>,
1171    predicted: Option<QuatFix>,
1172    inv_dt: Fix128,
1173) -> Vec3Fix {
1174    match (free, predicted) {
1175        (Some(f), _) if body.rotation == f.rotation => f.omega,
1176        (Some(f), _) => f.omega + angular_from_rotations(body.rotation, f.rotation, inv_dt),
1177        (None, Some(q)) if body.rotation == q => body.angular_velocity,
1178        (None, Some(q)) => body.angular_velocity + angular_from_rotations(body.rotation, q, inv_dt),
1179        (None, None) => angular_from_rotations(body.rotation, body.prev_rotation, inv_dt),
1180    }
1181}
1182
1183/// Snapshot of [`SolverBackend::Tgs`]'s per-frame warm-start impulse cache
1184/// effectiveness, returned by [`PhysicsWorld::tgs_cache_stats`].
1185///
1186/// Mirrors `crate::solver_tgs::ImpulseCacheStats` (which stays
1187/// crate-internal along with the rest of the `solver_tgs` family, see
1188/// [`SolverBackend`]'s module doc) so a host can read warm-start
1189/// diagnostics without depending on any `pub(crate)` type.
1190#[cfg(feature = "std")]
1191#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1192#[non_exhaustive]
1193pub struct TgsCacheStats {
1194    /// Number of contacts whose previous-tick impulse was found in the
1195    /// cache (a successful warm start).
1196    pub hits: u64,
1197    /// Number of contacts not found in the cache (new contact, or one
1198    /// whose entry was evicted by [`PhysicsWorld::step`]'s per-tick
1199    /// internal `ImpulseCache::sweep` call).
1200    pub misses: u64,
1201}
1202
1203#[cfg(feature = "std")]
1204impl TgsCacheStats {
1205    /// Constructs from raw hit/miss counts. Prefer
1206    /// [`PhysicsWorld::tgs_cache_stats`] in production; this exists so
1207    /// test/bench code can construct a specific ratio directly —
1208    /// `#[non_exhaustive]` blocks the struct-literal syntax outside this
1209    /// crate.
1210    #[must_use]
1211    pub fn new(hits: u64, misses: u64) -> Self {
1212        Self { hits, misses }
1213    }
1214
1215    /// Ratio in `[0.0, 1.0]`. Returns `0.0` when `hits + misses == 0`
1216    /// (no lookups performed yet). Delegates to the internal
1217    /// `ImpulseCacheStats::hit_rate` (the same `hits / (hits + misses)`
1218    /// formula) rather than recomputing it here, so this public wrapper
1219    /// has a real dependency on the internal type instead of merely
1220    /// duplicating its arithmetic.
1221    #[must_use]
1222    pub fn hit_rate(&self) -> f64 {
1223        crate::solver_tgs::ImpulseCacheStats {
1224            hits: self.hits,
1225            misses: self.misses,
1226        }
1227        .hit_rate()
1228    }
1229}
1230
1231#[cfg(feature = "std")]
1232impl From<crate::solver_tgs::ImpulseCacheStats> for TgsCacheStats {
1233    fn from(s: crate::solver_tgs::ImpulseCacheStats) -> Self {
1234        Self {
1235            hits: s.hits,
1236            misses: s.misses,
1237        }
1238    }
1239}
1240
1241/// XPBD physics solver configuration
1242#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1243pub struct SolverConfig {
1244    /// Number of substeps per frame
1245    pub substeps: usize,
1246    /// Number of constraint solver passes per substep (default 1 since 1.2.0,
1247    /// Small Steps: prefer more `substeps` over more `iterations`)
1248    pub iterations: usize,
1249    /// Gravity vector
1250    pub gravity: Vec3Fix,
1251    /// Global velocity-retention factor, applied **once per frame** (`step()`),
1252    /// independent of `substeps` (1.0 = no damping). Before 1.2.0 it was applied
1253    /// per substep, which made the result depend on `substeps`.
1254    pub damping: Fix128,
1255    /// **No effect since 1.2.0** (kept for struct compatibility).
1256    ///
1257    /// Distance constraints follow standard XPBD (`lambda = 0` at substep
1258    /// start, accumulated over iterations) and contacts are re-detected every
1259    /// substep and accumulate their multiplier within it, so there is no
1260    /// previous-substep multiplier left to warm-start from. Before 1.2.0 the
1261    /// factor biased both solvers and was the source of the iteration-dependent
1262    /// stiffness (distance) and the energy-creating re-push (contact).
1263    /// 0.8〜0.95 が安定的。デフォルト 0.85。
1264    pub warm_start_factor: Fix128,
1265    /// Which integrator [`PhysicsWorld::step`] dispatches to. Default
1266    /// [`SolverBackend::Xpbd`] — selecting it is structurally a no-op (the
1267    /// TGS dispatch branch is behind a runtime `matches!` check that is
1268    /// never taken), so every existing caller that does not set this field
1269    /// keeps today's `step` behavior bit-for-bit. See [`SolverBackend`] for
1270    /// the `Tgs` path's documented gaps.
1271    pub solver_backend: SolverBackend,
1272}
1273
1274/// Physics configuration (alias for `SolverConfig`)
1275pub type PhysicsConfig = SolverConfig;
1276
1277impl Default for SolverConfig {
1278    fn default() -> Self {
1279        Self {
1280            substeps: 8,
1281            // 1.2.0: Small Steps (Müller et al. 2020) — with collision detection
1282            // and constraint re-evaluation per substep, one Gauss-Seidel pass per
1283            // substep beats several; the previous 8 × 4 = 32 passes cost 4× for
1284            // no accuracy gain. Raise `iterations` only for stiff *rigid*
1285            // constraint chains that must converge within a single substep.
1286            iterations: 1,
1287            gravity: Vec3Fix::new(
1288                Fix128::ZERO,
1289                Fix128::from_int(-10), // -10 m/s^2
1290                Fix128::ZERO,
1291            ),
1292            damping: Fix128::from_ratio(99, 100), // 0.99 velocity retention
1293            warm_start_factor: Fix128::from_ratio(85, 100), // 0.85
1294            solver_backend: SolverBackend::Xpbd,
1295        }
1296    }
1297}
1298
1299// ============================================================================
1300// Constraint Batching (Graph Coloring)
1301// ============================================================================
1302
1303/// Constraint batch for parallel processing
1304///
1305/// A batch contains constraint indices that share no bodies,
1306/// allowing safe parallel modification.
1307#[derive(Clone, Debug, Default)]
1308pub struct ConstraintBatch {
1309    /// Indices into `distance_constraints`
1310    pub distance_indices: Vec<usize>,
1311    /// Indices into `contact_constraints`
1312    pub contact_indices: Vec<usize>,
1313}
1314
1315/// Pointer wrapper enabling parallel access to disjoint body slots.
1316///
1317/// # Safety
1318///
1319/// The graph coloring invariant (`rebuild_batches`) guarantees that within
1320/// a single constraint batch, no two constraints share a *dynamic* body
1321/// index. Static / kinematic bodies (`inv_mass == 0`) are excluded from
1322/// coloring and are only ever handed out as shared references
1323/// (`BodyRef::Static`), so many threads may read them concurrently while
1324/// no thread writes them.
1325#[cfg(feature = "parallel")]
1326struct BodySlicePtr {
1327    ptr: *mut RigidBody,
1328    len: usize,
1329}
1330
1331// SAFETY: `BodySlicePtr` is only used inside the parallel constraint solver
1332// where graph-coloring guarantees that no two threads take `&mut` to the
1333// same index. Static bodies are shared read-only, dynamic bodies appear in
1334// at most one constraint per batch, so all accesses are data-race-free.
1335#[cfg(feature = "parallel")]
1336unsafe impl Send for BodySlicePtr {}
1337#[cfg(feature = "parallel")]
1338unsafe impl Sync for BodySlicePtr {}
1339
1340#[cfg(feature = "parallel")]
1341impl BodySlicePtr {
1342    #[allow(clippy::mut_from_ref)]
1343    #[inline(always)]
1344    /// # Safety
1345    ///
1346    /// Caller must ensure `idx < self.len` and that no other thread
1347    /// concurrently accesses the same index (shared or exclusive).
1348    unsafe fn get_mut(&self, idx: usize) -> &mut RigidBody {
1349        debug_assert!(idx < self.len);
1350        &mut *self.ptr.add(idx)
1351    }
1352
1353    #[inline(always)]
1354    /// # Safety
1355    ///
1356    /// Caller must ensure `idx < self.len` and that no other thread
1357    /// concurrently holds a `&mut` to the same index.
1358    unsafe fn get(&self, idx: usize) -> &RigidBody {
1359        debug_assert!(idx < self.len);
1360        &*self.ptr.add(idx)
1361    }
1362
1363    #[inline(always)]
1364    /// Borrow slot `idx` as shared (static body) or exclusive (dynamic body).
1365    ///
1366    /// # Safety
1367    ///
1368    /// `is_static` must be the value recorded by `rebuild_batches` for this
1369    /// index (see `PhysicsWorld::batch_static_bodies`); passing `false` for a
1370    /// body that another thread also borrows is undefined behaviour.
1371    unsafe fn borrow(&self, idx: usize, is_static: bool) -> BodyRef<'_> {
1372        if is_static {
1373            BodyRef::Static(self.get(idx))
1374        } else {
1375            BodyRef::Dynamic(self.get_mut(idx))
1376        }
1377    }
1378}
1379
1380/// Static friction of a contact at the position level (Müller, Macklin,
1381/// Chentanez, Jeschke, Kim, "Detailed Rigid Body Simulation with Extended
1382/// Position Based Dynamics", SCA 2020, eq. (26)).
1383///
1384/// `disp_a` / `disp_b` are the displacements of the two bodies since the
1385/// start of the substep (`position − prev_position`), `lambda` the contact's
1386/// accumulated normal separation this substep (`cached_lambda`) and `mu` its
1387/// friction coefficient. The relative tangential displacement
1388/// `Δp_t = (Δx_a − Δx_b)` minus its normal part is cancelled when
1389/// `|Δp_t| < μ λ` (both are distances: the paper compares the multipliers
1390/// `λ_t < μ_s λ_n`, which share the factor `1 / (w_a + w_b)`), split by
1391/// inverse mass like the normal correction. Returns the corrections to
1392/// subtract from `a` and add to `b`, or `None` when the contact slides (or
1393/// has no tangential motion / no normal multiplier). Contact points are
1394/// taken at the body centres: the contact solve is translational only.
1395///
1396/// `mu` is the contact's friction (`ContactConstraint::friction`, after the
1397/// contact modifiers); the combined material carries one coefficient, so the
1398/// static and the kinetic coefficients are equal.
1399// LIMITATION(COV-RIGID-080): the combined material carries one coefficient, so the static and the kinetic coefficients are equal.
1400// LIMITATION(COV-RIGID-083): Contact points are taken at the body centres: the contact solve is translational only.
1401#[allow(clippy::too_many_arguments)]
1402#[inline]
1403fn static_friction_correction(
1404    disp_a: Vec3Fix,
1405    disp_b: Vec3Fix,
1406    normal: Vec3Fix,
1407    inv_mass_a: Fix128,
1408    inv_mass_b: Fix128,
1409    inv_w_sum: Fix128,
1410    mu: Fix128,
1411    lambda: Fix128,
1412) -> Option<(Vec3Fix, Vec3Fix)> {
1413    let limit = mu * lambda;
1414    if limit <= Fix128::ZERO {
1415        return None;
1416    }
1417    let dp = disp_a - disp_b;
1418    let dp_t = dp - normal * dp.dot(normal);
1419    let len_sq = dp_t.length_squared();
1420    if len_sq.is_zero() || len_sq >= limit * limit {
1421        return None;
1422    }
1423    Some((
1424        dp_t * (inv_mass_a * inv_w_sum),
1425        dp_t * (inv_mass_b * inv_w_sum),
1426    ))
1427}
1428
1429/// Body access handed to the parallel pair solvers.
1430///
1431/// Dynamic bodies are borrowed exclusively and receive position writes;
1432/// static / kinematic bodies are borrowed shared and position writes are
1433/// dropped (they would be no-ops anyway: `inv_mass == 0` makes every
1434/// correction zero and the sequential path writes the unchanged position).
1435#[cfg(feature = "parallel")]
1436enum BodyRef<'a> {
1437    Dynamic(&'a mut RigidBody),
1438    Static(&'a RigidBody),
1439}
1440
1441#[cfg(feature = "parallel")]
1442impl BodyRef<'_> {
1443    #[inline(always)]
1444    fn get(&self) -> &RigidBody {
1445        match self {
1446            BodyRef::Dynamic(b) => b,
1447            BodyRef::Static(b) => b,
1448        }
1449    }
1450
1451    #[inline(always)]
1452    fn set_position(&mut self, position: Vec3Fix) {
1453        if let BodyRef::Dynamic(b) = self {
1454            b.position = position;
1455        }
1456    }
1457}
1458
1459/// Pointer wrapper enabling parallel write-back of cached lambda.
1460///
1461/// Each constraint index appears in exactly one batch, so concurrent
1462/// mutation of distinct constraint slots is safe.
1463#[cfg(feature = "parallel")]
1464struct DistConstraintSlicePtr {
1465    ptr: *mut DistanceConstraint,
1466    len: usize,
1467}
1468
1469// SAFETY: `DistConstraintSlicePtr` is only used inside the parallel
1470// constraint solver where each constraint index appears in exactly one
1471// graph-colored batch. Concurrent `get_mut` calls target disjoint indices,
1472// preventing data races.
1473#[cfg(feature = "parallel")]
1474unsafe impl Send for DistConstraintSlicePtr {}
1475#[cfg(feature = "parallel")]
1476unsafe impl Sync for DistConstraintSlicePtr {}
1477
1478#[cfg(feature = "parallel")]
1479impl DistConstraintSlicePtr {
1480    #[allow(clippy::mut_from_ref)]
1481    #[inline(always)]
1482    /// # Safety
1483    ///
1484    /// Caller must ensure `idx < self.len` and that no other thread
1485    /// concurrently accesses the same index.
1486    unsafe fn get_mut(&self, idx: usize) -> &mut DistanceConstraint {
1487        debug_assert!(idx < self.len);
1488        &mut *self.ptr.add(idx)
1489    }
1490}
1491
1492/// Pointer wrapper enabling parallel write-back of cached lambda for contact constraints.
1493#[cfg(feature = "parallel")]
1494struct ContactConstraintSlicePtr {
1495    ptr: *mut ContactConstraint,
1496    len: usize,
1497}
1498
1499// SAFETY: Same guarantee as `DistConstraintSlicePtr` — each constraint index
1500// appears in exactly one graph-colored batch.
1501#[cfg(feature = "parallel")]
1502unsafe impl Send for ContactConstraintSlicePtr {}
1503#[cfg(feature = "parallel")]
1504unsafe impl Sync for ContactConstraintSlicePtr {}
1505
1506#[cfg(feature = "parallel")]
1507impl ContactConstraintSlicePtr {
1508    #[allow(clippy::mut_from_ref)]
1509    #[inline(always)]
1510    /// # Safety
1511    ///
1512    /// Caller must ensure `idx < self.len` and that no other thread
1513    /// concurrently accesses the same index.
1514    unsafe fn get_mut(&self, idx: usize) -> &mut ContactConstraint {
1515        debug_assert!(idx < self.len);
1516        &mut *self.ptr.add(idx)
1517    }
1518}
1519
1520/// Pre-solve contact hook callback type
1521///
1522/// Called before each contact is solved. Return `false` to skip this contact.
1523/// This allows game logic to filter contacts (e.g., one-way platforms,
1524/// character controllers that ignore certain collisions).
1525#[cfg(feature = "std")]
1526pub type PreSolveHook = Box<dyn Fn(usize, usize, &Contact) -> bool + Send + Sync>;
1527
1528/// Contact modification callback trait
1529///
1530/// Implement to modify contact properties before solving.
1531/// More powerful than `PreSolveHook`: can mutate normal, depth,
1532/// friction, and restitution per-contact.
1533#[cfg(feature = "std")]
1534pub trait ContactModifier: Send + Sync {
1535    /// Modify a contact before solving.
1536    ///
1537    /// Return `false` to discard the contact entirely.
1538    /// Modify the mutable references to change contact properties.
1539    fn modify_contact(
1540        &self,
1541        body_a: usize,
1542        body_b: usize,
1543        contact: &mut Contact,
1544        friction: &mut Fix128,
1545        restitution: &mut Fix128,
1546    ) -> bool;
1547}
1548
1549/// Typed, read-only observation of a single body's law-relevant state.
1550///
1551/// Public入口 for [`PhysicsWorld::observe_body`] / [`PhysicsWorld::observe_bodies`]
1552/// — 「Law が読む形の観測型」 observation 以前は blob の
1553/// parse か field 直読みしかなく、公開 interface が無かった
1554#[derive(Clone, Copy, Debug, PartialEq)]
1555pub struct BodyObservation {
1556    /// Body index this observation was taken from.
1557    ///
1558    /// ⚠️ **フレーム内でのみ安定** — [`PhysicsWorld::remove_body`] は
1559    /// `swap_remove` なので、body の生成・破棄を跨いで同じ index が同じ
1560    /// body を指す保証はない
1561    pub body_index: usize,
1562    /// Position (center of mass).
1563    pub position: Vec3Fix,
1564    /// Linear velocity.
1565    pub velocity: Vec3Fix,
1566    /// Orientation.
1567    pub rotation: QuatFix,
1568    /// Angular velocity.
1569    pub angular_velocity: Vec3Fix,
1570    /// Whether the body is currently asleep (island manager).
1571    pub sleeping: bool,
1572    /// Whether the body has at least one active contact this frame
1573    /// (`Begin` or `Persist`, from [`crate::event::EventCollector`]).
1574    ///
1575    /// ⚠️ **`grounded` ではない** — 一般 body に地面接触の定義は無いため、
1576    /// 「何らかの body と接触している」という弱い述語に留める
1577    pub in_contact: bool,
1578}
1579
1580/// The per-body settings of a [`PhysicsWorld`] kept outside [`RigidBody`],
1581/// moved together with a body between worlds (see `MultiWorld::transfer_body`).
1582#[derive(Clone, Debug)]
1583pub(crate) struct BodyAttachments {
1584    material: crate::material::MaterialId,
1585    collision_radius: Option<Fix128>,
1586    collider: Option<crate::body_collider::BodyCollider>,
1587    filter: CollisionFilter,
1588}
1589
1590/// XPBD physics world with batched constraint solving
1591///
1592/// `PhysicsWorld` integrates all physics subsystems:
1593/// - Rigid body dynamics with XPBD solver
1594/// - Automatic collision detection (BVH broad-phase + sphere narrow-phase)
1595/// - Joint constraints (Ball, Hinge, Fixed, Slider, Spring, D6, `ConeTwist`)
1596/// - Force fields (wind, gravity wells, buoyancy, vortex, drag)
1597/// - Collision filtering (layer/mask bitmasks)
1598/// - Contact events (begin/persist/end)
1599/// - Sleeping/island management
1600pub struct PhysicsWorld {
1601    /// Solver configuration
1602    pub config: SolverConfig,
1603    /// All rigid bodies in the world
1604    pub bodies: Vec<RigidBody>,
1605    /// Distance constraints between bodies
1606    pub distance_constraints: Vec<DistanceConstraint>,
1607    /// Contact constraints from collision detection
1608    pub contact_constraints: Vec<ContactConstraint>,
1609    /// SDF colliders for implicit collision detection
1610    pub sdf_colliders: Vec<SdfCollider>,
1611    /// Default collision radius for body-vs-SDF queries
1612    pub sdf_collision_radius: Fix128,
1613    /// Planes, height fields and triangle meshes that bodies rest on (see
1614    /// [`crate::static_collider`])
1615    static_colliders: Vec<crate::static_collider::StaticCollider>,
1616    /// Pre-colored constraint batches (computed on demand)
1617    constraint_batches: Vec<ConstraintBatch>,
1618    /// Whether batches need recomputation
1619    batches_dirty: bool,
1620    /// Per-body snapshot of `inv_mass == 0` taken by `rebuild_batches`.
1621    ///
1622    /// Bodies flagged here were excluded from graph coloring (they are never
1623    /// written by the pair solvers), so the parallel path must only ever take
1624    /// shared references to them. `solve_constraints_batched` re-validates
1625    /// this snapshot against the live bodies before dispatch and rebuilds the
1626    /// batches if any body changed static-ness in between.
1627    batch_static_bodies: Vec<bool>,
1628    /// Contact manifold cache for warm starting
1629    pub contact_cache: crate::contact_cache::ContactCache,
1630    /// Material pair lookup table
1631    pub material_table: crate::material::MaterialTable,
1632    /// Per-body material IDs
1633    pub body_materials: Vec<crate::material::MaterialId>,
1634    /// Pre-solve contact hooks (called before contact resolution)
1635    #[cfg(feature = "std")]
1636    pre_solve_hooks: Vec<PreSolveHook>,
1637    /// Contact modifiers (called before contact resolution, can mutate contact)
1638    #[cfg(feature = "std")]
1639    contact_modifiers: Vec<Box<dyn ContactModifier>>,
1640    /// Constraints the hooks / modifiers discarded in the last
1641    /// [`Self::apply_contact_filters`] pre-pass (indexed like
1642    /// `contact_constraints`; an index past the end counts as kept)
1643    #[cfg(feature = "std")]
1644    contact_discarded: Vec<bool>,
1645    /// Pre-solve normal velocity `v̄_n` of each contact constraint in the
1646    /// current substep (indexed like `contact_constraints`), recorded by
1647    /// [`Self::update_velocities`] before it derives the post-solve
1648    /// velocities; the restitution target is `−e v̄_n`
1649    contact_pre_vn: Vec<Fix128>,
1650    /// Free rotation of each body in the current substep, written by
1651    /// `integrate_positions` and read by `update_velocities` (indexed like
1652    /// `bodies`; `None`: no gyroscopic response). Transient: rebuilt every
1653    /// substep, so it carries no state across steps.
1654    free_rotation: Vec<Option<crate::gyroscopic::FreeRotation>>,
1655    /// Predicted pose (position, rotation) of each body in the current
1656    /// substep, written by `integrate_positions` after `x += v·h` and the
1657    /// rotation prediction, and read by `update_velocities` (indexed like
1658    /// `bodies`; `None`: the body was not predicted this substep — static,
1659    /// kinematic, sleeping or parked — and derives its velocities from
1660    /// `prev_position` / `prev_rotation`). Transient: rebuilt every substep,
1661    /// so it carries no state across steps.
1662    predicted_pose: Vec<Option<(Vec3Fix, QuatFix)>>,
1663    /// v0.11.0: installed GPU solver bridge for automatic contact-solve
1664    /// routing. When `Some`, every call to [`Self::step`] /
1665    /// [`Self::substep`] transparently routes contact-solve through the
1666    /// bridge instead of running the CPU solver inline. Installed and
1667    /// removed via [`Self::set_gpu_solver_bridge`] and
1668    /// [`Self::take_gpu_solver_bridge`]. Callers who want explicit
1669    /// control can bypass the field entirely by using
1670    /// [`Self::step_with_bridge`] / [`Self::substep_with_bridge`] /
1671    /// [`Self::solve_contact_constraints_with_bridge`] directly — those
1672    /// helpers ignore the installed bridge and use the borrowed one
1673    /// passed in.
1674    #[cfg(feature = "gpu-solver-bridge")]
1675    gpu_solver_bridge: Option<Box<dyn crate::gpu_bridge::GpuSolverBridge + Send + Sync>>,
1676
1677    // ── Integrated Subsystems ──────────────────────────────────────────
1678    /// Joint constraints (solved each substep alongside distance/contact)
1679    pub joints: Vec<Joint>,
1680    /// Joint motors (one-axis PD controllers), applied every substep by
1681    /// [`Self::step`]; see [`Self::add_joint_motor`]
1682    joint_motors: Vec<crate::motor::JointMotor>,
1683    /// Three-axis rotation motors `(joint index, controller)`, applied every
1684    /// substep by [`Self::step`]; see [`Self::add_joint_motor_3d`]
1685    joint_motors_3d: Vec<(usize, crate::motor::PdController3D)>,
1686    /// Force fields applied at the start of each step
1687    pub force_fields: Vec<ForceFieldInstance>,
1688    /// Contact and trigger event collector
1689    pub events: EventCollector,
1690    /// Island manager for sleeping and connectivity tracking
1691    pub islands: IslandManager,
1692    /// Per-body collision radius for automatic sphere-based collision detection.
1693    /// `None` means the body does not participate in auto-detection.
1694    body_collision_radii: Vec<Option<Fix128>>,
1695    /// Which broad-phase finds the candidate pairs.
1696    broadphase: Broadphase,
1697    /// The persistent tree of [`Broadphase::DynamicTree`] (unused otherwise).
1698    broadphase_tree: crate::dynamic_bvh::DynamicAabbTree,
1699    /// Body index → its proxy in `broadphase_tree`.
1700    broadphase_proxies: Vec<Option<u32>>,
1701    /// The layers of [`Broadphase::Hybrid`] (unused otherwise).
1702    broadphase_hybrid: BroadphaseHybrid,
1703    /// The convex collider a body carries, when it has one — one shape or a
1704    /// compound of them: the narrow-phase works on it instead of the bounding
1705    /// sphere (see [`crate::shape`], [`crate::compound`]).
1706    body_colliders: Vec<Option<crate::body_collider::BodyCollider>>,
1707    /// Per-body collision filter (layer/mask/group)
1708    body_filters: Vec<CollisionFilter>,
1709    /// 範囲外の積を踏んだか (sticky、[`PhysicsWorld::overflow_detected`])
1710    ///
1711    /// ⚠️ **`Fix128` の `Mul` 自体からは world に到達できない** (純粋な trait
1712    /// impl、crate は `no_std` 対応なので `thread_local` も使えない) ので、
1713    /// **発散が起きる経路** (積分と速度導出) で `checked_mul` を呼んでここに
1714    /// 立てる doctrine B-12 の「`mul` の中で world の flag を立てる」は
1715    /// 到達不能なため、WM-01 の結論「`mul` の積が範囲外を演算側で見る」を
1716    /// 呼び出し側で実現した形
1717    overflow_detected: bool,
1718    /// Substeps still to run in the current frame, counting the one being run
1719    /// (0 outside a substep loop). A kinematic body with a target closes
1720    /// `1 / kinematic_substeps_left` of its remaining gap in each substep, so
1721    /// the velocity derived after every substep is the frame velocity
1722    /// (see [`kinematic_substep_pose`]).
1723    kinematic_substeps_left: usize,
1724    /// Warm-start impulse cache for the [`SolverBackend::Tgs`] path, persisted
1725    /// across frames the same way [`Self::contact_cache`] is for XPBD.
1726    /// Unused (and empty) while `config.solver_backend` is `Xpbd`.
1727    #[cfg(feature = "std")]
1728    tgs_impulse_cache: crate::solver_tgs::ImpulseCache,
1729    /// Per-body identity that survives `remove_body`'s `swap_remove` (the
1730    /// index does not). The TGS warm-start cache keys contacts and distance
1731    /// constraints by these, so a reorder of `bodies` cannot hand one
1732    /// contact's cached impulse to another. Kept parallel to `bodies` by
1733    /// [`Self::sync_body_stable_ids`], which also covers bodies pushed onto
1734    /// the public `bodies` field directly.
1735    #[cfg(feature = "std")]
1736    body_stable_ids: Vec<u64>,
1737    /// The next id [`Self::sync_body_stable_ids`] hands out.
1738    #[cfg(feature = "std")]
1739    next_body_stable_id: u64,
1740    /// Skip parked sleeping bodies in `step` (see [`StageWork`]).
1741    sleep_skip: bool,
1742    /// Parked bodies, their tree and caches.
1743    park: ParkState,
1744    /// Bumped by every change that can invalidate a parked body's verdict but is
1745    /// not visible in its own state (shapes, radii, static colliders, removal).
1746    park_generation: u64,
1747    /// Work counters of the last `step`.
1748    stage_work: StageWork,
1749    /// SDF contacts resolved by the current (or last) step, in the order they
1750    /// were applied ([`Self::last_step_sdf_contacts`]). Cleared at the head of
1751    /// each step; the allocation is kept from step to step.
1752    sdf_contact_log: Vec<crate::sdf_collider::SdfContact>,
1753    /// Substep index the SDF contacts resolved now are recorded under.
1754    sdf_contact_substep: usize,
1755    /// The [`SdfField::generation`](crate::sdf_collider::SdfField::generation)
1756    /// of each SDF collider as last seen (at the previous step head, or when
1757    /// the collider was added); a change wakes the sleeping bodies.
1758    sdf_generations: Vec<u64>,
1759    /// Per-body buffers the parallel SDF resolution writes into before they
1760    /// are appended to `sdf_contact_log` in body order (kept from step to
1761    /// step).
1762    #[cfg(feature = "parallel")]
1763    sdf_contact_scratch: Vec<Vec<crate::sdf_collider::SdfContact>>,
1764    /// Participants in registration order (see [`crate::world_participant`]).
1765    /// Held behind a mutex so that the world stays `Sync` while a participant
1766    /// is only `Send`; every access during a step goes through `&mut self`
1767    /// ([`std::sync::Mutex::get_mut`], no locking).
1768    #[cfg(feature = "std")]
1769    participants: std::sync::Mutex<Vec<Box<dyn crate::world_participant::Participant>>>,
1770    /// Run order and declared ports of `participants`, rebuilt whenever a
1771    /// participant or a field is added; `None` while no participant is
1772    /// registered.
1773    #[cfg(feature = "std")]
1774    participant_plan: Option<crate::world_participant::ParticipantPlan>,
1775    /// The first fault recorded (sticky until [`Self::clear_fault`]).
1776    fault: Option<crate::world_participant::WorldFault>,
1777    /// Shared fields, owned by the world.
1778    fields: crate::world_participant::FieldBoard,
1779    /// Continuous collision of the step ([`Self::set_continuous_collision`]).
1780    ccd: WorldCcdConfig,
1781}
1782
1783/// Fold `bytes` into `hash` with FNV-1a (64-bit).
1784///
1785/// Used by [`PhysicsWorld::population_fingerprint`]; kept as a free
1786/// function (not a closure) to avoid a double-borrow of `hash`.
1787fn fnv1a_fold(hash: &mut u64, bytes: &[u8]) {
1788    for &b in bytes {
1789        *hash ^= u64::from(b);
1790        *hash = hash.wrapping_mul(0x0000_0001_0000_01b3);
1791    }
1792}
1793
1794/// The contact of one body with one SDF collider: as its shape or compound when it
1795/// has one, otherwise as a sphere of `radius` at its position.
1796#[cfg(feature = "std")]
1797fn sdf_contact_of(
1798    collider: Option<&crate::body_collider::BodyCollider>,
1799    body: &RigidBody,
1800    radius: Fix128,
1801    sdf: &crate::sdf_collider::SdfCollider,
1802) -> Option<crate::collider::Contact> {
1803    match collider {
1804        Some(c) => c.sdf_contact(body.position, body.rotation, sdf),
1805        None => crate::sdf_collider::collide_sphere_sdf(body.position, radius, sdf),
1806    }
1807}
1808
1809/// Pose of a kinematic body after one substep towards its target.
1810///
1811/// `left` is the number of substeps left in the frame including the current
1812/// one. Each substep closes `1 / left` of the remaining gap, which makes the
1813/// per-substep displacement the same in every substep (the gap shrinks by
1814/// `(left - 1) / left` each time), and the last substep (`left <= 1`, also the
1815/// value outside a substep loop) lands on the target exactly.
1816fn kinematic_substep_pose(
1817    position: Vec3Fix,
1818    rotation: QuatFix,
1819    target_pos: Vec3Fix,
1820    target_rot: QuatFix,
1821    left: usize,
1822) -> (Vec3Fix, QuatFix) {
1823    if left <= 1 {
1824        return (target_pos, target_rot);
1825    }
1826    let frac = Fix128::ONE / Fix128::from_int(left as i64);
1827    (
1828        position + (target_pos - position) * frac,
1829        crate::interpolation::slerp(rotation, target_rot, frac),
1830    )
1831}
1832
1833impl PhysicsWorld {
1834    /// Create a new, empty physics world with the given solver configuration.
1835    ///
1836    /// # Examples
1837    ///
1838    /// ```
1839    /// use alice_physics::{Fix128, PhysicsConfig, PhysicsWorld, RigidBody, Vec3Fix};
1840    ///
1841    /// let config = PhysicsConfig::default();
1842    /// let mut world = PhysicsWorld::new(config);
1843    ///
1844    /// // World starts with no bodies
1845    /// assert_eq!(world.bodies.len(), 0);
1846    ///
1847    /// // Add a body and verify it is tracked
1848    /// let body = RigidBody::new_dynamic(Vec3Fix::from_int(0, 5, 0), Fix128::ONE);
1849    /// let id = world.add_body(body);
1850    /// assert_eq!(id, 0);
1851    /// assert_eq!(world.bodies.len(), 1);
1852    /// ```
1853    #[must_use]
1854    pub fn new(config: SolverConfig) -> Self {
1855        Self {
1856            config,
1857            bodies: Vec::new(),
1858            distance_constraints: Vec::new(),
1859            contact_constraints: Vec::new(),
1860            sdf_colliders: Vec::new(),
1861            sdf_collision_radius: Fix128::from_ratio(1, 2), // 0.5 default
1862            static_colliders: Vec::new(),
1863            constraint_batches: Vec::new(),
1864            batches_dirty: true,
1865            batch_static_bodies: Vec::new(),
1866            contact_cache: crate::contact_cache::ContactCache::new(),
1867            material_table: crate::material::MaterialTable::new(),
1868            body_materials: Vec::new(),
1869            #[cfg(feature = "std")]
1870            pre_solve_hooks: Vec::new(),
1871            #[cfg(feature = "std")]
1872            contact_modifiers: Vec::new(),
1873            #[cfg(feature = "std")]
1874            contact_discarded: Vec::new(),
1875            contact_pre_vn: Vec::new(),
1876            free_rotation: Vec::new(),
1877            predicted_pose: Vec::new(),
1878            #[cfg(feature = "gpu-solver-bridge")]
1879            gpu_solver_bridge: None,
1880            joints: Vec::new(),
1881            joint_motors: Vec::new(),
1882            joint_motors_3d: Vec::new(),
1883            force_fields: Vec::new(),
1884            events: EventCollector::new(),
1885            islands: IslandManager::new(0, SleepConfig::default()),
1886            body_collision_radii: Vec::new(),
1887            broadphase: Broadphase::default(),
1888            broadphase_tree: crate::dynamic_bvh::DynamicAabbTree::new(),
1889            broadphase_proxies: Vec::new(),
1890            broadphase_hybrid: BroadphaseHybrid::new(),
1891            body_colliders: Vec::new(),
1892            body_filters: Vec::new(),
1893            overflow_detected: false,
1894            kinematic_substeps_left: 0,
1895            #[cfg(feature = "std")]
1896            tgs_impulse_cache: crate::solver_tgs::ImpulseCache::new(),
1897            #[cfg(feature = "std")]
1898            body_stable_ids: Vec::new(),
1899            #[cfg(feature = "std")]
1900            next_body_stable_id: 0,
1901            sleep_skip: true,
1902            park: ParkState::default(),
1903            park_generation: 0,
1904            stage_work: StageWork::default(),
1905            sdf_contact_log: Vec::new(),
1906            sdf_contact_substep: 0,
1907            sdf_generations: Vec::new(),
1908            #[cfg(feature = "parallel")]
1909            sdf_contact_scratch: Vec::new(),
1910            #[cfg(feature = "std")]
1911            participants: std::sync::Mutex::new(Vec::new()),
1912            #[cfg(feature = "std")]
1913            participant_plan: None,
1914            fault: None,
1915            fields: crate::world_participant::FieldBoard::new(),
1916            ccd: WorldCcdConfig::new(),
1917        }
1918    }
1919
1920    /// Reset the world to the same empty state [`Self::new`] produces,
1921    /// keeping the current [`SolverConfig`].
1922    ///
1923    /// # World Auditor の `reset()` 契約 (WM-07)
1924    ///
1925    /// 「同じ初期状態から N step を 2 回実行すると bit 一致する」を成立させる
1926    /// ための入口 `*self = Self::new(self.config)` と等価 (`config` は
1927    /// `Copy`) — `bodies` / `joints` / `force_fields` / `islands` /
1928    /// `overflow_detected` を含む **全 field が `new` と同じ既定値**に戻る
1929    /// (installed hook / GPU bridge も含む、呼び出し側が明示的に残したい
1930    /// 状態があれば `reset_world` の前に退避すること)
1931    ///
1932    /// [`Self::deserialize_state`] による rollback とは異なる経路 — rollback
1933    /// は「population を caller が replay で合わせてから特定 frame の連続状態を
1934    /// 復元する」契約だが、`reset_world` は「population ごと空にする」契約
1935    /// 両者は **「population を目標状態まで合わせてから連続状態を復元する」**
1936    /// という同じ形の特殊例 (`reset_world` は目標 population が空の場合)
1937    ///
1938    /// # Examples
1939    ///
1940    /// ```
1941    /// use alice_physics::{Fix128, PhysicsConfig, PhysicsWorld, RigidBody, Vec3Fix};
1942    ///
1943    /// let config = PhysicsConfig::default();
1944    /// let mut world = PhysicsWorld::new(config);
1945    /// world.add_body(RigidBody::new_dynamic(Vec3Fix::from_int(0, 5, 0), Fix128::ONE));
1946    /// assert_eq!(world.bodies.len(), 1);
1947    ///
1948    /// world.reset_world();
1949    /// assert_eq!(world.bodies.len(), 0);
1950    /// assert!(!world.overflow_detected());
1951    /// ```
1952    pub fn reset_world(&mut self) {
1953        *self = Self::new(self.config);
1954    }
1955
1956    /// v0.11.0: install a GPU solver bridge for automatic contact-solve
1957    /// routing. Subsequent calls to [`Self::step`] / the substep loop
1958    /// route contact-solve through this bridge instead of the CPU
1959    /// solver. Pass `Some(bridge)` to install; use
1960    /// [`Self::take_gpu_solver_bridge`] to remove.
1961    ///
1962    /// The bridge is stored as `Box<dyn GpuSolverBridge + Send + Sync>`,
1963    /// so implementers must satisfy `Send + Sync`. For ALICE-TRT v3.0.0+
1964    /// this is guaranteed because `TrtSolverAdapter` uses
1965    /// `Arc<GpuDevice>` and holds only `Send + Sync` fields.
1966    ///
1967    /// Callers who want explicit control without installing the bridge
1968    /// on the world (game-engine wrapper hosts, hot-swap harnesses)
1969    /// can continue to use [`Self::step_with_bridge`] /
1970    /// [`Self::substep_with_bridge`] /
1971    /// [`Self::solve_contact_constraints_with_bridge`] with an
1972    /// externally-owned bridge — those methods ignore the installed
1973    /// bridge and take a `&mut B` parameter.
1974    #[cfg(feature = "gpu-solver-bridge")]
1975    pub fn set_gpu_solver_bridge(
1976        &mut self,
1977        bridge: Option<Box<dyn crate::gpu_bridge::GpuSolverBridge + Send + Sync>>,
1978    ) {
1979        self.gpu_solver_bridge = bridge;
1980    }
1981
1982    /// v0.11.0: remove and return the currently installed GPU solver
1983    /// bridge, if any. Subsequent calls to [`Self::step`] /
1984    /// the substep loop revert to the CPU contact solver.
1985    #[cfg(feature = "gpu-solver-bridge")]
1986    pub fn take_gpu_solver_bridge(
1987        &mut self,
1988    ) -> Option<Box<dyn crate::gpu_bridge::GpuSolverBridge + Send + Sync>> {
1989        self.gpu_solver_bridge.take()
1990    }
1991
1992    /// v0.11.0: whether a GPU solver bridge is currently installed on
1993    /// this world. Cheap; does not touch the bridge.
1994    #[cfg(feature = "gpu-solver-bridge")]
1995    #[must_use]
1996    pub const fn gpu_solver_bridge_installed(&self) -> bool {
1997        self.gpu_solver_bridge.is_some()
1998    }
1999
2000    /// Bring `body_stable_ids` to `bodies.len()`: drop ids past the end and
2001    /// give every body without one a fresh id. Bodies keep their id for as
2002    /// long as they live; ids are never reused.
2003    #[cfg(feature = "std")]
2004    fn sync_body_stable_ids(&mut self) {
2005        let n = self.bodies.len();
2006        self.body_stable_ids.truncate(n);
2007        while self.body_stable_ids.len() < n {
2008            self.body_stable_ids.push(self.next_body_stable_id);
2009            self.next_body_stable_id += 1;
2010        }
2011    }
2012
2013    /// Add rigid body, returns index
2014    ///
2015    /// The body's rotations are stored as unit quaternions (see
2016    /// [`RigidBody::set_rotation`]).
2017    pub fn add_body(&mut self, mut body: RigidBody) -> usize {
2018        body.make_rotations_unit();
2019        let idx = self.bodies.len();
2020        self.bodies.push(body);
2021        self.body_materials.push(crate::material::DEFAULT_MATERIAL);
2022        self.body_collision_radii.push(None);
2023        self.body_colliders.push(None);
2024        self.body_filters.push(CollisionFilter::DEFAULT);
2025        self.islands.resize(idx + 1);
2026        #[cfg(feature = "std")]
2027        self.sync_body_stable_ids();
2028        idx
2029    }
2030
2031    /// Add rigid body with a collision sphere radius for automatic detection.
2032    ///
2033    /// Bodies with a collision radius participate in BVH broad-phase and
2034    /// sphere-sphere narrow-phase collision detection during `step()`.
2035    ///
2036    /// The body's rotations are stored as unit quaternions, as in
2037    /// [`Self::add_body`].
2038    pub fn add_body_with_radius(&mut self, mut body: RigidBody, radius: Fix128) -> usize {
2039        body.make_rotations_unit();
2040        let idx = self.bodies.len();
2041        self.bodies.push(body);
2042        self.body_materials.push(crate::material::DEFAULT_MATERIAL);
2043        self.body_collision_radii.push(Some(radius));
2044        self.body_colliders.push(None);
2045        self.body_filters.push(CollisionFilter::DEFAULT);
2046        self.islands.resize(idx + 1);
2047        #[cfg(feature = "std")]
2048        self.sync_body_stable_ids();
2049        idx
2050    }
2051
2052    /// Add a rigid body shaped like `shape`, made of material of `density`, with
2053    /// its centre of mass at `position`.
2054    ///
2055    /// The body gets the shape's mass (`density × volume`), the shape's principal
2056    /// inertia about the centre of mass (not the unit-sphere inertia
2057    /// [`RigidBody::new`] assigns), and the shape's bounding sphere about the
2058    /// centre of mass as its collision radius, so it takes part in the sphere
2059    /// broad- and narrow-phase like any body added with
2060    /// [`Self::add_body_with_radius`]. See [`crate::shape`] for the frames.
2061    ///
2062    /// # Errors
2063    ///
2064    /// [`ShapeError`](crate::shape::ShapeError) when the density is not positive,
2065    /// a dimension is not, or the mass or inertia does not fit `Fix128`. Nothing is
2066    /// added in that case.
2067    pub fn add_shaped_body(
2068        &mut self,
2069        shape: &crate::shape::Shape,
2070        density: Fix128,
2071        position: Vec3Fix,
2072    ) -> Result<usize, crate::shape::ShapeError> {
2073        let (mass, inertia) = shape.mass_and_inertia(density)?;
2074        let mut body = RigidBody::new_dynamic(position, mass);
2075        body.inv_inertia = Vec3Fix::new(
2076            Fix128::ONE / inertia.x,
2077            Fix128::ONE / inertia.y,
2078            Fix128::ONE / inertia.z,
2079        );
2080        let idx = self.add_body_with_radius(body, shape.bounding_radius());
2081        self.body_colliders[idx] = Some(crate::body_collider::BodyCollider::Shape(*shape));
2082        Ok(idx)
2083    }
2084
2085    /// Give an existing body a convex `shape` as its collider: its collision radius
2086    /// becomes the shape's bounding radius about the centre of mass, and a pair of
2087    /// bodies of which either carries a shape is decided by the shape
2088    /// ([`crate::collider::contact`]; against a body without one, by its collision
2089    /// sphere) instead of by the bounding spheres. Returns `false` for an out-of-range index.
2090    ///
2091    /// The body's position is taken as the shape's **centre of mass** (see
2092    /// [`crate::shape`]); its mass and inertia are not changed — use
2093    /// [`Self::add_shaped_body`] for a body whose mass properties come from the shape.
2094    pub fn set_body_shape(&mut self, body_idx: usize, shape: &crate::shape::Shape) -> bool {
2095        if body_idx >= self.bodies.len() || body_idx >= self.body_colliders.len() {
2096            return false;
2097        }
2098        self.park_generation = self.park_generation.wrapping_add(1);
2099        self.body_collision_radii[body_idx] = Some(shape.bounding_radius());
2100        self.body_colliders[body_idx] = Some(crate::body_collider::BodyCollider::Shape(*shape));
2101        true
2102    }
2103
2104    /// Whether the colliders of two bodies overlap, the same way the contact
2105    /// generation decides it: GJK on their shapes when both carry one
2106    /// ([`Self::set_body_shape`], [`Self::add_shaped_body`]), GJK on the shape and
2107    /// the other body's collision sphere when one does, otherwise the test of
2108    /// their collision spheres. `false` for an index that is not a body or a body
2109    /// without a collider. Touching exactly counts as overlapping when a shape is
2110    /// involved (GJK's convention) and as clear for two spheres (the contact
2111    /// generation's).
2112    #[must_use]
2113    pub fn colliders_overlap(&self, a: usize, b: usize) -> bool {
2114        let (Some(body_a), Some(body_b)) = (self.bodies.get(a), self.bodies.get(b)) else {
2115            return false;
2116        };
2117        let collider = |i: usize| self.body_colliders.get(i).and_then(Option::as_ref);
2118        let radius = |i: usize| self.body_collision_radii.get(i).copied().flatten();
2119        let pose_a = (body_a.position, body_a.rotation);
2120        let pose_b = (body_b.position, body_b.rotation);
2121        match (collider(a), collider(b)) {
2122            (Some(ca), Some(cb)) => crate::body_collider::colliders_meet(ca, pose_a, cb, pose_b),
2123            (Some(ca), None) => radius(b).is_some_and(|rb| {
2124                crate::body_collider::collider_meets_sphere(
2125                    ca,
2126                    pose_a,
2127                    crate::collider::Sphere::new(body_b.position, rb),
2128                )
2129            }),
2130            (None, Some(cb)) => radius(a).is_some_and(|ra| {
2131                crate::body_collider::collider_meets_sphere(
2132                    cb,
2133                    pose_b,
2134                    crate::collider::Sphere::new(body_a.position, ra),
2135                )
2136            }),
2137            (None, None) => match (radius(a), radius(b)) {
2138                (Some(ra), Some(rb)) => {
2139                    let reach = ra + rb;
2140                    (body_a.position - body_b.position).length_squared() < reach * reach
2141                }
2142                _ => false,
2143            },
2144        }
2145    }
2146
2147    /// Add a rigid body made of a [`CompoundShape`](crate::compound::CompoundShape):
2148    /// material of `density`, the compound's centre of mass at `position`.
2149    ///
2150    /// The mass is the children's total and the inertia is their summed tensor about
2151    /// the common centre of mass, which for an asymmetric compound has products of
2152    /// inertia; the body stores the **principal** moments, so its local frame is the
2153    /// principal frame, and its initial rotation is the rotation from that frame to
2154    /// the compound's as authored — the compound appears in the world exactly as it
2155    /// was built, translated so its centre of mass is at `position`. The body
2156    /// collides as its children (`body_collider`, internal): a gap between two
2157    /// children is not part of the body, and its collision radius is the sphere about
2158    /// the centre of mass that contains them all.
2159    ///
2160    /// # Errors
2161    ///
2162    /// [`ShapeError`](crate::shape::ShapeError): `NonPositiveDensity`;
2163    /// `DegenerateShape` for an empty compound or one with no volume;
2164    /// `MassNotRepresentable` when the mass or a principal moment is not positive or
2165    /// the sizes are too large for `Fix128`. Nothing is added in those cases.
2166    pub fn add_compound_body(
2167        &mut self,
2168        compound: &crate::compound::CompoundShape,
2169        density: Fix128,
2170        position: Vec3Fix,
2171    ) -> Result<usize, crate::shape::ShapeError> {
2172        use crate::shape::ShapeError;
2173        if density <= Fix128::ZERO {
2174            return Err(ShapeError::NonPositiveDensity);
2175        }
2176        if compound.is_empty() {
2177            return Err(ShapeError::DegenerateShape);
2178        }
2179        let props = compound.mass_properties(density);
2180        if props.mass <= Fix128::ZERO {
2181            return Err(ShapeError::DegenerateShape);
2182        }
2183        let (moments, axes) = crate::mass_properties::principal_axes(props.inertia_tensor);
2184        if !(moments.x > Fix128::ZERO && moments.y > Fix128::ZERO && moments.z > Fix128::ZERO) {
2185            return Err(ShapeError::MassNotRepresentable);
2186        }
2187        // The compound in the principal frame, centre of mass at the origin.
2188        let frame = crate::body_collider::quat_from_rotation(axes);
2189        let to_principal = frame.conjugate();
2190        let mut stored = crate::compound::CompoundShape::new();
2191        for child in &compound.children {
2192            stored.children.push(crate::compound::CompoundChild {
2193                shape: child.shape.clone(),
2194                local_position: to_principal
2195                    .rotate_vec(child.local_position - props.center_of_mass),
2196                local_rotation: to_principal.mul(child.local_rotation),
2197            });
2198        }
2199        let collider = crate::body_collider::BodyCollider::Compound(stored);
2200        let radius = collider.bounding_radius();
2201        // The largest quantity the solve forms, as an `f64`, before it is formed.
2202        let estimate = props.mass.to_f64() * radius.to_f64() * radius.to_f64();
2203        if !(estimate.is_finite() && estimate < (1u64 << 61) as f64) {
2204            return Err(ShapeError::MassNotRepresentable);
2205        }
2206        let mut body = RigidBody::new_dynamic(position, props.mass);
2207        body.rotation = frame;
2208        body.inv_inertia = Vec3Fix::new(
2209            Fix128::ONE / moments.x,
2210            Fix128::ONE / moments.y,
2211            Fix128::ONE / moments.z,
2212        );
2213        let idx = self.add_body_with_radius(body, radius);
2214        self.body_colliders[idx] = Some(collider);
2215        Ok(idx)
2216    }
2217
2218    /// Extend the per-body side tables with the defaults `add_body` gives, up
2219    /// to `bodies.len()` (bodies pushed onto the public field have none).
2220    fn pad_body_tables(&mut self) {
2221        let n = self.bodies.len();
2222        if self.body_materials.len() < n {
2223            self.body_materials
2224                .resize(n, crate::material::DEFAULT_MATERIAL);
2225        }
2226        if self.body_collision_radii.len() < n {
2227            self.body_collision_radii.resize(n, None);
2228        }
2229        if self.body_colliders.len() < n {
2230            self.body_colliders.resize(n, None);
2231        }
2232        if self.body_filters.len() < n {
2233            self.body_filters.resize(n, CollisionFilter::DEFAULT);
2234        }
2235    }
2236
2237    /// [`Self::remove_body`] that also returns the body's per-body settings
2238    /// (material, collision radius, collider, collision filter), for moving it
2239    /// into another world with [`Self::add_body_with_attachments`]. The stable
2240    /// id stays behind: ids belong to a world.
2241    pub(crate) fn remove_body_with_attachments(
2242        &mut self,
2243        idx: usize,
2244    ) -> Option<(RigidBody, BodyAttachments)> {
2245        // a body pushed onto the public `bodies` field may have no entries yet:
2246        // it carries the defaults `add_body` would give it
2247        let attachments = BodyAttachments {
2248            material: self
2249                .body_materials
2250                .get(idx)
2251                .copied()
2252                .unwrap_or(crate::material::DEFAULT_MATERIAL),
2253            collision_radius: self.body_collision_radii.get(idx).copied().flatten(),
2254            collider: self.body_colliders.get(idx).cloned().flatten(),
2255            filter: self.body_filters.get(idx).copied().unwrap_or_default(),
2256        };
2257        let body = self.remove_body(idx)?;
2258        Some((body, attachments))
2259    }
2260
2261    /// [`Self::add_body`] with the per-body settings taken by
2262    /// [`Self::remove_body_with_attachments`]; the body gets a new stable id.
2263    pub(crate) fn add_body_with_attachments(
2264        &mut self,
2265        body: RigidBody,
2266        attachments: BodyAttachments,
2267    ) -> usize {
2268        let idx = self.add_body(body);
2269        self.body_materials[idx] = attachments.material;
2270        self.body_collision_radii[idx] = attachments.collision_radius;
2271        self.body_colliders[idx] = attachments.collider;
2272        self.body_filters[idx] = attachments.filter;
2273        idx
2274    }
2275
2276    /// Remove a body by index (swap-remove).
2277    ///
2278    /// The last body is moved to fill the gap. All constraints and joints
2279    /// referencing the old last index are remapped. Returns the removed body,
2280    /// or `None` if the index is out of bounds.
2281    pub fn remove_body(&mut self, idx: usize) -> Option<RigidBody> {
2282        if idx >= self.bodies.len() {
2283            return None;
2284        }
2285        let last = self.bodies.len() - 1;
2286
2287        // 1. Drop every constraint / joint that references the body being removed.
2288        //    This must happen BEFORE the `last -> idx` remap: after `swap_remove`
2289        //    the moved last body occupies `idx`, so a constraint that still says
2290        //    `idx` would silently re-attach to the wrong body (2026-09-15 bug fix,
2291        //    found by the remove_body mutation tests; v1.1.0 and earlier produced
2292        //    `(idx, idx)` self-constraints for the swapped-in body).
2293        self.distance_constraints
2294            .retain(|c| c.body_a != idx && c.body_b != idx);
2295        self.contact_constraints
2296            .retain(|c| c.body_a != idx && c.body_b != idx);
2297        if !self.joint_motors.is_empty() || !self.joint_motors_3d.is_empty() {
2298            // Motors follow their joint: new index of each kept joint, `None`
2299            // for a joint dropped with the body.
2300            let mut next = 0;
2301            let joint_map: Vec<Option<usize>> = self
2302                .joints
2303                .iter()
2304                .map(|j| {
2305                    let (a, b) = j.bodies();
2306                    if a != idx && b != idx {
2307                        next += 1;
2308                        Some(next - 1)
2309                    } else {
2310                        None
2311                    }
2312                })
2313                .collect();
2314            self.remap_motor_joints(|j| joint_map.get(j).copied().flatten());
2315        }
2316        self.joints.retain(|j| {
2317            let (a, b) = j.bodies();
2318            a != idx && b != idx
2319        });
2320
2321        // Bring the ids level with `bodies` before both lose `idx` together
2322        // (a body pushed onto the public field has no id yet).
2323        #[cfg(feature = "std")]
2324        self.sync_body_stable_ids();
2325        // a body pushed onto the public `bodies` field has no side-table
2326        // entries yet: give it the defaults so the tables swap-remove in step
2327        self.pad_body_tables();
2328        let removed = self.bodies.swap_remove(idx);
2329        self.body_materials.swap_remove(idx);
2330        self.body_collision_radii.swap_remove(idx);
2331        self.body_colliders.swap_remove(idx);
2332        self.body_filters.swap_remove(idx);
2333        #[cfg(feature = "std")]
2334        self.body_stable_ids.swap_remove(idx);
2335
2336        // 2. Remap references from `last` -> `idx` in all remaining constraints and joints
2337        if idx != last {
2338            for c in &mut self.distance_constraints {
2339                if c.body_a == last {
2340                    c.body_a = idx;
2341                }
2342                if c.body_b == last {
2343                    c.body_b = idx;
2344                }
2345            }
2346            for c in &mut self.contact_constraints {
2347                if c.body_a == last {
2348                    c.body_a = idx;
2349                }
2350                if c.body_b == last {
2351                    c.body_b = idx;
2352                }
2353            }
2354            self.remap_joint_indices(last, idx);
2355        }
2356
2357        // Rebuild IslandManager to match new body count and connectivity
2358        let new_len = self.bodies.len();
2359        self.islands = IslandManager::new(new_len, self.islands.config);
2360        for j in &self.joints {
2361            let (a, b) = j.bodies();
2362            if a < new_len && b < new_len {
2363                self.islands.union(a, b);
2364            }
2365        }
2366
2367        self.park_generation = self.park_generation.wrapping_add(1);
2368        self.batches_dirty = true;
2369        Some(removed)
2370    }
2371
2372    /// Remap joint body indices after swap-remove
2373    fn remap_joint_indices(&mut self, from: usize, to: usize) {
2374        /// Remap a single joint's `body_a` and `body_b` fields.
2375        macro_rules! remap {
2376            ($j:expr) => {
2377                if $j.body_a == from {
2378                    $j.body_a = to;
2379                }
2380                if $j.body_b == from {
2381                    $j.body_b = to;
2382                }
2383            };
2384        }
2385        for joint in &mut self.joints {
2386            match joint {
2387                Joint::Ball(j) => {
2388                    remap!(j);
2389                }
2390                Joint::Hinge(j) => {
2391                    remap!(j);
2392                }
2393                Joint::Fixed(j) => {
2394                    remap!(j);
2395                }
2396                Joint::Slider(j) => {
2397                    remap!(j);
2398                }
2399                Joint::Spring(j) => {
2400                    remap!(j);
2401                }
2402                Joint::D6(j) => {
2403                    remap!(j);
2404                }
2405                Joint::ConeTwist(j) => {
2406                    remap!(j);
2407                }
2408            }
2409        }
2410    }
2411
2412    /// Number of bodies in the world
2413    #[inline]
2414    #[must_use]
2415    pub fn body_count(&self) -> usize {
2416        self.bodies.len()
2417    }
2418
2419    /// Number of active (non-sleeping) bodies
2420    #[must_use]
2421    pub fn active_body_count(&self) -> usize {
2422        self.bodies.len() - self.islands.sleeping_count().min(self.bodies.len())
2423    }
2424
2425    /// Set a body's material ID
2426    pub fn set_body_material(&mut self, body_idx: usize, material_id: crate::material::MaterialId) {
2427        if body_idx < self.body_materials.len() {
2428            self.body_materials[body_idx] = material_id;
2429        }
2430    }
2431
2432    /// Add a pre-solve contact hook
2433    ///
2434    /// The hook is called with (`body_a_index`, `body_b_index`, &contact).
2435    /// Return `false` to skip the contact (e.g., for one-way platforms).
2436    #[cfg(feature = "std")]
2437    pub fn add_pre_solve_hook(&mut self, hook: PreSolveHook) {
2438        self.pre_solve_hooks.push(hook);
2439    }
2440
2441    /// Clear all pre-solve hooks
2442    #[cfg(feature = "std")]
2443    pub fn clear_pre_solve_hooks(&mut self) {
2444        self.pre_solve_hooks.clear();
2445    }
2446
2447    /// Add a contact modifier
2448    ///
2449    /// Contact modifiers can mutate contact properties (normal, depth, friction,
2450    /// restitution) before solving. Return false from `modify_contact` to discard.
2451    #[cfg(feature = "std")]
2452    pub fn add_contact_modifier(&mut self, modifier: Box<dyn ContactModifier>) {
2453        self.contact_modifiers.push(modifier);
2454    }
2455
2456    /// Clear all contact modifiers
2457    #[cfg(feature = "std")]
2458    pub fn clear_contact_modifiers(&mut self) {
2459        self.contact_modifiers.clear();
2460    }
2461
2462    /// Begin a new simulation frame (updates contact cache lifecycle)
2463    pub fn begin_frame(&mut self) {
2464        self.contact_cache.begin_frame();
2465    }
2466
2467    /// End a simulation frame (prune old contacts from cache)
2468    pub fn end_frame(&mut self) {
2469        self.contact_cache.end_frame();
2470    }
2471
2472    // ── Joint Management ───────────────────────────────────────────────
2473
2474    /// Add a joint constraint, returns joint index.
2475    ///
2476    /// # Panics
2477    ///
2478    /// Panics if either body index in the joint is out of bounds.
2479    pub fn add_joint(&mut self, joint: Joint) -> usize {
2480        let (a, b) = joint.bodies();
2481        assert!(
2482            a < self.bodies.len() && b < self.bodies.len(),
2483            "Joint body indices ({}, {}) out of bounds (body count = {})",
2484            a,
2485            b,
2486            self.bodies.len()
2487        );
2488        let idx = self.joints.len();
2489        self.islands.resize(self.bodies.len());
2490        self.islands.union(a, b);
2491        self.joints.push(joint);
2492        idx
2493    }
2494
2495    /// Remove a joint by index (swap-remove), returns the removed joint
2496    ///
2497    /// Motors follow their joint: a motor on the removed joint is removed with
2498    /// it, and a motor on the last joint (moved into `idx`) is re-pointed to
2499    /// `idx`.
2500    pub fn remove_joint(&mut self, idx: usize) -> Option<Joint> {
2501        if idx >= self.joints.len() {
2502            return None;
2503        }
2504        let last = self.joints.len() - 1;
2505        self.remap_motor_joints(|j| {
2506            if j == idx {
2507                None
2508            } else if j == last {
2509                Some(idx)
2510            } else {
2511                Some(j)
2512            }
2513        });
2514        Some(self.joints.swap_remove(idx))
2515    }
2516
2517    /// Re-point (or drop, on `None`) every motor's joint index through `map`.
2518    fn remap_motor_joints(&mut self, map: impl Fn(usize) -> Option<usize>) {
2519        self.joint_motors.retain_mut(|m| match map(m.joint_index) {
2520            Some(j) => {
2521                m.joint_index = j;
2522                true
2523            }
2524            None => false,
2525        });
2526        self.joint_motors_3d
2527            .retain_mut(|(joint, _)| match map(*joint) {
2528                Some(j) => {
2529                    *joint = j;
2530                    true
2531                }
2532                None => false,
2533            });
2534    }
2535
2536    // ── Joint motors ──────────────────────────────────────────────────
2537
2538    /// Attach a one-axis PD motor to joint `joint_index`, returns the motor
2539    /// index.
2540    ///
2541    /// The motor is applied by [`Self::step`] every substep (see
2542    /// [`crate::motor::apply_motors`] for the generalised coordinate per joint
2543    /// type: the twist angle for a hinge, the centre distance otherwise). The
2544    /// controller is used as given, so a controller still in
2545    /// [`crate::motor::MotorMode::Off`] does nothing until a target is set
2546    /// ([`Self::set_joint_motor_velocity_target`] / [`Self::joint_motor_mut`]).
2547    /// Motors follow their joint through [`Self::remove_joint`] and
2548    /// [`Self::remove_body`]; a motor whose index no longer names a joint
2549    /// (after editing the public `joints` field directly) does nothing.
2550    pub fn add_joint_motor(
2551        &mut self,
2552        joint_index: usize,
2553        controller: crate::motor::PdController,
2554    ) -> usize {
2555        self.joint_motors
2556            .push(crate::motor::JointMotor::new(joint_index, controller));
2557        self.joint_motors.len() - 1
2558    }
2559
2560    /// The motor added as `motor` by [`Self::add_joint_motor`], for changing its
2561    /// gains or targets between steps.
2562    #[must_use]
2563    pub fn joint_motor_mut(&mut self, motor: usize) -> Option<&mut crate::motor::JointMotor> {
2564        self.joint_motors.get_mut(motor)
2565    }
2566
2567    /// Switch motor `motor` to velocity mode with `target` (rad/s for a hinge,
2568    /// m/s along the centre line otherwise). Returns `false` if there is no
2569    /// such motor.
2570    pub fn set_joint_motor_velocity_target(&mut self, motor: usize, target: Fix128) -> bool {
2571        match self.joint_motors.get_mut(motor) {
2572            Some(m) => {
2573                m.controller.set_velocity_target(target);
2574                true
2575            }
2576            None => false,
2577        }
2578    }
2579
2580    /// Turn motor `motor` off (it applies no force until a target is set
2581    /// again). Returns `false` if there is no such motor.
2582    pub fn disable_joint_motor(&mut self, motor: usize) -> bool {
2583        match self.joint_motors.get_mut(motor) {
2584            Some(m) => {
2585                m.controller.disable();
2586                true
2587            }
2588            None => false,
2589        }
2590    }
2591
2592    /// Attach a three-axis rotation motor with per-axis gains `kp` / `kd` and
2593    /// torque cap `max_torque` to joint `joint_index`, returns its index.
2594    ///
2595    /// The motor starts `Off`; [`Self::set_joint_motor_3d_rotation_target`]
2596    /// drives the joint's relative rotation `rotation_b * rotation_a⁻¹` to a
2597    /// target (see [`crate::motor::PdController3D::compute_torque`]). It
2598    /// follows its joint through removals like [`Self::add_joint_motor`].
2599    pub fn add_joint_motor_3d(
2600        &mut self,
2601        joint_index: usize,
2602        kp: Vec3Fix,
2603        kd: Vec3Fix,
2604        max_torque: Fix128,
2605    ) -> usize {
2606        self.joint_motors_3d.push((
2607            joint_index,
2608            crate::motor::PdController3D::new(kp, kd, max_torque),
2609        ));
2610        self.joint_motors_3d.len() - 1
2611    }
2612
2613    /// The controller of rotation motor `motor` ([`Self::add_joint_motor_3d`]).
2614    #[must_use]
2615    pub fn joint_motor_3d_mut(
2616        &mut self,
2617        motor: usize,
2618    ) -> Option<&mut crate::motor::PdController3D> {
2619        self.joint_motors_3d.get_mut(motor).map(|(_, c)| c)
2620    }
2621
2622    /// Drive rotation motor `motor` to the relative rotation `target`
2623    /// (position mode). Returns `false` if there is no such motor.
2624    pub fn set_joint_motor_3d_rotation_target(&mut self, motor: usize, target: QuatFix) -> bool {
2625        match self.joint_motors_3d.get_mut(motor) {
2626            Some((_, c)) => {
2627                c.set_rotation_target(target);
2628                true
2629            }
2630            None => false,
2631        }
2632    }
2633
2634    /// Apply every joint motor for one (sub)step of length `dt`.
2635    ///
2636    /// A sleeping body a motor moves is woken with its island, otherwise the
2637    /// motor's velocity change would be discarded by the sleep skip in
2638    /// `integrate_positions`. A motor whose output is zero does not wake
2639    /// anything, so a motor resting at its target lets the joint sleep.
2640    fn apply_joint_motors(&mut self, dt: Fix128) {
2641        if self.joint_motors.is_empty() && self.joint_motors_3d.is_empty() {
2642            return;
2643        }
2644        let mut asleep: Vec<(usize, Vec3Fix, Vec3Fix)> = Vec::new();
2645        let n = self.bodies.len();
2646        let pairs = self
2647            .joint_motors
2648            .iter()
2649            .map(|m| m.joint_index)
2650            .chain(self.joint_motors_3d.iter().map(|(j, _)| *j));
2651        for j in pairs {
2652            if let Some(joint) = self.joints.get(j) {
2653                let (a, b) = joint.bodies();
2654                for x in [a, b] {
2655                    if x < n && self.islands.is_sleeping(x) {
2656                        asleep.push((x, self.bodies[x].velocity, self.bodies[x].angular_velocity));
2657                    }
2658                }
2659            }
2660        }
2661        crate::motor::apply_motors(&self.joint_motors, &self.joints, &mut self.bodies, dt);
2662        crate::motor::apply_motors_3d(&self.joint_motors_3d, &self.joints, &mut self.bodies, dt);
2663        for (x, v, w) in asleep {
2664            let body = &self.bodies[x];
2665            if (body.velocity != v || body.angular_velocity != w)
2666                && x < self.islands.sleep_data.len()
2667            {
2668                self.islands.wake_island(x);
2669            }
2670        }
2671    }
2672
2673    /// Number of joints
2674    #[inline]
2675    #[must_use]
2676    pub fn joint_count(&self) -> usize {
2677        self.joints.len()
2678    }
2679
2680    // ── Force Field Management ────────────────────────────────────────
2681
2682    /// Add a force field, returns field index
2683    pub fn add_force_field(&mut self, field: ForceFieldInstance) -> usize {
2684        let idx = self.force_fields.len();
2685        self.force_fields.push(field);
2686        idx
2687    }
2688
2689    /// Remove a force field by index
2690    pub fn remove_force_field(&mut self, idx: usize) -> Option<ForceFieldInstance> {
2691        if idx >= self.force_fields.len() {
2692            return None;
2693        }
2694        Some(self.force_fields.swap_remove(idx))
2695    }
2696
2697    // ── Per-Body Collision Shape / Filter ──────────────────────────────
2698
2699    /// Set collision radius for a body (enables automatic sphere collision detection)
2700    pub fn set_body_collision_radius(&mut self, body_idx: usize, radius: Fix128) {
2701        if body_idx < self.body_collision_radii.len() {
2702            self.park_generation = self.park_generation.wrapping_add(1);
2703            self.body_collision_radii[body_idx] = Some(radius);
2704        }
2705    }
2706
2707    /// Remove collision radius (disable automatic collision detection for this body)
2708    pub fn clear_body_collision_radius(&mut self, body_idx: usize) {
2709        if body_idx < self.body_collision_radii.len() {
2710            self.park_generation = self.park_generation.wrapping_add(1);
2711            self.body_collision_radii[body_idx] = None;
2712        }
2713    }
2714
2715    /// Set collision filter for a body
2716    pub fn set_body_filter(&mut self, body_idx: usize, filter: CollisionFilter) {
2717        if body_idx < self.body_filters.len() {
2718            self.body_filters[body_idx] = filter;
2719        }
2720    }
2721
2722    /// Get collision filter for a body
2723    #[must_use]
2724    pub fn body_filter(&self, body_idx: usize) -> CollisionFilter {
2725        self.body_filters
2726            .get(body_idx)
2727            .copied()
2728            .unwrap_or(CollisionFilter::DEFAULT)
2729    }
2730
2731    // ── Sleeping ──────────────────────────────────────────────────────
2732
2733    /// Check if a body is sleeping
2734    #[inline]
2735    #[must_use]
2736    pub fn is_sleeping(&self, body_idx: usize) -> bool {
2737        self.islands.is_sleeping(body_idx)
2738    }
2739
2740    /// Wake up a body and all connected bodies in its island
2741    pub fn wake_body(&mut self, body_idx: usize) {
2742        // An index past the last body is ignored, like every other per-body
2743        // setter; the island union-find would index out of range otherwise.
2744        if body_idx >= self.bodies.len() {
2745            return;
2746        }
2747        self.islands.wake_island(body_idx);
2748    }
2749
2750    /// Set the sleep configuration
2751    pub fn set_sleep_config(&mut self, config: SleepConfig) {
2752        self.islands.config = config;
2753    }
2754
2755    /// Turn the sleep skip of [`Self::step`] on (the default) or off.
2756    ///
2757    /// With it on, a sleeping body at rest that no joint or distance constraint
2758    /// references is left out of every stage of the step, and its contacts are
2759    /// found through a persistent tree of such bodies instead of rebuilding the
2760    /// broad-phase over it (see [`StageWork`]). The simulation is bit-identical
2761    /// either way: the skip only drops work whose result is already known (a body
2762    /// at rest stays where it is, with the velocity it already has). Turning it off
2763    /// drops the tree; it is rebuilt on the next step with the skip on.
2764    ///
2765    /// Only `step` with the XPBD backend and [`Broadphase::Bvh`] skips; the other
2766    /// entry points and broad-phases ([`Broadphase::DynamicTree`],
2767    /// [`Broadphase::Hybrid`]) visit every body as before.
2768    pub fn set_sleep_skip(&mut self, enabled: bool) {
2769        self.sleep_skip = enabled;
2770        if !enabled {
2771            self.park.clear();
2772        }
2773    }
2774
2775    /// Whether the sleep skip of [`Self::step`] is on (see [`Self::set_sleep_skip`]).
2776    #[must_use]
2777    pub const fn sleep_skip(&self) -> bool {
2778        self.sleep_skip
2779    }
2780
2781    /// Work counters of the last [`Self::step`] (see [`StageWork`]).
2782    #[must_use]
2783    pub const fn stage_work(&self) -> StageWork {
2784        self.stage_work
2785    }
2786
2787    // ── Typed Observation (World Auditor WM-10, Physics 版) ────────────
2788
2789    /// Observe a single body's law-relevant state.
2790    ///
2791    /// Returns `None` if `body_idx` is out of range (there is no body at
2792    /// that index — note that [`Self::remove_body`] uses `swap_remove`, so
2793    /// `body_idx` is only stable within the frame it was observed).
2794    ///
2795    /// ⚠️ **`grounded` は意図的に持たない** — 一般 [`RigidBody`] に地面接触の
2796    /// 定義は存在せず (`character.rs` の `CharacterController::detect_ground`
2797    /// は特化アルゴリズムで、全 body に適用できる基準ではない) Law / goal
2798    /// 述語が「接地」を必要とする場合は `in_contact` + 接触法線
2799    /// ([`EventCollector::contact_events`] の [`crate::event::ContactEvent::normal`]) から
2800    /// 自前で構成する
2801    #[must_use]
2802    pub fn observe_body(&self, body_idx: usize) -> Option<BodyObservation> {
2803        let body = self.bodies.get(body_idx)?;
2804        let in_contact = self
2805            .events
2806            .contact_events()
2807            .iter()
2808            .any(|e| e.body_a == body_idx || e.body_b == body_idx);
2809        Some(BodyObservation {
2810            body_index: body_idx,
2811            position: body.position,
2812            velocity: body.velocity,
2813            rotation: body.rotation,
2814            angular_velocity: body.angular_velocity,
2815            sleeping: self.is_sleeping(body_idx),
2816            in_contact,
2817        })
2818    }
2819
2820    /// Observe every body in the world, in body index order.
2821    #[must_use]
2822    pub fn observe_bodies(&self) -> Vec<BodyObservation> {
2823        (0..self.bodies.len())
2824            .map(|i| {
2825                self.observe_body(i)
2826                    .expect("index came from 0..bodies.len(), always in range")
2827            })
2828            .collect()
2829    }
2830
2831    // ── Event Access ──────────────────────────────────────────────────
2832
2833    /// Get contact events from the last step
2834    #[inline]
2835    #[must_use]
2836    pub fn contact_events(&self) -> &[crate::event::ContactEvent] {
2837        self.events.contact_events()
2838    }
2839
2840    /// Get trigger events from the last step
2841    #[inline]
2842    #[must_use]
2843    pub fn trigger_events(&self) -> &[crate::event::TriggerEvent] {
2844        self.events.trigger_events()
2845    }
2846
2847    /// Drain (consume) contact events from the last step
2848    #[inline]
2849    pub fn drain_contact_events(&mut self) -> Vec<crate::event::ContactEvent> {
2850        self.events.drain_contact_events()
2851    }
2852
2853    /// Drain (consume) trigger events from the last step
2854    #[inline]
2855    pub fn drain_trigger_events(&mut self) -> Vec<crate::event::TriggerEvent> {
2856        self.events.drain_trigger_events()
2857    }
2858
2859    // ── Raycast on World ──────────────────────────────────────────────
2860
2861    /// What a ray query tests for body `i`: its collision radius and the shape or
2862    /// compound it carries, or `None` when the body has no collision radius (it
2863    /// takes part in no collision, so no ray sees it either).
2864    pub(crate) fn ray_geometry(
2865        &self,
2866        i: usize,
2867    ) -> Option<(Fix128, Option<&crate::body_collider::BodyCollider>)> {
2868        let radius = self.body_collision_radii.get(i).copied().flatten()?;
2869        Some((radius, self.body_colliders.get(i).and_then(Option::as_ref)))
2870    }
2871
2872    /// The static colliders, in index order.
2873    pub(crate) fn static_colliders_slice(&self) -> &[crate::static_collider::StaticCollider] {
2874        &self.static_colliders
2875    }
2876
2877    /// The normal force of one contact of the last step (see
2878    /// [`contact_forces`](Self::contact_forces)), or `None` when neither body can move.
2879    pub(crate) fn contact_constraint_force(
2880        &self,
2881        c: &ContactConstraint,
2882        dt: Fix128,
2883    ) -> Option<Fix128> {
2884        let h = self.substep_dt(dt);
2885        self.contact_force(c, h * h).map(|(_, _, force)| force)
2886    }
2887
2888    /// Cast a ray against all body collision spheres, returns (`body_index`, distance).
2889    ///
2890    /// Uses a BVH broad-phase to cull bodies outside the ray's bounding box,
2891    /// then performs exact ray-sphere intersection on candidates.
2892    /// Returns `None` if direction is zero or no body is hit.
2893    ///
2894    /// Every body is tested as its **bounding sphere** (its collision radius), even
2895    /// when it carries a shape or a compound, and static colliders and SDF
2896    /// colliders are not tested. This differs from the contacts, which a shaped
2897    /// body makes with its shape: a ray can hit the bounding sphere of a body
2898    /// that nothing touches there. [`Self::cast_ray`] tests the actual geometry
2899    /// (see [`crate::shape_raycast`]).
2900    #[must_use]
2901    pub fn raycast(
2902        &self,
2903        origin: Vec3Fix,
2904        direction: Vec3Fix,
2905        max_distance: Fix128,
2906    ) -> Option<(usize, Fix128)> {
2907        if direction.length_squared().is_zero() {
2908            return None;
2909        }
2910        let dir_norm = direction.normalize();
2911
2912        // Build BVH from collidable bodies for broad-phase culling
2913        let mut primitives = Vec::new();
2914        for i in 0..self.bodies.len() {
2915            if let Some(radius) = self.body_collision_radii.get(i).and_then(|r| *r) {
2916                let pos = self.bodies[i].position;
2917                let half = Vec3Fix::new(radius, radius, radius);
2918                let aabb = crate::collider::AABB::from_center_half(pos, half);
2919                primitives.push(crate::bvh::BvhPrimitive {
2920                    aabb,
2921                    index: i as u32,
2922                    morton: 0,
2923                });
2924            }
2925        }
2926
2927        if primitives.is_empty() {
2928            return None;
2929        }
2930
2931        // Compute ray AABB (bounding box of the ray segment)
2932        let endpoint = origin + dir_norm * max_distance;
2933        let ray_min = Vec3Fix::new(
2934            if origin.x < endpoint.x {
2935                origin.x
2936            } else {
2937                endpoint.x
2938            },
2939            if origin.y < endpoint.y {
2940                origin.y
2941            } else {
2942                endpoint.y
2943            },
2944            if origin.z < endpoint.z {
2945                origin.z
2946            } else {
2947                endpoint.z
2948            },
2949        );
2950        let ray_max = Vec3Fix::new(
2951            if origin.x > endpoint.x {
2952                origin.x
2953            } else {
2954                endpoint.x
2955            },
2956            if origin.y > endpoint.y {
2957                origin.y
2958            } else {
2959                endpoint.y
2960            },
2961            if origin.z > endpoint.z {
2962                origin.z
2963            } else {
2964                endpoint.z
2965            },
2966        );
2967        let ray_aabb = crate::collider::AABB {
2968            min: ray_min,
2969            max: ray_max,
2970        };
2971
2972        let bvh = crate::bvh::LinearBvh::build(primitives);
2973        let candidates = bvh.query(&ray_aabb);
2974
2975        // Narrow-phase: exact ray-sphere intersection on BVH candidates
2976        let mut best: Option<(usize, Fix128)> = None;
2977        for &prim_idx in &candidates {
2978            let i = prim_idx as usize;
2979            let radius = match self.body_collision_radii.get(i) {
2980                Some(Some(r)) => *r,
2981                _ => continue,
2982            };
2983            let oc = origin - self.bodies[i].position;
2984            let b = oc.dot(dir_norm);
2985            let c = oc.dot(oc) - radius * radius;
2986            let discriminant = b * b - c;
2987            if discriminant < Fix128::ZERO {
2988                continue;
2989            }
2990            let sqrt_d = discriminant.sqrt();
2991            // Try the nearest intersection first
2992            let mut t = -b - sqrt_d;
2993            // If nearest t is behind origin, try the far intersection (ray inside sphere)
2994            if t < Fix128::ZERO {
2995                t = -b + sqrt_d;
2996            }
2997            if t < Fix128::ZERO || t > max_distance {
2998                continue;
2999            }
3000            let dominated = match best {
3001                None => true,
3002                Some((_, prev_t)) => t < prev_t,
3003            };
3004            if dominated {
3005                best = Some((i, t));
3006            }
3007        }
3008        best
3009    }
3010
3011    /// Get combined material properties for a body pair
3012    ///
3013    /// # Claims
3014    ///
3015    /// - A body whose material is [`crate::material::DEFAULT_MATERIAL`] contributes
3016    ///   its own [`RigidBody::friction`] and [`RigidBody::restitution`] (the
3017    ///   default material is "no material assigned"); a body with any other
3018    ///   material contributes that material's `dynamic_friction` / `restitution`
3019    ///   and its own fields are not read.
3020    /// - Bodies built with the defaults (friction 0.3, restitution 0.5) give the
3021    ///   same result as before, since the default material holds the same values.
3022    /// - The two contributions are combined with the materials' combine rules; a
3023    ///   pair override registered for the two materials takes precedence over
3024    ///   both.
3025    /// - An index outside the body list counts as the default material with the
3026    ///   material's own values.
3027    #[must_use]
3028    pub fn combined_material(
3029        &self,
3030        body_a: usize,
3031        body_b: usize,
3032    ) -> crate::material::CombinedMaterial {
3033        let side = |i: usize| -> (crate::material::MaterialId, Option<(Fix128, Fix128)>) {
3034            let Some(&mat) = self.body_materials.get(i) else {
3035                return (crate::material::DEFAULT_MATERIAL, None);
3036            };
3037            if mat != crate::material::DEFAULT_MATERIAL {
3038                return (mat, None);
3039            }
3040            (mat, self.bodies.get(i).map(|b| (b.friction, b.restitution)))
3041        };
3042        let (mat_a, surface_a) = side(body_a);
3043        let (mat_b, surface_b) = side(body_b);
3044        self.material_table
3045            .combine_with_surface(mat_a, surface_a, mat_b, surface_b)
3046    }
3047
3048    /// Add distance constraint
3049    pub fn add_distance_constraint(&mut self, constraint: DistanceConstraint) {
3050        self.distance_constraints.push(constraint);
3051        self.batches_dirty = true;
3052    }
3053
3054    /// Add contact constraint
3055    pub fn add_contact(&mut self, contact: ContactConstraint) {
3056        // Update contact cache for warm starting
3057        let key = crate::contact_cache::BodyPairKey::new(contact.body_a, contact.body_b);
3058        let manifold = self
3059            .contact_cache
3060            .get_or_create(key, contact.friction, contact.restitution);
3061        manifold.add_or_update(
3062            &contact.contact,
3063            contact.contact.point_a,
3064            contact.contact.point_b,
3065        );
3066
3067        self.contact_constraints.push(contact);
3068        self.batches_dirty = true;
3069    }
3070
3071    /// Add contact constraint with material lookup
3072    ///
3073    /// Automatically looks up friction/restitution from the material table.
3074    pub fn add_contact_with_material(&mut self, body_a: usize, body_b: usize, contact: Contact) {
3075        let combined = self.combined_material(body_a, body_b);
3076        let constraint = ContactConstraint {
3077            body_a,
3078            body_b,
3079            contact,
3080            friction: combined.friction,
3081            restitution: combined.restitution,
3082            cached_lambda: Fix128::ZERO,
3083        };
3084        self.add_contact(constraint);
3085    }
3086
3087    /// Clear all contact constraints. `step()` does this at the start of every
3088    /// substep (1.2.0) before `detect_collisions`, so contacts added manually
3089    /// between frames are not seen by `step()`; drive the substep API yourself
3090    /// if you generate contacts outside the built-in sphere / SDF detection.
3091    pub fn clear_contacts(&mut self) {
3092        self.contact_constraints.clear();
3093        #[cfg(feature = "std")]
3094        self.contact_discarded.clear();
3095        self.batches_dirty = true;
3096    }
3097
3098    /// Rebuild constraint batches using greedy graph coloring
3099    ///
3100    /// Each batch contains constraints that share no bodies,
3101    /// allowing safe parallel modification.
3102    pub fn rebuild_batches(&mut self) {
3103        if !self.batches_dirty {
3104            return;
3105        }
3106
3107        self.constraint_batches.clear();
3108
3109        // Snapshot which bodies are static / kinematic. They are never written
3110        // by the pair solvers (every correction is multiplied by
3111        // `inv_mass == 0`), so the parallel path borrows them shared and they
3112        // impose no coloring constraint. Without this exclusion a single
3113        // static floor touched by N bodies would force N sequential batches.
3114        let num_bodies = self.bodies.len();
3115        self.batch_static_bodies.clear();
3116        self.batch_static_bodies
3117            .extend(self.bodies.iter().map(|b| b.inv_mass.is_zero()));
3118
3119        // Track which colors each dynamic body participates in. One growable
3120        // bitset per body (64 colors per word) — no upper bound on colors, so a
3121        // hub body shared by 65+ constraints still gets a unique color per
3122        // constraint instead of aliasing into a single overflow batch.
3123        let mut body_colors: Vec<Vec<u64>> = vec![Vec::new(); num_bodies];
3124
3125        // Color distance constraints
3126        for (constraint_idx, constraint) in self.distance_constraints.iter().enumerate() {
3127            let body_a = constraint.body_a;
3128            let body_b = constraint.body_b;
3129
3130            // Find first color where both bodies are free
3131            let color =
3132                Self::find_free_color(&body_colors, &self.batch_static_bodies, body_a, body_b);
3133
3134            // Ensure we have enough batches
3135            while self.constraint_batches.len() <= color {
3136                self.constraint_batches.push(ConstraintBatch::default());
3137            }
3138
3139            // Add constraint to batch
3140            self.constraint_batches[color]
3141                .distance_indices
3142                .push(constraint_idx);
3143
3144            // Mark bodies as used in this color (set bit)
3145            Self::mark_color(&mut body_colors, &self.batch_static_bodies, body_a, color);
3146            Self::mark_color(&mut body_colors, &self.batch_static_bodies, body_b, color);
3147        }
3148
3149        // Color contact constraints
3150        for (constraint_idx, constraint) in self.contact_constraints.iter().enumerate() {
3151            let body_a = constraint.body_a;
3152            let body_b = constraint.body_b;
3153
3154            let color =
3155                Self::find_free_color(&body_colors, &self.batch_static_bodies, body_a, body_b);
3156
3157            while self.constraint_batches.len() <= color {
3158                self.constraint_batches.push(ConstraintBatch::default());
3159            }
3160
3161            self.constraint_batches[color]
3162                .contact_indices
3163                .push(constraint_idx);
3164
3165            Self::mark_color(&mut body_colors, &self.batch_static_bodies, body_a, color);
3166            Self::mark_color(&mut body_colors, &self.batch_static_bodies, body_b, color);
3167        }
3168
3169        self.batches_dirty = false;
3170
3171        debug_assert!(
3172            self.batches_are_body_disjoint(),
3173            "graph coloring produced a batch with two constraints sharing a dynamic body"
3174        );
3175    }
3176
3177    /// Occupancy word `word` of body `idx`, or 0 if the body is static,
3178    /// out of range, or has no color at that word yet.
3179    #[inline(always)]
3180    fn color_word(
3181        body_colors: &[Vec<u64>],
3182        static_bodies: &[bool],
3183        idx: usize,
3184        word: usize,
3185    ) -> u64 {
3186        if static_bodies.get(idx).copied().unwrap_or(false) {
3187            return 0;
3188        }
3189        body_colors
3190            .get(idx)
3191            .and_then(|words| words.get(word))
3192            .copied()
3193            .unwrap_or(0)
3194    }
3195
3196    /// Find first color where both bodies are free (greedy coloring).
3197    ///
3198    /// Scans the per-body occupancy bitsets word by word (64 colors per
3199    /// word); the first word with a free bit yields the color. If every
3200    /// allocated word is saturated the next fresh word is used, so the
3201    /// number of colors is unbounded and two constraints sharing a dynamic
3202    /// body never land in the same batch.
3203    fn find_free_color(
3204        body_colors: &[Vec<u64>],
3205        static_bodies: &[bool],
3206        body_a: usize,
3207        body_b: usize,
3208    ) -> usize {
3209        let words_a = body_colors.get(body_a).map_or(0, Vec::len);
3210        let words_b = body_colors.get(body_b).map_or(0, Vec::len);
3211        let words = words_a.max(words_b);
3212
3213        for word in 0..words {
3214            let occupied = Self::color_word(body_colors, static_bodies, body_a, word)
3215                | Self::color_word(body_colors, static_bodies, body_b, word);
3216            if occupied != u64::MAX {
3217                return word * 64 + (!occupied).trailing_zeros() as usize;
3218            }
3219        }
3220
3221        words * 64
3222    }
3223
3224    /// Record that dynamic body `idx` participates in `color`.
3225    ///
3226    /// Static bodies and out-of-range indices are ignored (they are not
3227    /// coloring constraints). Grows the body's bitset on demand.
3228    #[inline(always)]
3229    fn mark_color(body_colors: &mut [Vec<u64>], static_bodies: &[bool], idx: usize, color: usize) {
3230        if static_bodies.get(idx).copied().unwrap_or(false) {
3231            return;
3232        }
3233        if let Some(words) = body_colors.get_mut(idx) {
3234            let word = color / 64;
3235            if words.len() <= word {
3236                words.resize(word + 1, 0);
3237            }
3238            words[word] |= 1u64 << (color % 64);
3239        }
3240    }
3241
3242    /// Check the graph coloring invariant: within one batch no two
3243    /// constraints reference the same dynamic body (static bodies as
3244    /// recorded in `batch_static_bodies` may repeat, they are read-only).
3245    ///
3246    /// O(total constraint references) with a per-body scratch marker.
3247    /// Used by `rebuild_batches`' `debug_assert!` and by tests.
3248    pub(crate) fn batches_are_body_disjoint(&self) -> bool {
3249        let num_bodies = self.bodies.len();
3250        // usize::MAX = untouched; otherwise the batch index that last used it.
3251        let mut last_batch: Vec<usize> = vec![usize::MAX; num_bodies];
3252
3253        for (batch_idx, batch) in self.constraint_batches.iter().enumerate() {
3254            let dist_bodies = batch.distance_indices.iter().map(|&i| {
3255                (
3256                    self.distance_constraints[i].body_a,
3257                    self.distance_constraints[i].body_b,
3258                )
3259            });
3260            let contact_bodies = batch.contact_indices.iter().map(|&i| {
3261                (
3262                    self.contact_constraints[i].body_a,
3263                    self.contact_constraints[i].body_b,
3264                )
3265            });
3266
3267            for (a, b) in dist_bodies.chain(contact_bodies) {
3268                for (k, idx) in [a, b].into_iter().enumerate() {
3269                    // A constraint referencing the same body twice counts once.
3270                    if (k == 1 && a == b) || idx >= num_bodies || self.batch_static_bodies[idx] {
3271                        continue;
3272                    }
3273                    if last_batch[idx] == batch_idx {
3274                        return false;
3275                    }
3276                    last_batch[idx] = batch_idx;
3277                }
3278            }
3279        }
3280        true
3281    }
3282
3283    /// Get number of constraint batches (colors used)
3284    #[must_use]
3285    pub fn num_batches(&self) -> usize {
3286        self.constraint_batches.len()
3287    }
3288
3289    /// Step simulation by dt (in fixed-point seconds)
3290    ///
3291    /// Integrated pipeline:
3292    /// 1. Begin event frame
3293    /// 2. Apply force fields to body velocities
3294    /// 3. Detect collisions (BVH broad-phase + sphere narrow-phase)
3295    /// 4. Substep loop (joint motors, integrate, solve constraints + joints,
3296    ///    update velocities)
3297    /// 5. Update sleeping states
3298    /// 6. End event frame (generates end-of-contact events)
3299    ///
3300    /// Deterministic: same inputs always produce same outputs.
3301    ///
3302    /// # Limitation: joints
3303    ///
3304    /// - `step` solves the joints once per substep, after and outside the
3305    ///   `iterations` loop that solves distance and contact constraints.
3306    /// - Joint compliance is therefore exact XPBD with respect to substeps:
3307    ///   each substep's single solve starts from `λ = 0`, so a compliant joint
3308    ///   under a constant load `F` settles at the stretch `F · compliance`
3309    ///   whatever `iterations` is.
3310    /// - Convergence of a rigid joint chain depends on `substeps` only, not on
3311    ///   `iterations`.
3312    ///
3313    /// # Participants
3314    ///
3315    /// `step` is [`Self::try_step`] with the result dropped: while a fault is
3316    /// recorded ([`Self::fault`]) it leaves the world unchanged. Without
3317    /// registered participants no fault is ever recorded and `step` runs as
3318    /// it always has.
3319    pub fn step(&mut self, dt: Fix128) {
3320        let _ = self.try_step(dt);
3321    }
3322
3323    /// Bring every body's rotations to unit length before a step reads them
3324    /// ([`RigidBody::make_rotations_unit`]), and rebuild the cached values of
3325    /// the SDF colliders from their fields ([`SdfCollider::update_cache`]:
3326    /// unit rotation, its conjugate as the inverse, the scale as `f32`).
3327    /// `rotation` is a public field, so a value assigned to it directly
3328    /// between steps reaches the step as it was written; a rotation already of
3329    /// unit length is left bit for bit, and a collider whose cache was already
3330    /// current is left bit for bit. A collider attached to a body is placed
3331    /// again from the body afterwards ([`Self::sync_sdf_colliders`]).
3332    fn make_body_rotations_unit(&mut self) {
3333        for body in &mut self.bodies {
3334            body.make_rotations_unit();
3335        }
3336        for collider in &mut self.sdf_colliders {
3337            collider.update_cache();
3338        }
3339    }
3340
3341    /// The step body behind [`Self::try_step`] (checks already done, `dt > 0`).
3342    fn run_step(&mut self, dt: Fix128) {
3343        self.make_body_rotations_unit();
3344        self.wake_on_sdf_shape_change();
3345        self.stage_work = StageWork::default();
3346        self.begin_sdf_contact_log();
3347        let mut frozen = self.participant_flags();
3348
3349        // `SolverBackend::Tgs` dispatch (std-only, see `SolverBackend` doc for
3350        // the no-std fallback). This `matches!` is `false` for every caller
3351        // that leaves `config.solver_backend` at its `Default` (`Xpbd`), so
3352        // the rest of this function — the pre-1.1 XPBD body — is reached and
3353        // executed exactly as before: no new code runs on the default path.
3354        #[cfg(feature = "std")]
3355        if matches!(self.config.solver_backend, SolverBackend::Tgs) {
3356            self.step_tgs(dt, &mut frozen);
3357            return;
3358        }
3359
3360        // Phase 0: Event frame lifecycle (contacts are cleared and re-detected
3361        // in every substep since 1.2.0, see `substep`)
3362        self.events.begin_frame();
3363
3364        // Phase 0.5: Rebuild island connectivity from current joints
3365        self.islands.resize(self.bodies.len());
3366        self.islands.reset_unions();
3367        for j in &self.joints {
3368            let (a, b) = j.bodies();
3369            if a < self.bodies.len() && b < self.bodies.len() {
3370                self.islands.union(a, b);
3371            }
3372        }
3373
3374        let substep_dt = dt / Fix128::from_int(self.config.substeps as i64);
3375
3376        // Phase 0.75: Park the sleeping bodies at rest (sleep skip). Every
3377        // stage below iterates `park.awake` while `park.active` is set.
3378        self.park_begin(dt, substep_dt);
3379
3380        // Phase 1: Apply force fields
3381        if !self.force_fields.is_empty() {
3382            if self.park.active {
3383                for k in 0..self.park.awake.len() {
3384                    let i = self.park.awake[k];
3385                    let body = &mut self.bodies[i];
3386                    if body.is_static() {
3387                        continue;
3388                    }
3389                    self.stage_work.force_field_bodies += 1;
3390                    body.velocity =
3391                        crate::force::force_field_velocity(&self.force_fields, i, body, dt);
3392                }
3393            } else {
3394                self.stage_work.force_field_bodies += self.bodies.len() as u64;
3395                apply_force_fields(&self.force_fields, &mut self.bodies, dt);
3396            }
3397        }
3398
3399        // Phase 2 (collision detection) moved into the substep — Small Steps
3400        // (Müller et al. 2020): a frame-level contact set solved 8× with a
3401        // stale depth re-pushed the penetration every substep and turned a
3402        // 5 m/s head-on collision into 700 m/s (1.2.0, R4-2).
3403
3404        // Phase 3: Substep loop
3405        let n = self.config.substeps;
3406        for i in 0..n {
3407            self.kinematic_substeps_left = n - i;
3408            self.park.first_substep = i == 0;
3409            self.sdf_contact_substep = i;
3410            // Participants run before the substep body (not inside `substep`,
3411            // which the unit tests call on its own).
3412            let overflow_at_start = self.participants_begin_substep(i, n, substep_dt, &mut frozen);
3413            self.substep(substep_dt);
3414            self.participants_end_substep(overflow_at_start, &frozen);
3415        }
3416        self.kinematic_substeps_left = 0;
3417        self.sdf_contact_substep = 0;
3418        self.park.first_substep = false;
3419
3420        // Phase 3.5: Frame-level damping (1.2.0, was per substep)
3421        self.apply_frame_damping();
3422
3423        // Phase 4: Update sleeping
3424        self.update_sleep_states();
3425        self.park.active = false;
3426
3427        // Phase 4.5: Leave every SDF collider attached to a body at that
3428        // body's final pose, which is what the queries between steps
3429        // (`sdf_contacts`, `sdf_ccd_hits`, the shape casts) read.
3430        self.sync_sdf_colliders();
3431
3432        // Phase 5: End event frame
3433        self.events.end_frame();
3434    }
3435
3436    /// Run [`Self::step`] `n` times with the same `dt`.
3437    ///
3438    /// The native counterpart of the Python `step_n` and WASM `stepN`
3439    /// bindings, which call this: the result is bit-identical to calling
3440    /// `step(dt)` `n` times in a loop, `n = 0` leaves the world unchanged, and
3441    /// a non-positive `dt` leaves it unchanged for every `n` (as `step` does).
3442    ///
3443    /// # Examples
3444    ///
3445    /// ```
3446    /// use alice_physics::{Fix128, PhysicsConfig, PhysicsWorld, RigidBody, Vec3Fix};
3447    ///
3448    /// let mut a = PhysicsWorld::new(PhysicsConfig::default());
3449    /// a.add_body(RigidBody::new_dynamic(Vec3Fix::from_int(0, 5, 0), Fix128::ONE));
3450    /// let mut b = PhysicsWorld::from_world_snapshot(&a.snapshot_world()).unwrap();
3451    ///
3452    /// let dt = Fix128::from_ratio(1, 60);
3453    /// a.step_n(3, dt);
3454    /// for _ in 0..3 {
3455    ///     b.step(dt);
3456    /// }
3457    /// assert_eq!(a.bodies[0].position, b.bodies[0].position);
3458    /// ```
3459    pub fn step_n(&mut self, n: usize, dt: Fix128) {
3460        for _ in 0..n {
3461            self.step(dt);
3462        }
3463    }
3464
3465    /// Phase 4 of [`Self::step`]: [`IslandManager::update_sleep`] over the bodies
3466    /// that are not parked, and for a parked body the same outcome without the
3467    /// evaluation (its velocity is the at-rest one its cache recorded as idle).
3468    fn update_sleep_states(&mut self) {
3469        if !self.park.active {
3470            self.stage_work.sleep_evaluated += self.bodies.len() as u64;
3471            self.islands.update_sleep(&self.bodies);
3472            return;
3473        }
3474        for k in 0..self.park.awake.len() {
3475            let i = self.park.awake[k];
3476            self.stage_work.sleep_evaluated += 1;
3477            self.islands.update_sleep_body(i, &self.bodies[i]);
3478        }
3479        for k in 0..self.park.parked_list.len() {
3480            let i = self.park.parked_list[k];
3481            if !self.park.parked[i] {
3482                continue; // woken during the step: evaluated above
3483            }
3484            self.stage_work.parked_sleep_updates += 1;
3485            self.islands
3486                .update_sleep_idle(i, self.bodies[i].is_static());
3487        }
3488    }
3489
3490    /// Decide which bodies are parked for this step (sleep skip, see
3491    /// [`StageWork`]) and bring the parked-body tree up to date.
3492    ///
3493    /// A body is parked when the step would change nothing about it but its
3494    /// `idle_frames`: it is asleep, not kinematic, not referenced by a joint or a
3495    /// distance constraint, and — for a dynamic body — `prev == position`,
3496    /// `prev_rotation == rotation`, a zero velocity, the angular velocity the
3497    /// derivation gives an unchanged rotation, an idle verdict on that velocity,
3498    /// and no static / SDF collider touching it. A contact with an awake body
3499    /// wakes it during the step like any sleeping body ([`ParkState::unpark`]).
3500    /// Nothing is parked while an SDF collider is attached to a body.
3501    fn park_begin(&mut self, dt: Fix128, substep_dt: Fix128) {
3502        self.park.active = false;
3503        let n = self.bodies.len();
3504        if !self.sleep_skip || self.broadphase != Broadphase::Bvh || n == 0 {
3505            if !self.park.proxies.is_empty() || self.park.key.is_some() {
3506                self.park.clear_counted(&mut self.stage_work);
3507            }
3508            return;
3509        }
3510        // A zero velocity must be idle, otherwise every sleeping body wakes this
3511        // step and there is nothing to skip.
3512        let cfg = self.islands.config;
3513        if !(Vec3Fix::ZERO.length() < cfg.linear_threshold) {
3514            self.park.clear_counted(&mut self.stage_work);
3515            return;
3516        }
3517        // A collider attached to a body moves with it during the step, after
3518        // this check has passed a parked body as clear of it: no parking while
3519        // such a collider is in the world.
3520        if self.sdf_colliders.iter().any(|c| c.body_index < n) {
3521            self.park.clear_counted(&mut self.stage_work);
3522            return;
3523        }
3524
3525        let key = ParkKey {
3526            substep_dt,
3527            sleep: cfg,
3528            generation: self.park_generation,
3529            sdf_radius: self.sdf_collision_radius,
3530            sdf: self
3531                .sdf_colliders
3532                .iter()
3533                .map(|c| {
3534                    (
3535                        c.position,
3536                        c.rotation,
3537                        c.scale,
3538                        c.body_index,
3539                        core::ptr::from_ref::<dyn crate::sdf_collider::SdfField>(c.field.as_ref())
3540                            .cast::<u8>() as usize,
3541                    )
3542                })
3543                .collect(),
3544        };
3545        // Fewer bodies than last time: indices may now name other bodies.
3546        if self.park.key.as_ref() != Some(&key) || n < self.park.parked.len() {
3547            self.park.clear_counted(&mut self.stage_work);
3548            self.park.key = Some(key);
3549        }
3550
3551        self.park.parked.resize(n, false);
3552        self.park.cache.resize(n, None);
3553        self.park.proxies.resize(n, None);
3554        self.park.constrained.clear();
3555        self.park.constrained.resize(n, false);
3556        for j in &self.joints {
3557            let (a, b) = j.bodies();
3558            for x in [a, b] {
3559                if x < n {
3560                    self.park.constrained[x] = true;
3561                }
3562            }
3563        }
3564        for c in &self.distance_constraints {
3565            for x in [c.body_a, c.body_b] {
3566                if x < n {
3567                    self.park.constrained[x] = true;
3568                }
3569            }
3570        }
3571
3572        self.park.frame_dt = dt;
3573        self.park.awake.clear();
3574        self.park.parked_list.clear();
3575        let inv_dt = Fix128::ONE / substep_dt;
3576        for i in 0..n {
3577            self.stage_work.sleep_scanned += 1;
3578            let (ok, recached) = self.park_check(i, inv_dt);
3579            self.park.parked[i] = ok;
3580            let radius = self.body_collision_radii.get(i).and_then(|r| *r);
3581            match (ok, radius) {
3582                (true, Some(r)) => {
3583                    self.park.parked_list.push(i);
3584                    if recached || self.park.proxies[i].is_none() {
3585                        self.park.remove_proxy(i, &mut self.stage_work);
3586                        let aabb = self.broadphase_box(i, r);
3587                        self.park.proxies[i] = Some(self.park.tree.insert(aabb, i as u32));
3588                        self.park.proxy_live += 1;
3589                        self.stage_work.tree_inserts += 1;
3590                    }
3591                }
3592                (true, None) => {
3593                    self.park.parked_list.push(i);
3594                    self.park.remove_proxy(i, &mut self.stage_work);
3595                }
3596                (false, _) => {
3597                    self.park.awake.push(i);
3598                    self.park.remove_proxy(i, &mut self.stage_work);
3599                }
3600            }
3601        }
3602        self.stage_work.parked = self.park.parked_list.len() as u64;
3603        self.park.active = true;
3604    }
3605
3606    /// `(parked, cache rewritten)` for body `i` (see [`Self::park_begin`]).
3607    fn park_check(&mut self, i: usize, inv_dt: Fix128) -> (bool, bool) {
3608        if !self.islands.is_sleeping(i) || self.park.constrained[i] {
3609            return (false, false);
3610        }
3611        let body = &self.bodies[i];
3612        let radius = self.body_collision_radii.get(i).and_then(|r| *r);
3613        match body.body_type {
3614            BodyType::Kinematic => (false, false),
3615            BodyType::Static => {
3616                if !body.is_static() {
3617                    return (false, false);
3618                }
3619                // A collider's box turns with the body; a sphere cube does not.
3620                let has_collider = self.body_colliders.get(i).is_some_and(Option::is_some);
3621                let c = ParkCache {
3622                    position: body.position,
3623                    rotation: body.rotation,
3624                    angular_velocity: Vec3Fix::ZERO,
3625                    radius,
3626                    static_kind: true,
3627                };
3628                match self.park.cache[i] {
3629                    Some(old)
3630                        if old.static_kind
3631                            && old.position == c.position
3632                            && old.radius == radius
3633                            && (old.rotation == c.rotation || !has_collider) =>
3634                    {
3635                        (true, false)
3636                    }
3637                    _ => {
3638                        self.park.cache[i] = Some(c);
3639                        (true, true)
3640                    }
3641                }
3642            }
3643            BodyType::Dynamic => {
3644                if body.prev_position != body.position
3645                    || body.prev_rotation != body.rotation
3646                    || body.velocity != Vec3Fix::ZERO
3647                {
3648                    return (false, false);
3649                }
3650                if let Some(c) = self.park.cache[i] {
3651                    if !c.static_kind
3652                        && c.position == body.position
3653                        && c.rotation == body.rotation
3654                        && c.radius == radius
3655                    {
3656                        return (body.angular_velocity == c.angular_velocity, false);
3657                    }
3658                }
3659                let omega = angular_from_rotations(body.rotation, body.rotation, inv_dt);
3660                if body.angular_velocity != omega
3661                    || !(omega.length() < self.islands.config.angular_threshold)
3662                    || !self.resolution_clear(i)
3663                {
3664                    self.park.cache[i] = None;
3665                    return (false, false);
3666                }
3667                self.park.cache[i] = Some(ParkCache {
3668                    position: body.position,
3669                    rotation: body.rotation,
3670                    angular_velocity: omega,
3671                    radius,
3672                    static_kind: false,
3673                });
3674                (true, true)
3675            }
3676        }
3677    }
3678
3679    /// No static or SDF collider would move body `i` where it is now (the
3680    /// resolution passes test every collider at the same position when none
3681    /// returns a contact).
3682    fn resolution_clear(&self, i: usize) -> bool {
3683        let body = &self.bodies[i];
3684        if body.is_static() || body.is_sensor {
3685            return true; // both resolution passes skip it
3686        }
3687        let radius = self
3688            .body_collision_radii
3689            .get(i)
3690            .and_then(|r| *r)
3691            .unwrap_or(self.sdf_collision_radius);
3692        for collider in &self.static_colliders {
3693            if collider.collide_sphere(body.position, radius).is_some() {
3694                return false;
3695            }
3696        }
3697        #[cfg(feature = "std")]
3698        {
3699            let collider = self.body_colliders.get(i).and_then(Option::as_ref);
3700            for sdf in &self.sdf_colliders {
3701                // Its own field never moves it (`resolve_sdf_collisions`).
3702                if sdf.body_index == i {
3703                    continue;
3704                }
3705                if sdf_contact_of(collider, body, self.sdf_collision_radius, sdf).is_some() {
3706                    return false;
3707                }
3708            }
3709        }
3710        true
3711    }
3712
3713    /// How many bodies a stage visits: the unparked ones while parking is in
3714    /// effect, otherwise all of them (pair with [`Self::stage_body`]).
3715    #[inline]
3716    fn stage_count(&self) -> usize {
3717        if self.park.active {
3718            self.park.awake.len()
3719        } else {
3720            self.bodies.len()
3721        }
3722    }
3723
3724    /// The `k`-th body a stage visits (see [`Self::stage_count`]).
3725    #[inline]
3726    fn stage_body(&self, k: usize) -> usize {
3727        if self.park.active {
3728            self.park.awake[k]
3729        } else {
3730            k
3731        }
3732    }
3733
3734    /// Number of bodies that are not parked (all of them without parking).
3735    #[cfg(feature = "parallel")]
3736    fn unparked_count(&self) -> u64 {
3737        self.stage_count() as u64
3738    }
3739
3740    /// Advances every [`BodyType::Kinematic`] body with a
3741    /// [`RigidBody::kinematic_target`] set, mirroring [`Self::integrate_positions`]'s
3742    /// `Kinematic` branch: velocity is derived from the position delta
3743    /// (`(target - position) / dt`), then position/rotation are snapped to
3744    /// the target. Runs once per full `dt` here (the `Tgs` path detects
3745    /// contacts once per tick, not once per sub-step, so there is no
3746    /// per-sub-step granularity to match) — called before collision
3747    /// detection so a moving kinematic body's contacts use its new
3748    /// position, same ordering as `step`'s per-sub-step kinematic advance
3749    /// relative to its per-sub-step contact handling.
3750    #[cfg(feature = "std")]
3751    fn advance_kinematic_targets_for_tgs(&mut self, dt: Fix128) {
3752        for body in &mut self.bodies {
3753            if body.body_type != BodyType::Kinematic {
3754                continue;
3755            }
3756            body.prev_position = body.position;
3757            body.prev_rotation = body.rotation;
3758            if let Some((target_pos, target_rot)) = body.kinematic_target {
3759                body.velocity = (target_pos - body.position) * (Fix128::ONE / dt);
3760                body.position = target_pos;
3761                body.rotation = target_rot;
3762            }
3763        }
3764    }
3765    /// Static colliders for the TGS path, with their positional correction
3766    /// carried into the velocities (see `step_tgs` Phase 3.2): a static
3767    /// contact pushes the body out and removes the velocity into the surface.
3768    #[cfg(feature = "std")]
3769    fn tgs_static_corrections(&mut self) {
3770        if !self.static_colliders.is_empty() {
3771            self.resolve_static_collisions(true);
3772        }
3773    }
3774
3775    /// The bodies the world's joints refer to, ascending and without
3776    /// repeats (indices past the body list are left out).
3777    #[cfg(feature = "std")]
3778    fn jointed_bodies(&self) -> Vec<usize> {
3779        let n = self.bodies.len();
3780        let mut out: Vec<usize> = self
3781            .joints
3782            .iter()
3783            .flat_map(|j| {
3784                let (a, b) = j.bodies();
3785                [a, b]
3786            })
3787            .filter(|&i| i < n)
3788            .collect();
3789        out.sort_unstable();
3790        out.dedup();
3791        out
3792    }
3793
3794    /// One TGS substep's joint solve: copies the jointed bodies' TGS state to
3795    /// the world bodies, applies the joint projection
3796    /// ([`Self::solve_joints_dispatch`], the call the XPBD substep makes) and
3797    /// writes the result back, adding each correction to the velocity as the
3798    /// XPBD velocity update would: `Δx / h` to the linear velocity and the
3799    /// angular velocity of the rotation change to the angular one. TGS keeps
3800    /// its velocities across substeps, so without the carry a corrected body
3801    /// would keep the velocity that took it off the constraint.
3802    #[cfg(feature = "std")]
3803    fn tgs_project_joints(
3804        &mut self,
3805        tgs_bodies: &mut [crate::solver_tgs_hooks_6dof_oriented::Body6DofOrientedState],
3806        jointed: &[usize],
3807        sub_dt: Fix128,
3808    ) {
3809        use crate::solver_tgs_backend::tgs_to_body;
3810        for &i in jointed {
3811            tgs_to_body(&tgs_bodies[i], &mut self.bodies[i]);
3812        }
3813        let before: Vec<(Vec3Fix, QuatFix)> = jointed
3814            .iter()
3815            .map(|&i| (self.bodies[i].position, self.bodies[i].rotation))
3816            .collect();
3817        self.solve_joints_dispatch(sub_dt);
3818        let inv_dt = Fix128::ONE / sub_dt;
3819        let mut overflow = false;
3820        for (&i, (p0, q0)) in jointed.iter().zip(before) {
3821            let body = &mut self.bodies[i];
3822            if body.is_dynamic() {
3823                if body.position != p0 {
3824                    match (body.position - p0).checked_scale(inv_dt) {
3825                        Some(dv) => body.velocity = body.velocity + dv,
3826                        None => overflow = true,
3827                    }
3828                }
3829                if body.rotation != q0 {
3830                    body.angular_velocity =
3831                        body.angular_velocity + angular_from_rotations(body.rotation, q0, inv_dt);
3832                }
3833            }
3834            let state = &mut tgs_bodies[i];
3835            state.position = [body.position.x, body.position.y, body.position.z];
3836            state.orientation = body.rotation;
3837            state.linear_velocity = [body.velocity.x, body.velocity.y, body.velocity.z];
3838            state.angular_velocity = [
3839                body.angular_velocity.x,
3840                body.angular_velocity.y,
3841                body.angular_velocity.z,
3842            ];
3843        }
3844        if overflow {
3845            self.note_rigid_overflow();
3846        }
3847    }
3848
3849    /// `SolverBackend::Tgs` body of [`Self::step`]. Mirrors `step`'s phase
3850    /// numbering so the two are easy to diff, but is a genuinely different
3851    /// algorithm: contacts are detected once per full `dt` (not re-detected
3852    /// every sub-step — the TGS family owns its own sub-stepping internally
3853    /// via [`crate::solver_tgs::tgs_step`]), and bodies are advanced by
3854    /// per-island impulse-based Gauss-Seidel instead of XPBD position
3855    /// projection. Joints, static colliders, contact filters, kinematic
3856    /// targets and SDF colliders are all handled — see [`SolverBackend`]'s
3857    /// notes for the joint solve and filter handling, Phase 3.2 below for the
3858    /// static colliders, and
3859    /// [`Self::advance_kinematic_targets_for_tgs`] /
3860    /// [`Self::resolve_sdf_collisions`] for the last two.
3861    #[cfg(feature = "std")]
3862    fn step_tgs(&mut self, dt: Fix128, frozen: &mut [bool]) {
3863        use crate::solver_tgs::{build_islands, DistanceRef};
3864        use crate::solver_tgs_backend::{body_to_tgs, contact_to_tgs, tgs_to_body};
3865        use crate::solver_tgs_hooks_6dof_oriented::Pgs6DofOrientedConfig;
3866        use crate::solver_tgs_hooks_6dof_oriented_scoped::solve_oriented_islands_serial;
3867
3868        // Phase 0: Event frame lifecycle (mirrors `step`; TGS detects contacts
3869        // once below instead of once per sub-step).
3870        self.events.begin_frame();
3871
3872        // Phase 0.5: Rebuild island connectivity from current joints (same
3873        // `IslandManager` bookkeeping `step` performs; the TGS islands used
3874        // for solving below are a separate, local structure built from the
3875        // same joint list via `build_islands`).
3876        self.islands.resize(self.bodies.len());
3877        self.islands.reset_unions();
3878        for j in &self.joints {
3879            let (a, b) = j.bodies();
3880            if a < self.bodies.len() && b < self.bodies.len() {
3881                self.islands.union(a, b);
3882            }
3883        }
3884
3885        // Phase 1: Apply force fields (identical call to `step`), then
3886        // advance kinematic targets before contacts are detected below.
3887        if !self.force_fields.is_empty() {
3888            apply_force_fields(&self.force_fields, &mut self.bodies, dt);
3889        }
3890        // Joint motors: once per tick with the full `dt`, like the force
3891        // fields (TGS owns its sub-stepping, see the method doc).
3892        self.apply_joint_motors(dt);
3893        self.advance_kinematic_targets_for_tgs(dt);
3894
3895        // Phase 2: Collision detection once for the whole tick (not
3896        // re-detected per sub-step — see the method doc).
3897        self.clear_contacts();
3898        self.detect_collisions();
3899
3900        // Phase 2.5: Resolve SDF collisions (implicit surface contacts),
3901        // mirroring `substep`'s Phase 1.5. `resolve_sdf_collisions` pushes
3902        // bodies directly out of SDF overlap via `body.position +=
3903        // normal*depth` — it does not go through the contact-constraint /
3904        // impulse pipeline at all, so it is correct regardless of which
3905        // `SolverBackend` subsequently integrates velocities, and is called
3906        // once here (not once per TGS inner sub-step) for the same reason
3907        // `detect_collisions` above is: TGS owns its own sub-stepping
3908        // internally and this path only detects/resolves once per full
3909        // `dt`, same as every other per-tick (not per-substep) phase here.
3910        #[cfg(feature = "std")]
3911        if !self.sdf_colliders.is_empty() {
3912            self.resolve_sdf_collisions();
3913        }
3914
3915        // Phase 2.7: Pre-solve hooks and contact modifiers, once over the
3916        // tick's contact set (the same pre-pass `substep` runs over each
3917        // substep's set). A modifier's result is written back to the
3918        // constraint and read by `contact_to_tgs` below; a vetoed contact is
3919        // left out of the TGS contact list.
3920        self.apply_contact_filters();
3921
3922        // Phase 3: Convert to the TGS body/contact representation, solve
3923        // every island, convert back.
3924        let n = self.bodies.len();
3925        let mut tgs_bodies: Vec<_> = self
3926            .bodies
3927            .iter()
3928            .enumerate()
3929            .map(|(i, b)| body_to_tgs(b, i as u64))
3930            .collect();
3931        // Warm-start keys name a contact / constraint by what it joins, not
3932        // by its position in a vector: the stable ids of its two bodies and
3933        // its ordinal among the entries that join the same pair this tick.
3934        self.sync_body_stable_ids();
3935        let ids = &self.body_stable_ids;
3936        // An out-of-range index (a hand-built constraint) gets a sentinel;
3937        // `build_islands` below rejects it before anything is solved.
3938        let id_of = |i: usize| ids.get(i).copied().unwrap_or(u64::MAX);
3939        let mut contact_ordinals = std::collections::BTreeMap::new();
3940        // Contacts vetoed by a hook or a modifier (`apply_contact_filters`
3941        // above) are not solved, as in the XPBD contact solve. The key is
3942        // taken before the veto is applied, so vetoing one contact of a pair
3943        // does not shift the ordinal (and so the warm-start entry) of the
3944        // pair's other contacts. A modified contact is converted from the
3945        // values the modifier wrote back. (Sensor pairs never reach
3946        // `contact_constraints`: detection reports them as triggers.)
3947        let discarded = &self.contact_discarded;
3948        let mut tgs_contacts: Vec<_> = self
3949            .contact_constraints
3950            .iter()
3951            .enumerate()
3952            .filter_map(|(i, c)| {
3953                // Orient every contact from the lower stable id to the higher
3954                // one. Detection orders a pair by index, and `swap_remove`
3955                // can flip the index order of two bodies whose ids did not
3956                // change; without this the same contact would come back
3957                // reversed (opposite normal and tangent basis) and miss.
3958                let (ia, ib) = (id_of(c.body_a), id_of(c.body_b));
3959                let flipped;
3960                let c = if ia > ib {
3961                    flipped = ContactConstraint {
3962                        body_a: c.body_b,
3963                        body_b: c.body_a,
3964                        contact: Contact {
3965                            depth: c.contact.depth,
3966                            normal: -c.contact.normal,
3967                            point_a: c.contact.point_b,
3968                            point_b: c.contact.point_a,
3969                        },
3970                        ..*c
3971                    };
3972                    &flipped
3973                } else {
3974                    c
3975                };
3976                let key = tgs_cache_key(
3977                    TGS_KEY_CONTACT,
3978                    ia.min(ib),
3979                    ia.max(ib),
3980                    &mut contact_ordinals,
3981                );
3982                if discarded.get(i).copied().unwrap_or(false) {
3983                    return None;
3984                }
3985                Some(contact_to_tgs(c, &self.bodies, key))
3986            })
3987            .collect();
3988        let joint_refs: Vec<DistanceRef<'_>> = self
3989            .distance_constraints
3990            .iter()
3991            .map(|joint| DistanceRef { joint })
3992            .collect();
3993        // Joints share `self.tgs_impulse_cache` with contacts; the kind tag
3994        // in the key keeps the two apart.
3995        let mut joint_ordinals = std::collections::BTreeMap::new();
3996        let mut tgs_joints: Vec<crate::solver_tgs_hooks_6dof_oriented::JointOriented> = self
3997            .distance_constraints
3998            .iter()
3999            .map(|j| crate::solver_tgs_hooks_6dof_oriented::JointOriented {
4000                body_a: j.body_a,
4001                body_b: j.body_b,
4002                stable_id: tgs_cache_key(
4003                    TGS_KEY_DISTANCE,
4004                    id_of(j.body_a),
4005                    id_of(j.body_b),
4006                    &mut joint_ordinals,
4007                ),
4008                local_anchor_a: [j.local_anchor_a.x, j.local_anchor_a.y, j.local_anchor_a.z],
4009                local_anchor_b: [j.local_anchor_b.x, j.local_anchor_b.y, j.local_anchor_b.z],
4010                target_distance: j.target_distance,
4011                accum: Fix128::ZERO,
4012            })
4013            .collect();
4014
4015        // `build_islands` only errs when a contact/joint references a body
4016        // index `>= n`; every index here comes from this same world's own
4017        // `detect_collisions` / `add_distance_constraint`, both of which are
4018        // bounds-checked against `self.bodies.len()` at insertion time, so
4019        // this is unreachable in practice. Treat it as "nothing to solve"
4020        // rather than panicking an FFI host.
4021        let islands = build_islands(&tgs_bodies, &tgs_contacts, &joint_refs).unwrap_or_default();
4022
4023        let cfg = Pgs6DofOrientedConfig {
4024            gravity: [
4025                self.config.gravity.x,
4026                self.config.gravity.y,
4027                self.config.gravity.z,
4028            ],
4029            ..Pgs6DofOrientedConfig::default()
4030        };
4031        let tgs_cfg = crate::solver_tgs::TgsConfig {
4032            substeps: self.config.substeps.max(1) as u32,
4033            velocity_iters: self.config.iterations.max(1) as u32,
4034            ..crate::solver_tgs::TgsConfig::default()
4035        };
4036        // Participants need a substep loop of their own: with none registered
4037        // the joint-free path below is the single call it always was. The
4038        // loop splits `dt` exactly as that call does, so a world whose
4039        // participants stage nothing gives the same bits either way.
4040        let with_participants = !frozen.is_empty();
4041        if self.joints.is_empty() && !with_participants {
4042            solve_oriented_islands_serial(
4043                &mut tgs_bodies,
4044                &mut tgs_contacts,
4045                &mut tgs_joints,
4046                &islands,
4047                &mut self.tgs_impulse_cache,
4048                cfg,
4049                &tgs_cfg,
4050                dt,
4051            );
4052        } else {
4053            // The world's joints (`Joint`: ball, hinge, fixed, slider, spring,
4054            // D6, cone-twist) are solved inside the substep loop: after each
4055            // substep's impulse solve and position advance, the joint
4056            // projection the XPBD substep runs is applied to the jointed
4057            // bodies and carried into their velocities. The loop splits `dt`
4058            // exactly as `tgs_step` does, so each island sees the same
4059            // substep sequence it would in a single call.
4060            let one_substep = crate::solver_tgs::TgsConfig {
4061                substeps: 1,
4062                ..tgs_cfg
4063            };
4064            //
4065            // The width is the one `tgs_step` uses, with or without
4066            // participants, and participants are handed the same `h`
4067            // ([`Self::participant_substep_width`]): a participant that
4068            // stages nothing then leaves the bodies bit for bit as in a world
4069            // without it, for every substep count.
4070            let substeps = self.config.substeps;
4071            let sub_dt = dt * Fix128::from_f32(1.0 / tgs_cfg.substeps as f32);
4072            let jointed = self.jointed_bodies();
4073            for s in 0..tgs_cfg.substeps {
4074                let overflow_at_start = if with_participants {
4075                    // Participants read the bodies as they are at the start of
4076                    // this substep, and their forces change the velocities
4077                    // the TGS solve starts from.
4078                    for (body, state) in self.bodies.iter_mut().zip(tgs_bodies.iter()) {
4079                        tgs_to_body(state, body);
4080                    }
4081                    let flag =
4082                        self.participants_begin_substep(s as usize, substeps, sub_dt, frozen);
4083                    for (state, body) in tgs_bodies.iter_mut().zip(self.bodies.iter()) {
4084                        state.linear_velocity = [body.velocity.x, body.velocity.y, body.velocity.z];
4085                        state.angular_velocity = [
4086                            body.angular_velocity.x,
4087                            body.angular_velocity.y,
4088                            body.angular_velocity.z,
4089                        ];
4090                    }
4091                    flag
4092                } else {
4093                    false
4094                };
4095                solve_oriented_islands_serial(
4096                    &mut tgs_bodies,
4097                    &mut tgs_contacts,
4098                    &mut tgs_joints,
4099                    &islands,
4100                    &mut self.tgs_impulse_cache,
4101                    cfg,
4102                    &one_substep,
4103                    sub_dt,
4104                );
4105                if !self.joints.is_empty() {
4106                    self.tgs_project_joints(&mut tgs_bodies, &jointed, sub_dt);
4107                }
4108                if with_participants {
4109                    // Keep `self.bodies` in step with the TGS state after every
4110                    // substep. The end-of-substep check reads only the
4111                    // overflow flag, not the bodies; the next substep copies
4112                    // the bodies out again before its participants run, and
4113                    // the loop end copies them once more.
4114                    for (body, state) in self.bodies.iter_mut().zip(tgs_bodies.iter()) {
4115                        tgs_to_body(state, body);
4116                    }
4117                    // この substep の範囲外を、参加者が読む前に world の印へ畳む
4118                    if tgs_bodies.iter().any(|s| s.overflow) {
4119                        self.note_rigid_overflow();
4120                    }
4121                    self.participants_end_substep(overflow_at_start, frozen);
4122                }
4123            }
4124        }
4125        // Every contact for this tick has now been visited (each contact's
4126        // warm-start entry was `take`n in `begin_substep` and re-`set` in
4127        // `end_substep` for every sub-step above) — evict any cache entry
4128        // that was not touched this tick, per `ImpulseCache::sweep`'s own
4129        // documented contract. Without this, a contact that stops recurring
4130        // (bodies separate, then the same body pair contacts again later)
4131        // would resurrect the old, stale impulse as a warm-start "hit"
4132        // instead of correctly starting from zero.
4133        self.tgs_impulse_cache.sweep();
4134        debug_assert_eq!(tgs_bodies.len(), n);
4135        for (body, state) in self.bodies.iter_mut().zip(tgs_bodies.iter()) {
4136            tgs_to_body(state, body);
4137        }
4138        // TGS は複製 `tgs_bodies` の上で積分するので、範囲外は複製の各 body の
4139        // `overflow` に溜まる (その body の位置は据え置き、XPBD の積分と同じ扱い)
4140        // 戻す時に world の sticky flag へ畳み込む
4141        if tgs_bodies.iter().any(|s| s.overflow) {
4142            self.note_rigid_overflow();
4143        }
4144
4145        // Phase 3.2: The world's static colliders. A static contact is a
4146        // position-level correction (the call `substep` makes); TGS owns
4147        // velocities, so it also removes the velocity into the surface.
4148        // Without that a body resting on a plane keeps the velocity gravity
4149        // gave it and sinks again next tick. (The joints were solved inside
4150        // the substep loop above.)
4151        self.tgs_static_corrections();
4152
4153        // Phase 3.5: Frame-level damping (identical call to `step`; TGS's own
4154        // per-substep gravity/impulse integration does not apply the global
4155        // `damping` factor, so this still needs to run here).
4156        self.apply_frame_damping();
4157
4158        // Phase 4: Update sleeping (identical call to `step`).
4159        self.islands.update_sleep(&self.bodies);
4160
4161        // Phase 4.5: Leave every SDF collider attached to a body at that
4162        // body's final pose, which is what the queries between steps
4163        // (`sdf_contacts`, `sdf_ccd_hits`, the shape casts) read.
4164        self.sync_sdf_colliders();
4165
4166        // Phase 5: End event frame (identical call to `step`).
4167        self.events.end_frame();
4168    }
4169
4170    /// Warm-start hit-rate diagnostics for the [`SolverBackend::Tgs`] path's
4171    /// per-contact impulse cache (see [`Self::reset_tgs_cache_stats`] to
4172    /// isolate a specific measurement window, e.g. after warm-up frames).
4173    ///
4174    /// Always `hits: 0, misses: 0` while `config.solver_backend` is
4175    /// `Xpbd` — the internal `step_tgs` (the only writer of this cache)
4176    /// is never called on that path.
4177    #[cfg(feature = "std")]
4178    #[must_use]
4179    pub fn tgs_cache_stats(&self) -> TgsCacheStats {
4180        self.tgs_impulse_cache.stats().into()
4181    }
4182
4183    /// Clears the [`Self::tgs_cache_stats`] hit/miss counters without
4184    /// touching the cached impulses themselves — the same contract as the
4185    /// internal `ImpulseCache::reset_stats`. Useful when a host wants to
4186    /// measure hit rate over a specific window instead of the cache's
4187    /// entire lifetime (e.g. after a scene's warm-up frames).
4188    #[cfg(feature = "std")]
4189    pub fn reset_tgs_cache_stats(&mut self) {
4190        self.tgs_impulse_cache.reset_stats();
4191    }
4192
4193    /// Step with batched constraint solving
4194    ///
4195    /// When `parallel` feature is enabled, processes independent constraint
4196    /// batches in parallel using Rayon. Includes the same integrated pipeline
4197    /// as `step()`: force fields, collision detection, joints, events, sleeping.
4198    ///
4199    /// # Determinism contract
4200    ///
4201    /// `step_parallel` is deterministic — the same world stepped twice, on any
4202    /// number of threads and on every platform, produces the same bits (graph
4203    /// colouring is deterministic and constraints inside a batch touch
4204    /// disjoint bodies). It is **not** bit-identical to [`Self::step`] once two
4205    /// constraints share a body: the batches are a different Gauss–Seidel
4206    /// ordering than constraint index order, and Gauss–Seidel is order
4207    /// dependent. Lockstep / rollback peers must therefore all use the same
4208    /// path (`fuzz/fuzz_targets/fuzz_step_parity.rs` checks both properties).
4209    ///
4210    /// # Solver backend
4211    ///
4212    /// `config.solver_backend` is not consulted: this path always runs the
4213    /// XPBD substep loop, so with [`SolverBackend::Tgs`] it gives the same
4214    /// bits as with [`SolverBackend::Xpbd`]. A batched TGS path does not exist
4215    /// yet; use [`Self::step`] for TGS.
4216    ///
4217    /// # Participants
4218    ///
4219    /// `step_parallel` is [`Self::try_step_parallel`] with the result dropped.
4220    #[cfg(feature = "parallel")]
4221    pub fn step_parallel(&mut self, dt: Fix128) {
4222        let _ = self.try_step_parallel(dt);
4223    }
4224
4225    /// The step body behind [`Self::try_step_parallel`] (checks already done,
4226    /// `dt > 0`).
4227    #[cfg(feature = "parallel")]
4228    fn run_step_parallel(&mut self, dt: Fix128) {
4229        self.make_body_rotations_unit();
4230        self.wake_on_sdf_shape_change();
4231        self.begin_sdf_contact_log();
4232        let mut frozen = self.participant_flags();
4233
4234        // Phase 0: Event frame lifecycle (contacts are cleared and re-detected
4235        // in every substep since 1.2.0, see `substep`)
4236        self.events.begin_frame();
4237
4238        // Phase 0.5: Rebuild island connectivity from current joints
4239        self.islands.resize(self.bodies.len());
4240        self.islands.reset_unions();
4241        for j in &self.joints {
4242            let (a, b) = j.bodies();
4243            if a < self.bodies.len() && b < self.bodies.len() {
4244                self.islands.union(a, b);
4245            }
4246        }
4247
4248        // Phase 1: Apply force fields
4249        if !self.force_fields.is_empty() {
4250            apply_force_fields(&self.force_fields, &mut self.bodies, dt);
4251        }
4252
4253        // Phase 2 (collision detection) is per substep since 1.2.0, see `step`.
4254
4255        // Phase 3: Substep loop (batches are rebuilt inside each substep after
4256        // detection, `solve_constraints_batched` re-colours on dirty)
4257        let substep_dt = dt / Fix128::from_int(self.config.substeps as i64);
4258        let n = self.config.substeps;
4259        for i in 0..n {
4260            self.kinematic_substeps_left = n - i;
4261            self.sdf_contact_substep = i;
4262            let overflow_at_start = self.participants_begin_substep(i, n, substep_dt, &mut frozen);
4263            self.substep_batched(substep_dt);
4264            self.participants_end_substep(overflow_at_start, &frozen);
4265        }
4266        self.kinematic_substeps_left = 0;
4267        self.sdf_contact_substep = 0;
4268
4269        // Phase 3.5: Frame-level damping (1.2.0, was per substep)
4270        self.apply_frame_damping();
4271
4272        // Phase 4: Update sleeping
4273        self.islands.update_sleep(&self.bodies);
4274
4275        // Phase 4.5: Leave every SDF collider attached to a body at that
4276        // body's final pose, which is what the queries between steps
4277        // (`sdf_contacts`, `sdf_ccd_hits`, the shape casts) read.
4278        self.sync_sdf_colliders();
4279
4280        // Phase 5: End event frame
4281        self.events.end_frame();
4282    }
4283
4284    /// Single substep
4285    fn substep(&mut self, dt: Fix128) {
4286        // 0. Joint motors act as external forces / torques of this substep.
4287        self.apply_joint_motors(dt);
4288        self.integrate_positions(dt);
4289        self.reset_lambdas();
4290
4291        // 1.1. Continuous collision of fast bodies (`world_ccd`, off by
4292        //      default: nothing runs while it is off).
4293        let ccd_hits = if self.ccd.is_enabled() {
4294            self.ccd_sweep()
4295        } else {
4296            Vec::new()
4297        };
4298
4299        // 1.2. Collision detection on the predicted positions (Small Steps).
4300        //      Contacts live for exactly one substep.
4301        self.clear_contacts();
4302        self.detect_collisions();
4303        if !ccd_hits.is_empty() {
4304            self.ccd_add_contacts(&ccd_hits);
4305        }
4306
4307        // 1.5. Resolve SDF collisions (implicit surface contacts)
4308        #[cfg(feature = "std")]
4309        if !self.sdf_colliders.is_empty() {
4310            self.resolve_sdf_collisions();
4311        }
4312        if !self.static_colliders.is_empty() {
4313            self.resolve_static_collisions(false);
4314        }
4315
4316        // 2. Solve constraints (sequential). Hooks and modifiers run once per
4317        //    substep, ahead of the iteration loop.
4318        #[cfg(feature = "std")]
4319        self.apply_contact_filters();
4320        for _ in 0..self.config.iterations {
4321            self.solve_distance_constraints(dt);
4322            self.solve_contact_constraints(dt);
4323        }
4324
4325        // 3. Solve joint constraints (v0.12.0: auto-routes through
4326        //    installed bridge if any, otherwise falls back to CPU
4327        //    `solve_joints`).
4328        self.solve_joints_dispatch(dt);
4329
4330        self.update_velocities(dt);
4331    }
4332
4333    /// Single substep with batched constraint solving
4334    #[cfg(feature = "parallel")]
4335    fn substep_batched(&mut self, dt: Fix128) {
4336        self.apply_joint_motors(dt);
4337        self.integrate_positions(dt);
4338        self.reset_lambdas();
4339
4340        // 1.1. Continuous collision of fast bodies, as in `substep`.
4341        let ccd_hits = if self.ccd.is_enabled() {
4342            self.ccd_sweep()
4343        } else {
4344            Vec::new()
4345        };
4346
4347        // 1.2. Collision detection on the predicted positions (Small Steps).
4348        self.clear_contacts();
4349        self.detect_collisions();
4350        if !ccd_hits.is_empty() {
4351            self.ccd_add_contacts(&ccd_hits);
4352        }
4353        self.rebuild_batches();
4354
4355        // 1.5. Resolve SDF collisions
4356        #[cfg(feature = "std")]
4357        if !self.sdf_colliders.is_empty() {
4358            self.resolve_sdf_collisions();
4359        }
4360        if !self.static_colliders.is_empty() {
4361            self.resolve_static_collisions(false);
4362        }
4363
4364        // 2. Solve constraints (batched). Hooks and modifiers run once per
4365        //    substep, ahead of the iteration loop, as in `substep`.
4366        #[cfg(feature = "std")]
4367        self.apply_contact_filters();
4368        for _ in 0..self.config.iterations {
4369            self.solve_constraints_batched(dt);
4370        }
4371
4372        // 3. Solve joint constraints (v0.12.0: auto-routes through
4373        //    installed bridge if any, otherwise falls back to CPU
4374        //    `solve_joints`).
4375        self.solve_joints_dispatch(dt);
4376
4377        self.update_velocities(dt);
4378    }
4379
4380    /// Integrate positions (shared between sequential and batched)
4381    ///
4382    /// # Claims
4383    ///
4384    /// - A kinematic body with a `kinematic_target` closes `1 / k` of the
4385    ///   remaining gap to the target in each substep, `k` being the substeps
4386    ///   left in the frame including this one, so it reaches the target
4387    ///   exactly in the last substep.
4388    /// - The velocity derived after every substep of that frame equals
4389    ///   `(target - start) / frame_dt` in m/s, so it is that value after
4390    ///   `step`, up to fixed-point rounding of the division by `k`.
4391    /// - A call outside a `step` substep loop (`k = 0`) snaps to the target
4392    ///   in one call, as before.
4393    /// - A kinematic body without a target keeps its pose and velocity.
4394    ///
4395    /// Sleeping dynamic bodies are skipped: only `prev_position/prev_rotation`
4396    /// are saved so that `update_velocities` derives zero velocity.
4397    #[inline]
4398    fn integrate_positions(&mut self, dt: Fix128) {
4399        // ⚠️ 範囲外の積を集める受け皿 — parallel branch の closure から
4400        // `self` に書けないので Atomic で受けて関数末尾で flag に畳み込む
4401        // (bool の OR は結合的・可換なので rayon の実行順に依存しない)
4402        let overflow = core::sync::atomic::AtomicBool::new(false);
4403        // Free rotation of this substep per body (`None`: no gyroscopic
4404        // response, the rotation is predicted from `ω` as before); read back
4405        // by `update_velocities` in the same substep.
4406        let mut free = core::mem::take(&mut self.free_rotation);
4407        free.clear();
4408        free.resize(self.bodies.len(), None);
4409        let mut predicted = core::mem::take(&mut self.predicted_pose);
4410        predicted.clear();
4411        predicted.resize(self.bodies.len(), None);
4412        #[cfg(feature = "parallel")]
4413        {
4414            let gravity = self.config.gravity;
4415            let left = self.kinematic_substeps_left;
4416            let sleep_data = &self.islands.sleep_data;
4417            let parked: &[bool] = if self.park.active {
4418                &self.park.parked
4419            } else {
4420                &[]
4421            };
4422            self.stage_work.integrated += self.unparked_count();
4423            self.bodies
4424                .par_iter_mut()
4425                .zip(free.par_iter_mut())
4426                .zip(predicted.par_iter_mut())
4427                .enumerate()
4428                .for_each(|(i, ((body, free), predicted))| {
4429                    if parked.get(i).copied().unwrap_or(false) {
4430                        return;
4431                    }
4432                    match body.body_type {
4433                        BodyType::Static => return,
4434                        BodyType::Kinematic => {
4435                            body.prev_position = body.position;
4436                            body.prev_rotation = body.rotation;
4437                            if let Some((target_pos, target_rot)) = body.kinematic_target {
4438                                let (p, r) = kinematic_substep_pose(
4439                                    body.position,
4440                                    body.rotation,
4441                                    target_pos,
4442                                    target_rot,
4443                                    left,
4444                                );
4445                                body.velocity = (p - body.position) * (Fix128::ONE / dt);
4446                                body.position = p;
4447                                body.rotation = r;
4448                            }
4449                            return;
4450                        }
4451                        BodyType::Dynamic => {}
4452                    }
4453
4454                    // Skip sleeping bodies (preserve prev for zero-velocity derivation)
4455                    if sleep_data
4456                        .get(i)
4457                        .is_some_and(super::sleeping::SleepData::is_sleeping)
4458                    {
4459                        body.prev_position = body.position;
4460                        body.prev_rotation = body.rotation;
4461                        return;
4462                    }
4463
4464                    // Store previous state
4465                    body.prev_position = body.position;
4466                    body.prev_rotation = body.rotation;
4467
4468                    // Apply gravity (with per-body scale)
4469                    body.velocity = body.velocity + gravity * body.gravity_scale * dt;
4470
4471                    // Damping is applied once per frame in `apply_frame_damping`
4472                    // (1.2.0). Applying it here, per substep, made the terminal
4473                    // velocity depend on `substeps` (v_inf = g*h*d/(1-d)).
4474
4475                    // Predict position
4476                    // ⚠️ parallel branch でも同じ検出を行う — 片方だけだと
4477                    // `--features parallel` で guard が silent に消える
4478                    // (closure から `self` に書けないので bool を reduce する)
4479                    // 位置の加算も検査する (`|x| ≥ 2⁶³` で反対側の端へ wrap するため)
4480                    match body
4481                        .velocity
4482                        .checked_scale(dt)
4483                        .and_then(|d| body.position.checked_add(d))
4484                    {
4485                        Some(p) => body.position = p,
4486                        None => overflow.store(true, core::sync::atomic::Ordering::Relaxed),
4487                    }
4488
4489                    // Predict rotation, with the gyroscopic term ω × Iω
4490                    let mut spin_overflow = false;
4491                    *free = predict_rotation(body, dt, &mut spin_overflow);
4492                    if spin_overflow {
4493                        overflow.store(true, core::sync::atomic::Ordering::Relaxed);
4494                    }
4495
4496                    // The predicted pose, against which `update_velocities`
4497                    // measures the solve's correction
4498                    *predicted = Some((body.position, body.rotation));
4499                });
4500            self.free_rotation = free;
4501            self.predicted_pose = predicted;
4502        }
4503
4504        #[cfg(not(feature = "parallel"))]
4505        {
4506            let count = self.stage_count();
4507            for k in 0..count {
4508                let i = self.stage_body(k);
4509                self.stage_work.integrated += 1;
4510                match self.bodies[i].body_type {
4511                    BodyType::Static => continue,
4512                    BodyType::Kinematic => {
4513                        self.bodies[i].prev_position = self.bodies[i].position;
4514                        self.bodies[i].prev_rotation = self.bodies[i].rotation;
4515                        if let Some((target_pos, target_rot)) = self.bodies[i].kinematic_target {
4516                            let (p, r) = kinematic_substep_pose(
4517                                self.bodies[i].position,
4518                                self.bodies[i].rotation,
4519                                target_pos,
4520                                target_rot,
4521                                self.kinematic_substeps_left,
4522                            );
4523                            self.bodies[i].velocity =
4524                                (p - self.bodies[i].position) * (Fix128::ONE / dt);
4525                            self.bodies[i].position = p;
4526                            self.bodies[i].rotation = r;
4527                        }
4528                        continue;
4529                    }
4530                    BodyType::Dynamic => {}
4531                }
4532
4533                // Skip sleeping bodies (preserve prev for zero-velocity derivation)
4534                if self.islands.is_sleeping(i) {
4535                    self.bodies[i].prev_position = self.bodies[i].position;
4536                    self.bodies[i].prev_rotation = self.bodies[i].rotation;
4537                    continue;
4538                }
4539
4540                // Store previous state
4541                self.bodies[i].prev_position = self.bodies[i].position;
4542                self.bodies[i].prev_rotation = self.bodies[i].rotation;
4543
4544                // Apply gravity (with per-body scale)
4545                let grav = self.config.gravity * self.bodies[i].gravity_scale * dt;
4546                self.bodies[i].velocity = self.bodies[i].velocity + grav;
4547
4548                // Damping: once per frame in `apply_frame_damping` (see the
4549                // `parallel` branch above for the rationale).
4550
4551                // Predict position
4552                // ⚠️ `checked_scale` で範囲外を検出する (WM-01 / B-12)
4553                // 範囲外なら **位置を動かさず** flag を立てる — 0 加算されて
4554                // 「原点で静止した自己整合な状態」に落ちるのを表に出すため
4555                // 位置の加算も検査する (`|x| ≥ 2⁶³` で反対側の端へ wrap するため)
4556                let body = &mut self.bodies[i];
4557                match body
4558                    .velocity
4559                    .checked_scale(dt)
4560                    .and_then(|d| body.position.checked_add(d))
4561                {
4562                    Some(p) => body.position = p,
4563                    None => overflow.store(true, core::sync::atomic::Ordering::Relaxed),
4564                }
4565
4566                // Predict rotation, with the gyroscopic term ω × Iω
4567                let mut spin_overflow = false;
4568                free[i] = predict_rotation(&mut self.bodies[i], dt, &mut spin_overflow);
4569                if spin_overflow {
4570                    overflow.store(true, core::sync::atomic::Ordering::Relaxed);
4571                }
4572
4573                // The predicted pose, against which `update_velocities`
4574                // measures the solve's correction
4575                predicted[i] = Some((self.bodies[i].position, self.bodies[i].rotation));
4576            }
4577            self.free_rotation = free;
4578            self.predicted_pose = predicted;
4579        }
4580
4581        // ⚠️ 受け皿を sticky flag に畳み込む (1 度立ったら落ちない)
4582        if overflow.load(core::sync::atomic::Ordering::Relaxed) {
4583            self.note_rigid_overflow();
4584        }
4585    }
4586
4587    /// Reset the XPBD Lagrange multipliers of the distance constraints at the
4588    /// start of a substep (Macklin 2016, Algorithm 1: `lambda = 0`).
4589    ///
4590    /// Within the substep `solve_distance_constraints` / `solve_distance_pair`
4591    /// accumulate `dlambda` into `cached_lambda`. Carrying a fraction of the
4592    /// previous substep's `lambda` over (the pre-1.2.0 "warm start") is not
4593    /// valid for a position-based multiplier: the carried value is treated as
4594    /// force already applied this substep although it was not, which inflates
4595    /// the steady-state extension of compliant constraints
4596    /// (`C = mg/k * (1 + wsf / (1 - wsf))`, 6.7x at `wsf = 0.85`). For rigid
4597    /// constraints (`compliance == 0`) `lambda` never enters the correction, so
4598    /// their behaviour is unchanged. `warm_start_factor` now only affects the
4599    /// contact solver (byte-exact parity contract with `GpuSolverBridge`
4600    /// implementations, see `solve_contact_pair`).
4601    fn reset_lambdas(&mut self) {
4602        for c in &mut self.distance_constraints {
4603            c.cached_lambda = Fix128::ZERO;
4604        }
4605    }
4606
4607    /// Apply global and per-body velocity damping **once per frame**.
4608    ///
4609    /// `SolverConfig::damping` / `RigidBody::linear_damping` /
4610    /// `RigidBody::angular_damping` are velocity-retention factors per
4611    /// `step()` call. Before 1.2.0 they were multiplied in every substep, so
4612    /// the default `substeps: 8, damping: 0.99` gave every body a terminal
4613    /// velocity of `g * h * d / (1 - d)` with `h = dt / substeps` — about
4614    /// 2 m/s under default gravity — and doubling `substeps` halved the fall
4615    /// speed. A precision parameter must not change the physics, so damping
4616    /// now runs after the substep loop, on the velocities derived by the last
4617    /// `update_velocities`. Static / kinematic / sleeping bodies are skipped.
4618    fn apply_frame_damping(&mut self) {
4619        let damping = self.config.damping;
4620        let count = self.stage_count();
4621        for k in 0..count {
4622            let i = self.stage_body(k);
4623            self.stage_work.damping_bodies += 1;
4624            if self.bodies[i].body_type != BodyType::Dynamic || self.islands.is_sleeping(i) {
4625                continue;
4626            }
4627            let body = &mut self.bodies[i];
4628            body.velocity = body.velocity * damping * body.linear_damping;
4629            body.angular_velocity = body.angular_velocity * damping * body.angular_damping;
4630        }
4631    }
4632
4633    /// Update velocities from position changes, then apply restitution and friction
4634    /// for active contact constraints.
4635    #[inline]
4636    fn update_velocities(&mut self, dt: Fix128) {
4637        let inv_dt = Fix128::ONE / dt;
4638        // ⚠️ 範囲外の積を集める受け皿 — parallel branch の closure から
4639        // `self` に書けないので Atomic で受けて関数末尾で flag に畳み込む
4640        // (bool の OR は結合的・可換なので rayon の実行順に依存しない)
4641        let overflow = core::sync::atomic::AtomicBool::new(false);
4642
4643        // --- Phase 0: Record the pre-solve normal velocity v̄_n per contact ---
4644        // `velocity` still holds the substep's predicted velocity (gravity
4645        // applied in `integrate_positions`, untouched by the position solve),
4646        // so this is the approach velocity before any constraint acted.
4647        self.contact_pre_vn.clear();
4648        for constraint in &self.contact_constraints {
4649            let relative =
4650                self.bodies[constraint.body_a].velocity - self.bodies[constraint.body_b].velocity;
4651            self.contact_pre_vn
4652                .push(relative.dot(constraint.contact.normal));
4653        }
4654
4655        // --- Phase 1: Derive velocities from position/rotation changes ---
4656        #[cfg(feature = "parallel")]
4657        {
4658            let parked: &[bool] = if self.park.active {
4659                &self.park.parked
4660            } else {
4661                &[]
4662            };
4663            self.stage_work.velocity_bodies += self.unparked_count();
4664            let free: &[Option<crate::gyroscopic::FreeRotation>] = &self.free_rotation;
4665            let predicted: &[Option<(Vec3Fix, QuatFix)>] = &self.predicted_pose;
4666            self.bodies
4667                .par_iter_mut()
4668                .enumerate()
4669                .for_each(|(i, body)| {
4670                    if body.body_type == BodyType::Static || parked.get(i).copied().unwrap_or(false)
4671                    {
4672                        return;
4673                    }
4674                    // ⚠️ 速度導出も範囲外を検出する (WM-01 経路 2)
4675                    // 範囲外なら速度を更新しない (0 に上書きされるのを防ぐ)
4676                    let pose = predicted.get(i).copied().flatten();
4677                    if !derive_linear_velocity(body, pose.map(|(x, _)| x), inv_dt) {
4678                        overflow.store(true, core::sync::atomic::Ordering::Relaxed);
4679                    }
4680                    // Angular velocity from rotation change:
4681                    // delta_q = rotation * prev_rotation^-1
4682                    // angular_velocity = 2 * delta_q.xyz / dt  (when delta_q.w > 0)
4683                    // (split bodies: the split's ω plus the solve's change)
4684                    let f = free.get(i).copied().flatten();
4685                    body.angular_velocity =
4686                        derived_angular_velocity(body, f, pose.map(|(_, q)| q), inv_dt);
4687                });
4688        }
4689
4690        #[cfg(not(feature = "parallel"))]
4691        {
4692            let count = self.stage_count();
4693            for k in 0..count {
4694                let i = self.stage_body(k);
4695                self.stage_work.velocity_bodies += 1;
4696                let body = &mut self.bodies[i];
4697                if body.body_type == BodyType::Static {
4698                    continue;
4699                }
4700                // ⚠️ 速度導出も範囲外を検出する (WM-01 経路 2)
4701                // 範囲外なら速度を更新しない (0 に上書きされるのを防ぐ)
4702                let pose = self.predicted_pose.get(i).copied().flatten();
4703                if !derive_linear_velocity(body, pose.map(|(x, _)| x), inv_dt) {
4704                    overflow.store(true, core::sync::atomic::Ordering::Relaxed);
4705                }
4706                // Angular velocity from rotation change (split bodies: the
4707                // split's ω plus the solve's change)
4708                let f = self.free_rotation.get(i).copied().flatten();
4709                let body = &mut self.bodies[i];
4710                body.angular_velocity =
4711                    derived_angular_velocity(body, f, pose.map(|(_, q)| q), inv_dt);
4712            }
4713        }
4714
4715        // --- Phase 2: Apply restitution and friction at contacts ---
4716        // Restitution threshold of Müller et al. 2020 (below): an approach
4717        // slower than `2 |g| h` is a resting contact and gets `e = 0`, so
4718        // resting bodies do not bounce on the gravity of one substep
4719        // (the same rule as the 2D solver).
4720        let restitution_threshold = self.config.gravity.length() * dt * Fix128::from_int(2);
4721        let num_contacts = self.contact_constraints.len();
4722        for i in 0..num_contacts {
4723            let constraint = self.contact_constraints[i];
4724            let body_a = self.bodies[constraint.body_a];
4725            let body_b = self.bodies[constraint.body_b];
4726
4727            if body_a.is_sensor || body_b.is_sensor {
4728                continue;
4729            }
4730
4731            // A contact the substep's hook / modifier pre-pass discarded gets
4732            // no velocity response either (the position pass skipped it).
4733            // Kept contacts read the friction / restitution the pre-pass
4734            // wrote back, so a modifier's values apply here too.
4735            #[cfg(feature = "std")]
4736            if self.contact_discarded.get(i).copied().unwrap_or(false) {
4737                continue;
4738            }
4739
4740            let w_sum = body_a.inv_mass + body_b.inv_mass;
4741            if w_sum < W_SUM_EPSILON {
4742                continue;
4743            }
4744
4745            let n = constraint.contact.normal;
4746            let relative_vel = body_a.velocity - body_b.velocity;
4747            let vn = relative_vel.dot(n);
4748
4749            // Restitution (Müller, Macklin, Chentanez, Jeschke, Kim,
4750            // "Detailed Rigid Body Simulation with Extended Position Based
4751            // Dynamics", SCA 2020, eq. (34)): the target normal velocity is
4752            // `max(−e v̄_n, 0)` with `v̄_n` the pre-solve normal velocity. The
4753            // post-solve `vn` only reflects how far into the substep the
4754            // bodies first touched (`vn = −gap / h`), so reversing `vn`
4755            // itself made the bounce depend on that phase. The correction is
4756            // one-sided (only raises `vn` to the target), as in the 2D solver.
4757            let pre_vn = self.contact_pre_vn.get(i).copied().unwrap_or(vn);
4758            let restitution = if pre_vn < -restitution_threshold {
4759                constraint.restitution
4760            } else {
4761                Fix128::ZERO
4762            };
4763            let target = -restitution * pre_vn;
4764            let target = if target.is_negative() {
4765                Fix128::ZERO
4766            } else {
4767                target
4768            };
4769            if vn < target {
4770                let delta_vn = target - vn;
4771                let impulse_n = n * delta_vn;
4772                let inv_w = Fix128::ONE / w_sum;
4773                self.bodies[constraint.body_a].velocity =
4774                    self.bodies[constraint.body_a].velocity + impulse_n * (body_a.inv_mass * inv_w);
4775                self.bodies[constraint.body_b].velocity =
4776                    self.bodies[constraint.body_b].velocity - impulse_n * (body_b.inv_mass * inv_w);
4777            }
4778
4779            // Friction: reduce tangential velocity
4780            let relative_vel2 =
4781                self.bodies[constraint.body_a].velocity - self.bodies[constraint.body_b].velocity;
4782            let vn2 = relative_vel2.dot(n);
4783            let tangent_vel = relative_vel2 - n * vn2;
4784            let tangent_speed_sq = tangent_vel.length_squared();
4785            if tangent_speed_sq > Fix128::ZERO {
4786                let tangent_speed = tangent_speed_sq.sqrt();
4787                let friction = constraint.friction;
4788                // Coulomb friction (Müller, Macklin, Chentanez, Jeschke, Kim,
4789                // "Detailed Rigid Body Simulation with Extended Position Based
4790                // Dynamics", SCA 2020, eq. (30)): the tangential correction
4791                // is capped by `h μ |f_n| w = μ λ_n / h` in relative velocity.
4792                // `cached_lambda` is this substep's accumulated normal
4793                // separation from the position solve (the paper's multiplier
4794                // times `w_sum`), so the cap is `μ λ / h`. The normal
4795                // velocity after the solve (`vn2`) is not the normal force:
4796                // it is about 0 for a resting contact, which removed the
4797                // friction of sliding and resting bodies.
4798                let max_friction_impulse = friction * constraint.cached_lambda * inv_dt;
4799                let applied = if tangent_speed < max_friction_impulse {
4800                    tangent_speed
4801                } else {
4802                    max_friction_impulse
4803                };
4804                let friction_dir = tangent_vel / tangent_speed;
4805                let friction_impulse = friction_dir * applied;
4806                let inv_w = Fix128::ONE / w_sum;
4807                self.bodies[constraint.body_a].velocity = self.bodies[constraint.body_a].velocity
4808                    - friction_impulse * (body_a.inv_mass * inv_w);
4809                self.bodies[constraint.body_b].velocity = self.bodies[constraint.body_b].velocity
4810                    + friction_impulse * (body_b.inv_mass * inv_w);
4811            }
4812        }
4813
4814        // ⚠️ 受け皿を sticky flag に畳み込む (1 度立ったら落ちない)
4815        if overflow.load(core::sync::atomic::Ordering::Relaxed) {
4816            self.note_rigid_overflow();
4817        }
4818    }
4819
4820    /// Run the pre-solve hooks and contact modifiers once over every
4821    /// contact constraint and keep the result.
4822    ///
4823    /// The serial, bridge and batched (`step_parallel`) substeps call this
4824    /// ahead of their iteration loop, so a relative change such as
4825    /// `friction *= 0.5` applies once per substep and not once per
4826    /// iteration, and every path calls each hook the same number of times.
4827    ///
4828    /// # Claims
4829    ///
4830    /// - Each hook and each modifier runs at most once per contact per call.
4831    /// - Sensor contacts are skipped without calling hooks or modifiers.
4832    /// - A modifier's result (contact, friction, restitution) is written back
4833    ///   to the constraint, and the solver passes read that value without
4834    ///   calling the modifier again.
4835    /// - A contact vetoed by a hook or discarded by a modifier is left
4836    ///   unmodified and is skipped by the solver passes (position and
4837    ///   velocity) until the next call.
4838    /// - With no hooks and no modifiers the call changes nothing.
4839    #[cfg(feature = "std")]
4840    fn apply_contact_filters(&mut self) {
4841        self.contact_discarded.clear();
4842        if self.pre_solve_hooks.is_empty() && self.contact_modifiers.is_empty() {
4843            return;
4844        }
4845        let num = self.contact_constraints.len();
4846        self.contact_discarded.resize(num, false);
4847        for i in 0..num {
4848            let constraint = self.contact_constraints[i];
4849            if self.bodies[constraint.body_a].is_sensor || self.bodies[constraint.body_b].is_sensor
4850            {
4851                continue;
4852            }
4853
4854            let mut contact = constraint.contact;
4855            let mut friction = constraint.friction;
4856            let mut restitution = constraint.restitution;
4857
4858            let mut skip = false;
4859            for hook in &self.pre_solve_hooks {
4860                if !hook(constraint.body_a, constraint.body_b, &contact) {
4861                    skip = true;
4862                    break;
4863                }
4864            }
4865            if !skip {
4866                for modifier in &self.contact_modifiers {
4867                    if !modifier.modify_contact(
4868                        constraint.body_a,
4869                        constraint.body_b,
4870                        &mut contact,
4871                        &mut friction,
4872                        &mut restitution,
4873                    ) {
4874                        skip = true;
4875                        break;
4876                    }
4877                }
4878            }
4879            if skip {
4880                self.contact_discarded[i] = true;
4881                continue;
4882            }
4883
4884            let slot = &mut self.contact_constraints[i];
4885            slot.contact = contact;
4886            slot.friction = friction;
4887            slot.restitution = restitution;
4888        }
4889    }
4890
4891    /// Solve constraints in batched parallel mode via Rayon.
4892    ///
4893    /// Within each colored batch, constraints share no body indices
4894    /// (guaranteed by `rebuild_batches` graph coloring), so they are
4895    /// dispatched to Rayon's thread pool for parallel execution.
4896    /// Between batches, a synchronization barrier ensures that
4897    /// earlier batch results are visible to later batches.
4898    ///
4899    /// Pre-solve hooks and contact modifiers are not called here: the
4900    /// substep runs [`Self::apply_contact_filters`] once before its
4901    /// iterations, and this pass skips the contacts it discarded and reads
4902    /// the values it wrote back.
4903    #[cfg(feature = "parallel")]
4904    fn solve_constraints_batched(&mut self, dt: Fix128) {
4905        // The coloring excluded bodies that were static when `rebuild_batches`
4906        // ran. If any body changed static-ness since (e.g. the user edited
4907        // `inv_mass` through `bodies` after the rebuild) the snapshot no longer
4908        // matches the live world and the batches must be recomputed, otherwise
4909        // two threads could take `&mut` to a body that is now dynamic.
4910        let snapshot_stale = self.batch_static_bodies.len() != self.bodies.len()
4911            || self
4912                .bodies
4913                .iter()
4914                .zip(&self.batch_static_bodies)
4915                .any(|(body, &was_static)| body.inv_mass.is_zero() != was_static);
4916        if snapshot_stale {
4917            self.batches_dirty = true;
4918            self.rebuild_batches();
4919        }
4920
4921        let num_batches = self.constraint_batches.len();
4922
4923        let bodies = BodySlicePtr {
4924            ptr: self.bodies.as_mut_ptr(),
4925            len: self.bodies.len(),
4926        };
4927        let dists = DistConstraintSlicePtr {
4928            ptr: self.distance_constraints.as_mut_ptr(),
4929            len: self.distance_constraints.len(),
4930        };
4931        let static_bodies: &[bool] = &self.batch_static_bodies;
4932        // Contacts the substep's hook / modifier pre-pass discarded (an index
4933        // past the end counts as kept)
4934        #[cfg(feature = "std")]
4935        let discarded: &[bool] = &self.contact_discarded;
4936        #[cfg(not(feature = "std"))]
4937        let discarded: &[bool] = &[];
4938
4939        for batch_idx in 0..num_batches {
4940            // Phase 1: Distance constraints — parallel within batch
4941            {
4942                let indices = &self.constraint_batches[batch_idx].distance_indices;
4943                indices.par_iter().for_each(|&idx| {
4944                    // SAFETY: Graph coloring guarantees no two constraints in
4945                    // this batch share a dynamic body index; static bodies are
4946                    // borrowed shared only (`static_bodies` is the snapshot the
4947                    // coloring was built from, validated above). Each
4948                    // constraint index appears in exactly one batch, so
4949                    // cached_lambda writes are also disjoint.
4950                    unsafe {
4951                        let constraint = dists.get_mut(idx);
4952                        let body_a =
4953                            bodies.borrow(constraint.body_a, static_bodies[constraint.body_a]);
4954                        let body_b =
4955                            bodies.borrow(constraint.body_b, static_bodies[constraint.body_b]);
4956                        Self::solve_distance_pair(body_a, body_b, constraint, dt);
4957                    }
4958                });
4959            }
4960
4961            // Phase 2: Contact constraints — parallel within batch
4962            {
4963                let contacts = ContactConstraintSlicePtr {
4964                    ptr: self.contact_constraints.as_mut_ptr(),
4965                    len: self.contact_constraints.len(),
4966                };
4967                let indices = &self.constraint_batches[batch_idx].contact_indices;
4968                indices.par_iter().for_each(|&idx| {
4969                    if discarded.get(idx).copied().unwrap_or(false) {
4970                        return;
4971                    }
4972                    // SAFETY: Same invariant as Phase 1 — disjoint dynamic
4973                    // bodies per batch, static bodies shared read-only,
4974                    // each constraint index in exactly one batch.
4975                    unsafe {
4976                        let constraint = contacts.get_mut(idx);
4977                        let body_a =
4978                            bodies.borrow(constraint.body_a, static_bodies[constraint.body_a]);
4979                        let body_b =
4980                            bodies.borrow(constraint.body_b, static_bodies[constraint.body_b]);
4981                        Self::solve_contact_pair(body_a, body_b, constraint);
4982                    }
4983                });
4984            }
4985        }
4986    }
4987
4988    /// Solve a single distance constraint given mutable body references.
4989    ///
4990    /// Extracted as a static method (no `&self`) for parallel dispatch.
4991    /// Warm-starting seeds the solver with the cached lambda from the
4992    /// previous substep, reducing iterations needed for convergence.
4993    #[cfg(feature = "parallel")]
4994    #[inline(always)]
4995    fn solve_distance_pair(
4996        mut body_a: BodyRef<'_>,
4997        mut body_b: BodyRef<'_>,
4998        constraint: &mut DistanceConstraint,
4999        dt: Fix128,
5000    ) {
5001        let (pos_a, rot_a, inv_mass_a) = {
5002            let a = body_a.get();
5003            (a.position, a.rotation, a.inv_mass)
5004        };
5005        let (pos_b, rot_b, inv_mass_b) = {
5006            let b = body_b.get();
5007            (b.position, b.rotation, b.inv_mass)
5008        };
5009
5010        let anchor_a = pos_a + rot_a.rotate_vec(constraint.local_anchor_a);
5011        let anchor_b = pos_b + rot_b.rotate_vec(constraint.local_anchor_b);
5012
5013        let delta = anchor_b - anchor_a;
5014        let (normal, distance) = delta.normalize_with_length();
5015
5016        if distance.is_zero() {
5017            return;
5018        }
5019
5020        let error = distance - constraint.target_distance;
5021
5022        let compliance_term = constraint.compliance / (dt * dt);
5023        let w_sum = inv_mass_a + inv_mass_b + compliance_term;
5024
5025        if w_sum < W_SUM_EPSILON {
5026            return;
5027        }
5028
5029        // XPBD (Macklin 2016): `cached_lambda` is the Lagrange multiplier
5030        // accumulated over this substep (zeroed by `reset_lambdas`).
5031        //   dlambda = (C - alpha~ * lambda) / (w_a + w_b + alpha~)
5032        //   lambda += dlambda,  dx = dlambda * w * grad C
5033        // Before 1.2.0 `lambda` was overwritten instead of accumulated, so the
5034        // alpha~*lambda term was always under-estimated and the effective
5035        // stiffness of compliant constraints depended on `iterations`.
5036        let inv_w_sum = Fix128::ONE / w_sum;
5037        let lambda = constraint.cached_lambda;
5038        let dlambda = (error - lambda * compliance_term) * inv_w_sum;
5039        constraint.cached_lambda = lambda + dlambda;
5040        let correction = normal * dlambda;
5041
5042        // Branchless: static bodies have inv_mass == ZERO, correction * ZERO == ZERO.
5043        // `set_position` is a no-op for `BodyRef::Static`, matching the
5044        // sequential path which writes the unchanged position.
5045        let delta_a = correction * inv_mass_a;
5046        let delta_b = correction * inv_mass_b;
5047        body_a.set_position(select_vec3(!inv_mass_a.is_zero(), pos_a + delta_a, pos_a));
5048        body_b.set_position(select_vec3(!inv_mass_b.is_zero(), pos_b - delta_b, pos_b));
5049    }
5050
5051    /// Solve a single contact constraint given mutable body references.
5052    ///
5053    /// Extracted as a static method (no `&self`) for parallel dispatch.
5054    /// Warm-starting seeds the solver with the cached lambda from the
5055    /// previous substep, reducing iterations needed for convergence.
5056    #[cfg(feature = "parallel")]
5057    #[inline(always)]
5058    fn solve_contact_pair(
5059        mut body_a: BodyRef<'_>,
5060        mut body_b: BodyRef<'_>,
5061        constraint: &mut ContactConstraint,
5062    ) {
5063        let (pos_a, prev_a, inv_mass_a, sensor_a) = {
5064            let a = body_a.get();
5065            (a.position, a.prev_position, a.inv_mass, a.is_sensor)
5066        };
5067        let (pos_b, prev_b, inv_mass_b, sensor_b) = {
5068            let b = body_b.get();
5069            (b.position, b.prev_position, b.inv_mass, b.is_sensor)
5070        };
5071
5072        // Skip physics response for sensor/trigger bodies
5073        if sensor_a || sensor_b {
5074            return;
5075        }
5076
5077        let contact = constraint.contact;
5078
5079        if contact.depth <= Fix128::ZERO {
5080            return;
5081        }
5082
5083        let w_sum = inv_mass_a + inv_mass_b;
5084        if w_sum < W_SUM_EPSILON {
5085            return;
5086        }
5087
5088        // Accumulated non-negative contact multiplier (1.2.0). The contact is
5089        // re-detected every substep, so `depth` is the penetration at the start
5090        // of this substep and `cached_lambda` the separation already applied in
5091        // earlier iterations: dlambda = depth - lambda, clamped to lambda >= 0.
5092        // Before 1.2.0 every iteration re-pushed `depth - 0.85 * lambda_prev`
5093        // against a frame-stale depth, which multiplied the separation by the
5094        // iteration and substep counts (5 m/s collision → 700 m/s at defaults).
5095        let inv_w_sum = Fix128::ONE / w_sum;
5096        let lambda = constraint.cached_lambda;
5097        let dlambda = contact.depth - lambda;
5098        let (mut pos_a, mut pos_b) = (pos_a, pos_b);
5099        if dlambda > Fix128::ZERO {
5100            constraint.cached_lambda = lambda + dlambda;
5101
5102            let correction = contact.normal * dlambda;
5103            let correction_a = correction * (inv_mass_a * inv_w_sum);
5104            let correction_b = correction * (inv_mass_b * inv_w_sum);
5105
5106            // Branchless: inv_mass == ZERO for static bodies, correction_x will be ZERO.
5107            pos_a = select_vec3(!inv_mass_a.is_zero(), pos_a + correction_a, pos_a);
5108            pos_b = select_vec3(!inv_mass_b.is_zero(), pos_b - correction_b, pos_b);
5109        }
5110
5111        // Static friction (Müller et al. 2020, eq. (26)), see
5112        // `static_friction_correction`.
5113        if let Some((fa, fb)) = static_friction_correction(
5114            pos_a - prev_a,
5115            pos_b - prev_b,
5116            contact.normal,
5117            inv_mass_a,
5118            inv_mass_b,
5119            inv_w_sum,
5120            constraint.friction,
5121            constraint.cached_lambda,
5122        ) {
5123            pos_a = select_vec3(!inv_mass_a.is_zero(), pos_a - fa, pos_a);
5124            pos_b = select_vec3(!inv_mass_b.is_zero(), pos_b + fb, pos_b);
5125        }
5126
5127        // `set_position` is a no-op for `BodyRef::Static`.
5128        body_a.set_position(pos_a);
5129        body_b.set_position(pos_b);
5130    }
5131
5132    /// Solve distance constraints (sequential) with warm-starting (Gap 3.1).
5133    ///
5134    /// Warm-starting seeds each constraint with its cached lambda from the
5135    /// previous substep, reducing iterations needed for convergence.
5136    #[inline(always)]
5137    fn solve_distance_constraints(&mut self, dt: Fix128) {
5138        let num_constraints = self.distance_constraints.len();
5139        for i in 0..num_constraints {
5140            let constraint = self.distance_constraints[i];
5141            let body_a = self.bodies[constraint.body_a];
5142            let body_b = self.bodies[constraint.body_b];
5143
5144            // Get world-space anchor positions
5145            let anchor_a = body_a.position + body_a.rotation.rotate_vec(constraint.local_anchor_a);
5146            let anchor_b = body_b.position + body_b.rotation.rotate_vec(constraint.local_anchor_b);
5147
5148            // Compute constraint error (single sqrt via normalize_with_length)
5149            let delta = anchor_b - anchor_a;
5150            let (normal, distance) = delta.normalize_with_length();
5151
5152            if distance.is_zero() {
5153                continue;
5154            }
5155
5156            let error = distance - constraint.target_distance;
5157
5158            // Compute compliance term (XPBD)
5159            let compliance_term = constraint.compliance / (dt * dt);
5160            let w_sum = body_a.inv_mass + body_b.inv_mass + compliance_term;
5161
5162            if w_sum < W_SUM_EPSILON {
5163                continue;
5164            }
5165
5166            // XPBD: accumulate the Lagrange multiplier over the substep
5167            // (see `solve_distance_pair` for the derivation).
5168            let inv_w_sum = Fix128::ONE / w_sum;
5169            let lambda = constraint.cached_lambda;
5170            let dlambda = (error - lambda * compliance_term) * inv_w_sum;
5171            self.distance_constraints[i].cached_lambda = lambda + dlambda;
5172            let correction = normal * dlambda;
5173
5174            // Branchless apply corrections: static bodies have inv_mass == ZERO.
5175            let delta_a = correction * body_a.inv_mass;
5176            let delta_b = correction * body_b.inv_mass;
5177            self.bodies[constraint.body_a].position = select_vec3(
5178                !body_a.inv_mass.is_zero(),
5179                self.bodies[constraint.body_a].position + delta_a,
5180                self.bodies[constraint.body_a].position,
5181            );
5182            self.bodies[constraint.body_b].position = select_vec3(
5183                !body_b.inv_mass.is_zero(),
5184                self.bodies[constraint.body_b].position - delta_b,
5185                self.bodies[constraint.body_b].position,
5186            );
5187        }
5188    }
5189
5190    /// One serial contact pass (one PGS iteration) over the constraints that
5191    /// [`Self::apply_contact_filters`] kept (Gap 2.3).
5192    ///
5193    /// # Claims
5194    ///
5195    /// - The pass does not call hooks or modifiers; it reads the values the
5196    ///   pre-pass wrote back, so the substep's iterations all see the same
5197    ///   friction, restitution and contact.
5198    /// - A modifier therefore runs once per substep, not once per iteration,
5199    ///   and a relative change such as `friction *= 0.5` does not compound
5200    ///   with `iterations`.
5201    /// - Sensor contacts and contacts discarded by the pre-pass are skipped.
5202    ///
5203    /// # v0.11.0 auto-routing
5204    ///
5205    /// If a GPU solver bridge has been installed on this world via
5206    /// [`Self::set_gpu_solver_bridge`], contact-solve is routed through
5207    /// the bridge (one pass, same filters as the pre-pass)
5208    /// instead of running the CPU inline solver below. The bridge is
5209    /// briefly taken out of `self` via `Option::take()` so the borrow
5210    /// checker sees `&mut self` for the routed call, then reinstalled
5211    /// on exit — the field is unchanged from the caller's perspective.
5212    ///
5213    /// If no bridge is installed, the existing CPU-only code path runs
5214    /// unchanged.
5215    fn solve_contact_constraints(&mut self, _dt: Fix128) {
5216        #[cfg(feature = "gpu-solver-bridge")]
5217        if let Some(mut bridge) = self.gpu_solver_bridge.take() {
5218            self.solve_contact_pass_with_bridge(bridge.as_mut());
5219            self.gpu_solver_bridge = Some(bridge);
5220            return;
5221        }
5222        let num_constraints = self.contact_constraints.len();
5223        for i in 0..num_constraints {
5224            let constraint = self.contact_constraints[i];
5225            let body_a = self.bodies[constraint.body_a];
5226            let body_b = self.bodies[constraint.body_b];
5227
5228            // Skip physics response for sensor/trigger bodies
5229            if body_a.is_sensor || body_b.is_sensor {
5230                continue;
5231            }
5232
5233            // Discarded by the substep's hook / modifier pre-pass
5234            #[cfg(feature = "std")]
5235            if self.contact_discarded.get(i).copied().unwrap_or(false) {
5236                continue;
5237            }
5238
5239            // The modifiers' result was written back to the constraint by the
5240            // pre-pass, so this is the value every iteration sees.
5241            let contact = constraint.contact;
5242
5243            // Only resolve if penetrating
5244            if contact.depth <= Fix128::ZERO {
5245                continue;
5246            }
5247
5248            let w_sum = body_a.inv_mass + body_b.inv_mass;
5249            if w_sum < W_SUM_EPSILON {
5250                continue;
5251            }
5252
5253            // Accumulated contact multiplier, see `solve_contact_pair`.
5254            let inv_w_sum = Fix128::ONE / w_sum;
5255            let lambda = constraint.cached_lambda;
5256            let dlambda = contact.depth - lambda;
5257            if dlambda > Fix128::ZERO {
5258                self.contact_constraints[i].cached_lambda = lambda + dlambda;
5259
5260                let correction = contact.normal * dlambda;
5261                let correction_a = correction * (body_a.inv_mass * inv_w_sum);
5262                let correction_b = correction * (body_b.inv_mass * inv_w_sum);
5263
5264                // Branchless: inv_mass == ZERO for static bodies, correction_x will be ZERO.
5265                self.bodies[constraint.body_a].position = select_vec3(
5266                    !body_a.inv_mass.is_zero(),
5267                    self.bodies[constraint.body_a].position + correction_a,
5268                    self.bodies[constraint.body_a].position,
5269                );
5270                self.bodies[constraint.body_b].position = select_vec3(
5271                    !body_b.inv_mass.is_zero(),
5272                    self.bodies[constraint.body_b].position - correction_b,
5273                    self.bodies[constraint.body_b].position,
5274                );
5275            }
5276
5277            // Static friction (Müller et al. 2020, eq. (26)), see
5278            // `static_friction_correction`.
5279            self.apply_static_friction(i, inv_w_sum);
5280        }
5281    }
5282
5283    /// Position-level static friction of contact `i` against the current
5284    /// body positions, shared by the serial and bridge contact passes (the
5285    /// batched pass calls [`static_friction_correction`] on its pair
5286    /// references). The caller has already skipped sensor, discarded and
5287    /// non-penetrating contacts.
5288    fn apply_static_friction(&mut self, i: usize, inv_w_sum: Fix128) {
5289        let constraint = self.contact_constraints[i];
5290        let a = self.bodies[constraint.body_a];
5291        let b = self.bodies[constraint.body_b];
5292        if let Some((fa, fb)) = static_friction_correction(
5293            a.position - a.prev_position,
5294            b.position - b.prev_position,
5295            constraint.contact.normal,
5296            a.inv_mass,
5297            b.inv_mass,
5298            inv_w_sum,
5299            constraint.friction,
5300            constraint.cached_lambda,
5301        ) {
5302            self.bodies[constraint.body_a].position =
5303                select_vec3(!a.inv_mass.is_zero(), a.position - fa, a.position);
5304            self.bodies[constraint.body_b].position =
5305                select_vec3(!b.inv_mass.is_zero(), b.position + fb, b.position);
5306        }
5307    }
5308
5309    /// v0.10.0 opt-in: run one PGS contact-solve iteration via a
5310    /// caller-supplied [`GpuSolverBridge`](crate::gpu_bridge::GpuSolverBridge) instead of the CPU-side
5311    /// `solve_contact_constraints` hot loop.
5312    ///
5313    /// # Claims
5314    ///
5315    /// - Each call runs the hooks and modifiers once, writes the result back
5316    ///   to the constraints, then runs one bridge pass over the survivors.
5317    /// - `substep_with_bridge` runs the hooks and modifiers once per substep
5318    ///   and then only the pass per iteration, so a relative modifier does
5319    ///   not compound with `iterations`.
5320    ///
5321    /// # Byte-exact CPU parity
5322    ///
5323    /// The bridge-routed path applies Stage A filters (sensor skip,
5324    /// pre-solve hooks, contact modifiers) on the CPU side upfront
5325    /// exactly as `solve_contact_constraints` does inline. Because
5326    /// the closures the hooks and modifiers evaluate depend only on
5327    /// their function inputs — never on live body state that
5328    /// changes during the loop — a batched upfront application is
5329    /// byte-exact equivalent to the CPU's per-constraint
5330    /// application. Stage B (depth ≤ 0 skip, w_sum < ε skip,
5331    /// warm-start biased lambda, cached_lambda write, position
5332    /// correction) is routed through the bridge, which runs the
5333    /// same sequential Gauss-Seidel semantics because bridge
5334    /// backends dispatch a single-workgroup single-thread compute
5335    /// kernel.
5336    ///
5337    /// # Constraint contract
5338    ///
5339    /// - The bridge receives a filtered `Vec<ContactConstraint>`
5340    ///   containing only the constraints that survived Stage A
5341    ///   (sensor-free, hook-approved, modifier-approved). The
5342    ///   contact / friction / restitution fields may be mutated
5343    ///   from the original slot values if a contact modifier
5344    ///   ran.
5345    /// - After readback the method writes each updated
5346    ///   `cached_lambda` back into `self.contact_constraints` via
5347    ///   an index map, mirroring the CPU write that lands at
5348    ///   `self.contact_constraints[i].cached_lambda = lambda` at
5349    ///   the corresponding slot.
5350    /// - Body positions are written back to `self.bodies[i].position`.
5351    ///
5352    /// # Panics
5353    ///
5354    /// Panics if the bridge implementation does not override the
5355    /// v0.9.0 contact-solve methods (`send_contact_constraints`,
5356    /// `send_body_state`, `dispatch_contact_solve_iteration`,
5357    /// `recv_contact_constraints`, `recv_body_positions`).
5358    #[cfg(feature = "gpu-solver-bridge")]
5359    pub fn solve_contact_constraints_with_bridge<B: crate::gpu_bridge::GpuSolverBridge + ?Sized>(
5360        &mut self,
5361        bridge: &mut B,
5362    ) {
5363        self.apply_contact_filters();
5364        self.solve_contact_pass_with_bridge(bridge);
5365    }
5366
5367    /// One bridge-routed PGS pass over the constraints that
5368    /// [`Self::apply_contact_filters`] kept. Does not call hooks or
5369    /// modifiers, so the iteration loop of a substep sees the values the
5370    /// pre-pass wrote.
5371    #[cfg(feature = "gpu-solver-bridge")]
5372    fn solve_contact_pass_with_bridge<B: crate::gpu_bridge::GpuSolverBridge + ?Sized>(
5373        &mut self,
5374        bridge: &mut B,
5375    ) {
5376        // ---- Stage A: select the surviving constraints ----
5377        let num_constraints = self.contact_constraints.len();
5378        let mut filtered: Vec<ContactConstraint> = Vec::with_capacity(num_constraints);
5379        let mut mapping: Vec<usize> = Vec::with_capacity(num_constraints);
5380
5381        for i in 0..num_constraints {
5382            let constraint = self.contact_constraints[i];
5383            let body_a = self.bodies[constraint.body_a];
5384            let body_b = self.bodies[constraint.body_b];
5385
5386            if body_a.is_sensor || body_b.is_sensor {
5387                continue;
5388            }
5389            if self.contact_discarded.get(i).copied().unwrap_or(false) {
5390                continue;
5391            }
5392
5393            let contact = constraint.contact;
5394            let friction = constraint.friction;
5395            let restitution = constraint.restitution;
5396
5397            filtered.push(ContactConstraint {
5398                body_a: constraint.body_a,
5399                body_b: constraint.body_b,
5400                contact,
5401                friction,
5402                restitution,
5403                cached_lambda: constraint.cached_lambda,
5404            });
5405            mapping.push(i);
5406        }
5407
5408        if filtered.is_empty() {
5409            return;
5410        }
5411
5412        // ---- Extract per-body state ----
5413        let mut positions: Vec<[Fix128; 3]> = Vec::with_capacity(self.bodies.len());
5414        let mut inv_masses: Vec<Fix128> = Vec::with_capacity(self.bodies.len());
5415        for body in &self.bodies {
5416            positions.push([body.position.x, body.position.y, body.position.z]);
5417            inv_masses.push(body.inv_mass);
5418        }
5419
5420        // ---- Stage B: route through bridge ----
5421        bridge.send_contact_constraints(&filtered);
5422        bridge.send_body_state(&positions, &inv_masses);
5423        bridge.dispatch_contact_solve_iteration(self.config.warm_start_factor);
5424
5425        let mut updated_constraints = filtered;
5426        let mut updated_positions = positions;
5427        bridge.recv_contact_constraints(&mut updated_constraints);
5428        bridge.recv_body_positions(&mut updated_positions);
5429
5430        // ---- Write back cached_lambda + positions ----
5431        for (filtered_idx, orig_slot) in mapping.iter().enumerate() {
5432            self.contact_constraints[*orig_slot].cached_lambda =
5433                updated_constraints[filtered_idx].cached_lambda;
5434        }
5435        for (i, pos) in updated_positions.iter().enumerate() {
5436            self.bodies[i].position = Vec3Fix::new(pos[0], pos[1], pos[2]);
5437        }
5438
5439        // Static friction (Müller et al. 2020, eq. (26)) on the CPU after
5440        // the bridge's normal pass, over the same filtered contacts (the
5441        // bridge contract carries only the normal multiplier).
5442        for &orig_slot in &mapping {
5443            let c = self.contact_constraints[orig_slot];
5444            let w_sum = self.bodies[c.body_a].inv_mass + self.bodies[c.body_b].inv_mass;
5445            if c.contact.depth <= Fix128::ZERO || w_sum < W_SUM_EPSILON {
5446                continue;
5447            }
5448            self.apply_static_friction(orig_slot, Fix128::ONE / w_sum);
5449        }
5450    }
5451
5452    /// v0.10.0 opt-in: run one full simulation step with the
5453    /// contact-solve stage routed through a caller-supplied
5454    /// [`GpuSolverBridge`](crate::gpu_bridge::GpuSolverBridge). Semantically equivalent to
5455    /// [`Self::step`] with `substep(_)` replaced by
5456    /// [`Self::substep_with_bridge`] inside the substep loop.
5457    ///
5458    /// # Byte-exact CPU parity
5459    ///
5460    /// When the bridge implementation is byte-exact vs the CPU
5461    /// `solve_contact_constraints`, a full `step_with_bridge`
5462    /// produces `self.bodies` and `self.contact_constraints` state
5463    /// byte-identical to what a CPU-only `step(dt)` call produces
5464    /// from the same initial state and configuration.
5465    ///
5466    /// # Solver backend
5467    ///
5468    /// `config.solver_backend` is not consulted: the bridge replaces the
5469    /// contact solve of the XPBD substep, so with [`SolverBackend::Tgs`] this
5470    /// gives the same bits as with [`SolverBackend::Xpbd`]. A bridge path for
5471    /// TGS does not exist yet; use [`Self::step`] for TGS.
5472    #[cfg(feature = "gpu-solver-bridge")]
5473    pub fn step_with_bridge<B: crate::gpu_bridge::GpuSolverBridge + ?Sized>(
5474        &mut self,
5475        bridge: &mut B,
5476        dt: Fix128,
5477    ) {
5478        // The checks of `try_step` (a recorded fault, a non-positive `dt`, a
5479        // participant step rule or a per-body field that does not fit): when
5480        // one fails nothing runs, as `step` does. This path runs the XPBD
5481        // substep loop whatever the backend, so the step rules are checked
5482        // against the width it hands out, `dt / substeps`.
5483        let h = Self::divided_substep_width(dt, self.config.substeps);
5484        if !matches!(self.check_step(dt, h), Ok(true)) {
5485            return;
5486        }
5487        self.make_body_rotations_unit();
5488        self.wake_on_sdf_shape_change();
5489        self.begin_sdf_contact_log();
5490        let mut frozen = self.participant_flags();
5491
5492        // Phase 0: Event frame lifecycle (contacts are cleared and re-detected
5493        // in every substep since 1.2.0, see `substep`)
5494        self.events.begin_frame();
5495
5496        // Phase 0.5: Rebuild island connectivity from current joints
5497        self.islands.resize(self.bodies.len());
5498        self.islands.reset_unions();
5499        for j in &self.joints {
5500            let (a, b) = j.bodies();
5501            if a < self.bodies.len() && b < self.bodies.len() {
5502                self.islands.union(a, b);
5503            }
5504        }
5505
5506        // Phase 1: Apply force fields
5507        if !self.force_fields.is_empty() {
5508            apply_force_fields(&self.force_fields, &mut self.bodies, dt);
5509        }
5510
5511        // Phase 2 (collision detection) is per substep since 1.2.0, see `step`.
5512
5513        // Phase 3: Substep loop with bridge-routed contact solve
5514        let substep_dt = dt / Fix128::from_int(self.config.substeps as i64);
5515        let n = self.config.substeps;
5516        for i in 0..n {
5517            self.kinematic_substeps_left = n - i;
5518            self.sdf_contact_substep = i;
5519            let overflow_at_start = self.participants_begin_substep(i, n, substep_dt, &mut frozen);
5520            self.substep_with_bridge(bridge, substep_dt);
5521            self.participants_end_substep(overflow_at_start, &frozen);
5522        }
5523        self.kinematic_substeps_left = 0;
5524        self.sdf_contact_substep = 0;
5525
5526        // Phase 3.5: Frame-level damping (1.2.0, was per substep)
5527        self.apply_frame_damping();
5528
5529        // Phase 4: Update sleeping
5530        self.islands.update_sleep(&self.bodies);
5531
5532        // Phase 4.5: Leave every SDF collider attached to a body at that
5533        // body's final pose, which is what the queries between steps
5534        // (`sdf_contacts`, `sdf_ccd_hits`, the shape casts) read.
5535        self.sync_sdf_colliders();
5536
5537        // Phase 5: End event frame
5538        self.events.end_frame();
5539    }
5540
5541    /// v0.10.0 opt-in: run one substep with the contact-solve stage
5542    /// routed through a caller-supplied [`GpuSolverBridge`](crate::gpu_bridge::GpuSolverBridge). The
5543    /// integrate + distance-projection + joint + velocity-update
5544    /// stages stay on the CPU; only the inner PGS contact-solve
5545    /// iterations are dispatched through the bridge.
5546    ///
5547    /// # Byte-exact CPU parity
5548    ///
5549    /// When the bridge implementation is byte-exact vs the CPU
5550    /// `solve_contact_constraints` (which every ALICE-TRT
5551    /// `TrtSolverAdapter` release since v2.6.0 asserts on the
5552    /// 3-platform CI matrix), a full `substep_with_bridge` sequence
5553    /// produces `self.bodies` and `self.contact_constraints` state
5554    /// byte-identical to what a CPU-only `substep(dt)` call produces
5555    /// from the same initial state and configuration.
5556    ///
5557    /// Called on its own, this runs no participant: the participants of
5558    /// [`crate::world_participant`] are called by the step loops
5559    /// ([`Self::step_with_bridge`], [`Self::try_step`]) around each substep.
5560    ///
5561    /// # Solver backend
5562    ///
5563    /// `config.solver_backend` is not consulted: the bridge replaces the
5564    /// contact solve of the XPBD substep, so with [`SolverBackend::Tgs`] this
5565    /// gives the same bits as with [`SolverBackend::Xpbd`]. A bridge path for
5566    /// TGS does not exist yet; use [`Self::step`] for TGS.
5567    #[cfg(feature = "gpu-solver-bridge")]
5568    pub fn substep_with_bridge<B: crate::gpu_bridge::GpuSolverBridge + ?Sized>(
5569        &mut self,
5570        bridge: &mut B,
5571        dt: Fix128,
5572    ) {
5573        // Callable on its own (outside `step_with_bridge`), so it is an entry
5574        // of its own for the rotations too.
5575        self.make_body_rotations_unit();
5576        self.apply_joint_motors(dt);
5577        self.integrate_positions(dt);
5578        self.reset_lambdas();
5579
5580        // 1.2. Collision detection on the predicted positions (Small Steps).
5581        self.clear_contacts();
5582        self.detect_collisions();
5583
5584        // 1.5. Resolve SDF collisions (implicit surface contacts)
5585        if !self.sdf_colliders.is_empty() {
5586            self.resolve_sdf_collisions();
5587        }
5588        if !self.static_colliders.is_empty() {
5589            self.resolve_static_collisions(false);
5590        }
5591
5592        // 2. Solve constraints (sequential). Distance stays CPU;
5593        //    contact routes through the bridge. The first iteration runs the
5594        //    hooks and modifiers once and solves; later iterations only solve,
5595        //    so a relative modifier does not compound with `iterations`.
5596        for iteration in 0..self.config.iterations {
5597            self.solve_distance_constraints(dt);
5598            if iteration == 0 {
5599                self.solve_contact_constraints_with_bridge(bridge);
5600            } else {
5601                self.solve_contact_pass_with_bridge(bridge);
5602            }
5603        }
5604
5605        // 3. Solve joint constraints — route through the same bridge, then
5606        //    remove the joints whose reaction force in that solve exceeded
5607        //    their break force (the same check `solve_joints_dispatch` runs).
5608        let forces = if self.joints.is_empty() {
5609            None
5610        } else {
5611            self.bridge_joint_reaction_forces(dt)
5612        };
5613        self.solve_joints_with_bridge(bridge, dt);
5614        if let Some(forces) = forces {
5615            self.break_overloaded_joints(&forces);
5616        }
5617
5618        self.update_velocities(dt);
5619    }
5620
5621    /// v0.12.0: run one joint-solve pass with the joint stage routed
5622    /// through a caller-supplied [`GpuSolverBridge`](crate::gpu_bridge::GpuSolverBridge). Extracts body
5623    /// positions, rotations and inverse masses into the shape the
5624    /// bridge expects, uploads them via `send_joints`, `send_body_state`
5625    /// and `send_body_rotations`, dispatches
5626    /// `dispatch_joint_solve_iteration(dt)`, reads back the
5627    /// post-solve positions via `recv_body_positions`, and writes
5628    /// them back into `self.bodies[i].position`.
5629    ///
5630    /// # Contract
5631    ///
5632    /// - The bridge must implement the v0.12.0 joint-solve methods
5633    ///   (`send_joints`, `send_body_rotations`,
5634    ///   `dispatch_joint_solve_iteration`); the default `panic!`
5635    ///   implementations surface backends that do not.
5636    /// - `dt` is the substep length used to compute
5637    ///   `compliance / (dt * dt)` inside the kernel.
5638    /// - A joint whose two ends are the same body is not uploaded
5639    ///   (the CPU solve skips it too, the body stays free); the
5640    ///   remaining joints keep their order.
5641    /// - When no joint remains (`self.joints` is empty or holds only
5642    ///   such joints), this method is a no-op and returns without
5643    ///   touching the bridge — useful for callers that
5644    ///   unconditionally route through a bridge and let this method
5645    ///   decide whether there's work to do.
5646    ///
5647    /// # Byte-exact CPU parity
5648    ///
5649    /// The CPU-side `solve_joints` runs each joint independently in
5650    /// index order; the bridge is required to reproduce that exact
5651    /// order and per-joint arithmetic. ALICE-TRT v3.1.0 satisfies
5652    /// this by dispatching a single-workgroup single-thread kernel
5653    /// that walks the uploaded joint list top to bottom.
5654    #[cfg(feature = "gpu-solver-bridge")]
5655    pub fn solve_joints_with_bridge<B: crate::gpu_bridge::GpuSolverBridge + ?Sized>(
5656        &mut self,
5657        bridge: &mut B,
5658        dt: Fix128,
5659    ) {
5660        // A joint whose two ends are the same body constrains nothing (a body
5661        // cannot move relative to itself); the CPU solve skips it, and a
5662        // backend walking the list would apply both sides' corrections to that
5663        // one body. Upload only the other joints, in their original order.
5664        let joints: Vec<crate::joint::Joint> = self
5665            .joints
5666            .iter()
5667            .filter(|j| {
5668                let (a, b) = j.bodies();
5669                a != b
5670            })
5671            .copied()
5672            .collect();
5673        if joints.is_empty() {
5674            return;
5675        }
5676
5677        // ---- Extract per-body state ----
5678        let mut positions: Vec<[Fix128; 3]> = Vec::with_capacity(self.bodies.len());
5679        let mut inv_masses: Vec<Fix128> = Vec::with_capacity(self.bodies.len());
5680        let mut rotations: Vec<[Fix128; 4]> = Vec::with_capacity(self.bodies.len());
5681        for body in &self.bodies {
5682            positions.push([body.position.x, body.position.y, body.position.z]);
5683            inv_masses.push(body.inv_mass);
5684            rotations.push([
5685                body.rotation.x,
5686                body.rotation.y,
5687                body.rotation.z,
5688                body.rotation.w,
5689            ]);
5690        }
5691
5692        // ---- Stage B: route through bridge ----
5693        bridge.send_joints(&joints);
5694        bridge.send_body_state(&positions, &inv_masses);
5695        bridge.send_body_rotations(&rotations);
5696        bridge.dispatch_joint_solve_iteration(dt);
5697
5698        let mut updated_positions = positions;
5699        bridge.recv_body_positions(&mut updated_positions);
5700
5701        // ---- Write back positions ----
5702        for (i, pos) in updated_positions.iter().enumerate() {
5703            self.bodies[i].position = crate::math::Vec3Fix::new(pos[0], pos[1], pos[2]);
5704        }
5705    }
5706
5707    /// v0.12.0 private helper: solve joints either through the
5708    /// installed GPU solver bridge (if any) or via the CPU
5709    /// `solve_joints` fallback. Used by [`Self::step`] / substep
5710    /// entry points to auto-route without every caller needing to
5711    /// know about the bridge.
5712    fn solve_joints_dispatch(&mut self, dt: Fix128) {
5713        if self.joints.is_empty() {
5714            return;
5715        }
5716        #[cfg(feature = "gpu-solver-bridge")]
5717        if let Some(mut bridge) = self.gpu_solver_bridge.take() {
5718            let forces = self.bridge_joint_reaction_forces(dt);
5719            self.solve_joints_with_bridge(bridge.as_mut(), dt);
5720            self.gpu_solver_bridge = Some(bridge);
5721            if let Some(forces) = forces {
5722                self.break_overloaded_joints(&forces);
5723            }
5724            return;
5725        }
5726        if self.joints.iter().any(|j| j.break_force().is_some()) {
5727            let forces =
5728                crate::joint::solve_joints_with_reaction_forces(&self.joints, &mut self.bodies, dt);
5729            self.break_overloaded_joints(&forces);
5730        } else {
5731            solve_joints(&self.joints, &mut self.bodies, dt);
5732        }
5733    }
5734
5735    /// The reaction forces the bridge's joint solve will transmit, when any
5736    /// joint can break: the trait returns only positions, so the CPU solve
5737    /// ([`crate::joint::solve_joints_with_reaction_forces`]) runs on a copy of
5738    /// the bodies. A bridge matches the CPU solve bit for bit
5739    /// ([`crate::gpu_bridge::GpuSolverBridge::assert_bit_exact_vs_cpu`]), so the
5740    /// copy sees the same multipliers. `None` when no joint has a break force.
5741    #[cfg(feature = "gpu-solver-bridge")]
5742    fn bridge_joint_reaction_forces(&self, dt: Fix128) -> Option<Vec<Fix128>> {
5743        if !self.joints.iter().any(|j| j.break_force().is_some()) {
5744            return None;
5745        }
5746        let mut copy = self.bodies.clone();
5747        Some(crate::joint::solve_joints_with_reaction_forces(
5748            &self.joints,
5749            &mut copy,
5750            dt,
5751        ))
5752    }
5753
5754    /// Remove every joint whose reaction force in this substep's joint solve
5755    /// (`forces`, in joint order, from
5756    /// [`crate::joint::solve_joints_with_reaction_forces`]) exceeds its
5757    /// `break_force`, and report each in
5758    /// [`crate::event::EventCollector::joint_break_events`].
5759    ///
5760    /// Runs after the joints are solved on every path that solves the world's
5761    /// joints: the XPBD substeps, the TGS joint projection and a bridge set with
5762    /// [`Self::set_gpu_solver_bridge`] go through [`Self::solve_joints_dispatch`],
5763    /// and [`Self::substep_with_bridge`] (so [`Self::step_with_bridge`]) calls it
5764    /// after its bridge joint solve. A broken joint's correction from this
5765    /// substep has been applied; it no longer acts from the next solve on.
5766    /// Joints are removed from the highest index down with the swap-remove of
5767    /// [`Self::remove_joint`] (motors follow it). No wake is needed: a joint
5768    /// transmits force only while its island is awake (a sleeping island's
5769    /// bodies do not move, so its joints carry no multiplier), so a joint that
5770    /// breaks always belongs to an awake island. The island unions are then rebuilt
5771    /// from the remaining joints, as at the start of every step, so the rest of
5772    /// the step (and a snapshot taken after it) no longer joins the two bodies.
5773    ///
5774    /// A joint whose bodies are both asleep is not moved by the solve, carries
5775    /// no multiplier and so does not break while they sleep (lowering its
5776    /// `break_force` through the public `joints` field takes effect once its
5777    /// island wakes).
5778    fn break_overloaded_joints(&mut self, forces: &[Fix128]) {
5779        let overloaded: Vec<(usize, Fix128)> = self
5780            .joints
5781            .iter()
5782            .zip(forces)
5783            .enumerate()
5784            .filter(|(_, (joint, force))| joint.break_force().is_some_and(|max| **force > max))
5785            .map(|(i, (_, force))| (i, *force))
5786            .collect();
5787        for &(index, force) in overloaded.iter().rev() {
5788            if let Some(joint) = self.remove_joint(index) {
5789                self.events
5790                    .report_joint_break(crate::event::JointBreakEvent {
5791                        index,
5792                        joint,
5793                        force,
5794                    });
5795            }
5796        }
5797        if !overloaded.is_empty() {
5798            self.islands.resize(self.bodies.len());
5799            self.islands.reset_unions();
5800            for j in &self.joints {
5801                let (a, b) = j.bodies();
5802                if a < self.bodies.len() && b < self.bodies.len() {
5803                    self.islands.union(a, b);
5804                }
5805            }
5806        }
5807    }
5808
5809    /// Add an SDF collider to the world
5810    ///
5811    /// A collider attached to a body ([`SdfCollider::new_dynamic`]) is placed
5812    /// at that body's current pose when the body exists, and follows the body
5813    /// from then on: every step copies the body's pose into it before SDF
5814    /// overlap is resolved and again at the end of the step.
5815    ///
5816    /// The collider's cached values are recomputed from its fields
5817    /// ([`SdfCollider::update_cache`]): a `rotation` or `scale` written to the
5818    /// collider directly before the call takes effect, and the rotation is
5819    /// stored as a unit quaternion.
5820    pub fn add_sdf_collider(&mut self, mut collider: SdfCollider) -> usize {
5821        collider.update_cache();
5822        collider.sync_to_body(&self.bodies);
5823        let idx = self.sdf_colliders.len();
5824        self.sdf_colliders.push(collider);
5825        self.sync_sdf_generations();
5826        idx
5827    }
5828
5829    /// Bring `sdf_generations` to one entry per collider: a collider without
5830    /// an entry (just added, or pushed onto the public field directly) is
5831    /// recorded at its current generation, so adding a collider wakes
5832    /// nothing; entries past the last collider are dropped.
5833    fn sync_sdf_generations(&mut self) {
5834        let n = self.sdf_colliders.len();
5835        self.sdf_generations.truncate(n);
5836        for c in &self.sdf_colliders[self.sdf_generations.len()..] {
5837            self.sdf_generations.push(c.field.generation());
5838        }
5839    }
5840
5841    /// Wake every sleeping body when an SDF collider changed shape since the
5842    /// previous step ([`SdfField::generation`](crate::sdf_collider::SdfField::generation)
5843    /// differs from the value last seen). A sleeping body is not integrated,
5844    /// so one resting on a surface that was carved away would otherwise stay
5845    /// where it was. The whole world is woken: a field does not report where
5846    /// it changed. Fields that never change (generation constant) wake
5847    /// nothing and leave the step bit for bit as before.
5848    fn wake_on_sdf_shape_change(&mut self) {
5849        self.sync_sdf_generations();
5850        let mut changed = false;
5851        for (seen, c) in self.sdf_generations.iter_mut().zip(&self.sdf_colliders) {
5852            let now = c.field.generation();
5853            if *seen != now {
5854                *seen = now;
5855                changed = true;
5856            }
5857        }
5858        if changed {
5859            // Through the public wake (`wake_body`, the body's island), as
5860            // every other caller that wakes a sleeping body by hand. The step
5861            // has not parked anything yet: `park_begin` runs later and parks
5862            // only bodies that are asleep.
5863            for i in 0..self.bodies.len() {
5864                if self.islands.is_sleeping(i) {
5865                    self.wake_body(i);
5866                }
5867            }
5868        }
5869    }
5870
5871    /// Empty the SDF contact record at the head of a step (the allocation is
5872    /// kept) and record from substep 0.
5873    fn begin_sdf_contact_log(&mut self) {
5874        self.sdf_contact_log.clear();
5875        self.sdf_contact_substep = 0;
5876    }
5877
5878    /// The SDF contacts resolved by the last step, every substep of it, in
5879    /// the order they were applied: by substep, then by body index, then by
5880    /// collider index.
5881    ///
5882    /// Each entry is one push-out of a body out of an SDF collider
5883    /// ([`SdfContact`](crate::sdf_collider::SdfContact)): where, along which
5884    /// normal, how deep, and how fast the body was approaching, and the
5885    /// collider's frame at that substep (a collider attached to a body moves
5886    /// with it during the step, so convert `point` with the recorded frame,
5887    /// not with the collider's pose after the step). The record is
5888    /// emptied at the head of each step ([`Self::step`], [`Self::try_step`],
5889    /// `step_parallel`, `step_with_bridge`); a step that does not run (a
5890    /// recorded fault, a non-positive `dt`) leaves it as it was.
5891    /// `substep_with_bridge` called on its own appends to it. Recording
5892    /// changes nothing in the step: a world whose record is never read steps
5893    /// to the same bits. The buffer is reused from step to step, so it holds
5894    /// its largest size so far (one entry per body per collider at most).
5895    ///
5896    /// [`Self::sdf_contacts`] is a different query: what the bodies overlap
5897    /// now, between steps, without moving anything.
5898    ///
5899    /// The participants of [`crate::world_participant`] read the contacts
5900    /// recorded so far in the current step through
5901    /// [`SubstepCtx::sdf_contacts`](crate::world_participant::SubstepCtx::sdf_contacts).
5902    #[must_use]
5903    pub fn last_step_sdf_contacts(&self) -> &[crate::sdf_collider::SdfContact] {
5904        &self.sdf_contact_log
5905    }
5906
5907    /// Copy each body's pose into the SDF colliders attached to it
5908    /// ([`crate::sdf_collider::sync_dynamic_sdf_colliders`]); static colliders
5909    /// are not touched.
5910    fn sync_sdf_colliders(&mut self) {
5911        crate::sdf_collider::sync_dynamic_sdf_colliders(&mut self.sdf_colliders, &self.bodies);
5912    }
5913
5914    /// Remove an SDF collider by index
5915    pub fn remove_sdf_collider(&mut self, idx: usize) -> Option<SdfCollider> {
5916        if idx < self.sdf_colliders.len() {
5917            if idx < self.sdf_generations.len() {
5918                self.sdf_generations.remove(idx);
5919            }
5920            Some(self.sdf_colliders.remove(idx))
5921        } else {
5922            None
5923        }
5924    }
5925
5926    /// Set the collision radius for body-vs-SDF queries
5927    pub fn set_sdf_collision_radius(&mut self, radius: Fix128) {
5928        self.sdf_collision_radius = radius;
5929    }
5930
5931    /// Resolve SDF collisions by directly correcting body positions.
5932    ///
5933    /// SDF colliders are treated as immovable surfaces (infinite mass).
5934    /// Each penetrating body is pushed out along the SDF gradient.
5935    ///
5936    /// When `parallel` feature is enabled, bodies are processed in parallel via Rayon.
5937    #[cfg(feature = "std")]
5938    fn resolve_sdf_collisions(&mut self) {
5939        // A collider attached to a body is resolved at that body's current
5940        // (predicted) pose.
5941        self.sync_sdf_colliders();
5942        let sdf_colliders = &self.sdf_colliders;
5943        let colliders = &self.body_colliders;
5944        let collision_radius = self.sdf_collision_radius;
5945        let substep = self.sdf_contact_substep;
5946
5947        // Each push-out is recorded into `log` (observation only: the record
5948        // is built from values the push-out computes and never read back).
5949        let push_out =
5950            |idx: usize, body: &mut RigidBody, log: &mut Vec<crate::sdf_collider::SdfContact>| {
5951                if body.is_static() || body.is_sensor {
5952                    return;
5953                }
5954                let collider = colliders.get(idx).and_then(Option::as_ref);
5955                for (collider_index, sdf) in sdf_colliders.iter().enumerate() {
5956                    // A body is never pushed out of its own field.
5957                    if sdf.body_index == idx {
5958                        continue;
5959                    }
5960                    if let Some(contact) = sdf_contact_of(collider, body, collision_radius, sdf) {
5961                        log.push(crate::sdf_collider::SdfContact {
5962                            body_index: idx,
5963                            collider_index,
5964                            point: contact.point_b,
5965                            normal: contact.normal,
5966                            depth: contact.depth,
5967                            approach_speed: -body.velocity.dot(contact.normal),
5968                            substep,
5969                            collider_position: sdf.position,
5970                            collider_rotation: sdf.rotation,
5971                            collider_scale: sdf.scale,
5972                        });
5973                        body.position = body.position + contact.normal * contact.depth;
5974                    }
5975                }
5976            };
5977
5978        #[cfg(feature = "parallel")]
5979        {
5980            let parked: &[bool] = if self.park.active {
5981                &self.park.parked
5982            } else {
5983                &[]
5984            };
5985            self.stage_work.resolution_bodies += self.unparked_count();
5986            let scratch = &mut self.sdf_contact_scratch;
5987            if scratch.len() < self.bodies.len() {
5988                scratch.resize_with(self.bodies.len(), Vec::new);
5989            }
5990            self.bodies
5991                .par_iter_mut()
5992                .zip(scratch.par_iter_mut())
5993                .enumerate()
5994                .for_each(|(idx, (body, out))| {
5995                    out.clear();
5996                    if !parked.get(idx).copied().unwrap_or(false) {
5997                        push_out(idx, body, out);
5998                    }
5999                });
6000            let log = &mut self.sdf_contact_log;
6001            for out in &scratch[..self.bodies.len()] {
6002                log.extend_from_slice(out);
6003            }
6004        }
6005
6006        #[cfg(not(feature = "parallel"))]
6007        {
6008            let count = if self.park.active {
6009                self.park.awake.len()
6010            } else {
6011                self.bodies.len()
6012            };
6013            for k in 0..count {
6014                let idx = if self.park.active {
6015                    self.park.awake[k]
6016                } else {
6017                    k
6018                };
6019                self.stage_work.resolution_bodies += 1;
6020                push_out(idx, &mut self.bodies[idx], &mut self.sdf_contact_log);
6021            }
6022        }
6023    }
6024
6025    /// The contacts of the bodies with the SDF colliders, as `(body index, contact)`:
6026    /// what [`step`](Self::step) would push each body out of, without moving
6027    /// anything. A body with a shape or a compound is tested as that collider, any
6028    /// other body as a sphere of [`sdf_collision_radius`](Self::sdf_collision_radius);
6029    /// static bodies, sensors and an SDF attached to the body itself are skipped.
6030    /// An SDF attached to a body is queried at the pose it was given when it was
6031    /// added or at the end of the last step; after moving a body by hand, call
6032    /// [`crate::sdf_collider::sync_dynamic_sdf_colliders`] on
6033    /// [`sdf_colliders`](Self::sdf_colliders) first.
6034    #[cfg(feature = "std")]
6035    #[must_use]
6036    pub fn sdf_contacts(&self) -> Vec<(usize, crate::collider::Contact)> {
6037        let mut contacts = Vec::new();
6038        for (idx, body) in self.bodies.iter().enumerate() {
6039            if body.is_static() || body.is_sensor {
6040                continue;
6041            }
6042            let collider = self.body_colliders.get(idx).and_then(Option::as_ref);
6043            for sdf in &self.sdf_colliders {
6044                if sdf.body_index == idx {
6045                    continue;
6046                }
6047                if let Some(c) = sdf_contact_of(collider, body, self.sdf_collision_radius, sdf) {
6048                    contacts.push((idx, c));
6049                }
6050            }
6051        }
6052        contacts
6053    }
6054
6055    /// The normal force each contact of the last step carries, as
6056    /// `(contact point, normal, force)` — the input [`crate::contact_viz`] draws.
6057    ///
6058    /// `dt` is the frame step passed to [`step`](Self::step). The solver applies a
6059    /// separation `λ` (`cached_lambda`) along the normal in the last substep, of
6060    /// length `h = dt / substeps`, which for bodies of inverse masses `wₐ, w_b` is
6061    /// the displacement of a force `F` over `h`: `λ = (wₐ + w_b)·F·h²`, so
6062    /// `F = λ / ((wₐ + w_b)·h²)`. A body resting under gravity then carries its
6063    /// weight: `m·g`. The normal points from B to A and the force acts on A. Contacts
6064    /// between two immovable bodies carry none and are left out. A body that has gone
6065    /// to sleep (about a minute of rest) is not solved, so its contacts are not there
6066    /// to report.
6067    #[must_use]
6068    pub fn contact_forces(&self, dt: Fix128) -> Vec<(Vec3Fix, Vec3Fix, Fix128)> {
6069        let h2 = self.substep_dt(dt) * self.substep_dt(dt);
6070        self.contact_constraints
6071            .iter()
6072            .filter_map(|c| self.contact_force(c, h2))
6073            .collect()
6074    }
6075
6076    /// The length `h` of one substep of a frame of `dt`.
6077    fn substep_dt(&self, dt: Fix128) -> Fix128 {
6078        dt / Fix128::from_int(self.config.substeps.max(1) as i64)
6079    }
6080
6081    /// `(point, normal, force)` of one contact, or `None` when neither body can move.
6082    fn contact_force(
6083        &self,
6084        c: &ContactConstraint,
6085        h2: Fix128,
6086    ) -> Option<(Vec3Fix, Vec3Fix, Fix128)> {
6087        let w = self.bodies.get(c.body_a)?.inv_mass + self.bodies.get(c.body_b)?.inv_mass;
6088        if w < W_SUM_EPSILON || h2.is_zero() {
6089            return None;
6090        }
6091        Some((
6092            c.contact.point_a,
6093            c.contact.normal,
6094            c.cached_lambda / (w * h2),
6095        ))
6096    }
6097
6098    /// Arrows for the normal forces of the last step's contacts
6099    /// ([`contact_forces`](Self::contact_forces) through
6100    /// [`crate::contact_viz::generate_contact_arrows`]).
6101    #[must_use]
6102    pub fn contact_arrows(&self, dt: Fix128) -> Vec<crate::contact_viz::ContactArrow> {
6103        crate::contact_viz::generate_contact_arrows(&self.contact_forces(dt))
6104    }
6105
6106    /// Two tangential arrows of magnitude `μ·F` for each contact of the last step,
6107    /// with the friction coefficient of that contact (they differ between material
6108    /// pairs).
6109    #[must_use]
6110    pub fn contact_friction_arrows(&self, dt: Fix128) -> Vec<crate::contact_viz::ContactArrow> {
6111        let h2 = self.substep_dt(dt) * self.substep_dt(dt);
6112        self.contact_constraints
6113            .iter()
6114            .filter_map(|c| Some((self.contact_force(c, h2)?, c.friction)))
6115            .flat_map(|(force, mu)| crate::contact_viz::generate_friction_arrows(&[force], mu))
6116            .collect()
6117    }
6118
6119    /// A friction cone (half-angle `atan μ`, height the normal force) for each
6120    /// contact of the last step, with that contact's friction coefficient.
6121    #[must_use]
6122    pub fn contact_friction_cones(&self, dt: Fix128) -> Vec<crate::contact_viz::FrictionCone> {
6123        let h2 = self.substep_dt(dt) * self.substep_dt(dt);
6124        self.contact_constraints
6125            .iter()
6126            .filter_map(|c| Some((self.contact_force(c, h2)?, c.friction)))
6127            .flat_map(|(force, mu)| crate::contact_viz::generate_friction_cones(&[force], mu))
6128            .collect()
6129    }
6130
6131    /// Add an immovable collision surface — a plane, a height field or a triangle
6132    /// mesh — and return its index. See [`crate::static_collider`].
6133    pub fn add_static_collider(
6134        &mut self,
6135        collider: crate::static_collider::StaticCollider,
6136    ) -> usize {
6137        let idx = self.static_colliders.len();
6138        self.park_generation = self.park_generation.wrapping_add(1);
6139        self.static_colliders.push(collider);
6140        idx
6141    }
6142
6143    /// Remove a static collider by index, returning it, or `None` when the index
6144    /// is out of range. Later colliders shift down by one.
6145    pub fn remove_static_collider(
6146        &mut self,
6147        idx: usize,
6148    ) -> Option<crate::static_collider::StaticCollider> {
6149        if idx < self.static_colliders.len() {
6150            self.park_generation = self.park_generation.wrapping_add(1);
6151            Some(self.static_colliders.remove(idx))
6152        } else {
6153            None
6154        }
6155    }
6156
6157    /// Number of static colliders.
6158    #[must_use]
6159    pub fn static_collider_count(&self) -> usize {
6160        self.static_colliders.len()
6161    }
6162
6163    /// Resolve every non-static, non-sensor body's collision sphere against the
6164    /// static colliders, in order, pushing it out along each contact normal.
6165    ///
6166    /// The sphere is the body's collision radius, or the world's default
6167    /// ([`Self::set_sdf_collision_radius`]) for a body without one.
6168    ///
6169    /// `remove_approach`: also drop the part of the body's velocity that
6170    /// points into the surface (an inelastic contact). The XPBD substep
6171    /// passes `false` — its `update_velocities` turns the push into velocity
6172    /// — and the TGS path passes `true`, because TGS keeps the velocity it
6173    /// integrated and a pushed-out body would otherwise keep falling into
6174    /// the surface.
6175    fn resolve_static_collisions(&mut self, remove_approach: bool) {
6176        let default_radius = self.sdf_collision_radius;
6177        let count = self.stage_count();
6178        for k in 0..count {
6179            let i = self.stage_body(k);
6180            self.stage_work.resolution_bodies += 1;
6181            let body = &mut self.bodies[i];
6182            if body.is_static() || body.is_sensor {
6183                continue;
6184            }
6185            let radius = self
6186                .body_collision_radii
6187                .get(i)
6188                .and_then(|r| *r)
6189                .unwrap_or(default_radius);
6190            for collider in &self.static_colliders {
6191                if let Some(contact) = collider.collide_sphere(body.position, radius) {
6192                    body.position = body.position + contact.normal * contact.depth;
6193                    if remove_approach {
6194                        let vn = body.velocity.dot(contact.normal);
6195                        if vn < Fix128::ZERO {
6196                            body.velocity = body.velocity - contact.normal * vn;
6197                        }
6198                    }
6199                }
6200            }
6201        }
6202    }
6203
6204    /// Select the broad-phase. Every kind gives bit-identical simulations (see
6205    /// [`Broadphase`]); switching drops the persistent tree and the
6206    /// [`Broadphase::Hybrid`] layers, which are rebuilt on the next step if
6207    /// that kind is chosen.
6208    pub fn set_broadphase(&mut self, kind: Broadphase) {
6209        self.park_generation = self.park_generation.wrapping_add(1);
6210        self.broadphase = kind;
6211        self.broadphase_reset();
6212    }
6213
6214    /// The broad-phase in use.
6215    #[must_use]
6216    pub fn broadphase(&self) -> Broadphase {
6217        self.broadphase
6218    }
6219
6220    /// What the [`Broadphase::DynamicTree`] tree holds: how many bodies have a
6221    /// proxy and how tall the tree is. Both are 0 until a step has run with that
6222    /// broad-phase.
6223    #[must_use]
6224    pub fn broadphase_stats(&self) -> BroadphaseStats {
6225        BroadphaseStats {
6226            proxies: self.broadphase_tree.proxy_count(),
6227            height: self.broadphase_tree.height(),
6228        }
6229    }
6230
6231    /// The fattened box the [`Broadphase::DynamicTree`] tree keeps for a body, or
6232    /// `None` when the body has no proxy (no collision radius, or no step yet).
6233    #[must_use]
6234    pub fn broadphase_proxy_aabb(&self, body_idx: usize) -> Option<AABB> {
6235        let proxy = (*self.broadphase_proxies.get(body_idx)?)?;
6236        if self.broadphase_tree.user_data(proxy) as usize != body_idx {
6237            return None;
6238        }
6239        Some(self.broadphase_tree.get_aabb(proxy))
6240    }
6241
6242    /// Forget the persistent tree and the hybrid layers: the next
6243    /// [`Broadphase::DynamicTree`] or [`Broadphase::Hybrid`] step rebuilds them
6244    /// from the bodies. Called when the broad-phase is switched.
6245    fn broadphase_reset(&mut self) {
6246        self.broadphase_tree = crate::dynamic_bvh::DynamicAabbTree::new();
6247        self.broadphase_proxies.clear();
6248        self.broadphase_hybrid = BroadphaseHybrid::new();
6249    }
6250
6251    /// The box the broad-phase keeps for body `i`, whose collision radius is
6252    /// `radius`: the closed-form world box of its collider when it carries one
6253    /// ([`crate::body_collider::BodyCollider::world_aabb`]), clipped to the cube
6254    /// of the collision sphere, otherwise that cube. Both contain the body's
6255    /// position, so the clip is never empty. Every pair the narrow-phase can
6256    /// report has overlapping boxes: a pair involving a collider is decided by
6257    /// GJK on the collider (and on the plain side's sphere), which lies inside
6258    /// these boxes.
6259    fn broadphase_box(&self, i: usize, radius: Fix128) -> AABB {
6260        let pos = self.bodies[i].position;
6261        let cube = AABB::from_center_half(pos, Vec3Fix::new(radius, radius, radius));
6262        match self.body_colliders.get(i).and_then(Option::as_ref) {
6263            Some(collider) => {
6264                let tight = collider.world_aabb(pos, self.bodies[i].rotation);
6265                let max = |x: Fix128, y: Fix128| if x > y { x } else { y };
6266                let min = |x: Fix128, y: Fix128| if x < y { x } else { y };
6267                AABB {
6268                    min: Vec3Fix::new(
6269                        max(tight.min.x, cube.min.x),
6270                        max(tight.min.y, cube.min.y),
6271                        max(tight.min.z, cube.min.z),
6272                    ),
6273                    max: Vec3Fix::new(
6274                        min(tight.max.x, cube.max.x),
6275                        min(tight.max.y, cube.max.y),
6276                        min(tight.max.z, cube.max.z),
6277                    ),
6278                }
6279            }
6280            None => cube,
6281        }
6282    }
6283
6284    /// The sorted candidate pairs of bodies whose boxes may overlap, from the
6285    /// primitives (body index + tight box) of every body with a collision radius.
6286    fn broadphase_pairs(&mut self, primitives: Vec<BvhPrimitive>) -> Vec<(u32, u32)> {
6287        match self.broadphase {
6288            Broadphase::Bvh => LinearBvh::build(primitives).find_pairs(),
6289            Broadphase::DynamicTree => {
6290                let n = self.bodies.len();
6291                // A proxy belongs to a body *index* (its user data), and re-boxing it
6292                // each step follows whichever body now has that index, so removing
6293                // or restoring bodies needs no rebuild; only the proxies of indices
6294                // that no longer exist have to go.
6295                for proxy in self
6296                    .broadphase_proxies
6297                    .drain(n.min(self.broadphase_proxies.len())..)
6298                    .flatten()
6299                {
6300                    self.broadphase_tree.remove(proxy);
6301                }
6302                self.broadphase_proxies.resize(n, None);
6303                let mut present = vec![false; n];
6304                for p in &primitives {
6305                    let i = p.index as usize;
6306                    present[i] = true;
6307                    match self.broadphase_proxies[i] {
6308                        Some(proxy) => {
6309                            self.broadphase_tree.update(proxy, p.aabb);
6310                        }
6311                        None => {
6312                            self.broadphase_proxies[i] =
6313                                Some(self.broadphase_tree.insert(p.aabb, p.index));
6314                        }
6315                    }
6316                }
6317                // A body that lost its collision radius leaves the tree.
6318                for (i, here) in present.iter().enumerate() {
6319                    if !here {
6320                        if let Some(proxy) = self.broadphase_proxies[i].take() {
6321                            self.broadphase_tree.remove(proxy);
6322                        }
6323                    }
6324                }
6325                self.broadphase_tree.find_pairs()
6326            }
6327            Broadphase::Hybrid => {
6328                let hybrid = &mut self.broadphase_hybrid;
6329                hybrid.clear_dynamic();
6330                for p in &primitives {
6331                    let is_static = self.bodies[p.index as usize].is_static();
6332                    hybrid.insert_dynamic(p.index, p.aabb, is_static);
6333                }
6334                hybrid.build_dynamic();
6335                let mut pairs = Vec::new();
6336                self.stage_work.broadphase_box_tests += hybrid.query_pairs(&mut pairs);
6337                pairs
6338            }
6339        }
6340    }
6341
6342    /// The sorted candidate pairs while parking is in effect: the pairs among the
6343    /// unparked `primitives` from a [`LinearBvh`] over them, plus every parked
6344    /// body whose proxy overlaps one of them, from `park.tree`. Pairs of two
6345    /// parked bodies are left out; the narrow-phase drops pairs of two sleeping
6346    /// bodies anyway, so the contacts and their order are those of
6347    /// [`Self::broadphase_pairs`] over every body.
6348    fn parked_broadphase_pairs(&mut self, primitives: Vec<BvhPrimitive>) -> Vec<(u32, u32)> {
6349        let mut pairs = Vec::new();
6350        for p in &primitives {
6351            let i = p.index;
6352            self.park.tree.query_callback(&p.aabb, |j| {
6353                pairs.push(if i < j { (i, j) } else { (j, i) });
6354            });
6355        }
6356        if primitives.len() >= 2 {
6357            pairs.extend(LinearBvh::build(primitives).find_pairs());
6358        }
6359        pairs.sort_unstable();
6360        pairs.dedup();
6361        pairs
6362    }
6363
6364    // ── Automatic Collision Detection ─────────────────────────────────
6365
6366    /// Detect collisions between bodies with collision radii.
6367    ///
6368    /// The broad-phase boxes are the collider's tight world box for a body that
6369    /// carries one and the cube of the collision sphere otherwise; the
6370    /// narrow-phase is exact sphere-sphere for two plain bodies and GJK/EPA for a
6371    /// pair involving a collider (the plain side as its sphere).
6372    /// Generates contact constraints and events automatically.
6373    /// Bodies without a collision radius are skipped.
6374    #[allow(clippy::too_many_lines, clippy::items_after_statements)]
6375    fn detect_collisions(&mut self) {
6376        let n = self.bodies.len();
6377        if n < 2 {
6378            return;
6379        }
6380
6381        // Build BVH from bodies that have collision radii (the unparked ones while
6382        // parking is in effect: parked bodies sit in `park.tree`)
6383        let mut primitives = Vec::new();
6384        let count = self.stage_count();
6385        for k in 0..count {
6386            let i = self.stage_body(k);
6387            if let Some(radius) = self.body_collision_radii.get(i).and_then(|r| *r) {
6388                primitives.push(BvhPrimitive {
6389                    aabb: self.broadphase_box(i, radius),
6390                    index: i as u32,
6391                    morton: 0,
6392                });
6393            }
6394        }
6395        self.stage_work.broadphase_primitives += primitives.len() as u64;
6396
6397        let parked_with_radius = if self.park.active {
6398            self.park.proxy_live
6399        } else {
6400            0
6401        };
6402        if primitives.len() + parked_with_radius < 2 {
6403            return;
6404        }
6405
6406        let pairs = if self.park.active {
6407            self.parked_broadphase_pairs(primitives)
6408        } else {
6409            self.broadphase_pairs(primitives)
6410        };
6411        self.stage_work.broadphase_pairs += pairs.len() as u64;
6412
6413        // Velocity of a body as the pre-skip step saw it here: a parked body
6414        // still carries the force-field kick of phase 1 in the first substep
6415        // (the velocity derivation zeroes it at the end of that substep).
6416        let velocity_of = |i: usize| -> Vec3Fix {
6417            let body = &self.bodies[i];
6418            if self.park.first_substep
6419                && !self.force_fields.is_empty()
6420                && self.park.is_parked(i)
6421                && !body.is_static()
6422            {
6423                crate::force::force_field_velocity(&self.force_fields, i, body, self.park.frame_dt)
6424            } else {
6425                body.velocity
6426            }
6427        };
6428
6429        // Collect results to avoid borrow conflicts
6430        struct ContactInfo {
6431            body_a: usize,
6432            body_b: usize,
6433            contact: Contact,
6434            normal: Vec3Fix,
6435            point: Vec3Fix,
6436            depth: Fix128,
6437            rel_vel: Fix128,
6438            is_sensor: bool,
6439        }
6440        let mut results: Vec<ContactInfo> = Vec::new();
6441        let mut radius_overflow = false;
6442
6443        for (a32, b32) in pairs {
6444            let a = a32 as usize;
6445            let b = b32 as usize;
6446            if a >= n || b >= n {
6447                continue;
6448            }
6449
6450            // Sphere-sphere narrow phase (safe indexing for deserialization robustness)
6451            let radius_a = self
6452                .body_collision_radii
6453                .get(a)
6454                .and_then(|r| *r)
6455                .unwrap_or(Fix128::ZERO);
6456            let radius_b = self
6457                .body_collision_radii
6458                .get(b)
6459                .and_then(|r| *r)
6460                .unwrap_or(Fix128::ZERO);
6461
6462            // Contact normal points from B to A (`Contact::normal` contract, same
6463            // as the EPA / SDF paths and what `solve_contact_constraints` /
6464            // `update_velocities` assume: A is moved along +n, B along -n).
6465            // Before 1.2.0 this path used `pos_b - pos_a` (A → B), so every
6466            // sphere-sphere contact pushed the bodies *into* each other and a
6467            // plain head-on collision accelerated both bodies (4 m/s → 114 m/s
6468            // in 4 frames). `tests/analytic_physics.rs::head_on_collision_*`.
6469            let delta = self.bodies[a].position - self.bodies[b].position;
6470            // 半径和が `|·| ≥ 2⁶³` だと wrap して負になり、どの距離とも比べられない
6471            // (`velocity_of` が `self` を借りているので、印は loop の後で立てる)
6472            let Some(combined_radius) = radius_a.checked_add(radius_b) else {
6473                radius_overflow = true;
6474                continue;
6475            };
6476            // Squared-distance early out *first*: the BVH candidate set is a
6477            // superset of the overlapping pairs (49k candidates for 2.7k contacts
6478            // on the 1000-sphere grid) and the quantised leaf AABBs cannot be
6479            // tightened without a BVH API change, so every candidate pays only
6480            // 4 checked multiplies + 2 checked adds + a compare here; the filter / static / sleep lookups
6481            // and the sqrt + 3 divisions of `normalize_with_length` run only for
6482            // real overlaps. Same `dist < combined_radius` decision (both sides
6483            // exact for |delta| < 2^31), same contact order.
6484            //
6485            // `|delta|` か半径和が `2³¹·⁵ ≈ 3.04e9` 以上だと 2 乗が wrap して
6486            // 重なりを見逃す (両辺が黙って別の値になる) 2 乗がどちらも範囲内
6487            // なら従来の比較のまま (bit 不変)、どちらかが範囲外の対だけ 2 乗を
6488            // 経ない長さで比べ、法線と距離もその長さから作る (`scaled`)
6489            let mut scaled: Option<(Vec3Fix, Fix128)> = None;
6490            // A pair involving a collider is decided by GJK on the collider, with
6491            // the other body's collider or, for a plain body, its sphere; such a
6492            // pair can overlap with coincident centres, where the sphere path
6493            // cannot give a normal. Its early out is the overlap of the two
6494            // broad-phase boxes (the collider's tight box, the plain body's
6495            // sphere cube), which contain everything GJK looks at.
6496            let collider_a = self.body_colliders.get(a).and_then(Option::as_ref);
6497            let collider_b = self.body_colliders.get(b).and_then(Option::as_ref);
6498            if collider_a.is_none() && collider_b.is_none() {
6499                let squares = delta
6500                    .checked_length_squared()
6501                    .zip(combined_radius.checked_mul(combined_radius));
6502                match squares {
6503                    Some((d2, r2)) => {
6504                        if d2 >= r2 || d2.is_zero() {
6505                            continue;
6506                        }
6507                    }
6508                    None => {
6509                        // 中心距離が `≥ 2⁶³` (表せない) なら、どの半径和
6510                        // (`< 2⁶³`、上で検査済) とも重ならない
6511                        let Some(dist) = delta.checked_length_scaled() else {
6512                            continue;
6513                        };
6514                        if dist >= combined_radius || dist.is_zero() {
6515                            continue;
6516                        }
6517                        let Some(normal) = delta.try_normalize_scaled() else {
6518                            continue;
6519                        };
6520                        scaled = Some((normal, dist));
6521                    }
6522                }
6523            } else if !self
6524                .broadphase_box(a, radius_a)
6525                .intersects(&self.broadphase_box(b, radius_b))
6526            {
6527                continue;
6528            }
6529
6530            // Filter check
6531            let filter_a = self
6532                .body_filters
6533                .get(a)
6534                .copied()
6535                .unwrap_or(CollisionFilter::DEFAULT);
6536            let filter_b = self
6537                .body_filters
6538                .get(b)
6539                .copied()
6540                .unwrap_or(CollisionFilter::DEFAULT);
6541            if !CollisionFilter::can_collide(&filter_a, &filter_b) {
6542                continue;
6543            }
6544
6545            // Skip static-static
6546            if self.bodies[a].is_static() && self.bodies[b].is_static() {
6547                continue;
6548            }
6549
6550            // Skip if both sleeping
6551            if self.islands.is_sleeping(a) && self.islands.is_sleeping(b) {
6552                continue;
6553            }
6554
6555            // The boxes overlap, which is necessary but not sufficient for the
6556            // solids to: let the shapes decide.
6557            if collider_a.is_some() || collider_b.is_some() {
6558                let pose_a = (self.bodies[a].position, self.bodies[a].rotation);
6559                let pose_b = (self.bodies[b].position, self.bodies[b].rotation);
6560                let hit = match (collider_a, collider_b) {
6561                    (Some(ca), Some(cb)) => {
6562                        crate::body_collider::contact_between(ca, pose_a, cb, pose_b)
6563                    }
6564                    (Some(ca), None) => crate::body_collider::contact_with_sphere(
6565                        ca,
6566                        pose_a,
6567                        crate::collider::Sphere::new(pose_b.0, radius_b),
6568                        true,
6569                    ),
6570                    (None, Some(cb)) => crate::body_collider::contact_with_sphere(
6571                        cb,
6572                        pose_b,
6573                        crate::collider::Sphere::new(pose_a.0, radius_a),
6574                        false,
6575                    ),
6576                    (None, None) => None,
6577                };
6578                if let Some(contact) = hit {
6579                    if contact.depth > Fix128::ZERO {
6580                        let rel_vel = (velocity_of(a) - velocity_of(b)).dot(contact.normal);
6581                        results.push(ContactInfo {
6582                            body_a: a,
6583                            body_b: b,
6584                            contact,
6585                            normal: contact.normal,
6586                            point: contact.point_a,
6587                            depth: contact.depth,
6588                            rel_vel,
6589                            is_sensor: self.bodies[a].is_sensor || self.bodies[b].is_sensor,
6590                        });
6591                    }
6592                }
6593                continue;
6594            }
6595
6596            let (normal, dist) = scaled.unwrap_or_else(|| delta.normalize_with_length());
6597
6598            if dist < combined_radius && !dist.is_zero() {
6599                let depth = combined_radius - dist;
6600                let point_a = self.bodies[a].position - normal * radius_a;
6601                let point_b = self.bodies[b].position + normal * radius_b;
6602                // Approach speed along the normal: negative while closing.
6603                let rel_vel = (velocity_of(a) - velocity_of(b)).dot(normal);
6604                let is_sensor = self.bodies[a].is_sensor || self.bodies[b].is_sensor;
6605
6606                let contact = Contact {
6607                    depth,
6608                    normal,
6609                    point_a,
6610                    point_b,
6611                };
6612
6613                results.push(ContactInfo {
6614                    body_a: a,
6615                    body_b: b,
6616                    contact,
6617                    normal,
6618                    point: point_a,
6619                    depth,
6620                    rel_vel,
6621                    is_sensor,
6622                });
6623            }
6624        }
6625
6626        if radius_overflow {
6627            self.note_rigid_overflow();
6628        }
6629
6630        // Apply results
6631        for info in results {
6632            // Report contact event
6633            self.events.report_contact(
6634                info.body_a,
6635                info.body_b,
6636                info.normal,
6637                info.point,
6638                info.depth,
6639                info.rel_vel,
6640            );
6641
6642            // Wake a *sleeping* island touched by this contact. Waking
6643            // unconditionally (pre-1.2.0) reset `idle_frames` of every body in
6644            // resting contact on every detection, so a stack could never fall
6645            // asleep, and `wake_island` walks all bodies — O(contacts × bodies)
6646            // per detection (3.7 ms of a 6 ms step at 2700 contacts / 1000 bodies).
6647            for x in [info.body_a, info.body_b] {
6648                if !self.islands.is_sleeping(x) {
6649                    continue;
6650                }
6651                if self.park.is_parked(x) {
6652                    // Parked bodies are on no joint, so their island is
6653                    // themselves: same outcome as `wake_island`, without its
6654                    // walk over every body.
6655                    self.islands.wake_body(x);
6656                    self.park.unpark(x, &mut self.stage_work);
6657                } else {
6658                    self.islands.wake_island(x);
6659                }
6660            }
6661
6662            if info.is_sensor {
6663                self.events.report_trigger(info.body_a, info.body_b);
6664            } else {
6665                self.add_contact_with_material(info.body_a, info.body_b, info.contact);
6666            }
6667        }
6668    }
6669
6670    /// Get body by index
6671    #[inline]
6672    #[must_use]
6673    pub fn get_body(&self, idx: usize) -> Option<&RigidBody> {
6674        self.bodies.get(idx)
6675    }
6676
6677    /// Get mutable body by index
6678    ///
6679    /// A rotation written through the returned reference is not normalized
6680    /// until the next step: a non-unit quaternion `q` scales what it rotates
6681    /// by `|q|^2` in the queries made before then (see [`RigidBody::rotation`]).
6682    #[inline]
6683    pub fn get_body_mut(&mut self, idx: usize) -> Option<&mut RigidBody> {
6684        self.bodies.get_mut(idx)
6685    }
6686
6687    /// 範囲外の積が起きた step があったか (sticky、WM-01 / doctrine B-12)
6688    ///
6689    /// `true` は **この world の軌道が信用できない** ことを意味する
6690    /// doctrine §3 の 3 値で言えば `undecided` — 探索では「この枝は未決定」
6691    ///
6692    /// # なぜ値ベースの検出器では足りないか
6693    ///
6694    /// `Fix128::mul` の積が範囲外で 2 の冪だと `hi` が厳密に 0 になり
6695    /// (`2⁴⁰ × 2⁴⁰`)、`position += velocity*dt` が 0 加算 →
6696    /// `update_velocities` が速度も 0 に上書き →
6697    /// **「原点で完全に静止した自己整合な状態」** に落ちる
6698    /// ⚠️ **静止は物理的に妥当なのでどの不変条件でも red にならない**
6699    ///
6700    /// # sticky である理由
6701    ///
6702    /// 1 度立ったら `step` を重ねても落ちない 落ちると探索が
6703    /// 「未決定だった枝」を後の step で決定済と誤認する
6704    ///
6705    /// ⚠️ **flag は状態の一部**なので [`Self::serialize_state`] の被覆に
6706    /// 入れる必要がある (入れないと巻き戻した先で `undecided` が消える、
6707    /// doctrine B-12 の指摘) — **format v2 で収録済** (world ごと 1 byte、
6708    /// [`Self::STATE_VERSION`] 参照)
6709    #[must_use]
6710    pub const fn overflow_detected(&self) -> bool {
6711        self.overflow_detected
6712    }
6713
6714    /// 剛体の経路が範囲外の値を踏んだことを [`Self::overflow_detected`] に立てる
6715    ///
6716    /// sticky flag を立てるのはこの関数だけ (積分の `v·dt` と位置の加算、
6717    /// 速度導出、球の接触の距離と半径和、角速度の大きさ、TGS の積分、
6718    /// TGS の関節投影の速度への繰り越し) 落とす経路は [`Self::reset_world`] と
6719    /// 状態の復元だけ
6720    #[inline]
6721    pub(crate) fn note_rigid_overflow(&mut self) {
6722        self.overflow_detected = true;
6723    }
6724
6725    /// [`Self::serialize_state`] blob の magic
6726    ///
6727    /// ⚠️ 旧 format は先頭が body 数だったので、magic を検査しないと
6728    /// **新実装が旧 blob を誤って解釈する**
6729    pub const STATE_MAGIC: [u8; 4] = *b"APHY";
6730
6731    /// [`Self::serialize_state`] blob の format version
6732    ///
6733    /// v1 = header 12 + body ごと 208 + body ごと sleep 5
6734    /// v2 = v1 + **world ごと overflow flag 1 byte** (doctrine B-12)
6735    /// v3 = v2 + **world ごと population fingerprint 8 byte**
6736    /// ([`Self::population_fingerprint`]、rollback の body 数不一致検査強化)
6737    ///
6738    /// 被覆を足す時はここを上げ、[`Self::deserialize_state`] で分岐する
6739    /// (不一致を silent に読み替えない — 旧 version の blob は `false` で拒否、
6740    /// v1 / v2 の blob は v3 実装に対して version 不一致で拒否される、
6741    /// 既存の v1→v2 bump と同じ方針)
6742    pub const STATE_VERSION: u16 = 3;
6743
6744    /// Serialize world state (for rollback netcode).
6745    ///
6746    /// # 被覆 (v1、2026-09-30)
6747    ///
6748    /// | 範囲 | 内容 |
6749    /// |---|---|
6750    /// | `[0..4)` | magic [`Self::STATE_MAGIC`] (`b"APHY"`) |
6751    /// | `[4..6)` | version u16 = [`Self::STATE_VERSION`] |
6752    /// | `[6..8)` | reserved u16 = 0 (将来の flag 用、8 byte 境界揃え) |
6753    /// | `[8..12)` | body 数 u32 |
6754    /// | `[12..)` | body ごとに 208 byte = position 48 + velocity 48 + rotation 64 + angular_velocity 48 |
6755    /// | 続き | body ごとに 5 byte = [`SleepState`] u8 + `idle_frames` u32 |
6756    /// | 続き | world ごと overflow flag 1 byte (v2) |
6757    /// | 続き | world ごと population fingerprint u64 (v3、[`Self::population_fingerprint`]) |
6758    ///
6759    /// ⚠️ **`SleepState` + `idle_frames` を v1 で被覆に入れた** (WM-08)
6760    /// 旧 format は body の運動状態だけを持ち、`deserialize_state` 末尾の
6761    /// `IslandManager::new` が sleep 状態を 0 に戻していたため、
6762    /// **巻き戻した先で眠るタイミングがずれて軌道が割れた**
6763    /// (`tests/wm08_state_coverage.rs` が対照実験付きで red を実測してから実装)
6764    ///
6765    /// ⚠️ **magic と version を入れた理由**: 旧 blob は先頭が body 数なので、
6766    /// 検査しないと**新実装が旧 blob を誤って解釈する** magic 不一致 /
6767    /// version 不一致は `deserialize_state` が `false` を返して明示的に拒否する
6768    ///
6769    /// 依然として保存 (= 復元) しないもの: constraints / joints / force
6770    /// fields / collision radii / filters / materials (rollback netcode
6771    /// では game state から毎 frame 再構築される前提) ⚠️ **v3 の population
6772    /// fingerprint はこれらの値を検査にのみ使う** (復元はしない) ので
6773    /// 「保存しない」契約自体は変わらない
6774    ///
6775    /// ⚠️ [`crate::netcode::SimulationChecksum`] は **本 blob の全 byte から
6776    /// 導出**される (被覆の単一源化、B-13) ので、ここに state を足すと
6777    /// checksum も自動で追従する
6778    #[must_use]
6779    pub fn serialize_state(&self) -> Vec<u8> {
6780        let mut data = Vec::new();
6781
6782        // Header: magic + version + reserved
6783        data.extend_from_slice(&Self::STATE_MAGIC);
6784        data.extend_from_slice(&Self::STATE_VERSION.to_le_bytes());
6785        data.extend_from_slice(&0u16.to_le_bytes());
6786
6787        // Body count
6788        let count = self.bodies.len() as u32;
6789        data.extend_from_slice(&count.to_le_bytes());
6790
6791        // Body states
6792        for body in &self.bodies {
6793            // Position (3 x Fix128 = 48 bytes)
6794            data.extend_from_slice(&body.position.x.hi.to_le_bytes());
6795            data.extend_from_slice(&body.position.x.lo.to_le_bytes());
6796            data.extend_from_slice(&body.position.y.hi.to_le_bytes());
6797            data.extend_from_slice(&body.position.y.lo.to_le_bytes());
6798            data.extend_from_slice(&body.position.z.hi.to_le_bytes());
6799            data.extend_from_slice(&body.position.z.lo.to_le_bytes());
6800
6801            // Velocity (3 x Fix128 = 48 bytes)
6802            data.extend_from_slice(&body.velocity.x.hi.to_le_bytes());
6803            data.extend_from_slice(&body.velocity.x.lo.to_le_bytes());
6804            data.extend_from_slice(&body.velocity.y.hi.to_le_bytes());
6805            data.extend_from_slice(&body.velocity.y.lo.to_le_bytes());
6806            data.extend_from_slice(&body.velocity.z.hi.to_le_bytes());
6807            data.extend_from_slice(&body.velocity.z.lo.to_le_bytes());
6808
6809            // Rotation (4 x Fix128 = 64 bytes)
6810            data.extend_from_slice(&body.rotation.x.hi.to_le_bytes());
6811            data.extend_from_slice(&body.rotation.x.lo.to_le_bytes());
6812            data.extend_from_slice(&body.rotation.y.hi.to_le_bytes());
6813            data.extend_from_slice(&body.rotation.y.lo.to_le_bytes());
6814            data.extend_from_slice(&body.rotation.z.hi.to_le_bytes());
6815            data.extend_from_slice(&body.rotation.z.lo.to_le_bytes());
6816            data.extend_from_slice(&body.rotation.w.hi.to_le_bytes());
6817            data.extend_from_slice(&body.rotation.w.lo.to_le_bytes());
6818
6819            // Angular velocity (3 x Fix128 = 48 bytes)
6820            data.extend_from_slice(&body.angular_velocity.x.hi.to_le_bytes());
6821            data.extend_from_slice(&body.angular_velocity.x.lo.to_le_bytes());
6822            data.extend_from_slice(&body.angular_velocity.y.hi.to_le_bytes());
6823            data.extend_from_slice(&body.angular_velocity.y.lo.to_le_bytes());
6824            data.extend_from_slice(&body.angular_velocity.z.hi.to_le_bytes());
6825            data.extend_from_slice(&body.angular_velocity.z.lo.to_le_bytes());
6826        }
6827
6828        // Sleep 状態 (body ごとに 5 byte、WM-08)
6829        //
6830        // ⚠️ `sleep_data` は body 数に合わせて resize されている前提だが、
6831        // 短い場合は既定 (Awake / 0) で埋める — blob の長さを body 数だけで
6832        // 決められるようにして、読み側の長さ検査を単純に保つ
6833        for i in 0..self.bodies.len() {
6834            let sd = self
6835                .islands
6836                .sleep_data
6837                .get(i)
6838                .copied()
6839                .unwrap_or_else(SleepData::new);
6840            data.push(match sd.state {
6841                SleepState::Awake => 0,
6842                SleepState::Sleeping => 1,
6843            });
6844            data.extend_from_slice(&sd.idle_frames.to_le_bytes());
6845        }
6846
6847        // overflow flag (world ごと 1 byte、v2、doctrine B-12)
6848        //
6849        // ⚠️ **flag は状態の一部** — blob に入れないと overflow した枝を
6850        // 巻き戻した先で `undecided` が消えて B-12 が目的を達成しない
6851        data.push(u8::from(self.overflow_detected));
6852
6853        // population fingerprint (world ごと 8 byte、v3)
6854        //
6855        // ⚠️ **位置 / 速度 / 回転 / sleep は含めない** ([`Self::population_fingerprint`]
6856        // 参照) — blob 自身が復元する量なので、ここに混ぜると検査が
6857        // 「基準が内側」になり population 不一致を検出できなくなる
6858        data.extend_from_slice(&self.population_fingerprint().to_le_bytes());
6859
6860        data
6861    }
6862
6863    /// FNV-1a 64-bit hash of per-body quantities [`Self::serialize_state`]
6864    /// does **not** restore (shape/mass/inertia/collider/filter/material),
6865    /// in body index order.
6866    ///
6867    /// # なぜこれが要るか (rollback body 数不一致、gap #3)
6868    ///
6869    /// [`Self::deserialize_state`] は body **数**の一致しか見ていない
6870    /// `remove_body` は `swap_remove` なので、count が元に戻っても
6871    /// index↔body の対応 (= population) が変わっていることがある —
6872    /// その場合 count 一致だけでは検出できず、**別の body に誤った状態を
6873    /// 書き込んで silent に通る** (fail-fast になっていない)
6874    ///
6875    /// ⚠️ **位置 / 速度 / 回転 / sleep を含めない** — それらは blob 自身が
6876    /// 復元する量なので、ここに混ぜると「自分で組んだ量で自分を検査する」
6877    /// 循環になり population 不一致を検出できなくなる
6878    #[must_use]
6879    pub fn population_fingerprint(&self) -> u64 {
6880        let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
6881        for i in 0..self.bodies.len() {
6882            let body = &self.bodies[i];
6883            fnv1a_fold(&mut hash, &body.inv_mass.hi.to_le_bytes());
6884            fnv1a_fold(&mut hash, &body.inv_mass.lo.to_le_bytes());
6885            fnv1a_fold(&mut hash, &body.inv_inertia.x.hi.to_le_bytes());
6886            fnv1a_fold(&mut hash, &body.inv_inertia.x.lo.to_le_bytes());
6887            fnv1a_fold(&mut hash, &body.inv_inertia.y.hi.to_le_bytes());
6888            fnv1a_fold(&mut hash, &body.inv_inertia.y.lo.to_le_bytes());
6889            fnv1a_fold(&mut hash, &body.inv_inertia.z.hi.to_le_bytes());
6890            fnv1a_fold(&mut hash, &body.inv_inertia.z.lo.to_le_bytes());
6891            fnv1a_fold(&mut hash, &[body.body_type as u8]);
6892            fnv1a_fold(&mut hash, &[u8::from(body.is_sensor)]);
6893            match self.body_collision_radii.get(i).copied().flatten() {
6894                Some(r) => {
6895                    fnv1a_fold(&mut hash, &[1u8]);
6896                    fnv1a_fold(&mut hash, &r.hi.to_le_bytes());
6897                    fnv1a_fold(&mut hash, &r.lo.to_le_bytes());
6898                }
6899                None => fnv1a_fold(&mut hash, &[0u8]),
6900            }
6901            let filter = self.body_filter(i);
6902            fnv1a_fold(&mut hash, &filter.layer.to_le_bytes());
6903            fnv1a_fold(&mut hash, &filter.mask.to_le_bytes());
6904            fnv1a_fold(&mut hash, &filter.group.to_le_bytes());
6905            let material = self
6906                .body_materials
6907                .get(i)
6908                .copied()
6909                .unwrap_or(crate::material::DEFAULT_MATERIAL);
6910            fnv1a_fold(&mut hash, &material.to_le_bytes());
6911        }
6912        hash
6913    }
6914
6915    /// Deserialize world state (for rollback netcode).
6916    ///
6917    /// Restores per-body transforms. The rotations are restored as stored
6918    /// and brought to unit length by the next step, so a non-unit rotation in
6919    /// `data` scales what it rotates by `|q|^2` in the queries made before
6920    /// that step (see [`RigidBody::rotation`]). Parallel arrays (collision radii,
6921    /// filters, materials, island manager) are resized to match the body
6922    /// count, preserving existing entries and zero-filling new ones.
6923    ///
6924    /// # Rollback の契約 (gap #3、2026-10-02)
6925    ///
6926    /// body **数**が一致していても **population** (どの body がどんな
6927    /// `inv_mass` / `inv_inertia` / collider / filter / material を持つか)
6928    /// が一致していなければ拒否する ([`Self::population_fingerprint`]、v3)
6929    /// caller は body の生成・破棄 (`add_body` / `remove_body`) と joints /
6930    /// force fields / filters / collider radii / materials の変更を、目標
6931    /// frame まで決定論的に replay してから本関数を呼ぶこと ⚠️
6932    /// **joints の再構築は本関数の呼び出し前に行う** — 本関数の末尾で
6933    /// island を joints から作り直すため、順序が逆だと sleep の島が
6934    /// 異なる形になる
6935    ///
6936    /// 不一致 (`false`) の場合、`self` は一切変更されない (すべての検査は
6937    /// body 状態を書き込む前に完了する)
6938    pub fn deserialize_state(&mut self, data: &[u8]) -> bool {
6939        // Header 12 byte: magic 4 + version 2 + reserved 2 + count 4
6940        if data.len() < 12 {
6941            return false;
6942        }
6943        // ⚠️ magic / version を検査しないと **旧 blob (先頭が body 数) を
6944        // 新 format として誤って解釈する** 不一致は明示的に拒否する
6945        if data[0..4] != Self::STATE_MAGIC {
6946            return false;
6947        }
6948        if u16::from_le_bytes([data[4], data[5]]) != Self::STATE_VERSION {
6949            return false;
6950        }
6951        // ⚠️ reserved も検査する — 検査しないと **blob の中に silent に
6952        // 無視される byte が残り**、「1 byte でも壊れたらどれかの field に
6953        // 反映される」という不変条件に穴が空く (将来 flag に使う時は
6954        // `STATE_VERSION` を上げて分岐する)
6955        if u16::from_le_bytes([data[6], data[7]]) != 0 {
6956            return false;
6957        }
6958
6959        let count = u32::from_le_bytes([data[8], data[9], data[10], data[11]]) as usize;
6960
6961        if count != self.bodies.len() {
6962            return false;
6963        }
6964
6965        // 長さ検査は body 数から一意に決まる (header + 運動状態 + sleep + overflow + fingerprint)
6966        let expected = 12 + count * 208 + count * 5 + 1 + 8;
6967        if data.len() < expected {
6968            return false;
6969        }
6970
6971        // ⚠️ **population fingerprint は body 状態を 1 byte も書く前に検査する**
6972        // (v3、gap #3) count が一致していても `swap_remove` + `add_body` で
6973        // population (= inv_mass / inertia / collider 等の組み合わせ) が
6974        // 変わっていれば拒否する — ここで return すれば self は未変更のまま
6975        let fp_offset = expected - 8;
6976        let stored_fingerprint = u64::from_le_bytes([
6977            data[fp_offset],
6978            data[fp_offset + 1],
6979            data[fp_offset + 2],
6980            data[fp_offset + 3],
6981            data[fp_offset + 4],
6982            data[fp_offset + 5],
6983            data[fp_offset + 6],
6984            data[fp_offset + 7],
6985        ]);
6986        if stored_fingerprint != self.population_fingerprint() {
6987            return false;
6988        }
6989
6990        // Per-body: position(48) + velocity(48) + rotation(64) + angular_velocity(48) = 208 bytes
6991        let mut offset = 12;
6992        for body in &mut self.bodies {
6993            if offset + 208 > data.len() {
6994                return false;
6995            }
6996
6997            // Helper to read Fix128
6998            let read_fix128 = |o: &mut usize| -> Fix128 {
6999                let hi = i64::from_le_bytes([
7000                    data[*o],
7001                    data[*o + 1],
7002                    data[*o + 2],
7003                    data[*o + 3],
7004                    data[*o + 4],
7005                    data[*o + 5],
7006                    data[*o + 6],
7007                    data[*o + 7],
7008                ]);
7009                *o += 8;
7010                let lo = u64::from_le_bytes([
7011                    data[*o],
7012                    data[*o + 1],
7013                    data[*o + 2],
7014                    data[*o + 3],
7015                    data[*o + 4],
7016                    data[*o + 5],
7017                    data[*o + 6],
7018                    data[*o + 7],
7019                ]);
7020                *o += 8;
7021                Fix128 { hi, lo }
7022            };
7023
7024            body.position.x = read_fix128(&mut offset);
7025            body.position.y = read_fix128(&mut offset);
7026            body.position.z = read_fix128(&mut offset);
7027
7028            body.velocity.x = read_fix128(&mut offset);
7029            body.velocity.y = read_fix128(&mut offset);
7030            body.velocity.z = read_fix128(&mut offset);
7031
7032            body.rotation.x = read_fix128(&mut offset);
7033            body.rotation.y = read_fix128(&mut offset);
7034            body.rotation.z = read_fix128(&mut offset);
7035            body.rotation.w = read_fix128(&mut offset);
7036
7037            body.angular_velocity.x = read_fix128(&mut offset);
7038            body.angular_velocity.y = read_fix128(&mut offset);
7039            body.angular_velocity.z = read_fix128(&mut offset);
7040        }
7041
7042        // Resync parallel arrays to match body count (extend AND truncate)
7043        let n = self.bodies.len();
7044        while self.body_collision_radii.len() < n {
7045            self.body_collision_radii.push(None);
7046        }
7047        self.body_collision_radii.truncate(n);
7048        self.body_colliders.resize(n, None);
7049        while self.body_filters.len() < n {
7050            self.body_filters.push(CollisionFilter::DEFAULT);
7051        }
7052        self.body_filters.truncate(n);
7053        while self.body_materials.len() < n {
7054            self.body_materials.push(crate::material::DEFAULT_MATERIAL);
7055        }
7056        self.body_materials.truncate(n);
7057        #[cfg(feature = "std")]
7058        self.sync_body_stable_ids();
7059        // ⚠️ `IslandManager::new` は sleep_data を既定 (Awake / idle_frames 0) に
7060        // 戻すので、**この後に blob から復元する** 順序を逆にすると復元が消える
7061        self.islands = IslandManager::new(n, self.islands.config);
7062
7063        // Sleep 状態の復元 (WM-08) — body の運動状態を読んだ直後から続く
7064        for i in 0..n {
7065            let state = match data[offset] {
7066                0 => SleepState::Awake,
7067                1 => SleepState::Sleeping,
7068                // 未知の値は拒否する (silent に Awake へ倒すと被覆の穴が
7069                // 「復元できた」ように見える)
7070                _ => return false,
7071            };
7072            offset += 1;
7073            let idle_frames = u32::from_le_bytes([
7074                data[offset],
7075                data[offset + 1],
7076                data[offset + 2],
7077                data[offset + 3],
7078            ]);
7079            offset += 4;
7080            if let Some(sd) = self.islands.sleep_data.get_mut(i) {
7081                sd.state = state;
7082                sd.idle_frames = idle_frames;
7083            }
7084        }
7085
7086        // overflow flag の復元 (v2)
7087        //
7088        // ⚠️ **読み込んだ状態に従う** (立てるだけでなく下げる) — sticky は
7089        // 「step を重ねても落ちない」ことであって、**別の枝を読み込んでも
7090        // 残る**ことではない 残すと探索で枝を跨いで汚染する
7091        self.overflow_detected = match data[offset] {
7092            0 => false,
7093            1 => true,
7094            // 未知の値は拒否 (silent に false へ倒すと undecided が消える)
7095            _ => return false,
7096        };
7097
7098        true
7099    }
7100}
7101
7102impl core::fmt::Debug for PhysicsWorld {
7103    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
7104        f.debug_struct("PhysicsWorld")
7105            .field("config", &self.config)
7106            .field("bodies", &self.bodies.len())
7107            .field("distance_constraints", &self.distance_constraints.len())
7108            .field("contact_constraints", &self.contact_constraints.len())
7109            .field("sdf_colliders", &self.sdf_colliders.len())
7110            .field("static_colliders", &self.static_colliders.len())
7111            .field("joints", &self.joints.len())
7112            .field("force_fields", &self.force_fields.len())
7113            .finish_non_exhaustive()
7114    }
7115}
7116
7117#[cfg(all(test, feature = "std"))]
7118mod tests {
7119    use super::*;
7120
7121    /// oracle: [`RigidBody::world_inv_inertia_apply`] (unchecked). On inputs
7122    /// in range the checked form gives the same bits, for rotations about
7123    /// several axes and an anisotropic `inv_inertia` (`1, 2, 4`), so a
7124    /// dropped rotation, a component taken from the wrong axis or a sign
7125    /// error in the checked quaternion product changes the result. The
7126    /// fixture asserts that rotation and anisotropy both matter here.
7127    #[test]
7128    fn checked_world_inv_inertia_apply_is_the_unchecked_one_in_range() {
7129        let torques = [
7130            Vec3Fix::from_int(3, -1, 2),
7131            Vec3Fix::new(
7132                Fix128::from_ratio(-5, 7),
7133                Fix128::from_ratio(11, 3),
7134                Fix128::from_int(-9),
7135            ),
7136        ];
7137        let rotations = [
7138            QuatFix::from_axis_angle(Vec3Fix::from_int(1, 2, 3), Fix128::from_ratio(7, 10)),
7139            QuatFix::from_axis_angle(Vec3Fix::from_int(-2, 1, 0), Fix128::from_ratio(-13, 5)),
7140            QuatFix::from_axis_angle(Vec3Fix::from_int(0, 0, 1), Fix128::from_ratio(1, 3)),
7141        ];
7142        for q in rotations {
7143            let mut body = RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE);
7144            body.inv_inertia = Vec3Fix::from_int(1, 2, 4);
7145            body.rotation = q;
7146            for tau in torques {
7147                let unchecked = body.world_inv_inertia_apply(tau);
7148                assert_eq!(
7149                    body.checked_world_inv_inertia_apply(tau),
7150                    Some(unchecked),
7151                    "q {q:?} tau {tau:?}"
7152                );
7153                let mut unrotated = body;
7154                unrotated.rotation = QuatFix::IDENTITY;
7155                assert_ne!(
7156                    unrotated.world_inv_inertia_apply(tau),
7157                    unchecked,
7158                    "fixture: the rotation must matter"
7159                );
7160                let mut isotropic = body;
7161                isotropic.inv_inertia = Vec3Fix::from_int(2, 2, 2);
7162                assert_ne!(
7163                    isotropic.world_inv_inertia_apply(tau),
7164                    unchecked,
7165                    "fixture: the anisotropy must matter"
7166                );
7167            }
7168        }
7169    }
7170
7171    /// oracle: the key is a function of (kind, ids, ordinal) only. Entries
7172    /// of one pair get distinct keys in arrival order, a second pass with a
7173    /// fresh counter reproduces them, and kind or direction separate keys.
7174    #[test]
7175    fn tgs_cache_key_separates_ordinal_kind_and_pair() {
7176        let mut ord = std::collections::BTreeMap::new();
7177        let k0 = tgs_cache_key(TGS_KEY_CONTACT, 3, 7, &mut ord);
7178        let k1 = tgs_cache_key(TGS_KEY_CONTACT, 3, 7, &mut ord);
7179        let other = tgs_cache_key(TGS_KEY_CONTACT, 3, 8, &mut ord);
7180        assert_ne!(k0, k1, "two entries of one pair must not share a key");
7181        let mut again = std::collections::BTreeMap::new();
7182        assert_eq!(tgs_cache_key(TGS_KEY_CONTACT, 3, 7, &mut again), k0);
7183        assert_eq!(tgs_cache_key(TGS_KEY_CONTACT, 3, 7, &mut again), k1);
7184        let mut fresh = std::collections::BTreeMap::new();
7185        assert_ne!(tgs_cache_key(TGS_KEY_DISTANCE, 3, 7, &mut fresh), k0);
7186        let mut fresh = std::collections::BTreeMap::new();
7187        assert_ne!(tgs_cache_key(TGS_KEY_CONTACT, 7, 3, &mut fresh), k0);
7188        assert_ne!(other, k0);
7189        assert_eq!(ord.get(&(3, 7)), Some(&2));
7190    }
7191
7192    #[test]
7193    fn test_rigid_body_creation() {
7194        let body = RigidBody::new(Vec3Fix::ZERO, Fix128::ONE);
7195        assert!(!body.is_static());
7196
7197        let static_body = RigidBody::new_static(Vec3Fix::ZERO);
7198        assert!(static_body.is_static());
7199    }
7200
7201    #[test]
7202    fn test_gravity_integration() {
7203        let config = SolverConfig::default();
7204        let mut world = PhysicsWorld::new(config);
7205
7206        let body = RigidBody::new(Vec3Fix::from_int(0, 10, 0), Fix128::ONE);
7207        world.add_body(body);
7208
7209        // Step for 1 second (in 10 steps of 0.1s)
7210        for _ in 0..10 {
7211            world.step(Fix128::from_ratio(1, 10));
7212        }
7213
7214        // Body should have fallen
7215        let body = world.get_body(0).unwrap();
7216        assert!(
7217            body.position.y < Fix128::from_int(10),
7218            "Body should have fallen"
7219        );
7220    }
7221
7222    #[test]
7223    fn test_distance_constraint() {
7224        let config = SolverConfig {
7225            substeps: 4,
7226            iterations: 8,
7227            gravity: Vec3Fix::ZERO, // No gravity for constraint test
7228            ..Default::default()
7229        };
7230        let mut world = PhysicsWorld::new(config);
7231
7232        // Two bodies connected by a distance constraint
7233        let a = world.add_body(RigidBody::new_static(Vec3Fix::ZERO));
7234        let b = world.add_body(RigidBody::new(Vec3Fix::from_int(5, 0, 0), Fix128::ONE));
7235
7236        world.add_distance_constraint(DistanceConstraint::new(
7237            a,
7238            b,
7239            Vec3Fix::ZERO,
7240            Vec3Fix::ZERO,
7241            Fix128::from_int(3), // Target distance: 3
7242        ));
7243
7244        // Step simulation
7245        for _ in 0..100 {
7246            world.step(Fix128::from_ratio(1, 60));
7247        }
7248
7249        // Body B should be approximately 3 units from body A
7250        let body_b = world.get_body(b).unwrap();
7251        let distance = body_b.position.length();
7252
7253        // Allow some tolerance due to gravity
7254        assert!(
7255            distance < Fix128::from_int(5),
7256            "Constraint should pull body closer"
7257        );
7258    }
7259
7260    #[test]
7261    fn test_state_serialization() {
7262        let config = SolverConfig::default();
7263        let mut world = PhysicsWorld::new(config);
7264
7265        world.add_body(RigidBody::new(Vec3Fix::from_int(1, 2, 3), Fix128::ONE));
7266        world.add_body(RigidBody::new(
7267            Vec3Fix::from_int(4, 5, 6),
7268            Fix128::from_int(2),
7269        ));
7270
7271        let state = world.serialize_state();
7272
7273        // Deserialize into another world with the same population (same
7274        // mass per body index) but different starting positions — v3 の
7275        // population fingerprint は mass 等の組み合わせが一致している
7276        // ことを要求する (gap #3、mass が異なれば別 population として拒否)
7277        let mut world2 = PhysicsWorld::new(config);
7278        world2.add_body(RigidBody::new(Vec3Fix::ZERO, Fix128::ONE));
7279        world2.add_body(RigidBody::new(Vec3Fix::ZERO, Fix128::from_int(2)));
7280
7281        assert!(world2.deserialize_state(&state));
7282
7283        assert_eq!(world2.bodies[0].position.x.hi, 1);
7284        assert_eq!(world2.bodies[0].position.y.hi, 2);
7285        assert_eq!(world2.bodies[1].position.x.hi, 4);
7286    }
7287
7288    #[test]
7289    fn test_determinism() {
7290        // Run the same simulation twice and verify identical results
7291        let config = SolverConfig::default();
7292
7293        let run_simulation = || {
7294            let mut world = PhysicsWorld::new(config);
7295            world.add_body(RigidBody::new(Vec3Fix::from_int(0, 10, 0), Fix128::ONE));
7296            world.add_body(RigidBody::new(
7297                Vec3Fix::from_int(5, 10, 0),
7298                Fix128::from_int(2),
7299            ));
7300
7301            for _ in 0..100 {
7302                world.step(Fix128::from_ratio(1, 60));
7303            }
7304
7305            world.serialize_state()
7306        };
7307
7308        let state1 = run_simulation();
7309        let state2 = run_simulation();
7310
7311        assert_eq!(state1, state2, "Simulation must be deterministic");
7312    }
7313
7314    #[test]
7315    fn test_body_material_assignment() {
7316        let config = SolverConfig::default();
7317        let mut world = PhysicsWorld::new(config);
7318
7319        let metal_id = world.material_table.register_metal();
7320        let rubber_id = world.material_table.register_rubber();
7321
7322        let a = world.add_body(RigidBody::new(Vec3Fix::ZERO, Fix128::ONE));
7323        let b = world.add_body(RigidBody::new(Vec3Fix::from_int(2, 0, 0), Fix128::ONE));
7324
7325        world.set_body_material(a, metal_id);
7326        world.set_body_material(b, rubber_id);
7327
7328        let combined = world.combined_material(a, b);
7329        assert!(
7330            combined.friction > Fix128::ZERO,
7331            "Combined friction should be positive"
7332        );
7333    }
7334
7335    #[test]
7336    fn test_contact_cache_integration() {
7337        let config = SolverConfig::default();
7338        let mut world = PhysicsWorld::new(config);
7339
7340        let a = world.add_body(RigidBody::new(Vec3Fix::ZERO, Fix128::ONE));
7341        let b = world.add_body(RigidBody::new(Vec3Fix::from_int(1, 0, 0), Fix128::ONE));
7342
7343        world.begin_frame();
7344
7345        let contact = Contact {
7346            depth: Fix128::from_ratio(1, 10),
7347            normal: Vec3Fix::UNIT_X,
7348            point_a: Vec3Fix::from_int(1, 0, 0),
7349            point_b: Vec3Fix::from_int(1, 0, 0),
7350        };
7351        world.add_contact_with_material(a, b, contact);
7352
7353        world.end_frame();
7354
7355        // Contact cache should have the manifold
7356        let key = crate::contact_cache::BodyPairKey::new(a, b);
7357        let manifold = world.contact_cache.find(&key);
7358        assert!(
7359            manifold.is_some(),
7360            "Contact cache should contain the manifold"
7361        );
7362    }
7363
7364    #[test]
7365    fn test_kinematic_body() {
7366        let config = SolverConfig::default();
7367        let mut world = PhysicsWorld::new(config);
7368
7369        let kinematic = RigidBody::new_kinematic(Vec3Fix::ZERO);
7370        let k_id = world.add_body(kinematic);
7371
7372        // Set kinematic target
7373        world.bodies[k_id].set_kinematic_target(Vec3Fix::from_int(5, 0, 0), QuatFix::IDENTITY);
7374
7375        world.step(Fix128::from_ratio(1, 60));
7376
7377        // Should have moved to target
7378        let pos = world.bodies[k_id].position;
7379        assert_eq!(pos.x.hi, 5, "Kinematic body should move to target");
7380    }
7381
7382    #[test]
7383    fn test_body_type_enum() {
7384        let dynamic = RigidBody::new(Vec3Fix::ZERO, Fix128::ONE);
7385        assert!(dynamic.is_dynamic());
7386        assert!(!dynamic.is_kinematic());
7387
7388        let static_b = RigidBody::new_static(Vec3Fix::ZERO);
7389        assert!(static_b.is_static());
7390        assert!(!static_b.is_dynamic());
7391
7392        let kinematic = RigidBody::new_kinematic(Vec3Fix::ZERO);
7393        assert!(kinematic.is_kinematic());
7394        assert!(kinematic.is_static()); // inv_mass is zero
7395    }
7396
7397    #[test]
7398    fn test_per_body_gravity_scale() {
7399        let config = SolverConfig::default();
7400        let mut world = PhysicsWorld::new(config);
7401
7402        // Body with zero gravity
7403        let mut no_grav = RigidBody::new(Vec3Fix::from_int(0, 10, 0), Fix128::ONE);
7404        no_grav.gravity_scale = Fix128::ZERO;
7405        let ng_id = world.add_body(no_grav);
7406
7407        // Body with normal gravity
7408        let normal = RigidBody::new(Vec3Fix::from_int(5, 10, 0), Fix128::ONE);
7409        let n_id = world.add_body(normal);
7410
7411        for _ in 0..60 {
7412            world.step(Fix128::from_ratio(1, 60));
7413        }
7414
7415        // No-gravity body should not have fallen
7416        let ng_y = world.bodies[ng_id].position.y;
7417        assert!(
7418            ng_y > Fix128::from_int(9),
7419            "Zero-gravity body should stay near y=10, got {ng_y:?}"
7420        );
7421
7422        // Normal body should have fallen (damping is strong, just check it fell at all)
7423        let n_y = world.bodies[n_id].position.y;
7424        assert!(
7425            n_y < Fix128::from_int(10),
7426            "Normal gravity body should fall"
7427        );
7428    }
7429
7430    #[test]
7431    fn test_sdf_ground_collision() {
7432        use crate::math::QuatFix;
7433        use crate::sdf_collider::{ClosureSdf, SdfCollider};
7434
7435        let config = SolverConfig::default();
7436        let mut world = PhysicsWorld::new(config);
7437        world.set_sdf_collision_radius(Fix128::from_ratio(1, 2)); // 0.5
7438
7439        // Add falling body at y=5
7440        let body = RigidBody::new(Vec3Fix::from_int(0, 5, 0), Fix128::ONE);
7441        let body_id = world.add_body(body);
7442
7443        // Add ground plane SDF at y=0
7444        let ground = ClosureSdf::new(|_x, y, _z| y, |_x, _y, _z| (0.0, 1.0, 0.0));
7445        world.add_sdf_collider(SdfCollider::new_static(
7446            Box::new(ground),
7447            Vec3Fix::ZERO,
7448            QuatFix::IDENTITY,
7449        ));
7450
7451        // Simulate 2 seconds (120 frames at 60fps)
7452        let dt = Fix128::from_ratio(1, 60);
7453        for _ in 0..120 {
7454            world.step(dt);
7455        }
7456
7457        // Body should have fallen but been stopped by SDF ground
7458        let pos = world.bodies[body_id].position;
7459        let y = pos.y.to_f32();
7460
7461        // Body should be near the ground (y ~ collision_radius = 0.5)
7462        assert!(y < 5.0, "Body should have fallen from y=5");
7463        assert!(
7464            y > -1.0,
7465            "Body should not have fallen through SDF ground, y={y}"
7466        );
7467    }
7468
7469    #[test]
7470    fn test_warm_start_factor_default() {
7471        let config = SolverConfig::default();
7472        // デフォルト 0.85
7473        let expected = Fix128::from_ratio(85, 100);
7474        assert_eq!(config.warm_start_factor, expected);
7475    }
7476
7477    #[test]
7478    fn test_warm_start_factor_convergence() {
7479        // warm_start_factor = 0.85 vs 0.0(無効)で収束速度を比較
7480        let make_world = |wsf: Fix128| {
7481            let config = SolverConfig {
7482                substeps: 4,
7483                iterations: 2, // 少ないイテレーションで差が出やすい
7484                gravity: Vec3Fix::ZERO,
7485                warm_start_factor: wsf,
7486                ..Default::default()
7487            };
7488            let mut world = PhysicsWorld::new(config);
7489            let a = world.add_body(RigidBody::new_static(Vec3Fix::ZERO));
7490            let b = world.add_body(RigidBody::new(Vec3Fix::from_int(5, 0, 0), Fix128::ONE));
7491            let constraint =
7492                DistanceConstraint::new(a, b, Vec3Fix::ZERO, Vec3Fix::ZERO, Fix128::from_int(3));
7493            world.add_distance_constraint(constraint);
7494            world
7495        };
7496
7497        let mut world_warm = make_world(Fix128::from_ratio(85, 100));
7498        let mut world_cold = make_world(Fix128::ZERO);
7499
7500        let dt = Fix128::from_ratio(1, 60);
7501        for _ in 0..30 {
7502            world_warm.step(dt);
7503            world_cold.step(dt);
7504        }
7505
7506        // Both should converge, but warm-started should be closer to target distance
7507        let dist_warm = (world_warm.bodies[1].position - world_warm.bodies[0].position)
7508            .length()
7509            .to_f32();
7510        let dist_cold = (world_cold.bodies[1].position - world_cold.bodies[0].position)
7511            .length()
7512            .to_f32();
7513
7514        let target = 3.0_f32;
7515        let err_warm = (dist_warm - target).abs();
7516        let err_cold = (dist_cold - target).abs();
7517        // warm-start版の誤差が小さいか、同等であること
7518        assert!(
7519            err_warm <= err_cold + 0.1,
7520            "Warm-start should converge at least as well: err_warm={err_warm}, err_cold={err_cold}"
7521        );
7522    }
7523
7524    #[test]
7525    fn test_contact_warm_start_cached_lambda() {
7526        // ContactConstraint の cached_lambda がデフォルトでゼロ
7527        let contact = Contact {
7528            depth: Fix128::from_ratio(1, 10),
7529            normal: Vec3Fix::UNIT_Y,
7530            point_a: Vec3Fix::ZERO,
7531            point_b: Vec3Fix::ZERO,
7532        };
7533        let cc = ContactConstraint::new(0, 1, contact);
7534        assert_eq!(cc.cached_lambda, Fix128::ZERO);
7535    }
7536
7537    #[test]
7538    fn test_manual_contact_pushes_body_out_by_depth() {
7539        // 手動 contact は substep が clear_contacts するので (1.2.0)、solver level の
7540        // `solve_contact_constraints` を直接 iterations 回呼んで検証
7541        let config = SolverConfig {
7542            substeps: 1,
7543            iterations: 2,
7544            gravity: Vec3Fix::ZERO,
7545            ..Default::default()
7546        };
7547        let mut world = PhysicsWorld::new(config);
7548        let _a = world.add_body(RigidBody::new_static(Vec3Fix::ZERO));
7549        let _b = world.add_body(RigidBody::new(
7550            Vec3Fix::from_f32(0.0, 0.05, 0.0),
7551            Fix128::ONE,
7552        ));
7553
7554        // normal は B→A 方向(ドキュメント仕様)。body_a が下にあるので下向き。
7555        // correction は normal 方向。body_a += correction, body_b -= correction。
7556        // body_a=static, body_b は -(-Y) = +Y に押し出される。
7557        let contact = Contact {
7558            depth: Fix128::from_ratio(5, 100), // 0.05 m 侵入
7559            normal: -Vec3Fix::UNIT_Y,          // B→A = 下向き
7560            point_a: Vec3Fix::ZERO,
7561            point_b: Vec3Fix::from_f32(0.0, 0.05, 0.0),
7562        };
7563        world.add_contact(ContactConstraint::new(0, 1, contact));
7564
7565        let dt = Fix128::from_ratio(1, 60);
7566        for _ in 0..world.config.iterations {
7567            world.solve_contact_constraints(dt);
7568        }
7569
7570        // body_b は depth ちょうど 1 回分 (0.05) 上に押し出される、2 iteration でも 2 倍にならない
7571        let y = world.bodies[1].position.y.to_f32();
7572        assert!(
7573            (y - 0.10).abs() < 1e-5,
7574            "Body should be pushed up by depth once: y={y}"
7575        );
7576    }
7577
7578    // -------------------------------------------------------------------
7579    // v0.11.0: PhysicsWorld auto-routing to installed GPU solver bridge
7580    // -------------------------------------------------------------------
7581
7582    /// Recording bridge implementation for the v0.11.0 auto-routing
7583    /// tests. Counts invocations of contact-solve methods via shared
7584    /// `Arc<AtomicUsize>` counters so the test can assert routing
7585    /// occurred without needing to reach inside the boxed trait object.
7586    ///
7587    /// All methods are no-ops on world state (positions are not
7588    /// modified), so a test that observes position drift while this
7589    /// bridge is installed is proof that the CPU path ran — the bridge
7590    /// intentionally does not solve.
7591    #[cfg(feature = "gpu-solver-bridge")]
7592    struct RecordingBridge {
7593        send_contact_count: std::sync::Arc<core::sync::atomic::AtomicUsize>,
7594        send_body_state_count: std::sync::Arc<core::sync::atomic::AtomicUsize>,
7595        dispatch_count: std::sync::Arc<core::sync::atomic::AtomicUsize>,
7596        // v0.12.0 joint-solve counters (default zero; used only by the
7597        // joint auto-routing tests below)
7598        send_joints_count: std::sync::Arc<core::sync::atomic::AtomicUsize>,
7599        send_body_rotations_count: std::sync::Arc<core::sync::atomic::AtomicUsize>,
7600        dispatch_joint_count: std::sync::Arc<core::sync::atomic::AtomicUsize>,
7601    }
7602
7603    #[cfg(feature = "gpu-solver-bridge")]
7604    impl crate::gpu_bridge::GpuSolverBridge for RecordingBridge {
7605        fn send_island(&mut self, _p: &[[Fix128; 3]], _v: &[[Fix128; 3]]) {}
7606        fn dispatch_iterations(&mut self, _iters: u32, _dt: Fix128) {}
7607        fn recv_island(&self, _p: &mut [[Fix128; 3]], _v: &mut [[Fix128; 3]]) {}
7608        fn assert_bit_exact_vs_cpu(
7609            &self,
7610            _fixture: &crate::gpu_bridge::DiffFixture,
7611        ) -> Result<(), crate::gpu_bridge::GpuDivergence> {
7612            Ok(())
7613        }
7614        fn send_contact_constraints(&mut self, _c: &[ContactConstraint]) {
7615            self.send_contact_count
7616                .fetch_add(1, core::sync::atomic::Ordering::SeqCst);
7617        }
7618        fn send_body_state(&mut self, _p: &[[Fix128; 3]], _i: &[Fix128]) {
7619            self.send_body_state_count
7620                .fetch_add(1, core::sync::atomic::Ordering::SeqCst);
7621        }
7622        fn dispatch_contact_solve_iteration(&mut self, _warm_start_factor: Fix128) {
7623            self.dispatch_count
7624                .fetch_add(1, core::sync::atomic::Ordering::SeqCst);
7625        }
7626        fn recv_contact_constraints(&self, _c: &mut [ContactConstraint]) {}
7627        fn recv_body_positions(&self, _p: &mut [[Fix128; 3]]) {}
7628        fn send_joints(&mut self, _j: &[crate::joint::Joint]) {
7629            self.send_joints_count
7630                .fetch_add(1, core::sync::atomic::Ordering::SeqCst);
7631        }
7632        fn send_body_rotations(&mut self, _r: &[[Fix128; 4]]) {
7633            self.send_body_rotations_count
7634                .fetch_add(1, core::sync::atomic::Ordering::SeqCst);
7635        }
7636        fn dispatch_joint_solve_iteration(&mut self, _dt: Fix128) {
7637            self.dispatch_joint_count
7638                .fetch_add(1, core::sync::atomic::Ordering::SeqCst);
7639        }
7640    }
7641
7642    /// Two overlapping unit spheres in zero gravity: since 1.2.0 contacts are
7643    /// re-detected in every substep, so the contact the bridge tests observe
7644    /// has to come from `detect_collisions` (a manually added contact would
7645    /// be cleared at the start of the substep).
7646    #[cfg(feature = "gpu-solver-bridge")]
7647    fn build_one_contact_world() -> PhysicsWorld {
7648        let mut world = PhysicsWorld::new(SolverConfig {
7649            gravity: Vec3Fix::ZERO,
7650            ..SolverConfig::default()
7651        });
7652        let body_a = RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE);
7653        let body_b = RigidBody::new_dynamic(
7654            Vec3Fix::new(Fix128::from_ratio(19, 10), Fix128::ZERO, Fix128::ZERO),
7655            Fix128::ONE,
7656        );
7657        world.add_body_with_radius(body_a, Fix128::ONE);
7658        world.add_body_with_radius(body_b, Fix128::ONE);
7659        world
7660    }
7661
7662    /// v0.11.0: attaching a bridge via `set_gpu_solver_bridge` routes
7663    /// contact-solve through the bridge on every subsequent `step`.
7664    /// Verified by counting bridge method invocations.
7665    #[cfg(feature = "gpu-solver-bridge")]
7666    #[test]
7667    fn physics_world_step_auto_routes_through_bridge_when_attached() {
7668        let send_contact_count = std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0));
7669        let send_body_state_count = std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0));
7670        let dispatch_count = std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0));
7671
7672        let bridge = RecordingBridge {
7673            send_contact_count: std::sync::Arc::clone(&send_contact_count),
7674            send_body_state_count: std::sync::Arc::clone(&send_body_state_count),
7675            dispatch_count: std::sync::Arc::clone(&dispatch_count),
7676            send_joints_count: std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0)),
7677            send_body_rotations_count: std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0)),
7678            dispatch_joint_count: std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0)),
7679        };
7680
7681        let mut world = build_one_contact_world();
7682        world.set_gpu_solver_bridge(Some(Box::new(bridge)));
7683        assert!(world.gpu_solver_bridge_installed());
7684
7685        // substep は solve_contact_constraints を `config.iterations`
7686        // 回呼ぶ (default 4)。auto-routing 経路はその全てを bridge に
7687        // 転送するはずなので、send / dispatch は
7688        // `config.iterations` 回発火する。
7689        let iters = world.config.iterations;
7690        let dt = Fix128::from_ratio(1, 60);
7691        world.substep(dt);
7692
7693        assert_eq!(
7694            send_contact_count.load(core::sync::atomic::Ordering::SeqCst),
7695            iters,
7696            "send_contact_constraints must be invoked once per PGS iteration"
7697        );
7698        assert_eq!(
7699            send_body_state_count.load(core::sync::atomic::Ordering::SeqCst),
7700            iters,
7701            "send_body_state must be invoked once per PGS iteration"
7702        );
7703        assert_eq!(
7704            dispatch_count.load(core::sync::atomic::Ordering::SeqCst),
7705            iters,
7706            "dispatch_contact_solve_iteration must fire once per PGS iteration"
7707        );
7708    }
7709
7710    /// v0.11.0: without a bridge installed, `step` falls back to the CPU
7711    /// contact solver. Verified by observing that contact-affected body
7712    /// positions drift — the CPU path applies the position correction
7713    /// that the recording bridge above would not.
7714    #[cfg(feature = "gpu-solver-bridge")]
7715    #[test]
7716    fn physics_world_step_falls_back_to_cpu_when_bridge_detached() {
7717        let mut world = build_one_contact_world();
7718        assert!(!world.gpu_solver_bridge_installed());
7719
7720        let x_before = world.bodies[0].position.x;
7721        let dt = Fix128::from_ratio(1, 60);
7722        world.substep(dt);
7723        let x_after = world.bodies[0].position.x;
7724
7725        // CPU contact solve applies a position correction along the
7726        // contact normal (x axis). Any non-zero drift confirms the CPU
7727        // path was taken instead of the (absent) bridge path.
7728        assert_ne!(
7729            x_before, x_after,
7730            "CPU contact solver must adjust position when no bridge is installed"
7731        );
7732    }
7733
7734    /// v0.11.0: attach / take lifecycle preserves the field invariant
7735    /// (`gpu_solver_bridge_installed` reflects the current state) and
7736    /// `take_gpu_solver_bridge` returns the same box on the first
7737    /// call and `None` on subsequent calls.
7738    #[cfg(feature = "gpu-solver-bridge")]
7739    #[test]
7740    fn physics_world_bridge_attach_detach_lifecycle() {
7741        let mut world = PhysicsWorld::new(SolverConfig::default());
7742        assert!(!world.gpu_solver_bridge_installed());
7743
7744        let bridge = RecordingBridge {
7745            send_contact_count: std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0)),
7746            send_body_state_count: std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0)),
7747            dispatch_count: std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0)),
7748            send_joints_count: std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0)),
7749            send_body_rotations_count: std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0)),
7750            dispatch_joint_count: std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0)),
7751        };
7752        world.set_gpu_solver_bridge(Some(Box::new(bridge)));
7753        assert!(world.gpu_solver_bridge_installed());
7754
7755        let taken = world.take_gpu_solver_bridge();
7756        assert!(taken.is_some(), "first take must return the installed box");
7757        assert!(!world.gpu_solver_bridge_installed());
7758
7759        let taken_again = world.take_gpu_solver_bridge();
7760        assert!(
7761            taken_again.is_none(),
7762            "subsequent take must return None when nothing is installed"
7763        );
7764    }
7765
7766    // -------------------------------------------------------------------
7767    // v0.12.0: PhysicsWorld auto-routing joint solve through installed bridge
7768    // -------------------------------------------------------------------
7769
7770    #[cfg(feature = "gpu-solver-bridge")]
7771    fn build_one_ball_joint_world() -> PhysicsWorld {
7772        use crate::joint::{BallJoint, Joint};
7773        use crate::math::Vec3Fix;
7774        let mut world = PhysicsWorld::new(SolverConfig::default());
7775        let body_a = RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE);
7776        let body_b = RigidBody::new_dynamic(
7777            Vec3Fix::new(Fix128::from_int(1), Fix128::ZERO, Fix128::ZERO),
7778            Fix128::ONE,
7779        );
7780        world.add_body(body_a);
7781        world.add_body(body_b);
7782        world.joints.push(Joint::Ball(BallJoint::new(
7783            0,
7784            1,
7785            Vec3Fix::ZERO,
7786            Vec3Fix::ZERO,
7787        )));
7788        world
7789    }
7790
7791    /// v0.12.0: attaching a bridge routes joint-solve through the
7792    /// bridge on every subsequent `substep` — verified by counting
7793    /// bridge method invocations. Joint solve runs **once per
7794    /// substep** (unlike contact solve which iterates
7795    /// `config.iterations` times), so the counters should each
7796    /// increment by exactly one after a single substep.
7797    #[cfg(feature = "gpu-solver-bridge")]
7798    #[test]
7799    fn physics_world_substep_auto_routes_joint_solve_through_bridge() {
7800        let send_joints_count = std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0));
7801        let send_body_rotations_count =
7802            std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0));
7803        let dispatch_joint_count = std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0));
7804
7805        let bridge = RecordingBridge {
7806            send_contact_count: std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0)),
7807            send_body_state_count: std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0)),
7808            dispatch_count: std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0)),
7809            send_joints_count: std::sync::Arc::clone(&send_joints_count),
7810            send_body_rotations_count: std::sync::Arc::clone(&send_body_rotations_count),
7811            dispatch_joint_count: std::sync::Arc::clone(&dispatch_joint_count),
7812        };
7813
7814        let mut world = build_one_ball_joint_world();
7815        world.set_gpu_solver_bridge(Some(Box::new(bridge)));
7816
7817        let dt = Fix128::from_ratio(1, 60);
7818        world.substep(dt);
7819
7820        assert_eq!(
7821            send_joints_count.load(core::sync::atomic::Ordering::SeqCst),
7822            1,
7823            "send_joints must be invoked exactly once per substep"
7824        );
7825        assert_eq!(
7826            send_body_rotations_count.load(core::sync::atomic::Ordering::SeqCst),
7827            1,
7828            "send_body_rotations must be invoked exactly once per substep"
7829        );
7830        assert_eq!(
7831            dispatch_joint_count.load(core::sync::atomic::Ordering::SeqCst),
7832            1,
7833            "dispatch_joint_solve_iteration must fire exactly once per substep (single Baumgarte projection, not PGS)"
7834        );
7835    }
7836
7837    /// v0.12.0: without a bridge installed, joint solve falls back to
7838    /// the CPU `solve_joints`. Verified by observing that the two
7839    /// bodies (initially 1 unit apart with a zero-anchor ball joint)
7840    /// get pulled together by the joint constraint.
7841    #[cfg(feature = "gpu-solver-bridge")]
7842    #[test]
7843    fn physics_world_substep_falls_back_to_cpu_joint_solve_when_bridge_detached() {
7844        let mut world = build_one_ball_joint_world();
7845        assert!(!world.gpu_solver_bridge_installed());
7846
7847        let a_before = world.bodies[0].position.x;
7848        let b_before = world.bodies[1].position.x;
7849        let dt = Fix128::from_ratio(1, 60);
7850        world.substep(dt);
7851        let a_after = world.bodies[0].position.x;
7852        let b_after = world.bodies[1].position.x;
7853
7854        // Zero-anchor ball joint constrains anchor_a == anchor_b,
7855        // which for zero local anchors is body positions themselves.
7856        // CPU `solve_ball_joint` therefore pulls each body toward
7857        // the other by half the separation — bodies should move
7858        // toward each other but not past each other.
7859        let a_delta = (a_after - a_before).to_f32();
7860        let b_delta = (b_after - b_before).to_f32();
7861        assert!(
7862            a_delta > 0.0,
7863            "body_a should move in +x toward body_b (delta = {a_delta})"
7864        );
7865        assert!(
7866            b_delta < 0.0,
7867            "body_b should move in -x toward body_a (delta = {b_delta})"
7868        );
7869    }
7870
7871    /// v0.12.0: attach → observe joint routing → take →
7872    /// verify subsequent substep uses CPU path — a mixed-lifecycle
7873    /// test that exercises both the auto-routing and fallback code
7874    /// paths on the same `PhysicsWorld` instance.
7875    #[cfg(feature = "gpu-solver-bridge")]
7876    #[test]
7877    fn physics_world_substep_joint_solve_bridge_attach_take_lifecycle() {
7878        let dispatch_joint_count = std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0));
7879        let bridge = RecordingBridge {
7880            send_contact_count: std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0)),
7881            send_body_state_count: std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0)),
7882            dispatch_count: std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0)),
7883            send_joints_count: std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0)),
7884            send_body_rotations_count: std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0)),
7885            dispatch_joint_count: std::sync::Arc::clone(&dispatch_joint_count),
7886        };
7887
7888        let mut world = build_one_ball_joint_world();
7889        world.set_gpu_solver_bridge(Some(Box::new(bridge)));
7890
7891        let dt = Fix128::from_ratio(1, 60);
7892        world.substep(dt);
7893        assert_eq!(
7894            dispatch_joint_count.load(core::sync::atomic::Ordering::SeqCst),
7895            1,
7896            "first substep must route through bridge"
7897        );
7898
7899        let taken = world.take_gpu_solver_bridge();
7900        assert!(taken.is_some(), "bridge take must return Some");
7901
7902        // After detach, the bridge's counter must not increment on
7903        // subsequent substeps — the CPU path takes over.
7904        let a_before = world.bodies[0].position.x;
7905        world.substep(dt);
7906        let a_after = world.bodies[0].position.x;
7907        assert_eq!(
7908            dispatch_joint_count.load(core::sync::atomic::Ordering::SeqCst),
7909            1,
7910            "after take_gpu_solver_bridge, no further bridge dispatch may occur"
7911        );
7912        assert_ne!(
7913            a_before, a_after,
7914            "CPU joint solve should move body_a toward body_b after detach"
7915        );
7916    }
7917
7918    // ------------------------------------------------------------------
7919    // Graph coloring soundness (v1.0.1): >64 colors + static exclusion
7920    // ------------------------------------------------------------------
7921
7922    /// Hub body + `spokes` distance constraints, each spoke its own body.
7923    fn hub_world(hub: RigidBody, spokes: usize) -> PhysicsWorld {
7924        let mut world = PhysicsWorld::new(SolverConfig {
7925            gravity: Vec3Fix::ZERO,
7926            ..Default::default()
7927        });
7928        let hub_idx = world.add_body(hub);
7929        for i in 0..spokes {
7930            let spoke = world.add_body(RigidBody::new_dynamic(
7931                Vec3Fix::from_int(i as i64 + 1, 0, 0),
7932                Fix128::ONE,
7933            ));
7934            world.add_distance_constraint(DistanceConstraint::new(
7935                hub_idx,
7936                spoke,
7937                Vec3Fix::ZERO,
7938                Vec3Fix::ZERO,
7939                Fix128::ONE,
7940            ));
7941        }
7942        world
7943    }
7944
7945    #[test]
7946    fn coloring_dynamic_hub_over_64_spokes_gets_one_color_per_constraint() {
7947        // Pre-1.0.1: colors saturated at 64 and every further constraint
7948        // aliased into batch 64 while sharing the hub body.
7949        for spokes in [65usize, 70, 200] {
7950            let mut world = hub_world(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE), spokes);
7951            world.rebuild_batches();
7952            assert_eq!(
7953                world.num_batches(),
7954                spokes,
7955                "dynamic hub with {spokes} spokes needs {spokes} batches"
7956            );
7957            assert!(world.batches_are_body_disjoint());
7958        }
7959    }
7960
7961    #[test]
7962    fn coloring_static_hub_imposes_no_constraint() {
7963        // A static floor touched by many bodies must not serialize the
7964        // solver: all spokes are distinct dynamic bodies → single batch.
7965        let mut world = hub_world(RigidBody::new_static(Vec3Fix::ZERO), 200);
7966        world.rebuild_batches();
7967        assert_eq!(world.num_batches(), 1);
7968        assert!(world.batches_are_body_disjoint());
7969    }
7970
7971    #[test]
7972    fn coloring_contacts_and_distances_share_color_space() {
7973        // Two constraint kinds on the same dynamic pair must land in
7974        // different batches (the contact pass runs after the distance pass
7975        // but colors are shared across both).
7976        let mut world = PhysicsWorld::new(SolverConfig::default());
7977        let a = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
7978        let b = world.add_body(RigidBody::new_dynamic(
7979            Vec3Fix::from_int(1, 0, 0),
7980            Fix128::ONE,
7981        ));
7982        world.add_distance_constraint(DistanceConstraint::new(
7983            a,
7984            b,
7985            Vec3Fix::ZERO,
7986            Vec3Fix::ZERO,
7987            Fix128::ONE,
7988        ));
7989        world.add_contact(ContactConstraint::new(
7990            a,
7991            b,
7992            Contact {
7993                point_a: Vec3Fix::ZERO,
7994                point_b: Vec3Fix::ZERO,
7995                normal: Vec3Fix::from_int(1, 0, 0),
7996                depth: Fix128::from_ratio(1, 10),
7997            },
7998        ));
7999        world.rebuild_batches();
8000        assert_eq!(world.num_batches(), 2);
8001        assert!(world.batches_are_body_disjoint());
8002    }
8003
8004    #[test]
8005    fn coloring_disjoint_check_detects_violation() {
8006        // Hand-craft an invalid batch to prove the checker is not vacuous.
8007        let mut world = hub_world(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE), 2);
8008        world.rebuild_batches();
8009        assert!(world.batches_are_body_disjoint());
8010        let mut bad = ConstraintBatch::default();
8011        bad.distance_indices.extend([0usize, 1]);
8012        world.constraint_batches = vec![bad];
8013        assert!(!world.batches_are_body_disjoint());
8014    }
8015
8016    #[cfg(feature = "parallel")]
8017    #[test]
8018    fn parallel_step_static_hub_matches_sequential_solver() {
8019        // Static hub + 70 spokes: pre-1.0.1 this scene created a batch with
8020        // 6 constraints all holding `&mut` to the hub. Step both solver
8021        // paths and require bit-exact agreement.
8022        let sub_dt = Fix128::from_ratio(1, 240);
8023        let mut par = hub_world(RigidBody::new_static(Vec3Fix::ZERO), 70);
8024        let mut seq = hub_world(RigidBody::new_static(Vec3Fix::ZERO), 70);
8025        par.rebuild_batches();
8026        seq.rebuild_batches();
8027        assert_eq!(par.num_batches(), 1, "static hub → single batch");
8028        for _ in 0..120 {
8029            par.substep_batched(sub_dt);
8030            seq.substep(sub_dt);
8031        }
8032        for (p, s) in par.bodies.iter().zip(&seq.bodies) {
8033            assert_eq!(p.position, s.position);
8034            assert_eq!(p.velocity, s.velocity);
8035        }
8036        assert!(par.batches_are_body_disjoint());
8037    }
8038
8039    #[cfg(feature = "parallel")]
8040    #[test]
8041    fn parallel_step_rebuilds_when_body_becomes_dynamic_after_coloring() {
8042        // Coloring excluded the hub as static; flipping it to dynamic
8043        // without touching constraints must not leave the stale snapshot
8044        // in place (two threads would otherwise take `&mut` to the hub).
8045        let mut world = hub_world(RigidBody::new_static(Vec3Fix::ZERO), 70);
8046        world.rebuild_batches();
8047        assert_eq!(world.num_batches(), 1);
8048        world.bodies[0].inv_mass = Fix128::ONE;
8049        world.solve_constraints_batched(Fix128::from_ratio(1, 60));
8050        assert_eq!(world.num_batches(), 70);
8051        assert!(world.batches_are_body_disjoint());
8052    }
8053
8054    // ------------------------------------------------------------------
8055    // Mutation-score tests (2026-09-15, quality-deep core score 32% 対応)
8056    // 各 test は cargo-mutants の missed 変異 (演算子置換 / 分岐反転 / 符号削除)
8057    // を値の厳密一致で殺すことを目的にする  dt は 2 の冪の逆数 (1/4) にして
8058    // inv_dt が Fix128 で exact になるようにし、expected は独立に手計算した値
8059    // ------------------------------------------------------------------
8060
8061    /// gravity 0 / damping なしの world (integrate の副作用を消して単機能を観測)
8062    fn quiet_world() -> PhysicsWorld {
8063        let config = SolverConfig {
8064            gravity: Vec3Fix::ZERO,
8065            damping: Fix128::ONE,
8066            ..SolverConfig::default()
8067        };
8068        PhysicsWorld::new(config)
8069    }
8070
8071    fn v3(x: i64, y: i64, z: i64) -> Vec3Fix {
8072        Vec3Fix::from_int(x, y, z)
8073    }
8074
8075    fn r(n: i64, d: i64) -> Fix128 {
8076        Fix128::from_ratio(n, d)
8077    }
8078
8079    #[test]
8080    fn update_velocities_linear_velocity_is_position_delta_times_inv_dt() {
8081        let mut world = quiet_world();
8082        let dyn_idx = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
8083        let static_idx = world.add_body(RigidBody::new_static(v3(5, 5, 5)));
8084        let dt = r(1, 4); // inv_dt = 4 exactly
8085
8086        // prev → position の差分 (1/4, -1/2, 3/4) → velocity は (1, -2, 3)
8087        world.bodies[dyn_idx].prev_position = v3(1, 1, 1);
8088        world.bodies[dyn_idx].position = Vec3Fix::new(
8089            Fix128::ONE + r(1, 4),
8090            Fix128::ONE - r(1, 2),
8091            Fix128::ONE + r(3, 4),
8092        );
8093        // static body にも差分を仕込む: 処理対象外なので velocity は ZERO のまま
8094        world.bodies[static_idx].prev_position = v3(0, 0, 0);
8095        world.bodies[static_idx].position = v3(5, 5, 5);
8096
8097        world.update_velocities(dt);
8098
8099        assert_eq!(world.bodies[dyn_idx].velocity, v3(1, -2, 3));
8100        assert_eq!(world.bodies[static_idx].velocity, Vec3Fix::ZERO);
8101    }
8102
8103    #[test]
8104    fn update_velocities_angular_velocity_from_rotation_delta_positive_w() {
8105        let mut world = quiet_world();
8106        let idx = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
8107        let dt = r(1, 4);
8108        // 小さい回転 (θ = 1/2 rad) → dq.w > 0 の分岐
8109        let q = QuatFix::from_axis_angle(v3(1, 2, 3), r(1, 2));
8110        world.bodies[idx].prev_rotation = QuatFix::IDENTITY;
8111        world.bodies[idx].rotation = q;
8112
8113        world.update_velocities(dt);
8114
8115        // dq = q: θ = 1/2 rad about (1, 2, 3)/√14 ⇒ ω = axis · θ / dt = axis · 2
8116        let got = world.bodies[idx].angular_velocity;
8117        let n = crate::det_math::sqrt64(14.0);
8118        let want = [2.0 / n, 4.0 / n, 6.0 / n];
8119        for (g, e) in [got.x, got.y, got.z].iter().zip(want) {
8120            assert!((g.to_f64() - e).abs() < 1e-12, "{} vs {e}", g.to_f64());
8121        }
8122    }
8123
8124    #[test]
8125    fn update_velocities_angular_velocity_negative_w_flips_sign() {
8126        let mut world = quiet_world();
8127        let idx = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
8128        let dt = r(1, 4);
8129        // θ = 3π/2 → w = cos(3π/4) < 0 → 符号反転分岐
8130        let theta = Fix128::PI + Fix128::HALF_PI;
8131        let q = QuatFix::from_axis_angle(v3(1, -2, 3), theta);
8132        assert!(q.w < Fix128::ZERO, "test precondition: w must be negative");
8133        world.bodies[idx].prev_rotation = QuatFix::IDENTITY;
8134        world.bodies[idx].rotation = q;
8135
8136        world.update_velocities(dt);
8137
8138        // 3π/2 about a = (1, −2, 3)/√14 equals π/2 about −a (w folded to
8139        // ≥ 0) ⇒ ω = −a · (π/2) / dt = −a · 2π; every component non-zero so
8140        // each sign is checked
8141        let got = world.bodies[idx].angular_velocity;
8142        let n = crate::det_math::sqrt64(14.0);
8143        let k = 2.0 * core::f64::consts::PI / n;
8144        let want = [-k, 2.0 * k, -3.0 * k];
8145        for (g, e) in [got.x, got.y, got.z].iter().zip(want) {
8146            assert!((g.to_f64() - e).abs() < 1e-12, "{} vs {e}", g.to_f64());
8147        }
8148    }
8149
8150    #[test]
8151    fn update_velocities_angular_velocity_zero_w_takes_positive_branch() {
8152        let mut world = quiet_world();
8153        let idx = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
8154        let dt = r(1, 4);
8155        // w == 0 ちょうど (π 回転) → `<` は false = 正の分岐 (`<=` / `==` 変異はここで死ぬ)
8156        let q = QuatFix::new(Fix128::ZERO, Fix128::ZERO, Fix128::ONE, Fix128::ZERO);
8157        world.bodies[idx].prev_rotation = QuatFix::IDENTITY;
8158        world.bodies[idx].rotation = q;
8159
8160        world.update_velocities(dt);
8161
8162        // π about z (w = 0 takes the positive branch) ⇒ ω = (0, 0, π / dt) = (0, 0, 4π)
8163        let got = world.bodies[idx].angular_velocity;
8164        assert_eq!((got.x, got.y), (Fix128::ZERO, Fix128::ZERO));
8165        let want = 4.0 * core::f64::consts::PI;
8166        assert!(
8167            (got.z.to_f64() - want).abs() < 1e-12,
8168            "{} vs {want}",
8169            got.z.to_f64()
8170        );
8171    }
8172
8173    /// 接触 1 本を直接差し込む helper (normal は B → A 方向 = +x)
8174    fn push_contact(
8175        world: &mut PhysicsWorld,
8176        a: usize,
8177        b: usize,
8178        friction: Fix128,
8179        restitution: Fix128,
8180    ) {
8181        let contact = Contact {
8182            depth: r(1, 100),
8183            normal: v3(1, 0, 0),
8184            point_a: Vec3Fix::ZERO,
8185            point_b: Vec3Fix::ZERO,
8186        };
8187        let mut c = ContactConstraint::new(a, b, contact);
8188        c.friction = friction;
8189        c.restitution = restitution;
8190        world.contact_constraints.push(c);
8191    }
8192
8193    /// Phase 1 は velocity を (position - prev) * inv_dt で上書きするので、
8194    /// 速度 v を与えるには prev_position = position - v * dt を仕込む (dt = 1/4 固定)
8195    /// 反発は pre-solve の法線速度 v̄_n を使うので `velocity` にも同じ v を入れる
8196    /// (拘束が位置を動かさなかった substep = pre と post が一致する場合)
8197    fn give_velocity(world: &mut PhysicsWorld, idx: usize, v: Vec3Fix) {
8198        let b = &mut world.bodies[idx];
8199        b.velocity = v;
8200        b.prev_position = b.position - v * r(1, 4);
8201        b.prev_rotation = b.rotation;
8202    }
8203
8204    /// 全 body の prev を現在値に揃える (速度 0)
8205    fn freeze_positions(world: &mut PhysicsWorld) {
8206        for b in &mut world.bodies {
8207            b.prev_position = b.position;
8208            b.prev_rotation = b.rotation;
8209        }
8210    }
8211
8212    #[test]
8213    fn update_velocities_restitution_splits_impulse_by_inverse_mass() {
8214        let mut world = quiet_world();
8215        let a = world.add_body(RigidBody::new_dynamic(v3(0, 0, 0), Fix128::ONE));
8216        let b = world.add_body(RigidBody::new_dynamic(v3(1, 0, 0), Fix128::ONE));
8217        push_contact(&mut world, a, b, Fix128::ZERO, r(1, 2));
8218        freeze_positions(&mut world);
8219        // A が B に向かって -x に 1 で接近 (vn = -1 < 0)
8220        give_velocity(&mut world, a, v3(-1, 0, 0));
8221
8222        world.update_velocities(r(1, 4));
8223
8224        // delta_vn = -(1 + 0.5) * (-1) = 1.5、inv_w = 1/2 → A += 0.75、B -= 0.75
8225        assert_eq!(
8226            world.bodies[a].velocity,
8227            Vec3Fix::new(-r(1, 4), Fix128::ZERO, Fix128::ZERO)
8228        );
8229        assert_eq!(
8230            world.bodies[b].velocity,
8231            Vec3Fix::new(-r(3, 4), Fix128::ZERO, Fix128::ZERO)
8232        );
8233    }
8234
8235    #[test]
8236    fn update_velocities_restitution_skips_separating_contact() {
8237        let mut world = quiet_world();
8238        let a = world.add_body(RigidBody::new_dynamic(v3(0, 0, 0), Fix128::ONE));
8239        let b = world.add_body(RigidBody::new_dynamic(v3(1, 0, 0), Fix128::ONE));
8240        push_contact(&mut world, a, b, Fix128::ZERO, r(1, 2));
8241        freeze_positions(&mut world);
8242        // vn = +1 > 0 (離れていく) → restitution なし、tangent なし → 無変化
8243        give_velocity(&mut world, a, v3(1, 0, 0));
8244
8245        world.update_velocities(r(1, 4));
8246
8247        assert_eq!(world.bodies[a].velocity, v3(1, 0, 0));
8248        assert_eq!(world.bodies[b].velocity, Vec3Fix::ZERO);
8249    }
8250
8251    #[test]
8252    fn update_velocities_restitution_uses_mass_ratio() {
8253        let mut world = quiet_world();
8254        // A: inv_mass 1、B: inv_mass 3 (直接設定、1/(1/3) の丸めを避ける) → inv_w = 1/4 exact
8255        let a = world.add_body(RigidBody::new_dynamic(v3(0, 0, 0), Fix128::ONE));
8256        let b = world.add_body(RigidBody::new_dynamic(v3(1, 0, 0), Fix128::ONE));
8257        world.bodies[b].inv_mass = Fix128::from_int(3);
8258        push_contact(&mut world, a, b, Fix128::ZERO, Fix128::ZERO);
8259        freeze_positions(&mut world);
8260        give_velocity(&mut world, a, v3(-2, 0, 0)); // vn = -2、e = 0 → delta_vn = 2
8261
8262        world.update_velocities(r(1, 4));
8263
8264        // A += 2 * (1 * 1/4) = 0.5 → -1.5、B -= 2 * (3 * 1/4) = 1.5 → -1.5 (完全非弾性で速度一致)
8265        assert_eq!(
8266            world.bodies[a].velocity,
8267            Vec3Fix::new(-r(3, 2), Fix128::ZERO, Fix128::ZERO)
8268        );
8269        assert_eq!(
8270            world.bodies[b].velocity,
8271            Vec3Fix::new(-r(3, 2), Fix128::ZERO, Fix128::ZERO)
8272        );
8273    }
8274
8275    #[test]
8276    fn update_velocities_friction_clamped_by_coulomb_limit() {
8277        let mut world = quiet_world();
8278        let a = world.add_body(RigidBody::new_dynamic(v3(0, 0, 0), Fix128::ONE));
8279        let b = world.add_body(RigidBody::new_static(v3(1, 0, 0))); // inv_mass 0 → w_sum = 1
8280        push_contact(&mut world, a, b, r(1, 2), Fix128::ZERO);
8281        // 位置解の法線 λ = 1 → Coulomb 上限 μ λ / dt = 0.5 * 1 * 4 = 2
8282        world.contact_constraints[0].cached_lambda = Fix128::ONE;
8283        freeze_positions(&mut world);
8284        // 法線成分は正 (離れる) にして restitution を素通りさせる
8285        give_velocity(&mut world, a, v3(4, 3, 0));
8286
8287        world.update_velocities(r(1, 4));
8288
8289        // tangent_speed = 3、max = 2 → clamp 2、A.y -= 2 * 1 → 1、x は不変
8290        assert_eq!(
8291            world.bodies[a].velocity,
8292            Vec3Fix::new(Fix128::from_int(4), Fix128::ONE, Fix128::ZERO)
8293        );
8294        assert_eq!(world.bodies[b].velocity, Vec3Fix::ZERO);
8295    }
8296
8297    #[test]
8298    fn update_velocities_friction_below_limit_removes_all_tangential_velocity() {
8299        let mut world = quiet_world();
8300        let a = world.add_body(RigidBody::new_dynamic(v3(0, 0, 0), Fix128::ONE));
8301        let b = world.add_body(RigidBody::new_static(v3(1, 0, 0)));
8302        push_contact(&mut world, a, b, Fix128::ONE, Fix128::ZERO);
8303        // μ = 1、λ = 1 → max = μ λ / dt = 4
8304        world.contact_constraints[0].cached_lambda = Fix128::ONE;
8305        freeze_positions(&mut world);
8306        give_velocity(&mut world, a, v3(4, 0, 3));
8307
8308        world.update_velocities(r(1, 4));
8309
8310        // tangent_speed = 3 < max 4 → applied = 3 → z 成分 0
8311        assert_eq!(
8312            world.bodies[a].velocity,
8313            Vec3Fix::new(Fix128::from_int(4), Fix128::ZERO, Fix128::ZERO)
8314        );
8315    }
8316
8317    #[test]
8318    fn update_velocities_friction_boundary_equal_to_limit_uses_limit() {
8319        let mut world = quiet_world();
8320        let a = world.add_body(RigidBody::new_dynamic(v3(0, 0, 0), Fix128::ONE));
8321        let b = world.add_body(RigidBody::new_static(v3(1, 0, 0)));
8322        push_contact(&mut world, a, b, Fix128::ONE, Fix128::ZERO);
8323        // μ = 1、λ = 3/4 → max = μ λ / dt = 3
8324        world.contact_constraints[0].cached_lambda = r(3, 4);
8325        freeze_positions(&mut world);
8326        // tangent_speed == max (3 == 3): `<` は false → max 側 (値は同じ 3 だが `<=`/`>` 変異で経路が変わる)
8327        give_velocity(&mut world, a, v3(3, 3, 0));
8328
8329        world.update_velocities(r(1, 4));
8330
8331        assert_eq!(
8332            world.bodies[a].velocity,
8333            Vec3Fix::new(Fix128::from_int(3), Fix128::ZERO, Fix128::ZERO)
8334        );
8335    }
8336
8337    #[test]
8338    fn update_velocities_sensor_on_either_side_skips_response() {
8339        for sensor_side in 0..2 {
8340            let mut world = quiet_world();
8341            let a = world.add_body(RigidBody::new_dynamic(v3(0, 0, 0), Fix128::ONE));
8342            let b = world.add_body(RigidBody::new_dynamic(v3(1, 0, 0), Fix128::ONE));
8343            world.bodies[if sensor_side == 0 { a } else { b }].is_sensor = true;
8344            push_contact(&mut world, a, b, r(1, 2), r(1, 2));
8345            freeze_positions(&mut world);
8346            give_velocity(&mut world, a, v3(-1, 2, 0));
8347
8348            world.update_velocities(r(1, 4));
8349
8350            assert_eq!(
8351                world.bodies[a].velocity,
8352                v3(-1, 2, 0),
8353                "sensor side {sensor_side}"
8354            );
8355            assert_eq!(
8356                world.bodies[b].velocity,
8357                Vec3Fix::ZERO,
8358                "sensor side {sensor_side}"
8359            );
8360        }
8361    }
8362
8363    #[test]
8364    fn update_velocities_two_static_bodies_are_skipped() {
8365        let mut world = quiet_world();
8366        let a = world.add_body(RigidBody::new_static(v3(0, 0, 0)));
8367        let b = world.add_body(RigidBody::new_static(v3(1, 0, 0)));
8368        push_contact(&mut world, a, b, Fix128::ONE, Fix128::ONE);
8369        // static は Phase 1 で skip、Phase 2 は w_sum < ε で skip → velocity は不変 (手書き値のまま)
8370        freeze_positions(&mut world);
8371        world.bodies[a].velocity = v3(-1, 1, 0);
8372
8373        world.update_velocities(r(1, 4));
8374
8375        assert_eq!(world.bodies[a].velocity, v3(-1, 1, 0));
8376    }
8377
8378    // ---- raycast ------------------------------------------------------
8379
8380    /// 半径 2 の球を pos に置いた world (gravity なし)
8381    fn sphere_world(positions: &[Vec3Fix]) -> PhysicsWorld {
8382        let mut world = quiet_world();
8383        for &p in positions {
8384            world.add_body_with_radius(RigidBody::new_static(p), Fix128::from_int(2));
8385        }
8386        world
8387    }
8388
8389    #[test]
8390    fn raycast_hit_distance_is_exact_closed_form() {
8391        // 原点から +x、球 (10,0,0) r=2 → b=-10, c=96, disc=4, t = 10 - 2 = 8
8392        let world = sphere_world(&[v3(10, 0, 0)]);
8393        let hit = world.raycast(Vec3Fix::ZERO, v3(1, 0, 0), Fix128::from_int(100));
8394        assert_eq!(hit, Some((0, Fix128::from_int(8))));
8395        // direction は正規化される: (3,0,0) でも同じ t
8396        let hit2 = world.raycast(Vec3Fix::ZERO, v3(3, 0, 0), Fix128::from_int(100));
8397        assert_eq!(hit2, Some((0, Fix128::from_int(8))));
8398    }
8399
8400    #[test]
8401    fn raycast_along_every_axis_and_direction() {
8402        // ray AABB の min/max 選択 (x/y/z × </>) を全て通す
8403        for (dir, pos) in [
8404            (v3(1, 0, 0), v3(10, 0, 0)),
8405            (v3(-1, 0, 0), v3(-10, 0, 0)),
8406            (v3(0, 1, 0), v3(0, 10, 0)),
8407            (v3(0, -1, 0), v3(0, -10, 0)),
8408            (v3(0, 0, 1), v3(0, 0, 10)),
8409            (v3(0, 0, -1), v3(0, 0, -10)),
8410        ] {
8411            let world = sphere_world(&[pos]);
8412            assert_eq!(
8413                world.raycast(Vec3Fix::ZERO, dir, Fix128::from_int(100)),
8414                Some((0, Fix128::from_int(8))),
8415                "dir {dir:?}"
8416            );
8417            // 逆向きは外れる
8418            assert_eq!(
8419                world.raycast(
8420                    Vec3Fix::ZERO,
8421                    dir * Fix128::from_int(-1),
8422                    Fix128::from_int(100)
8423                ),
8424                None
8425            );
8426        }
8427    }
8428
8429    #[test]
8430    fn raycast_origin_on_surface_hits_at_zero_not_far_side() {
8431        // origin (8,0,0)、球 (10,0,0) r=2: oc=(-2,0,0), b=-2, c=0, disc=4 → t_near = 0 (`<` は false)
8432        let world = sphere_world(&[v3(10, 0, 0)]);
8433        let hit = world.raycast(v3(8, 0, 0), v3(1, 0, 0), Fix128::from_int(100));
8434        assert_eq!(hit, Some((0, Fix128::ZERO)));
8435    }
8436
8437    #[test]
8438    fn raycast_from_inside_sphere_uses_far_intersection() {
8439        // origin = 球心: b=0, c=-4, disc=4 → t_near = -2 < 0 → t_far = 2
8440        let world = sphere_world(&[v3(10, 0, 0)]);
8441        let hit = world.raycast(v3(10, 0, 0), v3(1, 0, 0), Fix128::from_int(100));
8442        assert_eq!(hit, Some((0, Fix128::from_int(2))));
8443    }
8444
8445    #[test]
8446    fn raycast_max_distance_boundary_inclusive() {
8447        let world = sphere_world(&[v3(10, 0, 0)]);
8448        // t = 8 == max → hit (`>` は false)
8449        assert_eq!(
8450            world.raycast(Vec3Fix::ZERO, v3(1, 0, 0), Fix128::from_int(8)),
8451            Some((0, Fix128::from_int(8)))
8452        );
8453        // t = 8 > max 7 → miss (ray AABB は球に届かない or 距離で reject)
8454        assert_eq!(
8455            world.raycast(Vec3Fix::ZERO, v3(1, 0, 0), Fix128::from_int(7)),
8456            None
8457        );
8458        // AABB は届くが距離で reject: max = 7.9 (AABB は球 [8,12] と 7.9 で非交差)、
8459        // 代わりに max = 9 で AABB 交差 + t=8 ≤ 9 → hit、max = 8 - ε 相当は上で網羅
8460        assert_eq!(
8461            world.raycast(Vec3Fix::ZERO, v3(1, 0, 0), Fix128::from_int(9)),
8462            Some((0, Fix128::from_int(8)))
8463        );
8464    }
8465
8466    #[test]
8467    fn raycast_returns_nearest_regardless_of_body_order() {
8468        // 手前 (10) と奥 (20) の球、index 順を両方試す
8469        let near = v3(10, 0, 0);
8470        let far = v3(20, 0, 0);
8471        let w1 = sphere_world(&[far, near]);
8472        assert_eq!(
8473            w1.raycast(Vec3Fix::ZERO, v3(1, 0, 0), Fix128::from_int(100)),
8474            Some((1, Fix128::from_int(8)))
8475        );
8476        let w2 = sphere_world(&[near, far]);
8477        assert_eq!(
8478            w2.raycast(Vec3Fix::ZERO, v3(1, 0, 0), Fix128::from_int(100)),
8479            Some((0, Fix128::from_int(8)))
8480        );
8481    }
8482
8483    #[test]
8484    fn raycast_misses_sphere_beside_the_ray() {
8485        // 球 (10, 5, 0) r=2: 最接近距離 5 > 2 → disc < 0 → None
8486        let world = sphere_world(&[v3(10, 5, 0)]);
8487        assert_eq!(
8488            world.raycast(Vec3Fix::ZERO, v3(1, 0, 0), Fix128::from_int(100)),
8489            None
8490        );
8491        // 球 (10, 2, 0): 接線 (disc = 0) → t = 10 で hit
8492        let tangent = sphere_world(&[v3(10, 2, 0)]);
8493        assert_eq!(
8494            tangent.raycast(Vec3Fix::ZERO, v3(1, 0, 0), Fix128::from_int(100)),
8495            Some((0, Fix128::from_int(10)))
8496        );
8497    }
8498
8499    #[test]
8500    fn raycast_zero_direction_and_no_collidable_bodies_return_none() {
8501        let world = sphere_world(&[v3(10, 0, 0)]);
8502        assert_eq!(
8503            world.raycast(Vec3Fix::ZERO, Vec3Fix::ZERO, Fix128::from_int(100)),
8504            None
8505        );
8506        let mut no_radius = quiet_world();
8507        no_radius.add_body(RigidBody::new_static(v3(10, 0, 0)));
8508        assert_eq!(
8509            no_radius.raycast(Vec3Fix::ZERO, v3(1, 0, 0), Fix128::from_int(100)),
8510            None
8511        );
8512        // radius を後から消すと当たらなくなる
8513        let mut cleared = sphere_world(&[v3(10, 0, 0)]);
8514        cleared.clear_body_collision_radius(0);
8515        assert_eq!(
8516            cleared.raycast(Vec3Fix::ZERO, v3(1, 0, 0), Fix128::from_int(100)),
8517            None
8518        );
8519        // set で復活
8520        cleared.set_body_collision_radius(0, Fix128::from_int(2));
8521        assert_eq!(
8522            cleared.raycast(Vec3Fix::ZERO, v3(1, 0, 0), Fix128::from_int(100)),
8523            Some((0, Fix128::from_int(8)))
8524        );
8525    }
8526
8527    // ---- remove_body ---------------------------------------------------
8528
8529    fn dc(a: usize, b: usize) -> DistanceConstraint {
8530        DistanceConstraint {
8531            body_a: a,
8532            body_b: b,
8533            local_anchor_a: Vec3Fix::ZERO,
8534            local_anchor_b: Vec3Fix::ZERO,
8535            target_distance: Fix128::ONE,
8536            compliance: Fix128::ZERO,
8537            cached_lambda: Fix128::ZERO,
8538        }
8539    }
8540
8541    fn three_body_world_with_all_pairs() -> PhysicsWorld {
8542        let mut world = quiet_world();
8543        for i in 0..3 {
8544            world.add_body(RigidBody::new_dynamic(v3(i, 0, 0), Fix128::ONE));
8545        }
8546        world.add_distance_constraint(dc(0, 1));
8547        world.add_distance_constraint(dc(1, 2));
8548        world.add_distance_constraint(dc(0, 2));
8549        world.contact_constraints.push(ContactConstraint::new(
8550            0,
8551            1,
8552            Contact {
8553                depth: Fix128::ZERO,
8554                normal: v3(1, 0, 0),
8555                point_a: Vec3Fix::ZERO,
8556                point_b: Vec3Fix::ZERO,
8557            },
8558        ));
8559        world.contact_constraints.push(ContactConstraint::new(
8560            1,
8561            2,
8562            Contact {
8563                depth: Fix128::ZERO,
8564                normal: v3(1, 0, 0),
8565                point_a: Vec3Fix::ZERO,
8566                point_b: Vec3Fix::ZERO,
8567            },
8568        ));
8569        world.contact_constraints.push(ContactConstraint::new(
8570            0,
8571            2,
8572            Contact {
8573                depth: Fix128::ZERO,
8574                normal: v3(1, 0, 0),
8575                point_a: Vec3Fix::ZERO,
8576                point_b: Vec3Fix::ZERO,
8577            },
8578        ));
8579        world
8580    }
8581
8582    fn dist_pairs(world: &PhysicsWorld) -> Vec<(usize, usize)> {
8583        world
8584            .distance_constraints
8585            .iter()
8586            .map(|c| (c.body_a, c.body_b))
8587            .collect()
8588    }
8589
8590    fn contact_pairs(world: &PhysicsWorld) -> Vec<(usize, usize)> {
8591        world
8592            .contact_constraints
8593            .iter()
8594            .map(|c| (c.body_a, c.body_b))
8595            .collect()
8596    }
8597
8598    #[test]
8599    fn remove_body_middle_drops_its_constraints_and_remaps_last() {
8600        let mut world = three_body_world_with_all_pairs();
8601        let removed = world.remove_body(1).expect("index 1 exists");
8602        assert_eq!(removed.position, v3(1, 0, 0));
8603        assert_eq!(world.bodies.len(), 2);
8604        // 旧 A2 が index 1 に移動
8605        assert_eq!(world.bodies[1].position, v3(2, 0, 0));
8606        // A1 に触る 2 本は消え、(0-2) だけが (0,1) に remap
8607        assert_eq!(dist_pairs(&world), vec![(0, 1)]);
8608        assert_eq!(contact_pairs(&world), vec![(0, 1)]);
8609        assert_eq!(world.body_materials.len(), 2);
8610        assert_eq!(world.body_collision_radii.len(), 2);
8611        assert_eq!(world.body_filters.len(), 2);
8612    }
8613
8614    #[test]
8615    fn remove_body_first_remaps_last_into_slot_zero() {
8616        let mut world = three_body_world_with_all_pairs();
8617        world.remove_body(0).expect("index 0 exists");
8618        assert_eq!(world.bodies[0].position, v3(2, 0, 0));
8619        assert_eq!(world.bodies[1].position, v3(1, 0, 0));
8620        // 残るのは (1-2) → (1,0)
8621        assert_eq!(dist_pairs(&world), vec![(1, 0)]);
8622        assert_eq!(contact_pairs(&world), vec![(1, 0)]);
8623    }
8624
8625    #[test]
8626    fn remove_body_last_needs_no_remap() {
8627        let mut world = three_body_world_with_all_pairs();
8628        world.remove_body(2).expect("index 2 exists");
8629        assert_eq!(world.bodies.len(), 2);
8630        assert_eq!(world.bodies[1].position, v3(1, 0, 0));
8631        assert_eq!(dist_pairs(&world), vec![(0, 1)]);
8632        assert_eq!(contact_pairs(&world), vec![(0, 1)]);
8633    }
8634
8635    #[test]
8636    fn remove_body_out_of_range_is_none_and_leaves_world_untouched() {
8637        let mut world = three_body_world_with_all_pairs();
8638        assert!(world.remove_body(3).is_none());
8639        assert!(world.remove_body(usize::MAX).is_none());
8640        assert_eq!(world.bodies.len(), 3);
8641        assert_eq!(dist_pairs(&world), vec![(0, 1), (1, 2), (0, 2)]);
8642    }
8643
8644    #[test]
8645    fn remove_body_never_leaves_self_or_dangling_references() {
8646        // 4 body 全 pair、各 index を順に消して不変条件を検査
8647        for victim in 0..4 {
8648            let mut world = quiet_world();
8649            for i in 0..4 {
8650                world.add_body(RigidBody::new_dynamic(v3(i, 0, 0), Fix128::ONE));
8651            }
8652            for a in 0..4 {
8653                for b in (a + 1)..4 {
8654                    world.add_distance_constraint(dc(a, b));
8655                }
8656            }
8657            world.remove_body(victim).expect("in range");
8658            let n = world.bodies.len();
8659            assert_eq!(n, 3);
8660            // 残 3 body の全 pair = 3 本、自己参照なし、範囲内
8661            assert_eq!(world.distance_constraints.len(), 3, "victim {victim}");
8662            for c in &world.distance_constraints {
8663                assert!(
8664                    c.body_a < n && c.body_b < n,
8665                    "victim {victim}: dangling {:?}",
8666                    (c.body_a, c.body_b)
8667                );
8668                assert_ne!(c.body_a, c.body_b, "victim {victim}: self constraint");
8669            }
8670            // 残った body の position 集合 = 元 4 点から victim を除いたもの
8671            let mut xs: Vec<i64> = world.bodies.iter().map(|b| b.position.x.hi).collect();
8672            xs.sort_unstable();
8673            let mut expected: Vec<i64> = (0..4).filter(|&i| i != victim as i64).collect();
8674            expected.sort_unstable();
8675            assert_eq!(xs, expected);
8676        }
8677    }
8678
8679    #[test]
8680    fn remove_body_with_joint_drops_joint_and_rebuilds_islands() {
8681        let mut world = quiet_world();
8682        for i in 0..3 {
8683            world.add_body(RigidBody::new_dynamic(v3(i, 0, 0), Fix128::ONE));
8684        }
8685        let j = crate::joint::BallJoint::new(1, 2, Vec3Fix::ZERO, Vec3Fix::ZERO);
8686        world.add_joint(Joint::Ball(j));
8687        let j2 = crate::joint::BallJoint::new(0, 2, Vec3Fix::ZERO, Vec3Fix::ZERO);
8688        world.add_joint(Joint::Ball(j2));
8689        assert_eq!(world.joint_count(), 2);
8690        world.remove_body(1).expect("in range");
8691        // (1-2) は消え、(0-2) は (0,1) に remap
8692        assert_eq!(world.joint_count(), 1);
8693        assert_eq!(world.joints[0].bodies(), (0, 1));
8694        // island は新 body 数で再構築され、joint で 0 と 1 が同一 island
8695        assert_eq!(world.islands.find(0), world.islands.find(1));
8696    }
8697
8698    // ---- detect_collisions --------------------------------------------
8699
8700    /// |a - b| が 2^-60 未満 (normalize の sqrt 丸め 数 ulp を許容、変異の差は桁違いなので検出力は不変)
8701    fn near(a: Fix128, b: Fix128) -> bool {
8702        let d = (a - b).abs();
8703        d.hi == 0 && d.lo < (1u64 << 4)
8704    }
8705
8706    fn assert_near_vec(a: Vec3Fix, b: Vec3Fix, what: &str) {
8707        assert!(
8708            near(a.x, b.x) && near(a.y, b.y) && near(a.z, b.z),
8709            "{what}: {a:?} vs {b:?}"
8710        );
8711    }
8712
8713    /// 半径 r の dynamic 球 2 個を x 軸上 dist 離して置く
8714    fn two_spheres(dist_num: i64, dist_den: i64, r_a: Fix128, r_b: Fix128) -> PhysicsWorld {
8715        let mut world = quiet_world();
8716        world.add_body_with_radius(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE), r_a);
8717        world.add_body_with_radius(
8718            RigidBody::new_dynamic(
8719                Vec3Fix::new(r(dist_num, dist_den), Fix128::ZERO, Fix128::ZERO),
8720                Fix128::ONE,
8721            ),
8722            r_b,
8723        );
8724        world
8725    }
8726
8727    #[test]
8728    fn detect_collisions_overlapping_spheres_produce_exact_contact() {
8729        // r=1, r=1、距離 1.5 → depth = 0.5、normal = -x (B → A、`Contact::normal` 契約)、
8730        // point_a = pos_a - n·r_a = (1,0,0)、point_b = pos_b + n·r_b = (0.5,0,0)
8731        // (1.2.0 以前は a → b で pin していた = 押し込み方向の bug を test が固定していた)
8732        let mut world = two_spheres(3, 2, Fix128::ONE, Fix128::ONE);
8733        world.detect_collisions();
8734        assert_eq!(world.contact_constraints.len(), 1);
8735        let c = &world.contact_constraints[0];
8736        assert_eq!((c.body_a, c.body_b), (0, 1));
8737        assert!(
8738            near(c.contact.depth, r(1, 2)),
8739            "depth {:?}",
8740            c.contact.depth
8741        );
8742        assert_near_vec(c.contact.normal, v3(-1, 0, 0), "normal");
8743        assert_near_vec(c.contact.point_a, v3(1, 0, 0), "point_a");
8744        assert_near_vec(
8745            c.contact.point_b,
8746            Vec3Fix::new(r(1, 2), Fix128::ZERO, Fix128::ZERO),
8747            "point_b",
8748        );
8749        // event も 1 件、rel_vel は 0
8750        assert_eq!(world.contact_events().len(), 1);
8751        assert!(world.trigger_events().is_empty());
8752    }
8753
8754    #[test]
8755    fn detect_collisions_asymmetric_radii_split_points_correctly() {
8756        // r_a = 2, r_b = 1/2、距離 2 → combined 2.5、depth 0.5
8757        let mut world = two_spheres(2, 1, Fix128::from_int(2), r(1, 2));
8758        world.detect_collisions();
8759        assert_eq!(world.contact_constraints.len(), 1);
8760        let c = world.contact_constraints[0].contact;
8761        assert!(near(c.depth, r(1, 2)), "depth {:?}", c.depth);
8762        assert_near_vec(c.point_a, v3(2, 0, 0), "point_a"); // a.pos + n * r_a
8763        assert_near_vec(
8764            c.point_b,
8765            Vec3Fix::new(r(3, 2), Fix128::ZERO, Fix128::ZERO),
8766            "point_b",
8767        ); // b.pos - n * r_b
8768    }
8769
8770    #[test]
8771    fn detect_collisions_touching_exactly_is_not_a_contact() {
8772        // dist == combined (2 == 1+1): `<` は false → 接触なし (`<=` 変異はここで死ぬ)
8773        let mut world = two_spheres(2, 1, Fix128::ONE, Fix128::ONE);
8774        world.detect_collisions();
8775        assert!(world.contact_constraints.is_empty());
8776        assert!(world.contact_events().is_empty());
8777        // 少しでも近ければ接触
8778        let mut world2 = two_spheres(199, 100, Fix128::ONE, Fix128::ONE);
8779        world2.detect_collisions();
8780        assert_eq!(world2.contact_constraints.len(), 1);
8781    }
8782
8783    #[test]
8784    fn detect_collisions_coincident_centers_are_skipped() {
8785        // dist == 0 → normal 不定なので skip (`!dist.is_zero()`)
8786        let mut world = two_spheres(0, 1, Fix128::ONE, Fix128::ONE);
8787        world.detect_collisions();
8788        assert!(world.contact_constraints.is_empty());
8789    }
8790
8791    #[test]
8792    fn detect_collisions_relative_velocity_is_reported_along_normal() {
8793        let mut world = two_spheres(3, 2, Fix128::ONE, Fix128::ONE);
8794        world.bodies[0].velocity = v3(2, 7, 0); // a → b 方向 +2 (y 成分は normal 直交で無視)
8795        world.bodies[1].velocity = v3(-1, 0, 0);
8796        world.detect_collisions();
8797        let ev = &world.contact_events()[0];
8798        // rel_vel = (v_a - v_b) · n = (3, 7, 0) · (-1,0,0) = -3 (接近中は負、B → A normal)
8799        assert!(
8800            near(ev.relative_velocity, Fix128::from_int(-3)),
8801            "rel_vel {:?}",
8802            ev.relative_velocity
8803        );
8804        assert!(near(ev.depth, r(1, 2)), "depth {:?}", ev.depth);
8805    }
8806
8807    #[test]
8808    fn detect_collisions_sensor_reports_trigger_instead_of_constraint() {
8809        for sensor_side in 0..2 {
8810            let mut world = two_spheres(3, 2, Fix128::ONE, Fix128::ONE);
8811            world.bodies[sensor_side].is_sensor = true;
8812            world.detect_collisions();
8813            assert!(world.contact_constraints.is_empty(), "side {sensor_side}");
8814            assert_eq!(world.trigger_events().len(), 1, "side {sensor_side}");
8815            assert_eq!(world.contact_events().len(), 1, "side {sensor_side}");
8816        }
8817    }
8818
8819    #[test]
8820    fn detect_collisions_static_static_pair_is_skipped_but_static_dynamic_is_not() {
8821        let mut world = quiet_world();
8822        world.add_body_with_radius(RigidBody::new_static(Vec3Fix::ZERO), Fix128::ONE);
8823        world.add_body_with_radius(
8824            RigidBody::new_static(Vec3Fix::new(r(3, 2), Fix128::ZERO, Fix128::ZERO)),
8825            Fix128::ONE,
8826        );
8827        world.detect_collisions();
8828        assert!(world.contact_constraints.is_empty());
8829
8830        let mut mixed = quiet_world();
8831        mixed.add_body_with_radius(RigidBody::new_static(Vec3Fix::ZERO), Fix128::ONE);
8832        mixed.add_body_with_radius(
8833            RigidBody::new_dynamic(
8834                Vec3Fix::new(r(3, 2), Fix128::ZERO, Fix128::ZERO),
8835                Fix128::ONE,
8836            ),
8837            Fix128::ONE,
8838        );
8839        mixed.detect_collisions();
8840        assert_eq!(mixed.contact_constraints.len(), 1);
8841    }
8842
8843    #[test]
8844    fn detect_collisions_filter_blocks_same_group_and_masked_layers() {
8845        // 同一 group (非 0) → 衝突しない
8846        let mut same_group = two_spheres(3, 2, Fix128::ONE, Fix128::ONE);
8847        let g = CollisionFilter {
8848            layer: 1,
8849            mask: u32::MAX,
8850            group: 7,
8851        };
8852        same_group.set_body_filter(0, g);
8853        same_group.set_body_filter(1, g);
8854        same_group.detect_collisions();
8855        assert!(same_group.contact_constraints.is_empty());
8856
8857        // 片方向 mask 不一致 (a.layer & b.mask == 0) → 衝突しない (`&&` の右側も検査)
8858        for side in 0..2 {
8859            let mut masked = two_spheres(3, 2, Fix128::ONE, Fix128::ONE);
8860            masked.set_body_filter(
8861                side,
8862                CollisionFilter {
8863                    layer: 1 << 5,
8864                    mask: u32::MAX,
8865                    group: 0,
8866                },
8867            );
8868            masked.set_body_filter(
8869                1 - side,
8870                CollisionFilter {
8871                    layer: 1,
8872                    mask: !(1 << 5),
8873                    group: 0,
8874                },
8875            );
8876            masked.detect_collisions();
8877            assert!(masked.contact_constraints.is_empty(), "side {side}");
8878        }
8879
8880        // 異なる group + 相互 mask 一致 → 衝突する
8881        let mut ok = two_spheres(3, 2, Fix128::ONE, Fix128::ONE);
8882        ok.set_body_filter(
8883            0,
8884            CollisionFilter {
8885                layer: 1,
8886                mask: u32::MAX,
8887                group: 1,
8888            },
8889        );
8890        ok.set_body_filter(
8891            1,
8892            CollisionFilter {
8893                layer: 1,
8894                mask: u32::MAX,
8895                group: 2,
8896            },
8897        );
8898        ok.detect_collisions();
8899        assert_eq!(ok.contact_constraints.len(), 1);
8900    }
8901
8902    #[test]
8903    fn detect_collisions_both_sleeping_skipped_one_sleeping_wakes() {
8904        let mut both = two_spheres(3, 2, Fix128::ONE, Fix128::ONE);
8905        both.islands.sleep_data[0].state = crate::sleeping::SleepState::Sleeping;
8906        both.islands.sleep_data[1].state = crate::sleeping::SleepState::Sleeping;
8907        both.detect_collisions();
8908        assert!(both.contact_constraints.is_empty());
8909
8910        let mut one = two_spheres(3, 2, Fix128::ONE, Fix128::ONE);
8911        one.islands.sleep_data[0].state = crate::sleeping::SleepState::Sleeping;
8912        one.detect_collisions();
8913        assert_eq!(one.contact_constraints.len(), 1);
8914        // 接触で起こされる
8915        assert!(!one.islands.is_sleeping(0));
8916    }
8917
8918    #[test]
8919    fn detect_collisions_needs_two_collidable_bodies() {
8920        // body 1 個 / radius 付き 1 個だけ → 何も起きない
8921        let mut single = quiet_world();
8922        single.add_body_with_radius(
8923            RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE),
8924            Fix128::ONE,
8925        );
8926        single.detect_collisions();
8927        assert!(single.contact_constraints.is_empty());
8928
8929        let mut one_radius = quiet_world();
8930        one_radius.add_body_with_radius(
8931            RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE),
8932            Fix128::ONE,
8933        );
8934        one_radius.add_body(RigidBody::new_dynamic(
8935            Vec3Fix::new(r(1, 2), Fix128::ZERO, Fix128::ZERO),
8936            Fix128::ONE,
8937        ));
8938        one_radius.detect_collisions();
8939        assert!(one_radius.contact_constraints.is_empty());
8940    }
8941
8942    #[test]
8943    fn detect_collisions_uses_combined_material_of_pair() {
8944        let mut world = two_spheres(3, 2, Fix128::ONE, Fix128::ONE);
8945        world.detect_collisions();
8946        let c = world.contact_constraints[0];
8947        let combined = world.combined_material(0, 1);
8948        assert_eq!(c.friction, combined.friction);
8949        assert_eq!(c.restitution, combined.restitution);
8950    }
8951
8952    // ---- solve_distance_constraints -----------------------------------
8953
8954    #[test]
8955    fn solve_distance_constraints_rigid_splits_error_by_inverse_mass() {
8956        // A (inv 1) at 0、B (inv 3) at x=4、target 2 → error 2、w_sum 4、lambda 1/2
8957        // A += n*λ*1 = +0.5、B -= n*λ*3 = -1.5 → 距離 4 - 2 = 2 で一発収束
8958        let mut world = quiet_world();
8959        let a = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
8960        let b = world.add_body(RigidBody::new_dynamic(v3(4, 0, 0), Fix128::ONE));
8961        world.bodies[b].inv_mass = Fix128::from_int(3);
8962        let mut c = dc(a, b);
8963        c.target_distance = Fix128::from_int(2);
8964        world.add_distance_constraint(c);
8965
8966        world.solve_distance_constraints(r(1, 4));
8967
8968        assert_eq!(
8969            world.bodies[a].position,
8970            Vec3Fix::new(r(1, 2), Fix128::ZERO, Fix128::ZERO)
8971        );
8972        assert_eq!(
8973            world.bodies[b].position,
8974            Vec3Fix::new(r(5, 2), Fix128::ZERO, Fix128::ZERO)
8975        );
8976        assert_eq!(world.distance_constraints[0].cached_lambda, r(1, 2));
8977    }
8978
8979    #[test]
8980    fn solve_distance_constraints_static_side_does_not_move() {
8981        // A static at 0、B (inv 1) at 4、target 1 → error 3、w_sum 1 → B -= 3 → x = 1
8982        let mut world = quiet_world();
8983        let a = world.add_body(RigidBody::new_static(Vec3Fix::ZERO));
8984        let b = world.add_body(RigidBody::new_dynamic(v3(4, 0, 0), Fix128::ONE));
8985        world.add_distance_constraint(dc(a, b)); // target 1
8986
8987        world.solve_distance_constraints(r(1, 4));
8988
8989        assert_eq!(world.bodies[a].position, Vec3Fix::ZERO);
8990        assert_eq!(world.bodies[b].position, v3(1, 0, 0));
8991    }
8992
8993    #[test]
8994    fn solve_distance_constraints_negative_error_pushes_apart() {
8995        // 距離 1 < target 3 → error -2、両 inv 1 → w_sum 2、λ = -1 → A -= 1 (x=-1)、B += 1 (x=2)
8996        let mut world = quiet_world();
8997        let a = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
8998        let b = world.add_body(RigidBody::new_dynamic(v3(1, 0, 0), Fix128::ONE));
8999        let mut c = dc(a, b);
9000        c.target_distance = Fix128::from_int(3);
9001        world.add_distance_constraint(c);
9002
9003        world.solve_distance_constraints(r(1, 4));
9004
9005        assert_eq!(world.bodies[a].position, v3(-1, 0, 0));
9006        assert_eq!(world.bodies[b].position, v3(2, 0, 0));
9007    }
9008
9009    #[test]
9010    fn solve_distance_constraints_compliance_softens_correction() {
9011        // compliance 1/16、dt 1/4 → compliance_term = (1/16)/(1/16) = 1
9012        // A static、B inv 1 at 4、target 2 → error 2、w_sum = 1 + 1 = 2、λ = 1 → B: 4 - 1 = 3
9013        let mut world = quiet_world();
9014        let a = world.add_body(RigidBody::new_static(Vec3Fix::ZERO));
9015        let b = world.add_body(RigidBody::new_dynamic(v3(4, 0, 0), Fix128::ONE));
9016        let mut c = dc(a, b);
9017        c.target_distance = Fix128::from_int(2);
9018        c.compliance = r(1, 16);
9019        world.add_distance_constraint(c);
9020
9021        world.solve_distance_constraints(r(1, 4));
9022
9023        assert_eq!(world.bodies[b].position, v3(3, 0, 0));
9024        assert_eq!(world.distance_constraints[0].cached_lambda, Fix128::ONE);
9025    }
9026
9027    #[test]
9028    fn solve_distance_constraints_accumulates_lambda_xpbd() {
9029        // substep 途中 (前 iteration で λ = 1 が累積済)、compliance_term = 1 (上と同設定)
9030        // dλ = (C - α̃λ) / w_sum = (2 - 1*1) / 2 = 1/2 → B: 4 - 0.5 = 3.5、λ = 1 + 1/2 = 3/2
9031        // (1.2.0 以前は λ を上書きしていたので cached_lambda = 1/2 になっていた)
9032        let mut world = quiet_world();
9033        let a = world.add_body(RigidBody::new_static(Vec3Fix::ZERO));
9034        let b = world.add_body(RigidBody::new_dynamic(v3(4, 0, 0), Fix128::ONE));
9035        let mut c = dc(a, b);
9036        c.target_distance = Fix128::from_int(2);
9037        c.compliance = r(1, 16);
9038        c.cached_lambda = Fix128::ONE;
9039        world.add_distance_constraint(c);
9040
9041        world.solve_distance_constraints(r(1, 4));
9042
9043        assert_eq!(
9044            world.bodies[b].position,
9045            Vec3Fix::new(r(7, 2), Fix128::ZERO, Fix128::ZERO)
9046        );
9047        assert_eq!(world.distance_constraints[0].cached_lambda, r(3, 2));
9048    }
9049
9050    #[test]
9051    fn reset_lambdas_zeroes_distance_lambda() {
9052        let mut world = quiet_world();
9053        let a = world.add_body(RigidBody::new_static(Vec3Fix::ZERO));
9054        let b = world.add_body(RigidBody::new_dynamic(v3(4, 0, 0), Fix128::ONE));
9055        let mut c = dc(a, b);
9056        c.cached_lambda = Fix128::from_int(3);
9057        world.add_distance_constraint(c);
9058        world.reset_lambdas();
9059        assert_eq!(world.distance_constraints[0].cached_lambda, Fix128::ZERO);
9060    }
9061
9062    #[test]
9063    fn xpbd_compliant_extension_is_iteration_independent() {
9064        // 1 kg を compliance 0.01 (k = 100 N/m) の距離拘束で吊る → 定常伸び mg/k = 0.1 m
9065        // iterations を 1..16 と変えても同じ伸びに収束する (1.2.0 以前は iter 倍で伸び半減)
9066        let run = |iters: usize| {
9067            let mut world = quiet_world();
9068            world.config.gravity = v3(0, -10, 0);
9069            world.config.damping = Fix128::ONE;
9070            world.config.substeps = 4;
9071            world.config.iterations = iters;
9072            let a = world.add_body(RigidBody::new_static(Vec3Fix::ZERO));
9073            let b = world.add_body(RigidBody::new_dynamic(v3(0, -1, 0), Fix128::ONE));
9074            world.bodies[b].linear_damping = r(9, 10); // settle the oscillation
9075            let mut c = dc(a, b);
9076            c.target_distance = Fix128::ONE;
9077            c.compliance = r(1, 100);
9078            world.add_distance_constraint(c);
9079            for _ in 0..600 {
9080                world.step(r(1, 60));
9081            }
9082            (-world.bodies[b].position.y - Fix128::ONE).to_f64()
9083        };
9084        let ext: Vec<f64> = [1usize, 2, 4, 8, 16].iter().map(|&i| run(i)).collect();
9085        for (i, e) in ext.iter().enumerate() {
9086            assert!(
9087                (e - 0.1).abs() < 0.01,
9088                "iterations index {i}: extension {e} m, expected 0.1 ± 0.01 (all: {ext:?})"
9089            );
9090        }
9091    }
9092
9093    #[test]
9094    fn solve_distance_constraints_anchor_offsets_are_rotated_into_world() {
9095        // B に local anchor (0,1,0)、B を z 軸 π 回転 → world anchor は B.pos + (0,-1,0)
9096        // A static at 0、B at (0,2,0) → anchor_b = (0,1,0)、距離 1 == target 1 → 補正なし
9097        let mut world = quiet_world();
9098        let a = world.add_body(RigidBody::new_static(Vec3Fix::ZERO));
9099        let b = world.add_body(RigidBody::new_dynamic(v3(0, 2, 0), Fix128::ONE));
9100        world.bodies[b].rotation =
9101            QuatFix::new(Fix128::ZERO, Fix128::ZERO, Fix128::ONE, Fix128::ZERO);
9102        let mut c = dc(a, b);
9103        c.local_anchor_b = v3(0, 1, 0);
9104        world.add_distance_constraint(c);
9105
9106        world.solve_distance_constraints(r(1, 4));
9107
9108        assert_eq!(world.bodies[b].position, v3(0, 2, 0));
9109        // 回転していなければ anchor は (0,3,0)、距離 3 → error 2 → B は -2 動く
9110        let mut unrotated = quiet_world();
9111        let a2 = unrotated.add_body(RigidBody::new_static(Vec3Fix::ZERO));
9112        let b2 = unrotated.add_body(RigidBody::new_dynamic(v3(0, 2, 0), Fix128::ONE));
9113        let mut c2 = dc(a2, b2);
9114        c2.local_anchor_b = v3(0, 1, 0);
9115        unrotated.add_distance_constraint(c2);
9116        unrotated.solve_distance_constraints(r(1, 4));
9117        assert_near_vec(unrotated.bodies[b2].position, v3(0, 0, 0), "unrotated B");
9118        // normalize の sqrt 丸め 数 ulp
9119    }
9120
9121    #[test]
9122    fn solve_distance_constraints_skips_coincident_and_double_static() {
9123        let mut world = quiet_world();
9124        let a = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
9125        let b = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE)); // 同一点
9126        world.add_distance_constraint(dc(a, b));
9127        world.solve_distance_constraints(r(1, 4));
9128        assert_eq!(world.bodies[a].position, Vec3Fix::ZERO);
9129        assert_eq!(world.bodies[b].position, Vec3Fix::ZERO);
9130        assert_eq!(world.distance_constraints[0].cached_lambda, Fix128::ZERO);
9131
9132        let mut statics = quiet_world();
9133        let s1 = statics.add_body(RigidBody::new_static(Vec3Fix::ZERO));
9134        let s2 = statics.add_body(RigidBody::new_static(v3(5, 0, 0)));
9135        statics.add_distance_constraint(dc(s1, s2));
9136        statics.solve_distance_constraints(r(1, 4));
9137        assert_eq!(statics.bodies[s2].position, v3(5, 0, 0));
9138        assert_eq!(statics.distance_constraints[0].cached_lambda, Fix128::ZERO);
9139    }
9140
9141    // ---- solve_contact_constraints ------------------------------------
9142
9143    fn contact_world(inv_a: Fix128, inv_b: Fix128, depth: Fix128) -> PhysicsWorld {
9144        let mut world = quiet_world();
9145        let a = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
9146        let b = world.add_body(RigidBody::new_dynamic(v3(1, 0, 0), Fix128::ONE));
9147        world.bodies[a].inv_mass = inv_a;
9148        world.bodies[b].inv_mass = inv_b;
9149        let contact = Contact {
9150            depth,
9151            normal: v3(1, 0, 0),
9152            point_a: Vec3Fix::ZERO,
9153            point_b: Vec3Fix::ZERO,
9154        };
9155        world
9156            .contact_constraints
9157            .push(ContactConstraint::new(a, b, contact));
9158        world
9159    }
9160
9161    #[test]
9162    fn solve_contact_constraints_pushes_apart_by_depth_weighted_by_inverse_mass() {
9163        // inv 1 / inv 3、depth 1 → λ = 1、inv_w = 1/4 → A += n * 1/4、B -= n * 3/4
9164        let mut world = contact_world(Fix128::ONE, Fix128::from_int(3), Fix128::ONE);
9165        world.solve_contact_constraints(r(1, 4));
9166        assert_eq!(
9167            world.bodies[0].position,
9168            Vec3Fix::new(r(1, 4), Fix128::ZERO, Fix128::ZERO)
9169        );
9170        assert_eq!(
9171            world.bodies[1].position,
9172            Vec3Fix::new(r(1, 4), Fix128::ZERO, Fix128::ZERO)
9173        );
9174        assert_eq!(world.contact_constraints[0].cached_lambda, Fix128::ONE);
9175    }
9176
9177    #[test]
9178    fn solve_contact_constraints_zero_or_negative_depth_is_skipped() {
9179        for depth in [Fix128::ZERO, -r(1, 2)] {
9180            let mut world = contact_world(Fix128::ONE, Fix128::ONE, depth);
9181            world.solve_contact_constraints(r(1, 4));
9182            assert_eq!(world.bodies[0].position, Vec3Fix::ZERO, "depth {depth:?}");
9183            assert_eq!(world.bodies[1].position, v3(1, 0, 0), "depth {depth:?}");
9184            assert_eq!(world.contact_constraints[0].cached_lambda, Fix128::ZERO);
9185        }
9186    }
9187
9188    #[test]
9189    fn solve_contact_constraints_static_side_absorbs_nothing() {
9190        // A static (inv 0)、B inv 1、depth 1/2 → B -= 1/2、A 不動
9191        let mut world = contact_world(Fix128::ZERO, Fix128::ONE, r(1, 2));
9192        world.solve_contact_constraints(r(1, 4));
9193        assert_eq!(world.bodies[0].position, Vec3Fix::ZERO);
9194        assert_eq!(
9195            world.bodies[1].position,
9196            Vec3Fix::new(r(1, 2), Fix128::ZERO, Fix128::ZERO)
9197        );
9198        // 両 static は skip、cached_lambda も 0 のまま
9199        let mut both = contact_world(Fix128::ZERO, Fix128::ZERO, r(1, 2));
9200        both.solve_contact_constraints(r(1, 4));
9201        assert_eq!(both.bodies[1].position, v3(1, 0, 0));
9202        assert_eq!(both.contact_constraints[0].cached_lambda, Fix128::ZERO);
9203    }
9204
9205    #[test]
9206    fn solve_contact_constraints_accumulates_lambda_and_pushes_depth_once() {
9207        // depth 1/2、λ 累積済 1 (≥ depth) → dλ ≤ 0 → 動かない、λ 不変
9208        let mut world = contact_world(Fix128::ONE, Fix128::ONE, r(1, 2));
9209        world.contact_constraints[0].cached_lambda = Fix128::ONE;
9210        world.solve_contact_constraints(r(1, 4));
9211        assert_eq!(world.bodies[0].position, Vec3Fix::ZERO);
9212        assert_eq!(world.contact_constraints[0].cached_lambda, Fix128::ONE);
9213
9214        // λ == depth ちょうど → dλ = 0 → 動かない
9215        let mut edge = contact_world(Fix128::ONE, Fix128::ONE, r(1, 2));
9216        edge.contact_constraints[0].cached_lambda = r(1, 2);
9217        edge.solve_contact_constraints(r(1, 4));
9218        assert_eq!(edge.bodies[0].position, Vec3Fix::ZERO);
9219
9220        // λ 1/4 → dλ = 1/2 - 1/4 = 1/4 → A += 1/8 (w_a / w_sum = 1/2)、λ = 1/2
9221        let mut partial = contact_world(Fix128::ONE, Fix128::ONE, r(1, 2));
9222        partial.contact_constraints[0].cached_lambda = r(1, 4);
9223        partial.solve_contact_constraints(r(1, 4));
9224        assert_eq!(
9225            partial.bodies[0].position,
9226            Vec3Fix::new(r(1, 8), Fix128::ZERO, Fix128::ZERO)
9227        );
9228        assert_eq!(partial.contact_constraints[0].cached_lambda, r(1, 2));
9229
9230        // 4 iteration 回しても総押し出し量は depth 1 回分 (1.2.0 以前は iteration 毎に再 push)
9231        let mut iters = contact_world(Fix128::ONE, Fix128::ONE, r(1, 2));
9232        for _ in 0..4 {
9233            iters.solve_contact_constraints(r(1, 4));
9234        }
9235        assert_eq!(
9236            iters.bodies[0].position,
9237            Vec3Fix::new(r(1, 4), Fix128::ZERO, Fix128::ZERO)
9238        );
9239        assert_eq!(
9240            iters.bodies[1].position,
9241            Vec3Fix::new(r(3, 4), Fix128::ZERO, Fix128::ZERO)
9242        );
9243    }
9244
9245    #[test]
9246    fn solve_contact_constraints_sensor_skips_response() {
9247        for side in 0..2 {
9248            let mut world = contact_world(Fix128::ONE, Fix128::ONE, Fix128::ONE);
9249            world.bodies[side].is_sensor = true;
9250            world.solve_contact_constraints(r(1, 4));
9251            assert_eq!(world.bodies[0].position, Vec3Fix::ZERO, "side {side}");
9252            assert_eq!(world.bodies[1].position, v3(1, 0, 0), "side {side}");
9253        }
9254    }
9255
9256    #[cfg(feature = "std")]
9257    #[test]
9258    fn solve_contact_constraints_pre_solve_hook_can_discard_and_modifier_can_rescale() {
9259        // hook が false → skip
9260        let mut vetoed = contact_world(Fix128::ONE, Fix128::ONE, Fix128::ONE);
9261        vetoed.add_pre_solve_hook(Box::new(|_a, _b, _c| false));
9262        vetoed.apply_contact_filters();
9263        vetoed.solve_contact_constraints(r(1, 4));
9264        assert_eq!(vetoed.bodies[0].position, Vec3Fix::ZERO);
9265
9266        // hook が true → 通常通り
9267        let mut allowed = contact_world(Fix128::ONE, Fix128::ONE, Fix128::ONE);
9268        allowed.add_pre_solve_hook(Box::new(|_a, _b, _c| true));
9269        allowed.apply_contact_filters();
9270        allowed.solve_contact_constraints(r(1, 4));
9271        assert_eq!(
9272            allowed.bodies[0].position,
9273            Vec3Fix::new(r(1, 2), Fix128::ZERO, Fix128::ZERO)
9274        );
9275
9276        // modifier が depth を半分に → λ = 1/2 → A += 1/4
9277        struct Halve;
9278        impl ContactModifier for Halve {
9279            fn modify_contact(
9280                &self,
9281                _a: usize,
9282                _b: usize,
9283                c: &mut Contact,
9284                _f: &mut Fix128,
9285                _r: &mut Fix128,
9286            ) -> bool {
9287                c.depth = c.depth.half();
9288                true
9289            }
9290        }
9291        let mut halved = contact_world(Fix128::ONE, Fix128::ONE, Fix128::ONE);
9292        halved.add_contact_modifier(Box::new(Halve));
9293        halved.apply_contact_filters();
9294        halved.solve_contact_constraints(r(1, 4));
9295        assert_eq!(
9296            halved.bodies[0].position,
9297            Vec3Fix::new(r(1, 4), Fix128::ZERO, Fix128::ZERO)
9298        );
9299
9300        // modifier が false → 捨てる
9301        struct Discard;
9302        impl ContactModifier for Discard {
9303            fn modify_contact(
9304                &self,
9305                _a: usize,
9306                _b: usize,
9307                _c: &mut Contact,
9308                _f: &mut Fix128,
9309                _r: &mut Fix128,
9310            ) -> bool {
9311                false
9312            }
9313        }
9314        let mut discarded = contact_world(Fix128::ONE, Fix128::ONE, Fix128::ONE);
9315        discarded.add_contact_modifier(Box::new(Discard));
9316        discarded.apply_contact_filters();
9317        discarded.solve_contact_constraints(r(1, 4));
9318        assert_eq!(discarded.bodies[0].position, Vec3Fix::ZERO);
9319    }
9320
9321    // ---- RigidBody force / torque / impulse ---------------------------
9322
9323    /// inv_mass 2、inv_inertia (1, 2, 4) の body、初期 velocity / angular を非零にして加算を観測
9324    fn loaded_body() -> RigidBody {
9325        let mut b = RigidBody::new_dynamic(v3(1, 2, 3), Fix128::ONE);
9326        b.inv_mass = Fix128::from_int(2);
9327        b.inv_inertia = v3(1, 2, 4);
9328        b.velocity = v3(10, 20, 30);
9329        b.angular_velocity = v3(-1, -2, -3);
9330        b
9331    }
9332
9333    #[test]
9334    fn add_force_scales_by_inverse_mass_and_dt() {
9335        let mut b = loaded_body();
9336        b.add_force(v3(4, -8, 16), r(1, 4)); // Δv = F * 2 * 1/4 = (2, -4, 8)
9337        assert_eq!(b.velocity, v3(12, 16, 38));
9338        assert_eq!(b.angular_velocity, v3(-1, -2, -3));
9339        let mut st = RigidBody::new_static(Vec3Fix::ZERO);
9340        st.add_force(v3(4, 4, 4), Fix128::ONE);
9341        assert_eq!(st.velocity, Vec3Fix::ZERO);
9342    }
9343
9344    #[test]
9345    fn add_torque_scales_each_axis_by_inverse_inertia_and_dt() {
9346        let mut b = loaded_body();
9347        b.add_torque(v3(8, 8, 8), r(1, 4)); // Δω = (8*1, 8*2, 8*4) * 1/4 = (2, 4, 8)
9348        assert_eq!(b.angular_velocity, v3(1, 2, 5));
9349        assert_eq!(b.velocity, v3(10, 20, 30));
9350        let mut st = RigidBody::new_static(Vec3Fix::ZERO);
9351        st.add_torque(v3(8, 8, 8), Fix128::ONE);
9352        assert_eq!(st.angular_velocity, Vec3Fix::ZERO);
9353    }
9354
9355    #[test]
9356    fn apply_impulse_scales_by_inverse_mass_only() {
9357        let mut b = loaded_body();
9358        b.apply_impulse(v3(1, 2, 3)); // Δv = (2, 4, 6)
9359        assert_eq!(b.velocity, v3(12, 24, 36));
9360        assert_eq!(b.angular_velocity, v3(-1, -2, -3));
9361        let mut st = RigidBody::new_static(Vec3Fix::ZERO);
9362        st.apply_impulse(v3(1, 1, 1));
9363        assert_eq!(st.velocity, Vec3Fix::ZERO);
9364    }
9365
9366    #[test]
9367    fn apply_impulse_at_adds_lever_arm_torque() {
9368        // position (1,2,3)、point (2,2,3) → r = (1,0,0)、impulse (0,3,0) → r × J = (0,0,3)
9369        // Δω = (0*1, 0*2, 3*4) = (0,0,12)、Δv = J * 2 = (0,6,0)
9370        let mut b = loaded_body();
9371        b.apply_impulse_at(v3(0, 3, 0), v3(2, 2, 3));
9372        assert_eq!(b.velocity, v3(10, 26, 30));
9373        assert_eq!(b.angular_velocity, v3(-1, -2, 9));
9374
9375        // r = (0,1,0)、J = (0,0,5) → r × J = (5,0,0) → Δω = (5,0,0)
9376        let mut c = loaded_body();
9377        c.apply_impulse_at(v3(0, 0, 5), v3(1, 3, 3));
9378        assert_eq!(c.angular_velocity, v3(4, -2, -3));
9379        assert_eq!(c.velocity, v3(10, 20, 40));
9380
9381        // r = (0,0,1)、J = (7,0,0) → r × J = (0,7,0) → Δω = (0,14,0)
9382        let mut d = loaded_body();
9383        d.apply_impulse_at(v3(7, 0, 0), v3(1, 2, 4));
9384        assert_eq!(d.angular_velocity, v3(-1, 12, -3));
9385
9386        // 着力点 = 重心 → torque なし
9387        let mut e = loaded_body();
9388        e.apply_impulse_at(v3(7, 0, 0), v3(1, 2, 3));
9389        assert_eq!(e.angular_velocity, v3(-1, -2, -3));
9390        assert_eq!(e.velocity, v3(24, 20, 30));
9391
9392        let mut st = RigidBody::new_static(Vec3Fix::ZERO);
9393        st.apply_impulse_at(v3(1, 1, 1), v3(1, 0, 0));
9394        assert_eq!(st.velocity, Vec3Fix::ZERO);
9395        assert_eq!(st.angular_velocity, Vec3Fix::ZERO);
9396    }
9397
9398    // ---- serialize / deserialize ---------------------------------------
9399
9400    fn snapshot_world() -> PhysicsWorld {
9401        let mut world = quiet_world();
9402        let mut a = RigidBody::new_dynamic(v3(1, 2, 3), Fix128::ONE);
9403        a.velocity = v3(4, 5, 6);
9404        a.angular_velocity = v3(7, 8, 9);
9405        a.rotation = QuatFix::from_axis_angle(v3(1, 1, 0), r(1, 3));
9406        world.add_body(a);
9407        let mut b = RigidBody::new_dynamic(v3(-1, -2, -3), Fix128::ONE);
9408        b.velocity = Vec3Fix::new(r(1, 3), r(-2, 7), r(5, 11));
9409        world.add_body(b);
9410        world
9411    }
9412
9413    #[test]
9414    fn deserialize_state_roundtrip_restores_every_field_bit_exact() {
9415        let src = snapshot_world();
9416        let bytes = src.serialize_state();
9417        // v3: header 12 + body ごと 208 + body ごと sleep 5 + world ごと flag 1 + fingerprint 8
9418        assert_eq!(bytes.len(), 12 + 2 * 208 + 2 * 5 + 1 + 8);
9419        let mut dst = quiet_world();
9420        dst.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
9421        dst.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
9422        assert!(dst.deserialize_state(&bytes));
9423        for i in 0..2 {
9424            assert_eq!(dst.bodies[i].position, src.bodies[i].position, "pos {i}");
9425            assert_eq!(dst.bodies[i].velocity, src.bodies[i].velocity, "vel {i}");
9426            assert_eq!(dst.bodies[i].rotation, src.bodies[i].rotation, "rot {i}");
9427            assert_eq!(
9428                dst.bodies[i].angular_velocity, src.bodies[i].angular_velocity,
9429                "ang {i}"
9430            );
9431        }
9432        // 並列配列は body 数に同期
9433        assert_eq!(dst.body_collision_radii.len(), 2);
9434        assert_eq!(dst.body_filters.len(), 2);
9435        assert_eq!(dst.body_materials.len(), 2);
9436    }
9437
9438    #[test]
9439    fn deserialize_state_every_single_byte_flip_changes_some_field() {
9440        // 1 byte でも壊れた snapshot は必ずどれかの field に反映される (silent 無視 / 別 field 混入を検出)
9441        //
9442        // ⚠️ v3 format は 5 領域に分かれる — **どの領域にも silent に無視される
9443        // byte が無い**ことを領域ごとに確かめる
9444        //   [0..12)                              header (magic / version / reserved) → **拒否される**
9445        //   [12, 12+n*208)                       body の運動状態 → 該当 body の該当 field が変わる
9446        //   [12+n*208, 12+n*208+n*5)             sleep 状態 → 該当 body の sleep_data が変わる
9447        //   [12+n*208+n*5, 12+n*208+n*5+1)       overflow flag (1 byte)
9448        //   [12+n*208+n*5+1, ..+9)               population fingerprint (8 byte) → **拒否される**
9449        let src = snapshot_world();
9450        let bytes = src.serialize_state();
9451        let body_end = 12 + 2 * 208;
9452        let flag_pos = bytes.len() - 9;
9453
9454        // 領域 1: header — 1 byte 壊れたら受け付けない
9455        for pos in 0..12 {
9456            let mut bad = bytes.clone();
9457            bad[pos] ^= 0x01;
9458            let mut dst = snapshot_world();
9459            assert!(
9460                !dst.deserialize_state(&bad),
9461                "header byte {pos} を壊しても受け付けている (silent に無視される byte がある)"
9462            );
9463        }
9464
9465        // 領域 4: flag (fingerprint 直前の 1 byte) — 壊したら overflow_detected が変わる
9466        //
9467        // ⚠️ v2 で足した領域も「silent に無視される byte が無い」不変条件の
9468        // 対象に含める (flag は 0/1 なので 0x01 の flip で必ず値が変わる)
9469        {
9470            let pos = flag_pos;
9471            let mut bad = bytes.clone();
9472            bad[pos] ^= 0x01;
9473            let mut dst = snapshot_world();
9474            assert!(dst.deserialize_state(&bad), "flag byte {pos}");
9475            assert_ne!(
9476                dst.overflow_detected(),
9477                src.overflow_detected(),
9478                "flag byte {pos} を壊しても overflow_detected が変わらない"
9479            );
9480        }
9481
9482        // 領域 5: population fingerprint (末尾 8 byte) — 1 byte でも壊れたら
9483        // 拒否する (= count は一致しているが fingerprint 不一致で fail-fast、
9484        // gap #3 の population 検査そのもの)
9485        for pos in bytes.len() - 8..bytes.len() {
9486            let mut bad = bytes.clone();
9487            bad[pos] ^= 0x01;
9488            let mut dst = snapshot_world();
9489            assert!(
9490                !dst.deserialize_state(&bad),
9491                "fingerprint byte {pos} を壊しても受け付けている"
9492            );
9493            // 拒否時は元の状態を保つ
9494            assert_eq!(dst.bodies[0].position, src.bodies[0].position);
9495        }
9496
9497        // 領域 3: sleep — 1 byte 壊れたら sleep_data が変わる (flag の手前まで)
9498        for pos in body_end..flag_pos {
9499            let mut bad = bytes.clone();
9500            bad[pos] ^= 0x01;
9501            let mut dst = snapshot_world();
9502            assert!(dst.deserialize_state(&bad), "sleep byte {pos}");
9503            let i = (pos - body_end) / 5;
9504            assert_ne!(
9505                dst.islands.sleep_data[i], src.islands.sleep_data[i],
9506                "sleep byte {pos} は body {i} の sleep_data を変えるべき"
9507            );
9508        }
9509
9510        // 領域 2: body の運動状態
9511        for pos in 12..body_end {
9512            let mut bad = bytes.clone();
9513            bad[pos] ^= 0x01;
9514            let mut dst = snapshot_world();
9515            assert!(dst.deserialize_state(&bad), "byte {pos}");
9516            let body = (pos - 12) / 208;
9517            let field = ((pos - 12) % 208) / 48; // 0 pos / 1 vel / 2 rot(64 byte = 2 slot 相当は 128..192) / 3 ang
9518            let changed_body = (0..2)
9519                .filter(|&i| {
9520                    let (s, d) = (&src.bodies[i], &dst.bodies[i]);
9521                    s.position != d.position
9522                        || s.velocity != d.velocity
9523                        || s.rotation != d.rotation
9524                        || s.angular_velocity != d.angular_velocity
9525                })
9526                .collect::<Vec<_>>();
9527            assert_eq!(
9528                changed_body,
9529                vec![body],
9530                "byte {pos} must change only body {body}"
9531            );
9532            let (s, d) = (&src.bodies[body], &dst.bodies[body]);
9533            let off = (pos - 12) % 208;
9534            match off {
9535                0..=47 => assert!(
9536                    s.position != d.position && s.velocity == d.velocity,
9537                    "byte {pos} → position"
9538                ),
9539                48..=95 => assert!(
9540                    s.velocity != d.velocity && s.position == d.position,
9541                    "byte {pos} → velocity"
9542                ),
9543                96..=159 => assert!(
9544                    s.rotation != d.rotation && s.angular_velocity == d.angular_velocity,
9545                    "byte {pos} → rotation"
9546                ),
9547                _ => assert!(
9548                    s.angular_velocity != d.angular_velocity && s.rotation == d.rotation,
9549                    "byte {pos} → angular"
9550                ),
9551            }
9552            let _ = field;
9553        }
9554    }
9555
9556    #[test]
9557    fn deserialize_state_rejects_short_or_mismatched_input() {
9558        let src = snapshot_world();
9559        let bytes = src.serialize_state();
9560        let mut dst = snapshot_world();
9561        assert!(!dst.deserialize_state(&bytes[..11])); // header 未満 (v1 は 12 byte)
9562        assert!(!dst.deserialize_state(&bytes[..12 + 208 + 100])); // 2 体目が途中で切れる
9563        assert!(
9564            !dst.deserialize_state(&bytes[..bytes.len() - 1]),
9565            "末尾 (v3 の fingerprint) が 1 byte 欠けても拒否する (長さ検査が body 数から一意に決まる)"
9566        );
9567        // ⚠️ 旧 format (先頭が body 数、header なし) は magic 不一致で拒否される
9568        // 検査しないと **旧 blob の先頭 4 byte を magic と読んで誤解釈する**
9569        let mut legacy = 2u32.to_le_bytes().to_vec();
9570        legacy.extend_from_slice(&bytes[12..12 + 2 * 208]);
9571        assert!(
9572            !dst.deserialize_state(&legacy),
9573            "旧 format の blob は magic 不一致で拒否されるべき"
9574        );
9575        // ⚠️ v1 blob (magic は合うが version が 1) も拒否される
9576        // version を検査しないと **flag 1 byte 短い blob を v2 として読む**
9577        let mut v1 = bytes.clone();
9578        v1[4] = 1;
9579        v1.truncate(v1.len() - 1);
9580        assert!(
9581            !dst.deserialize_state(&v1),
9582            "v1 blob は version 不一致で拒否されるべき"
9583        );
9584        let mut one_body = quiet_world();
9585        one_body.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
9586        assert!(!one_body.deserialize_state(&bytes)); // count 不一致
9587                                                      // 拒否時は元の状態を保つ (先頭 body だけ書き換わっていない)
9588        assert_eq!(one_body.bodies[0].position, Vec3Fix::ZERO);
9589        // ぴったり 1 body 分 (header + 運動状態 + sleep) なら OK
9590        let mut exact = quiet_world();
9591        exact.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
9592        let mut one = PhysicsWorld::STATE_MAGIC.to_vec();
9593        one.extend_from_slice(&PhysicsWorld::STATE_VERSION.to_le_bytes());
9594        one.extend_from_slice(&0u16.to_le_bytes());
9595        one.extend_from_slice(&1u32.to_le_bytes());
9596        one.extend_from_slice(&bytes[12..12 + 208]);
9597        one.extend_from_slice(&bytes[12 + 2 * 208..12 + 2 * 208 + 5]);
9598        one.push(0); // v2: flag
9599        one.extend_from_slice(&exact.population_fingerprint().to_le_bytes()); // v3: fingerprint
9600        assert!(exact.deserialize_state(&one));
9601        assert_eq!(exact.bodies[0].position, v3(1, 2, 3));
9602    }
9603
9604    // ---- integrate_positions -----------------------------------------
9605
9606    #[test]
9607    fn integrate_positions_applies_gravity_damping_and_predicts_position() {
9608        let mut world = quiet_world();
9609        world.config.gravity = v3(0, -8, 0);
9610        world.config.damping = r(1, 2);
9611        let i = world.add_body(RigidBody::new_dynamic(v3(1, 1, 1), Fix128::ONE));
9612        world.bodies[i].velocity = v3(4, 0, 0);
9613        world.bodies[i].gravity_scale = r(1, 2);
9614        world.bodies[i].linear_damping = r(1, 2);
9615        let dt = r(1, 4);
9616
9617        world.integrate_positions(dt);
9618
9619        // v = (4,0,0) + g*scale*dt = (4, -1, 0)  (damping は integrate では掛からない、1.2.0)
9620        // pos = (1,1,1) + v*dt = (2, 3/4, 1)、prev = (1,1,1)
9621        let b = &world.bodies[i];
9622        assert_eq!(
9623            b.velocity,
9624            Vec3Fix::new(Fix128::from_int(4), -Fix128::ONE, Fix128::ZERO)
9625        );
9626        assert_eq!(
9627            b.position,
9628            Vec3Fix::new(Fix128::from_int(2), r(3, 4), Fix128::ONE)
9629        );
9630        assert_eq!(b.prev_position, v3(1, 1, 1));
9631
9632        // frame damping: global 1/2 * per-body 1/2 → (1, -1/4, 0)
9633        world.apply_frame_damping();
9634        assert_eq!(
9635            world.bodies[i].velocity,
9636            Vec3Fix::new(Fix128::ONE, -r(1, 4), Fix128::ZERO)
9637        );
9638    }
9639
9640    #[test]
9641    fn integrate_positions_angular_damping_and_rotation_prediction() {
9642        let mut world = quiet_world();
9643        world.config.damping = Fix128::ONE;
9644        let i = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
9645        world.bodies[i].angular_velocity = v3(0, 0, 2);
9646        world.bodies[i].angular_damping = r(1, 2); // frame damping → (0,0,1)
9647        let dt = r(1, 4);
9648
9649        world.integrate_positions(dt);
9650
9651        let b = &world.bodies[i];
9652        // integrate は damping を掛けない (1.2.0)、角速度は 2 のまま
9653        assert_eq!(b.angular_velocity, v3(0, 0, 2));
9654        // 回転は z 軸 angle = 2 * 1/4 の quaternion (正規化済) と一致
9655        let expected = QuatFix::from_axis_angle(v3(0, 0, 1), r(1, 2))
9656            .mul(QuatFix::IDENTITY)
9657            .normalize();
9658        assert_eq!(b.rotation, expected);
9659        assert_eq!(b.prev_rotation, QuatFix::IDENTITY);
9660        assert!(b.rotation != QuatFix::IDENTITY);
9661        world.apply_frame_damping();
9662        assert_eq!(world.bodies[i].angular_velocity, v3(0, 0, 1));
9663    }
9664
9665    #[test]
9666    fn apply_frame_damping_skips_static_kinematic_and_sleeping() {
9667        let mut world = quiet_world();
9668        world.config.damping = r(1, 2);
9669        let st = world.add_body(RigidBody::new_static(Vec3Fix::ZERO));
9670        let mut kin = RigidBody::new_static(Vec3Fix::ZERO);
9671        kin.body_type = BodyType::Kinematic;
9672        let ki = world.add_body(kin);
9673        let dy = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
9674        for &i in &[st, ki, dy] {
9675            world.bodies[i].velocity = v3(2, 0, 0);
9676            world.bodies[i].angular_velocity = v3(0, 4, 0);
9677        }
9678        world.apply_frame_damping();
9679        assert_eq!(world.bodies[st].velocity, v3(2, 0, 0));
9680        assert_eq!(world.bodies[ki].velocity, v3(2, 0, 0));
9681        assert_eq!(world.bodies[dy].velocity, v3(1, 0, 0));
9682        assert_eq!(world.bodies[dy].angular_velocity, v3(0, 2, 0));
9683    }
9684
9685    #[test]
9686    fn free_fall_is_substep_independent_with_frame_damping() {
9687        // 重力 -10、damping ONE、1 秒 (60 frame) → y = -5.0、substeps を変えても同じ
9688        // (1.2.0 以前は substep 内 damping で終端速度 g*h*d/(1-d) が substeps 依存だった)
9689        let fall = |substeps: usize| {
9690            let mut world = quiet_world();
9691            world.config.gravity = v3(0, -10, 0);
9692            world.config.damping = Fix128::ONE;
9693            world.config.substeps = substeps;
9694            let b = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
9695            for _ in 0..60 {
9696                world.step(r(1, 60));
9697            }
9698            world.bodies[b].position.y.to_f64()
9699        };
9700        let ys: Vec<f64> = [1usize, 2, 4, 8, 16].iter().map(|&n| fall(n)).collect();
9701        for y in &ys {
9702            // symplectic Euler の離散化誤差 g*dt/2 = 1/12 → -5 - 0.083、substeps 増で -5 に寄る
9703            assert!(
9704                (y + 5.0).abs() < 0.1,
9705                "free fall y = {y}, expected -5.0 ± 0.1 (all: {ys:?})"
9706            );
9707        }
9708        // 既定 config (damping 0.99 / frame) でも 1 秒で 3.5 m 以上落ちる (旧: -1.64 m)
9709        let mut world = PhysicsWorld::new(PhysicsConfig::default());
9710        let b = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
9711        for _ in 0..60 {
9712            world.step(r(1, 60));
9713        }
9714        let y = world.bodies[b].position.y.to_f64();
9715        assert!(y < -3.5, "default config fell only to y = {y}");
9716    }
9717
9718    #[test]
9719    fn integrate_positions_static_kinematic_and_sleeping_paths() {
9720        let mut world = quiet_world();
9721        world.config.gravity = v3(0, -8, 0);
9722        let st = world.add_body(RigidBody::new_static(v3(0, 5, 0)));
9723        let mut kin = RigidBody::new_static(v3(0, 0, 0));
9724        kin.body_type = BodyType::Kinematic;
9725        kin.set_kinematic_target(v3(2, 0, 0), QuatFix::IDENTITY);
9726        let ki = world.add_body(kin);
9727        let sl = world.add_body(RigidBody::new_dynamic(v3(9, 9, 9), Fix128::ONE));
9728        world.islands.sleep_data[sl].state = crate::sleeping::SleepState::Sleeping;
9729        let dt = r(1, 4);
9730
9731        world.integrate_positions(dt);
9732
9733        // static: 完全不動
9734        assert_eq!(world.bodies[st].position, v3(0, 5, 0));
9735        assert_eq!(world.bodies[st].velocity, Vec3Fix::ZERO);
9736        // kinematic: target に snap、velocity = Δ/dt = (8,0,0)、prev は旧位置
9737        assert_eq!(world.bodies[ki].position, v3(2, 0, 0));
9738        assert_eq!(world.bodies[ki].velocity, v3(8, 0, 0));
9739        assert_eq!(world.bodies[ki].prev_position, Vec3Fix::ZERO);
9740        // sleeping: 重力なし、prev == position
9741        assert_eq!(world.bodies[sl].position, v3(9, 9, 9));
9742        assert_eq!(world.bodies[sl].prev_position, v3(9, 9, 9));
9743        assert_eq!(world.bodies[sl].velocity, Vec3Fix::ZERO);
9744    }
9745
9746    // ---- step (substep 分割 / 重力の閉形式) ----------------------------
9747
9748    #[test]
9749    fn step_free_fall_matches_closed_form_over_substeps() {
9750        // gravity -8、damping 1、substeps 4、dt 1 → substep_dt 1/4
9751        // 半陰的 Euler: v_k = -8 * k/4、x_k = x_{k-1} + v_k/4 → 1 step 後 v = -8、x = -(2+4+6+8)/4 = -5
9752        let mut world = quiet_world();
9753        world.config.gravity = v3(0, -8, 0);
9754        world.config.damping = Fix128::ONE;
9755        world.config.substeps = 4;
9756        world.config.iterations = 1;
9757        let i = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
9758
9759        world.step(Fix128::ONE);
9760
9761        assert_eq!(world.bodies[i].velocity, v3(0, -8, 0));
9762        assert_eq!(world.bodies[i].position, v3(0, -5, 0));
9763        // substeps 1 なら x = -8 (1 回で落ちる)
9764        let mut single = quiet_world();
9765        single.config.gravity = v3(0, -8, 0);
9766        single.config.damping = Fix128::ONE;
9767        single.config.substeps = 1;
9768        let j = single.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
9769        single.step(Fix128::ONE);
9770        assert_eq!(single.bodies[j].position, v3(0, -8, 0));
9771    }
9772
9773    // ---- setters / counters ------------------------------------------
9774
9775    #[test]
9776    fn combined_material_uses_registered_pair_override_and_ignores_out_of_range() {
9777        let mut world = quiet_world();
9778        let a = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
9779        let b = world.add_body(RigidBody::new_dynamic(v3(1, 0, 0), Fix128::ONE));
9780        let metal = world.material_table.register_metal();
9781        world
9782            .material_table
9783            .set_pair_override(0, metal, r(1, 8), r(7, 8));
9784        world.set_body_material(b, metal);
9785        let c = world.combined_material(a, b);
9786        assert_eq!((c.friction, c.restitution), (r(1, 8), r(7, 8)));
9787        // 順序を入れ替えても同じ (対称)
9788        let c2 = world.combined_material(b, a);
9789        assert_eq!((c2.friction, c2.restitution), (r(1, 8), r(7, 8)));
9790        // 範囲外 index は DEFAULT_MATERIAL 扱い = (default, metal) の組ではなく (default, default)
9791        let d = world.combined_material(a, 99);
9792        let dd = world.material_table.combine(0, 0);
9793        assert_eq!((d.friction, d.restitution), (dd.friction, dd.restitution));
9794        // set_body_material の範囲外は無視
9795        world.set_body_material(99, metal);
9796        assert_eq!(world.body_materials.len(), 2);
9797    }
9798
9799    #[test]
9800    fn body_setters_ignore_out_of_range_and_apply_in_range() {
9801        let mut world = quiet_world();
9802        let a = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
9803        world.set_body_collision_radius(a, r(3, 2));
9804        assert_eq!(world.body_collision_radii[a], Some(r(3, 2)));
9805        world.set_body_collision_radius(5, Fix128::ONE);
9806        assert_eq!(world.body_collision_radii.len(), 1);
9807        world.clear_body_collision_radius(5);
9808        assert_eq!(world.body_collision_radii[a], Some(r(3, 2)));
9809        world.clear_body_collision_radius(a);
9810        assert_eq!(world.body_collision_radii[a], None);
9811
9812        let f = CollisionFilter {
9813            layer: 4,
9814            mask: 8,
9815            group: 2,
9816        };
9817        world.set_body_filter(a, f);
9818        assert_eq!(world.body_filters[a], f);
9819        world.set_body_filter(5, CollisionFilter::DEFAULT);
9820        assert_eq!(world.body_filters[a], f);
9821    }
9822
9823    #[test]
9824    fn active_body_count_excludes_sleeping() {
9825        let mut world = quiet_world();
9826        for i in 0..3 {
9827            world.add_body(RigidBody::new_dynamic(v3(i, 0, 0), Fix128::ONE));
9828        }
9829        assert_eq!(world.active_body_count(), 3);
9830        world.islands.sleep_data[1].state = crate::sleeping::SleepState::Sleeping;
9831        assert_eq!(world.active_body_count(), 2);
9832        world.islands.sleep_data[0].state = crate::sleeping::SleepState::Sleeping;
9833        world.islands.sleep_data[2].state = crate::sleeping::SleepState::Sleeping;
9834        assert_eq!(world.active_body_count(), 0);
9835    }
9836
9837    // ---- hook / modifier clearing, RigidBody builders -------------------
9838
9839    #[cfg(feature = "std")]
9840    #[test]
9841    fn clear_pre_solve_hooks_restores_normal_contact_response() {
9842        // veto hook が入っている間は contact が捨てられて A は動かない
9843        let mut world = contact_world(Fix128::ONE, Fix128::ONE, Fix128::ONE);
9844        world.add_pre_solve_hook(Box::new(|_a, _b, _c| false));
9845        world.add_pre_solve_hook(Box::new(|_a, _b, _c| false));
9846        assert_eq!(world.pre_solve_hooks.len(), 2);
9847        world.apply_contact_filters();
9848        world.solve_contact_constraints(r(1, 4));
9849        assert_eq!(world.bodies[0].position, Vec3Fix::ZERO);
9850
9851        // clear 後は hook 0 個 → 通常通り λ = 1、inv_w = 1/2 → A += 1/2
9852        world.clear_pre_solve_hooks();
9853        assert!(world.pre_solve_hooks.is_empty());
9854        world.apply_contact_filters();
9855        world.solve_contact_constraints(r(1, 4));
9856        assert_eq!(
9857            world.bodies[0].position,
9858            Vec3Fix::new(r(1, 2), Fix128::ZERO, Fix128::ZERO)
9859        );
9860        assert_eq!(
9861            world.bodies[1].position,
9862            Vec3Fix::new(r(1, 2), Fix128::ZERO, Fix128::ZERO)
9863        );
9864    }
9865
9866    #[cfg(feature = "std")]
9867    #[test]
9868    fn clear_contact_modifiers_restores_normal_contact_response() {
9869        struct Discard;
9870        impl ContactModifier for Discard {
9871            fn modify_contact(
9872                &self,
9873                _a: usize,
9874                _b: usize,
9875                _c: &mut Contact,
9876                _f: &mut Fix128,
9877                _r: &mut Fix128,
9878            ) -> bool {
9879                false
9880            }
9881        }
9882        let mut world = contact_world(Fix128::ONE, Fix128::ONE, Fix128::ONE);
9883        world.add_contact_modifier(Box::new(Discard));
9884        assert_eq!(world.contact_modifiers.len(), 1);
9885        world.apply_contact_filters();
9886        world.solve_contact_constraints(r(1, 4));
9887        assert_eq!(world.bodies[0].position, Vec3Fix::ZERO);
9888
9889        world.clear_contact_modifiers();
9890        assert!(world.contact_modifiers.is_empty());
9891        world.apply_contact_filters();
9892        world.solve_contact_constraints(r(1, 4));
9893        assert_eq!(
9894            world.bodies[0].position,
9895            Vec3Fix::new(r(1, 2), Fix128::ZERO, Fix128::ZERO)
9896        );
9897    }
9898
9899    #[test]
9900    fn set_angular_velocity_overwrites_and_drives_rotation_prediction() {
9901        let mut world = quiet_world();
9902        let i = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
9903        world.bodies[i].angular_velocity = v3(5, 5, 5);
9904        world.bodies[i].set_angular_velocity(v3(0, 0, 2));
9905        assert_eq!(world.bodies[i].angular_velocity, v3(0, 0, 2));
9906        // 直接代入 (apply_impulse 系と違い static でも通る)
9907        let mut st = RigidBody::new_static(Vec3Fix::ZERO);
9908        st.set_angular_velocity(v3(1, 0, 0));
9909        assert_eq!(st.angular_velocity, v3(1, 0, 0));
9910
9911        // 設定した ω が積分で使われる: z 軸 angle = 2 * 1/4 の回転
9912        world.integrate_positions(r(1, 4));
9913        let expected = QuatFix::from_axis_angle(v3(0, 0, 1), r(1, 2))
9914            .mul(QuatFix::IDENTITY)
9915            .normalize();
9916        assert_eq!(world.bodies[i].rotation, expected);
9917    }
9918
9919    #[test]
9920    fn with_angular_damping_is_stored_and_applied_once_per_frame_by_step() {
9921        let body = RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE).with_angular_damping(r(1, 2));
9922        assert_eq!(body.angular_damping, r(1, 2));
9923        assert_eq!(body.linear_damping, Fix128::ONE);
9924
9925        // substeps を変えても damping は frame 毎に 1 回だけ: ω_damped == ω_undamped * 1 * 1/2
9926        // (substep 毎なら substeps=4 で 1/16 になる)
9927        for substeps in [1usize, 4] {
9928            let mut undamped = quiet_world();
9929            undamped.config.substeps = substeps;
9930            let u = undamped.add_body(
9931                RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE).with_velocity(v3(3, 0, 0)),
9932            );
9933            undamped.bodies[u].set_angular_velocity(v3(0, 0, 2));
9934
9935            let mut damped = quiet_world();
9936            damped.config.substeps = substeps;
9937            let d = damped.add_body(
9938                RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE)
9939                    .with_velocity(v3(3, 0, 0))
9940                    .with_angular_damping(r(1, 2)),
9941            );
9942            damped.bodies[d].set_angular_velocity(v3(0, 0, 2));
9943
9944            undamped.step(r(1, 4));
9945            damped.step(r(1, 4));
9946
9947            let expected = undamped.bodies[u].angular_velocity * Fix128::ONE * r(1, 2);
9948            assert_eq!(
9949                damped.bodies[d].angular_velocity, expected,
9950                "substeps {substeps}"
9951            );
9952            // 角 damping は線速度に影響しない
9953            assert_eq!(
9954                damped.bodies[d].velocity, undamped.bodies[u].velocity,
9955                "substeps {substeps}"
9956            );
9957            let ratio = damped.bodies[d].angular_velocity.z.to_f64()
9958                / undamped.bodies[u].angular_velocity.z.to_f64();
9959            assert!(
9960                (ratio - 0.5).abs() < 1e-9,
9961                "substeps {substeps}: ratio {ratio}"
9962            );
9963        }
9964    }
9965
9966    #[test]
9967    fn with_sensor_stores_flag_and_sensor_body_skips_contact_response() {
9968        let sensor = RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE).with_sensor(true);
9969        assert!(sensor.is_sensor);
9970        assert!(!sensor.with_sensor(false).is_sensor);
9971
9972        // contact_world と同じ配置だが A を builder で sensor にする
9973        let mut world = quiet_world();
9974        let a =
9975            world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE).with_sensor(true));
9976        let b = world.add_body(RigidBody::new_dynamic(v3(1, 0, 0), Fix128::ONE));
9977        let contact = Contact {
9978            depth: Fix128::ONE,
9979            normal: v3(1, 0, 0),
9980            point_a: Vec3Fix::ZERO,
9981            point_b: Vec3Fix::ZERO,
9982        };
9983        world
9984            .contact_constraints
9985            .push(ContactConstraint::new(a, b, contact));
9986        world.solve_contact_constraints(r(1, 4));
9987        assert_eq!(world.bodies[a].position, Vec3Fix::ZERO);
9988        assert_eq!(world.bodies[b].position, v3(1, 0, 0));
9989
9990        // 自動検出でも sensor は trigger event になり contact constraint を作らない
9991        let mut auto = quiet_world();
9992        auto.add_body_with_radius(
9993            RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE).with_sensor(true),
9994            Fix128::ONE,
9995        );
9996        auto.add_body_with_radius(
9997            RigidBody::new_dynamic(
9998                Vec3Fix::new(r(3, 2), Fix128::ZERO, Fix128::ZERO),
9999                Fix128::ONE,
10000            ),
10001            Fix128::ONE,
10002        );
10003        auto.detect_collisions();
10004        assert!(auto.contact_constraints.is_empty());
10005        assert_eq!(auto.trigger_events().len(), 1);
10006    }
10007
10008    #[test]
10009    fn raycast_far_intersection_off_center_and_max_distance_cull_inside_aabb() {
10010        // origin (9,0,0) が球 (10,0,0) r=2 の内側 (中心から 1 ずれ): b=-1, c=-3, disc=4 → t_near=-1<0 → t_far = 1+2 = 3
10011        let world = sphere_world(&[v3(10, 0, 0)]);
10012        assert_eq!(
10013            world.raycast(v3(9, 0, 0), v3(1, 0, 0), Fix128::from_int(100)),
10014            Some((0, Fix128::from_int(3)))
10015        );
10016        // 球 (10,0,0) r=3 に y=2 の平行 ray: AABB [7,13] は max 7.5 の ray box と重なるが t = 10 - √5 ≈ 7.76 > 7.5 → None
10017        let mut big = quiet_world();
10018        big.add_body_with_radius(RigidBody::new_static(v3(10, 0, 0)), Fix128::from_int(3));
10019        assert_eq!(big.raycast(v3(0, 2, 0), v3(1, 0, 0), r(15, 2)), None);
10020        assert!(big
10021            .raycast(v3(0, 2, 0), v3(1, 0, 0), Fix128::from_int(8))
10022            .is_some());
10023    }
10024
10025    // ---- mutation-kill tests (cargo-mutants missed list, 2026-09-15) ----
10026
10027    /// `RigidBody::set_velocity` / `set_position` / `set_rotation` → `()`,
10028    /// `mass` → `Default` / `/` → `*`, `speed` → `Default`: every setter stores
10029    /// the exact value (position / rotation also reset the XPBD `prev_*`),
10030    /// mass 4 has `inv_mass` 1/4 and `mass()` must invert it back to 4 (the
10031    /// `*` mutant returns 1/4), a static body reports 0, and |(3, -4, 0)| = 5.
10032    #[test]
10033    fn rigid_body_setters_and_mass_speed_report_exact_values() {
10034        let mut body = RigidBody::new(v3(1, 1, 1), Fix128::from_int(4));
10035        assert_eq!(body.inv_mass, r(1, 4));
10036        assert_eq!(body.mass(), Fix128::from_int(4));
10037        assert_eq!(RigidBody::new_static(Vec3Fix::ZERO).mass(), Fix128::ZERO);
10038
10039        body.set_velocity(v3(3, -4, 0));
10040        assert_eq!(body.velocity, v3(3, -4, 0));
10041        assert_eq!(body.speed(), Fix128::from_int(5));
10042
10043        body.set_position(v3(7, -8, 9));
10044        assert_eq!(body.position, v3(7, -8, 9));
10045        assert_eq!(body.prev_position, v3(7, -8, 9));
10046
10047        let q = QuatFix::from_axis_angle(v3(0, 1, 0), r(1, 3));
10048        assert_ne!(q, QuatFix::IDENTITY);
10049        body.set_rotation(q);
10050        assert_eq!(body.rotation, q);
10051        assert_eq!(body.prev_rotation, q);
10052    }
10053
10054    /// `apply_impulse_at` line 297 / `add_torque` line 321 `*` → `/` on the x
10055    /// component: with `inv_inertia = (4, 4, 4)` and torque x = 2 the product
10056    /// is 8 (impulse) / 8 · dt = 4 (torque, dt = 1/2); the quotient would be
10057    /// 1/2 / 1/4. Lever arm r = (0, 1, 0), impulse (0, 0, 2) → r × F = (2, 0, 0).
10058    #[test]
10059    fn apply_impulse_at_and_add_torque_multiply_x_by_inverse_inertia() {
10060        let mut body = RigidBody::new(v3(5, 5, 5), Fix128::ONE);
10061        body.inv_inertia = v3(4, 4, 4);
10062        body.apply_impulse_at(v3(0, 0, 2), v3(5, 6, 5));
10063        assert_eq!(body.velocity, v3(0, 0, 2));
10064        assert_eq!(body.angular_velocity, v3(8, 0, 0));
10065
10066        let mut spun = RigidBody::new(Vec3Fix::ZERO, Fix128::ONE);
10067        spun.inv_inertia = v3(4, 4, 4);
10068        spun.add_torque(v3(2, 0, 0), r(1, 2));
10069        assert_eq!(spun.angular_velocity, v3(4, 0, 0));
10070    }
10071
10072    /// `body_count` → `0` / `1`, `get_body` → `Some(Default)`, `get_body_mut` →
10073    /// `None` / `Some(Default)`: counts 0 → 2 → 1 across add / remove, the
10074    /// accessors return the body at that slot (position (3, 4, 5), not the
10075    /// default origin) and `None` past the end; a write through `get_body_mut`
10076    /// must land in `bodies`.
10077    #[test]
10078    fn body_count_and_get_body_accessors_track_the_body_vector() {
10079        let mut world = quiet_world();
10080        assert_eq!(world.body_count(), 0);
10081        assert!(world.get_body(0).is_none());
10082        assert!(world.get_body_mut(0).is_none());
10083
10084        world.add_body(RigidBody::new_dynamic(v3(3, 4, 5), Fix128::ONE));
10085        world.add_body(RigidBody::new_dynamic(v3(6, 7, 8), Fix128::ONE));
10086        assert_eq!(world.body_count(), 2);
10087        assert_eq!(world.get_body(0).map(|b| b.position), Some(v3(3, 4, 5)));
10088        assert_eq!(world.get_body(1).map(|b| b.position), Some(v3(6, 7, 8)));
10089        assert!(world.get_body(2).is_none());
10090
10091        world
10092            .get_body_mut(1)
10093            .expect("body 1 exists")
10094            .set_velocity(v3(-1, -2, -3));
10095        assert_eq!(world.bodies[1].velocity, v3(-1, -2, -3));
10096        assert!(world.get_body_mut(2).is_none());
10097
10098        assert!(world.remove_body(0).is_some());
10099        assert_eq!(world.body_count(), 1);
10100    }
10101
10102    /// `set_body_material` line 1109, `set_body_collision_radius` line 1213,
10103    /// `clear_body_collision_radius` line 1220, `set_body_filter` line 1227,
10104    /// `combined_material` lines 1413 / 1418 `<` → `<=`: an index equal to the
10105    /// body count is out of range and must be ignored (the mutants index one
10106    /// past the end and panic). `body_filter` → `Default` is killed by reading
10107    /// back a non-default filter.
10108    #[test]
10109    fn index_equal_to_body_count_is_out_of_range_for_every_setter() {
10110        let mut world = quiet_world();
10111        let a = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
10112        let len = world.body_count();
10113        let metal = world.material_table.register_metal();
10114
10115        world.set_body_material(len, metal);
10116        world.set_body_collision_radius(len, Fix128::ONE);
10117        world.clear_body_collision_radius(len);
10118        world.set_body_filter(len, CollisionFilter::NONE);
10119        assert_eq!(
10120            world.body_materials,
10121            vec![crate::material::DEFAULT_MATERIAL]
10122        );
10123        assert_eq!(world.body_collision_radii, vec![None]);
10124        assert_eq!(world.body_filters, vec![CollisionFilter::DEFAULT]);
10125
10126        world.set_body_material(a, metal);
10127        let default_metal = world
10128            .material_table
10129            .combine(crate::material::DEFAULT_MATERIAL, metal);
10130        let default_default = world.material_table.combine(0, 0);
10131        assert_ne!(default_metal.friction, default_default.friction);
10132        for (x, y, want) in [
10133            (len, a, default_metal),
10134            (a, len, default_metal),
10135            (len, len, default_default),
10136        ] {
10137            let c = world.combined_material(x, y);
10138            assert_eq!(
10139                (c.friction, c.restitution),
10140                (want.friction, want.restitution),
10141                "({x}, {y}) must treat the out-of-range side as the default material"
10142            );
10143        }
10144        let custom = CollisionFilter {
10145            layer: 1 << 3,
10146            mask: 1 << 9,
10147            group: 5,
10148        };
10149        world.set_body_filter(a, custom);
10150        assert_eq!(world.body_filter(a), custom);
10151        assert_eq!(world.body_filter(len), CollisionFilter::DEFAULT);
10152    }
10153
10154    /// `begin_frame` / `end_frame` → `()` and `clear_contacts` → `()`: a cached
10155    /// manifold ages by one per `begin_frame` (4 calls → `stale_frames == 4`)
10156    /// and `end_frame` prunes it once it exceeds `max_stale_frames` (3);
10157    /// `clear_contacts` empties the constraint list.
10158    #[test]
10159    fn contact_cache_frame_lifecycle_ages_and_prunes_manifolds() {
10160        let mut world = quiet_world();
10161        let a = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
10162        let b = world.add_body(RigidBody::new_dynamic(v3(1, 0, 0), Fix128::ONE));
10163        push_contact(&mut world, a, b, r(1, 2), r(1, 2));
10164        let c = world.contact_constraints.pop().expect("pushed");
10165        world.add_contact(c);
10166        assert_eq!(world.contact_constraints.len(), 1);
10167        assert_eq!(world.contact_cache.manifold_count(), 1);
10168        assert_eq!(world.contact_cache.manifolds[0].stale_frames, 0);
10169
10170        for _ in 0..4 {
10171            world.begin_frame();
10172        }
10173        assert_eq!(world.contact_cache.manifolds[0].stale_frames, 4);
10174        world.end_frame();
10175        assert_eq!(world.contact_cache.manifold_count(), 0);
10176
10177        world.clear_contacts();
10178        assert!(world.contact_constraints.is_empty());
10179    }
10180
10181    /// `remove_joint` → `None` and line 1179 `>=` → `<`: two joints get
10182    /// indices 0 / 1, index 2 is out of range (`None`), removing 0 returns
10183    /// `Some` and leaves one joint, after which index 1 is out of range.
10184    #[test]
10185    fn add_and_remove_joint_roundtrip() {
10186        use crate::joint::{BallJoint, Joint};
10187        let mut world = quiet_world();
10188        let a = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
10189        let b = world.add_body(RigidBody::new_dynamic(v3(1, 0, 0), Fix128::ONE));
10190        let j0 = world.add_joint(Joint::Ball(BallJoint::new(
10191            a,
10192            b,
10193            Vec3Fix::ZERO,
10194            Vec3Fix::ZERO,
10195        )));
10196        let j1 = world.add_joint(Joint::Ball(BallJoint::new(
10197            b,
10198            a,
10199            v3(1, 0, 0),
10200            Vec3Fix::ZERO,
10201        )));
10202        assert_eq!((j0, j1), (0, 1));
10203        assert_eq!(world.joint_count(), 2);
10204        assert!(world.remove_joint(2).is_none());
10205        assert_eq!(world.joint_count(), 2);
10206        let removed = world.remove_joint(0).expect("joint 0 exists");
10207        assert_eq!(removed.bodies(), (a, b));
10208        assert_eq!(world.joint_count(), 1);
10209        assert_eq!(world.joints[0].bodies(), (b, a));
10210        assert!(world.remove_joint(1).is_none());
10211    }
10212
10213    /// `add_force_field` → `0` / `1`, `remove_force_field` → `None` and line
10214    /// 1203 `>=` → `<`, `step` line 1697 `!` deleted: fields get indices 0 / 1,
10215    /// index 2 is `None`, removing 1 leaves one field, and `step` applies the
10216    /// remaining directional field once per frame: F = (2, 0, 0), m = 1,
10217    /// dt = 1/4 → v = 1/2, then 8 substeps of 1/32 move x by 8 · (1/2)(1/32) =
10218    /// 1/8 (quiet world: no gravity, no damping).
10219    #[test]
10220    fn force_field_add_remove_and_step_applies_directional_force() {
10221        use crate::force::{ForceField, ForceFieldInstance};
10222        let mut world = quiet_world();
10223        let body = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
10224        let wind = ForceFieldInstance::new(ForceField::Directional {
10225            direction: Vec3Fix::UNIT_X,
10226            strength: Fix128::from_int(2),
10227        });
10228        let drag = ForceFieldInstance::new(ForceField::Drag {
10229            coefficient: Fix128::ONE,
10230        });
10231        assert_eq!(world.add_force_field(wind), 0);
10232        assert_eq!(world.add_force_field(drag), 1);
10233        assert_eq!(world.force_fields.len(), 2);
10234        assert!(world.remove_force_field(2).is_none());
10235        assert!(matches!(
10236            world.remove_force_field(1).map(|f| f.field),
10237            Some(ForceField::Drag { .. })
10238        ));
10239        assert!(world.remove_force_field(1).is_none());
10240        assert_eq!(world.force_fields.len(), 1);
10241
10242        world.step(r(1, 4));
10243        assert_eq!(
10244            world.bodies[body].velocity,
10245            Vec3Fix::new(r(1, 2), Fix128::ZERO, Fix128::ZERO)
10246        );
10247        assert_eq!(
10248            world.bodies[body].position,
10249            Vec3Fix::new(r(1, 8), Fix128::ZERO, Fix128::ZERO)
10250        );
10251    }
10252
10253    /// The live-proxy count the broad-phase reads every substep equals the
10254    /// number of proxies in the parked-body tree through parking, a contact
10255    /// unpark, a manual wake, a removal and turning the skip off.
10256    #[test]
10257    fn park_proxy_live_count_tracks_the_tree() {
10258        let mut w = PhysicsWorld::new(PhysicsConfig {
10259            gravity: Vec3Fix::ZERO,
10260            ..PhysicsConfig::default()
10261        });
10262        let r = Fix128::from_ratio(1, 2);
10263        for i in 0..20 {
10264            w.add_body_with_radius(
10265                RigidBody::new_dynamic(Vec3Fix::from_int(i * 3, 0, 0), Fix128::ONE),
10266                r,
10267            );
10268            w.islands.sleep_data[i as usize].state = SleepState::Sleeping;
10269        }
10270        let check = |w: &PhysicsWorld, ctx: &str| {
10271            assert_eq!(w.park.proxy_live, w.park.tree.proxy_count(), "{ctx}");
10272            assert_eq!(
10273                w.park.proxy_live,
10274                w.park.proxies.iter().flatten().count(),
10275                "{ctx}"
10276            );
10277        };
10278        w.step(Fix128::from_ratio(1, 60));
10279        check(&w, "parked");
10280        assert_eq!(w.park.proxy_live, 20);
10281        // Awake body 0 moved into body 1: the contact unparks it.
10282        w.wake_body(0);
10283        w.bodies[0].set_position(Vec3Fix::new(
10284            Fix128::from_ratio(22, 10),
10285            Fix128::ZERO,
10286            Fix128::ZERO,
10287        ));
10288        w.step(Fix128::from_ratio(1, 60));
10289        check(&w, "contact");
10290        assert!(w.stage_work().unparked > 0);
10291        w.wake_body(7);
10292        w.step(Fix128::from_ratio(1, 60));
10293        check(&w, "manual wake");
10294        w.remove_body(3);
10295        w.step(Fix128::from_ratio(1, 60));
10296        check(&w, "remove");
10297        w.set_sleep_skip(false);
10298        check(&w, "off");
10299        assert_eq!(w.park.proxy_live, 0);
10300    }
10301
10302    /// `is_sleeping` → `false` / `true` and `wake_body` → `()`: after one step
10303    /// the static body is asleep (static bodies always are), the moving
10304    /// dynamic body and an out-of-range index are not, and `wake_body` wakes
10305    /// the static one again.
10306    #[test]
10307    fn is_sleeping_and_wake_body_reflect_island_state() {
10308        let mut world = quiet_world();
10309        let floor = world.add_body(RigidBody::new_static(Vec3Fix::ZERO));
10310        let mover = world.add_body(RigidBody::new_dynamic(v3(0, 5, 0), Fix128::ONE));
10311        world.bodies[mover].velocity = v3(5, 0, 0);
10312        world.step(r(1, 4));
10313        assert!(world.is_sleeping(floor));
10314        assert!(!world.is_sleeping(mover));
10315        assert!(!world.is_sleeping(99));
10316        world.wake_body(floor);
10317        assert!(!world.is_sleeping(floor));
10318        assert!(!world.is_sleeping(mover));
10319    }
10320
10321    /// `set_sleep_config` → `()`: with `frames_to_sleep = 2` an idle body is
10322    /// asleep after two frames; under the default 60 frames it is not.
10323    #[test]
10324    fn set_sleep_config_changes_frames_to_sleep() {
10325        let cfg = SleepConfig {
10326            frames_to_sleep: 2,
10327            ..SleepConfig::default()
10328        };
10329        let mut quick = quiet_world();
10330        let idle = quick.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
10331        quick.set_sleep_config(cfg);
10332        assert_eq!(quick.islands.config, cfg);
10333        quick.step(r(1, 4));
10334        assert!(!quick.is_sleeping(idle));
10335        quick.step(r(1, 4));
10336        assert!(quick.is_sleeping(idle));
10337
10338        let mut slow = quiet_world();
10339        let idle2 = slow.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
10340        slow.step(r(1, 4));
10341        slow.step(r(1, 4));
10342        assert!(!slow.is_sleeping(idle2));
10343    }
10344
10345    /// `drain_contact_events` / `drain_trigger_events` → `vec![]`: draining
10346    /// returns the events of the last detection and leaves the collector
10347    /// empty.
10348    #[test]
10349    fn drain_events_return_the_events_and_empty_the_collector() {
10350        let mut world = two_spheres(3, 2, Fix128::ONE, Fix128::ONE);
10351        world.detect_collisions();
10352        assert_eq!(world.contact_events().len(), 1);
10353        let drained = world.drain_contact_events();
10354        assert_eq!(drained.len(), 1);
10355        assert_eq!((drained[0].body_a, drained[0].body_b), (0, 1));
10356        assert_eq!(drained[0].event_type, crate::event::ContactEventType::Begin);
10357        assert!(world.contact_events().is_empty());
10358        assert!(world.drain_contact_events().is_empty());
10359
10360        let mut sensor = two_spheres(3, 2, Fix128::ONE, Fix128::ONE);
10361        sensor.bodies[1].is_sensor = true;
10362        sensor.detect_collisions();
10363        let triggers = sensor.drain_trigger_events();
10364        assert_eq!(triggers.len(), 1);
10365        assert!(triggers[0].entered);
10366        assert!(sensor.trigger_events().is_empty());
10367        assert!(sensor.drain_trigger_events().is_empty());
10368    }
10369
10370    /// `step` line 1691 `<` → `==` / `>`: the joint island rebuild at the start
10371    /// of every step (`reset_unions` then `union` for every in-range joint)
10372    /// must leave both bodies of a joint in one island.
10373    #[test]
10374    fn step_rebuilds_joint_islands_from_scratch() {
10375        use crate::joint::{BallJoint, Joint};
10376        let mut world = quiet_world();
10377        let a = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
10378        let b = world.add_body(RigidBody::new_dynamic(v3(1, 0, 0), Fix128::ONE));
10379        let c = world.add_body(RigidBody::new_dynamic(v3(9, 0, 0), Fix128::ONE));
10380        world.add_joint(Joint::Ball(BallJoint::new(
10381            a,
10382            b,
10383            Vec3Fix::ZERO,
10384            Vec3Fix::ZERO,
10385        )));
10386        // `add_joint` already unions; wipe that so only `step`'s rebuild counts
10387        world.islands.reset_unions();
10388        assert_ne!(world.islands.find(a), world.islands.find(b));
10389        world.step(r(1, 4));
10390        assert_eq!(world.islands.find(a), world.islands.find(b));
10391        assert_ne!(world.islands.find(a), world.islands.find(c));
10392    }
10393
10394    /// `integrate_positions` line 1905 `-` → `+`: a kinematic body's velocity
10395    /// is (target - position) / dt = ((5, 2, 3) - (1, 2, 3)) · 4 = (16, 0, 0)
10396    /// right after integration (the sum would give (24, 16, 24)); the body
10397    /// lands on the target and `prev_position` keeps the old position.
10398    #[test]
10399    fn integrate_positions_kinematic_velocity_is_target_minus_position_over_dt() {
10400        let mut world = quiet_world();
10401        let k = world.add_body(RigidBody::new_kinematic(v3(1, 2, 3)));
10402        world.bodies[k].set_kinematic_target(v3(5, 2, 3), QuatFix::IDENTITY);
10403        world.integrate_positions(r(1, 4));
10404        assert_eq!(world.bodies[k].velocity, v3(16, 0, 0));
10405        assert_eq!(world.bodies[k].position, v3(5, 2, 3));
10406        assert_eq!(world.bodies[k].prev_position, v3(1, 2, 3));
10407    }
10408
10409    /// Same closed form as the `integrate_positions` test immediately above,
10410    /// but through the `Tgs`-path helper instead: velocity is
10411    /// (target - position) / dt = ((5, 2, 3) - (1, 2, 3)) · 4 = (16, 0, 0),
10412    /// the body lands exactly on the target, and `prev_position` keeps the
10413    /// pre-advance position (mirrors `integrate_positions`' `Kinematic`
10414    /// branch bit-for-bit; this is the oracle for
10415    /// `advance_kinematic_targets_for_tgs` itself, not for `step_tgs`).
10416    #[cfg(feature = "std")]
10417    #[test]
10418    fn advance_kinematic_targets_for_tgs_matches_integrate_positions_closed_form() {
10419        let mut world = quiet_world();
10420        let k = world.add_body(RigidBody::new_kinematic(v3(1, 2, 3)));
10421        world.bodies[k].set_kinematic_target(v3(5, 2, 3), QuatFix::IDENTITY);
10422        world.advance_kinematic_targets_for_tgs(r(1, 4));
10423        assert_eq!(world.bodies[k].velocity, v3(16, 0, 0));
10424        assert_eq!(world.bodies[k].position, v3(5, 2, 3));
10425        assert_eq!(world.bodies[k].prev_position, v3(1, 2, 3));
10426    }
10427
10428    /// A dynamic body with no target is untouched by the `Tgs`-path helper
10429    /// (only `BodyType::Kinematic` bodies are visited at all).
10430    #[cfg(feature = "std")]
10431    #[test]
10432    fn advance_kinematic_targets_for_tgs_leaves_dynamic_bodies_untouched() {
10433        let mut world = quiet_world();
10434        let d = world.add_body(RigidBody::new_dynamic(v3(1, 2, 3), Fix128::ONE));
10435        world.bodies[d].velocity = v3(7, 8, 9);
10436        world.advance_kinematic_targets_for_tgs(r(1, 4));
10437        assert_eq!(world.bodies[d].position, v3(1, 2, 3));
10438        assert_eq!(world.bodies[d].velocity, v3(7, 8, 9));
10439    }
10440
10441    /// A kinematic body with no target set (`kinematic_target == None`) is
10442    /// left at rest — only `prev_position`/`prev_rotation` are refreshed,
10443    /// matching `integrate_positions`' `Kinematic` branch (the `if let
10444    /// Some(...)` guards the actual advance in both).
10445    #[cfg(feature = "std")]
10446    #[test]
10447    fn advance_kinematic_targets_for_tgs_without_a_target_stays_at_rest() {
10448        let mut world = quiet_world();
10449        let k = world.add_body(RigidBody::new_kinematic(v3(1, 2, 3)));
10450        world.advance_kinematic_targets_for_tgs(r(1, 4));
10451        assert_eq!(world.bodies[k].position, v3(1, 2, 3));
10452        assert_eq!(world.bodies[k].velocity, Vec3Fix::ZERO);
10453        assert_eq!(world.bodies[k].prev_position, v3(1, 2, 3));
10454    }
10455
10456    /// `update_velocities` lines 2055 / 2073 `-` → `+` (relative velocity),
10457    /// 2089 `/` → `*` (`inv_w`), 2091 / 2093 `*` → `/` and 2093 `+` → `-`
10458    /// (friction split): both bodies dynamic (inv 1 / inv 3 → inv_w = 1/4),
10459    /// contact normal +y (B → A), vA = (3, -4, 0), vB = (1, -2, 0), e = 1/2,
10460    /// μ = 1/2.
10461    /// Restitution: vn = (2, -2, 0)·n = -2 → Δvn = 3 → A += 3/4, B -= 9/4 →
10462    /// A (3, -13/4, 0), B (1, -17/4, 0). Friction: rel = (2, 1, 0), vn2 = 1,
10463    /// tangent (2, 0, 0) speed 2, limit μ·|vn2| = 1/2 → impulse (1/2, 0, 0);
10464    /// A -= 1/2 · 1/4, B += 1/2 · 3/4 → A (23/8, -13/4, 0), B (11/8, -17/4, 0).
10465    /// (`inv_w = w_sum` would give A.x = 1 / B.x = 7, `+` → `-` on B gives 5/8.)
10466    #[test]
10467    fn update_velocities_friction_and_restitution_split_between_two_dynamic_bodies() {
10468        let mut world = quiet_world();
10469        let a = world.add_body(RigidBody::new_dynamic(v3(0, 1, 0), Fix128::ONE));
10470        let b = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
10471        world.bodies[b].inv_mass = Fix128::from_int(3);
10472        let contact = Contact {
10473            depth: r(1, 100),
10474            normal: v3(0, 1, 0),
10475            point_a: Vec3Fix::ZERO,
10476            point_b: Vec3Fix::ZERO,
10477        };
10478        let mut c = ContactConstraint::new(a, b, contact);
10479        c.friction = r(1, 2);
10480        c.restitution = r(1, 2);
10481        // 位置解の法線 λ = 1/4 → Coulomb 上限 μ λ / dt = 1/2 * 1/4 * 4 = 1/2
10482        c.cached_lambda = r(1, 4);
10483        world.contact_constraints.push(c);
10484        freeze_positions(&mut world);
10485        give_velocity(&mut world, a, v3(3, -4, 0));
10486        give_velocity(&mut world, b, v3(1, -2, 0));
10487
10488        world.update_velocities(r(1, 4));
10489
10490        assert_near_vec(
10491            world.bodies[a].velocity,
10492            Vec3Fix::new(r(23, 8), r(-13, 4), Fix128::ZERO),
10493            "A",
10494        );
10495        assert_near_vec(
10496            world.bodies[b].velocity,
10497            Vec3Fix::new(r(11, 8), r(-17, 4), Fix128::ZERO),
10498            "B",
10499        );
10500    }
10501
10502    /// Half of `W_SUM_EPSILON` (an inverse mass strictly below the skip
10503    /// threshold).
10504    fn half_epsilon() -> Fix128 {
10505        Fix128 {
10506            hi: 0,
10507            lo: W_SUM_EPSILON.lo >> 1,
10508        }
10509    }
10510
10511    /// `update_velocities` line 2050 `<` → `<=` / `==`: a contact whose
10512    /// `w_sum` is exactly `W_SUM_EPSILON` is solved (A: inv_mass = ε against a
10513    /// static B, vn = -4, e = 1/2 → Δvn = 6, inv_w = 1/ε, ε · 1/ε = 1 → A.y =
10514    /// -4 + 6 = 2), one with `w_sum = ε/2` is skipped (A keeps (0, -4, 0)).
10515    #[test]
10516    fn update_velocities_w_sum_epsilon_boundary() {
10517        for (inv_mass, expected_y) in [(W_SUM_EPSILON, 2), (half_epsilon(), -4)] {
10518            let mut world = quiet_world();
10519            let a = world.add_body(RigidBody::new_dynamic(v3(0, 1, 0), Fix128::ONE));
10520            let b = world.add_body(RigidBody::new_static(Vec3Fix::ZERO));
10521            world.bodies[a].inv_mass = inv_mass;
10522            let contact = Contact {
10523                depth: r(1, 100),
10524                normal: v3(0, 1, 0),
10525                point_a: Vec3Fix::ZERO,
10526                point_b: Vec3Fix::ZERO,
10527            };
10528            let mut c = ContactConstraint::new(a, b, contact);
10529            c.friction = Fix128::ZERO;
10530            c.restitution = r(1, 2);
10531            world.contact_constraints.push(c);
10532            freeze_positions(&mut world);
10533            give_velocity(&mut world, a, v3(0, -4, 0));
10534
10535            world.update_velocities(r(1, 4));
10536
10537            assert_eq!(
10538                world.bodies[a].velocity,
10539                v3(0, expected_y, 0),
10540                "inv_mass {inv_mass:?}"
10541            );
10542        }
10543    }
10544
10545    /// `solve_distance_constraints` line 2394 `<` → `<=` / `==`: with A at
10546    /// (0, 4, 0), inv_mass = ε, static B at the origin and target 1 the error
10547    /// is 3 and `w_sum == ε` is solved: dlambda = 3/ε, correction · ε moves A
10548    /// by the full error to (0, 1, 0). With inv_mass = ε/2 the constraint is
10549    /// skipped and A stays at (0, 4, 0).
10550    #[test]
10551    fn solve_distance_constraints_w_sum_epsilon_boundary() {
10552        for (inv_mass, expected) in [(W_SUM_EPSILON, v3(0, 1, 0)), (half_epsilon(), v3(0, 4, 0))] {
10553            let mut world = quiet_world();
10554            let a = world.add_body(RigidBody::new_dynamic(v3(0, 4, 0), Fix128::ONE));
10555            let b = world.add_body(RigidBody::new_static(Vec3Fix::ZERO));
10556            world.bodies[a].inv_mass = inv_mass;
10557            world.add_distance_constraint(dc(a, b));
10558            world.solve_distance_constraints(r(1, 4));
10559            assert_near_vec(world.bodies[a].position, expected, "A position");
10560            assert_eq!(world.bodies[b].position, Vec3Fix::ZERO);
10561        }
10562    }
10563
10564    /// `solve_contact_constraints` line 2497 `<` → `<=` / `==`: depth 1 along
10565    /// +x against a static B; `w_sum == ε` pushes A by the full depth
10566    /// (ε · 1/ε = 1) to (1, 0, 0), `w_sum = ε/2` is skipped.
10567    #[test]
10568    fn solve_contact_constraints_w_sum_epsilon_boundary() {
10569        for (inv_mass, expected) in [
10570            (W_SUM_EPSILON, v3(1, 0, 0)),
10571            (half_epsilon(), Vec3Fix::ZERO),
10572        ] {
10573            let mut world = contact_world(inv_mass, Fix128::ZERO, Fix128::ONE);
10574            world.solve_contact_constraints(r(1, 4));
10575            assert_eq!(world.bodies[0].position, expected, "inv_mass {inv_mass:?}");
10576            assert_eq!(world.bodies[1].position, v3(1, 0, 0));
10577        }
10578    }
10579
10580    /// `solve_distance_constraints` line 2377 `+` → `-` (anchor A) and line
10581    /// 2407 `*` → `/` (`delta_a`): A at (0, 2, 0) with local anchor (0, 1, 0)
10582    /// and inv_mass 2, static B at the origin, target 1 → world anchor
10583    /// (0, 3, 0), error 2, w_sum 2, dlambda 1, correction (0, -1, 0) · 2 →
10584    /// A lands on the origin. The `-` anchor gives error 0 (A stays), the `/`
10585    /// gives half the move (A at (0, 3/2, 0)).
10586    #[test]
10587    fn solve_distance_constraints_anchor_a_offset_and_inverse_mass_scaling() {
10588        let mut world = quiet_world();
10589        let a = world.add_body(RigidBody::new_dynamic(v3(0, 2, 0), Fix128::ONE));
10590        let b = world.add_body(RigidBody::new_static(Vec3Fix::ZERO));
10591        world.bodies[a].inv_mass = Fix128::from_int(2);
10592        let mut c = dc(a, b);
10593        c.local_anchor_a = v3(0, 1, 0);
10594        world.add_distance_constraint(c);
10595
10596        world.solve_distance_constraints(r(1, 4));
10597
10598        assert_near_vec(world.bodies[a].position, Vec3Fix::ZERO, "A");
10599        assert_eq!(world.bodies[b].position, Vec3Fix::ZERO);
10600        assert_eq!(world.distance_constraints[0].cached_lambda, Fix128::ONE);
10601    }
10602
10603    /// `detect_collisions` line 2933 / 2952 `<` → `>` (early return with 3
10604    /// bodies / 3 primitives) and line 3009 `*` → `+` (squared-distance early
10605    /// out): three collidable bodies, A at the origin and B at (2, 1, 1)
10606    /// (dist² = 6) with radii 3/2 each (combined 3: 6 < 3² = 9 overlaps, but
10607    /// 6 ≥ 3 + 3 = 6 would be culled), C far away. Exactly one contact, depth
10608    /// 3 - √6.
10609    #[test]
10610    fn detect_collisions_three_bodies_squared_distance_cull_uses_squared_radius() {
10611        let mut world = quiet_world();
10612        let radius = r(3, 2);
10613        world.add_body_with_radius(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE), radius);
10614        world.add_body_with_radius(RigidBody::new_dynamic(v3(2, 1, 1), Fix128::ONE), radius);
10615        world.add_body_with_radius(RigidBody::new_dynamic(v3(50, 50, 50), Fix128::ONE), radius);
10616        world.detect_collisions();
10617        assert_eq!(world.contact_constraints.len(), 1);
10618        let c = world.contact_constraints[0];
10619        assert_eq!((c.body_a, c.body_b), (0, 1));
10620        let expected_depth = Fix128::from_int(3) - Fix128::from_int(6).sqrt();
10621        assert!(
10622            near(c.contact.depth, expected_depth),
10623            "depth {:?} vs {expected_depth:?}",
10624            c.contact.depth
10625        );
10626        assert_eq!(world.contact_events().len(), 1);
10627    }
10628
10629    /// `remove_sdf_collider` → `None` and line 2863 `<` → `<=` / `==` / `>`,
10630    /// `set_sdf_collision_radius` → `()`: index 1 of a single collider is out
10631    /// of range (`None`, no panic), index 0 is removed, and the radius setter
10632    /// stores 3/4.
10633    #[test]
10634    fn remove_sdf_collider_boundary_and_sdf_radius_setter() {
10635        use crate::sdf_collider::{ClosureSdf, SdfCollider};
10636        let mut world = quiet_world();
10637        let ground = ClosureSdf::new(|_x, y, _z| y, |_x, _y, _z| (0.0, 1.0, 0.0));
10638        assert_eq!(
10639            world.add_sdf_collider(SdfCollider::new_static(
10640                Box::new(ground),
10641                Vec3Fix::ZERO,
10642                QuatFix::IDENTITY,
10643            )),
10644            0
10645        );
10646        assert!(world.remove_sdf_collider(1).is_none());
10647        assert_eq!(world.sdf_colliders.len(), 1);
10648        assert!(world.remove_sdf_collider(0).is_some());
10649        assert!(world.sdf_colliders.is_empty());
10650        assert!(world.remove_sdf_collider(0).is_none());
10651
10652        world.set_sdf_collision_radius(r(3, 4));
10653        assert_eq!(world.sdf_collision_radius, r(3, 4));
10654    }
10655
10656    /// `deserialize_state` line 3173 `<` → `<=`: a 4-byte snapshot (header
10657    /// only, count 0) of an empty world is valid.
10658    #[test]
10659    fn deserialize_state_accepts_header_only_snapshot_of_empty_world() {
10660        let src = quiet_world();
10661        let bytes = src.serialize_state();
10662        // v3: body 0 体でも header 12 + flag 1 + fingerprint 8
10663        assert_eq!(bytes.len(), 21);
10664        let mut dst = quiet_world();
10665        assert!(dst.deserialize_state(&bytes));
10666        assert_eq!(dst.body_count(), 0);
10667    }
10668
10669    /// `deserialize_state` lines 3237 / 3241 / 3245 `<` → `==` / `>`: when the
10670    /// parallel arrays are shorter than `bodies` (a body pushed straight into
10671    /// `bodies`), the resync loops must extend them to the body count with
10672    /// default entries.
10673    #[test]
10674    fn deserialize_state_extends_short_parallel_arrays_to_body_count() {
10675        let mut world = quiet_world();
10676        world.add_body(RigidBody::new_dynamic(v3(1, 2, 3), Fix128::ONE));
10677        world.set_body_collision_radius(0, r(1, 2));
10678        world
10679            .bodies
10680            .push(RigidBody::new_dynamic(v3(4, 5, 6), Fix128::ONE));
10681        world
10682            .bodies
10683            .push(RigidBody::new_dynamic(v3(7, 8, 9), Fix128::ONE));
10684        assert_eq!(world.body_collision_radii.len(), 1);
10685        let bytes = world.serialize_state();
10686        assert!(world.deserialize_state(&bytes));
10687        assert_eq!(world.body_collision_radii, vec![Some(r(1, 2)), None, None]);
10688        assert_eq!(world.body_filters, vec![CollisionFilter::DEFAULT; 3]);
10689        assert_eq!(
10690            world.body_materials,
10691            vec![crate::material::DEFAULT_MATERIAL; 3]
10692        );
10693        assert_eq!(world.islands.sleep_data.len(), 3);
10694        assert_eq!(world.bodies[2].position, v3(7, 8, 9));
10695    }
10696
10697    // ---- observe_body / observe_bodies (World Auditor WM-10 Physics 版) ----
10698
10699    #[test]
10700    fn observe_body_returns_none_out_of_range_and_fields_for_in_range() {
10701        let mut world = quiet_world();
10702        let mut a = RigidBody::new_dynamic(v3(1, 2, 3), Fix128::ONE);
10703        a.velocity = v3(4, 5, 6);
10704        a.angular_velocity = v3(7, 8, 9);
10705        world.add_body(a);
10706
10707        assert!(world.observe_body(1).is_none(), "範囲外 index は None");
10708
10709        let obs = world.observe_body(0).expect("範囲内 index は Some");
10710        assert_eq!(obs.body_index, 0);
10711        assert_eq!(obs.position, v3(1, 2, 3));
10712        assert_eq!(obs.velocity, v3(4, 5, 6));
10713        assert_eq!(obs.angular_velocity, v3(7, 8, 9));
10714        assert!(!obs.sleeping);
10715        assert!(!obs.in_contact, "接触していない body は in_contact = false");
10716    }
10717
10718    #[test]
10719    fn observe_bodies_returns_one_entry_per_body_in_index_order() {
10720        let mut world = quiet_world();
10721        world.add_body(RigidBody::new_dynamic(v3(1, 0, 0), Fix128::ONE));
10722        world.add_body(RigidBody::new_dynamic(v3(2, 0, 0), Fix128::ONE));
10723        world.add_body(RigidBody::new_dynamic(v3(3, 0, 0), Fix128::ONE));
10724
10725        let obs = world.observe_bodies();
10726        assert_eq!(obs.len(), 3);
10727        for (i, o) in obs.iter().enumerate() {
10728            assert_eq!(o.body_index, i);
10729            assert_eq!(o.position, world.bodies[i].position);
10730        }
10731    }
10732
10733    #[test]
10734    fn observe_body_reports_in_contact_from_this_frame_events() {
10735        // 重なった 2 球を 1 step 進めると contact event (Begin) が出る
10736        // (`tests/wm07_rollback_event_parity.rs` と同じ idiom — 最初から重なっている配置)
10737        let mut world = PhysicsWorld::new(PhysicsConfig {
10738            gravity: Vec3Fix::ZERO,
10739            ..PhysicsConfig::default()
10740        });
10741        let radius = Fix128::from_int(2);
10742        let a = RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE);
10743        world.add_body_with_radius(a, radius);
10744        let b = RigidBody::new_dynamic(v3(1, 0, 0), Fix128::ONE);
10745        world.add_body_with_radius(b, radius);
10746        let c = RigidBody::new_dynamic(v3(100, 100, 100), Fix128::ONE);
10747        world.add_body_with_radius(c, Fix128::from_ratio(1, 10));
10748
10749        world.step(Fix128::from_ratio(1, 60));
10750
10751        assert!(world.observe_body(0).unwrap().in_contact);
10752        assert!(world.observe_body(1).unwrap().in_contact);
10753        assert!(
10754            !world.observe_body(2).unwrap().in_contact,
10755            "遠くの body は接触していないはず"
10756        );
10757    }
10758
10759    /// `batches_are_body_disjoint` line 1646 `&&` → `||` / `a == b` → `a != b`
10760    /// (conflict through the *second* body of both constraints) and `||` →
10761    /// `&&` (a self-constraint counts its body once): constraints (1, 0) and
10762    /// (2, 0) share body 0 only as `body_b`, so one batch holding both is
10763    /// invalid; a batch holding only the self-constraint (1, 1) is valid.
10764    #[test]
10765    fn batches_are_body_disjoint_checks_second_body_and_self_constraints() {
10766        let mut world = quiet_world();
10767        for i in 0..3i64 {
10768            world.add_body(RigidBody::new_dynamic(v3(i, 0, 0), Fix128::ONE));
10769        }
10770        world.add_distance_constraint(dc(1, 0));
10771        world.add_distance_constraint(dc(2, 0));
10772        world.rebuild_batches();
10773        assert_eq!(world.num_batches(), 2);
10774        assert!(world.batches_are_body_disjoint());
10775
10776        let mut shared_b = ConstraintBatch::default();
10777        shared_b.distance_indices.extend([0usize, 1]);
10778        world.constraint_batches = vec![shared_b];
10779        assert!(!world.batches_are_body_disjoint());
10780
10781        world.distance_constraints.push(dc(1, 1));
10782        let mut self_only = ConstraintBatch::default();
10783        self_only.distance_indices.push(2);
10784        world.constraint_batches = vec![self_only];
10785        assert!(world.batches_are_body_disjoint());
10786
10787        let mut self_and_other = ConstraintBatch::default();
10788        self_and_other.distance_indices.extend([2usize, 0]);
10789        world.constraint_batches = vec![self_and_other];
10790        assert!(!world.batches_are_body_disjoint());
10791    }
10792
10793    /// `raycast` line 1397 `<` → `<=` / `==`: (1) two spheres hit at exactly
10794    /// the same t (mirror images across the ray) — the first BVH candidate
10795    /// wins; the candidates come in Morton order, so the sphere with the
10796    /// smaller y (body 1) is reported. (2) A ray cast in -x meets the far
10797    /// sphere first in Morton order (smaller x) and the nearer sphere second;
10798    /// the nearer one (body 1, t = 10 - 2 = 8) must replace it.
10799    #[test]
10800    fn raycast_ties_keep_first_candidate_and_nearer_later_candidate_replaces() {
10801        let mut tie = quiet_world();
10802        tie.add_body_with_radius(
10803            RigidBody::new_static(Vec3Fix::new(Fix128::from_int(5), r(1, 2), Fix128::ZERO)),
10804            Fix128::ONE,
10805        );
10806        tie.add_body_with_radius(
10807            RigidBody::new_static(Vec3Fix::new(Fix128::from_int(5), r(-1, 2), Fix128::ZERO)),
10808            Fix128::ONE,
10809        );
10810        let hit = tie
10811            .raycast(Vec3Fix::ZERO, v3(1, 0, 0), Fix128::from_int(20))
10812            .expect("both spheres straddle the ray");
10813        assert_eq!(hit.0, 1);
10814        // t = 5 - sqrt(3/4) for both
10815        let expected_t = Fix128::from_int(5) - r(3, 4).sqrt();
10816        assert!(near(hit.1, expected_t), "t {:?} vs {expected_t:?}", hit.1);
10817
10818        let far_first = sphere_world(&[v3(5, 0, 0), v3(10, 0, 0)]);
10819        assert_eq!(
10820            far_first.raycast(v3(20, 0, 0), v3(-1, 0, 0), Fix128::from_int(100)),
10821            Some((1, Fix128::from_int(8)))
10822        );
10823    }
10824
10825    /// `Debug for PhysicsWorld` → `Ok(())`: the output names the type and the
10826    /// body count.
10827    #[test]
10828    fn physics_world_debug_lists_type_and_counts() {
10829        let mut world = quiet_world();
10830        world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
10831        world.add_body(RigidBody::new_dynamic(v3(1, 0, 0), Fix128::ONE));
10832        let text = format!("{world:?}");
10833        assert!(text.starts_with("PhysicsWorld"), "{text}");
10834        assert!(text.contains("bodies: 2"), "{text}");
10835        assert!(text.contains("joints: 0"), "{text}");
10836    }
10837
10838    // ── closed-form unit tests of the shape / motor / contact-force /
10839    //    participant paths (they are otherwise exercised only from tests/) ──
10840
10841    fn half_unit_box() -> crate::shape::Shape {
10842        crate::shape::Shape::Box {
10843            half_extents: Vec3Fix::from_int(1, 1, 1),
10844        }
10845    }
10846
10847    /// oracle: a box of half-extent 1 at the origin and a sphere of radius 1
10848    /// meet when the sphere centre is within `1 + 1 = 2` of the box face along
10849    /// `x` (1.5: overlap, 2.5: clear), whichever side carries the shape; two
10850    /// shaped boxes meet when the centres are under `1 + 1 = 2` apart; two
10851    /// plain spheres of radius 1 meet when the centres are under 2 apart.
10852    /// An index past the bodies, or a body with no collider, never overlaps,
10853    /// and `set_body_shape` refuses an index past the bodies.
10854    #[test]
10855    fn colliders_overlap_follows_the_closed_form_gaps() {
10856        let mut world = quiet_world();
10857        let shaped = world.add_body(RigidBody::new_static(Vec3Fix::ZERO));
10858        assert!(world.set_body_shape(shaped, &half_unit_box()));
10859        assert!(!world.set_body_shape(99, &half_unit_box()));
10860        let near = world.add_body_with_radius(
10861            RigidBody::new(
10862                Vec3Fix::new(Fix128::from_ratio(3, 2), Fix128::ZERO, Fix128::ZERO),
10863                Fix128::ONE,
10864            ),
10865            Fix128::ONE,
10866        );
10867        let far = world.add_body_with_radius(
10868            RigidBody::new(
10869                Vec3Fix::new(Fix128::from_ratio(5, 2), Fix128::ZERO, Fix128::ZERO),
10870                Fix128::ONE,
10871            ),
10872            Fix128::ONE,
10873        );
10874        let bare = world.add_body(RigidBody::new(Vec3Fix::ZERO, Fix128::ONE));
10875        // shape against sphere, both argument orders
10876        assert!(world.colliders_overlap(shaped, near));
10877        assert!(world.colliders_overlap(near, shaped));
10878        assert!(!world.colliders_overlap(shaped, far));
10879        assert!(!world.colliders_overlap(far, shaped));
10880        // two plain spheres 1 apart (< 2) overlap
10881        assert!(world.colliders_overlap(near, far));
10882        // a body without a collider, or an index past the bodies
10883        assert!(!world.colliders_overlap(shaped, bare));
10884        assert!(!world.colliders_overlap(near, bare));
10885        assert!(!world.colliders_overlap(near, 99));
10886        // shape against shape: the far body becomes a box 2.5 away (clear),
10887        // then the near one a box 1.5 away (overlapping)
10888        assert!(world.set_body_shape(far, &half_unit_box()));
10889        assert!(!world.colliders_overlap(shaped, far));
10890        assert!(world.set_body_shape(near, &half_unit_box()));
10891        assert!(world.colliders_overlap(shaped, near));
10892    }
10893
10894    /// oracle: a sphere of radius 1/2 dropped onto the top face (`y = 1`) of a
10895    /// static box of half-extent 1 comes to rest with its centre at
10896    /// `1 + 1/2 = 3/2`, the contact of a shaped body with a plain sphere
10897    /// (the GJK path of the narrow phase), for either body order.
10898    #[test]
10899    fn sphere_rests_on_a_shaped_box_at_face_plus_radius() {
10900        for box_first in [true, false] {
10901            let mut world = PhysicsWorld::new(SolverConfig::default());
10902            let ball = || {
10903                RigidBody::new(
10904                    Vec3Fix::new(Fix128::ZERO, Fix128::from_ratio(8, 5), Fix128::ZERO),
10905                    Fix128::ONE,
10906                )
10907            };
10908            let (floor, b) = if box_first {
10909                let f = world.add_body(RigidBody::new_static(Vec3Fix::ZERO));
10910                let b = world.add_body_with_radius(ball(), Fix128::from_ratio(1, 2));
10911                (f, b)
10912            } else {
10913                let b = world.add_body_with_radius(ball(), Fix128::from_ratio(1, 2));
10914                let f = world.add_body(RigidBody::new_static(Vec3Fix::ZERO));
10915                (f, b)
10916            };
10917            assert!(world.set_body_shape(floor, &half_unit_box()));
10918            for _ in 0..120 {
10919                world.step(Fix128::from_ratio(1, 60));
10920            }
10921            let y = world.bodies[b].position.y.to_f64();
10922            assert!((y - 1.5).abs() < 0.02, "box_first {box_first}: y {y}");
10923            assert!(world.bodies[b].position.x.abs() < Fix128::from_ratio(1, 1000));
10924        }
10925    }
10926
10927    /// oracle: the motor accessors report `false` / `None` for an index that
10928    /// was never returned, set the mode they name, and `remove_joint`
10929    /// drops the motors of the removed joint and re-points those of the
10930    /// last joint to the freed index (swap-remove).
10931    #[test]
10932    fn joint_motor_accessors_and_removal_remap() {
10933        let mut world = quiet_world();
10934        for x in 0..3 {
10935            world.add_body(RigidBody::new(Vec3Fix::from_int(x, 0, 0), Fix128::ONE));
10936        }
10937        let ball = |a, b| {
10938            Joint::Ball(crate::joint::BallJoint::new(
10939                a,
10940                b,
10941                Vec3Fix::ZERO,
10942                Vec3Fix::ZERO,
10943            ))
10944        };
10945        let j0 = world.add_joint(ball(0, 1));
10946        let j1 = world.add_joint(ball(1, 2));
10947        let pd = crate::motor::PdController::new(Fix128::ONE, Fix128::ONE, Fix128::from_int(10));
10948        let m0 = world.add_joint_motor(j0, pd);
10949        let m1 = world.add_joint_motor(j1, pd);
10950        assert!(world.set_joint_motor_velocity_target(m1, Fix128::from_int(2)));
10951        assert!(!world.set_joint_motor_velocity_target(9, Fix128::ONE));
10952        assert_eq!(
10953            world.joint_motor_mut(m1).unwrap().controller.mode,
10954            crate::motor::MotorMode::Velocity
10955        );
10956        assert!(world.disable_joint_motor(m1));
10957        assert!(!world.disable_joint_motor(9));
10958        assert_eq!(
10959            world.joint_motor_mut(m1).unwrap().controller.mode,
10960            crate::motor::MotorMode::Off
10961        );
10962        assert!(world.joint_motor_mut(9).is_none());
10963
10964        let r0 = world.add_joint_motor_3d(
10965            j0,
10966            Vec3Fix::from_int(1, 1, 1),
10967            Vec3Fix::from_int(1, 1, 1),
10968            Fix128::from_int(5),
10969        );
10970        let r1 = world.add_joint_motor_3d(
10971            j1,
10972            Vec3Fix::from_int(1, 1, 1),
10973            Vec3Fix::from_int(1, 1, 1),
10974            Fix128::from_int(5),
10975        );
10976        let target = QuatFix::from_axis_angle(Vec3Fix::from_int(0, 1, 0), Fix128::from_ratio(1, 4));
10977        assert!(world.set_joint_motor_3d_rotation_target(r1, target));
10978        assert!(!world.set_joint_motor_3d_rotation_target(9, target));
10979        assert_eq!(
10980            world.joint_motor_3d_mut(r1).unwrap().target_rotation,
10981            target
10982        );
10983        assert!(world.joint_motor_3d_mut(9).is_none());
10984        let _ = (m0, r0);
10985
10986        assert!(world.remove_joint(7).is_none());
10987        assert!(world.remove_joint(j0).is_some());
10988        // the motors of joint 0 are gone, the ones of joint 1 now name joint 0
10989        assert_eq!(world.joint_motors.len(), 1);
10990        assert_eq!(world.joint_motors[0].joint_index, 0);
10991        assert_eq!(world.joint_motors_3d.len(), 1);
10992        assert_eq!(world.joint_motors_3d[0].0, 0);
10993        assert_eq!(world.joint_motors_3d[0].1.target_rotation, target);
10994    }
10995
10996    /// oracle: a contact carrying `λ` between a body of mass 1 (`w = 1`) and a
10997    /// static body (`w = 0`), in a frame `dt = 1` of 8 substeps (`h = 1/8`),
10998    /// carries the force `F = λ / (w·h²) = 64·λ`; with `λ = 1/2` that is 32.
10999    /// It gives one normal arrow of 32, two friction arrows of `μ·F = 0.3·32`
11000    /// and one cone of height 32. A contact between two static bodies
11001    /// carries none.
11002    #[test]
11003    fn contact_forces_are_lambda_over_w_h_squared() {
11004        let mut world = PhysicsWorld::new(SolverConfig::default());
11005        let a = world.add_body(RigidBody::new(Vec3Fix::from_int(0, 1, 0), Fix128::ONE));
11006        let b = world.add_body(RigidBody::new_static(Vec3Fix::ZERO));
11007        let s = world.add_body(RigidBody::new_static(Vec3Fix::from_int(5, 0, 0)));
11008        let contact = Contact {
11009            depth: Fix128::from_ratio(1, 100),
11010            normal: Vec3Fix::from_int(0, 1, 0),
11011            point_a: Vec3Fix::from_int(0, 1, 0),
11012            point_b: Vec3Fix::ZERO,
11013        };
11014        let mut c = ContactConstraint::new(a, b, contact);
11015        c.cached_lambda = Fix128::from_ratio(1, 2);
11016        let mut dead = ContactConstraint::new(b, s, contact);
11017        dead.cached_lambda = Fix128::ONE;
11018        world.contact_constraints = vec![c, dead];
11019        let dt = Fix128::ONE;
11020        let forces = world.contact_forces(dt);
11021        assert_eq!(forces.len(), 1);
11022        assert_eq!(forces[0].0, Vec3Fix::from_int(0, 1, 0));
11023        assert_eq!(forces[0].1, Vec3Fix::from_int(0, 1, 0));
11024        assert_eq!(forces[0].2, Fix128::from_int(32));
11025        assert_eq!(
11026            world.contact_constraint_force(&world.contact_constraints[0], dt),
11027            Some(Fix128::from_int(32))
11028        );
11029        assert_eq!(
11030            world.contact_constraint_force(&world.contact_constraints[1], dt),
11031            None
11032        );
11033        let arrows = world.contact_arrows(dt);
11034        assert_eq!(arrows.len(), 1);
11035        assert_eq!(arrows[0].force_magnitude, Fix128::from_int(32));
11036        let friction = world.contact_friction_arrows(dt);
11037        assert_eq!(friction.len(), 2);
11038        let mu_f = (Fix128::from_int(32) * Fix128::from_ratio(3, 10)).to_f64();
11039        for f in &friction {
11040            assert!((f.force_magnitude.to_f64() - 9.6).abs() < 1e-9 && (mu_f - 9.6).abs() < 1e-9);
11041        }
11042        let cones = world.contact_friction_cones(dt);
11043        assert_eq!(cones.len(), 1);
11044        assert_eq!(cones[0].height, Fix128::from_int(32));
11045    }
11046
11047    /// oracle: static colliders are indexed in insertion order, `remove`
11048    /// shifts the later ones down and refuses an index past the end.
11049    #[test]
11050    fn static_collider_add_remove_count() {
11051        let mut world = quiet_world();
11052        let plane = |h: i64| {
11053            crate::static_collider::StaticCollider::Plane(
11054                crate::plane_collider::PlaneCollider::new(
11055                    Vec3Fix::from_int(0, 1, 0),
11056                    Fix128::from_int(h),
11057                ),
11058            )
11059        };
11060        assert_eq!(world.add_static_collider(plane(0)), 0);
11061        assert_eq!(world.add_static_collider(plane(1)), 1);
11062        assert_eq!(world.static_collider_count(), 2);
11063        assert!(world.remove_static_collider(2).is_none());
11064        assert!(world.remove_static_collider(0).is_some());
11065        assert_eq!(world.static_collider_count(), 1);
11066        // the plane at height 1 moved to index 0: a sphere of radius 1/2 at
11067        // y = 1 (its centre on that plane) touches it with depth 1/2
11068        let c = world.static_colliders_slice()[0]
11069            .collide_sphere(Vec3Fix::from_int(0, 1, 0), Fix128::from_ratio(1, 2))
11070            .expect("touching the plane at y = 1");
11071        assert!((c.depth.to_f64() - 0.5).abs() < 1e-9, "{c:?}");
11072    }
11073
11074    /// oracle: with the dynamic-tree broad-phase every body with a collision
11075    /// radius has a proxy after a step, and its fattened box contains the
11076    /// sphere's own box (`centre ± r`); a body without a radius has none.
11077    /// With the hybrid broad-phase two overlapping spheres (gap −1/2) are
11078    /// found and pushed apart, while a far one is left at rest.
11079    #[test]
11080    fn dynamic_tree_and_hybrid_broadphases_find_the_pairs() {
11081        for kind in [Broadphase::DynamicTree, Broadphase::Hybrid] {
11082            let mut world = quiet_world();
11083            world.set_broadphase(kind);
11084            assert_eq!(world.broadphase(), kind);
11085            let a =
11086                world.add_body_with_radius(RigidBody::new(Vec3Fix::ZERO, Fix128::ONE), Fix128::ONE);
11087            let b = world.add_body_with_radius(
11088                RigidBody::new(
11089                    Vec3Fix::new(Fix128::from_ratio(3, 2), Fix128::ZERO, Fix128::ZERO),
11090                    Fix128::ONE,
11091                ),
11092                Fix128::ONE,
11093            );
11094            let far = world.add_body_with_radius(
11095                RigidBody::new(Vec3Fix::from_int(50, 0, 0), Fix128::ONE),
11096                Fix128::ONE,
11097            );
11098            let bare = world.add_body(RigidBody::new(Vec3Fix::from_int(-50, 0, 0), Fix128::ONE));
11099            world.step(Fix128::from_ratio(1, 60));
11100            let gap = (world.bodies[b].position - world.bodies[a].position).length();
11101            assert!(gap > Fix128::from_ratio(3, 2), "{kind:?}: gap {gap:?}");
11102            assert_eq!(world.bodies[far].position, Vec3Fix::from_int(50, 0, 0));
11103            if kind == Broadphase::DynamicTree {
11104                assert_eq!(world.broadphase_stats().proxies, 3);
11105                let bx = world.broadphase_proxy_aabb(far).expect("proxy");
11106                assert!(bx.min.x <= Fix128::from_int(49) && bx.max.x >= Fix128::from_int(51));
11107                assert!(world.broadphase_proxy_aabb(bare).is_none());
11108                assert!(world.broadphase_proxy_aabb(99).is_none());
11109            }
11110        }
11111    }
11112
11113    /// A participant adding a constant force to one body (or failing).
11114    struct ConstantPush {
11115        body: usize,
11116        force: Vec3Fix,
11117        fail: bool,
11118    }
11119
11120    impl crate::world_participant::Participant for ConstantPush {
11121        fn kind(&self) -> crate::world_participant::ParticipantKind {
11122            crate::world_participant::ParticipantKind::new(0x5055_5348)
11123        }
11124        fn substep(
11125            &mut self,
11126            ctx: &mut crate::world_participant::SubstepCtx<'_>,
11127            _h: Fix128,
11128        ) -> Result<(), crate::world_participant::ParticipantFault> {
11129            if self.fail {
11130                return Err(crate::world_participant::ParticipantFault::InvalidState);
11131            }
11132            ctx.add_force(self.body, self.force)
11133                .map_err(|_| crate::world_participant::ParticipantFault::InvalidState)
11134        }
11135        fn observe(&self, _: &mut crate::world_participant::ObservationSink) {}
11136        fn write_state(&self, _: &mut Vec<u8>) {}
11137        fn check_state(&self, _: &[u8]) -> Result<(), crate::world_participant::StateError> {
11138            Ok(())
11139        }
11140        fn read_state(&mut self, _: &[u8]) {}
11141    }
11142
11143    /// oracle: under the TGS backend, with no gravity and no damping, a
11144    /// participant applying `F = (4, 0, 0)` to a body of mass 2 for one frame
11145    /// `dt = 1/64` (8 dyadic substeps) gives `Δv = F·dt/m = 1/32` along `x`,
11146    /// with and without a joint in the world; a participant that fails
11147    /// records a participant fault and applies nothing.
11148    #[test]
11149    fn tgs_participant_force_gives_f_dt_over_m() {
11150        for with_joint in [false, true] {
11151            for fail in [false, true] {
11152                let mut world = PhysicsWorld::new(SolverConfig {
11153                    gravity: Vec3Fix::ZERO,
11154                    damping: Fix128::ONE,
11155                    solver_backend: SolverBackend::Tgs,
11156                    ..Default::default()
11157                });
11158                let body = world.add_body(RigidBody::new(Vec3Fix::ZERO, Fix128::from_int(2)));
11159                if with_joint {
11160                    let other =
11161                        world.add_body(RigidBody::new(Vec3Fix::from_int(0, 10, 0), Fix128::ONE));
11162                    let anchor = world.add_body(RigidBody::new_static(Vec3Fix::from_int(0, 12, 0)));
11163                    world.add_joint(Joint::Ball(crate::joint::BallJoint::new(
11164                        other,
11165                        anchor,
11166                        Vec3Fix::ZERO,
11167                        Vec3Fix::from_int(0, -2, 0),
11168                    )));
11169                }
11170                world
11171                    .add_participant(Box::new(ConstantPush {
11172                        body,
11173                        force: Vec3Fix::from_int(4, 0, 0),
11174                        fail,
11175                    }))
11176                    .expect("register");
11177                world.step(Fix128::from_ratio(1, 64));
11178                let v = world.bodies[body].velocity;
11179                if fail {
11180                    assert_eq!(v, Vec3Fix::ZERO, "joint {with_joint}");
11181                    assert!(
11182                        matches!(
11183                            world.fault(),
11184                            Some(crate::world_participant::WorldFault::Participant {
11185                                index: 0,
11186                                ..
11187                            })
11188                        ),
11189                        "joint {with_joint}: {:?}",
11190                        world.fault()
11191                    );
11192                } else {
11193                    assert!(
11194                        (v.x.to_f64() - 1.0 / 32.0).abs() < 1e-12,
11195                        "joint {with_joint}: v {v:?}"
11196                    );
11197                    assert!(
11198                        v.y.is_zero() && v.z.is_zero(),
11199                        "joint {with_joint}: v {v:?}"
11200                    );
11201                    assert_eq!(world.fault(), None);
11202                }
11203            }
11204        }
11205    }
11206
11207    /// oracle: for an isotropic `inv_inertia = (c, c, c)` the checked world
11208    /// inverse inertia is `Some(c · τ)` (component-wise `checked_mul`) for 64
11209    /// non-identity orientations, equal to the unchecked value, and `None`
11210    /// when a product leaves the range.
11211    #[test]
11212    fn checked_world_inv_inertia_apply_of_an_isotropic_body_is_c_tau() {
11213        let c = Fix128::from_ratio(5, 2);
11214        let tau = Vec3Fix::new(
11215            Fix128::from_ratio(3, 7),
11216            Fix128::from_ratio(-11, 13),
11217            Fix128::from_ratio(1, 3),
11218        );
11219        let expected = Vec3Fix::new(tau.x * c, tau.y * c, tau.z * c);
11220        let mut differ = 0;
11221        for i in 0..16_i64 {
11222            let axis = Vec3Fix::new(
11223                Fix128::from_ratio(1 + i, 3),
11224                Fix128::from_ratio(2 - i, 5),
11225                Fix128::from_ratio(3 + 2 * i, 7),
11226            )
11227            .normalize();
11228            for k in 1..=4_i64 {
11229                let mut b = RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE);
11230                b.inv_inertia = Vec3Fix::new(c, c, c);
11231                b.rotation =
11232                    QuatFix::from_axis_angle(axis, Fix128::from_ratio(7 * k + i, 9)).normalize();
11233                let got = b.checked_world_inv_inertia_apply(tau);
11234                assert_eq!(got, Some(b.world_inv_inertia_apply(tau)));
11235                differ += usize::from(got != Some(expected));
11236            }
11237        }
11238        assert_eq!(differ, 0, "{differ}/64 orientations differ from c·τ");
11239        let mut big = RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE);
11240        big.inv_inertia = Vec3Fix::new(
11241            Fix128::from_int(4),
11242            Fix128::from_int(4),
11243            Fix128::from_int(4),
11244        );
11245        let huge = Vec3Fix::new(Fix128::from_int(1_i64 << 62), Fix128::ZERO, Fix128::ZERO);
11246        assert_eq!(big.checked_world_inv_inertia_apply(huge), None);
11247    }
11248}
11249
11250/// loom model of the invariant `BodySlicePtr` / `DistConstraintSlicePtr` /
11251/// `ContactConstraintSlicePtr` rely on: every constraint of one colour batch
11252/// touches disjoint dynamic bodies, so the batch can be written by any
11253/// number of threads without synchronisation.
11254///
11255/// The raw-pointer wrappers themselves are invisible to loom (plain memory);
11256/// what loom can exhaustively check is the *protocol*: take the real batches
11257/// `rebuild_batches` produces for scenes that exercise the hard cases (a
11258/// shared static floor, a dynamic hub, rods under contact), give every
11259/// constraint of a batch to its own loom thread, and let each thread write
11260/// its two body slots through a `loom::cell::UnsafeCell`. If the colouring
11261/// ever placed two constraints sharing a dynamic body in one batch, loom
11262/// reports the unsynchronised concurrent access on one of its interleavings.
11263/// Run with `RUSTFLAGS="--cfg loom" cargo test --lib loom_` (quality-deep).
11264#[cfg(all(test, loom))]
11265mod loom_tests {
11266    use super::*;
11267    use loom::cell::UnsafeCell;
11268    use loom::sync::Arc;
11269    use loom::thread;
11270
11271    fn v3(x: i64, y: i64, z: i64) -> Vec3Fix {
11272        Vec3Fix::from_int(x, y, z)
11273    }
11274
11275    /// (dynamic-body pairs per batch) for a scene: static bodies are excluded
11276    /// because the solver borrows them shared (`BodyRef::Static`).
11277    fn batches_of(world: &mut PhysicsWorld) -> Vec<Vec<(Option<usize>, Option<usize>)>> {
11278        world.rebuild_batches();
11279        let dynamic = |i: usize| (!world.bodies[i].inv_mass.is_zero()).then_some(i);
11280        world
11281            .constraint_batches
11282            .iter()
11283            .map(|b| {
11284                let mut pairs = Vec::new();
11285                for &ci in &b.distance_indices {
11286                    let c = world.distance_constraints[ci];
11287                    pairs.push((dynamic(c.body_a), dynamic(c.body_b)));
11288                }
11289                for &ci in &b.contact_indices {
11290                    let c = world.contact_constraints[ci];
11291                    pairs.push((dynamic(c.body_a), dynamic(c.body_b)));
11292                }
11293                pairs
11294            })
11295            .collect()
11296    }
11297
11298    fn check_batches(batches: Vec<Vec<(Option<usize>, Option<usize>)>>, n_bodies: usize) {
11299        for batch in batches {
11300            // loom explores every interleaving of up to a handful of threads
11301            for chunk in batch.chunks(3) {
11302                let chunk: Vec<_> = chunk.to_vec();
11303                let n = n_bodies;
11304                loom::model(move || {
11305                    let slots: Arc<Vec<UnsafeCell<u32>>> =
11306                        Arc::new((0..n).map(|_| UnsafeCell::new(0)).collect());
11307                    let handles: Vec<_> = chunk
11308                        .iter()
11309                        .copied()
11310                        .map(|(a, b)| {
11311                            let slots = Arc::clone(&slots);
11312                            thread::spawn(move || {
11313                                for body in [a, b].into_iter().flatten() {
11314                                    // the solver writes position / velocity of
11315                                    // each dynamic body of its constraint
11316                                    slots[body].with_mut(|p| unsafe { *p += 1 });
11317                                }
11318                            })
11319                        })
11320                        .collect();
11321                    for h in handles {
11322                        h.join().unwrap();
11323                    }
11324                });
11325            }
11326        }
11327    }
11328
11329    #[test]
11330    fn loom_batches_touch_disjoint_dynamic_bodies_static_floor_hub() {
11331        // 6 dynamic spheres resting on one static sphere + 2 rods
11332        let mut world = PhysicsWorld::new(SolverConfig {
11333            gravity: Vec3Fix::ZERO,
11334            ..SolverConfig::default()
11335        });
11336        let floor =
11337            world.add_body_with_radius(RigidBody::new_static(Vec3Fix::ZERO), Fix128::from_int(4));
11338        let mut ids = Vec::new();
11339        for i in 0..6i64 {
11340            let angle = Fix128::from_ratio(i, 6) * Fix128::TWO_PI;
11341            let (s, c) = angle.sin_cos();
11342            let pos = Vec3Fix::new(
11343                c * Fix128::from_int(4),
11344                s * Fix128::from_int(4),
11345                Fix128::ZERO,
11346            );
11347            ids.push(
11348                world.add_body_with_radius(RigidBody::new_dynamic(pos, Fix128::ONE), Fix128::ONE),
11349            );
11350        }
11351        for w in ids.windows(2).take(2) {
11352            world.add_distance_constraint(DistanceConstraint {
11353                body_a: w[0],
11354                body_b: w[1],
11355                local_anchor_a: Vec3Fix::ZERO,
11356                local_anchor_b: Vec3Fix::ZERO,
11357                target_distance: Fix128::from_int(4),
11358                compliance: Fix128::ZERO,
11359                cached_lambda: Fix128::ZERO,
11360            });
11361        }
11362        let _ = floor;
11363        world.clear_contacts();
11364        world.detect_collisions();
11365        assert!(
11366            !world.contact_constraints.is_empty(),
11367            "spheres must touch the floor"
11368        );
11369        let n = world.bodies.len();
11370        check_batches(batches_of(&mut world), n);
11371    }
11372
11373    #[test]
11374    fn loom_batches_touch_disjoint_dynamic_bodies_dynamic_hub() {
11375        // one dynamic hub connected to 5 dynamic spokes: every spoke rod
11376        // shares the hub, so the colouring must serialise them into 5 batches
11377        let mut world = PhysicsWorld::new(SolverConfig {
11378            gravity: Vec3Fix::ZERO,
11379            ..SolverConfig::default()
11380        });
11381        let hub = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
11382        for i in 0..5i64 {
11383            let spoke = world.add_body(RigidBody::new_dynamic(v3(3 * (i + 1), 0, 0), Fix128::ONE));
11384            world.add_distance_constraint(DistanceConstraint {
11385                body_a: hub,
11386                body_b: spoke,
11387                local_anchor_a: Vec3Fix::ZERO,
11388                local_anchor_b: Vec3Fix::ZERO,
11389                target_distance: Fix128::from_int(3 * (i + 1)),
11390                compliance: Fix128::ZERO,
11391                cached_lambda: Fix128::ZERO,
11392            });
11393        }
11394        let n = world.bodies.len();
11395        let batches = batches_of(&mut world);
11396        assert_eq!(batches.len(), 5, "hub forces one batch per spoke");
11397        check_batches(batches, n);
11398    }
11399}