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    ///
1680    /// [`Self::add_joint`] checks that both body indices are in range. A joint
1681    /// pushed here directly with an index past the last body makes the next
1682    /// step panic in the joint solve (index out of bounds), except a joint whose
1683    /// two indices are equal: the solve skips it, and so do the island rebuilds.
1684    pub joints: Vec<Joint>,
1685    /// Joint motors (one-axis PD controllers), applied every substep by
1686    /// [`Self::step`]; see [`Self::add_joint_motor`]
1687    joint_motors: Vec<crate::motor::JointMotor>,
1688    /// Three-axis rotation motors `(joint index, controller)`, applied every
1689    /// substep by [`Self::step`]; see [`Self::add_joint_motor_3d`]
1690    joint_motors_3d: Vec<(usize, crate::motor::PdController3D)>,
1691    /// Force fields applied at the start of each step
1692    pub force_fields: Vec<ForceFieldInstance>,
1693    /// Contact and trigger event collector
1694    pub events: EventCollector,
1695    /// Island manager for sleeping and connectivity tracking
1696    pub islands: IslandManager,
1697    /// Per-body collision radius for automatic sphere-based collision detection.
1698    /// `None` means the body does not participate in auto-detection.
1699    body_collision_radii: Vec<Option<Fix128>>,
1700    /// Which broad-phase finds the candidate pairs.
1701    broadphase: Broadphase,
1702    /// The persistent tree of [`Broadphase::DynamicTree`] (unused otherwise).
1703    broadphase_tree: crate::dynamic_bvh::DynamicAabbTree,
1704    /// Body index → its proxy in `broadphase_tree`.
1705    broadphase_proxies: Vec<Option<u32>>,
1706    /// The layers of [`Broadphase::Hybrid`] (unused otherwise).
1707    broadphase_hybrid: BroadphaseHybrid,
1708    /// The convex collider a body carries, when it has one — one shape or a
1709    /// compound of them: the narrow-phase works on it instead of the bounding
1710    /// sphere (see [`crate::shape`], [`crate::compound`]).
1711    body_colliders: Vec<Option<crate::body_collider::BodyCollider>>,
1712    /// Per-body collision filter (layer/mask/group)
1713    body_filters: Vec<CollisionFilter>,
1714    /// 範囲外の積を踏んだか (sticky、[`PhysicsWorld::overflow_detected`])
1715    ///
1716    /// ⚠️ **`Fix128` の `Mul` 自体からは world に到達できない** (純粋な trait
1717    /// impl、crate は `no_std` 対応なので `thread_local` も使えない) ので、
1718    /// **発散が起きる経路** (積分と速度導出) で `checked_mul` を呼んでここに
1719    /// 立てる doctrine B-12 の「`mul` の中で world の flag を立てる」は
1720    /// 到達不能なため、WM-01 の結論「`mul` の積が範囲外を演算側で見る」を
1721    /// 呼び出し側で実現した形
1722    overflow_detected: bool,
1723    /// Substeps still to run in the current frame, counting the one being run
1724    /// (0 outside a substep loop). A kinematic body with a target closes
1725    /// `1 / kinematic_substeps_left` of its remaining gap in each substep, so
1726    /// the velocity derived after every substep is the frame velocity
1727    /// (see [`kinematic_substep_pose`]).
1728    kinematic_substeps_left: usize,
1729    /// Warm-start impulse cache for the [`SolverBackend::Tgs`] path, persisted
1730    /// across frames the same way [`Self::contact_cache`] is for XPBD.
1731    /// Unused (and empty) while `config.solver_backend` is `Xpbd`.
1732    #[cfg(feature = "std")]
1733    tgs_impulse_cache: crate::solver_tgs::ImpulseCache,
1734    /// Per-body identity that survives `remove_body`'s `swap_remove` (the
1735    /// index does not). The TGS warm-start cache keys contacts and distance
1736    /// constraints by these, so a reorder of `bodies` cannot hand one
1737    /// contact's cached impulse to another. Kept parallel to `bodies` by
1738    /// [`Self::sync_body_stable_ids`], which also covers bodies pushed onto
1739    /// the public `bodies` field directly.
1740    #[cfg(feature = "std")]
1741    body_stable_ids: Vec<u64>,
1742    /// The next id [`Self::sync_body_stable_ids`] hands out.
1743    #[cfg(feature = "std")]
1744    next_body_stable_id: u64,
1745    /// Skip parked sleeping bodies in `step` (see [`StageWork`]).
1746    sleep_skip: bool,
1747    /// Parked bodies, their tree and caches.
1748    park: ParkState,
1749    /// Bumped by every change that can invalidate a parked body's verdict but is
1750    /// not visible in its own state (shapes, radii, static colliders, removal).
1751    park_generation: u64,
1752    /// Work counters of the last `step`.
1753    stage_work: StageWork,
1754    /// SDF contacts resolved by the current (or last) step, in the order they
1755    /// were applied ([`Self::last_step_sdf_contacts`]). Cleared at the head of
1756    /// each step; the allocation is kept from step to step.
1757    sdf_contact_log: Vec<crate::sdf_collider::SdfContact>,
1758    /// Substep index the SDF contacts resolved now are recorded under.
1759    sdf_contact_substep: usize,
1760    /// The [`SdfField::generation`](crate::sdf_collider::SdfField::generation)
1761    /// of each SDF collider as last seen (at the previous step head, or when
1762    /// the collider was added); a change wakes the sleeping bodies.
1763    sdf_generations: Vec<u64>,
1764    /// Per-body buffers the parallel SDF resolution writes into before they
1765    /// are appended to `sdf_contact_log` in body order (kept from step to
1766    /// step).
1767    #[cfg(feature = "parallel")]
1768    sdf_contact_scratch: Vec<Vec<crate::sdf_collider::SdfContact>>,
1769    /// Participants in registration order (see [`crate::world_participant`]).
1770    /// Held behind a mutex so that the world stays `Sync` while a participant
1771    /// is only `Send`; every access during a step goes through `&mut self`
1772    /// ([`std::sync::Mutex::get_mut`], no locking).
1773    #[cfg(feature = "std")]
1774    participants: std::sync::Mutex<Vec<Box<dyn crate::world_participant::Participant>>>,
1775    /// Run order and declared ports of `participants`, rebuilt whenever a
1776    /// participant or a field is added; `None` while no participant is
1777    /// registered.
1778    #[cfg(feature = "std")]
1779    participant_plan: Option<crate::world_participant::ParticipantPlan>,
1780    /// The first fault recorded (sticky until [`Self::clear_fault`]).
1781    fault: Option<crate::world_participant::WorldFault>,
1782    /// Shared fields, owned by the world.
1783    fields: crate::world_participant::FieldBoard,
1784    /// Continuous collision of the step ([`Self::set_continuous_collision`]).
1785    ccd: WorldCcdConfig,
1786}
1787
1788/// Fold `bytes` into `hash` with FNV-1a (64-bit).
1789///
1790/// Used by [`PhysicsWorld::population_fingerprint`]; kept as a free
1791/// function (not a closure) to avoid a double-borrow of `hash`.
1792fn fnv1a_fold(hash: &mut u64, bytes: &[u8]) {
1793    for &b in bytes {
1794        *hash ^= u64::from(b);
1795        *hash = hash.wrapping_mul(0x0000_0001_0000_01b3);
1796    }
1797}
1798
1799/// The contact of one body with one SDF collider: as its shape or compound when it
1800/// has one, otherwise as a sphere of `radius` at its position.
1801#[cfg(feature = "std")]
1802fn sdf_contact_of(
1803    collider: Option<&crate::body_collider::BodyCollider>,
1804    body: &RigidBody,
1805    radius: Fix128,
1806    sdf: &crate::sdf_collider::SdfCollider,
1807) -> Option<crate::collider::Contact> {
1808    match collider {
1809        Some(c) => c.sdf_contact(body.position, body.rotation, sdf),
1810        None => crate::sdf_collider::collide_sphere_sdf(body.position, radius, sdf),
1811    }
1812}
1813
1814/// Pose of a kinematic body after one substep towards its target.
1815///
1816/// `left` is the number of substeps left in the frame including the current
1817/// one. Each substep closes `1 / left` of the remaining gap, which makes the
1818/// per-substep displacement the same in every substep (the gap shrinks by
1819/// `(left - 1) / left` each time), and the last substep (`left <= 1`, also the
1820/// value outside a substep loop) lands on the target exactly.
1821fn kinematic_substep_pose(
1822    position: Vec3Fix,
1823    rotation: QuatFix,
1824    target_pos: Vec3Fix,
1825    target_rot: QuatFix,
1826    left: usize,
1827) -> (Vec3Fix, QuatFix) {
1828    if left <= 1 {
1829        return (target_pos, target_rot);
1830    }
1831    let frac = Fix128::ONE / Fix128::from_int(left as i64);
1832    (
1833        position + (target_pos - position) * frac,
1834        crate::interpolation::slerp(rotation, target_rot, frac),
1835    )
1836}
1837
1838impl PhysicsWorld {
1839    /// Create a new, empty physics world with the given solver configuration.
1840    ///
1841    /// # Examples
1842    ///
1843    /// ```
1844    /// use alice_physics::{Fix128, PhysicsConfig, PhysicsWorld, RigidBody, Vec3Fix};
1845    ///
1846    /// let config = PhysicsConfig::default();
1847    /// let mut world = PhysicsWorld::new(config);
1848    ///
1849    /// // World starts with no bodies
1850    /// assert_eq!(world.bodies.len(), 0);
1851    ///
1852    /// // Add a body and verify it is tracked
1853    /// let body = RigidBody::new_dynamic(Vec3Fix::from_int(0, 5, 0), Fix128::ONE);
1854    /// let id = world.add_body(body);
1855    /// assert_eq!(id, 0);
1856    /// assert_eq!(world.bodies.len(), 1);
1857    /// ```
1858    #[must_use]
1859    pub fn new(config: SolverConfig) -> Self {
1860        Self {
1861            config,
1862            bodies: Vec::new(),
1863            distance_constraints: Vec::new(),
1864            contact_constraints: Vec::new(),
1865            sdf_colliders: Vec::new(),
1866            sdf_collision_radius: Fix128::from_ratio(1, 2), // 0.5 default
1867            static_colliders: Vec::new(),
1868            constraint_batches: Vec::new(),
1869            batches_dirty: true,
1870            batch_static_bodies: Vec::new(),
1871            contact_cache: crate::contact_cache::ContactCache::new(),
1872            material_table: crate::material::MaterialTable::new(),
1873            body_materials: Vec::new(),
1874            #[cfg(feature = "std")]
1875            pre_solve_hooks: Vec::new(),
1876            #[cfg(feature = "std")]
1877            contact_modifiers: Vec::new(),
1878            #[cfg(feature = "std")]
1879            contact_discarded: Vec::new(),
1880            contact_pre_vn: Vec::new(),
1881            free_rotation: Vec::new(),
1882            predicted_pose: Vec::new(),
1883            #[cfg(feature = "gpu-solver-bridge")]
1884            gpu_solver_bridge: None,
1885            joints: Vec::new(),
1886            joint_motors: Vec::new(),
1887            joint_motors_3d: Vec::new(),
1888            force_fields: Vec::new(),
1889            events: EventCollector::new(),
1890            islands: IslandManager::new(0, SleepConfig::default()),
1891            body_collision_radii: Vec::new(),
1892            broadphase: Broadphase::default(),
1893            broadphase_tree: crate::dynamic_bvh::DynamicAabbTree::new(),
1894            broadphase_proxies: Vec::new(),
1895            broadphase_hybrid: BroadphaseHybrid::new(),
1896            body_colliders: Vec::new(),
1897            body_filters: Vec::new(),
1898            overflow_detected: false,
1899            kinematic_substeps_left: 0,
1900            #[cfg(feature = "std")]
1901            tgs_impulse_cache: crate::solver_tgs::ImpulseCache::new(),
1902            #[cfg(feature = "std")]
1903            body_stable_ids: Vec::new(),
1904            #[cfg(feature = "std")]
1905            next_body_stable_id: 0,
1906            sleep_skip: true,
1907            park: ParkState::default(),
1908            park_generation: 0,
1909            stage_work: StageWork::default(),
1910            sdf_contact_log: Vec::new(),
1911            sdf_contact_substep: 0,
1912            sdf_generations: Vec::new(),
1913            #[cfg(feature = "parallel")]
1914            sdf_contact_scratch: Vec::new(),
1915            #[cfg(feature = "std")]
1916            participants: std::sync::Mutex::new(Vec::new()),
1917            #[cfg(feature = "std")]
1918            participant_plan: None,
1919            fault: None,
1920            fields: crate::world_participant::FieldBoard::new(),
1921            ccd: WorldCcdConfig::new(),
1922        }
1923    }
1924
1925    /// Reset the world to the same empty state [`Self::new`] produces,
1926    /// keeping the current [`SolverConfig`].
1927    ///
1928    /// # World Auditor の `reset()` 契約 (WM-07)
1929    ///
1930    /// 「同じ初期状態から N step を 2 回実行すると bit 一致する」を成立させる
1931    /// ための入口 `*self = Self::new(self.config)` と等価 (`config` は
1932    /// `Copy`) — `bodies` / `joints` / `force_fields` / `islands` /
1933    /// `overflow_detected` を含む **全 field が `new` と同じ既定値**に戻る
1934    /// (installed hook / GPU bridge も含む、呼び出し側が明示的に残したい
1935    /// 状態があれば `reset_world` の前に退避すること)
1936    ///
1937    /// [`Self::deserialize_state`] による rollback とは異なる経路 — rollback
1938    /// は「population を caller が replay で合わせてから特定 frame の連続状態を
1939    /// 復元する」契約だが、`reset_world` は「population ごと空にする」契約
1940    /// 両者は **「population を目標状態まで合わせてから連続状態を復元する」**
1941    /// という同じ形の特殊例 (`reset_world` は目標 population が空の場合)
1942    ///
1943    /// # Examples
1944    ///
1945    /// ```
1946    /// use alice_physics::{Fix128, PhysicsConfig, PhysicsWorld, RigidBody, Vec3Fix};
1947    ///
1948    /// let config = PhysicsConfig::default();
1949    /// let mut world = PhysicsWorld::new(config);
1950    /// world.add_body(RigidBody::new_dynamic(Vec3Fix::from_int(0, 5, 0), Fix128::ONE));
1951    /// assert_eq!(world.bodies.len(), 1);
1952    ///
1953    /// world.reset_world();
1954    /// assert_eq!(world.bodies.len(), 0);
1955    /// assert!(!world.overflow_detected());
1956    /// ```
1957    pub fn reset_world(&mut self) {
1958        *self = Self::new(self.config);
1959    }
1960
1961    /// v0.11.0: install a GPU solver bridge for automatic contact-solve
1962    /// routing. Subsequent calls to [`Self::step`] / the substep loop
1963    /// route contact-solve through this bridge instead of the CPU
1964    /// solver. Pass `Some(bridge)` to install; use
1965    /// [`Self::take_gpu_solver_bridge`] to remove.
1966    ///
1967    /// The bridge is stored as `Box<dyn GpuSolverBridge + Send + Sync>`,
1968    /// so implementers must satisfy `Send + Sync`. For ALICE-TRT v3.0.0+
1969    /// this is guaranteed because `TrtSolverAdapter` uses
1970    /// `Arc<GpuDevice>` and holds only `Send + Sync` fields.
1971    ///
1972    /// Callers who want explicit control without installing the bridge
1973    /// on the world (game-engine wrapper hosts, hot-swap harnesses)
1974    /// can continue to use [`Self::step_with_bridge`] /
1975    /// [`Self::substep_with_bridge`] /
1976    /// [`Self::solve_contact_constraints_with_bridge`] with an
1977    /// externally-owned bridge — those methods ignore the installed
1978    /// bridge and take a `&mut B` parameter.
1979    #[cfg(feature = "gpu-solver-bridge")]
1980    pub fn set_gpu_solver_bridge(
1981        &mut self,
1982        bridge: Option<Box<dyn crate::gpu_bridge::GpuSolverBridge + Send + Sync>>,
1983    ) {
1984        self.gpu_solver_bridge = bridge;
1985    }
1986
1987    /// v0.11.0: remove and return the currently installed GPU solver
1988    /// bridge, if any. Subsequent calls to [`Self::step`] /
1989    /// the substep loop revert to the CPU contact solver.
1990    #[cfg(feature = "gpu-solver-bridge")]
1991    pub fn take_gpu_solver_bridge(
1992        &mut self,
1993    ) -> Option<Box<dyn crate::gpu_bridge::GpuSolverBridge + Send + Sync>> {
1994        self.gpu_solver_bridge.take()
1995    }
1996
1997    /// v0.11.0: whether a GPU solver bridge is currently installed on
1998    /// this world. Cheap; does not touch the bridge.
1999    #[cfg(feature = "gpu-solver-bridge")]
2000    #[must_use]
2001    pub const fn gpu_solver_bridge_installed(&self) -> bool {
2002        self.gpu_solver_bridge.is_some()
2003    }
2004
2005    /// Bring `body_stable_ids` to `bodies.len()`: drop ids past the end and
2006    /// give every body without one a fresh id. Bodies keep their id for as
2007    /// long as they live; ids are never reused.
2008    #[cfg(feature = "std")]
2009    fn sync_body_stable_ids(&mut self) {
2010        let n = self.bodies.len();
2011        self.body_stable_ids.truncate(n);
2012        while self.body_stable_ids.len() < n {
2013            self.body_stable_ids.push(self.next_body_stable_id);
2014            self.next_body_stable_id += 1;
2015        }
2016    }
2017
2018    /// Add rigid body, returns index
2019    ///
2020    /// The body's rotations are stored as unit quaternions (see
2021    /// [`RigidBody::set_rotation`]).
2022    pub fn add_body(&mut self, mut body: RigidBody) -> usize {
2023        body.make_rotations_unit();
2024        let idx = self.bodies.len();
2025        self.bodies.push(body);
2026        self.body_materials.push(crate::material::DEFAULT_MATERIAL);
2027        self.body_collision_radii.push(None);
2028        self.body_colliders.push(None);
2029        self.body_filters.push(CollisionFilter::DEFAULT);
2030        self.islands.resize(idx + 1);
2031        #[cfg(feature = "std")]
2032        self.sync_body_stable_ids();
2033        idx
2034    }
2035
2036    /// Add rigid body with a collision sphere radius for automatic detection.
2037    ///
2038    /// Bodies with a collision radius participate in BVH broad-phase and
2039    /// sphere-sphere narrow-phase collision detection during `step()`.
2040    ///
2041    /// The body's rotations are stored as unit quaternions, as in
2042    /// [`Self::add_body`].
2043    pub fn add_body_with_radius(&mut self, mut body: RigidBody, radius: Fix128) -> usize {
2044        body.make_rotations_unit();
2045        let idx = self.bodies.len();
2046        self.bodies.push(body);
2047        self.body_materials.push(crate::material::DEFAULT_MATERIAL);
2048        self.body_collision_radii.push(Some(radius));
2049        self.body_colliders.push(None);
2050        self.body_filters.push(CollisionFilter::DEFAULT);
2051        self.islands.resize(idx + 1);
2052        #[cfg(feature = "std")]
2053        self.sync_body_stable_ids();
2054        idx
2055    }
2056
2057    /// Add a rigid body shaped like `shape`, made of material of `density`, with
2058    /// its centre of mass at `position`.
2059    ///
2060    /// The body gets the shape's mass (`density × volume`), the shape's principal
2061    /// inertia about the centre of mass (not the unit-sphere inertia
2062    /// [`RigidBody::new`] assigns), and the shape's bounding sphere about the
2063    /// centre of mass as its collision radius, so it takes part in the sphere
2064    /// broad- and narrow-phase like any body added with
2065    /// [`Self::add_body_with_radius`]. See [`crate::shape`] for the frames.
2066    ///
2067    /// # Errors
2068    ///
2069    /// [`ShapeError`](crate::shape::ShapeError) when the density is not positive,
2070    /// a dimension is not, or the mass or inertia does not fit `Fix128`. Nothing is
2071    /// added in that case.
2072    pub fn add_shaped_body(
2073        &mut self,
2074        shape: &crate::shape::Shape,
2075        density: Fix128,
2076        position: Vec3Fix,
2077    ) -> Result<usize, crate::shape::ShapeError> {
2078        let (mass, inertia) = shape.mass_and_inertia(density)?;
2079        let mut body = RigidBody::new_dynamic(position, mass);
2080        body.inv_inertia = Vec3Fix::new(
2081            Fix128::ONE / inertia.x,
2082            Fix128::ONE / inertia.y,
2083            Fix128::ONE / inertia.z,
2084        );
2085        let idx = self.add_body_with_radius(body, shape.bounding_radius());
2086        self.body_colliders[idx] = Some(crate::body_collider::BodyCollider::Shape(*shape));
2087        Ok(idx)
2088    }
2089
2090    /// Give an existing body a convex `shape` as its collider: its collision radius
2091    /// becomes the shape's bounding radius about the centre of mass, and a pair of
2092    /// bodies of which either carries a shape is decided by the shape
2093    /// ([`crate::collider::contact`]; against a body without one, by its collision
2094    /// sphere) instead of by the bounding spheres. Returns `false` for an out-of-range index.
2095    ///
2096    /// The body's position is taken as the shape's **centre of mass** (see
2097    /// [`crate::shape`]); its mass and inertia are not changed — use
2098    /// [`Self::add_shaped_body`] for a body whose mass properties come from the shape.
2099    pub fn set_body_shape(&mut self, body_idx: usize, shape: &crate::shape::Shape) -> bool {
2100        if body_idx >= self.bodies.len() || body_idx >= self.body_colliders.len() {
2101            return false;
2102        }
2103        self.park_generation = self.park_generation.wrapping_add(1);
2104        self.body_collision_radii[body_idx] = Some(shape.bounding_radius());
2105        self.body_colliders[body_idx] = Some(crate::body_collider::BodyCollider::Shape(*shape));
2106        true
2107    }
2108
2109    /// Whether the colliders of two bodies overlap, the same way the contact
2110    /// generation decides it: GJK on their shapes when both carry one
2111    /// ([`Self::set_body_shape`], [`Self::add_shaped_body`]), GJK on the shape and
2112    /// the other body's collision sphere when one does, otherwise the test of
2113    /// their collision spheres. `false` for an index that is not a body or a body
2114    /// without a collider. Touching exactly counts as overlapping when a shape is
2115    /// involved (GJK's convention) and as clear for two spheres (the contact
2116    /// generation's).
2117    #[must_use]
2118    pub fn colliders_overlap(&self, a: usize, b: usize) -> bool {
2119        let (Some(body_a), Some(body_b)) = (self.bodies.get(a), self.bodies.get(b)) else {
2120            return false;
2121        };
2122        let collider = |i: usize| self.body_colliders.get(i).and_then(Option::as_ref);
2123        let radius = |i: usize| self.body_collision_radii.get(i).copied().flatten();
2124        let pose_a = (body_a.position, body_a.rotation);
2125        let pose_b = (body_b.position, body_b.rotation);
2126        match (collider(a), collider(b)) {
2127            (Some(ca), Some(cb)) => crate::body_collider::colliders_meet(ca, pose_a, cb, pose_b),
2128            (Some(ca), None) => radius(b).is_some_and(|rb| {
2129                crate::body_collider::collider_meets_sphere(
2130                    ca,
2131                    pose_a,
2132                    crate::collider::Sphere::new(body_b.position, rb),
2133                )
2134            }),
2135            (None, Some(cb)) => radius(a).is_some_and(|ra| {
2136                crate::body_collider::collider_meets_sphere(
2137                    cb,
2138                    pose_b,
2139                    crate::collider::Sphere::new(body_a.position, ra),
2140                )
2141            }),
2142            (None, None) => match (radius(a), radius(b)) {
2143                (Some(ra), Some(rb)) => {
2144                    let reach = ra + rb;
2145                    (body_a.position - body_b.position).length_squared() < reach * reach
2146                }
2147                _ => false,
2148            },
2149        }
2150    }
2151
2152    /// Add a rigid body made of a [`CompoundShape`](crate::compound::CompoundShape):
2153    /// material of `density`, the compound's centre of mass at `position`.
2154    ///
2155    /// The mass is the children's total and the inertia is their summed tensor about
2156    /// the common centre of mass, which for an asymmetric compound has products of
2157    /// inertia; the body stores the **principal** moments, so its local frame is the
2158    /// principal frame, and its initial rotation is the rotation from that frame to
2159    /// the compound's as authored — the compound appears in the world exactly as it
2160    /// was built, translated so its centre of mass is at `position`. The body
2161    /// collides as its children (`body_collider`, internal): a gap between two
2162    /// children is not part of the body, and its collision radius is the sphere about
2163    /// the centre of mass that contains them all.
2164    ///
2165    /// # Errors
2166    ///
2167    /// [`ShapeError`](crate::shape::ShapeError): `NonPositiveDensity`;
2168    /// `DegenerateShape` for an empty compound or one with no volume;
2169    /// `MassNotRepresentable` when the mass or a principal moment is not positive or
2170    /// the sizes are too large for `Fix128`. Nothing is added in those cases.
2171    pub fn add_compound_body(
2172        &mut self,
2173        compound: &crate::compound::CompoundShape,
2174        density: Fix128,
2175        position: Vec3Fix,
2176    ) -> Result<usize, crate::shape::ShapeError> {
2177        use crate::shape::ShapeError;
2178        if density <= Fix128::ZERO {
2179            return Err(ShapeError::NonPositiveDensity);
2180        }
2181        if compound.is_empty() {
2182            return Err(ShapeError::DegenerateShape);
2183        }
2184        let props = compound.mass_properties(density);
2185        if props.mass <= Fix128::ZERO {
2186            return Err(ShapeError::DegenerateShape);
2187        }
2188        let (moments, axes) = crate::mass_properties::principal_axes(props.inertia_tensor);
2189        if !(moments.x > Fix128::ZERO && moments.y > Fix128::ZERO && moments.z > Fix128::ZERO) {
2190            return Err(ShapeError::MassNotRepresentable);
2191        }
2192        // The compound in the principal frame, centre of mass at the origin.
2193        let frame = crate::body_collider::quat_from_rotation(axes);
2194        let to_principal = frame.conjugate();
2195        let mut stored = crate::compound::CompoundShape::new();
2196        for child in &compound.children {
2197            stored.children.push(crate::compound::CompoundChild {
2198                shape: child.shape.clone(),
2199                local_position: to_principal
2200                    .rotate_vec(child.local_position - props.center_of_mass),
2201                local_rotation: to_principal.mul(child.local_rotation),
2202            });
2203        }
2204        let collider = crate::body_collider::BodyCollider::Compound(stored);
2205        let radius = collider.bounding_radius();
2206        // The largest quantity the solve forms, as an `f64`, before it is formed.
2207        let estimate = props.mass.to_f64() * radius.to_f64() * radius.to_f64();
2208        if !(estimate.is_finite() && estimate < (1u64 << 61) as f64) {
2209            return Err(ShapeError::MassNotRepresentable);
2210        }
2211        let mut body = RigidBody::new_dynamic(position, props.mass);
2212        body.rotation = frame;
2213        body.inv_inertia = Vec3Fix::new(
2214            Fix128::ONE / moments.x,
2215            Fix128::ONE / moments.y,
2216            Fix128::ONE / moments.z,
2217        );
2218        let idx = self.add_body_with_radius(body, radius);
2219        self.body_colliders[idx] = Some(collider);
2220        Ok(idx)
2221    }
2222
2223    /// Extend the per-body side tables with the defaults `add_body` gives, up
2224    /// to `bodies.len()` (bodies pushed onto the public field have none).
2225    fn pad_body_tables(&mut self) {
2226        let n = self.bodies.len();
2227        if self.body_materials.len() < n {
2228            self.body_materials
2229                .resize(n, crate::material::DEFAULT_MATERIAL);
2230        }
2231        if self.body_collision_radii.len() < n {
2232            self.body_collision_radii.resize(n, None);
2233        }
2234        if self.body_colliders.len() < n {
2235            self.body_colliders.resize(n, None);
2236        }
2237        if self.body_filters.len() < n {
2238            self.body_filters.resize(n, CollisionFilter::DEFAULT);
2239        }
2240    }
2241
2242    /// [`Self::remove_body`] that also returns the body's per-body settings
2243    /// (material, collision radius, collider, collision filter), for moving it
2244    /// into another world with [`Self::add_body_with_attachments`]. The stable
2245    /// id stays behind: ids belong to a world.
2246    pub(crate) fn remove_body_with_attachments(
2247        &mut self,
2248        idx: usize,
2249    ) -> Option<(RigidBody, BodyAttachments)> {
2250        // a body pushed onto the public `bodies` field may have no entries yet:
2251        // it carries the defaults `add_body` would give it
2252        let attachments = BodyAttachments {
2253            material: self
2254                .body_materials
2255                .get(idx)
2256                .copied()
2257                .unwrap_or(crate::material::DEFAULT_MATERIAL),
2258            collision_radius: self.body_collision_radii.get(idx).copied().flatten(),
2259            collider: self.body_colliders.get(idx).cloned().flatten(),
2260            filter: self.body_filters.get(idx).copied().unwrap_or_default(),
2261        };
2262        let body = self.remove_body(idx)?;
2263        Some((body, attachments))
2264    }
2265
2266    /// [`Self::add_body`] with the per-body settings taken by
2267    /// [`Self::remove_body_with_attachments`]; the body gets a new stable id.
2268    pub(crate) fn add_body_with_attachments(
2269        &mut self,
2270        body: RigidBody,
2271        attachments: BodyAttachments,
2272    ) -> usize {
2273        let idx = self.add_body(body);
2274        self.body_materials[idx] = attachments.material;
2275        self.body_collision_radii[idx] = attachments.collision_radius;
2276        self.body_colliders[idx] = attachments.collider;
2277        self.body_filters[idx] = attachments.filter;
2278        idx
2279    }
2280
2281    /// Remove a body by index (swap-remove).
2282    ///
2283    /// The last body is moved to fill the gap. All constraints and joints
2284    /// referencing the old last index are remapped. Returns the removed body,
2285    /// or `None` if the index is out of bounds.
2286    pub fn remove_body(&mut self, idx: usize) -> Option<RigidBody> {
2287        if idx >= self.bodies.len() {
2288            return None;
2289        }
2290        let last = self.bodies.len() - 1;
2291
2292        // 1. Drop every constraint / joint that references the body being removed.
2293        //    This must happen BEFORE the `last -> idx` remap: after `swap_remove`
2294        //    the moved last body occupies `idx`, so a constraint that still says
2295        //    `idx` would silently re-attach to the wrong body (2026-09-15 bug fix,
2296        //    found by the remove_body mutation tests; v1.1.0 and earlier produced
2297        //    `(idx, idx)` self-constraints for the swapped-in body).
2298        self.distance_constraints
2299            .retain(|c| c.body_a != idx && c.body_b != idx);
2300        self.contact_constraints
2301            .retain(|c| c.body_a != idx && c.body_b != idx);
2302        if !self.joint_motors.is_empty() || !self.joint_motors_3d.is_empty() {
2303            // Motors follow their joint: new index of each kept joint, `None`
2304            // for a joint dropped with the body.
2305            let mut next = 0;
2306            let joint_map: Vec<Option<usize>> = self
2307                .joints
2308                .iter()
2309                .map(|j| {
2310                    let (a, b) = j.bodies();
2311                    if a != idx && b != idx {
2312                        next += 1;
2313                        Some(next - 1)
2314                    } else {
2315                        None
2316                    }
2317                })
2318                .collect();
2319            self.remap_motor_joints(|j| joint_map.get(j).copied().flatten());
2320        }
2321        self.joints.retain(|j| {
2322            let (a, b) = j.bodies();
2323            a != idx && b != idx
2324        });
2325
2326        // Bring the ids level with `bodies` before both lose `idx` together
2327        // (a body pushed onto the public field has no id yet).
2328        #[cfg(feature = "std")]
2329        self.sync_body_stable_ids();
2330        // a body pushed onto the public `bodies` field has no side-table
2331        // entries yet: give it the defaults so the tables swap-remove in step
2332        self.pad_body_tables();
2333        let removed = self.bodies.swap_remove(idx);
2334        self.body_materials.swap_remove(idx);
2335        self.body_collision_radii.swap_remove(idx);
2336        self.body_colliders.swap_remove(idx);
2337        self.body_filters.swap_remove(idx);
2338        #[cfg(feature = "std")]
2339        self.body_stable_ids.swap_remove(idx);
2340
2341        // 2. Remap references from `last` -> `idx` in all remaining constraints and joints
2342        if idx != last {
2343            for c in &mut self.distance_constraints {
2344                if c.body_a == last {
2345                    c.body_a = idx;
2346                }
2347                if c.body_b == last {
2348                    c.body_b = idx;
2349                }
2350            }
2351            for c in &mut self.contact_constraints {
2352                if c.body_a == last {
2353                    c.body_a = idx;
2354                }
2355                if c.body_b == last {
2356                    c.body_b = idx;
2357                }
2358            }
2359            self.remap_joint_indices(last, idx);
2360        }
2361
2362        // Rebuild IslandManager to match new body count and connectivity
2363        let new_len = self.bodies.len();
2364        self.islands = IslandManager::new(new_len, self.islands.config);
2365        for j in &self.joints {
2366            let (a, b) = j.bodies();
2367            if a < new_len && b < new_len {
2368                self.islands.union(a, b);
2369            }
2370        }
2371
2372        self.park_generation = self.park_generation.wrapping_add(1);
2373        self.batches_dirty = true;
2374        Some(removed)
2375    }
2376
2377    /// Remap joint body indices after swap-remove
2378    fn remap_joint_indices(&mut self, from: usize, to: usize) {
2379        /// Remap a single joint's `body_a` and `body_b` fields.
2380        macro_rules! remap {
2381            ($j:expr) => {
2382                if $j.body_a == from {
2383                    $j.body_a = to;
2384                }
2385                if $j.body_b == from {
2386                    $j.body_b = to;
2387                }
2388            };
2389        }
2390        for joint in &mut self.joints {
2391            match joint {
2392                Joint::Ball(j) => {
2393                    remap!(j);
2394                }
2395                Joint::Hinge(j) => {
2396                    remap!(j);
2397                }
2398                Joint::Fixed(j) => {
2399                    remap!(j);
2400                }
2401                Joint::Slider(j) => {
2402                    remap!(j);
2403                }
2404                Joint::Spring(j) => {
2405                    remap!(j);
2406                }
2407                Joint::D6(j) => {
2408                    remap!(j);
2409                }
2410                Joint::ConeTwist(j) => {
2411                    remap!(j);
2412                }
2413            }
2414        }
2415    }
2416
2417    /// Number of bodies in the world
2418    #[inline]
2419    #[must_use]
2420    pub fn body_count(&self) -> usize {
2421        self.bodies.len()
2422    }
2423
2424    /// Number of active (non-sleeping) bodies
2425    #[must_use]
2426    pub fn active_body_count(&self) -> usize {
2427        self.bodies.len() - self.islands.sleeping_count().min(self.bodies.len())
2428    }
2429
2430    /// Set a body's material ID
2431    pub fn set_body_material(&mut self, body_idx: usize, material_id: crate::material::MaterialId) {
2432        if body_idx < self.body_materials.len() {
2433            self.body_materials[body_idx] = material_id;
2434        }
2435    }
2436
2437    /// Add a pre-solve contact hook
2438    ///
2439    /// The hook is called with (`body_a_index`, `body_b_index`, &contact).
2440    /// Return `false` to skip the contact (e.g., for one-way platforms).
2441    #[cfg(feature = "std")]
2442    pub fn add_pre_solve_hook(&mut self, hook: PreSolveHook) {
2443        self.pre_solve_hooks.push(hook);
2444    }
2445
2446    /// Clear all pre-solve hooks
2447    #[cfg(feature = "std")]
2448    pub fn clear_pre_solve_hooks(&mut self) {
2449        self.pre_solve_hooks.clear();
2450    }
2451
2452    /// Add a contact modifier
2453    ///
2454    /// Contact modifiers can mutate contact properties (normal, depth, friction,
2455    /// restitution) before solving. Return false from `modify_contact` to discard.
2456    #[cfg(feature = "std")]
2457    pub fn add_contact_modifier(&mut self, modifier: Box<dyn ContactModifier>) {
2458        self.contact_modifiers.push(modifier);
2459    }
2460
2461    /// Clear all contact modifiers
2462    #[cfg(feature = "std")]
2463    pub fn clear_contact_modifiers(&mut self) {
2464        self.contact_modifiers.clear();
2465    }
2466
2467    /// Begin a new simulation frame (updates contact cache lifecycle)
2468    pub fn begin_frame(&mut self) {
2469        self.contact_cache.begin_frame();
2470    }
2471
2472    /// End a simulation frame (prune old contacts from cache)
2473    pub fn end_frame(&mut self) {
2474        self.contact_cache.end_frame();
2475    }
2476
2477    // ── Joint Management ───────────────────────────────────────────────
2478
2479    /// Add a joint constraint, returns joint index.
2480    ///
2481    /// # Panics
2482    ///
2483    /// Panics if either body index in the joint is out of bounds.
2484    pub fn add_joint(&mut self, joint: Joint) -> usize {
2485        let (a, b) = joint.bodies();
2486        assert!(
2487            a < self.bodies.len() && b < self.bodies.len(),
2488            "Joint body indices ({}, {}) out of bounds (body count = {})",
2489            a,
2490            b,
2491            self.bodies.len()
2492        );
2493        let idx = self.joints.len();
2494        self.islands.resize(self.bodies.len());
2495        self.islands.union(a, b);
2496        self.joints.push(joint);
2497        idx
2498    }
2499
2500    /// Remove a joint by index (swap-remove), returns the removed joint
2501    ///
2502    /// Motors follow their joint: a motor on the removed joint is removed with
2503    /// it, and a motor on the last joint (moved into `idx`) is re-pointed to
2504    /// `idx`.
2505    pub fn remove_joint(&mut self, idx: usize) -> Option<Joint> {
2506        if idx >= self.joints.len() {
2507            return None;
2508        }
2509        let last = self.joints.len() - 1;
2510        self.remap_motor_joints(|j| {
2511            if j == idx {
2512                None
2513            } else if j == last {
2514                Some(idx)
2515            } else {
2516                Some(j)
2517            }
2518        });
2519        Some(self.joints.swap_remove(idx))
2520    }
2521
2522    /// Re-point (or drop, on `None`) every motor's joint index through `map`.
2523    fn remap_motor_joints(&mut self, map: impl Fn(usize) -> Option<usize>) {
2524        self.joint_motors.retain_mut(|m| match map(m.joint_index) {
2525            Some(j) => {
2526                m.joint_index = j;
2527                true
2528            }
2529            None => false,
2530        });
2531        self.joint_motors_3d
2532            .retain_mut(|(joint, _)| match map(*joint) {
2533                Some(j) => {
2534                    *joint = j;
2535                    true
2536                }
2537                None => false,
2538            });
2539    }
2540
2541    // ── Joint motors ──────────────────────────────────────────────────
2542
2543    /// Attach a one-axis PD motor to joint `joint_index`, returns the motor
2544    /// index.
2545    ///
2546    /// The motor is applied by [`Self::step`] every substep (see
2547    /// [`crate::motor::apply_motors`] for the generalised coordinate per joint
2548    /// type: the twist angle for a hinge, the centre distance otherwise). The
2549    /// controller is used as given, so a controller still in
2550    /// [`crate::motor::MotorMode::Off`] does nothing until a target is set
2551    /// ([`Self::set_joint_motor_velocity_target`] / [`Self::joint_motor_mut`]).
2552    /// Motors follow their joint through [`Self::remove_joint`] and
2553    /// [`Self::remove_body`]; a motor whose index no longer names a joint
2554    /// (after editing the public `joints` field directly) does nothing.
2555    pub fn add_joint_motor(
2556        &mut self,
2557        joint_index: usize,
2558        controller: crate::motor::PdController,
2559    ) -> usize {
2560        self.joint_motors
2561            .push(crate::motor::JointMotor::new(joint_index, controller));
2562        self.joint_motors.len() - 1
2563    }
2564
2565    /// The motor added as `motor` by [`Self::add_joint_motor`], for changing its
2566    /// gains or targets between steps.
2567    #[must_use]
2568    pub fn joint_motor_mut(&mut self, motor: usize) -> Option<&mut crate::motor::JointMotor> {
2569        self.joint_motors.get_mut(motor)
2570    }
2571
2572    /// Switch motor `motor` to velocity mode with `target` (rad/s for a hinge,
2573    /// m/s along the centre line otherwise). Returns `false` if there is no
2574    /// such motor.
2575    pub fn set_joint_motor_velocity_target(&mut self, motor: usize, target: Fix128) -> bool {
2576        match self.joint_motors.get_mut(motor) {
2577            Some(m) => {
2578                m.controller.set_velocity_target(target);
2579                true
2580            }
2581            None => false,
2582        }
2583    }
2584
2585    /// Turn motor `motor` off (it applies no force until a target is set
2586    /// again). Returns `false` if there is no such motor.
2587    pub fn disable_joint_motor(&mut self, motor: usize) -> bool {
2588        match self.joint_motors.get_mut(motor) {
2589            Some(m) => {
2590                m.controller.disable();
2591                true
2592            }
2593            None => false,
2594        }
2595    }
2596
2597    /// Attach a three-axis rotation motor with per-axis gains `kp` / `kd` and
2598    /// torque cap `max_torque` to joint `joint_index`, returns its index.
2599    ///
2600    /// The motor starts `Off`; [`Self::set_joint_motor_3d_rotation_target`]
2601    /// drives the joint's relative rotation `rotation_b * rotation_a⁻¹` to a
2602    /// target (see [`crate::motor::PdController3D::compute_torque`]). It
2603    /// follows its joint through removals like [`Self::add_joint_motor`].
2604    pub fn add_joint_motor_3d(
2605        &mut self,
2606        joint_index: usize,
2607        kp: Vec3Fix,
2608        kd: Vec3Fix,
2609        max_torque: Fix128,
2610    ) -> usize {
2611        self.joint_motors_3d.push((
2612            joint_index,
2613            crate::motor::PdController3D::new(kp, kd, max_torque),
2614        ));
2615        self.joint_motors_3d.len() - 1
2616    }
2617
2618    /// The controller of rotation motor `motor` ([`Self::add_joint_motor_3d`]).
2619    #[must_use]
2620    pub fn joint_motor_3d_mut(
2621        &mut self,
2622        motor: usize,
2623    ) -> Option<&mut crate::motor::PdController3D> {
2624        self.joint_motors_3d.get_mut(motor).map(|(_, c)| c)
2625    }
2626
2627    /// Drive rotation motor `motor` to the relative rotation `target`
2628    /// (position mode). Returns `false` if there is no such motor.
2629    pub fn set_joint_motor_3d_rotation_target(&mut self, motor: usize, target: QuatFix) -> bool {
2630        match self.joint_motors_3d.get_mut(motor) {
2631            Some((_, c)) => {
2632                c.set_rotation_target(target);
2633                true
2634            }
2635            None => false,
2636        }
2637    }
2638
2639    /// Apply every joint motor for one (sub)step of length `dt`.
2640    ///
2641    /// A sleeping body a motor moves is woken with its island, otherwise the
2642    /// motor's velocity change would be discarded by the sleep skip in
2643    /// `integrate_positions`. A motor whose output is zero does not wake
2644    /// anything, so a motor resting at its target lets the joint sleep.
2645    fn apply_joint_motors(&mut self, dt: Fix128) {
2646        if self.joint_motors.is_empty() && self.joint_motors_3d.is_empty() {
2647            return;
2648        }
2649        let mut asleep: Vec<(usize, Vec3Fix, Vec3Fix)> = Vec::new();
2650        let n = self.bodies.len();
2651        let pairs = self
2652            .joint_motors
2653            .iter()
2654            .map(|m| m.joint_index)
2655            .chain(self.joint_motors_3d.iter().map(|(j, _)| *j));
2656        for j in pairs {
2657            if let Some(joint) = self.joints.get(j) {
2658                let (a, b) = joint.bodies();
2659                for x in [a, b] {
2660                    if x < n && self.islands.is_sleeping(x) {
2661                        asleep.push((x, self.bodies[x].velocity, self.bodies[x].angular_velocity));
2662                    }
2663                }
2664            }
2665        }
2666        crate::motor::apply_motors(&self.joint_motors, &self.joints, &mut self.bodies, dt);
2667        crate::motor::apply_motors_3d(&self.joint_motors_3d, &self.joints, &mut self.bodies, dt);
2668        for (x, v, w) in asleep {
2669            let body = &self.bodies[x];
2670            if (body.velocity != v || body.angular_velocity != w)
2671                && x < self.islands.sleep_data.len()
2672            {
2673                self.islands.wake_island(x);
2674            }
2675        }
2676    }
2677
2678    /// Number of joints
2679    #[inline]
2680    #[must_use]
2681    pub fn joint_count(&self) -> usize {
2682        self.joints.len()
2683    }
2684
2685    // ── Force Field Management ────────────────────────────────────────
2686
2687    /// Add a force field, returns field index
2688    pub fn add_force_field(&mut self, field: ForceFieldInstance) -> usize {
2689        let idx = self.force_fields.len();
2690        self.force_fields.push(field);
2691        idx
2692    }
2693
2694    /// Remove a force field by index
2695    pub fn remove_force_field(&mut self, idx: usize) -> Option<ForceFieldInstance> {
2696        if idx >= self.force_fields.len() {
2697            return None;
2698        }
2699        Some(self.force_fields.swap_remove(idx))
2700    }
2701
2702    // ── Per-Body Collision Shape / Filter ──────────────────────────────
2703
2704    /// Set collision radius for a body (enables automatic sphere collision detection)
2705    pub fn set_body_collision_radius(&mut self, body_idx: usize, radius: Fix128) {
2706        if body_idx < self.body_collision_radii.len() {
2707            self.park_generation = self.park_generation.wrapping_add(1);
2708            self.body_collision_radii[body_idx] = Some(radius);
2709        }
2710    }
2711
2712    /// Remove collision radius (disable automatic collision detection for this body)
2713    pub fn clear_body_collision_radius(&mut self, body_idx: usize) {
2714        if body_idx < self.body_collision_radii.len() {
2715            self.park_generation = self.park_generation.wrapping_add(1);
2716            self.body_collision_radii[body_idx] = None;
2717        }
2718    }
2719
2720    /// Set collision filter for a body
2721    pub fn set_body_filter(&mut self, body_idx: usize, filter: CollisionFilter) {
2722        if body_idx < self.body_filters.len() {
2723            self.body_filters[body_idx] = filter;
2724        }
2725    }
2726
2727    /// Get collision filter for a body
2728    #[must_use]
2729    pub fn body_filter(&self, body_idx: usize) -> CollisionFilter {
2730        self.body_filters
2731            .get(body_idx)
2732            .copied()
2733            .unwrap_or(CollisionFilter::DEFAULT)
2734    }
2735
2736    // ── Sleeping ──────────────────────────────────────────────────────
2737
2738    /// Check if a body is sleeping
2739    #[inline]
2740    #[must_use]
2741    pub fn is_sleeping(&self, body_idx: usize) -> bool {
2742        self.islands.is_sleeping(body_idx)
2743    }
2744
2745    /// Wake up a body and all connected bodies in its island
2746    pub fn wake_body(&mut self, body_idx: usize) {
2747        // An index past the last body is ignored, like every other per-body
2748        // setter; the island union-find would index out of range otherwise.
2749        if body_idx >= self.bodies.len() {
2750            return;
2751        }
2752        self.islands.wake_island(body_idx);
2753    }
2754
2755    /// Set the sleep configuration
2756    pub fn set_sleep_config(&mut self, config: SleepConfig) {
2757        self.islands.config = config;
2758    }
2759
2760    /// Turn the sleep skip of [`Self::step`] on (the default) or off.
2761    ///
2762    /// With it on, a sleeping body at rest that no joint or distance constraint
2763    /// references is left out of every stage of the step, and its contacts are
2764    /// found through a persistent tree of such bodies instead of rebuilding the
2765    /// broad-phase over it (see [`StageWork`]). The simulation is bit-identical
2766    /// either way: the skip only drops work whose result is already known (a body
2767    /// at rest stays where it is, with the velocity it already has). Turning it off
2768    /// drops the tree; it is rebuilt on the next step with the skip on.
2769    ///
2770    /// Only `step` with the XPBD backend and [`Broadphase::Bvh`] skips; the other
2771    /// entry points and broad-phases ([`Broadphase::DynamicTree`],
2772    /// [`Broadphase::Hybrid`]) visit every body as before.
2773    pub fn set_sleep_skip(&mut self, enabled: bool) {
2774        self.sleep_skip = enabled;
2775        if !enabled {
2776            self.park.clear();
2777        }
2778    }
2779
2780    /// Whether the sleep skip of [`Self::step`] is on (see [`Self::set_sleep_skip`]).
2781    #[must_use]
2782    pub const fn sleep_skip(&self) -> bool {
2783        self.sleep_skip
2784    }
2785
2786    /// Work counters of the last [`Self::step`] (see [`StageWork`]).
2787    #[must_use]
2788    pub const fn stage_work(&self) -> StageWork {
2789        self.stage_work
2790    }
2791
2792    // ── Typed Observation (World Auditor WM-10, Physics 版) ────────────
2793
2794    /// Observe a single body's law-relevant state.
2795    ///
2796    /// Returns `None` if `body_idx` is out of range (there is no body at
2797    /// that index — note that [`Self::remove_body`] uses `swap_remove`, so
2798    /// `body_idx` is only stable within the frame it was observed).
2799    ///
2800    /// ⚠️ **`grounded` は意図的に持たない** — 一般 [`RigidBody`] に地面接触の
2801    /// 定義は存在せず (`character.rs` の `CharacterController::detect_ground`
2802    /// は特化アルゴリズムで、全 body に適用できる基準ではない) Law / goal
2803    /// 述語が「接地」を必要とする場合は `in_contact` + 接触法線
2804    /// ([`EventCollector::contact_events`] の [`crate::event::ContactEvent::normal`]) から
2805    /// 自前で構成する
2806    #[must_use]
2807    pub fn observe_body(&self, body_idx: usize) -> Option<BodyObservation> {
2808        let body = self.bodies.get(body_idx)?;
2809        let in_contact = self
2810            .events
2811            .contact_events()
2812            .iter()
2813            .any(|e| e.body_a == body_idx || e.body_b == body_idx);
2814        Some(BodyObservation {
2815            body_index: body_idx,
2816            position: body.position,
2817            velocity: body.velocity,
2818            rotation: body.rotation,
2819            angular_velocity: body.angular_velocity,
2820            sleeping: self.is_sleeping(body_idx),
2821            in_contact,
2822        })
2823    }
2824
2825    /// Observe every body in the world, in body index order.
2826    #[must_use]
2827    pub fn observe_bodies(&self) -> Vec<BodyObservation> {
2828        (0..self.bodies.len())
2829            .map(|i| {
2830                self.observe_body(i)
2831                    .expect("index came from 0..bodies.len(), always in range")
2832            })
2833            .collect()
2834    }
2835
2836    // ── Event Access ──────────────────────────────────────────────────
2837
2838    /// Get contact events from the last step
2839    #[inline]
2840    #[must_use]
2841    pub fn contact_events(&self) -> &[crate::event::ContactEvent] {
2842        self.events.contact_events()
2843    }
2844
2845    /// Get trigger events from the last step
2846    #[inline]
2847    #[must_use]
2848    pub fn trigger_events(&self) -> &[crate::event::TriggerEvent] {
2849        self.events.trigger_events()
2850    }
2851
2852    /// Drain (consume) contact events from the last step
2853    #[inline]
2854    pub fn drain_contact_events(&mut self) -> Vec<crate::event::ContactEvent> {
2855        self.events.drain_contact_events()
2856    }
2857
2858    /// Drain (consume) trigger events from the last step
2859    #[inline]
2860    pub fn drain_trigger_events(&mut self) -> Vec<crate::event::TriggerEvent> {
2861        self.events.drain_trigger_events()
2862    }
2863
2864    // ── Raycast on World ──────────────────────────────────────────────
2865
2866    /// What a ray query tests for body `i`: its collision radius and the shape or
2867    /// compound it carries, or `None` when the body has no collision radius (it
2868    /// takes part in no collision, so no ray sees it either).
2869    pub(crate) fn ray_geometry(
2870        &self,
2871        i: usize,
2872    ) -> Option<(Fix128, Option<&crate::body_collider::BodyCollider>)> {
2873        let radius = self.body_collision_radii.get(i).copied().flatten()?;
2874        Some((radius, self.body_colliders.get(i).and_then(Option::as_ref)))
2875    }
2876
2877    /// The static colliders, in index order.
2878    pub(crate) fn static_colliders_slice(&self) -> &[crate::static_collider::StaticCollider] {
2879        &self.static_colliders
2880    }
2881
2882    /// The normal force of one contact of the last step (see
2883    /// [`contact_forces`](Self::contact_forces)), or `None` when neither body can move.
2884    pub(crate) fn contact_constraint_force(
2885        &self,
2886        c: &ContactConstraint,
2887        dt: Fix128,
2888    ) -> Option<Fix128> {
2889        let h = self.substep_dt(dt);
2890        self.contact_force(c, h * h).map(|(_, _, force)| force)
2891    }
2892
2893    /// Cast a ray against all body collision spheres, returns (`body_index`, distance).
2894    ///
2895    /// Uses a BVH broad-phase to cull bodies outside the ray's bounding box,
2896    /// then performs exact ray-sphere intersection on candidates.
2897    /// Returns `None` if direction is zero or no body is hit.
2898    ///
2899    /// Every body is tested as its **bounding sphere** (its collision radius), even
2900    /// when it carries a shape or a compound, and static colliders and SDF
2901    /// colliders are not tested. This differs from the contacts, which a shaped
2902    /// body makes with its shape: a ray can hit the bounding sphere of a body
2903    /// that nothing touches there. [`Self::cast_ray`] tests the actual geometry
2904    /// (see [`crate::shape_raycast`]).
2905    #[must_use]
2906    pub fn raycast(
2907        &self,
2908        origin: Vec3Fix,
2909        direction: Vec3Fix,
2910        max_distance: Fix128,
2911    ) -> Option<(usize, Fix128)> {
2912        if direction.length_squared().is_zero() {
2913            return None;
2914        }
2915        let dir_norm = direction.normalize();
2916
2917        // Build BVH from collidable bodies for broad-phase culling
2918        let mut primitives = Vec::new();
2919        for i in 0..self.bodies.len() {
2920            if let Some(radius) = self.body_collision_radii.get(i).and_then(|r| *r) {
2921                let pos = self.bodies[i].position;
2922                let half = Vec3Fix::new(radius, radius, radius);
2923                let aabb = crate::collider::AABB::from_center_half(pos, half);
2924                primitives.push(crate::bvh::BvhPrimitive {
2925                    aabb,
2926                    index: i as u32,
2927                    morton: 0,
2928                });
2929            }
2930        }
2931
2932        if primitives.is_empty() {
2933            return None;
2934        }
2935
2936        // Compute ray AABB (bounding box of the ray segment)
2937        let endpoint = origin + dir_norm * max_distance;
2938        let ray_min = Vec3Fix::new(
2939            if origin.x < endpoint.x {
2940                origin.x
2941            } else {
2942                endpoint.x
2943            },
2944            if origin.y < endpoint.y {
2945                origin.y
2946            } else {
2947                endpoint.y
2948            },
2949            if origin.z < endpoint.z {
2950                origin.z
2951            } else {
2952                endpoint.z
2953            },
2954        );
2955        let ray_max = Vec3Fix::new(
2956            if origin.x > endpoint.x {
2957                origin.x
2958            } else {
2959                endpoint.x
2960            },
2961            if origin.y > endpoint.y {
2962                origin.y
2963            } else {
2964                endpoint.y
2965            },
2966            if origin.z > endpoint.z {
2967                origin.z
2968            } else {
2969                endpoint.z
2970            },
2971        );
2972        let ray_aabb = crate::collider::AABB {
2973            min: ray_min,
2974            max: ray_max,
2975        };
2976
2977        let bvh = crate::bvh::LinearBvh::build(primitives);
2978        let candidates = bvh.query(&ray_aabb);
2979
2980        // Narrow-phase: exact ray-sphere intersection on BVH candidates
2981        let mut best: Option<(usize, Fix128)> = None;
2982        for &prim_idx in &candidates {
2983            let i = prim_idx as usize;
2984            let radius = match self.body_collision_radii.get(i) {
2985                Some(Some(r)) => *r,
2986                _ => continue,
2987            };
2988            let oc = origin - self.bodies[i].position;
2989            let b = oc.dot(dir_norm);
2990            let c = oc.dot(oc) - radius * radius;
2991            let discriminant = b * b - c;
2992            if discriminant < Fix128::ZERO {
2993                continue;
2994            }
2995            let sqrt_d = discriminant.sqrt();
2996            // Try the nearest intersection first
2997            let mut t = -b - sqrt_d;
2998            // If nearest t is behind origin, try the far intersection (ray inside sphere)
2999            if t < Fix128::ZERO {
3000                t = -b + sqrt_d;
3001            }
3002            if t < Fix128::ZERO || t > max_distance {
3003                continue;
3004            }
3005            let dominated = match best {
3006                None => true,
3007                Some((_, prev_t)) => t < prev_t,
3008            };
3009            if dominated {
3010                best = Some((i, t));
3011            }
3012        }
3013        best
3014    }
3015
3016    /// Get combined material properties for a body pair
3017    ///
3018    /// # Claims
3019    ///
3020    /// - A body whose material is [`crate::material::DEFAULT_MATERIAL`] contributes
3021    ///   its own [`RigidBody::friction`] and [`RigidBody::restitution`] (the
3022    ///   default material is "no material assigned"); a body with any other
3023    ///   material contributes that material's `dynamic_friction` / `restitution`
3024    ///   and its own fields are not read.
3025    /// - Bodies built with the defaults (friction 0.3, restitution 0.5) give the
3026    ///   same result as before, since the default material holds the same values.
3027    /// - The two contributions are combined with the materials' combine rules; a
3028    ///   pair override registered for the two materials takes precedence over
3029    ///   both.
3030    /// - An index outside the body list counts as the default material with the
3031    ///   material's own values.
3032    #[must_use]
3033    pub fn combined_material(
3034        &self,
3035        body_a: usize,
3036        body_b: usize,
3037    ) -> crate::material::CombinedMaterial {
3038        let side = |i: usize| -> (crate::material::MaterialId, Option<(Fix128, Fix128)>) {
3039            let Some(&mat) = self.body_materials.get(i) else {
3040                return (crate::material::DEFAULT_MATERIAL, None);
3041            };
3042            if mat != crate::material::DEFAULT_MATERIAL {
3043                return (mat, None);
3044            }
3045            (mat, self.bodies.get(i).map(|b| (b.friction, b.restitution)))
3046        };
3047        let (mat_a, surface_a) = side(body_a);
3048        let (mat_b, surface_b) = side(body_b);
3049        self.material_table
3050            .combine_with_surface(mat_a, surface_a, mat_b, surface_b)
3051    }
3052
3053    /// Add distance constraint
3054    pub fn add_distance_constraint(&mut self, constraint: DistanceConstraint) {
3055        self.distance_constraints.push(constraint);
3056        self.batches_dirty = true;
3057    }
3058
3059    /// Add contact constraint
3060    pub fn add_contact(&mut self, contact: ContactConstraint) {
3061        // Update contact cache for warm starting
3062        let key = crate::contact_cache::BodyPairKey::new(contact.body_a, contact.body_b);
3063        let manifold = self
3064            .contact_cache
3065            .get_or_create(key, contact.friction, contact.restitution);
3066        manifold.add_or_update(
3067            &contact.contact,
3068            contact.contact.point_a,
3069            contact.contact.point_b,
3070        );
3071
3072        self.contact_constraints.push(contact);
3073        self.batches_dirty = true;
3074    }
3075
3076    /// Add contact constraint with material lookup
3077    ///
3078    /// Automatically looks up friction/restitution from the material table.
3079    pub fn add_contact_with_material(&mut self, body_a: usize, body_b: usize, contact: Contact) {
3080        let combined = self.combined_material(body_a, body_b);
3081        let constraint = ContactConstraint {
3082            body_a,
3083            body_b,
3084            contact,
3085            friction: combined.friction,
3086            restitution: combined.restitution,
3087            cached_lambda: Fix128::ZERO,
3088        };
3089        self.add_contact(constraint);
3090    }
3091
3092    /// Clear all contact constraints. `step()` does this at the start of every
3093    /// substep (1.2.0) before `detect_collisions`, so contacts added manually
3094    /// between frames are not seen by `step()`; drive the substep API yourself
3095    /// if you generate contacts outside the built-in sphere / SDF detection.
3096    pub fn clear_contacts(&mut self) {
3097        self.contact_constraints.clear();
3098        #[cfg(feature = "std")]
3099        self.contact_discarded.clear();
3100        self.batches_dirty = true;
3101    }
3102
3103    /// Rebuild constraint batches using greedy graph coloring
3104    ///
3105    /// Each batch contains constraints that share no bodies,
3106    /// allowing safe parallel modification.
3107    pub fn rebuild_batches(&mut self) {
3108        if !self.batches_dirty {
3109            return;
3110        }
3111
3112        self.constraint_batches.clear();
3113
3114        // Snapshot which bodies are static / kinematic. They are never written
3115        // by the pair solvers (every correction is multiplied by
3116        // `inv_mass == 0`), so the parallel path borrows them shared and they
3117        // impose no coloring constraint. Without this exclusion a single
3118        // static floor touched by N bodies would force N sequential batches.
3119        let num_bodies = self.bodies.len();
3120        self.batch_static_bodies.clear();
3121        self.batch_static_bodies
3122            .extend(self.bodies.iter().map(|b| b.inv_mass.is_zero()));
3123
3124        // Track which colors each dynamic body participates in. One growable
3125        // bitset per body (64 colors per word) — no upper bound on colors, so a
3126        // hub body shared by 65+ constraints still gets a unique color per
3127        // constraint instead of aliasing into a single overflow batch.
3128        let mut body_colors: Vec<Vec<u64>> = vec![Vec::new(); num_bodies];
3129
3130        // Color distance constraints
3131        for (constraint_idx, constraint) in self.distance_constraints.iter().enumerate() {
3132            let body_a = constraint.body_a;
3133            let body_b = constraint.body_b;
3134
3135            // Find first color where both bodies are free
3136            let color =
3137                Self::find_free_color(&body_colors, &self.batch_static_bodies, body_a, body_b);
3138
3139            // Ensure we have enough batches
3140            while self.constraint_batches.len() <= color {
3141                self.constraint_batches.push(ConstraintBatch::default());
3142            }
3143
3144            // Add constraint to batch
3145            self.constraint_batches[color]
3146                .distance_indices
3147                .push(constraint_idx);
3148
3149            // Mark bodies as used in this color (set bit)
3150            Self::mark_color(&mut body_colors, &self.batch_static_bodies, body_a, color);
3151            Self::mark_color(&mut body_colors, &self.batch_static_bodies, body_b, color);
3152        }
3153
3154        // Color contact constraints
3155        for (constraint_idx, constraint) in self.contact_constraints.iter().enumerate() {
3156            let body_a = constraint.body_a;
3157            let body_b = constraint.body_b;
3158
3159            let color =
3160                Self::find_free_color(&body_colors, &self.batch_static_bodies, body_a, body_b);
3161
3162            while self.constraint_batches.len() <= color {
3163                self.constraint_batches.push(ConstraintBatch::default());
3164            }
3165
3166            self.constraint_batches[color]
3167                .contact_indices
3168                .push(constraint_idx);
3169
3170            Self::mark_color(&mut body_colors, &self.batch_static_bodies, body_a, color);
3171            Self::mark_color(&mut body_colors, &self.batch_static_bodies, body_b, color);
3172        }
3173
3174        self.batches_dirty = false;
3175
3176        debug_assert!(
3177            self.batches_are_body_disjoint(),
3178            "graph coloring produced a batch with two constraints sharing a dynamic body"
3179        );
3180    }
3181
3182    /// Occupancy word `word` of body `idx`, or 0 if the body is static,
3183    /// out of range, or has no color at that word yet.
3184    #[inline(always)]
3185    fn color_word(
3186        body_colors: &[Vec<u64>],
3187        static_bodies: &[bool],
3188        idx: usize,
3189        word: usize,
3190    ) -> u64 {
3191        if static_bodies.get(idx).copied().unwrap_or(false) {
3192            return 0;
3193        }
3194        body_colors
3195            .get(idx)
3196            .and_then(|words| words.get(word))
3197            .copied()
3198            .unwrap_or(0)
3199    }
3200
3201    /// Find first color where both bodies are free (greedy coloring).
3202    ///
3203    /// Scans the per-body occupancy bitsets word by word (64 colors per
3204    /// word); the first word with a free bit yields the color. If every
3205    /// allocated word is saturated the next fresh word is used, so the
3206    /// number of colors is unbounded and two constraints sharing a dynamic
3207    /// body never land in the same batch.
3208    fn find_free_color(
3209        body_colors: &[Vec<u64>],
3210        static_bodies: &[bool],
3211        body_a: usize,
3212        body_b: usize,
3213    ) -> usize {
3214        let words_a = body_colors.get(body_a).map_or(0, Vec::len);
3215        let words_b = body_colors.get(body_b).map_or(0, Vec::len);
3216        let words = words_a.max(words_b);
3217
3218        for word in 0..words {
3219            let occupied = Self::color_word(body_colors, static_bodies, body_a, word)
3220                | Self::color_word(body_colors, static_bodies, body_b, word);
3221            if occupied != u64::MAX {
3222                return word * 64 + (!occupied).trailing_zeros() as usize;
3223            }
3224        }
3225
3226        words * 64
3227    }
3228
3229    /// Record that dynamic body `idx` participates in `color`.
3230    ///
3231    /// Static bodies and out-of-range indices are ignored (they are not
3232    /// coloring constraints). Grows the body's bitset on demand.
3233    #[inline(always)]
3234    fn mark_color(body_colors: &mut [Vec<u64>], static_bodies: &[bool], idx: usize, color: usize) {
3235        if static_bodies.get(idx).copied().unwrap_or(false) {
3236            return;
3237        }
3238        if let Some(words) = body_colors.get_mut(idx) {
3239            let word = color / 64;
3240            if words.len() <= word {
3241                words.resize(word + 1, 0);
3242            }
3243            words[word] |= 1u64 << (color % 64);
3244        }
3245    }
3246
3247    /// Check the graph coloring invariant: within one batch no two
3248    /// constraints reference the same dynamic body (static bodies as
3249    /// recorded in `batch_static_bodies` may repeat, they are read-only).
3250    ///
3251    /// O(total constraint references) with a per-body scratch marker.
3252    /// Used by `rebuild_batches`' `debug_assert!` and by tests.
3253    pub(crate) fn batches_are_body_disjoint(&self) -> bool {
3254        let num_bodies = self.bodies.len();
3255        // usize::MAX = untouched; otherwise the batch index that last used it.
3256        let mut last_batch: Vec<usize> = vec![usize::MAX; num_bodies];
3257
3258        for (batch_idx, batch) in self.constraint_batches.iter().enumerate() {
3259            let dist_bodies = batch.distance_indices.iter().map(|&i| {
3260                (
3261                    self.distance_constraints[i].body_a,
3262                    self.distance_constraints[i].body_b,
3263                )
3264            });
3265            let contact_bodies = batch.contact_indices.iter().map(|&i| {
3266                (
3267                    self.contact_constraints[i].body_a,
3268                    self.contact_constraints[i].body_b,
3269                )
3270            });
3271
3272            for (a, b) in dist_bodies.chain(contact_bodies) {
3273                for (k, idx) in [a, b].into_iter().enumerate() {
3274                    // A constraint referencing the same body twice counts once.
3275                    if (k == 1 && a == b) || idx >= num_bodies || self.batch_static_bodies[idx] {
3276                        continue;
3277                    }
3278                    if last_batch[idx] == batch_idx {
3279                        return false;
3280                    }
3281                    last_batch[idx] = batch_idx;
3282                }
3283            }
3284        }
3285        true
3286    }
3287
3288    /// Get number of constraint batches (colors used)
3289    #[must_use]
3290    pub fn num_batches(&self) -> usize {
3291        self.constraint_batches.len()
3292    }
3293
3294    /// Step simulation by dt (in fixed-point seconds)
3295    ///
3296    /// Integrated pipeline:
3297    /// 1. Begin event frame
3298    /// 2. Apply force fields to body velocities
3299    /// 3. Detect collisions (BVH broad-phase + sphere narrow-phase)
3300    /// 4. Substep loop (joint motors, integrate, solve constraints + joints,
3301    ///    update velocities)
3302    /// 5. Update sleeping states
3303    /// 6. End event frame (generates end-of-contact events)
3304    ///
3305    /// Deterministic: same inputs always produce same outputs.
3306    ///
3307    /// # Limitation: joints
3308    ///
3309    /// - `step` solves the joints once per substep, after and outside the
3310    ///   `iterations` loop that solves distance and contact constraints.
3311    /// - Joint compliance is therefore exact XPBD with respect to substeps:
3312    ///   each substep's single solve starts from `λ = 0`, so a compliant joint
3313    ///   under a constant load `F` settles at the stretch `F · compliance`
3314    ///   whatever `iterations` is.
3315    /// - Convergence of a rigid joint chain depends on `substeps` only, not on
3316    ///   `iterations`.
3317    ///
3318    /// # Participants
3319    ///
3320    /// `step` is [`Self::try_step`] with the result dropped: while a fault is
3321    /// recorded ([`Self::fault`]) it leaves the world unchanged. Without
3322    /// registered participants no fault is ever recorded and `step` runs as
3323    /// it always has.
3324    pub fn step(&mut self, dt: Fix128) {
3325        let _ = self.try_step(dt);
3326    }
3327
3328    /// Bring every body's rotations to unit length before a step reads them
3329    /// ([`RigidBody::make_rotations_unit`]), and rebuild the cached values of
3330    /// the SDF colliders from their fields ([`SdfCollider::update_cache`]:
3331    /// unit rotation, its conjugate as the inverse, the scale as `f32`).
3332    /// `rotation` is a public field, so a value assigned to it directly
3333    /// between steps reaches the step as it was written; a rotation already of
3334    /// unit length is left bit for bit, and a collider whose cache was already
3335    /// current is left bit for bit. A collider attached to a body is placed
3336    /// again from the body afterwards ([`Self::sync_sdf_colliders`]).
3337    fn make_body_rotations_unit(&mut self) {
3338        for body in &mut self.bodies {
3339            body.make_rotations_unit();
3340        }
3341        for collider in &mut self.sdf_colliders {
3342            collider.update_cache();
3343        }
3344    }
3345
3346    /// The step body behind [`Self::try_step`] (checks already done, `dt > 0`).
3347    fn run_step(&mut self, dt: Fix128) {
3348        self.make_body_rotations_unit();
3349        self.wake_on_sdf_shape_change();
3350        self.stage_work = StageWork::default();
3351        self.begin_sdf_contact_log();
3352        let mut frozen = self.participant_flags();
3353
3354        // `SolverBackend::Tgs` dispatch (std-only, see `SolverBackend` doc for
3355        // the no-std fallback). This `matches!` is `false` for every caller
3356        // that leaves `config.solver_backend` at its `Default` (`Xpbd`), so
3357        // the rest of this function — the pre-1.1 XPBD body — is reached and
3358        // executed exactly as before: no new code runs on the default path.
3359        #[cfg(feature = "std")]
3360        if matches!(self.config.solver_backend, SolverBackend::Tgs) {
3361            self.step_tgs(dt, &mut frozen);
3362            return;
3363        }
3364
3365        // Phase 0: Event frame lifecycle (contacts are cleared and re-detected
3366        // in every substep since 1.2.0, see `substep`)
3367        self.events.begin_frame();
3368
3369        // Phase 0.5: Rebuild island connectivity from current joints
3370        self.islands.resize(self.bodies.len());
3371        self.islands.reset_unions();
3372        for j in &self.joints {
3373            let (a, b) = j.bodies();
3374            if a < self.bodies.len() && b < self.bodies.len() {
3375                self.islands.union(a, b);
3376            }
3377        }
3378
3379        let substep_dt = dt / Fix128::from_int(self.config.substeps as i64);
3380
3381        // Phase 0.75: Park the sleeping bodies at rest (sleep skip). Every
3382        // stage below iterates `park.awake` while `park.active` is set.
3383        self.park_begin(dt, substep_dt);
3384
3385        // Phase 1: Apply force fields
3386        if !self.force_fields.is_empty() {
3387            if self.park.active {
3388                for k in 0..self.park.awake.len() {
3389                    let i = self.park.awake[k];
3390                    let body = &mut self.bodies[i];
3391                    if body.is_static() {
3392                        continue;
3393                    }
3394                    self.stage_work.force_field_bodies += 1;
3395                    body.velocity =
3396                        crate::force::force_field_velocity(&self.force_fields, i, body, dt);
3397                }
3398            } else {
3399                self.stage_work.force_field_bodies += self.bodies.len() as u64;
3400                apply_force_fields(&self.force_fields, &mut self.bodies, dt);
3401            }
3402        }
3403
3404        // Phase 2 (collision detection) moved into the substep — Small Steps
3405        // (Müller et al. 2020): a frame-level contact set solved 8× with a
3406        // stale depth re-pushed the penetration every substep and turned a
3407        // 5 m/s head-on collision into 700 m/s (1.2.0, R4-2).
3408
3409        // Phase 3: Substep loop
3410        let n = self.config.substeps;
3411        for i in 0..n {
3412            self.kinematic_substeps_left = n - i;
3413            self.park.first_substep = i == 0;
3414            self.sdf_contact_substep = i;
3415            // Participants run before the substep body (not inside `substep`,
3416            // which the unit tests call on its own).
3417            let overflow_at_start = self.participants_begin_substep(i, n, substep_dt, &mut frozen);
3418            self.substep(substep_dt);
3419            self.participants_end_substep(overflow_at_start, &frozen);
3420        }
3421        self.kinematic_substeps_left = 0;
3422        self.sdf_contact_substep = 0;
3423        self.park.first_substep = false;
3424
3425        // Phase 3.5: Frame-level damping (1.2.0, was per substep)
3426        self.apply_frame_damping();
3427
3428        // Phase 4: Update sleeping
3429        self.update_sleep_states();
3430        self.park.active = false;
3431
3432        // Phase 4.5: Leave every SDF collider attached to a body at that
3433        // body's final pose, which is what the queries between steps
3434        // (`sdf_contacts`, `sdf_ccd_hits`, the shape casts) read.
3435        self.sync_sdf_colliders();
3436
3437        // Phase 5: End event frame
3438        self.events.end_frame();
3439    }
3440
3441    /// Run [`Self::step`] `n` times with the same `dt`.
3442    ///
3443    /// The native counterpart of the Python `step_n` and WASM `stepN`
3444    /// bindings, which call this: the result is bit-identical to calling
3445    /// `step(dt)` `n` times in a loop, `n = 0` leaves the world unchanged, and
3446    /// a non-positive `dt` leaves it unchanged for every `n` (as `step` does).
3447    ///
3448    /// # Examples
3449    ///
3450    /// ```
3451    /// use alice_physics::{Fix128, PhysicsConfig, PhysicsWorld, RigidBody, Vec3Fix};
3452    ///
3453    /// let mut a = PhysicsWorld::new(PhysicsConfig::default());
3454    /// a.add_body(RigidBody::new_dynamic(Vec3Fix::from_int(0, 5, 0), Fix128::ONE));
3455    /// let mut b = PhysicsWorld::from_world_snapshot(&a.snapshot_world()).unwrap();
3456    ///
3457    /// let dt = Fix128::from_ratio(1, 60);
3458    /// a.step_n(3, dt);
3459    /// for _ in 0..3 {
3460    ///     b.step(dt);
3461    /// }
3462    /// assert_eq!(a.bodies[0].position, b.bodies[0].position);
3463    /// ```
3464    pub fn step_n(&mut self, n: usize, dt: Fix128) {
3465        for _ in 0..n {
3466            self.step(dt);
3467        }
3468    }
3469
3470    /// Phase 4 of [`Self::step`]: [`IslandManager::update_sleep`] over the bodies
3471    /// that are not parked, and for a parked body the same outcome without the
3472    /// evaluation (its velocity is the at-rest one its cache recorded as idle).
3473    fn update_sleep_states(&mut self) {
3474        if !self.park.active {
3475            self.stage_work.sleep_evaluated += self.bodies.len() as u64;
3476            self.islands.update_sleep(&self.bodies);
3477            return;
3478        }
3479        for k in 0..self.park.awake.len() {
3480            let i = self.park.awake[k];
3481            self.stage_work.sleep_evaluated += 1;
3482            self.islands.update_sleep_body(i, &self.bodies[i]);
3483        }
3484        for k in 0..self.park.parked_list.len() {
3485            let i = self.park.parked_list[k];
3486            if !self.park.parked[i] {
3487                continue; // woken during the step: evaluated above
3488            }
3489            self.stage_work.parked_sleep_updates += 1;
3490            self.islands
3491                .update_sleep_idle(i, self.bodies[i].is_static());
3492        }
3493    }
3494
3495    /// Decide which bodies are parked for this step (sleep skip, see
3496    /// [`StageWork`]) and bring the parked-body tree up to date.
3497    ///
3498    /// A body is parked when the step would change nothing about it but its
3499    /// `idle_frames`: it is asleep, not kinematic, not referenced by a joint or a
3500    /// distance constraint, and — for a dynamic body — `prev == position`,
3501    /// `prev_rotation == rotation`, a zero velocity, the angular velocity the
3502    /// derivation gives an unchanged rotation, an idle verdict on that velocity,
3503    /// and no static / SDF collider touching it. A contact with an awake body
3504    /// wakes it during the step like any sleeping body ([`ParkState::unpark`]).
3505    /// Nothing is parked while an SDF collider is attached to a body.
3506    fn park_begin(&mut self, dt: Fix128, substep_dt: Fix128) {
3507        self.park.active = false;
3508        let n = self.bodies.len();
3509        if !self.sleep_skip || self.broadphase != Broadphase::Bvh || n == 0 {
3510            if !self.park.proxies.is_empty() || self.park.key.is_some() {
3511                self.park.clear_counted(&mut self.stage_work);
3512            }
3513            return;
3514        }
3515        // A zero velocity must be idle, otherwise every sleeping body wakes this
3516        // step and there is nothing to skip.
3517        let cfg = self.islands.config;
3518        if !(Vec3Fix::ZERO.length() < cfg.linear_threshold) {
3519            self.park.clear_counted(&mut self.stage_work);
3520            return;
3521        }
3522        // A collider attached to a body moves with it during the step, after
3523        // this check has passed a parked body as clear of it: no parking while
3524        // such a collider is in the world.
3525        if self.sdf_colliders.iter().any(|c| c.body_index < n) {
3526            self.park.clear_counted(&mut self.stage_work);
3527            return;
3528        }
3529
3530        let key = ParkKey {
3531            substep_dt,
3532            sleep: cfg,
3533            generation: self.park_generation,
3534            sdf_radius: self.sdf_collision_radius,
3535            sdf: self
3536                .sdf_colliders
3537                .iter()
3538                .map(|c| {
3539                    (
3540                        c.position,
3541                        c.rotation,
3542                        c.scale,
3543                        c.body_index,
3544                        core::ptr::from_ref::<dyn crate::sdf_collider::SdfField>(c.field.as_ref())
3545                            .cast::<u8>() as usize,
3546                    )
3547                })
3548                .collect(),
3549        };
3550        // Fewer bodies than last time: indices may now name other bodies.
3551        if self.park.key.as_ref() != Some(&key) || n < self.park.parked.len() {
3552            self.park.clear_counted(&mut self.stage_work);
3553            self.park.key = Some(key);
3554        }
3555
3556        self.park.parked.resize(n, false);
3557        self.park.cache.resize(n, None);
3558        self.park.proxies.resize(n, None);
3559        self.park.constrained.clear();
3560        self.park.constrained.resize(n, false);
3561        for j in &self.joints {
3562            let (a, b) = j.bodies();
3563            for x in [a, b] {
3564                if x < n {
3565                    self.park.constrained[x] = true;
3566                }
3567            }
3568        }
3569        for c in &self.distance_constraints {
3570            for x in [c.body_a, c.body_b] {
3571                if x < n {
3572                    self.park.constrained[x] = true;
3573                }
3574            }
3575        }
3576
3577        self.park.frame_dt = dt;
3578        self.park.awake.clear();
3579        self.park.parked_list.clear();
3580        let inv_dt = Fix128::ONE / substep_dt;
3581        for i in 0..n {
3582            self.stage_work.sleep_scanned += 1;
3583            let (ok, recached) = self.park_check(i, inv_dt);
3584            self.park.parked[i] = ok;
3585            let radius = self.body_collision_radii.get(i).and_then(|r| *r);
3586            match (ok, radius) {
3587                (true, Some(r)) => {
3588                    self.park.parked_list.push(i);
3589                    if recached || self.park.proxies[i].is_none() {
3590                        self.park.remove_proxy(i, &mut self.stage_work);
3591                        let aabb = self.broadphase_box(i, r);
3592                        self.park.proxies[i] = Some(self.park.tree.insert(aabb, i as u32));
3593                        self.park.proxy_live += 1;
3594                        self.stage_work.tree_inserts += 1;
3595                    }
3596                }
3597                (true, None) => {
3598                    self.park.parked_list.push(i);
3599                    self.park.remove_proxy(i, &mut self.stage_work);
3600                }
3601                (false, _) => {
3602                    self.park.awake.push(i);
3603                    self.park.remove_proxy(i, &mut self.stage_work);
3604                }
3605            }
3606        }
3607        self.stage_work.parked = self.park.parked_list.len() as u64;
3608        self.park.active = true;
3609    }
3610
3611    /// `(parked, cache rewritten)` for body `i` (see [`Self::park_begin`]).
3612    fn park_check(&mut self, i: usize, inv_dt: Fix128) -> (bool, bool) {
3613        if !self.islands.is_sleeping(i) || self.park.constrained[i] {
3614            return (false, false);
3615        }
3616        let body = &self.bodies[i];
3617        let radius = self.body_collision_radii.get(i).and_then(|r| *r);
3618        match body.body_type {
3619            BodyType::Kinematic => (false, false),
3620            BodyType::Static => {
3621                if !body.is_static() {
3622                    return (false, false);
3623                }
3624                // A collider's box turns with the body; a sphere cube does not.
3625                let has_collider = self.body_colliders.get(i).is_some_and(Option::is_some);
3626                let c = ParkCache {
3627                    position: body.position,
3628                    rotation: body.rotation,
3629                    angular_velocity: Vec3Fix::ZERO,
3630                    radius,
3631                    static_kind: true,
3632                };
3633                match self.park.cache[i] {
3634                    Some(old)
3635                        if old.static_kind
3636                            && old.position == c.position
3637                            && old.radius == radius
3638                            && (old.rotation == c.rotation || !has_collider) =>
3639                    {
3640                        (true, false)
3641                    }
3642                    _ => {
3643                        self.park.cache[i] = Some(c);
3644                        (true, true)
3645                    }
3646                }
3647            }
3648            BodyType::Dynamic => {
3649                if body.prev_position != body.position
3650                    || body.prev_rotation != body.rotation
3651                    || body.velocity != Vec3Fix::ZERO
3652                {
3653                    return (false, false);
3654                }
3655                if let Some(c) = self.park.cache[i] {
3656                    if !c.static_kind
3657                        && c.position == body.position
3658                        && c.rotation == body.rotation
3659                        && c.radius == radius
3660                    {
3661                        return (body.angular_velocity == c.angular_velocity, false);
3662                    }
3663                }
3664                let omega = angular_from_rotations(body.rotation, body.rotation, inv_dt);
3665                if body.angular_velocity != omega
3666                    || !(omega.length() < self.islands.config.angular_threshold)
3667                    || !self.resolution_clear(i)
3668                {
3669                    self.park.cache[i] = None;
3670                    return (false, false);
3671                }
3672                self.park.cache[i] = Some(ParkCache {
3673                    position: body.position,
3674                    rotation: body.rotation,
3675                    angular_velocity: omega,
3676                    radius,
3677                    static_kind: false,
3678                });
3679                (true, true)
3680            }
3681        }
3682    }
3683
3684    /// No static or SDF collider would move body `i` where it is now (the
3685    /// resolution passes test every collider at the same position when none
3686    /// returns a contact).
3687    fn resolution_clear(&self, i: usize) -> bool {
3688        let body = &self.bodies[i];
3689        if body.is_static() || body.is_sensor {
3690            return true; // both resolution passes skip it
3691        }
3692        let radius = self
3693            .body_collision_radii
3694            .get(i)
3695            .and_then(|r| *r)
3696            .unwrap_or(self.sdf_collision_radius);
3697        for collider in &self.static_colliders {
3698            if collider.collide_sphere(body.position, radius).is_some() {
3699                return false;
3700            }
3701        }
3702        #[cfg(feature = "std")]
3703        {
3704            let collider = self.body_colliders.get(i).and_then(Option::as_ref);
3705            for sdf in &self.sdf_colliders {
3706                // Its own field never moves it (`resolve_sdf_collisions`).
3707                if sdf.body_index == i {
3708                    continue;
3709                }
3710                if sdf_contact_of(collider, body, self.sdf_collision_radius, sdf).is_some() {
3711                    return false;
3712                }
3713            }
3714        }
3715        true
3716    }
3717
3718    /// How many bodies a stage visits: the unparked ones while parking is in
3719    /// effect, otherwise all of them (pair with [`Self::stage_body`]).
3720    #[inline]
3721    fn stage_count(&self) -> usize {
3722        if self.park.active {
3723            self.park.awake.len()
3724        } else {
3725            self.bodies.len()
3726        }
3727    }
3728
3729    /// The `k`-th body a stage visits (see [`Self::stage_count`]).
3730    #[inline]
3731    fn stage_body(&self, k: usize) -> usize {
3732        if self.park.active {
3733            self.park.awake[k]
3734        } else {
3735            k
3736        }
3737    }
3738
3739    /// Number of bodies that are not parked (all of them without parking).
3740    #[cfg(feature = "parallel")]
3741    fn unparked_count(&self) -> u64 {
3742        self.stage_count() as u64
3743    }
3744
3745    /// Advances every [`BodyType::Kinematic`] body with a
3746    /// [`RigidBody::kinematic_target`] set, mirroring [`Self::integrate_positions`]'s
3747    /// `Kinematic` branch: velocity is derived from the position delta
3748    /// (`(target - position) / dt`), then position/rotation are snapped to
3749    /// the target. Runs once per full `dt` here (the `Tgs` path detects
3750    /// contacts once per tick, not once per sub-step, so there is no
3751    /// per-sub-step granularity to match) — called before collision
3752    /// detection so a moving kinematic body's contacts use its new
3753    /// position, same ordering as `step`'s per-sub-step kinematic advance
3754    /// relative to its per-sub-step contact handling.
3755    #[cfg(feature = "std")]
3756    fn advance_kinematic_targets_for_tgs(&mut self, dt: Fix128) {
3757        for body in &mut self.bodies {
3758            if body.body_type != BodyType::Kinematic {
3759                continue;
3760            }
3761            body.prev_position = body.position;
3762            body.prev_rotation = body.rotation;
3763            if let Some((target_pos, target_rot)) = body.kinematic_target {
3764                body.velocity = (target_pos - body.position) * (Fix128::ONE / dt);
3765                body.position = target_pos;
3766                body.rotation = target_rot;
3767            }
3768        }
3769    }
3770    /// Static colliders for the TGS path, with their positional correction
3771    /// carried into the velocities (see `step_tgs` Phase 3.2): a static
3772    /// contact pushes the body out and removes the velocity into the surface.
3773    #[cfg(feature = "std")]
3774    fn tgs_static_corrections(&mut self) {
3775        if !self.static_colliders.is_empty() {
3776            self.resolve_static_collisions(true);
3777        }
3778    }
3779
3780    /// The bodies the world's joints refer to, ascending and without
3781    /// repeats (indices past the body list are left out).
3782    #[cfg(feature = "std")]
3783    fn jointed_bodies(&self) -> Vec<usize> {
3784        let n = self.bodies.len();
3785        let mut out: Vec<usize> = self
3786            .joints
3787            .iter()
3788            .flat_map(|j| {
3789                let (a, b) = j.bodies();
3790                [a, b]
3791            })
3792            .filter(|&i| i < n)
3793            .collect();
3794        out.sort_unstable();
3795        out.dedup();
3796        out
3797    }
3798
3799    /// One TGS substep's joint solve: copies the jointed bodies' TGS state to
3800    /// the world bodies, applies the joint projection
3801    /// ([`Self::solve_joints_dispatch`], the call the XPBD substep makes) and
3802    /// writes the result back, adding each correction to the velocity as the
3803    /// XPBD velocity update would: `Δx / h` to the linear velocity and the
3804    /// angular velocity of the rotation change to the angular one. TGS keeps
3805    /// its velocities across substeps, so without the carry a corrected body
3806    /// would keep the velocity that took it off the constraint.
3807    #[cfg(feature = "std")]
3808    fn tgs_project_joints(
3809        &mut self,
3810        tgs_bodies: &mut [crate::solver_tgs_hooks_6dof_oriented::Body6DofOrientedState],
3811        jointed: &[usize],
3812        sub_dt: Fix128,
3813    ) {
3814        use crate::solver_tgs_backend::tgs_to_body;
3815        for &i in jointed {
3816            tgs_to_body(&tgs_bodies[i], &mut self.bodies[i]);
3817        }
3818        let before: Vec<(Vec3Fix, QuatFix)> = jointed
3819            .iter()
3820            .map(|&i| (self.bodies[i].position, self.bodies[i].rotation))
3821            .collect();
3822        self.solve_joints_dispatch(sub_dt);
3823        let inv_dt = Fix128::ONE / sub_dt;
3824        let mut overflow = false;
3825        for (&i, (p0, q0)) in jointed.iter().zip(before) {
3826            let body = &mut self.bodies[i];
3827            if body.is_dynamic() {
3828                if body.position != p0 {
3829                    match (body.position - p0).checked_scale(inv_dt) {
3830                        Some(dv) => body.velocity = body.velocity + dv,
3831                        None => overflow = true,
3832                    }
3833                }
3834                if body.rotation != q0 {
3835                    body.angular_velocity =
3836                        body.angular_velocity + angular_from_rotations(body.rotation, q0, inv_dt);
3837                }
3838            }
3839            let state = &mut tgs_bodies[i];
3840            state.position = [body.position.x, body.position.y, body.position.z];
3841            state.orientation = body.rotation;
3842            state.linear_velocity = [body.velocity.x, body.velocity.y, body.velocity.z];
3843            state.angular_velocity = [
3844                body.angular_velocity.x,
3845                body.angular_velocity.y,
3846                body.angular_velocity.z,
3847            ];
3848        }
3849        if overflow {
3850            self.note_rigid_overflow();
3851        }
3852    }
3853
3854    /// `SolverBackend::Tgs` body of [`Self::step`]. Mirrors `step`'s phase
3855    /// numbering so the two are easy to diff, but is a genuinely different
3856    /// algorithm: contacts are detected once per full `dt` (not re-detected
3857    /// every sub-step — the TGS family owns its own sub-stepping internally
3858    /// via [`crate::solver_tgs::tgs_step`]), and bodies are advanced by
3859    /// per-island impulse-based Gauss-Seidel instead of XPBD position
3860    /// projection. Joints, static colliders, contact filters, kinematic
3861    /// targets and SDF colliders are all handled — see [`SolverBackend`]'s
3862    /// notes for the joint solve and filter handling, Phase 3.2 below for the
3863    /// static colliders, and
3864    /// [`Self::advance_kinematic_targets_for_tgs`] /
3865    /// [`Self::resolve_sdf_collisions`] for the last two.
3866    #[cfg(feature = "std")]
3867    fn step_tgs(&mut self, dt: Fix128, frozen: &mut [bool]) {
3868        use crate::solver_tgs::{build_islands, DistanceRef};
3869        use crate::solver_tgs_backend::{body_to_tgs, contact_to_tgs, tgs_to_body};
3870        use crate::solver_tgs_hooks_6dof_oriented::Pgs6DofOrientedConfig;
3871        use crate::solver_tgs_hooks_6dof_oriented_scoped::solve_oriented_islands_serial;
3872
3873        // Phase 0: Event frame lifecycle (mirrors `step`; TGS detects contacts
3874        // once below instead of once per sub-step).
3875        self.events.begin_frame();
3876
3877        // Phase 0.5: Rebuild island connectivity from current joints (same
3878        // `IslandManager` bookkeeping `step` performs; the TGS islands used
3879        // for solving below are a separate, local structure built from the
3880        // same joint list via `build_islands`).
3881        self.islands.resize(self.bodies.len());
3882        self.islands.reset_unions();
3883        for j in &self.joints {
3884            let (a, b) = j.bodies();
3885            if a < self.bodies.len() && b < self.bodies.len() {
3886                self.islands.union(a, b);
3887            }
3888        }
3889
3890        // Phase 1: Apply force fields (identical call to `step`), then
3891        // advance kinematic targets before contacts are detected below.
3892        if !self.force_fields.is_empty() {
3893            apply_force_fields(&self.force_fields, &mut self.bodies, dt);
3894        }
3895        // Joint motors: once per tick with the full `dt`, like the force
3896        // fields (TGS owns its sub-stepping, see the method doc).
3897        self.apply_joint_motors(dt);
3898        self.advance_kinematic_targets_for_tgs(dt);
3899
3900        // Phase 2: Collision detection once for the whole tick (not
3901        // re-detected per sub-step — see the method doc).
3902        self.clear_contacts();
3903        self.detect_collisions();
3904
3905        // Phase 2.5: Resolve SDF collisions (implicit surface contacts),
3906        // mirroring `substep`'s Phase 1.5. `resolve_sdf_collisions` pushes
3907        // bodies directly out of SDF overlap via `body.position +=
3908        // normal*depth` — it does not go through the contact-constraint /
3909        // impulse pipeline at all, so it is correct regardless of which
3910        // `SolverBackend` subsequently integrates velocities, and is called
3911        // once here (not once per TGS inner sub-step) for the same reason
3912        // `detect_collisions` above is: TGS owns its own sub-stepping
3913        // internally and this path only detects/resolves once per full
3914        // `dt`, same as every other per-tick (not per-substep) phase here.
3915        #[cfg(feature = "std")]
3916        if !self.sdf_colliders.is_empty() {
3917            self.resolve_sdf_collisions();
3918        }
3919
3920        // Phase 2.7: Pre-solve hooks and contact modifiers, once over the
3921        // tick's contact set (the same pre-pass `substep` runs over each
3922        // substep's set). A modifier's result is written back to the
3923        // constraint and read by `contact_to_tgs` below; a vetoed contact is
3924        // left out of the TGS contact list.
3925        self.apply_contact_filters();
3926
3927        // Phase 3: Convert to the TGS body/contact representation, solve
3928        // every island, convert back.
3929        let n = self.bodies.len();
3930        let mut tgs_bodies: Vec<_> = self
3931            .bodies
3932            .iter()
3933            .enumerate()
3934            .map(|(i, b)| body_to_tgs(b, i as u64))
3935            .collect();
3936        // Warm-start keys name a contact / constraint by what it joins, not
3937        // by its position in a vector: the stable ids of its two bodies and
3938        // its ordinal among the entries that join the same pair this tick.
3939        self.sync_body_stable_ids();
3940        let ids = &self.body_stable_ids;
3941        // An out-of-range index (a hand-built constraint) gets a sentinel;
3942        // `build_islands` below rejects it before anything is solved.
3943        let id_of = |i: usize| ids.get(i).copied().unwrap_or(u64::MAX);
3944        let mut contact_ordinals = std::collections::BTreeMap::new();
3945        // Contacts vetoed by a hook or a modifier (`apply_contact_filters`
3946        // above) are not solved, as in the XPBD contact solve. The key is
3947        // taken before the veto is applied, so vetoing one contact of a pair
3948        // does not shift the ordinal (and so the warm-start entry) of the
3949        // pair's other contacts. A modified contact is converted from the
3950        // values the modifier wrote back. (Sensor pairs never reach
3951        // `contact_constraints`: detection reports them as triggers.)
3952        let discarded = &self.contact_discarded;
3953        let mut tgs_contacts: Vec<_> = self
3954            .contact_constraints
3955            .iter()
3956            .enumerate()
3957            .filter_map(|(i, c)| {
3958                // Orient every contact from the lower stable id to the higher
3959                // one. Detection orders a pair by index, and `swap_remove`
3960                // can flip the index order of two bodies whose ids did not
3961                // change; without this the same contact would come back
3962                // reversed (opposite normal and tangent basis) and miss.
3963                let (ia, ib) = (id_of(c.body_a), id_of(c.body_b));
3964                let flipped;
3965                let c = if ia > ib {
3966                    flipped = ContactConstraint {
3967                        body_a: c.body_b,
3968                        body_b: c.body_a,
3969                        contact: Contact {
3970                            depth: c.contact.depth,
3971                            normal: -c.contact.normal,
3972                            point_a: c.contact.point_b,
3973                            point_b: c.contact.point_a,
3974                        },
3975                        ..*c
3976                    };
3977                    &flipped
3978                } else {
3979                    c
3980                };
3981                let key = tgs_cache_key(
3982                    TGS_KEY_CONTACT,
3983                    ia.min(ib),
3984                    ia.max(ib),
3985                    &mut contact_ordinals,
3986                );
3987                if discarded.get(i).copied().unwrap_or(false) {
3988                    return None;
3989                }
3990                Some(contact_to_tgs(c, &self.bodies, key))
3991            })
3992            .collect();
3993        let joint_refs: Vec<DistanceRef<'_>> = self
3994            .distance_constraints
3995            .iter()
3996            .map(|joint| DistanceRef { joint })
3997            .collect();
3998        // Joints share `self.tgs_impulse_cache` with contacts; the kind tag
3999        // in the key keeps the two apart.
4000        let mut joint_ordinals = std::collections::BTreeMap::new();
4001        let mut tgs_joints: Vec<crate::solver_tgs_hooks_6dof_oriented::JointOriented> = self
4002            .distance_constraints
4003            .iter()
4004            .map(|j| crate::solver_tgs_hooks_6dof_oriented::JointOriented {
4005                body_a: j.body_a,
4006                body_b: j.body_b,
4007                stable_id: tgs_cache_key(
4008                    TGS_KEY_DISTANCE,
4009                    id_of(j.body_a),
4010                    id_of(j.body_b),
4011                    &mut joint_ordinals,
4012                ),
4013                local_anchor_a: [j.local_anchor_a.x, j.local_anchor_a.y, j.local_anchor_a.z],
4014                local_anchor_b: [j.local_anchor_b.x, j.local_anchor_b.y, j.local_anchor_b.z],
4015                target_distance: j.target_distance,
4016                accum: Fix128::ZERO,
4017            })
4018            .collect();
4019
4020        // `build_islands` only errs when a contact/joint references a body
4021        // index `>= n`; every index here comes from this same world's own
4022        // `detect_collisions` / `add_distance_constraint`, both of which are
4023        // bounds-checked against `self.bodies.len()` at insertion time, so
4024        // this is unreachable in practice. Treat it as "nothing to solve"
4025        // rather than panicking an FFI host.
4026        let islands = build_islands(&tgs_bodies, &tgs_contacts, &joint_refs).unwrap_or_default();
4027
4028        let cfg = Pgs6DofOrientedConfig {
4029            gravity: [
4030                self.config.gravity.x,
4031                self.config.gravity.y,
4032                self.config.gravity.z,
4033            ],
4034            ..Pgs6DofOrientedConfig::default()
4035        };
4036        let tgs_cfg = crate::solver_tgs::TgsConfig {
4037            substeps: self.config.substeps.max(1) as u32,
4038            velocity_iters: self.config.iterations.max(1) as u32,
4039            ..crate::solver_tgs::TgsConfig::default()
4040        };
4041        // Participants need a substep loop of their own: with none registered
4042        // the joint-free path below is the single call it always was. The
4043        // loop splits `dt` exactly as that call does, so a world whose
4044        // participants stage nothing gives the same bits either way.
4045        let with_participants = !frozen.is_empty();
4046        if self.joints.is_empty() && !with_participants {
4047            solve_oriented_islands_serial(
4048                &mut tgs_bodies,
4049                &mut tgs_contacts,
4050                &mut tgs_joints,
4051                &islands,
4052                &mut self.tgs_impulse_cache,
4053                cfg,
4054                &tgs_cfg,
4055                dt,
4056            );
4057        } else {
4058            // The world's joints (`Joint`: ball, hinge, fixed, slider, spring,
4059            // D6, cone-twist) are solved inside the substep loop: after each
4060            // substep's impulse solve and position advance, the joint
4061            // projection the XPBD substep runs is applied to the jointed
4062            // bodies and carried into their velocities. The loop splits `dt`
4063            // exactly as `tgs_step` does, so each island sees the same
4064            // substep sequence it would in a single call.
4065            let one_substep = crate::solver_tgs::TgsConfig {
4066                substeps: 1,
4067                ..tgs_cfg
4068            };
4069            //
4070            // The width is the one `tgs_step` uses, with or without
4071            // participants, and participants are handed the same `h`
4072            // ([`Self::participant_substep_width`]): a participant that
4073            // stages nothing then leaves the bodies bit for bit as in a world
4074            // without it, for every substep count.
4075            let substeps = self.config.substeps;
4076            let sub_dt = dt * Fix128::from_f32(1.0 / tgs_cfg.substeps as f32);
4077            let jointed = self.jointed_bodies();
4078            for s in 0..tgs_cfg.substeps {
4079                let overflow_at_start = if with_participants {
4080                    // Participants read the bodies as they are at the start of
4081                    // this substep, and their forces change the velocities
4082                    // the TGS solve starts from.
4083                    for (body, state) in self.bodies.iter_mut().zip(tgs_bodies.iter()) {
4084                        tgs_to_body(state, body);
4085                    }
4086                    let flag =
4087                        self.participants_begin_substep(s as usize, substeps, sub_dt, frozen);
4088                    for (state, body) in tgs_bodies.iter_mut().zip(self.bodies.iter()) {
4089                        state.linear_velocity = [body.velocity.x, body.velocity.y, body.velocity.z];
4090                        state.angular_velocity = [
4091                            body.angular_velocity.x,
4092                            body.angular_velocity.y,
4093                            body.angular_velocity.z,
4094                        ];
4095                    }
4096                    flag
4097                } else {
4098                    false
4099                };
4100                solve_oriented_islands_serial(
4101                    &mut tgs_bodies,
4102                    &mut tgs_contacts,
4103                    &mut tgs_joints,
4104                    &islands,
4105                    &mut self.tgs_impulse_cache,
4106                    cfg,
4107                    &one_substep,
4108                    sub_dt,
4109                );
4110                if !self.joints.is_empty() {
4111                    self.tgs_project_joints(&mut tgs_bodies, &jointed, sub_dt);
4112                }
4113                if with_participants {
4114                    // Keep `self.bodies` in step with the TGS state after every
4115                    // substep. The end-of-substep check reads only the
4116                    // overflow flag, not the bodies; the next substep copies
4117                    // the bodies out again before its participants run, and
4118                    // the loop end copies them once more.
4119                    for (body, state) in self.bodies.iter_mut().zip(tgs_bodies.iter()) {
4120                        tgs_to_body(state, body);
4121                    }
4122                    // この substep の範囲外を、参加者が読む前に world の印へ畳む
4123                    if tgs_bodies.iter().any(|s| s.overflow) {
4124                        self.note_rigid_overflow();
4125                    }
4126                    self.participants_end_substep(overflow_at_start, frozen);
4127                }
4128            }
4129        }
4130        // Every contact for this tick has now been visited (each contact's
4131        // warm-start entry was `take`n in `begin_substep` and re-`set` in
4132        // `end_substep` for every sub-step above) — evict any cache entry
4133        // that was not touched this tick, per `ImpulseCache::sweep`'s own
4134        // documented contract. Without this, a contact that stops recurring
4135        // (bodies separate, then the same body pair contacts again later)
4136        // would resurrect the old, stale impulse as a warm-start "hit"
4137        // instead of correctly starting from zero.
4138        self.tgs_impulse_cache.sweep();
4139        debug_assert_eq!(tgs_bodies.len(), n);
4140        for (body, state) in self.bodies.iter_mut().zip(tgs_bodies.iter()) {
4141            tgs_to_body(state, body);
4142        }
4143        // TGS は複製 `tgs_bodies` の上で積分するので、範囲外は複製の各 body の
4144        // `overflow` に溜まる (その body の位置は据え置き、XPBD の積分と同じ扱い)
4145        // 戻す時に world の sticky flag へ畳み込む
4146        if tgs_bodies.iter().any(|s| s.overflow) {
4147            self.note_rigid_overflow();
4148        }
4149
4150        // Phase 3.2: The world's static colliders. A static contact is a
4151        // position-level correction (the call `substep` makes); TGS owns
4152        // velocities, so it also removes the velocity into the surface.
4153        // Without that a body resting on a plane keeps the velocity gravity
4154        // gave it and sinks again next tick. (The joints were solved inside
4155        // the substep loop above.)
4156        self.tgs_static_corrections();
4157
4158        // Phase 3.5: Frame-level damping (identical call to `step`; TGS's own
4159        // per-substep gravity/impulse integration does not apply the global
4160        // `damping` factor, so this still needs to run here).
4161        self.apply_frame_damping();
4162
4163        // Phase 4: Update sleeping (identical call to `step`).
4164        self.islands.update_sleep(&self.bodies);
4165
4166        // Phase 4.5: Leave every SDF collider attached to a body at that
4167        // body's final pose, which is what the queries between steps
4168        // (`sdf_contacts`, `sdf_ccd_hits`, the shape casts) read.
4169        self.sync_sdf_colliders();
4170
4171        // Phase 5: End event frame (identical call to `step`).
4172        self.events.end_frame();
4173    }
4174
4175    /// Warm-start hit-rate diagnostics for the [`SolverBackend::Tgs`] path's
4176    /// per-contact impulse cache (see [`Self::reset_tgs_cache_stats`] to
4177    /// isolate a specific measurement window, e.g. after warm-up frames).
4178    ///
4179    /// Always `hits: 0, misses: 0` while `config.solver_backend` is
4180    /// `Xpbd` — the internal `step_tgs` (the only writer of this cache)
4181    /// is never called on that path.
4182    #[cfg(feature = "std")]
4183    #[must_use]
4184    pub fn tgs_cache_stats(&self) -> TgsCacheStats {
4185        self.tgs_impulse_cache.stats().into()
4186    }
4187
4188    /// Clears the [`Self::tgs_cache_stats`] hit/miss counters without
4189    /// touching the cached impulses themselves — the same contract as the
4190    /// internal `ImpulseCache::reset_stats`. Useful when a host wants to
4191    /// measure hit rate over a specific window instead of the cache's
4192    /// entire lifetime (e.g. after a scene's warm-up frames).
4193    #[cfg(feature = "std")]
4194    pub fn reset_tgs_cache_stats(&mut self) {
4195        self.tgs_impulse_cache.reset_stats();
4196    }
4197
4198    /// Step with batched constraint solving
4199    ///
4200    /// When `parallel` feature is enabled, processes independent constraint
4201    /// batches in parallel using Rayon. Includes the same integrated pipeline
4202    /// as `step()`: force fields, collision detection, joints, events, sleeping.
4203    ///
4204    /// # Determinism contract
4205    ///
4206    /// `step_parallel` is deterministic — the same world stepped twice, on any
4207    /// number of threads and on every platform, produces the same bits (graph
4208    /// colouring is deterministic and constraints inside a batch touch
4209    /// disjoint bodies). It is **not** bit-identical to [`Self::step`] once two
4210    /// constraints share a body: the batches are a different Gauss–Seidel
4211    /// ordering than constraint index order, and Gauss–Seidel is order
4212    /// dependent. Lockstep / rollback peers must therefore all use the same
4213    /// path (`fuzz/fuzz_targets/fuzz_step_parity.rs` checks both properties).
4214    ///
4215    /// # Solver backend
4216    ///
4217    /// `config.solver_backend` is not consulted: this path always runs the
4218    /// XPBD substep loop, so with [`SolverBackend::Tgs`] it gives the same
4219    /// bits as with [`SolverBackend::Xpbd`]. A batched TGS path does not exist
4220    /// yet; use [`Self::step`] for TGS.
4221    ///
4222    /// # Participants
4223    ///
4224    /// `step_parallel` is [`Self::try_step_parallel`] with the result dropped.
4225    #[cfg(feature = "parallel")]
4226    pub fn step_parallel(&mut self, dt: Fix128) {
4227        let _ = self.try_step_parallel(dt);
4228    }
4229
4230    /// The step body behind [`Self::try_step_parallel`] (checks already done,
4231    /// `dt > 0`).
4232    #[cfg(feature = "parallel")]
4233    fn run_step_parallel(&mut self, dt: Fix128) {
4234        self.make_body_rotations_unit();
4235        self.wake_on_sdf_shape_change();
4236        self.begin_sdf_contact_log();
4237        let mut frozen = self.participant_flags();
4238
4239        // Phase 0: Event frame lifecycle (contacts are cleared and re-detected
4240        // in every substep since 1.2.0, see `substep`)
4241        self.events.begin_frame();
4242
4243        // Phase 0.5: Rebuild island connectivity from current joints
4244        self.islands.resize(self.bodies.len());
4245        self.islands.reset_unions();
4246        for j in &self.joints {
4247            let (a, b) = j.bodies();
4248            if a < self.bodies.len() && b < self.bodies.len() {
4249                self.islands.union(a, b);
4250            }
4251        }
4252
4253        // Phase 1: Apply force fields
4254        if !self.force_fields.is_empty() {
4255            apply_force_fields(&self.force_fields, &mut self.bodies, dt);
4256        }
4257
4258        // Phase 2 (collision detection) is per substep since 1.2.0, see `step`.
4259
4260        // Phase 3: Substep loop (batches are rebuilt inside each substep after
4261        // detection, `solve_constraints_batched` re-colours on dirty)
4262        let substep_dt = dt / Fix128::from_int(self.config.substeps as i64);
4263        let n = self.config.substeps;
4264        for i in 0..n {
4265            self.kinematic_substeps_left = n - i;
4266            self.sdf_contact_substep = i;
4267            let overflow_at_start = self.participants_begin_substep(i, n, substep_dt, &mut frozen);
4268            self.substep_batched(substep_dt);
4269            self.participants_end_substep(overflow_at_start, &frozen);
4270        }
4271        self.kinematic_substeps_left = 0;
4272        self.sdf_contact_substep = 0;
4273
4274        // Phase 3.5: Frame-level damping (1.2.0, was per substep)
4275        self.apply_frame_damping();
4276
4277        // Phase 4: Update sleeping
4278        self.islands.update_sleep(&self.bodies);
4279
4280        // Phase 4.5: Leave every SDF collider attached to a body at that
4281        // body's final pose, which is what the queries between steps
4282        // (`sdf_contacts`, `sdf_ccd_hits`, the shape casts) read.
4283        self.sync_sdf_colliders();
4284
4285        // Phase 5: End event frame
4286        self.events.end_frame();
4287    }
4288
4289    /// Single substep
4290    fn substep(&mut self, dt: Fix128) {
4291        // 0. Joint motors act as external forces / torques of this substep.
4292        self.apply_joint_motors(dt);
4293        self.integrate_positions(dt);
4294        self.reset_lambdas();
4295
4296        // 1.1. Continuous collision of fast bodies (`world_ccd`, off by
4297        //      default: nothing runs while it is off).
4298        let ccd_hits = if self.ccd.is_enabled() {
4299            self.ccd_sweep()
4300        } else {
4301            Vec::new()
4302        };
4303
4304        // 1.2. Collision detection on the predicted positions (Small Steps).
4305        //      Contacts live for exactly one substep.
4306        self.clear_contacts();
4307        self.detect_collisions();
4308        if !ccd_hits.is_empty() {
4309            self.ccd_add_contacts(&ccd_hits);
4310        }
4311
4312        // 1.5. Resolve SDF collisions (implicit surface contacts)
4313        #[cfg(feature = "std")]
4314        if !self.sdf_colliders.is_empty() {
4315            self.resolve_sdf_collisions();
4316        }
4317        if !self.static_colliders.is_empty() {
4318            self.resolve_static_collisions(false);
4319        }
4320
4321        // 2. Solve constraints (sequential). Hooks and modifiers run once per
4322        //    substep, ahead of the iteration loop.
4323        #[cfg(feature = "std")]
4324        self.apply_contact_filters();
4325        for _ in 0..self.config.iterations {
4326            self.solve_distance_constraints(dt);
4327            self.solve_contact_constraints(dt);
4328        }
4329
4330        // 3. Solve joint constraints (v0.12.0: auto-routes through
4331        //    installed bridge if any, otherwise falls back to CPU
4332        //    `solve_joints`).
4333        self.solve_joints_dispatch(dt);
4334
4335        self.update_velocities(dt);
4336    }
4337
4338    /// Single substep with batched constraint solving
4339    #[cfg(feature = "parallel")]
4340    fn substep_batched(&mut self, dt: Fix128) {
4341        self.apply_joint_motors(dt);
4342        self.integrate_positions(dt);
4343        self.reset_lambdas();
4344
4345        // 1.1. Continuous collision of fast bodies, as in `substep`.
4346        let ccd_hits = if self.ccd.is_enabled() {
4347            self.ccd_sweep()
4348        } else {
4349            Vec::new()
4350        };
4351
4352        // 1.2. Collision detection on the predicted positions (Small Steps).
4353        self.clear_contacts();
4354        self.detect_collisions();
4355        if !ccd_hits.is_empty() {
4356            self.ccd_add_contacts(&ccd_hits);
4357        }
4358        self.rebuild_batches();
4359
4360        // 1.5. Resolve SDF collisions
4361        #[cfg(feature = "std")]
4362        if !self.sdf_colliders.is_empty() {
4363            self.resolve_sdf_collisions();
4364        }
4365        if !self.static_colliders.is_empty() {
4366            self.resolve_static_collisions(false);
4367        }
4368
4369        // 2. Solve constraints (batched). Hooks and modifiers run once per
4370        //    substep, ahead of the iteration loop, as in `substep`.
4371        #[cfg(feature = "std")]
4372        self.apply_contact_filters();
4373        for _ in 0..self.config.iterations {
4374            self.solve_constraints_batched(dt);
4375        }
4376
4377        // 3. Solve joint constraints (v0.12.0: auto-routes through
4378        //    installed bridge if any, otherwise falls back to CPU
4379        //    `solve_joints`).
4380        self.solve_joints_dispatch(dt);
4381
4382        self.update_velocities(dt);
4383    }
4384
4385    /// Integrate positions (shared between sequential and batched)
4386    ///
4387    /// # Claims
4388    ///
4389    /// - A kinematic body with a `kinematic_target` closes `1 / k` of the
4390    ///   remaining gap to the target in each substep, `k` being the substeps
4391    ///   left in the frame including this one, so it reaches the target
4392    ///   exactly in the last substep.
4393    /// - The velocity derived after every substep of that frame equals
4394    ///   `(target - start) / frame_dt` in m/s, so it is that value after
4395    ///   `step`, up to fixed-point rounding of the division by `k`.
4396    /// - A call outside a `step` substep loop (`k = 0`) snaps to the target
4397    ///   in one call, as before.
4398    /// - A kinematic body without a target keeps its pose and velocity.
4399    ///
4400    /// Sleeping dynamic bodies are skipped: only `prev_position/prev_rotation`
4401    /// are saved so that `update_velocities` derives zero velocity.
4402    #[inline]
4403    fn integrate_positions(&mut self, dt: Fix128) {
4404        // ⚠️ 範囲外の積を集める受け皿 — parallel branch の closure から
4405        // `self` に書けないので Atomic で受けて関数末尾で flag に畳み込む
4406        // (bool の OR は結合的・可換なので rayon の実行順に依存しない)
4407        let overflow = core::sync::atomic::AtomicBool::new(false);
4408        // Free rotation of this substep per body (`None`: no gyroscopic
4409        // response, the rotation is predicted from `ω` as before); read back
4410        // by `update_velocities` in the same substep.
4411        let mut free = core::mem::take(&mut self.free_rotation);
4412        free.clear();
4413        free.resize(self.bodies.len(), None);
4414        let mut predicted = core::mem::take(&mut self.predicted_pose);
4415        predicted.clear();
4416        predicted.resize(self.bodies.len(), None);
4417        #[cfg(feature = "parallel")]
4418        {
4419            let gravity = self.config.gravity;
4420            let left = self.kinematic_substeps_left;
4421            let sleep_data = &self.islands.sleep_data;
4422            let parked: &[bool] = if self.park.active {
4423                &self.park.parked
4424            } else {
4425                &[]
4426            };
4427            self.stage_work.integrated += self.unparked_count();
4428            self.bodies
4429                .par_iter_mut()
4430                .zip(free.par_iter_mut())
4431                .zip(predicted.par_iter_mut())
4432                .enumerate()
4433                .for_each(|(i, ((body, free), predicted))| {
4434                    if parked.get(i).copied().unwrap_or(false) {
4435                        return;
4436                    }
4437                    match body.body_type {
4438                        BodyType::Static => return,
4439                        BodyType::Kinematic => {
4440                            body.prev_position = body.position;
4441                            body.prev_rotation = body.rotation;
4442                            if let Some((target_pos, target_rot)) = body.kinematic_target {
4443                                let (p, r) = kinematic_substep_pose(
4444                                    body.position,
4445                                    body.rotation,
4446                                    target_pos,
4447                                    target_rot,
4448                                    left,
4449                                );
4450                                body.velocity = (p - body.position) * (Fix128::ONE / dt);
4451                                body.position = p;
4452                                body.rotation = r;
4453                            }
4454                            return;
4455                        }
4456                        BodyType::Dynamic => {}
4457                    }
4458
4459                    // Skip sleeping bodies (preserve prev for zero-velocity derivation)
4460                    if sleep_data
4461                        .get(i)
4462                        .is_some_and(super::sleeping::SleepData::is_sleeping)
4463                    {
4464                        body.prev_position = body.position;
4465                        body.prev_rotation = body.rotation;
4466                        return;
4467                    }
4468
4469                    // Store previous state
4470                    body.prev_position = body.position;
4471                    body.prev_rotation = body.rotation;
4472
4473                    // Apply gravity (with per-body scale)
4474                    body.velocity = body.velocity + gravity * body.gravity_scale * dt;
4475
4476                    // Damping is applied once per frame in `apply_frame_damping`
4477                    // (1.2.0). Applying it here, per substep, made the terminal
4478                    // velocity depend on `substeps` (v_inf = g*h*d/(1-d)).
4479
4480                    // Predict position
4481                    // ⚠️ parallel branch でも同じ検出を行う — 片方だけだと
4482                    // `--features parallel` で guard が silent に消える
4483                    // (closure から `self` に書けないので bool を reduce する)
4484                    // 位置の加算も検査する (`|x| ≥ 2⁶³` で反対側の端へ wrap するため)
4485                    match body
4486                        .velocity
4487                        .checked_scale(dt)
4488                        .and_then(|d| body.position.checked_add(d))
4489                    {
4490                        Some(p) => body.position = p,
4491                        None => overflow.store(true, core::sync::atomic::Ordering::Relaxed),
4492                    }
4493
4494                    // Predict rotation, with the gyroscopic term ω × Iω
4495                    let mut spin_overflow = false;
4496                    *free = predict_rotation(body, dt, &mut spin_overflow);
4497                    if spin_overflow {
4498                        overflow.store(true, core::sync::atomic::Ordering::Relaxed);
4499                    }
4500
4501                    // The predicted pose, against which `update_velocities`
4502                    // measures the solve's correction
4503                    *predicted = Some((body.position, body.rotation));
4504                });
4505            self.free_rotation = free;
4506            self.predicted_pose = predicted;
4507        }
4508
4509        #[cfg(not(feature = "parallel"))]
4510        {
4511            let count = self.stage_count();
4512            for k in 0..count {
4513                let i = self.stage_body(k);
4514                self.stage_work.integrated += 1;
4515                match self.bodies[i].body_type {
4516                    BodyType::Static => continue,
4517                    BodyType::Kinematic => {
4518                        self.bodies[i].prev_position = self.bodies[i].position;
4519                        self.bodies[i].prev_rotation = self.bodies[i].rotation;
4520                        if let Some((target_pos, target_rot)) = self.bodies[i].kinematic_target {
4521                            let (p, r) = kinematic_substep_pose(
4522                                self.bodies[i].position,
4523                                self.bodies[i].rotation,
4524                                target_pos,
4525                                target_rot,
4526                                self.kinematic_substeps_left,
4527                            );
4528                            self.bodies[i].velocity =
4529                                (p - self.bodies[i].position) * (Fix128::ONE / dt);
4530                            self.bodies[i].position = p;
4531                            self.bodies[i].rotation = r;
4532                        }
4533                        continue;
4534                    }
4535                    BodyType::Dynamic => {}
4536                }
4537
4538                // Skip sleeping bodies (preserve prev for zero-velocity derivation)
4539                if self.islands.is_sleeping(i) {
4540                    self.bodies[i].prev_position = self.bodies[i].position;
4541                    self.bodies[i].prev_rotation = self.bodies[i].rotation;
4542                    continue;
4543                }
4544
4545                // Store previous state
4546                self.bodies[i].prev_position = self.bodies[i].position;
4547                self.bodies[i].prev_rotation = self.bodies[i].rotation;
4548
4549                // Apply gravity (with per-body scale)
4550                let grav = self.config.gravity * self.bodies[i].gravity_scale * dt;
4551                self.bodies[i].velocity = self.bodies[i].velocity + grav;
4552
4553                // Damping: once per frame in `apply_frame_damping` (see the
4554                // `parallel` branch above for the rationale).
4555
4556                // Predict position
4557                // ⚠️ `checked_scale` で範囲外を検出する (WM-01 / B-12)
4558                // 範囲外なら **位置を動かさず** flag を立てる — 0 加算されて
4559                // 「原点で静止した自己整合な状態」に落ちるのを表に出すため
4560                // 位置の加算も検査する (`|x| ≥ 2⁶³` で反対側の端へ wrap するため)
4561                let body = &mut self.bodies[i];
4562                match body
4563                    .velocity
4564                    .checked_scale(dt)
4565                    .and_then(|d| body.position.checked_add(d))
4566                {
4567                    Some(p) => body.position = p,
4568                    None => overflow.store(true, core::sync::atomic::Ordering::Relaxed),
4569                }
4570
4571                // Predict rotation, with the gyroscopic term ω × Iω
4572                let mut spin_overflow = false;
4573                free[i] = predict_rotation(&mut self.bodies[i], dt, &mut spin_overflow);
4574                if spin_overflow {
4575                    overflow.store(true, core::sync::atomic::Ordering::Relaxed);
4576                }
4577
4578                // The predicted pose, against which `update_velocities`
4579                // measures the solve's correction
4580                predicted[i] = Some((self.bodies[i].position, self.bodies[i].rotation));
4581            }
4582            self.free_rotation = free;
4583            self.predicted_pose = predicted;
4584        }
4585
4586        // ⚠️ 受け皿を sticky flag に畳み込む (1 度立ったら落ちない)
4587        if overflow.load(core::sync::atomic::Ordering::Relaxed) {
4588            self.note_rigid_overflow();
4589        }
4590    }
4591
4592    /// Reset the XPBD Lagrange multipliers of the distance constraints at the
4593    /// start of a substep (Macklin 2016, Algorithm 1: `lambda = 0`).
4594    ///
4595    /// Within the substep `solve_distance_constraints` / `solve_distance_pair`
4596    /// accumulate `dlambda` into `cached_lambda`. Carrying a fraction of the
4597    /// previous substep's `lambda` over (the pre-1.2.0 "warm start") is not
4598    /// valid for a position-based multiplier: the carried value is treated as
4599    /// force already applied this substep although it was not, which inflates
4600    /// the steady-state extension of compliant constraints
4601    /// (`C = mg/k * (1 + wsf / (1 - wsf))`, 6.7x at `wsf = 0.85`). For rigid
4602    /// constraints (`compliance == 0`) `lambda` never enters the correction, so
4603    /// their behaviour is unchanged. `warm_start_factor` now only affects the
4604    /// contact solver (byte-exact parity contract with `GpuSolverBridge`
4605    /// implementations, see `solve_contact_pair`).
4606    fn reset_lambdas(&mut self) {
4607        for c in &mut self.distance_constraints {
4608            c.cached_lambda = Fix128::ZERO;
4609        }
4610    }
4611
4612    /// Apply global and per-body velocity damping **once per frame**.
4613    ///
4614    /// `SolverConfig::damping` / `RigidBody::linear_damping` /
4615    /// `RigidBody::angular_damping` are velocity-retention factors per
4616    /// `step()` call. Before 1.2.0 they were multiplied in every substep, so
4617    /// the default `substeps: 8, damping: 0.99` gave every body a terminal
4618    /// velocity of `g * h * d / (1 - d)` with `h = dt / substeps` — about
4619    /// 2 m/s under default gravity — and doubling `substeps` halved the fall
4620    /// speed. A precision parameter must not change the physics, so damping
4621    /// now runs after the substep loop, on the velocities derived by the last
4622    /// `update_velocities`. Static / kinematic / sleeping bodies are skipped.
4623    fn apply_frame_damping(&mut self) {
4624        let damping = self.config.damping;
4625        let count = self.stage_count();
4626        for k in 0..count {
4627            let i = self.stage_body(k);
4628            self.stage_work.damping_bodies += 1;
4629            if self.bodies[i].body_type != BodyType::Dynamic || self.islands.is_sleeping(i) {
4630                continue;
4631            }
4632            let body = &mut self.bodies[i];
4633            body.velocity = body.velocity * damping * body.linear_damping;
4634            body.angular_velocity = body.angular_velocity * damping * body.angular_damping;
4635        }
4636    }
4637
4638    /// Update velocities from position changes, then apply restitution and friction
4639    /// for active contact constraints.
4640    #[inline]
4641    fn update_velocities(&mut self, dt: Fix128) {
4642        let inv_dt = Fix128::ONE / dt;
4643        // ⚠️ 範囲外の積を集める受け皿 — parallel branch の closure から
4644        // `self` に書けないので Atomic で受けて関数末尾で flag に畳み込む
4645        // (bool の OR は結合的・可換なので rayon の実行順に依存しない)
4646        let overflow = core::sync::atomic::AtomicBool::new(false);
4647
4648        // --- Phase 0: Record the pre-solve normal velocity v̄_n per contact ---
4649        // `velocity` still holds the substep's predicted velocity (gravity
4650        // applied in `integrate_positions`, untouched by the position solve),
4651        // so this is the approach velocity before any constraint acted.
4652        self.contact_pre_vn.clear();
4653        for constraint in &self.contact_constraints {
4654            let relative =
4655                self.bodies[constraint.body_a].velocity - self.bodies[constraint.body_b].velocity;
4656            self.contact_pre_vn
4657                .push(relative.dot(constraint.contact.normal));
4658        }
4659
4660        // --- Phase 1: Derive velocities from position/rotation changes ---
4661        #[cfg(feature = "parallel")]
4662        {
4663            let parked: &[bool] = if self.park.active {
4664                &self.park.parked
4665            } else {
4666                &[]
4667            };
4668            self.stage_work.velocity_bodies += self.unparked_count();
4669            let free: &[Option<crate::gyroscopic::FreeRotation>] = &self.free_rotation;
4670            let predicted: &[Option<(Vec3Fix, QuatFix)>] = &self.predicted_pose;
4671            self.bodies
4672                .par_iter_mut()
4673                .enumerate()
4674                .for_each(|(i, body)| {
4675                    if body.body_type == BodyType::Static || parked.get(i).copied().unwrap_or(false)
4676                    {
4677                        return;
4678                    }
4679                    // ⚠️ 速度導出も範囲外を検出する (WM-01 経路 2)
4680                    // 範囲外なら速度を更新しない (0 に上書きされるのを防ぐ)
4681                    let pose = predicted.get(i).copied().flatten();
4682                    if !derive_linear_velocity(body, pose.map(|(x, _)| x), inv_dt) {
4683                        overflow.store(true, core::sync::atomic::Ordering::Relaxed);
4684                    }
4685                    // Angular velocity from rotation change:
4686                    // delta_q = rotation * prev_rotation^-1
4687                    // angular_velocity = 2 * delta_q.xyz / dt  (when delta_q.w > 0)
4688                    // (split bodies: the split's ω plus the solve's change)
4689                    let f = free.get(i).copied().flatten();
4690                    body.angular_velocity =
4691                        derived_angular_velocity(body, f, pose.map(|(_, q)| q), inv_dt);
4692                });
4693        }
4694
4695        #[cfg(not(feature = "parallel"))]
4696        {
4697            let count = self.stage_count();
4698            for k in 0..count {
4699                let i = self.stage_body(k);
4700                self.stage_work.velocity_bodies += 1;
4701                let body = &mut self.bodies[i];
4702                if body.body_type == BodyType::Static {
4703                    continue;
4704                }
4705                // ⚠️ 速度導出も範囲外を検出する (WM-01 経路 2)
4706                // 範囲外なら速度を更新しない (0 に上書きされるのを防ぐ)
4707                let pose = self.predicted_pose.get(i).copied().flatten();
4708                if !derive_linear_velocity(body, pose.map(|(x, _)| x), inv_dt) {
4709                    overflow.store(true, core::sync::atomic::Ordering::Relaxed);
4710                }
4711                // Angular velocity from rotation change (split bodies: the
4712                // split's ω plus the solve's change)
4713                let f = self.free_rotation.get(i).copied().flatten();
4714                let body = &mut self.bodies[i];
4715                body.angular_velocity =
4716                    derived_angular_velocity(body, f, pose.map(|(_, q)| q), inv_dt);
4717            }
4718        }
4719
4720        // --- Phase 2: Apply restitution and friction at contacts ---
4721        // Restitution threshold of Müller et al. 2020 (below): an approach
4722        // slower than `2 |g| h` is a resting contact and gets `e = 0`, so
4723        // resting bodies do not bounce on the gravity of one substep
4724        // (the same rule as the 2D solver).
4725        let restitution_threshold = self.config.gravity.length() * dt * Fix128::from_int(2);
4726        let num_contacts = self.contact_constraints.len();
4727        for i in 0..num_contacts {
4728            let constraint = self.contact_constraints[i];
4729            let body_a = self.bodies[constraint.body_a];
4730            let body_b = self.bodies[constraint.body_b];
4731
4732            if body_a.is_sensor || body_b.is_sensor {
4733                continue;
4734            }
4735
4736            // A contact the substep's hook / modifier pre-pass discarded gets
4737            // no velocity response either (the position pass skipped it).
4738            // Kept contacts read the friction / restitution the pre-pass
4739            // wrote back, so a modifier's values apply here too.
4740            #[cfg(feature = "std")]
4741            if self.contact_discarded.get(i).copied().unwrap_or(false) {
4742                continue;
4743            }
4744
4745            let w_sum = body_a.inv_mass + body_b.inv_mass;
4746            if w_sum < W_SUM_EPSILON {
4747                continue;
4748            }
4749
4750            let n = constraint.contact.normal;
4751            let relative_vel = body_a.velocity - body_b.velocity;
4752            let vn = relative_vel.dot(n);
4753
4754            // Restitution (Müller, Macklin, Chentanez, Jeschke, Kim,
4755            // "Detailed Rigid Body Simulation with Extended Position Based
4756            // Dynamics", SCA 2020, eq. (34)): the target normal velocity is
4757            // `max(−e v̄_n, 0)` with `v̄_n` the pre-solve normal velocity. The
4758            // post-solve `vn` only reflects how far into the substep the
4759            // bodies first touched (`vn = −gap / h`), so reversing `vn`
4760            // itself made the bounce depend on that phase. The correction is
4761            // one-sided (only raises `vn` to the target), as in the 2D solver.
4762            let pre_vn = self.contact_pre_vn.get(i).copied().unwrap_or(vn);
4763            let restitution = if pre_vn < -restitution_threshold {
4764                constraint.restitution
4765            } else {
4766                Fix128::ZERO
4767            };
4768            let target = -restitution * pre_vn;
4769            let target = if target.is_negative() {
4770                Fix128::ZERO
4771            } else {
4772                target
4773            };
4774            if vn < target {
4775                let delta_vn = target - vn;
4776                let impulse_n = n * delta_vn;
4777                let inv_w = Fix128::ONE / w_sum;
4778                self.bodies[constraint.body_a].velocity =
4779                    self.bodies[constraint.body_a].velocity + impulse_n * (body_a.inv_mass * inv_w);
4780                self.bodies[constraint.body_b].velocity =
4781                    self.bodies[constraint.body_b].velocity - impulse_n * (body_b.inv_mass * inv_w);
4782            }
4783
4784            // Friction: reduce tangential velocity
4785            let relative_vel2 =
4786                self.bodies[constraint.body_a].velocity - self.bodies[constraint.body_b].velocity;
4787            let vn2 = relative_vel2.dot(n);
4788            let tangent_vel = relative_vel2 - n * vn2;
4789            let tangent_speed_sq = tangent_vel.length_squared();
4790            if tangent_speed_sq > Fix128::ZERO {
4791                let tangent_speed = tangent_speed_sq.sqrt();
4792                let friction = constraint.friction;
4793                // Coulomb friction (Müller, Macklin, Chentanez, Jeschke, Kim,
4794                // "Detailed Rigid Body Simulation with Extended Position Based
4795                // Dynamics", SCA 2020, eq. (30)): the tangential correction
4796                // is capped by `h μ |f_n| w = μ λ_n / h` in relative velocity.
4797                // `cached_lambda` is this substep's accumulated normal
4798                // separation from the position solve (the paper's multiplier
4799                // times `w_sum`), so the cap is `μ λ / h`. The normal
4800                // velocity after the solve (`vn2`) is not the normal force:
4801                // it is about 0 for a resting contact, which removed the
4802                // friction of sliding and resting bodies.
4803                let max_friction_impulse = friction * constraint.cached_lambda * inv_dt;
4804                let applied = if tangent_speed < max_friction_impulse {
4805                    tangent_speed
4806                } else {
4807                    max_friction_impulse
4808                };
4809                let friction_dir = tangent_vel / tangent_speed;
4810                let friction_impulse = friction_dir * applied;
4811                let inv_w = Fix128::ONE / w_sum;
4812                self.bodies[constraint.body_a].velocity = self.bodies[constraint.body_a].velocity
4813                    - friction_impulse * (body_a.inv_mass * inv_w);
4814                self.bodies[constraint.body_b].velocity = self.bodies[constraint.body_b].velocity
4815                    + friction_impulse * (body_b.inv_mass * inv_w);
4816            }
4817        }
4818
4819        // ⚠️ 受け皿を sticky flag に畳み込む (1 度立ったら落ちない)
4820        if overflow.load(core::sync::atomic::Ordering::Relaxed) {
4821            self.note_rigid_overflow();
4822        }
4823    }
4824
4825    /// Run the pre-solve hooks and contact modifiers once over every
4826    /// contact constraint and keep the result.
4827    ///
4828    /// The serial, bridge and batched (`step_parallel`) substeps call this
4829    /// ahead of their iteration loop, so a relative change such as
4830    /// `friction *= 0.5` applies once per substep and not once per
4831    /// iteration, and every path calls each hook the same number of times.
4832    ///
4833    /// # Claims
4834    ///
4835    /// - Each hook and each modifier runs at most once per contact per call.
4836    /// - Sensor contacts are skipped without calling hooks or modifiers.
4837    /// - A modifier's result (contact, friction, restitution) is written back
4838    ///   to the constraint, and the solver passes read that value without
4839    ///   calling the modifier again.
4840    /// - A contact vetoed by a hook or discarded by a modifier is left
4841    ///   unmodified and is skipped by the solver passes (position and
4842    ///   velocity) until the next call.
4843    /// - With no hooks and no modifiers the call changes nothing.
4844    #[cfg(feature = "std")]
4845    fn apply_contact_filters(&mut self) {
4846        self.contact_discarded.clear();
4847        if self.pre_solve_hooks.is_empty() && self.contact_modifiers.is_empty() {
4848            return;
4849        }
4850        let num = self.contact_constraints.len();
4851        self.contact_discarded.resize(num, false);
4852        for i in 0..num {
4853            let constraint = self.contact_constraints[i];
4854            if self.bodies[constraint.body_a].is_sensor || self.bodies[constraint.body_b].is_sensor
4855            {
4856                continue;
4857            }
4858
4859            let mut contact = constraint.contact;
4860            let mut friction = constraint.friction;
4861            let mut restitution = constraint.restitution;
4862
4863            let mut skip = false;
4864            for hook in &self.pre_solve_hooks {
4865                if !hook(constraint.body_a, constraint.body_b, &contact) {
4866                    skip = true;
4867                    break;
4868                }
4869            }
4870            if !skip {
4871                for modifier in &self.contact_modifiers {
4872                    if !modifier.modify_contact(
4873                        constraint.body_a,
4874                        constraint.body_b,
4875                        &mut contact,
4876                        &mut friction,
4877                        &mut restitution,
4878                    ) {
4879                        skip = true;
4880                        break;
4881                    }
4882                }
4883            }
4884            if skip {
4885                self.contact_discarded[i] = true;
4886                continue;
4887            }
4888
4889            let slot = &mut self.contact_constraints[i];
4890            slot.contact = contact;
4891            slot.friction = friction;
4892            slot.restitution = restitution;
4893        }
4894    }
4895
4896    /// Solve constraints in batched parallel mode via Rayon.
4897    ///
4898    /// Within each colored batch, constraints share no body indices
4899    /// (guaranteed by `rebuild_batches` graph coloring), so they are
4900    /// dispatched to Rayon's thread pool for parallel execution.
4901    /// Between batches, a synchronization barrier ensures that
4902    /// earlier batch results are visible to later batches.
4903    ///
4904    /// Pre-solve hooks and contact modifiers are not called here: the
4905    /// substep runs [`Self::apply_contact_filters`] once before its
4906    /// iterations, and this pass skips the contacts it discarded and reads
4907    /// the values it wrote back.
4908    #[cfg(feature = "parallel")]
4909    fn solve_constraints_batched(&mut self, dt: Fix128) {
4910        // The coloring excluded bodies that were static when `rebuild_batches`
4911        // ran. If any body changed static-ness since (e.g. the user edited
4912        // `inv_mass` through `bodies` after the rebuild) the snapshot no longer
4913        // matches the live world and the batches must be recomputed, otherwise
4914        // two threads could take `&mut` to a body that is now dynamic.
4915        let snapshot_stale = self.batch_static_bodies.len() != self.bodies.len()
4916            || self
4917                .bodies
4918                .iter()
4919                .zip(&self.batch_static_bodies)
4920                .any(|(body, &was_static)| body.inv_mass.is_zero() != was_static);
4921        if snapshot_stale {
4922            self.batches_dirty = true;
4923            self.rebuild_batches();
4924        }
4925
4926        let num_batches = self.constraint_batches.len();
4927
4928        let bodies = BodySlicePtr {
4929            ptr: self.bodies.as_mut_ptr(),
4930            len: self.bodies.len(),
4931        };
4932        let dists = DistConstraintSlicePtr {
4933            ptr: self.distance_constraints.as_mut_ptr(),
4934            len: self.distance_constraints.len(),
4935        };
4936        let static_bodies: &[bool] = &self.batch_static_bodies;
4937        // Contacts the substep's hook / modifier pre-pass discarded (an index
4938        // past the end counts as kept)
4939        #[cfg(feature = "std")]
4940        let discarded: &[bool] = &self.contact_discarded;
4941        #[cfg(not(feature = "std"))]
4942        let discarded: &[bool] = &[];
4943
4944        for batch_idx in 0..num_batches {
4945            // Phase 1: Distance constraints — parallel within batch
4946            {
4947                let indices = &self.constraint_batches[batch_idx].distance_indices;
4948                indices.par_iter().for_each(|&idx| {
4949                    // SAFETY: Graph coloring guarantees no two constraints in
4950                    // this batch share a dynamic body index; static bodies are
4951                    // borrowed shared only (`static_bodies` is the snapshot the
4952                    // coloring was built from, validated above). Each
4953                    // constraint index appears in exactly one batch, so
4954                    // cached_lambda writes are also disjoint.
4955                    unsafe {
4956                        let constraint = dists.get_mut(idx);
4957                        let body_a =
4958                            bodies.borrow(constraint.body_a, static_bodies[constraint.body_a]);
4959                        let body_b =
4960                            bodies.borrow(constraint.body_b, static_bodies[constraint.body_b]);
4961                        Self::solve_distance_pair(body_a, body_b, constraint, dt);
4962                    }
4963                });
4964            }
4965
4966            // Phase 2: Contact constraints — parallel within batch
4967            {
4968                let contacts = ContactConstraintSlicePtr {
4969                    ptr: self.contact_constraints.as_mut_ptr(),
4970                    len: self.contact_constraints.len(),
4971                };
4972                let indices = &self.constraint_batches[batch_idx].contact_indices;
4973                indices.par_iter().for_each(|&idx| {
4974                    if discarded.get(idx).copied().unwrap_or(false) {
4975                        return;
4976                    }
4977                    // SAFETY: Same invariant as Phase 1 — disjoint dynamic
4978                    // bodies per batch, static bodies shared read-only,
4979                    // each constraint index in exactly one batch.
4980                    unsafe {
4981                        let constraint = contacts.get_mut(idx);
4982                        let body_a =
4983                            bodies.borrow(constraint.body_a, static_bodies[constraint.body_a]);
4984                        let body_b =
4985                            bodies.borrow(constraint.body_b, static_bodies[constraint.body_b]);
4986                        Self::solve_contact_pair(body_a, body_b, constraint);
4987                    }
4988                });
4989            }
4990        }
4991    }
4992
4993    /// Solve a single distance constraint given mutable body references.
4994    ///
4995    /// Extracted as a static method (no `&self`) for parallel dispatch.
4996    /// Warm-starting seeds the solver with the cached lambda from the
4997    /// previous substep, reducing iterations needed for convergence.
4998    #[cfg(feature = "parallel")]
4999    #[inline(always)]
5000    fn solve_distance_pair(
5001        mut body_a: BodyRef<'_>,
5002        mut body_b: BodyRef<'_>,
5003        constraint: &mut DistanceConstraint,
5004        dt: Fix128,
5005    ) {
5006        let (pos_a, rot_a, inv_mass_a) = {
5007            let a = body_a.get();
5008            (a.position, a.rotation, a.inv_mass)
5009        };
5010        let (pos_b, rot_b, inv_mass_b) = {
5011            let b = body_b.get();
5012            (b.position, b.rotation, b.inv_mass)
5013        };
5014
5015        let anchor_a = pos_a + rot_a.rotate_vec(constraint.local_anchor_a);
5016        let anchor_b = pos_b + rot_b.rotate_vec(constraint.local_anchor_b);
5017
5018        let delta = anchor_b - anchor_a;
5019        let (normal, distance) = delta.normalize_with_length();
5020
5021        if distance.is_zero() {
5022            return;
5023        }
5024
5025        let error = distance - constraint.target_distance;
5026
5027        let compliance_term = constraint.compliance / (dt * dt);
5028        let w_sum = inv_mass_a + inv_mass_b + compliance_term;
5029
5030        if w_sum < W_SUM_EPSILON {
5031            return;
5032        }
5033
5034        // XPBD (Macklin 2016): `cached_lambda` is the Lagrange multiplier
5035        // accumulated over this substep (zeroed by `reset_lambdas`).
5036        //   dlambda = (C - alpha~ * lambda) / (w_a + w_b + alpha~)
5037        //   lambda += dlambda,  dx = dlambda * w * grad C
5038        // Before 1.2.0 `lambda` was overwritten instead of accumulated, so the
5039        // alpha~*lambda term was always under-estimated and the effective
5040        // stiffness of compliant constraints depended on `iterations`.
5041        let inv_w_sum = Fix128::ONE / w_sum;
5042        let lambda = constraint.cached_lambda;
5043        let dlambda = (error - lambda * compliance_term) * inv_w_sum;
5044        constraint.cached_lambda = lambda + dlambda;
5045        let correction = normal * dlambda;
5046
5047        // Branchless: static bodies have inv_mass == ZERO, correction * ZERO == ZERO.
5048        // `set_position` is a no-op for `BodyRef::Static`, matching the
5049        // sequential path which writes the unchanged position.
5050        let delta_a = correction * inv_mass_a;
5051        let delta_b = correction * inv_mass_b;
5052        body_a.set_position(select_vec3(!inv_mass_a.is_zero(), pos_a + delta_a, pos_a));
5053        body_b.set_position(select_vec3(!inv_mass_b.is_zero(), pos_b - delta_b, pos_b));
5054    }
5055
5056    /// Solve a single contact constraint given mutable body references.
5057    ///
5058    /// Extracted as a static method (no `&self`) for parallel dispatch.
5059    /// Warm-starting seeds the solver with the cached lambda from the
5060    /// previous substep, reducing iterations needed for convergence.
5061    #[cfg(feature = "parallel")]
5062    #[inline(always)]
5063    fn solve_contact_pair(
5064        mut body_a: BodyRef<'_>,
5065        mut body_b: BodyRef<'_>,
5066        constraint: &mut ContactConstraint,
5067    ) {
5068        let (pos_a, prev_a, inv_mass_a, sensor_a) = {
5069            let a = body_a.get();
5070            (a.position, a.prev_position, a.inv_mass, a.is_sensor)
5071        };
5072        let (pos_b, prev_b, inv_mass_b, sensor_b) = {
5073            let b = body_b.get();
5074            (b.position, b.prev_position, b.inv_mass, b.is_sensor)
5075        };
5076
5077        // Skip physics response for sensor/trigger bodies
5078        if sensor_a || sensor_b {
5079            return;
5080        }
5081
5082        let contact = constraint.contact;
5083
5084        if contact.depth <= Fix128::ZERO {
5085            return;
5086        }
5087
5088        let w_sum = inv_mass_a + inv_mass_b;
5089        if w_sum < W_SUM_EPSILON {
5090            return;
5091        }
5092
5093        // Accumulated non-negative contact multiplier (1.2.0). The contact is
5094        // re-detected every substep, so `depth` is the penetration at the start
5095        // of this substep and `cached_lambda` the separation already applied in
5096        // earlier iterations: dlambda = depth - lambda, clamped to lambda >= 0.
5097        // Before 1.2.0 every iteration re-pushed `depth - 0.85 * lambda_prev`
5098        // against a frame-stale depth, which multiplied the separation by the
5099        // iteration and substep counts (5 m/s collision → 700 m/s at defaults).
5100        let inv_w_sum = Fix128::ONE / w_sum;
5101        let lambda = constraint.cached_lambda;
5102        let dlambda = contact.depth - lambda;
5103        let (mut pos_a, mut pos_b) = (pos_a, pos_b);
5104        if dlambda > Fix128::ZERO {
5105            constraint.cached_lambda = lambda + dlambda;
5106
5107            let correction = contact.normal * dlambda;
5108            let correction_a = correction * (inv_mass_a * inv_w_sum);
5109            let correction_b = correction * (inv_mass_b * inv_w_sum);
5110
5111            // Branchless: inv_mass == ZERO for static bodies, correction_x will be ZERO.
5112            pos_a = select_vec3(!inv_mass_a.is_zero(), pos_a + correction_a, pos_a);
5113            pos_b = select_vec3(!inv_mass_b.is_zero(), pos_b - correction_b, pos_b);
5114        }
5115
5116        // Static friction (Müller et al. 2020, eq. (26)), see
5117        // `static_friction_correction`.
5118        if let Some((fa, fb)) = static_friction_correction(
5119            pos_a - prev_a,
5120            pos_b - prev_b,
5121            contact.normal,
5122            inv_mass_a,
5123            inv_mass_b,
5124            inv_w_sum,
5125            constraint.friction,
5126            constraint.cached_lambda,
5127        ) {
5128            pos_a = select_vec3(!inv_mass_a.is_zero(), pos_a - fa, pos_a);
5129            pos_b = select_vec3(!inv_mass_b.is_zero(), pos_b + fb, pos_b);
5130        }
5131
5132        // `set_position` is a no-op for `BodyRef::Static`.
5133        body_a.set_position(pos_a);
5134        body_b.set_position(pos_b);
5135    }
5136
5137    /// Solve distance constraints (sequential) with warm-starting (Gap 3.1).
5138    ///
5139    /// Warm-starting seeds each constraint with its cached lambda from the
5140    /// previous substep, reducing iterations needed for convergence.
5141    #[inline(always)]
5142    fn solve_distance_constraints(&mut self, dt: Fix128) {
5143        let num_constraints = self.distance_constraints.len();
5144        for i in 0..num_constraints {
5145            let constraint = self.distance_constraints[i];
5146            let body_a = self.bodies[constraint.body_a];
5147            let body_b = self.bodies[constraint.body_b];
5148
5149            // Get world-space anchor positions
5150            let anchor_a = body_a.position + body_a.rotation.rotate_vec(constraint.local_anchor_a);
5151            let anchor_b = body_b.position + body_b.rotation.rotate_vec(constraint.local_anchor_b);
5152
5153            // Compute constraint error (single sqrt via normalize_with_length)
5154            let delta = anchor_b - anchor_a;
5155            let (normal, distance) = delta.normalize_with_length();
5156
5157            if distance.is_zero() {
5158                continue;
5159            }
5160
5161            let error = distance - constraint.target_distance;
5162
5163            // Compute compliance term (XPBD)
5164            let compliance_term = constraint.compliance / (dt * dt);
5165            let w_sum = body_a.inv_mass + body_b.inv_mass + compliance_term;
5166
5167            if w_sum < W_SUM_EPSILON {
5168                continue;
5169            }
5170
5171            // XPBD: accumulate the Lagrange multiplier over the substep
5172            // (see `solve_distance_pair` for the derivation).
5173            let inv_w_sum = Fix128::ONE / w_sum;
5174            let lambda = constraint.cached_lambda;
5175            let dlambda = (error - lambda * compliance_term) * inv_w_sum;
5176            self.distance_constraints[i].cached_lambda = lambda + dlambda;
5177            let correction = normal * dlambda;
5178
5179            // Branchless apply corrections: static bodies have inv_mass == ZERO.
5180            let delta_a = correction * body_a.inv_mass;
5181            let delta_b = correction * body_b.inv_mass;
5182            self.bodies[constraint.body_a].position = select_vec3(
5183                !body_a.inv_mass.is_zero(),
5184                self.bodies[constraint.body_a].position + delta_a,
5185                self.bodies[constraint.body_a].position,
5186            );
5187            self.bodies[constraint.body_b].position = select_vec3(
5188                !body_b.inv_mass.is_zero(),
5189                self.bodies[constraint.body_b].position - delta_b,
5190                self.bodies[constraint.body_b].position,
5191            );
5192        }
5193    }
5194
5195    /// One serial contact pass (one PGS iteration) over the constraints that
5196    /// [`Self::apply_contact_filters`] kept (Gap 2.3).
5197    ///
5198    /// # Claims
5199    ///
5200    /// - The pass does not call hooks or modifiers; it reads the values the
5201    ///   pre-pass wrote back, so the substep's iterations all see the same
5202    ///   friction, restitution and contact.
5203    /// - A modifier therefore runs once per substep, not once per iteration,
5204    ///   and a relative change such as `friction *= 0.5` does not compound
5205    ///   with `iterations`.
5206    /// - Sensor contacts and contacts discarded by the pre-pass are skipped.
5207    ///
5208    /// # v0.11.0 auto-routing
5209    ///
5210    /// If a GPU solver bridge has been installed on this world via
5211    /// [`Self::set_gpu_solver_bridge`], contact-solve is routed through
5212    /// the bridge (one pass, same filters as the pre-pass)
5213    /// instead of running the CPU inline solver below. The bridge is
5214    /// briefly taken out of `self` via `Option::take()` so the borrow
5215    /// checker sees `&mut self` for the routed call, then reinstalled
5216    /// on exit — the field is unchanged from the caller's perspective.
5217    ///
5218    /// If no bridge is installed, the existing CPU-only code path runs
5219    /// unchanged.
5220    fn solve_contact_constraints(&mut self, _dt: Fix128) {
5221        #[cfg(feature = "gpu-solver-bridge")]
5222        if let Some(mut bridge) = self.gpu_solver_bridge.take() {
5223            self.solve_contact_pass_with_bridge(bridge.as_mut());
5224            self.gpu_solver_bridge = Some(bridge);
5225            return;
5226        }
5227        let num_constraints = self.contact_constraints.len();
5228        for i in 0..num_constraints {
5229            let constraint = self.contact_constraints[i];
5230            let body_a = self.bodies[constraint.body_a];
5231            let body_b = self.bodies[constraint.body_b];
5232
5233            // Skip physics response for sensor/trigger bodies
5234            if body_a.is_sensor || body_b.is_sensor {
5235                continue;
5236            }
5237
5238            // Discarded by the substep's hook / modifier pre-pass
5239            #[cfg(feature = "std")]
5240            if self.contact_discarded.get(i).copied().unwrap_or(false) {
5241                continue;
5242            }
5243
5244            // The modifiers' result was written back to the constraint by the
5245            // pre-pass, so this is the value every iteration sees.
5246            let contact = constraint.contact;
5247
5248            // Only resolve if penetrating
5249            if contact.depth <= Fix128::ZERO {
5250                continue;
5251            }
5252
5253            let w_sum = body_a.inv_mass + body_b.inv_mass;
5254            if w_sum < W_SUM_EPSILON {
5255                continue;
5256            }
5257
5258            // Accumulated contact multiplier, see `solve_contact_pair`.
5259            let inv_w_sum = Fix128::ONE / w_sum;
5260            let lambda = constraint.cached_lambda;
5261            let dlambda = contact.depth - lambda;
5262            if dlambda > Fix128::ZERO {
5263                self.contact_constraints[i].cached_lambda = lambda + dlambda;
5264
5265                let correction = contact.normal * dlambda;
5266                let correction_a = correction * (body_a.inv_mass * inv_w_sum);
5267                let correction_b = correction * (body_b.inv_mass * inv_w_sum);
5268
5269                // Branchless: inv_mass == ZERO for static bodies, correction_x will be ZERO.
5270                self.bodies[constraint.body_a].position = select_vec3(
5271                    !body_a.inv_mass.is_zero(),
5272                    self.bodies[constraint.body_a].position + correction_a,
5273                    self.bodies[constraint.body_a].position,
5274                );
5275                self.bodies[constraint.body_b].position = select_vec3(
5276                    !body_b.inv_mass.is_zero(),
5277                    self.bodies[constraint.body_b].position - correction_b,
5278                    self.bodies[constraint.body_b].position,
5279                );
5280            }
5281
5282            // Static friction (Müller et al. 2020, eq. (26)), see
5283            // `static_friction_correction`.
5284            self.apply_static_friction(i, inv_w_sum);
5285        }
5286    }
5287
5288    /// Position-level static friction of contact `i` against the current
5289    /// body positions, shared by the serial and bridge contact passes (the
5290    /// batched pass calls [`static_friction_correction`] on its pair
5291    /// references). The caller has already skipped sensor, discarded and
5292    /// non-penetrating contacts.
5293    fn apply_static_friction(&mut self, i: usize, inv_w_sum: Fix128) {
5294        let constraint = self.contact_constraints[i];
5295        let a = self.bodies[constraint.body_a];
5296        let b = self.bodies[constraint.body_b];
5297        if let Some((fa, fb)) = static_friction_correction(
5298            a.position - a.prev_position,
5299            b.position - b.prev_position,
5300            constraint.contact.normal,
5301            a.inv_mass,
5302            b.inv_mass,
5303            inv_w_sum,
5304            constraint.friction,
5305            constraint.cached_lambda,
5306        ) {
5307            self.bodies[constraint.body_a].position =
5308                select_vec3(!a.inv_mass.is_zero(), a.position - fa, a.position);
5309            self.bodies[constraint.body_b].position =
5310                select_vec3(!b.inv_mass.is_zero(), b.position + fb, b.position);
5311        }
5312    }
5313
5314    /// v0.10.0 opt-in: run one PGS contact-solve iteration via a
5315    /// caller-supplied [`GpuSolverBridge`](crate::gpu_bridge::GpuSolverBridge) instead of the CPU-side
5316    /// `solve_contact_constraints` hot loop.
5317    ///
5318    /// # Claims
5319    ///
5320    /// - Each call runs the hooks and modifiers once, writes the result back
5321    ///   to the constraints, then runs one bridge pass over the survivors.
5322    /// - `substep_with_bridge` runs the hooks and modifiers once per substep
5323    ///   and then only the pass per iteration, so a relative modifier does
5324    ///   not compound with `iterations`.
5325    ///
5326    /// # Byte-exact CPU parity
5327    ///
5328    /// The bridge-routed path applies Stage A filters (sensor skip,
5329    /// pre-solve hooks, contact modifiers) on the CPU side upfront
5330    /// exactly as `solve_contact_constraints` does inline. Because
5331    /// the closures the hooks and modifiers evaluate depend only on
5332    /// their function inputs — never on live body state that
5333    /// changes during the loop — a batched upfront application is
5334    /// byte-exact equivalent to the CPU's per-constraint
5335    /// application. Stage B (depth ≤ 0 skip, w_sum < ε skip,
5336    /// warm-start biased lambda, cached_lambda write, position
5337    /// correction) is routed through the bridge, which runs the
5338    /// same sequential Gauss-Seidel semantics because bridge
5339    /// backends dispatch a single-workgroup single-thread compute
5340    /// kernel.
5341    ///
5342    /// # Constraint contract
5343    ///
5344    /// - The bridge receives a filtered `Vec<ContactConstraint>`
5345    ///   containing only the constraints that survived Stage A
5346    ///   (sensor-free, hook-approved, modifier-approved). The
5347    ///   contact / friction / restitution fields may be mutated
5348    ///   from the original slot values if a contact modifier
5349    ///   ran.
5350    /// - After readback the method writes each updated
5351    ///   `cached_lambda` back into `self.contact_constraints` via
5352    ///   an index map, mirroring the CPU write that lands at
5353    ///   `self.contact_constraints[i].cached_lambda = lambda` at
5354    ///   the corresponding slot.
5355    /// - Body positions are written back to `self.bodies[i].position`.
5356    ///
5357    /// # Panics
5358    ///
5359    /// Panics if the bridge implementation does not override the
5360    /// v0.9.0 contact-solve methods (`send_contact_constraints`,
5361    /// `send_body_state`, `dispatch_contact_solve_iteration`,
5362    /// `recv_contact_constraints`, `recv_body_positions`).
5363    #[cfg(feature = "gpu-solver-bridge")]
5364    pub fn solve_contact_constraints_with_bridge<B: crate::gpu_bridge::GpuSolverBridge + ?Sized>(
5365        &mut self,
5366        bridge: &mut B,
5367    ) {
5368        self.apply_contact_filters();
5369        self.solve_contact_pass_with_bridge(bridge);
5370    }
5371
5372    /// One bridge-routed PGS pass over the constraints that
5373    /// [`Self::apply_contact_filters`] kept. Does not call hooks or
5374    /// modifiers, so the iteration loop of a substep sees the values the
5375    /// pre-pass wrote.
5376    #[cfg(feature = "gpu-solver-bridge")]
5377    fn solve_contact_pass_with_bridge<B: crate::gpu_bridge::GpuSolverBridge + ?Sized>(
5378        &mut self,
5379        bridge: &mut B,
5380    ) {
5381        // ---- Stage A: select the surviving constraints ----
5382        let num_constraints = self.contact_constraints.len();
5383        let mut filtered: Vec<ContactConstraint> = Vec::with_capacity(num_constraints);
5384        let mut mapping: Vec<usize> = Vec::with_capacity(num_constraints);
5385
5386        for i in 0..num_constraints {
5387            let constraint = self.contact_constraints[i];
5388            let body_a = self.bodies[constraint.body_a];
5389            let body_b = self.bodies[constraint.body_b];
5390
5391            if body_a.is_sensor || body_b.is_sensor {
5392                continue;
5393            }
5394            if self.contact_discarded.get(i).copied().unwrap_or(false) {
5395                continue;
5396            }
5397
5398            let contact = constraint.contact;
5399            let friction = constraint.friction;
5400            let restitution = constraint.restitution;
5401
5402            filtered.push(ContactConstraint {
5403                body_a: constraint.body_a,
5404                body_b: constraint.body_b,
5405                contact,
5406                friction,
5407                restitution,
5408                cached_lambda: constraint.cached_lambda,
5409            });
5410            mapping.push(i);
5411        }
5412
5413        if filtered.is_empty() {
5414            return;
5415        }
5416
5417        // ---- Extract per-body state ----
5418        let mut positions: Vec<[Fix128; 3]> = Vec::with_capacity(self.bodies.len());
5419        let mut inv_masses: Vec<Fix128> = Vec::with_capacity(self.bodies.len());
5420        for body in &self.bodies {
5421            positions.push([body.position.x, body.position.y, body.position.z]);
5422            inv_masses.push(body.inv_mass);
5423        }
5424
5425        // ---- Stage B: route through bridge ----
5426        bridge.send_contact_constraints(&filtered);
5427        bridge.send_body_state(&positions, &inv_masses);
5428        bridge.dispatch_contact_solve_iteration(self.config.warm_start_factor);
5429
5430        let mut updated_constraints = filtered;
5431        let mut updated_positions = positions;
5432        bridge.recv_contact_constraints(&mut updated_constraints);
5433        bridge.recv_body_positions(&mut updated_positions);
5434
5435        // ---- Write back cached_lambda + positions ----
5436        for (filtered_idx, orig_slot) in mapping.iter().enumerate() {
5437            self.contact_constraints[*orig_slot].cached_lambda =
5438                updated_constraints[filtered_idx].cached_lambda;
5439        }
5440        for (i, pos) in updated_positions.iter().enumerate() {
5441            self.bodies[i].position = Vec3Fix::new(pos[0], pos[1], pos[2]);
5442        }
5443
5444        // Static friction (Müller et al. 2020, eq. (26)) on the CPU after
5445        // the bridge's normal pass, over the same filtered contacts (the
5446        // bridge contract carries only the normal multiplier).
5447        for &orig_slot in &mapping {
5448            let c = self.contact_constraints[orig_slot];
5449            let w_sum = self.bodies[c.body_a].inv_mass + self.bodies[c.body_b].inv_mass;
5450            if c.contact.depth <= Fix128::ZERO || w_sum < W_SUM_EPSILON {
5451                continue;
5452            }
5453            self.apply_static_friction(orig_slot, Fix128::ONE / w_sum);
5454        }
5455    }
5456
5457    /// v0.10.0 opt-in: run one full simulation step with the
5458    /// contact-solve stage routed through a caller-supplied
5459    /// [`GpuSolverBridge`](crate::gpu_bridge::GpuSolverBridge). Semantically equivalent to
5460    /// [`Self::step`] with `substep(_)` replaced by
5461    /// [`Self::substep_with_bridge`] inside the substep loop.
5462    ///
5463    /// # Byte-exact CPU parity
5464    ///
5465    /// When the bridge implementation is byte-exact vs the CPU
5466    /// `solve_contact_constraints`, a full `step_with_bridge`
5467    /// produces `self.bodies` and `self.contact_constraints` state
5468    /// byte-identical to what a CPU-only `step(dt)` call produces
5469    /// from the same initial state and configuration.
5470    ///
5471    /// # Solver backend
5472    ///
5473    /// `config.solver_backend` is not consulted: the bridge replaces the
5474    /// contact solve of the XPBD substep, so with [`SolverBackend::Tgs`] this
5475    /// gives the same bits as with [`SolverBackend::Xpbd`]. A bridge path for
5476    /// TGS does not exist yet; use [`Self::step`] for TGS.
5477    #[cfg(feature = "gpu-solver-bridge")]
5478    pub fn step_with_bridge<B: crate::gpu_bridge::GpuSolverBridge + ?Sized>(
5479        &mut self,
5480        bridge: &mut B,
5481        dt: Fix128,
5482    ) {
5483        // The checks of `try_step` (a recorded fault, a non-positive `dt`, a
5484        // participant step rule or a per-body field that does not fit): when
5485        // one fails nothing runs, as `step` does. This path runs the XPBD
5486        // substep loop whatever the backend, so the step rules are checked
5487        // against the width it hands out, `dt / substeps`.
5488        let h = Self::divided_substep_width(dt, self.config.substeps);
5489        if !matches!(self.check_step(dt, h), Ok(true)) {
5490            return;
5491        }
5492        self.make_body_rotations_unit();
5493        self.wake_on_sdf_shape_change();
5494        self.begin_sdf_contact_log();
5495        let mut frozen = self.participant_flags();
5496
5497        // Phase 0: Event frame lifecycle (contacts are cleared and re-detected
5498        // in every substep since 1.2.0, see `substep`)
5499        self.events.begin_frame();
5500
5501        // Phase 0.5: Rebuild island connectivity from current joints
5502        self.islands.resize(self.bodies.len());
5503        self.islands.reset_unions();
5504        for j in &self.joints {
5505            let (a, b) = j.bodies();
5506            if a < self.bodies.len() && b < self.bodies.len() {
5507                self.islands.union(a, b);
5508            }
5509        }
5510
5511        // Phase 1: Apply force fields
5512        if !self.force_fields.is_empty() {
5513            apply_force_fields(&self.force_fields, &mut self.bodies, dt);
5514        }
5515
5516        // Phase 2 (collision detection) is per substep since 1.2.0, see `step`.
5517
5518        // Phase 3: Substep loop with bridge-routed contact solve
5519        let substep_dt = dt / Fix128::from_int(self.config.substeps as i64);
5520        let n = self.config.substeps;
5521        for i in 0..n {
5522            self.kinematic_substeps_left = n - i;
5523            self.sdf_contact_substep = i;
5524            let overflow_at_start = self.participants_begin_substep(i, n, substep_dt, &mut frozen);
5525            self.substep_with_bridge(bridge, substep_dt);
5526            self.participants_end_substep(overflow_at_start, &frozen);
5527        }
5528        self.kinematic_substeps_left = 0;
5529        self.sdf_contact_substep = 0;
5530
5531        // Phase 3.5: Frame-level damping (1.2.0, was per substep)
5532        self.apply_frame_damping();
5533
5534        // Phase 4: Update sleeping
5535        self.islands.update_sleep(&self.bodies);
5536
5537        // Phase 4.5: Leave every SDF collider attached to a body at that
5538        // body's final pose, which is what the queries between steps
5539        // (`sdf_contacts`, `sdf_ccd_hits`, the shape casts) read.
5540        self.sync_sdf_colliders();
5541
5542        // Phase 5: End event frame
5543        self.events.end_frame();
5544    }
5545
5546    /// v0.10.0 opt-in: run one substep with the contact-solve stage
5547    /// routed through a caller-supplied [`GpuSolverBridge`](crate::gpu_bridge::GpuSolverBridge). The
5548    /// integrate + distance-projection + joint + velocity-update
5549    /// stages stay on the CPU; only the inner PGS contact-solve
5550    /// iterations are dispatched through the bridge.
5551    ///
5552    /// # Byte-exact CPU parity
5553    ///
5554    /// When the bridge implementation is byte-exact vs the CPU
5555    /// `solve_contact_constraints` (which every ALICE-TRT
5556    /// `TrtSolverAdapter` release since v2.6.0 asserts on the
5557    /// 3-platform CI matrix), a full `substep_with_bridge` sequence
5558    /// produces `self.bodies` and `self.contact_constraints` state
5559    /// byte-identical to what a CPU-only `substep(dt)` call produces
5560    /// from the same initial state and configuration.
5561    ///
5562    /// Called on its own, this runs no participant: the participants of
5563    /// [`crate::world_participant`] are called by the step loops
5564    /// ([`Self::step_with_bridge`], [`Self::try_step`]) around each substep.
5565    ///
5566    /// # Solver backend
5567    ///
5568    /// `config.solver_backend` is not consulted: the bridge replaces the
5569    /// contact solve of the XPBD substep, so with [`SolverBackend::Tgs`] this
5570    /// gives the same bits as with [`SolverBackend::Xpbd`]. A bridge path for
5571    /// TGS does not exist yet; use [`Self::step`] for TGS.
5572    #[cfg(feature = "gpu-solver-bridge")]
5573    pub fn substep_with_bridge<B: crate::gpu_bridge::GpuSolverBridge + ?Sized>(
5574        &mut self,
5575        bridge: &mut B,
5576        dt: Fix128,
5577    ) {
5578        // Callable on its own (outside `step_with_bridge`), so it is an entry
5579        // of its own for the rotations too.
5580        self.make_body_rotations_unit();
5581        self.apply_joint_motors(dt);
5582        self.integrate_positions(dt);
5583        self.reset_lambdas();
5584
5585        // 1.2. Collision detection on the predicted positions (Small Steps).
5586        self.clear_contacts();
5587        self.detect_collisions();
5588
5589        // 1.5. Resolve SDF collisions (implicit surface contacts)
5590        if !self.sdf_colliders.is_empty() {
5591            self.resolve_sdf_collisions();
5592        }
5593        if !self.static_colliders.is_empty() {
5594            self.resolve_static_collisions(false);
5595        }
5596
5597        // 2. Solve constraints (sequential). Distance stays CPU;
5598        //    contact routes through the bridge. The first iteration runs the
5599        //    hooks and modifiers once and solves; later iterations only solve,
5600        //    so a relative modifier does not compound with `iterations`.
5601        for iteration in 0..self.config.iterations {
5602            self.solve_distance_constraints(dt);
5603            if iteration == 0 {
5604                self.solve_contact_constraints_with_bridge(bridge);
5605            } else {
5606                self.solve_contact_pass_with_bridge(bridge);
5607            }
5608        }
5609
5610        // 3. Solve joint constraints — route through the same bridge, then
5611        //    remove the joints whose reaction force in that solve exceeded
5612        //    their break force (the same check `solve_joints_dispatch` runs).
5613        let forces = if self.joints.is_empty() {
5614            None
5615        } else {
5616            self.bridge_joint_reaction_forces(dt)
5617        };
5618        self.solve_joints_with_bridge(bridge, dt);
5619        if let Some(forces) = forces {
5620            self.break_overloaded_joints(&forces);
5621        }
5622
5623        self.update_velocities(dt);
5624    }
5625
5626    /// v0.12.0: run one joint-solve pass with the joint stage routed
5627    /// through a caller-supplied [`GpuSolverBridge`](crate::gpu_bridge::GpuSolverBridge). Extracts body
5628    /// positions, rotations and inverse masses into the shape the
5629    /// bridge expects, uploads them via `send_joints`, `send_body_state`
5630    /// and `send_body_rotations`, dispatches
5631    /// `dispatch_joint_solve_iteration(dt)`, reads back the
5632    /// post-solve positions via `recv_body_positions`, and writes
5633    /// them back into `self.bodies[i].position`.
5634    ///
5635    /// # Contract
5636    ///
5637    /// - The bridge must implement the v0.12.0 joint-solve methods
5638    ///   (`send_joints`, `send_body_rotations`,
5639    ///   `dispatch_joint_solve_iteration`); the default `panic!`
5640    ///   implementations surface backends that do not.
5641    /// - `dt` is the substep length used to compute
5642    ///   `compliance / (dt * dt)` inside the kernel.
5643    /// - A joint whose two ends are the same body is not uploaded
5644    ///   (the CPU solve skips it too, the body stays free); the
5645    ///   remaining joints keep their order.
5646    /// - When no joint remains (`self.joints` is empty or holds only
5647    ///   such joints), this method is a no-op and returns without
5648    ///   touching the bridge — useful for callers that
5649    ///   unconditionally route through a bridge and let this method
5650    ///   decide whether there's work to do.
5651    ///
5652    /// # Byte-exact CPU parity
5653    ///
5654    /// The CPU-side `solve_joints` runs each joint independently in
5655    /// index order; the bridge is required to reproduce that exact
5656    /// order and per-joint arithmetic. ALICE-TRT v3.1.0 satisfies
5657    /// this by dispatching a single-workgroup single-thread kernel
5658    /// that walks the uploaded joint list top to bottom.
5659    #[cfg(feature = "gpu-solver-bridge")]
5660    pub fn solve_joints_with_bridge<B: crate::gpu_bridge::GpuSolverBridge + ?Sized>(
5661        &mut self,
5662        bridge: &mut B,
5663        dt: Fix128,
5664    ) {
5665        // A joint whose two ends are the same body constrains nothing (a body
5666        // cannot move relative to itself); the CPU solve skips it, and a
5667        // backend walking the list would apply both sides' corrections to that
5668        // one body. Upload only the other joints, in their original order.
5669        let joints: Vec<crate::joint::Joint> = self
5670            .joints
5671            .iter()
5672            .filter(|j| {
5673                let (a, b) = j.bodies();
5674                a != b
5675            })
5676            .copied()
5677            .collect();
5678        if joints.is_empty() {
5679            return;
5680        }
5681
5682        // ---- Extract per-body state ----
5683        let mut positions: Vec<[Fix128; 3]> = Vec::with_capacity(self.bodies.len());
5684        let mut inv_masses: Vec<Fix128> = Vec::with_capacity(self.bodies.len());
5685        let mut rotations: Vec<[Fix128; 4]> = Vec::with_capacity(self.bodies.len());
5686        for body in &self.bodies {
5687            positions.push([body.position.x, body.position.y, body.position.z]);
5688            inv_masses.push(body.inv_mass);
5689            rotations.push([
5690                body.rotation.x,
5691                body.rotation.y,
5692                body.rotation.z,
5693                body.rotation.w,
5694            ]);
5695        }
5696
5697        // ---- Stage B: route through bridge ----
5698        bridge.send_joints(&joints);
5699        bridge.send_body_state(&positions, &inv_masses);
5700        bridge.send_body_rotations(&rotations);
5701        bridge.dispatch_joint_solve_iteration(dt);
5702
5703        let mut updated_positions = positions;
5704        bridge.recv_body_positions(&mut updated_positions);
5705
5706        // ---- Write back positions ----
5707        for (i, pos) in updated_positions.iter().enumerate() {
5708            self.bodies[i].position = crate::math::Vec3Fix::new(pos[0], pos[1], pos[2]);
5709        }
5710    }
5711
5712    /// v0.12.0 private helper: solve joints either through the
5713    /// installed GPU solver bridge (if any) or via the CPU
5714    /// `solve_joints` fallback. Used by [`Self::step`] / substep
5715    /// entry points to auto-route without every caller needing to
5716    /// know about the bridge.
5717    fn solve_joints_dispatch(&mut self, dt: Fix128) {
5718        if self.joints.is_empty() {
5719            return;
5720        }
5721        #[cfg(feature = "gpu-solver-bridge")]
5722        if let Some(mut bridge) = self.gpu_solver_bridge.take() {
5723            let forces = self.bridge_joint_reaction_forces(dt);
5724            self.solve_joints_with_bridge(bridge.as_mut(), dt);
5725            self.gpu_solver_bridge = Some(bridge);
5726            if let Some(forces) = forces {
5727                self.break_overloaded_joints(&forces);
5728            }
5729            return;
5730        }
5731        if self.joints.iter().any(|j| j.break_force().is_some()) {
5732            let forces =
5733                crate::joint::solve_joints_with_reaction_forces(&self.joints, &mut self.bodies, dt);
5734            self.break_overloaded_joints(&forces);
5735        } else {
5736            solve_joints(&self.joints, &mut self.bodies, dt);
5737        }
5738    }
5739
5740    /// The reaction forces the bridge's joint solve will transmit, when any
5741    /// joint can break: the trait returns only positions, so the CPU solve
5742    /// ([`crate::joint::solve_joints_with_reaction_forces`]) runs on a copy of
5743    /// the bodies. A bridge matches the CPU solve bit for bit
5744    /// ([`crate::gpu_bridge::GpuSolverBridge::assert_bit_exact_vs_cpu`]), so the
5745    /// copy sees the same multipliers. `None` when no joint has a break force.
5746    #[cfg(feature = "gpu-solver-bridge")]
5747    fn bridge_joint_reaction_forces(&self, dt: Fix128) -> Option<Vec<Fix128>> {
5748        if !self.joints.iter().any(|j| j.break_force().is_some()) {
5749            return None;
5750        }
5751        let mut copy = self.bodies.clone();
5752        Some(crate::joint::solve_joints_with_reaction_forces(
5753            &self.joints,
5754            &mut copy,
5755            dt,
5756        ))
5757    }
5758
5759    /// Remove every joint whose reaction force in this substep's joint solve
5760    /// (`forces`, in joint order, from
5761    /// [`crate::joint::solve_joints_with_reaction_forces`]) exceeds its
5762    /// `break_force`, and report each in
5763    /// [`crate::event::EventCollector::joint_break_events`].
5764    ///
5765    /// Runs after the joints are solved on every path that solves the world's
5766    /// joints: the XPBD substeps, the TGS joint projection and a bridge set with
5767    /// [`Self::set_gpu_solver_bridge`] go through [`Self::solve_joints_dispatch`],
5768    /// and [`Self::substep_with_bridge`] (so [`Self::step_with_bridge`]) calls it
5769    /// after its bridge joint solve. A broken joint's correction from this
5770    /// substep has been applied; it no longer acts from the next solve on.
5771    /// Joints are removed from the highest index down with the swap-remove of
5772    /// [`Self::remove_joint`] (motors follow it). No wake is needed: a joint
5773    /// transmits force only while its island is awake (a sleeping island's
5774    /// bodies do not move, so its joints carry no multiplier), so a joint that
5775    /// breaks always belongs to an awake island. The island unions are then rebuilt
5776    /// from the remaining joints, as at the start of every step, so the rest of
5777    /// the step (and a snapshot taken after it) no longer joins the two bodies.
5778    ///
5779    /// A joint whose bodies are both asleep is not moved by the solve, carries
5780    /// no multiplier and so does not break while they sleep (lowering its
5781    /// `break_force` through the public `joints` field takes effect once its
5782    /// island wakes).
5783    fn break_overloaded_joints(&mut self, forces: &[Fix128]) {
5784        let overloaded: Vec<(usize, Fix128)> = self
5785            .joints
5786            .iter()
5787            .zip(forces)
5788            .enumerate()
5789            .filter(|(_, (joint, force))| joint.break_force().is_some_and(|max| **force > max))
5790            .map(|(i, (_, force))| (i, *force))
5791            .collect();
5792        for &(index, force) in overloaded.iter().rev() {
5793            if let Some(joint) = self.remove_joint(index) {
5794                self.events
5795                    .report_joint_break(crate::event::JointBreakEvent {
5796                        index,
5797                        joint,
5798                        force,
5799                    });
5800            }
5801        }
5802        if !overloaded.is_empty() {
5803            self.islands.resize(self.bodies.len());
5804            self.islands.reset_unions();
5805            for j in &self.joints {
5806                let (a, b) = j.bodies();
5807                if a < self.bodies.len() && b < self.bodies.len() {
5808                    self.islands.union(a, b);
5809                }
5810            }
5811        }
5812    }
5813
5814    /// Add an SDF collider to the world
5815    ///
5816    /// A collider attached to a body ([`SdfCollider::new_dynamic`]) is placed
5817    /// at that body's current pose when the body exists, and follows the body
5818    /// from then on: every step copies the body's pose into it before SDF
5819    /// overlap is resolved and again at the end of the step.
5820    ///
5821    /// The collider's cached values are recomputed from its fields
5822    /// ([`SdfCollider::update_cache`]): a `rotation` or `scale` written to the
5823    /// collider directly before the call takes effect, and the rotation is
5824    /// stored as a unit quaternion.
5825    pub fn add_sdf_collider(&mut self, mut collider: SdfCollider) -> usize {
5826        collider.update_cache();
5827        collider.sync_to_body(&self.bodies);
5828        let idx = self.sdf_colliders.len();
5829        self.sdf_colliders.push(collider);
5830        self.sync_sdf_generations();
5831        idx
5832    }
5833
5834    /// Bring `sdf_generations` to one entry per collider: a collider without
5835    /// an entry (just added, or pushed onto the public field directly) is
5836    /// recorded at its current generation, so adding a collider wakes
5837    /// nothing; entries past the last collider are dropped.
5838    fn sync_sdf_generations(&mut self) {
5839        let n = self.sdf_colliders.len();
5840        self.sdf_generations.truncate(n);
5841        for c in &self.sdf_colliders[self.sdf_generations.len()..] {
5842            self.sdf_generations.push(c.field.generation());
5843        }
5844    }
5845
5846    /// Wake every sleeping body when an SDF collider changed shape since the
5847    /// previous step ([`SdfField::generation`](crate::sdf_collider::SdfField::generation)
5848    /// differs from the value last seen). A sleeping body is not integrated,
5849    /// so one resting on a surface that was carved away would otherwise stay
5850    /// where it was. The whole world is woken: a field does not report where
5851    /// it changed. Fields that never change (generation constant) wake
5852    /// nothing and leave the step bit for bit as before.
5853    fn wake_on_sdf_shape_change(&mut self) {
5854        self.sync_sdf_generations();
5855        let mut changed = false;
5856        for (seen, c) in self.sdf_generations.iter_mut().zip(&self.sdf_colliders) {
5857            let now = c.field.generation();
5858            if *seen != now {
5859                *seen = now;
5860                changed = true;
5861            }
5862        }
5863        if changed {
5864            // Through the public wake (`wake_body`, the body's island), as
5865            // every other caller that wakes a sleeping body by hand. The step
5866            // has not parked anything yet: `park_begin` runs later and parks
5867            // only bodies that are asleep.
5868            for i in 0..self.bodies.len() {
5869                if self.islands.is_sleeping(i) {
5870                    self.wake_body(i);
5871                }
5872            }
5873        }
5874    }
5875
5876    /// Empty the SDF contact record at the head of a step (the allocation is
5877    /// kept) and record from substep 0.
5878    fn begin_sdf_contact_log(&mut self) {
5879        self.sdf_contact_log.clear();
5880        self.sdf_contact_substep = 0;
5881    }
5882
5883    /// The SDF contacts resolved by the last step, every substep of it, in
5884    /// the order they were applied: by substep, then by body index, then by
5885    /// collider index.
5886    ///
5887    /// Each entry is one push-out of a body out of an SDF collider
5888    /// ([`SdfContact`](crate::sdf_collider::SdfContact)): where, along which
5889    /// normal, how deep, and how fast the body was approaching, and the
5890    /// collider's frame at that substep (a collider attached to a body moves
5891    /// with it during the step, so convert `point` with the recorded frame,
5892    /// not with the collider's pose after the step). The record is
5893    /// emptied at the head of each step ([`Self::step`], [`Self::try_step`],
5894    /// `step_parallel`, `step_with_bridge`); a step that does not run (a
5895    /// recorded fault, a non-positive `dt`) leaves it as it was.
5896    /// `substep_with_bridge` called on its own appends to it. Recording
5897    /// changes nothing in the step: a world whose record is never read steps
5898    /// to the same bits. The buffer is reused from step to step, so it holds
5899    /// its largest size so far (one entry per body per collider at most).
5900    ///
5901    /// [`Self::sdf_contacts`] is a different query: what the bodies overlap
5902    /// now, between steps, without moving anything.
5903    ///
5904    /// The participants of [`crate::world_participant`] read the contacts
5905    /// recorded so far in the current step through
5906    /// [`SubstepCtx::sdf_contacts`](crate::world_participant::SubstepCtx::sdf_contacts).
5907    #[must_use]
5908    pub fn last_step_sdf_contacts(&self) -> &[crate::sdf_collider::SdfContact] {
5909        &self.sdf_contact_log
5910    }
5911
5912    /// Copy each body's pose into the SDF colliders attached to it
5913    /// ([`crate::sdf_collider::sync_dynamic_sdf_colliders`]); static colliders
5914    /// are not touched.
5915    fn sync_sdf_colliders(&mut self) {
5916        crate::sdf_collider::sync_dynamic_sdf_colliders(&mut self.sdf_colliders, &self.bodies);
5917    }
5918
5919    /// Remove an SDF collider by index
5920    pub fn remove_sdf_collider(&mut self, idx: usize) -> Option<SdfCollider> {
5921        if idx < self.sdf_colliders.len() {
5922            if idx < self.sdf_generations.len() {
5923                self.sdf_generations.remove(idx);
5924            }
5925            Some(self.sdf_colliders.remove(idx))
5926        } else {
5927            None
5928        }
5929    }
5930
5931    /// Set the collision radius for body-vs-SDF queries
5932    pub fn set_sdf_collision_radius(&mut self, radius: Fix128) {
5933        self.sdf_collision_radius = radius;
5934    }
5935
5936    /// Resolve SDF collisions by directly correcting body positions.
5937    ///
5938    /// SDF colliders are treated as immovable surfaces (infinite mass).
5939    /// Each penetrating body is pushed out along the SDF gradient.
5940    ///
5941    /// When `parallel` feature is enabled, bodies are processed in parallel via Rayon.
5942    #[cfg(feature = "std")]
5943    fn resolve_sdf_collisions(&mut self) {
5944        // A collider attached to a body is resolved at that body's current
5945        // (predicted) pose.
5946        self.sync_sdf_colliders();
5947        let sdf_colliders = &self.sdf_colliders;
5948        let colliders = &self.body_colliders;
5949        let collision_radius = self.sdf_collision_radius;
5950        let substep = self.sdf_contact_substep;
5951
5952        // Each push-out is recorded into `log` (observation only: the record
5953        // is built from values the push-out computes and never read back).
5954        let push_out =
5955            |idx: usize, body: &mut RigidBody, log: &mut Vec<crate::sdf_collider::SdfContact>| {
5956                if body.is_static() || body.is_sensor {
5957                    return;
5958                }
5959                let collider = colliders.get(idx).and_then(Option::as_ref);
5960                for (collider_index, sdf) in sdf_colliders.iter().enumerate() {
5961                    // A body is never pushed out of its own field.
5962                    if sdf.body_index == idx {
5963                        continue;
5964                    }
5965                    if let Some(contact) = sdf_contact_of(collider, body, collision_radius, sdf) {
5966                        log.push(crate::sdf_collider::SdfContact {
5967                            body_index: idx,
5968                            collider_index,
5969                            point: contact.point_b,
5970                            normal: contact.normal,
5971                            depth: contact.depth,
5972                            approach_speed: -body.velocity.dot(contact.normal),
5973                            substep,
5974                            collider_position: sdf.position,
5975                            collider_rotation: sdf.rotation,
5976                            collider_scale: sdf.scale,
5977                        });
5978                        body.position = body.position + contact.normal * contact.depth;
5979                    }
5980                }
5981            };
5982
5983        #[cfg(feature = "parallel")]
5984        {
5985            let parked: &[bool] = if self.park.active {
5986                &self.park.parked
5987            } else {
5988                &[]
5989            };
5990            self.stage_work.resolution_bodies += self.unparked_count();
5991            let scratch = &mut self.sdf_contact_scratch;
5992            if scratch.len() < self.bodies.len() {
5993                scratch.resize_with(self.bodies.len(), Vec::new);
5994            }
5995            self.bodies
5996                .par_iter_mut()
5997                .zip(scratch.par_iter_mut())
5998                .enumerate()
5999                .for_each(|(idx, (body, out))| {
6000                    out.clear();
6001                    if !parked.get(idx).copied().unwrap_or(false) {
6002                        push_out(idx, body, out);
6003                    }
6004                });
6005            let log = &mut self.sdf_contact_log;
6006            for out in &scratch[..self.bodies.len()] {
6007                log.extend_from_slice(out);
6008            }
6009        }
6010
6011        #[cfg(not(feature = "parallel"))]
6012        {
6013            let count = if self.park.active {
6014                self.park.awake.len()
6015            } else {
6016                self.bodies.len()
6017            };
6018            for k in 0..count {
6019                let idx = if self.park.active {
6020                    self.park.awake[k]
6021                } else {
6022                    k
6023                };
6024                self.stage_work.resolution_bodies += 1;
6025                push_out(idx, &mut self.bodies[idx], &mut self.sdf_contact_log);
6026            }
6027        }
6028    }
6029
6030    /// The contacts of the bodies with the SDF colliders, as `(body index, contact)`:
6031    /// what [`step`](Self::step) would push each body out of, without moving
6032    /// anything. A body with a shape or a compound is tested as that collider, any
6033    /// other body as a sphere of [`sdf_collision_radius`](Self::sdf_collision_radius);
6034    /// static bodies, sensors and an SDF attached to the body itself are skipped.
6035    /// An SDF attached to a body is queried at the pose it was given when it was
6036    /// added or at the end of the last step; after moving a body by hand, call
6037    /// [`crate::sdf_collider::sync_dynamic_sdf_colliders`] on
6038    /// [`sdf_colliders`](Self::sdf_colliders) first.
6039    #[cfg(feature = "std")]
6040    #[must_use]
6041    pub fn sdf_contacts(&self) -> Vec<(usize, crate::collider::Contact)> {
6042        let mut contacts = Vec::new();
6043        for (idx, body) in self.bodies.iter().enumerate() {
6044            if body.is_static() || body.is_sensor {
6045                continue;
6046            }
6047            let collider = self.body_colliders.get(idx).and_then(Option::as_ref);
6048            for sdf in &self.sdf_colliders {
6049                if sdf.body_index == idx {
6050                    continue;
6051                }
6052                if let Some(c) = sdf_contact_of(collider, body, self.sdf_collision_radius, sdf) {
6053                    contacts.push((idx, c));
6054                }
6055            }
6056        }
6057        contacts
6058    }
6059
6060    /// The normal force each contact of the last step carries, as
6061    /// `(contact point, normal, force)` — the input [`crate::contact_viz`] draws.
6062    ///
6063    /// `dt` is the frame step passed to [`step`](Self::step). The solver applies a
6064    /// separation `λ` (`cached_lambda`) along the normal in the last substep, of
6065    /// length `h = dt / substeps`, which for bodies of inverse masses `wₐ, w_b` is
6066    /// the displacement of a force `F` over `h`: `λ = (wₐ + w_b)·F·h²`, so
6067    /// `F = λ / ((wₐ + w_b)·h²)`. A body resting under gravity then carries its
6068    /// weight: `m·g`. The normal points from B to A and the force acts on A. Contacts
6069    /// between two immovable bodies carry none and are left out. A body that has gone
6070    /// to sleep (about a minute of rest) is not solved, so its contacts are not there
6071    /// to report.
6072    #[must_use]
6073    pub fn contact_forces(&self, dt: Fix128) -> Vec<(Vec3Fix, Vec3Fix, Fix128)> {
6074        let h2 = self.substep_dt(dt) * self.substep_dt(dt);
6075        self.contact_constraints
6076            .iter()
6077            .filter_map(|c| self.contact_force(c, h2))
6078            .collect()
6079    }
6080
6081    /// The length `h` of one substep of a frame of `dt`.
6082    fn substep_dt(&self, dt: Fix128) -> Fix128 {
6083        dt / Fix128::from_int(self.config.substeps.max(1) as i64)
6084    }
6085
6086    /// `(point, normal, force)` of one contact, or `None` when neither body can move.
6087    fn contact_force(
6088        &self,
6089        c: &ContactConstraint,
6090        h2: Fix128,
6091    ) -> Option<(Vec3Fix, Vec3Fix, Fix128)> {
6092        let w = self.bodies.get(c.body_a)?.inv_mass + self.bodies.get(c.body_b)?.inv_mass;
6093        if w < W_SUM_EPSILON || h2.is_zero() {
6094            return None;
6095        }
6096        Some((
6097            c.contact.point_a,
6098            c.contact.normal,
6099            c.cached_lambda / (w * h2),
6100        ))
6101    }
6102
6103    /// Arrows for the normal forces of the last step's contacts
6104    /// ([`contact_forces`](Self::contact_forces) through
6105    /// [`crate::contact_viz::generate_contact_arrows`]).
6106    #[must_use]
6107    pub fn contact_arrows(&self, dt: Fix128) -> Vec<crate::contact_viz::ContactArrow> {
6108        crate::contact_viz::generate_contact_arrows(&self.contact_forces(dt))
6109    }
6110
6111    /// Two tangential arrows of magnitude `μ·F` for each contact of the last step,
6112    /// with the friction coefficient of that contact (they differ between material
6113    /// pairs).
6114    #[must_use]
6115    pub fn contact_friction_arrows(&self, dt: Fix128) -> Vec<crate::contact_viz::ContactArrow> {
6116        let h2 = self.substep_dt(dt) * self.substep_dt(dt);
6117        self.contact_constraints
6118            .iter()
6119            .filter_map(|c| Some((self.contact_force(c, h2)?, c.friction)))
6120            .flat_map(|(force, mu)| crate::contact_viz::generate_friction_arrows(&[force], mu))
6121            .collect()
6122    }
6123
6124    /// A friction cone (half-angle `atan μ`, height the normal force) for each
6125    /// contact of the last step, with that contact's friction coefficient.
6126    #[must_use]
6127    pub fn contact_friction_cones(&self, dt: Fix128) -> Vec<crate::contact_viz::FrictionCone> {
6128        let h2 = self.substep_dt(dt) * self.substep_dt(dt);
6129        self.contact_constraints
6130            .iter()
6131            .filter_map(|c| Some((self.contact_force(c, h2)?, c.friction)))
6132            .flat_map(|(force, mu)| crate::contact_viz::generate_friction_cones(&[force], mu))
6133            .collect()
6134    }
6135
6136    /// Add an immovable collision surface — a plane, a height field or a triangle
6137    /// mesh — and return its index. See [`crate::static_collider`].
6138    pub fn add_static_collider(
6139        &mut self,
6140        collider: crate::static_collider::StaticCollider,
6141    ) -> usize {
6142        let idx = self.static_colliders.len();
6143        self.park_generation = self.park_generation.wrapping_add(1);
6144        self.static_colliders.push(collider);
6145        idx
6146    }
6147
6148    /// Remove a static collider by index, returning it, or `None` when the index
6149    /// is out of range. Later colliders shift down by one.
6150    pub fn remove_static_collider(
6151        &mut self,
6152        idx: usize,
6153    ) -> Option<crate::static_collider::StaticCollider> {
6154        if idx < self.static_colliders.len() {
6155            self.park_generation = self.park_generation.wrapping_add(1);
6156            Some(self.static_colliders.remove(idx))
6157        } else {
6158            None
6159        }
6160    }
6161
6162    /// Number of static colliders.
6163    #[must_use]
6164    pub fn static_collider_count(&self) -> usize {
6165        self.static_colliders.len()
6166    }
6167
6168    /// Resolve every non-static, non-sensor body's collision sphere against the
6169    /// static colliders, in order, pushing it out along each contact normal.
6170    ///
6171    /// The sphere is the body's collision radius, or the world's default
6172    /// ([`Self::set_sdf_collision_radius`]) for a body without one.
6173    ///
6174    /// `remove_approach`: also drop the part of the body's velocity that
6175    /// points into the surface (an inelastic contact). The XPBD substep
6176    /// passes `false` — its `update_velocities` turns the push into velocity
6177    /// — and the TGS path passes `true`, because TGS keeps the velocity it
6178    /// integrated and a pushed-out body would otherwise keep falling into
6179    /// the surface.
6180    fn resolve_static_collisions(&mut self, remove_approach: bool) {
6181        let default_radius = self.sdf_collision_radius;
6182        let count = self.stage_count();
6183        for k in 0..count {
6184            let i = self.stage_body(k);
6185            self.stage_work.resolution_bodies += 1;
6186            let body = &mut self.bodies[i];
6187            if body.is_static() || body.is_sensor {
6188                continue;
6189            }
6190            let radius = self
6191                .body_collision_radii
6192                .get(i)
6193                .and_then(|r| *r)
6194                .unwrap_or(default_radius);
6195            for collider in &self.static_colliders {
6196                if let Some(contact) = collider.collide_sphere(body.position, radius) {
6197                    body.position = body.position + contact.normal * contact.depth;
6198                    if remove_approach {
6199                        let vn = body.velocity.dot(contact.normal);
6200                        if vn < Fix128::ZERO {
6201                            body.velocity = body.velocity - contact.normal * vn;
6202                        }
6203                    }
6204                }
6205            }
6206        }
6207    }
6208
6209    /// Select the broad-phase. Every kind gives bit-identical simulations (see
6210    /// [`Broadphase`]); switching drops the persistent tree and the
6211    /// [`Broadphase::Hybrid`] layers, which are rebuilt on the next step if
6212    /// that kind is chosen.
6213    pub fn set_broadphase(&mut self, kind: Broadphase) {
6214        self.park_generation = self.park_generation.wrapping_add(1);
6215        self.broadphase = kind;
6216        self.broadphase_reset();
6217    }
6218
6219    /// The broad-phase in use.
6220    #[must_use]
6221    pub fn broadphase(&self) -> Broadphase {
6222        self.broadphase
6223    }
6224
6225    /// What the [`Broadphase::DynamicTree`] tree holds: how many bodies have a
6226    /// proxy and how tall the tree is. Both are 0 until a step has run with that
6227    /// broad-phase.
6228    #[must_use]
6229    pub fn broadphase_stats(&self) -> BroadphaseStats {
6230        BroadphaseStats {
6231            proxies: self.broadphase_tree.proxy_count(),
6232            height: self.broadphase_tree.height(),
6233        }
6234    }
6235
6236    /// The fattened box the [`Broadphase::DynamicTree`] tree keeps for a body, or
6237    /// `None` when the body has no proxy (no collision radius, or no step yet).
6238    #[must_use]
6239    pub fn broadphase_proxy_aabb(&self, body_idx: usize) -> Option<AABB> {
6240        let proxy = (*self.broadphase_proxies.get(body_idx)?)?;
6241        if self.broadphase_tree.user_data(proxy) as usize != body_idx {
6242            return None;
6243        }
6244        Some(self.broadphase_tree.get_aabb(proxy))
6245    }
6246
6247    /// Forget the persistent tree and the hybrid layers: the next
6248    /// [`Broadphase::DynamicTree`] or [`Broadphase::Hybrid`] step rebuilds them
6249    /// from the bodies. Called when the broad-phase is switched.
6250    fn broadphase_reset(&mut self) {
6251        self.broadphase_tree = crate::dynamic_bvh::DynamicAabbTree::new();
6252        self.broadphase_proxies.clear();
6253        self.broadphase_hybrid = BroadphaseHybrid::new();
6254    }
6255
6256    /// The box the broad-phase keeps for body `i`, whose collision radius is
6257    /// `radius`: the closed-form world box of its collider when it carries one
6258    /// ([`crate::body_collider::BodyCollider::world_aabb`]), clipped to the cube
6259    /// of the collision sphere, otherwise that cube. Both contain the body's
6260    /// position, so the clip is never empty. Every pair the narrow-phase can
6261    /// report has overlapping boxes: a pair involving a collider is decided by
6262    /// GJK on the collider (and on the plain side's sphere), which lies inside
6263    /// these boxes.
6264    fn broadphase_box(&self, i: usize, radius: Fix128) -> AABB {
6265        let pos = self.bodies[i].position;
6266        let cube = AABB::from_center_half(pos, Vec3Fix::new(radius, radius, radius));
6267        match self.body_colliders.get(i).and_then(Option::as_ref) {
6268            Some(collider) => {
6269                let tight = collider.world_aabb(pos, self.bodies[i].rotation);
6270                let max = |x: Fix128, y: Fix128| if x > y { x } else { y };
6271                let min = |x: Fix128, y: Fix128| if x < y { x } else { y };
6272                AABB {
6273                    min: Vec3Fix::new(
6274                        max(tight.min.x, cube.min.x),
6275                        max(tight.min.y, cube.min.y),
6276                        max(tight.min.z, cube.min.z),
6277                    ),
6278                    max: Vec3Fix::new(
6279                        min(tight.max.x, cube.max.x),
6280                        min(tight.max.y, cube.max.y),
6281                        min(tight.max.z, cube.max.z),
6282                    ),
6283                }
6284            }
6285            None => cube,
6286        }
6287    }
6288
6289    /// The sorted candidate pairs of bodies whose boxes may overlap, from the
6290    /// primitives (body index + tight box) of every body with a collision radius.
6291    fn broadphase_pairs(&mut self, primitives: Vec<BvhPrimitive>) -> Vec<(u32, u32)> {
6292        match self.broadphase {
6293            Broadphase::Bvh => LinearBvh::build(primitives).find_pairs(),
6294            Broadphase::DynamicTree => {
6295                let n = self.bodies.len();
6296                // A proxy belongs to a body *index* (its user data), and re-boxing it
6297                // each step follows whichever body now has that index, so removing
6298                // or restoring bodies needs no rebuild; only the proxies of indices
6299                // that no longer exist have to go.
6300                for proxy in self
6301                    .broadphase_proxies
6302                    .drain(n.min(self.broadphase_proxies.len())..)
6303                    .flatten()
6304                {
6305                    self.broadphase_tree.remove(proxy);
6306                }
6307                self.broadphase_proxies.resize(n, None);
6308                let mut present = vec![false; n];
6309                for p in &primitives {
6310                    let i = p.index as usize;
6311                    present[i] = true;
6312                    match self.broadphase_proxies[i] {
6313                        Some(proxy) => {
6314                            self.broadphase_tree.update(proxy, p.aabb);
6315                        }
6316                        None => {
6317                            self.broadphase_proxies[i] =
6318                                Some(self.broadphase_tree.insert(p.aabb, p.index));
6319                        }
6320                    }
6321                }
6322                // A body that lost its collision radius leaves the tree.
6323                for (i, here) in present.iter().enumerate() {
6324                    if !here {
6325                        if let Some(proxy) = self.broadphase_proxies[i].take() {
6326                            self.broadphase_tree.remove(proxy);
6327                        }
6328                    }
6329                }
6330                self.broadphase_tree.find_pairs()
6331            }
6332            Broadphase::Hybrid => {
6333                let hybrid = &mut self.broadphase_hybrid;
6334                hybrid.clear_dynamic();
6335                for p in &primitives {
6336                    let is_static = self.bodies[p.index as usize].is_static();
6337                    hybrid.insert_dynamic(p.index, p.aabb, is_static);
6338                }
6339                hybrid.build_dynamic();
6340                let mut pairs = Vec::new();
6341                self.stage_work.broadphase_box_tests += hybrid.query_pairs(&mut pairs);
6342                pairs
6343            }
6344        }
6345    }
6346
6347    /// The sorted candidate pairs while parking is in effect: the pairs among the
6348    /// unparked `primitives` from a [`LinearBvh`] over them, plus every parked
6349    /// body whose proxy overlaps one of them, from `park.tree`. Pairs of two
6350    /// parked bodies are left out; the narrow-phase drops pairs of two sleeping
6351    /// bodies anyway, so the contacts and their order are those of
6352    /// [`Self::broadphase_pairs`] over every body.
6353    fn parked_broadphase_pairs(&mut self, primitives: Vec<BvhPrimitive>) -> Vec<(u32, u32)> {
6354        let mut pairs = Vec::new();
6355        for p in &primitives {
6356            let i = p.index;
6357            self.park.tree.query_callback(&p.aabb, |j| {
6358                pairs.push(if i < j { (i, j) } else { (j, i) });
6359            });
6360        }
6361        if primitives.len() >= 2 {
6362            pairs.extend(LinearBvh::build(primitives).find_pairs());
6363        }
6364        pairs.sort_unstable();
6365        pairs.dedup();
6366        pairs
6367    }
6368
6369    // ── Automatic Collision Detection ─────────────────────────────────
6370
6371    /// Detect collisions between bodies with collision radii.
6372    ///
6373    /// The broad-phase boxes are the collider's tight world box for a body that
6374    /// carries one and the cube of the collision sphere otherwise; the
6375    /// narrow-phase is exact sphere-sphere for two plain bodies and GJK/EPA for a
6376    /// pair involving a collider (the plain side as its sphere).
6377    /// Generates contact constraints and events automatically.
6378    /// Bodies without a collision radius are skipped.
6379    #[allow(clippy::too_many_lines, clippy::items_after_statements)]
6380    fn detect_collisions(&mut self) {
6381        let n = self.bodies.len();
6382        if n < 2 {
6383            return;
6384        }
6385
6386        // Build BVH from bodies that have collision radii (the unparked ones while
6387        // parking is in effect: parked bodies sit in `park.tree`)
6388        let mut primitives = Vec::new();
6389        let count = self.stage_count();
6390        for k in 0..count {
6391            let i = self.stage_body(k);
6392            if let Some(radius) = self.body_collision_radii.get(i).and_then(|r| *r) {
6393                primitives.push(BvhPrimitive {
6394                    aabb: self.broadphase_box(i, radius),
6395                    index: i as u32,
6396                    morton: 0,
6397                });
6398            }
6399        }
6400        self.stage_work.broadphase_primitives += primitives.len() as u64;
6401
6402        let parked_with_radius = if self.park.active {
6403            self.park.proxy_live
6404        } else {
6405            0
6406        };
6407        if primitives.len() + parked_with_radius < 2 {
6408            return;
6409        }
6410
6411        let pairs = if self.park.active {
6412            self.parked_broadphase_pairs(primitives)
6413        } else {
6414            self.broadphase_pairs(primitives)
6415        };
6416        self.stage_work.broadphase_pairs += pairs.len() as u64;
6417
6418        // Velocity of a body as the pre-skip step saw it here: a parked body
6419        // still carries the force-field kick of phase 1 in the first substep
6420        // (the velocity derivation zeroes it at the end of that substep).
6421        let velocity_of = |i: usize| -> Vec3Fix {
6422            let body = &self.bodies[i];
6423            if self.park.first_substep
6424                && !self.force_fields.is_empty()
6425                && self.park.is_parked(i)
6426                && !body.is_static()
6427            {
6428                crate::force::force_field_velocity(&self.force_fields, i, body, self.park.frame_dt)
6429            } else {
6430                body.velocity
6431            }
6432        };
6433
6434        // Collect results to avoid borrow conflicts
6435        struct ContactInfo {
6436            body_a: usize,
6437            body_b: usize,
6438            contact: Contact,
6439            normal: Vec3Fix,
6440            point: Vec3Fix,
6441            depth: Fix128,
6442            rel_vel: Fix128,
6443            is_sensor: bool,
6444        }
6445        let mut results: Vec<ContactInfo> = Vec::new();
6446        let mut radius_overflow = false;
6447
6448        for (a32, b32) in pairs {
6449            let a = a32 as usize;
6450            let b = b32 as usize;
6451            if a >= n || b >= n {
6452                continue;
6453            }
6454
6455            // Sphere-sphere narrow phase (safe indexing for deserialization robustness)
6456            let radius_a = self
6457                .body_collision_radii
6458                .get(a)
6459                .and_then(|r| *r)
6460                .unwrap_or(Fix128::ZERO);
6461            let radius_b = self
6462                .body_collision_radii
6463                .get(b)
6464                .and_then(|r| *r)
6465                .unwrap_or(Fix128::ZERO);
6466
6467            // Contact normal points from B to A (`Contact::normal` contract, same
6468            // as the EPA / SDF paths and what `solve_contact_constraints` /
6469            // `update_velocities` assume: A is moved along +n, B along -n).
6470            // Before 1.2.0 this path used `pos_b - pos_a` (A → B), so every
6471            // sphere-sphere contact pushed the bodies *into* each other and a
6472            // plain head-on collision accelerated both bodies (4 m/s → 114 m/s
6473            // in 4 frames). `tests/analytic_physics.rs::head_on_collision_*`.
6474            let delta = self.bodies[a].position - self.bodies[b].position;
6475            // 半径和が `|·| ≥ 2⁶³` だと wrap して負になり、どの距離とも比べられない
6476            // (`velocity_of` が `self` を借りているので、印は loop の後で立てる)
6477            let Some(combined_radius) = radius_a.checked_add(radius_b) else {
6478                radius_overflow = true;
6479                continue;
6480            };
6481            // Squared-distance early out *first*: the BVH candidate set is a
6482            // superset of the overlapping pairs (49k candidates for 2.7k contacts
6483            // on the 1000-sphere grid) and the quantised leaf AABBs cannot be
6484            // tightened without a BVH API change, so every candidate pays only
6485            // 4 checked multiplies + 2 checked adds + a compare here; the filter / static / sleep lookups
6486            // and the sqrt + 3 divisions of `normalize_with_length` run only for
6487            // real overlaps. Same `dist < combined_radius` decision (both sides
6488            // exact for |delta| < 2^31), same contact order.
6489            //
6490            // `|delta|` か半径和が `2³¹·⁵ ≈ 3.04e9` 以上だと 2 乗が wrap して
6491            // 重なりを見逃す (両辺が黙って別の値になる) 2 乗がどちらも範囲内
6492            // なら従来の比較のまま (bit 不変)、どちらかが範囲外の対だけ 2 乗を
6493            // 経ない長さで比べ、法線と距離もその長さから作る (`scaled`)
6494            let mut scaled: Option<(Vec3Fix, Fix128)> = None;
6495            // A pair involving a collider is decided by GJK on the collider, with
6496            // the other body's collider or, for a plain body, its sphere; such a
6497            // pair can overlap with coincident centres, where the sphere path
6498            // cannot give a normal. Its early out is the overlap of the two
6499            // broad-phase boxes (the collider's tight box, the plain body's
6500            // sphere cube), which contain everything GJK looks at.
6501            let collider_a = self.body_colliders.get(a).and_then(Option::as_ref);
6502            let collider_b = self.body_colliders.get(b).and_then(Option::as_ref);
6503            if collider_a.is_none() && collider_b.is_none() {
6504                let squares = delta
6505                    .checked_length_squared()
6506                    .zip(combined_radius.checked_mul(combined_radius));
6507                match squares {
6508                    Some((d2, r2)) => {
6509                        if d2 >= r2 || d2.is_zero() {
6510                            continue;
6511                        }
6512                    }
6513                    None => {
6514                        // 中心距離が `≥ 2⁶³` (表せない) なら、どの半径和
6515                        // (`< 2⁶³`、上で検査済) とも重ならない
6516                        let Some(dist) = delta.checked_length_scaled() else {
6517                            continue;
6518                        };
6519                        if dist >= combined_radius || dist.is_zero() {
6520                            continue;
6521                        }
6522                        let Some(normal) = delta.try_normalize_scaled() else {
6523                            continue;
6524                        };
6525                        scaled = Some((normal, dist));
6526                    }
6527                }
6528            } else if !self
6529                .broadphase_box(a, radius_a)
6530                .intersects(&self.broadphase_box(b, radius_b))
6531            {
6532                continue;
6533            }
6534
6535            // Filter check
6536            let filter_a = self
6537                .body_filters
6538                .get(a)
6539                .copied()
6540                .unwrap_or(CollisionFilter::DEFAULT);
6541            let filter_b = self
6542                .body_filters
6543                .get(b)
6544                .copied()
6545                .unwrap_or(CollisionFilter::DEFAULT);
6546            if !CollisionFilter::can_collide(&filter_a, &filter_b) {
6547                continue;
6548            }
6549
6550            // Skip static-static
6551            if self.bodies[a].is_static() && self.bodies[b].is_static() {
6552                continue;
6553            }
6554
6555            // Skip if both sleeping
6556            if self.islands.is_sleeping(a) && self.islands.is_sleeping(b) {
6557                continue;
6558            }
6559
6560            // The boxes overlap, which is necessary but not sufficient for the
6561            // solids to: let the shapes decide.
6562            if collider_a.is_some() || collider_b.is_some() {
6563                let pose_a = (self.bodies[a].position, self.bodies[a].rotation);
6564                let pose_b = (self.bodies[b].position, self.bodies[b].rotation);
6565                let hit = match (collider_a, collider_b) {
6566                    (Some(ca), Some(cb)) => {
6567                        crate::body_collider::contact_between(ca, pose_a, cb, pose_b)
6568                    }
6569                    (Some(ca), None) => crate::body_collider::contact_with_sphere(
6570                        ca,
6571                        pose_a,
6572                        crate::collider::Sphere::new(pose_b.0, radius_b),
6573                        true,
6574                    ),
6575                    (None, Some(cb)) => crate::body_collider::contact_with_sphere(
6576                        cb,
6577                        pose_b,
6578                        crate::collider::Sphere::new(pose_a.0, radius_a),
6579                        false,
6580                    ),
6581                    (None, None) => None,
6582                };
6583                if let Some(contact) = hit {
6584                    if contact.depth > Fix128::ZERO {
6585                        let rel_vel = (velocity_of(a) - velocity_of(b)).dot(contact.normal);
6586                        results.push(ContactInfo {
6587                            body_a: a,
6588                            body_b: b,
6589                            contact,
6590                            normal: contact.normal,
6591                            point: contact.point_a,
6592                            depth: contact.depth,
6593                            rel_vel,
6594                            is_sensor: self.bodies[a].is_sensor || self.bodies[b].is_sensor,
6595                        });
6596                    }
6597                }
6598                continue;
6599            }
6600
6601            let (normal, dist) = scaled.unwrap_or_else(|| delta.normalize_with_length());
6602
6603            if dist < combined_radius && !dist.is_zero() {
6604                let depth = combined_radius - dist;
6605                let point_a = self.bodies[a].position - normal * radius_a;
6606                let point_b = self.bodies[b].position + normal * radius_b;
6607                // Approach speed along the normal: negative while closing.
6608                let rel_vel = (velocity_of(a) - velocity_of(b)).dot(normal);
6609                let is_sensor = self.bodies[a].is_sensor || self.bodies[b].is_sensor;
6610
6611                let contact = Contact {
6612                    depth,
6613                    normal,
6614                    point_a,
6615                    point_b,
6616                };
6617
6618                results.push(ContactInfo {
6619                    body_a: a,
6620                    body_b: b,
6621                    contact,
6622                    normal,
6623                    point: point_a,
6624                    depth,
6625                    rel_vel,
6626                    is_sensor,
6627                });
6628            }
6629        }
6630
6631        if radius_overflow {
6632            self.note_rigid_overflow();
6633        }
6634
6635        // Apply results
6636        for info in results {
6637            // Report contact event
6638            self.events.report_contact(
6639                info.body_a,
6640                info.body_b,
6641                info.normal,
6642                info.point,
6643                info.depth,
6644                info.rel_vel,
6645            );
6646
6647            // Wake a *sleeping* island touched by this contact. Waking
6648            // unconditionally (pre-1.2.0) reset `idle_frames` of every body in
6649            // resting contact on every detection, so a stack could never fall
6650            // asleep, and `wake_island` walks all bodies — O(contacts × bodies)
6651            // per detection (3.7 ms of a 6 ms step at 2700 contacts / 1000 bodies).
6652            for x in [info.body_a, info.body_b] {
6653                if !self.islands.is_sleeping(x) {
6654                    continue;
6655                }
6656                if self.park.is_parked(x) {
6657                    // Parked bodies are on no joint, so their island is
6658                    // themselves: same outcome as `wake_island`, without its
6659                    // walk over every body.
6660                    self.islands.wake_body(x);
6661                    self.park.unpark(x, &mut self.stage_work);
6662                } else {
6663                    self.islands.wake_island(x);
6664                }
6665            }
6666
6667            if info.is_sensor {
6668                self.events.report_trigger(info.body_a, info.body_b);
6669            } else {
6670                self.add_contact_with_material(info.body_a, info.body_b, info.contact);
6671            }
6672        }
6673    }
6674
6675    /// Get body by index
6676    #[inline]
6677    #[must_use]
6678    pub fn get_body(&self, idx: usize) -> Option<&RigidBody> {
6679        self.bodies.get(idx)
6680    }
6681
6682    /// Get mutable body by index
6683    ///
6684    /// A rotation written through the returned reference is not normalized
6685    /// until the next step: a non-unit quaternion `q` scales what it rotates
6686    /// by `|q|^2` in the queries made before then (see [`RigidBody::rotation`]).
6687    #[inline]
6688    pub fn get_body_mut(&mut self, idx: usize) -> Option<&mut RigidBody> {
6689        self.bodies.get_mut(idx)
6690    }
6691
6692    /// 範囲外の積が起きた step があったか (sticky、WM-01 / doctrine B-12)
6693    ///
6694    /// `true` は **この world の軌道が信用できない** ことを意味する
6695    /// doctrine §3 の 3 値で言えば `undecided` — 探索では「この枝は未決定」
6696    ///
6697    /// # なぜ値ベースの検出器では足りないか
6698    ///
6699    /// `Fix128::mul` の積が範囲外で 2 の冪だと `hi` が厳密に 0 になり
6700    /// (`2⁴⁰ × 2⁴⁰`)、`position += velocity*dt` が 0 加算 →
6701    /// `update_velocities` が速度も 0 に上書き →
6702    /// **「原点で完全に静止した自己整合な状態」** に落ちる
6703    /// ⚠️ **静止は物理的に妥当なのでどの不変条件でも red にならない**
6704    ///
6705    /// # sticky である理由
6706    ///
6707    /// 1 度立ったら `step` を重ねても落ちない 落ちると探索が
6708    /// 「未決定だった枝」を後の step で決定済と誤認する
6709    ///
6710    /// ⚠️ **flag は状態の一部**なので [`Self::serialize_state`] の被覆に
6711    /// 入れる必要がある (入れないと巻き戻した先で `undecided` が消える、
6712    /// doctrine B-12 の指摘) — **format v2 で収録済** (world ごと 1 byte、
6713    /// [`Self::STATE_VERSION`] 参照)
6714    #[must_use]
6715    pub const fn overflow_detected(&self) -> bool {
6716        self.overflow_detected
6717    }
6718
6719    /// 剛体の経路が範囲外の値を踏んだことを [`Self::overflow_detected`] に立てる
6720    ///
6721    /// sticky flag を立てるのはこの関数だけ (積分の `v·dt` と位置の加算、
6722    /// 速度導出、球の接触の距離と半径和、角速度の大きさ、TGS の積分、
6723    /// TGS の関節投影の速度への繰り越し) 落とす経路は [`Self::reset_world`] と
6724    /// 状態の復元だけ
6725    #[inline]
6726    pub(crate) fn note_rigid_overflow(&mut self) {
6727        self.overflow_detected = true;
6728    }
6729
6730    /// [`Self::serialize_state`] blob の magic
6731    ///
6732    /// ⚠️ 旧 format は先頭が body 数だったので、magic を検査しないと
6733    /// **新実装が旧 blob を誤って解釈する**
6734    pub const STATE_MAGIC: [u8; 4] = *b"APHY";
6735
6736    /// [`Self::serialize_state`] blob の format version
6737    ///
6738    /// v1 = header 12 + body ごと 208 + body ごと sleep 5
6739    /// v2 = v1 + **world ごと overflow flag 1 byte** (doctrine B-12)
6740    /// v3 = v2 + **world ごと population fingerprint 8 byte**
6741    /// ([`Self::population_fingerprint`]、rollback の body 数不一致検査強化)
6742    ///
6743    /// 被覆を足す時はここを上げ、[`Self::deserialize_state`] で分岐する
6744    /// (不一致を silent に読み替えない — 旧 version の blob は `false` で拒否、
6745    /// v1 / v2 の blob は v3 実装に対して version 不一致で拒否される、
6746    /// 既存の v1→v2 bump と同じ方針)
6747    pub const STATE_VERSION: u16 = 3;
6748
6749    /// Serialize world state (for rollback netcode).
6750    ///
6751    /// # 被覆 (v1、2026-09-30)
6752    ///
6753    /// | 範囲 | 内容 |
6754    /// |---|---|
6755    /// | `[0..4)` | magic [`Self::STATE_MAGIC`] (`b"APHY"`) |
6756    /// | `[4..6)` | version u16 = [`Self::STATE_VERSION`] |
6757    /// | `[6..8)` | reserved u16 = 0 (将来の flag 用、8 byte 境界揃え) |
6758    /// | `[8..12)` | body 数 u32 |
6759    /// | `[12..)` | body ごとに 208 byte = position 48 + velocity 48 + rotation 64 + angular_velocity 48 |
6760    /// | 続き | body ごとに 5 byte = [`SleepState`] u8 + `idle_frames` u32 |
6761    /// | 続き | world ごと overflow flag 1 byte (v2) |
6762    /// | 続き | world ごと population fingerprint u64 (v3、[`Self::population_fingerprint`]) |
6763    ///
6764    /// ⚠️ **`SleepState` + `idle_frames` を v1 で被覆に入れた** (WM-08)
6765    /// 旧 format は body の運動状態だけを持ち、`deserialize_state` 末尾の
6766    /// `IslandManager::new` が sleep 状態を 0 に戻していたため、
6767    /// **巻き戻した先で眠るタイミングがずれて軌道が割れた**
6768    /// (`tests/wm08_state_coverage.rs` が対照実験付きで red を実測してから実装)
6769    ///
6770    /// ⚠️ **magic と version を入れた理由**: 旧 blob は先頭が body 数なので、
6771    /// 検査しないと**新実装が旧 blob を誤って解釈する** magic 不一致 /
6772    /// version 不一致は `deserialize_state` が `false` を返して明示的に拒否する
6773    ///
6774    /// 依然として保存 (= 復元) しないもの: constraints / joints / force
6775    /// fields / collision radii / filters / materials (rollback netcode
6776    /// では game state から毎 frame 再構築される前提) ⚠️ **v3 の population
6777    /// fingerprint はこれらの値を検査にのみ使う** (復元はしない) ので
6778    /// 「保存しない」契約自体は変わらない
6779    ///
6780    /// ⚠️ [`crate::netcode::SimulationChecksum`] は **本 blob の全 byte から
6781    /// 導出**される (被覆の単一源化、B-13) ので、ここに state を足すと
6782    /// checksum も自動で追従する
6783    #[must_use]
6784    pub fn serialize_state(&self) -> Vec<u8> {
6785        let mut data = Vec::new();
6786
6787        // Header: magic + version + reserved
6788        data.extend_from_slice(&Self::STATE_MAGIC);
6789        data.extend_from_slice(&Self::STATE_VERSION.to_le_bytes());
6790        data.extend_from_slice(&0u16.to_le_bytes());
6791
6792        // Body count
6793        let count = self.bodies.len() as u32;
6794        data.extend_from_slice(&count.to_le_bytes());
6795
6796        // Body states
6797        for body in &self.bodies {
6798            // Position (3 x Fix128 = 48 bytes)
6799            data.extend_from_slice(&body.position.x.hi.to_le_bytes());
6800            data.extend_from_slice(&body.position.x.lo.to_le_bytes());
6801            data.extend_from_slice(&body.position.y.hi.to_le_bytes());
6802            data.extend_from_slice(&body.position.y.lo.to_le_bytes());
6803            data.extend_from_slice(&body.position.z.hi.to_le_bytes());
6804            data.extend_from_slice(&body.position.z.lo.to_le_bytes());
6805
6806            // Velocity (3 x Fix128 = 48 bytes)
6807            data.extend_from_slice(&body.velocity.x.hi.to_le_bytes());
6808            data.extend_from_slice(&body.velocity.x.lo.to_le_bytes());
6809            data.extend_from_slice(&body.velocity.y.hi.to_le_bytes());
6810            data.extend_from_slice(&body.velocity.y.lo.to_le_bytes());
6811            data.extend_from_slice(&body.velocity.z.hi.to_le_bytes());
6812            data.extend_from_slice(&body.velocity.z.lo.to_le_bytes());
6813
6814            // Rotation (4 x Fix128 = 64 bytes)
6815            data.extend_from_slice(&body.rotation.x.hi.to_le_bytes());
6816            data.extend_from_slice(&body.rotation.x.lo.to_le_bytes());
6817            data.extend_from_slice(&body.rotation.y.hi.to_le_bytes());
6818            data.extend_from_slice(&body.rotation.y.lo.to_le_bytes());
6819            data.extend_from_slice(&body.rotation.z.hi.to_le_bytes());
6820            data.extend_from_slice(&body.rotation.z.lo.to_le_bytes());
6821            data.extend_from_slice(&body.rotation.w.hi.to_le_bytes());
6822            data.extend_from_slice(&body.rotation.w.lo.to_le_bytes());
6823
6824            // Angular velocity (3 x Fix128 = 48 bytes)
6825            data.extend_from_slice(&body.angular_velocity.x.hi.to_le_bytes());
6826            data.extend_from_slice(&body.angular_velocity.x.lo.to_le_bytes());
6827            data.extend_from_slice(&body.angular_velocity.y.hi.to_le_bytes());
6828            data.extend_from_slice(&body.angular_velocity.y.lo.to_le_bytes());
6829            data.extend_from_slice(&body.angular_velocity.z.hi.to_le_bytes());
6830            data.extend_from_slice(&body.angular_velocity.z.lo.to_le_bytes());
6831        }
6832
6833        // Sleep 状態 (body ごとに 5 byte、WM-08)
6834        //
6835        // ⚠️ `sleep_data` は body 数に合わせて resize されている前提だが、
6836        // 短い場合は既定 (Awake / 0) で埋める — blob の長さを body 数だけで
6837        // 決められるようにして、読み側の長さ検査を単純に保つ
6838        for i in 0..self.bodies.len() {
6839            let sd = self
6840                .islands
6841                .sleep_data
6842                .get(i)
6843                .copied()
6844                .unwrap_or_else(SleepData::new);
6845            data.push(match sd.state {
6846                SleepState::Awake => 0,
6847                SleepState::Sleeping => 1,
6848            });
6849            data.extend_from_slice(&sd.idle_frames.to_le_bytes());
6850        }
6851
6852        // overflow flag (world ごと 1 byte、v2、doctrine B-12)
6853        //
6854        // ⚠️ **flag は状態の一部** — blob に入れないと overflow した枝を
6855        // 巻き戻した先で `undecided` が消えて B-12 が目的を達成しない
6856        data.push(u8::from(self.overflow_detected));
6857
6858        // population fingerprint (world ごと 8 byte、v3)
6859        //
6860        // ⚠️ **位置 / 速度 / 回転 / sleep は含めない** ([`Self::population_fingerprint`]
6861        // 参照) — blob 自身が復元する量なので、ここに混ぜると検査が
6862        // 「基準が内側」になり population 不一致を検出できなくなる
6863        data.extend_from_slice(&self.population_fingerprint().to_le_bytes());
6864
6865        data
6866    }
6867
6868    /// FNV-1a 64-bit hash of per-body quantities [`Self::serialize_state`]
6869    /// does **not** restore (shape/mass/inertia/collider/filter/material),
6870    /// in body index order.
6871    ///
6872    /// # なぜこれが要るか (rollback body 数不一致、gap #3)
6873    ///
6874    /// [`Self::deserialize_state`] は body **数**の一致しか見ていない
6875    /// `remove_body` は `swap_remove` なので、count が元に戻っても
6876    /// index↔body の対応 (= population) が変わっていることがある —
6877    /// その場合 count 一致だけでは検出できず、**別の body に誤った状態を
6878    /// 書き込んで silent に通る** (fail-fast になっていない)
6879    ///
6880    /// ⚠️ **位置 / 速度 / 回転 / sleep を含めない** — それらは blob 自身が
6881    /// 復元する量なので、ここに混ぜると「自分で組んだ量で自分を検査する」
6882    /// 循環になり population 不一致を検出できなくなる
6883    #[must_use]
6884    pub fn population_fingerprint(&self) -> u64 {
6885        let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
6886        for i in 0..self.bodies.len() {
6887            let body = &self.bodies[i];
6888            fnv1a_fold(&mut hash, &body.inv_mass.hi.to_le_bytes());
6889            fnv1a_fold(&mut hash, &body.inv_mass.lo.to_le_bytes());
6890            fnv1a_fold(&mut hash, &body.inv_inertia.x.hi.to_le_bytes());
6891            fnv1a_fold(&mut hash, &body.inv_inertia.x.lo.to_le_bytes());
6892            fnv1a_fold(&mut hash, &body.inv_inertia.y.hi.to_le_bytes());
6893            fnv1a_fold(&mut hash, &body.inv_inertia.y.lo.to_le_bytes());
6894            fnv1a_fold(&mut hash, &body.inv_inertia.z.hi.to_le_bytes());
6895            fnv1a_fold(&mut hash, &body.inv_inertia.z.lo.to_le_bytes());
6896            fnv1a_fold(&mut hash, &[body.body_type as u8]);
6897            fnv1a_fold(&mut hash, &[u8::from(body.is_sensor)]);
6898            match self.body_collision_radii.get(i).copied().flatten() {
6899                Some(r) => {
6900                    fnv1a_fold(&mut hash, &[1u8]);
6901                    fnv1a_fold(&mut hash, &r.hi.to_le_bytes());
6902                    fnv1a_fold(&mut hash, &r.lo.to_le_bytes());
6903                }
6904                None => fnv1a_fold(&mut hash, &[0u8]),
6905            }
6906            let filter = self.body_filter(i);
6907            fnv1a_fold(&mut hash, &filter.layer.to_le_bytes());
6908            fnv1a_fold(&mut hash, &filter.mask.to_le_bytes());
6909            fnv1a_fold(&mut hash, &filter.group.to_le_bytes());
6910            let material = self
6911                .body_materials
6912                .get(i)
6913                .copied()
6914                .unwrap_or(crate::material::DEFAULT_MATERIAL);
6915            fnv1a_fold(&mut hash, &material.to_le_bytes());
6916        }
6917        hash
6918    }
6919
6920    /// Deserialize world state (for rollback netcode).
6921    ///
6922    /// Restores per-body transforms. The rotations are restored as stored
6923    /// and brought to unit length by the next step, so a non-unit rotation in
6924    /// `data` scales what it rotates by `|q|^2` in the queries made before
6925    /// that step (see [`RigidBody::rotation`]). Parallel arrays (collision radii,
6926    /// filters, materials, island manager) are resized to match the body
6927    /// count, preserving existing entries and zero-filling new ones.
6928    ///
6929    /// # Rollback の契約 (gap #3、2026-10-02)
6930    ///
6931    /// body **数**が一致していても **population** (どの body がどんな
6932    /// `inv_mass` / `inv_inertia` / collider / filter / material を持つか)
6933    /// が一致していなければ拒否する ([`Self::population_fingerprint`]、v3)
6934    /// caller は body の生成・破棄 (`add_body` / `remove_body`) と joints /
6935    /// force fields / filters / collider radii / materials の変更を、目標
6936    /// frame まで決定論的に replay してから本関数を呼ぶこと ⚠️
6937    /// **joints の再構築は本関数の呼び出し前に行う** — 本関数の末尾で
6938    /// island を joints から作り直すため、順序が逆だと sleep の島が
6939    /// 異なる形になる
6940    ///
6941    /// 不一致 (`false`) の場合、`self` は一切変更されない (すべての検査は
6942    /// body 状態を書き込む前に完了する)
6943    pub fn deserialize_state(&mut self, data: &[u8]) -> bool {
6944        // Header 12 byte: magic 4 + version 2 + reserved 2 + count 4
6945        if data.len() < 12 {
6946            return false;
6947        }
6948        // ⚠️ magic / version を検査しないと **旧 blob (先頭が body 数) を
6949        // 新 format として誤って解釈する** 不一致は明示的に拒否する
6950        if data[0..4] != Self::STATE_MAGIC {
6951            return false;
6952        }
6953        if u16::from_le_bytes([data[4], data[5]]) != Self::STATE_VERSION {
6954            return false;
6955        }
6956        // ⚠️ reserved も検査する — 検査しないと **blob の中に silent に
6957        // 無視される byte が残り**、「1 byte でも壊れたらどれかの field に
6958        // 反映される」という不変条件に穴が空く (将来 flag に使う時は
6959        // `STATE_VERSION` を上げて分岐する)
6960        if u16::from_le_bytes([data[6], data[7]]) != 0 {
6961            return false;
6962        }
6963
6964        let count = u32::from_le_bytes([data[8], data[9], data[10], data[11]]) as usize;
6965
6966        if count != self.bodies.len() {
6967            return false;
6968        }
6969
6970        // 長さ検査は body 数から一意に決まる (header + 運動状態 + sleep + overflow + fingerprint)
6971        let expected = 12 + count * 208 + count * 5 + 1 + 8;
6972        if data.len() < expected {
6973            return false;
6974        }
6975
6976        // ⚠️ **population fingerprint は body 状態を 1 byte も書く前に検査する**
6977        // (v3、gap #3) count が一致していても `swap_remove` + `add_body` で
6978        // population (= inv_mass / inertia / collider 等の組み合わせ) が
6979        // 変わっていれば拒否する — ここで return すれば self は未変更のまま
6980        let fp_offset = expected - 8;
6981        let stored_fingerprint = u64::from_le_bytes([
6982            data[fp_offset],
6983            data[fp_offset + 1],
6984            data[fp_offset + 2],
6985            data[fp_offset + 3],
6986            data[fp_offset + 4],
6987            data[fp_offset + 5],
6988            data[fp_offset + 6],
6989            data[fp_offset + 7],
6990        ]);
6991        if stored_fingerprint != self.population_fingerprint() {
6992            return false;
6993        }
6994
6995        // Per-body: position(48) + velocity(48) + rotation(64) + angular_velocity(48) = 208 bytes
6996        let mut offset = 12;
6997        for body in &mut self.bodies {
6998            if offset + 208 > data.len() {
6999                return false;
7000            }
7001
7002            // Helper to read Fix128
7003            let read_fix128 = |o: &mut usize| -> Fix128 {
7004                let hi = i64::from_le_bytes([
7005                    data[*o],
7006                    data[*o + 1],
7007                    data[*o + 2],
7008                    data[*o + 3],
7009                    data[*o + 4],
7010                    data[*o + 5],
7011                    data[*o + 6],
7012                    data[*o + 7],
7013                ]);
7014                *o += 8;
7015                let lo = u64::from_le_bytes([
7016                    data[*o],
7017                    data[*o + 1],
7018                    data[*o + 2],
7019                    data[*o + 3],
7020                    data[*o + 4],
7021                    data[*o + 5],
7022                    data[*o + 6],
7023                    data[*o + 7],
7024                ]);
7025                *o += 8;
7026                Fix128 { hi, lo }
7027            };
7028
7029            body.position.x = read_fix128(&mut offset);
7030            body.position.y = read_fix128(&mut offset);
7031            body.position.z = read_fix128(&mut offset);
7032
7033            body.velocity.x = read_fix128(&mut offset);
7034            body.velocity.y = read_fix128(&mut offset);
7035            body.velocity.z = read_fix128(&mut offset);
7036
7037            body.rotation.x = read_fix128(&mut offset);
7038            body.rotation.y = read_fix128(&mut offset);
7039            body.rotation.z = read_fix128(&mut offset);
7040            body.rotation.w = read_fix128(&mut offset);
7041
7042            body.angular_velocity.x = read_fix128(&mut offset);
7043            body.angular_velocity.y = read_fix128(&mut offset);
7044            body.angular_velocity.z = read_fix128(&mut offset);
7045        }
7046
7047        // Resync parallel arrays to match body count (extend AND truncate)
7048        let n = self.bodies.len();
7049        while self.body_collision_radii.len() < n {
7050            self.body_collision_radii.push(None);
7051        }
7052        self.body_collision_radii.truncate(n);
7053        self.body_colliders.resize(n, None);
7054        while self.body_filters.len() < n {
7055            self.body_filters.push(CollisionFilter::DEFAULT);
7056        }
7057        self.body_filters.truncate(n);
7058        while self.body_materials.len() < n {
7059            self.body_materials.push(crate::material::DEFAULT_MATERIAL);
7060        }
7061        self.body_materials.truncate(n);
7062        #[cfg(feature = "std")]
7063        self.sync_body_stable_ids();
7064        // ⚠️ `IslandManager::new` は sleep_data を既定 (Awake / idle_frames 0) に
7065        // 戻すので、**この後に blob から復元する** 順序を逆にすると復元が消える
7066        self.islands = IslandManager::new(n, self.islands.config);
7067
7068        // Sleep 状態の復元 (WM-08) — body の運動状態を読んだ直後から続く
7069        for i in 0..n {
7070            let state = match data[offset] {
7071                0 => SleepState::Awake,
7072                1 => SleepState::Sleeping,
7073                // 未知の値は拒否する (silent に Awake へ倒すと被覆の穴が
7074                // 「復元できた」ように見える)
7075                _ => return false,
7076            };
7077            offset += 1;
7078            let idle_frames = u32::from_le_bytes([
7079                data[offset],
7080                data[offset + 1],
7081                data[offset + 2],
7082                data[offset + 3],
7083            ]);
7084            offset += 4;
7085            if let Some(sd) = self.islands.sleep_data.get_mut(i) {
7086                sd.state = state;
7087                sd.idle_frames = idle_frames;
7088            }
7089        }
7090
7091        // overflow flag の復元 (v2)
7092        //
7093        // ⚠️ **読み込んだ状態に従う** (立てるだけでなく下げる) — sticky は
7094        // 「step を重ねても落ちない」ことであって、**別の枝を読み込んでも
7095        // 残る**ことではない 残すと探索で枝を跨いで汚染する
7096        self.overflow_detected = match data[offset] {
7097            0 => false,
7098            1 => true,
7099            // 未知の値は拒否 (silent に false へ倒すと undecided が消える)
7100            _ => return false,
7101        };
7102
7103        true
7104    }
7105}
7106
7107impl core::fmt::Debug for PhysicsWorld {
7108    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
7109        f.debug_struct("PhysicsWorld")
7110            .field("config", &self.config)
7111            .field("bodies", &self.bodies.len())
7112            .field("distance_constraints", &self.distance_constraints.len())
7113            .field("contact_constraints", &self.contact_constraints.len())
7114            .field("sdf_colliders", &self.sdf_colliders.len())
7115            .field("static_colliders", &self.static_colliders.len())
7116            .field("joints", &self.joints.len())
7117            .field("force_fields", &self.force_fields.len())
7118            .finish_non_exhaustive()
7119    }
7120}
7121
7122#[cfg(all(test, feature = "std"))]
7123mod tests {
7124    use super::*;
7125
7126    /// oracle: [`RigidBody::world_inv_inertia_apply`] (unchecked). On inputs
7127    /// in range the checked form gives the same bits, for rotations about
7128    /// several axes and an anisotropic `inv_inertia` (`1, 2, 4`), so a
7129    /// dropped rotation, a component taken from the wrong axis or a sign
7130    /// error in the checked quaternion product changes the result. The
7131    /// fixture asserts that rotation and anisotropy both matter here.
7132    #[test]
7133    fn checked_world_inv_inertia_apply_is_the_unchecked_one_in_range() {
7134        let torques = [
7135            Vec3Fix::from_int(3, -1, 2),
7136            Vec3Fix::new(
7137                Fix128::from_ratio(-5, 7),
7138                Fix128::from_ratio(11, 3),
7139                Fix128::from_int(-9),
7140            ),
7141        ];
7142        let rotations = [
7143            QuatFix::from_axis_angle(Vec3Fix::from_int(1, 2, 3), Fix128::from_ratio(7, 10)),
7144            QuatFix::from_axis_angle(Vec3Fix::from_int(-2, 1, 0), Fix128::from_ratio(-13, 5)),
7145            QuatFix::from_axis_angle(Vec3Fix::from_int(0, 0, 1), Fix128::from_ratio(1, 3)),
7146        ];
7147        for q in rotations {
7148            let mut body = RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE);
7149            body.inv_inertia = Vec3Fix::from_int(1, 2, 4);
7150            body.rotation = q;
7151            for tau in torques {
7152                let unchecked = body.world_inv_inertia_apply(tau);
7153                assert_eq!(
7154                    body.checked_world_inv_inertia_apply(tau),
7155                    Some(unchecked),
7156                    "q {q:?} tau {tau:?}"
7157                );
7158                let mut unrotated = body;
7159                unrotated.rotation = QuatFix::IDENTITY;
7160                assert_ne!(
7161                    unrotated.world_inv_inertia_apply(tau),
7162                    unchecked,
7163                    "fixture: the rotation must matter"
7164                );
7165                let mut isotropic = body;
7166                isotropic.inv_inertia = Vec3Fix::from_int(2, 2, 2);
7167                assert_ne!(
7168                    isotropic.world_inv_inertia_apply(tau),
7169                    unchecked,
7170                    "fixture: the anisotropy must matter"
7171                );
7172            }
7173        }
7174    }
7175
7176    /// oracle: the key is a function of (kind, ids, ordinal) only. Entries
7177    /// of one pair get distinct keys in arrival order, a second pass with a
7178    /// fresh counter reproduces them, and kind or direction separate keys.
7179    #[test]
7180    fn tgs_cache_key_separates_ordinal_kind_and_pair() {
7181        let mut ord = std::collections::BTreeMap::new();
7182        let k0 = tgs_cache_key(TGS_KEY_CONTACT, 3, 7, &mut ord);
7183        let k1 = tgs_cache_key(TGS_KEY_CONTACT, 3, 7, &mut ord);
7184        let other = tgs_cache_key(TGS_KEY_CONTACT, 3, 8, &mut ord);
7185        assert_ne!(k0, k1, "two entries of one pair must not share a key");
7186        let mut again = std::collections::BTreeMap::new();
7187        assert_eq!(tgs_cache_key(TGS_KEY_CONTACT, 3, 7, &mut again), k0);
7188        assert_eq!(tgs_cache_key(TGS_KEY_CONTACT, 3, 7, &mut again), k1);
7189        let mut fresh = std::collections::BTreeMap::new();
7190        assert_ne!(tgs_cache_key(TGS_KEY_DISTANCE, 3, 7, &mut fresh), k0);
7191        let mut fresh = std::collections::BTreeMap::new();
7192        assert_ne!(tgs_cache_key(TGS_KEY_CONTACT, 7, 3, &mut fresh), k0);
7193        assert_ne!(other, k0);
7194        assert_eq!(ord.get(&(3, 7)), Some(&2));
7195    }
7196
7197    #[test]
7198    fn test_rigid_body_creation() {
7199        let body = RigidBody::new(Vec3Fix::ZERO, Fix128::ONE);
7200        assert!(!body.is_static());
7201
7202        let static_body = RigidBody::new_static(Vec3Fix::ZERO);
7203        assert!(static_body.is_static());
7204    }
7205
7206    #[test]
7207    fn test_gravity_integration() {
7208        let config = SolverConfig::default();
7209        let mut world = PhysicsWorld::new(config);
7210
7211        let body = RigidBody::new(Vec3Fix::from_int(0, 10, 0), Fix128::ONE);
7212        world.add_body(body);
7213
7214        // Step for 1 second (in 10 steps of 0.1s)
7215        for _ in 0..10 {
7216            world.step(Fix128::from_ratio(1, 10));
7217        }
7218
7219        // Body should have fallen
7220        let body = world.get_body(0).unwrap();
7221        assert!(
7222            body.position.y < Fix128::from_int(10),
7223            "Body should have fallen"
7224        );
7225    }
7226
7227    #[test]
7228    fn test_distance_constraint() {
7229        let config = SolverConfig {
7230            substeps: 4,
7231            iterations: 8,
7232            gravity: Vec3Fix::ZERO, // No gravity for constraint test
7233            ..Default::default()
7234        };
7235        let mut world = PhysicsWorld::new(config);
7236
7237        // Two bodies connected by a distance constraint
7238        let a = world.add_body(RigidBody::new_static(Vec3Fix::ZERO));
7239        let b = world.add_body(RigidBody::new(Vec3Fix::from_int(5, 0, 0), Fix128::ONE));
7240
7241        world.add_distance_constraint(DistanceConstraint::new(
7242            a,
7243            b,
7244            Vec3Fix::ZERO,
7245            Vec3Fix::ZERO,
7246            Fix128::from_int(3), // Target distance: 3
7247        ));
7248
7249        // Step simulation
7250        for _ in 0..100 {
7251            world.step(Fix128::from_ratio(1, 60));
7252        }
7253
7254        // Body B should be approximately 3 units from body A
7255        let body_b = world.get_body(b).unwrap();
7256        let distance = body_b.position.length();
7257
7258        // Allow some tolerance due to gravity
7259        assert!(
7260            distance < Fix128::from_int(5),
7261            "Constraint should pull body closer"
7262        );
7263    }
7264
7265    #[test]
7266    fn test_state_serialization() {
7267        let config = SolverConfig::default();
7268        let mut world = PhysicsWorld::new(config);
7269
7270        world.add_body(RigidBody::new(Vec3Fix::from_int(1, 2, 3), Fix128::ONE));
7271        world.add_body(RigidBody::new(
7272            Vec3Fix::from_int(4, 5, 6),
7273            Fix128::from_int(2),
7274        ));
7275
7276        let state = world.serialize_state();
7277
7278        // Deserialize into another world with the same population (same
7279        // mass per body index) but different starting positions — v3 の
7280        // population fingerprint は mass 等の組み合わせが一致している
7281        // ことを要求する (gap #3、mass が異なれば別 population として拒否)
7282        let mut world2 = PhysicsWorld::new(config);
7283        world2.add_body(RigidBody::new(Vec3Fix::ZERO, Fix128::ONE));
7284        world2.add_body(RigidBody::new(Vec3Fix::ZERO, Fix128::from_int(2)));
7285
7286        assert!(world2.deserialize_state(&state));
7287
7288        assert_eq!(world2.bodies[0].position.x.hi, 1);
7289        assert_eq!(world2.bodies[0].position.y.hi, 2);
7290        assert_eq!(world2.bodies[1].position.x.hi, 4);
7291    }
7292
7293    #[test]
7294    fn test_determinism() {
7295        // Run the same simulation twice and verify identical results
7296        let config = SolverConfig::default();
7297
7298        let run_simulation = || {
7299            let mut world = PhysicsWorld::new(config);
7300            world.add_body(RigidBody::new(Vec3Fix::from_int(0, 10, 0), Fix128::ONE));
7301            world.add_body(RigidBody::new(
7302                Vec3Fix::from_int(5, 10, 0),
7303                Fix128::from_int(2),
7304            ));
7305
7306            for _ in 0..100 {
7307                world.step(Fix128::from_ratio(1, 60));
7308            }
7309
7310            world.serialize_state()
7311        };
7312
7313        let state1 = run_simulation();
7314        let state2 = run_simulation();
7315
7316        assert_eq!(state1, state2, "Simulation must be deterministic");
7317    }
7318
7319    #[test]
7320    fn test_body_material_assignment() {
7321        let config = SolverConfig::default();
7322        let mut world = PhysicsWorld::new(config);
7323
7324        let metal_id = world.material_table.register_metal();
7325        let rubber_id = world.material_table.register_rubber();
7326
7327        let a = world.add_body(RigidBody::new(Vec3Fix::ZERO, Fix128::ONE));
7328        let b = world.add_body(RigidBody::new(Vec3Fix::from_int(2, 0, 0), Fix128::ONE));
7329
7330        world.set_body_material(a, metal_id);
7331        world.set_body_material(b, rubber_id);
7332
7333        let combined = world.combined_material(a, b);
7334        assert!(
7335            combined.friction > Fix128::ZERO,
7336            "Combined friction should be positive"
7337        );
7338    }
7339
7340    #[test]
7341    fn test_contact_cache_integration() {
7342        let config = SolverConfig::default();
7343        let mut world = PhysicsWorld::new(config);
7344
7345        let a = world.add_body(RigidBody::new(Vec3Fix::ZERO, Fix128::ONE));
7346        let b = world.add_body(RigidBody::new(Vec3Fix::from_int(1, 0, 0), Fix128::ONE));
7347
7348        world.begin_frame();
7349
7350        let contact = Contact {
7351            depth: Fix128::from_ratio(1, 10),
7352            normal: Vec3Fix::UNIT_X,
7353            point_a: Vec3Fix::from_int(1, 0, 0),
7354            point_b: Vec3Fix::from_int(1, 0, 0),
7355        };
7356        world.add_contact_with_material(a, b, contact);
7357
7358        world.end_frame();
7359
7360        // Contact cache should have the manifold
7361        let key = crate::contact_cache::BodyPairKey::new(a, b);
7362        let manifold = world.contact_cache.find(&key);
7363        assert!(
7364            manifold.is_some(),
7365            "Contact cache should contain the manifold"
7366        );
7367    }
7368
7369    #[test]
7370    fn test_kinematic_body() {
7371        let config = SolverConfig::default();
7372        let mut world = PhysicsWorld::new(config);
7373
7374        let kinematic = RigidBody::new_kinematic(Vec3Fix::ZERO);
7375        let k_id = world.add_body(kinematic);
7376
7377        // Set kinematic target
7378        world.bodies[k_id].set_kinematic_target(Vec3Fix::from_int(5, 0, 0), QuatFix::IDENTITY);
7379
7380        world.step(Fix128::from_ratio(1, 60));
7381
7382        // Should have moved to target
7383        let pos = world.bodies[k_id].position;
7384        assert_eq!(pos.x.hi, 5, "Kinematic body should move to target");
7385    }
7386
7387    #[test]
7388    fn test_body_type_enum() {
7389        let dynamic = RigidBody::new(Vec3Fix::ZERO, Fix128::ONE);
7390        assert!(dynamic.is_dynamic());
7391        assert!(!dynamic.is_kinematic());
7392
7393        let static_b = RigidBody::new_static(Vec3Fix::ZERO);
7394        assert!(static_b.is_static());
7395        assert!(!static_b.is_dynamic());
7396
7397        let kinematic = RigidBody::new_kinematic(Vec3Fix::ZERO);
7398        assert!(kinematic.is_kinematic());
7399        assert!(kinematic.is_static()); // inv_mass is zero
7400    }
7401
7402    #[test]
7403    fn test_per_body_gravity_scale() {
7404        let config = SolverConfig::default();
7405        let mut world = PhysicsWorld::new(config);
7406
7407        // Body with zero gravity
7408        let mut no_grav = RigidBody::new(Vec3Fix::from_int(0, 10, 0), Fix128::ONE);
7409        no_grav.gravity_scale = Fix128::ZERO;
7410        let ng_id = world.add_body(no_grav);
7411
7412        // Body with normal gravity
7413        let normal = RigidBody::new(Vec3Fix::from_int(5, 10, 0), Fix128::ONE);
7414        let n_id = world.add_body(normal);
7415
7416        for _ in 0..60 {
7417            world.step(Fix128::from_ratio(1, 60));
7418        }
7419
7420        // No-gravity body should not have fallen
7421        let ng_y = world.bodies[ng_id].position.y;
7422        assert!(
7423            ng_y > Fix128::from_int(9),
7424            "Zero-gravity body should stay near y=10, got {ng_y:?}"
7425        );
7426
7427        // Normal body should have fallen (damping is strong, just check it fell at all)
7428        let n_y = world.bodies[n_id].position.y;
7429        assert!(
7430            n_y < Fix128::from_int(10),
7431            "Normal gravity body should fall"
7432        );
7433    }
7434
7435    #[test]
7436    fn test_sdf_ground_collision() {
7437        use crate::math::QuatFix;
7438        use crate::sdf_collider::{ClosureSdf, SdfCollider};
7439
7440        let config = SolverConfig::default();
7441        let mut world = PhysicsWorld::new(config);
7442        world.set_sdf_collision_radius(Fix128::from_ratio(1, 2)); // 0.5
7443
7444        // Add falling body at y=5
7445        let body = RigidBody::new(Vec3Fix::from_int(0, 5, 0), Fix128::ONE);
7446        let body_id = world.add_body(body);
7447
7448        // Add ground plane SDF at y=0
7449        let ground = ClosureSdf::new(|_x, y, _z| y, |_x, _y, _z| (0.0, 1.0, 0.0));
7450        world.add_sdf_collider(SdfCollider::new_static(
7451            Box::new(ground),
7452            Vec3Fix::ZERO,
7453            QuatFix::IDENTITY,
7454        ));
7455
7456        // Simulate 2 seconds (120 frames at 60fps)
7457        let dt = Fix128::from_ratio(1, 60);
7458        for _ in 0..120 {
7459            world.step(dt);
7460        }
7461
7462        // Body should have fallen but been stopped by SDF ground
7463        let pos = world.bodies[body_id].position;
7464        let y = pos.y.to_f32();
7465
7466        // Body should be near the ground (y ~ collision_radius = 0.5)
7467        assert!(y < 5.0, "Body should have fallen from y=5");
7468        assert!(
7469            y > -1.0,
7470            "Body should not have fallen through SDF ground, y={y}"
7471        );
7472    }
7473
7474    #[test]
7475    fn test_warm_start_factor_default() {
7476        let config = SolverConfig::default();
7477        // デフォルト 0.85
7478        let expected = Fix128::from_ratio(85, 100);
7479        assert_eq!(config.warm_start_factor, expected);
7480    }
7481
7482    #[test]
7483    fn test_warm_start_factor_convergence() {
7484        // warm_start_factor = 0.85 vs 0.0(無効)で収束速度を比較
7485        let make_world = |wsf: Fix128| {
7486            let config = SolverConfig {
7487                substeps: 4,
7488                iterations: 2, // 少ないイテレーションで差が出やすい
7489                gravity: Vec3Fix::ZERO,
7490                warm_start_factor: wsf,
7491                ..Default::default()
7492            };
7493            let mut world = PhysicsWorld::new(config);
7494            let a = world.add_body(RigidBody::new_static(Vec3Fix::ZERO));
7495            let b = world.add_body(RigidBody::new(Vec3Fix::from_int(5, 0, 0), Fix128::ONE));
7496            let constraint =
7497                DistanceConstraint::new(a, b, Vec3Fix::ZERO, Vec3Fix::ZERO, Fix128::from_int(3));
7498            world.add_distance_constraint(constraint);
7499            world
7500        };
7501
7502        let mut world_warm = make_world(Fix128::from_ratio(85, 100));
7503        let mut world_cold = make_world(Fix128::ZERO);
7504
7505        let dt = Fix128::from_ratio(1, 60);
7506        for _ in 0..30 {
7507            world_warm.step(dt);
7508            world_cold.step(dt);
7509        }
7510
7511        // Both should converge, but warm-started should be closer to target distance
7512        let dist_warm = (world_warm.bodies[1].position - world_warm.bodies[0].position)
7513            .length()
7514            .to_f32();
7515        let dist_cold = (world_cold.bodies[1].position - world_cold.bodies[0].position)
7516            .length()
7517            .to_f32();
7518
7519        let target = 3.0_f32;
7520        let err_warm = (dist_warm - target).abs();
7521        let err_cold = (dist_cold - target).abs();
7522        // warm-start版の誤差が小さいか、同等であること
7523        assert!(
7524            err_warm <= err_cold + 0.1,
7525            "Warm-start should converge at least as well: err_warm={err_warm}, err_cold={err_cold}"
7526        );
7527    }
7528
7529    #[test]
7530    fn test_contact_warm_start_cached_lambda() {
7531        // ContactConstraint の cached_lambda がデフォルトでゼロ
7532        let contact = Contact {
7533            depth: Fix128::from_ratio(1, 10),
7534            normal: Vec3Fix::UNIT_Y,
7535            point_a: Vec3Fix::ZERO,
7536            point_b: Vec3Fix::ZERO,
7537        };
7538        let cc = ContactConstraint::new(0, 1, contact);
7539        assert_eq!(cc.cached_lambda, Fix128::ZERO);
7540    }
7541
7542    #[test]
7543    fn test_manual_contact_pushes_body_out_by_depth() {
7544        // 手動 contact は substep が clear_contacts するので (1.2.0)、solver level の
7545        // `solve_contact_constraints` を直接 iterations 回呼んで検証
7546        let config = SolverConfig {
7547            substeps: 1,
7548            iterations: 2,
7549            gravity: Vec3Fix::ZERO,
7550            ..Default::default()
7551        };
7552        let mut world = PhysicsWorld::new(config);
7553        let _a = world.add_body(RigidBody::new_static(Vec3Fix::ZERO));
7554        let _b = world.add_body(RigidBody::new(
7555            Vec3Fix::from_f32(0.0, 0.05, 0.0),
7556            Fix128::ONE,
7557        ));
7558
7559        // normal は B→A 方向(ドキュメント仕様)。body_a が下にあるので下向き。
7560        // correction は normal 方向。body_a += correction, body_b -= correction。
7561        // body_a=static, body_b は -(-Y) = +Y に押し出される。
7562        let contact = Contact {
7563            depth: Fix128::from_ratio(5, 100), // 0.05 m 侵入
7564            normal: -Vec3Fix::UNIT_Y,          // B→A = 下向き
7565            point_a: Vec3Fix::ZERO,
7566            point_b: Vec3Fix::from_f32(0.0, 0.05, 0.0),
7567        };
7568        world.add_contact(ContactConstraint::new(0, 1, contact));
7569
7570        let dt = Fix128::from_ratio(1, 60);
7571        for _ in 0..world.config.iterations {
7572            world.solve_contact_constraints(dt);
7573        }
7574
7575        // body_b は depth ちょうど 1 回分 (0.05) 上に押し出される、2 iteration でも 2 倍にならない
7576        let y = world.bodies[1].position.y.to_f32();
7577        assert!(
7578            (y - 0.10).abs() < 1e-5,
7579            "Body should be pushed up by depth once: y={y}"
7580        );
7581    }
7582
7583    // -------------------------------------------------------------------
7584    // v0.11.0: PhysicsWorld auto-routing to installed GPU solver bridge
7585    // -------------------------------------------------------------------
7586
7587    /// Recording bridge implementation for the v0.11.0 auto-routing
7588    /// tests. Counts invocations of contact-solve methods via shared
7589    /// `Arc<AtomicUsize>` counters so the test can assert routing
7590    /// occurred without needing to reach inside the boxed trait object.
7591    ///
7592    /// All methods are no-ops on world state (positions are not
7593    /// modified), so a test that observes position drift while this
7594    /// bridge is installed is proof that the CPU path ran — the bridge
7595    /// intentionally does not solve.
7596    #[cfg(feature = "gpu-solver-bridge")]
7597    struct RecordingBridge {
7598        send_contact_count: std::sync::Arc<core::sync::atomic::AtomicUsize>,
7599        send_body_state_count: std::sync::Arc<core::sync::atomic::AtomicUsize>,
7600        dispatch_count: std::sync::Arc<core::sync::atomic::AtomicUsize>,
7601        // v0.12.0 joint-solve counters (default zero; used only by the
7602        // joint auto-routing tests below)
7603        send_joints_count: std::sync::Arc<core::sync::atomic::AtomicUsize>,
7604        send_body_rotations_count: std::sync::Arc<core::sync::atomic::AtomicUsize>,
7605        dispatch_joint_count: std::sync::Arc<core::sync::atomic::AtomicUsize>,
7606    }
7607
7608    #[cfg(feature = "gpu-solver-bridge")]
7609    impl crate::gpu_bridge::GpuSolverBridge for RecordingBridge {
7610        fn send_island(&mut self, _p: &[[Fix128; 3]], _v: &[[Fix128; 3]]) {}
7611        fn dispatch_iterations(&mut self, _iters: u32, _dt: Fix128) {}
7612        fn recv_island(&self, _p: &mut [[Fix128; 3]], _v: &mut [[Fix128; 3]]) {}
7613        fn assert_bit_exact_vs_cpu(
7614            &self,
7615            _fixture: &crate::gpu_bridge::DiffFixture,
7616        ) -> Result<(), crate::gpu_bridge::GpuDivergence> {
7617            Ok(())
7618        }
7619        fn send_contact_constraints(&mut self, _c: &[ContactConstraint]) {
7620            self.send_contact_count
7621                .fetch_add(1, core::sync::atomic::Ordering::SeqCst);
7622        }
7623        fn send_body_state(&mut self, _p: &[[Fix128; 3]], _i: &[Fix128]) {
7624            self.send_body_state_count
7625                .fetch_add(1, core::sync::atomic::Ordering::SeqCst);
7626        }
7627        fn dispatch_contact_solve_iteration(&mut self, _warm_start_factor: Fix128) {
7628            self.dispatch_count
7629                .fetch_add(1, core::sync::atomic::Ordering::SeqCst);
7630        }
7631        fn recv_contact_constraints(&self, _c: &mut [ContactConstraint]) {}
7632        fn recv_body_positions(&self, _p: &mut [[Fix128; 3]]) {}
7633        fn send_joints(&mut self, _j: &[crate::joint::Joint]) {
7634            self.send_joints_count
7635                .fetch_add(1, core::sync::atomic::Ordering::SeqCst);
7636        }
7637        fn send_body_rotations(&mut self, _r: &[[Fix128; 4]]) {
7638            self.send_body_rotations_count
7639                .fetch_add(1, core::sync::atomic::Ordering::SeqCst);
7640        }
7641        fn dispatch_joint_solve_iteration(&mut self, _dt: Fix128) {
7642            self.dispatch_joint_count
7643                .fetch_add(1, core::sync::atomic::Ordering::SeqCst);
7644        }
7645    }
7646
7647    /// Two overlapping unit spheres in zero gravity: since 1.2.0 contacts are
7648    /// re-detected in every substep, so the contact the bridge tests observe
7649    /// has to come from `detect_collisions` (a manually added contact would
7650    /// be cleared at the start of the substep).
7651    #[cfg(feature = "gpu-solver-bridge")]
7652    fn build_one_contact_world() -> PhysicsWorld {
7653        let mut world = PhysicsWorld::new(SolverConfig {
7654            gravity: Vec3Fix::ZERO,
7655            ..SolverConfig::default()
7656        });
7657        let body_a = RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE);
7658        let body_b = RigidBody::new_dynamic(
7659            Vec3Fix::new(Fix128::from_ratio(19, 10), Fix128::ZERO, Fix128::ZERO),
7660            Fix128::ONE,
7661        );
7662        world.add_body_with_radius(body_a, Fix128::ONE);
7663        world.add_body_with_radius(body_b, Fix128::ONE);
7664        world
7665    }
7666
7667    /// v0.11.0: attaching a bridge via `set_gpu_solver_bridge` routes
7668    /// contact-solve through the bridge on every subsequent `step`.
7669    /// Verified by counting bridge method invocations.
7670    #[cfg(feature = "gpu-solver-bridge")]
7671    #[test]
7672    fn physics_world_step_auto_routes_through_bridge_when_attached() {
7673        let send_contact_count = std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0));
7674        let send_body_state_count = std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0));
7675        let dispatch_count = std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0));
7676
7677        let bridge = RecordingBridge {
7678            send_contact_count: std::sync::Arc::clone(&send_contact_count),
7679            send_body_state_count: std::sync::Arc::clone(&send_body_state_count),
7680            dispatch_count: std::sync::Arc::clone(&dispatch_count),
7681            send_joints_count: std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0)),
7682            send_body_rotations_count: std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0)),
7683            dispatch_joint_count: std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0)),
7684        };
7685
7686        let mut world = build_one_contact_world();
7687        world.set_gpu_solver_bridge(Some(Box::new(bridge)));
7688        assert!(world.gpu_solver_bridge_installed());
7689
7690        // substep は solve_contact_constraints を `config.iterations`
7691        // 回呼ぶ (default 4)。auto-routing 経路はその全てを bridge に
7692        // 転送するはずなので、send / dispatch は
7693        // `config.iterations` 回発火する。
7694        let iters = world.config.iterations;
7695        let dt = Fix128::from_ratio(1, 60);
7696        world.substep(dt);
7697
7698        assert_eq!(
7699            send_contact_count.load(core::sync::atomic::Ordering::SeqCst),
7700            iters,
7701            "send_contact_constraints must be invoked once per PGS iteration"
7702        );
7703        assert_eq!(
7704            send_body_state_count.load(core::sync::atomic::Ordering::SeqCst),
7705            iters,
7706            "send_body_state must be invoked once per PGS iteration"
7707        );
7708        assert_eq!(
7709            dispatch_count.load(core::sync::atomic::Ordering::SeqCst),
7710            iters,
7711            "dispatch_contact_solve_iteration must fire once per PGS iteration"
7712        );
7713    }
7714
7715    /// v0.11.0: without a bridge installed, `step` falls back to the CPU
7716    /// contact solver. Verified by observing that contact-affected body
7717    /// positions drift — the CPU path applies the position correction
7718    /// that the recording bridge above would not.
7719    #[cfg(feature = "gpu-solver-bridge")]
7720    #[test]
7721    fn physics_world_step_falls_back_to_cpu_when_bridge_detached() {
7722        let mut world = build_one_contact_world();
7723        assert!(!world.gpu_solver_bridge_installed());
7724
7725        let x_before = world.bodies[0].position.x;
7726        let dt = Fix128::from_ratio(1, 60);
7727        world.substep(dt);
7728        let x_after = world.bodies[0].position.x;
7729
7730        // CPU contact solve applies a position correction along the
7731        // contact normal (x axis). Any non-zero drift confirms the CPU
7732        // path was taken instead of the (absent) bridge path.
7733        assert_ne!(
7734            x_before, x_after,
7735            "CPU contact solver must adjust position when no bridge is installed"
7736        );
7737    }
7738
7739    /// v0.11.0: attach / take lifecycle preserves the field invariant
7740    /// (`gpu_solver_bridge_installed` reflects the current state) and
7741    /// `take_gpu_solver_bridge` returns the same box on the first
7742    /// call and `None` on subsequent calls.
7743    #[cfg(feature = "gpu-solver-bridge")]
7744    #[test]
7745    fn physics_world_bridge_attach_detach_lifecycle() {
7746        let mut world = PhysicsWorld::new(SolverConfig::default());
7747        assert!(!world.gpu_solver_bridge_installed());
7748
7749        let bridge = RecordingBridge {
7750            send_contact_count: std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0)),
7751            send_body_state_count: std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0)),
7752            dispatch_count: std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0)),
7753            send_joints_count: std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0)),
7754            send_body_rotations_count: std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0)),
7755            dispatch_joint_count: std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0)),
7756        };
7757        world.set_gpu_solver_bridge(Some(Box::new(bridge)));
7758        assert!(world.gpu_solver_bridge_installed());
7759
7760        let taken = world.take_gpu_solver_bridge();
7761        assert!(taken.is_some(), "first take must return the installed box");
7762        assert!(!world.gpu_solver_bridge_installed());
7763
7764        let taken_again = world.take_gpu_solver_bridge();
7765        assert!(
7766            taken_again.is_none(),
7767            "subsequent take must return None when nothing is installed"
7768        );
7769    }
7770
7771    // -------------------------------------------------------------------
7772    // v0.12.0: PhysicsWorld auto-routing joint solve through installed bridge
7773    // -------------------------------------------------------------------
7774
7775    #[cfg(feature = "gpu-solver-bridge")]
7776    fn build_one_ball_joint_world() -> PhysicsWorld {
7777        use crate::joint::{BallJoint, Joint};
7778        use crate::math::Vec3Fix;
7779        let mut world = PhysicsWorld::new(SolverConfig::default());
7780        let body_a = RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE);
7781        let body_b = RigidBody::new_dynamic(
7782            Vec3Fix::new(Fix128::from_int(1), Fix128::ZERO, Fix128::ZERO),
7783            Fix128::ONE,
7784        );
7785        world.add_body(body_a);
7786        world.add_body(body_b);
7787        world.joints.push(Joint::Ball(BallJoint::new(
7788            0,
7789            1,
7790            Vec3Fix::ZERO,
7791            Vec3Fix::ZERO,
7792        )));
7793        world
7794    }
7795
7796    /// v0.12.0: attaching a bridge routes joint-solve through the
7797    /// bridge on every subsequent `substep` — verified by counting
7798    /// bridge method invocations. Joint solve runs **once per
7799    /// substep** (unlike contact solve which iterates
7800    /// `config.iterations` times), so the counters should each
7801    /// increment by exactly one after a single substep.
7802    #[cfg(feature = "gpu-solver-bridge")]
7803    #[test]
7804    fn physics_world_substep_auto_routes_joint_solve_through_bridge() {
7805        let send_joints_count = std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0));
7806        let send_body_rotations_count =
7807            std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0));
7808        let dispatch_joint_count = std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0));
7809
7810        let bridge = RecordingBridge {
7811            send_contact_count: std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0)),
7812            send_body_state_count: std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0)),
7813            dispatch_count: std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0)),
7814            send_joints_count: std::sync::Arc::clone(&send_joints_count),
7815            send_body_rotations_count: std::sync::Arc::clone(&send_body_rotations_count),
7816            dispatch_joint_count: std::sync::Arc::clone(&dispatch_joint_count),
7817        };
7818
7819        let mut world = build_one_ball_joint_world();
7820        world.set_gpu_solver_bridge(Some(Box::new(bridge)));
7821
7822        let dt = Fix128::from_ratio(1, 60);
7823        world.substep(dt);
7824
7825        assert_eq!(
7826            send_joints_count.load(core::sync::atomic::Ordering::SeqCst),
7827            1,
7828            "send_joints must be invoked exactly once per substep"
7829        );
7830        assert_eq!(
7831            send_body_rotations_count.load(core::sync::atomic::Ordering::SeqCst),
7832            1,
7833            "send_body_rotations must be invoked exactly once per substep"
7834        );
7835        assert_eq!(
7836            dispatch_joint_count.load(core::sync::atomic::Ordering::SeqCst),
7837            1,
7838            "dispatch_joint_solve_iteration must fire exactly once per substep (single Baumgarte projection, not PGS)"
7839        );
7840    }
7841
7842    /// v0.12.0: without a bridge installed, joint solve falls back to
7843    /// the CPU `solve_joints`. Verified by observing that the two
7844    /// bodies (initially 1 unit apart with a zero-anchor ball joint)
7845    /// get pulled together by the joint constraint.
7846    #[cfg(feature = "gpu-solver-bridge")]
7847    #[test]
7848    fn physics_world_substep_falls_back_to_cpu_joint_solve_when_bridge_detached() {
7849        let mut world = build_one_ball_joint_world();
7850        assert!(!world.gpu_solver_bridge_installed());
7851
7852        let a_before = world.bodies[0].position.x;
7853        let b_before = world.bodies[1].position.x;
7854        let dt = Fix128::from_ratio(1, 60);
7855        world.substep(dt);
7856        let a_after = world.bodies[0].position.x;
7857        let b_after = world.bodies[1].position.x;
7858
7859        // Zero-anchor ball joint constrains anchor_a == anchor_b,
7860        // which for zero local anchors is body positions themselves.
7861        // CPU `solve_ball_joint` therefore pulls each body toward
7862        // the other by half the separation — bodies should move
7863        // toward each other but not past each other.
7864        let a_delta = (a_after - a_before).to_f32();
7865        let b_delta = (b_after - b_before).to_f32();
7866        assert!(
7867            a_delta > 0.0,
7868            "body_a should move in +x toward body_b (delta = {a_delta})"
7869        );
7870        assert!(
7871            b_delta < 0.0,
7872            "body_b should move in -x toward body_a (delta = {b_delta})"
7873        );
7874    }
7875
7876    /// v0.12.0: attach → observe joint routing → take →
7877    /// verify subsequent substep uses CPU path — a mixed-lifecycle
7878    /// test that exercises both the auto-routing and fallback code
7879    /// paths on the same `PhysicsWorld` instance.
7880    #[cfg(feature = "gpu-solver-bridge")]
7881    #[test]
7882    fn physics_world_substep_joint_solve_bridge_attach_take_lifecycle() {
7883        let dispatch_joint_count = std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0));
7884        let bridge = RecordingBridge {
7885            send_contact_count: std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0)),
7886            send_body_state_count: std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0)),
7887            dispatch_count: std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0)),
7888            send_joints_count: std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0)),
7889            send_body_rotations_count: std::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0)),
7890            dispatch_joint_count: std::sync::Arc::clone(&dispatch_joint_count),
7891        };
7892
7893        let mut world = build_one_ball_joint_world();
7894        world.set_gpu_solver_bridge(Some(Box::new(bridge)));
7895
7896        let dt = Fix128::from_ratio(1, 60);
7897        world.substep(dt);
7898        assert_eq!(
7899            dispatch_joint_count.load(core::sync::atomic::Ordering::SeqCst),
7900            1,
7901            "first substep must route through bridge"
7902        );
7903
7904        let taken = world.take_gpu_solver_bridge();
7905        assert!(taken.is_some(), "bridge take must return Some");
7906
7907        // After detach, the bridge's counter must not increment on
7908        // subsequent substeps — the CPU path takes over.
7909        let a_before = world.bodies[0].position.x;
7910        world.substep(dt);
7911        let a_after = world.bodies[0].position.x;
7912        assert_eq!(
7913            dispatch_joint_count.load(core::sync::atomic::Ordering::SeqCst),
7914            1,
7915            "after take_gpu_solver_bridge, no further bridge dispatch may occur"
7916        );
7917        assert_ne!(
7918            a_before, a_after,
7919            "CPU joint solve should move body_a toward body_b after detach"
7920        );
7921    }
7922
7923    // ------------------------------------------------------------------
7924    // Graph coloring soundness (v1.0.1): >64 colors + static exclusion
7925    // ------------------------------------------------------------------
7926
7927    /// Hub body + `spokes` distance constraints, each spoke its own body.
7928    fn hub_world(hub: RigidBody, spokes: usize) -> PhysicsWorld {
7929        let mut world = PhysicsWorld::new(SolverConfig {
7930            gravity: Vec3Fix::ZERO,
7931            ..Default::default()
7932        });
7933        let hub_idx = world.add_body(hub);
7934        for i in 0..spokes {
7935            let spoke = world.add_body(RigidBody::new_dynamic(
7936                Vec3Fix::from_int(i as i64 + 1, 0, 0),
7937                Fix128::ONE,
7938            ));
7939            world.add_distance_constraint(DistanceConstraint::new(
7940                hub_idx,
7941                spoke,
7942                Vec3Fix::ZERO,
7943                Vec3Fix::ZERO,
7944                Fix128::ONE,
7945            ));
7946        }
7947        world
7948    }
7949
7950    #[test]
7951    fn coloring_dynamic_hub_over_64_spokes_gets_one_color_per_constraint() {
7952        // Pre-1.0.1: colors saturated at 64 and every further constraint
7953        // aliased into batch 64 while sharing the hub body.
7954        for spokes in [65usize, 70, 200] {
7955            let mut world = hub_world(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE), spokes);
7956            world.rebuild_batches();
7957            assert_eq!(
7958                world.num_batches(),
7959                spokes,
7960                "dynamic hub with {spokes} spokes needs {spokes} batches"
7961            );
7962            assert!(world.batches_are_body_disjoint());
7963        }
7964    }
7965
7966    #[test]
7967    fn coloring_static_hub_imposes_no_constraint() {
7968        // A static floor touched by many bodies must not serialize the
7969        // solver: all spokes are distinct dynamic bodies → single batch.
7970        let mut world = hub_world(RigidBody::new_static(Vec3Fix::ZERO), 200);
7971        world.rebuild_batches();
7972        assert_eq!(world.num_batches(), 1);
7973        assert!(world.batches_are_body_disjoint());
7974    }
7975
7976    #[test]
7977    fn coloring_contacts_and_distances_share_color_space() {
7978        // Two constraint kinds on the same dynamic pair must land in
7979        // different batches (the contact pass runs after the distance pass
7980        // but colors are shared across both).
7981        let mut world = PhysicsWorld::new(SolverConfig::default());
7982        let a = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
7983        let b = world.add_body(RigidBody::new_dynamic(
7984            Vec3Fix::from_int(1, 0, 0),
7985            Fix128::ONE,
7986        ));
7987        world.add_distance_constraint(DistanceConstraint::new(
7988            a,
7989            b,
7990            Vec3Fix::ZERO,
7991            Vec3Fix::ZERO,
7992            Fix128::ONE,
7993        ));
7994        world.add_contact(ContactConstraint::new(
7995            a,
7996            b,
7997            Contact {
7998                point_a: Vec3Fix::ZERO,
7999                point_b: Vec3Fix::ZERO,
8000                normal: Vec3Fix::from_int(1, 0, 0),
8001                depth: Fix128::from_ratio(1, 10),
8002            },
8003        ));
8004        world.rebuild_batches();
8005        assert_eq!(world.num_batches(), 2);
8006        assert!(world.batches_are_body_disjoint());
8007    }
8008
8009    #[test]
8010    fn coloring_disjoint_check_detects_violation() {
8011        // Hand-craft an invalid batch to prove the checker is not vacuous.
8012        let mut world = hub_world(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE), 2);
8013        world.rebuild_batches();
8014        assert!(world.batches_are_body_disjoint());
8015        let mut bad = ConstraintBatch::default();
8016        bad.distance_indices.extend([0usize, 1]);
8017        world.constraint_batches = vec![bad];
8018        assert!(!world.batches_are_body_disjoint());
8019    }
8020
8021    #[cfg(feature = "parallel")]
8022    #[test]
8023    fn parallel_step_static_hub_matches_sequential_solver() {
8024        // Static hub + 70 spokes: pre-1.0.1 this scene created a batch with
8025        // 6 constraints all holding `&mut` to the hub. Step both solver
8026        // paths and require bit-exact agreement.
8027        let sub_dt = Fix128::from_ratio(1, 240);
8028        let mut par = hub_world(RigidBody::new_static(Vec3Fix::ZERO), 70);
8029        let mut seq = hub_world(RigidBody::new_static(Vec3Fix::ZERO), 70);
8030        par.rebuild_batches();
8031        seq.rebuild_batches();
8032        assert_eq!(par.num_batches(), 1, "static hub → single batch");
8033        for _ in 0..120 {
8034            par.substep_batched(sub_dt);
8035            seq.substep(sub_dt);
8036        }
8037        for (p, s) in par.bodies.iter().zip(&seq.bodies) {
8038            assert_eq!(p.position, s.position);
8039            assert_eq!(p.velocity, s.velocity);
8040        }
8041        assert!(par.batches_are_body_disjoint());
8042    }
8043
8044    #[cfg(feature = "parallel")]
8045    #[test]
8046    fn parallel_step_rebuilds_when_body_becomes_dynamic_after_coloring() {
8047        // Coloring excluded the hub as static; flipping it to dynamic
8048        // without touching constraints must not leave the stale snapshot
8049        // in place (two threads would otherwise take `&mut` to the hub).
8050        let mut world = hub_world(RigidBody::new_static(Vec3Fix::ZERO), 70);
8051        world.rebuild_batches();
8052        assert_eq!(world.num_batches(), 1);
8053        world.bodies[0].inv_mass = Fix128::ONE;
8054        world.solve_constraints_batched(Fix128::from_ratio(1, 60));
8055        assert_eq!(world.num_batches(), 70);
8056        assert!(world.batches_are_body_disjoint());
8057    }
8058
8059    // ------------------------------------------------------------------
8060    // Mutation-score tests (2026-09-15, quality-deep core score 32% 対応)
8061    // 各 test は cargo-mutants の missed 変異 (演算子置換 / 分岐反転 / 符号削除)
8062    // を値の厳密一致で殺すことを目的にする  dt は 2 の冪の逆数 (1/4) にして
8063    // inv_dt が Fix128 で exact になるようにし、expected は独立に手計算した値
8064    // ------------------------------------------------------------------
8065
8066    /// gravity 0 / damping なしの world (integrate の副作用を消して単機能を観測)
8067    fn quiet_world() -> PhysicsWorld {
8068        let config = SolverConfig {
8069            gravity: Vec3Fix::ZERO,
8070            damping: Fix128::ONE,
8071            ..SolverConfig::default()
8072        };
8073        PhysicsWorld::new(config)
8074    }
8075
8076    fn v3(x: i64, y: i64, z: i64) -> Vec3Fix {
8077        Vec3Fix::from_int(x, y, z)
8078    }
8079
8080    fn r(n: i64, d: i64) -> Fix128 {
8081        Fix128::from_ratio(n, d)
8082    }
8083
8084    #[test]
8085    fn update_velocities_linear_velocity_is_position_delta_times_inv_dt() {
8086        let mut world = quiet_world();
8087        let dyn_idx = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
8088        let static_idx = world.add_body(RigidBody::new_static(v3(5, 5, 5)));
8089        let dt = r(1, 4); // inv_dt = 4 exactly
8090
8091        // prev → position の差分 (1/4, -1/2, 3/4) → velocity は (1, -2, 3)
8092        world.bodies[dyn_idx].prev_position = v3(1, 1, 1);
8093        world.bodies[dyn_idx].position = Vec3Fix::new(
8094            Fix128::ONE + r(1, 4),
8095            Fix128::ONE - r(1, 2),
8096            Fix128::ONE + r(3, 4),
8097        );
8098        // static body にも差分を仕込む: 処理対象外なので velocity は ZERO のまま
8099        world.bodies[static_idx].prev_position = v3(0, 0, 0);
8100        world.bodies[static_idx].position = v3(5, 5, 5);
8101
8102        world.update_velocities(dt);
8103
8104        assert_eq!(world.bodies[dyn_idx].velocity, v3(1, -2, 3));
8105        assert_eq!(world.bodies[static_idx].velocity, Vec3Fix::ZERO);
8106    }
8107
8108    #[test]
8109    fn update_velocities_angular_velocity_from_rotation_delta_positive_w() {
8110        let mut world = quiet_world();
8111        let idx = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
8112        let dt = r(1, 4);
8113        // 小さい回転 (θ = 1/2 rad) → dq.w > 0 の分岐
8114        let q = QuatFix::from_axis_angle(v3(1, 2, 3), r(1, 2));
8115        world.bodies[idx].prev_rotation = QuatFix::IDENTITY;
8116        world.bodies[idx].rotation = q;
8117
8118        world.update_velocities(dt);
8119
8120        // dq = q: θ = 1/2 rad about (1, 2, 3)/√14 ⇒ ω = axis · θ / dt = axis · 2
8121        let got = world.bodies[idx].angular_velocity;
8122        let n = crate::det_math::sqrt64(14.0);
8123        let want = [2.0 / n, 4.0 / n, 6.0 / n];
8124        for (g, e) in [got.x, got.y, got.z].iter().zip(want) {
8125            assert!((g.to_f64() - e).abs() < 1e-12, "{} vs {e}", g.to_f64());
8126        }
8127    }
8128
8129    #[test]
8130    fn update_velocities_angular_velocity_negative_w_flips_sign() {
8131        let mut world = quiet_world();
8132        let idx = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
8133        let dt = r(1, 4);
8134        // θ = 3π/2 → w = cos(3π/4) < 0 → 符号反転分岐
8135        let theta = Fix128::PI + Fix128::HALF_PI;
8136        let q = QuatFix::from_axis_angle(v3(1, -2, 3), theta);
8137        assert!(q.w < Fix128::ZERO, "test precondition: w must be negative");
8138        world.bodies[idx].prev_rotation = QuatFix::IDENTITY;
8139        world.bodies[idx].rotation = q;
8140
8141        world.update_velocities(dt);
8142
8143        // 3π/2 about a = (1, −2, 3)/√14 equals π/2 about −a (w folded to
8144        // ≥ 0) ⇒ ω = −a · (π/2) / dt = −a · 2π; every component non-zero so
8145        // each sign is checked
8146        let got = world.bodies[idx].angular_velocity;
8147        let n = crate::det_math::sqrt64(14.0);
8148        let k = 2.0 * core::f64::consts::PI / n;
8149        let want = [-k, 2.0 * k, -3.0 * k];
8150        for (g, e) in [got.x, got.y, got.z].iter().zip(want) {
8151            assert!((g.to_f64() - e).abs() < 1e-12, "{} vs {e}", g.to_f64());
8152        }
8153    }
8154
8155    #[test]
8156    fn update_velocities_angular_velocity_zero_w_takes_positive_branch() {
8157        let mut world = quiet_world();
8158        let idx = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
8159        let dt = r(1, 4);
8160        // w == 0 ちょうど (π 回転) → `<` は false = 正の分岐 (`<=` / `==` 変異はここで死ぬ)
8161        let q = QuatFix::new(Fix128::ZERO, Fix128::ZERO, Fix128::ONE, Fix128::ZERO);
8162        world.bodies[idx].prev_rotation = QuatFix::IDENTITY;
8163        world.bodies[idx].rotation = q;
8164
8165        world.update_velocities(dt);
8166
8167        // π about z (w = 0 takes the positive branch) ⇒ ω = (0, 0, π / dt) = (0, 0, 4π)
8168        let got = world.bodies[idx].angular_velocity;
8169        assert_eq!((got.x, got.y), (Fix128::ZERO, Fix128::ZERO));
8170        let want = 4.0 * core::f64::consts::PI;
8171        assert!(
8172            (got.z.to_f64() - want).abs() < 1e-12,
8173            "{} vs {want}",
8174            got.z.to_f64()
8175        );
8176    }
8177
8178    /// 接触 1 本を直接差し込む helper (normal は B → A 方向 = +x)
8179    fn push_contact(
8180        world: &mut PhysicsWorld,
8181        a: usize,
8182        b: usize,
8183        friction: Fix128,
8184        restitution: Fix128,
8185    ) {
8186        let contact = Contact {
8187            depth: r(1, 100),
8188            normal: v3(1, 0, 0),
8189            point_a: Vec3Fix::ZERO,
8190            point_b: Vec3Fix::ZERO,
8191        };
8192        let mut c = ContactConstraint::new(a, b, contact);
8193        c.friction = friction;
8194        c.restitution = restitution;
8195        world.contact_constraints.push(c);
8196    }
8197
8198    /// Phase 1 は velocity を (position - prev) * inv_dt で上書きするので、
8199    /// 速度 v を与えるには prev_position = position - v * dt を仕込む (dt = 1/4 固定)
8200    /// 反発は pre-solve の法線速度 v̄_n を使うので `velocity` にも同じ v を入れる
8201    /// (拘束が位置を動かさなかった substep = pre と post が一致する場合)
8202    fn give_velocity(world: &mut PhysicsWorld, idx: usize, v: Vec3Fix) {
8203        let b = &mut world.bodies[idx];
8204        b.velocity = v;
8205        b.prev_position = b.position - v * r(1, 4);
8206        b.prev_rotation = b.rotation;
8207    }
8208
8209    /// 全 body の prev を現在値に揃える (速度 0)
8210    fn freeze_positions(world: &mut PhysicsWorld) {
8211        for b in &mut world.bodies {
8212            b.prev_position = b.position;
8213            b.prev_rotation = b.rotation;
8214        }
8215    }
8216
8217    #[test]
8218    fn update_velocities_restitution_splits_impulse_by_inverse_mass() {
8219        let mut world = quiet_world();
8220        let a = world.add_body(RigidBody::new_dynamic(v3(0, 0, 0), Fix128::ONE));
8221        let b = world.add_body(RigidBody::new_dynamic(v3(1, 0, 0), Fix128::ONE));
8222        push_contact(&mut world, a, b, Fix128::ZERO, r(1, 2));
8223        freeze_positions(&mut world);
8224        // A が B に向かって -x に 1 で接近 (vn = -1 < 0)
8225        give_velocity(&mut world, a, v3(-1, 0, 0));
8226
8227        world.update_velocities(r(1, 4));
8228
8229        // delta_vn = -(1 + 0.5) * (-1) = 1.5、inv_w = 1/2 → A += 0.75、B -= 0.75
8230        assert_eq!(
8231            world.bodies[a].velocity,
8232            Vec3Fix::new(-r(1, 4), Fix128::ZERO, Fix128::ZERO)
8233        );
8234        assert_eq!(
8235            world.bodies[b].velocity,
8236            Vec3Fix::new(-r(3, 4), Fix128::ZERO, Fix128::ZERO)
8237        );
8238    }
8239
8240    #[test]
8241    fn update_velocities_restitution_skips_separating_contact() {
8242        let mut world = quiet_world();
8243        let a = world.add_body(RigidBody::new_dynamic(v3(0, 0, 0), Fix128::ONE));
8244        let b = world.add_body(RigidBody::new_dynamic(v3(1, 0, 0), Fix128::ONE));
8245        push_contact(&mut world, a, b, Fix128::ZERO, r(1, 2));
8246        freeze_positions(&mut world);
8247        // vn = +1 > 0 (離れていく) → restitution なし、tangent なし → 無変化
8248        give_velocity(&mut world, a, v3(1, 0, 0));
8249
8250        world.update_velocities(r(1, 4));
8251
8252        assert_eq!(world.bodies[a].velocity, v3(1, 0, 0));
8253        assert_eq!(world.bodies[b].velocity, Vec3Fix::ZERO);
8254    }
8255
8256    #[test]
8257    fn update_velocities_restitution_uses_mass_ratio() {
8258        let mut world = quiet_world();
8259        // A: inv_mass 1、B: inv_mass 3 (直接設定、1/(1/3) の丸めを避ける) → inv_w = 1/4 exact
8260        let a = world.add_body(RigidBody::new_dynamic(v3(0, 0, 0), Fix128::ONE));
8261        let b = world.add_body(RigidBody::new_dynamic(v3(1, 0, 0), Fix128::ONE));
8262        world.bodies[b].inv_mass = Fix128::from_int(3);
8263        push_contact(&mut world, a, b, Fix128::ZERO, Fix128::ZERO);
8264        freeze_positions(&mut world);
8265        give_velocity(&mut world, a, v3(-2, 0, 0)); // vn = -2、e = 0 → delta_vn = 2
8266
8267        world.update_velocities(r(1, 4));
8268
8269        // A += 2 * (1 * 1/4) = 0.5 → -1.5、B -= 2 * (3 * 1/4) = 1.5 → -1.5 (完全非弾性で速度一致)
8270        assert_eq!(
8271            world.bodies[a].velocity,
8272            Vec3Fix::new(-r(3, 2), Fix128::ZERO, Fix128::ZERO)
8273        );
8274        assert_eq!(
8275            world.bodies[b].velocity,
8276            Vec3Fix::new(-r(3, 2), Fix128::ZERO, Fix128::ZERO)
8277        );
8278    }
8279
8280    #[test]
8281    fn update_velocities_friction_clamped_by_coulomb_limit() {
8282        let mut world = quiet_world();
8283        let a = world.add_body(RigidBody::new_dynamic(v3(0, 0, 0), Fix128::ONE));
8284        let b = world.add_body(RigidBody::new_static(v3(1, 0, 0))); // inv_mass 0 → w_sum = 1
8285        push_contact(&mut world, a, b, r(1, 2), Fix128::ZERO);
8286        // 位置解の法線 λ = 1 → Coulomb 上限 μ λ / dt = 0.5 * 1 * 4 = 2
8287        world.contact_constraints[0].cached_lambda = Fix128::ONE;
8288        freeze_positions(&mut world);
8289        // 法線成分は正 (離れる) にして restitution を素通りさせる
8290        give_velocity(&mut world, a, v3(4, 3, 0));
8291
8292        world.update_velocities(r(1, 4));
8293
8294        // tangent_speed = 3、max = 2 → clamp 2、A.y -= 2 * 1 → 1、x は不変
8295        assert_eq!(
8296            world.bodies[a].velocity,
8297            Vec3Fix::new(Fix128::from_int(4), Fix128::ONE, Fix128::ZERO)
8298        );
8299        assert_eq!(world.bodies[b].velocity, Vec3Fix::ZERO);
8300    }
8301
8302    #[test]
8303    fn update_velocities_friction_below_limit_removes_all_tangential_velocity() {
8304        let mut world = quiet_world();
8305        let a = world.add_body(RigidBody::new_dynamic(v3(0, 0, 0), Fix128::ONE));
8306        let b = world.add_body(RigidBody::new_static(v3(1, 0, 0)));
8307        push_contact(&mut world, a, b, Fix128::ONE, Fix128::ZERO);
8308        // μ = 1、λ = 1 → max = μ λ / dt = 4
8309        world.contact_constraints[0].cached_lambda = Fix128::ONE;
8310        freeze_positions(&mut world);
8311        give_velocity(&mut world, a, v3(4, 0, 3));
8312
8313        world.update_velocities(r(1, 4));
8314
8315        // tangent_speed = 3 < max 4 → applied = 3 → z 成分 0
8316        assert_eq!(
8317            world.bodies[a].velocity,
8318            Vec3Fix::new(Fix128::from_int(4), Fix128::ZERO, Fix128::ZERO)
8319        );
8320    }
8321
8322    #[test]
8323    fn update_velocities_friction_boundary_equal_to_limit_uses_limit() {
8324        let mut world = quiet_world();
8325        let a = world.add_body(RigidBody::new_dynamic(v3(0, 0, 0), Fix128::ONE));
8326        let b = world.add_body(RigidBody::new_static(v3(1, 0, 0)));
8327        push_contact(&mut world, a, b, Fix128::ONE, Fix128::ZERO);
8328        // μ = 1、λ = 3/4 → max = μ λ / dt = 3
8329        world.contact_constraints[0].cached_lambda = r(3, 4);
8330        freeze_positions(&mut world);
8331        // tangent_speed == max (3 == 3): `<` は false → max 側 (値は同じ 3 だが `<=`/`>` 変異で経路が変わる)
8332        give_velocity(&mut world, a, v3(3, 3, 0));
8333
8334        world.update_velocities(r(1, 4));
8335
8336        assert_eq!(
8337            world.bodies[a].velocity,
8338            Vec3Fix::new(Fix128::from_int(3), Fix128::ZERO, Fix128::ZERO)
8339        );
8340    }
8341
8342    #[test]
8343    fn update_velocities_sensor_on_either_side_skips_response() {
8344        for sensor_side in 0..2 {
8345            let mut world = quiet_world();
8346            let a = world.add_body(RigidBody::new_dynamic(v3(0, 0, 0), Fix128::ONE));
8347            let b = world.add_body(RigidBody::new_dynamic(v3(1, 0, 0), Fix128::ONE));
8348            world.bodies[if sensor_side == 0 { a } else { b }].is_sensor = true;
8349            push_contact(&mut world, a, b, r(1, 2), r(1, 2));
8350            freeze_positions(&mut world);
8351            give_velocity(&mut world, a, v3(-1, 2, 0));
8352
8353            world.update_velocities(r(1, 4));
8354
8355            assert_eq!(
8356                world.bodies[a].velocity,
8357                v3(-1, 2, 0),
8358                "sensor side {sensor_side}"
8359            );
8360            assert_eq!(
8361                world.bodies[b].velocity,
8362                Vec3Fix::ZERO,
8363                "sensor side {sensor_side}"
8364            );
8365        }
8366    }
8367
8368    #[test]
8369    fn update_velocities_two_static_bodies_are_skipped() {
8370        let mut world = quiet_world();
8371        let a = world.add_body(RigidBody::new_static(v3(0, 0, 0)));
8372        let b = world.add_body(RigidBody::new_static(v3(1, 0, 0)));
8373        push_contact(&mut world, a, b, Fix128::ONE, Fix128::ONE);
8374        // static は Phase 1 で skip、Phase 2 は w_sum < ε で skip → velocity は不変 (手書き値のまま)
8375        freeze_positions(&mut world);
8376        world.bodies[a].velocity = v3(-1, 1, 0);
8377
8378        world.update_velocities(r(1, 4));
8379
8380        assert_eq!(world.bodies[a].velocity, v3(-1, 1, 0));
8381    }
8382
8383    // ---- raycast ------------------------------------------------------
8384
8385    /// 半径 2 の球を pos に置いた world (gravity なし)
8386    fn sphere_world(positions: &[Vec3Fix]) -> PhysicsWorld {
8387        let mut world = quiet_world();
8388        for &p in positions {
8389            world.add_body_with_radius(RigidBody::new_static(p), Fix128::from_int(2));
8390        }
8391        world
8392    }
8393
8394    #[test]
8395    fn raycast_hit_distance_is_exact_closed_form() {
8396        // 原点から +x、球 (10,0,0) r=2 → b=-10, c=96, disc=4, t = 10 - 2 = 8
8397        let world = sphere_world(&[v3(10, 0, 0)]);
8398        let hit = world.raycast(Vec3Fix::ZERO, v3(1, 0, 0), Fix128::from_int(100));
8399        assert_eq!(hit, Some((0, Fix128::from_int(8))));
8400        // direction は正規化される: (3,0,0) でも同じ t
8401        let hit2 = world.raycast(Vec3Fix::ZERO, v3(3, 0, 0), Fix128::from_int(100));
8402        assert_eq!(hit2, Some((0, Fix128::from_int(8))));
8403    }
8404
8405    #[test]
8406    fn raycast_along_every_axis_and_direction() {
8407        // ray AABB の min/max 選択 (x/y/z × </>) を全て通す
8408        for (dir, pos) in [
8409            (v3(1, 0, 0), v3(10, 0, 0)),
8410            (v3(-1, 0, 0), v3(-10, 0, 0)),
8411            (v3(0, 1, 0), v3(0, 10, 0)),
8412            (v3(0, -1, 0), v3(0, -10, 0)),
8413            (v3(0, 0, 1), v3(0, 0, 10)),
8414            (v3(0, 0, -1), v3(0, 0, -10)),
8415        ] {
8416            let world = sphere_world(&[pos]);
8417            assert_eq!(
8418                world.raycast(Vec3Fix::ZERO, dir, Fix128::from_int(100)),
8419                Some((0, Fix128::from_int(8))),
8420                "dir {dir:?}"
8421            );
8422            // 逆向きは外れる
8423            assert_eq!(
8424                world.raycast(
8425                    Vec3Fix::ZERO,
8426                    dir * Fix128::from_int(-1),
8427                    Fix128::from_int(100)
8428                ),
8429                None
8430            );
8431        }
8432    }
8433
8434    #[test]
8435    fn raycast_origin_on_surface_hits_at_zero_not_far_side() {
8436        // origin (8,0,0)、球 (10,0,0) r=2: oc=(-2,0,0), b=-2, c=0, disc=4 → t_near = 0 (`<` は false)
8437        let world = sphere_world(&[v3(10, 0, 0)]);
8438        let hit = world.raycast(v3(8, 0, 0), v3(1, 0, 0), Fix128::from_int(100));
8439        assert_eq!(hit, Some((0, Fix128::ZERO)));
8440    }
8441
8442    #[test]
8443    fn raycast_from_inside_sphere_uses_far_intersection() {
8444        // origin = 球心: b=0, c=-4, disc=4 → t_near = -2 < 0 → t_far = 2
8445        let world = sphere_world(&[v3(10, 0, 0)]);
8446        let hit = world.raycast(v3(10, 0, 0), v3(1, 0, 0), Fix128::from_int(100));
8447        assert_eq!(hit, Some((0, Fix128::from_int(2))));
8448    }
8449
8450    #[test]
8451    fn raycast_max_distance_boundary_inclusive() {
8452        let world = sphere_world(&[v3(10, 0, 0)]);
8453        // t = 8 == max → hit (`>` は false)
8454        assert_eq!(
8455            world.raycast(Vec3Fix::ZERO, v3(1, 0, 0), Fix128::from_int(8)),
8456            Some((0, Fix128::from_int(8)))
8457        );
8458        // t = 8 > max 7 → miss (ray AABB は球に届かない or 距離で reject)
8459        assert_eq!(
8460            world.raycast(Vec3Fix::ZERO, v3(1, 0, 0), Fix128::from_int(7)),
8461            None
8462        );
8463        // AABB は届くが距離で reject: max = 7.9 (AABB は球 [8,12] と 7.9 で非交差)、
8464        // 代わりに max = 9 で AABB 交差 + t=8 ≤ 9 → hit、max = 8 - ε 相当は上で網羅
8465        assert_eq!(
8466            world.raycast(Vec3Fix::ZERO, v3(1, 0, 0), Fix128::from_int(9)),
8467            Some((0, Fix128::from_int(8)))
8468        );
8469    }
8470
8471    #[test]
8472    fn raycast_returns_nearest_regardless_of_body_order() {
8473        // 手前 (10) と奥 (20) の球、index 順を両方試す
8474        let near = v3(10, 0, 0);
8475        let far = v3(20, 0, 0);
8476        let w1 = sphere_world(&[far, near]);
8477        assert_eq!(
8478            w1.raycast(Vec3Fix::ZERO, v3(1, 0, 0), Fix128::from_int(100)),
8479            Some((1, Fix128::from_int(8)))
8480        );
8481        let w2 = sphere_world(&[near, far]);
8482        assert_eq!(
8483            w2.raycast(Vec3Fix::ZERO, v3(1, 0, 0), Fix128::from_int(100)),
8484            Some((0, Fix128::from_int(8)))
8485        );
8486    }
8487
8488    #[test]
8489    fn raycast_misses_sphere_beside_the_ray() {
8490        // 球 (10, 5, 0) r=2: 最接近距離 5 > 2 → disc < 0 → None
8491        let world = sphere_world(&[v3(10, 5, 0)]);
8492        assert_eq!(
8493            world.raycast(Vec3Fix::ZERO, v3(1, 0, 0), Fix128::from_int(100)),
8494            None
8495        );
8496        // 球 (10, 2, 0): 接線 (disc = 0) → t = 10 で hit
8497        let tangent = sphere_world(&[v3(10, 2, 0)]);
8498        assert_eq!(
8499            tangent.raycast(Vec3Fix::ZERO, v3(1, 0, 0), Fix128::from_int(100)),
8500            Some((0, Fix128::from_int(10)))
8501        );
8502    }
8503
8504    #[test]
8505    fn raycast_zero_direction_and_no_collidable_bodies_return_none() {
8506        let world = sphere_world(&[v3(10, 0, 0)]);
8507        assert_eq!(
8508            world.raycast(Vec3Fix::ZERO, Vec3Fix::ZERO, Fix128::from_int(100)),
8509            None
8510        );
8511        let mut no_radius = quiet_world();
8512        no_radius.add_body(RigidBody::new_static(v3(10, 0, 0)));
8513        assert_eq!(
8514            no_radius.raycast(Vec3Fix::ZERO, v3(1, 0, 0), Fix128::from_int(100)),
8515            None
8516        );
8517        // radius を後から消すと当たらなくなる
8518        let mut cleared = sphere_world(&[v3(10, 0, 0)]);
8519        cleared.clear_body_collision_radius(0);
8520        assert_eq!(
8521            cleared.raycast(Vec3Fix::ZERO, v3(1, 0, 0), Fix128::from_int(100)),
8522            None
8523        );
8524        // set で復活
8525        cleared.set_body_collision_radius(0, Fix128::from_int(2));
8526        assert_eq!(
8527            cleared.raycast(Vec3Fix::ZERO, v3(1, 0, 0), Fix128::from_int(100)),
8528            Some((0, Fix128::from_int(8)))
8529        );
8530    }
8531
8532    // ---- remove_body ---------------------------------------------------
8533
8534    fn dc(a: usize, b: usize) -> DistanceConstraint {
8535        DistanceConstraint {
8536            body_a: a,
8537            body_b: b,
8538            local_anchor_a: Vec3Fix::ZERO,
8539            local_anchor_b: Vec3Fix::ZERO,
8540            target_distance: Fix128::ONE,
8541            compliance: Fix128::ZERO,
8542            cached_lambda: Fix128::ZERO,
8543        }
8544    }
8545
8546    fn three_body_world_with_all_pairs() -> PhysicsWorld {
8547        let mut world = quiet_world();
8548        for i in 0..3 {
8549            world.add_body(RigidBody::new_dynamic(v3(i, 0, 0), Fix128::ONE));
8550        }
8551        world.add_distance_constraint(dc(0, 1));
8552        world.add_distance_constraint(dc(1, 2));
8553        world.add_distance_constraint(dc(0, 2));
8554        world.contact_constraints.push(ContactConstraint::new(
8555            0,
8556            1,
8557            Contact {
8558                depth: Fix128::ZERO,
8559                normal: v3(1, 0, 0),
8560                point_a: Vec3Fix::ZERO,
8561                point_b: Vec3Fix::ZERO,
8562            },
8563        ));
8564        world.contact_constraints.push(ContactConstraint::new(
8565            1,
8566            2,
8567            Contact {
8568                depth: Fix128::ZERO,
8569                normal: v3(1, 0, 0),
8570                point_a: Vec3Fix::ZERO,
8571                point_b: Vec3Fix::ZERO,
8572            },
8573        ));
8574        world.contact_constraints.push(ContactConstraint::new(
8575            0,
8576            2,
8577            Contact {
8578                depth: Fix128::ZERO,
8579                normal: v3(1, 0, 0),
8580                point_a: Vec3Fix::ZERO,
8581                point_b: Vec3Fix::ZERO,
8582            },
8583        ));
8584        world
8585    }
8586
8587    fn dist_pairs(world: &PhysicsWorld) -> Vec<(usize, usize)> {
8588        world
8589            .distance_constraints
8590            .iter()
8591            .map(|c| (c.body_a, c.body_b))
8592            .collect()
8593    }
8594
8595    fn contact_pairs(world: &PhysicsWorld) -> Vec<(usize, usize)> {
8596        world
8597            .contact_constraints
8598            .iter()
8599            .map(|c| (c.body_a, c.body_b))
8600            .collect()
8601    }
8602
8603    #[test]
8604    fn remove_body_middle_drops_its_constraints_and_remaps_last() {
8605        let mut world = three_body_world_with_all_pairs();
8606        let removed = world.remove_body(1).expect("index 1 exists");
8607        assert_eq!(removed.position, v3(1, 0, 0));
8608        assert_eq!(world.bodies.len(), 2);
8609        // 旧 A2 が index 1 に移動
8610        assert_eq!(world.bodies[1].position, v3(2, 0, 0));
8611        // A1 に触る 2 本は消え、(0-2) だけが (0,1) に remap
8612        assert_eq!(dist_pairs(&world), vec![(0, 1)]);
8613        assert_eq!(contact_pairs(&world), vec![(0, 1)]);
8614        assert_eq!(world.body_materials.len(), 2);
8615        assert_eq!(world.body_collision_radii.len(), 2);
8616        assert_eq!(world.body_filters.len(), 2);
8617    }
8618
8619    #[test]
8620    fn remove_body_first_remaps_last_into_slot_zero() {
8621        let mut world = three_body_world_with_all_pairs();
8622        world.remove_body(0).expect("index 0 exists");
8623        assert_eq!(world.bodies[0].position, v3(2, 0, 0));
8624        assert_eq!(world.bodies[1].position, v3(1, 0, 0));
8625        // 残るのは (1-2) → (1,0)
8626        assert_eq!(dist_pairs(&world), vec![(1, 0)]);
8627        assert_eq!(contact_pairs(&world), vec![(1, 0)]);
8628    }
8629
8630    #[test]
8631    fn remove_body_last_needs_no_remap() {
8632        let mut world = three_body_world_with_all_pairs();
8633        world.remove_body(2).expect("index 2 exists");
8634        assert_eq!(world.bodies.len(), 2);
8635        assert_eq!(world.bodies[1].position, v3(1, 0, 0));
8636        assert_eq!(dist_pairs(&world), vec![(0, 1)]);
8637        assert_eq!(contact_pairs(&world), vec![(0, 1)]);
8638    }
8639
8640    #[test]
8641    fn remove_body_out_of_range_is_none_and_leaves_world_untouched() {
8642        let mut world = three_body_world_with_all_pairs();
8643        assert!(world.remove_body(3).is_none());
8644        assert!(world.remove_body(usize::MAX).is_none());
8645        assert_eq!(world.bodies.len(), 3);
8646        assert_eq!(dist_pairs(&world), vec![(0, 1), (1, 2), (0, 2)]);
8647    }
8648
8649    #[test]
8650    fn remove_body_never_leaves_self_or_dangling_references() {
8651        // 4 body 全 pair、各 index を順に消して不変条件を検査
8652        for victim in 0..4 {
8653            let mut world = quiet_world();
8654            for i in 0..4 {
8655                world.add_body(RigidBody::new_dynamic(v3(i, 0, 0), Fix128::ONE));
8656            }
8657            for a in 0..4 {
8658                for b in (a + 1)..4 {
8659                    world.add_distance_constraint(dc(a, b));
8660                }
8661            }
8662            world.remove_body(victim).expect("in range");
8663            let n = world.bodies.len();
8664            assert_eq!(n, 3);
8665            // 残 3 body の全 pair = 3 本、自己参照なし、範囲内
8666            assert_eq!(world.distance_constraints.len(), 3, "victim {victim}");
8667            for c in &world.distance_constraints {
8668                assert!(
8669                    c.body_a < n && c.body_b < n,
8670                    "victim {victim}: dangling {:?}",
8671                    (c.body_a, c.body_b)
8672                );
8673                assert_ne!(c.body_a, c.body_b, "victim {victim}: self constraint");
8674            }
8675            // 残った body の position 集合 = 元 4 点から victim を除いたもの
8676            let mut xs: Vec<i64> = world.bodies.iter().map(|b| b.position.x.hi).collect();
8677            xs.sort_unstable();
8678            let mut expected: Vec<i64> = (0..4).filter(|&i| i != victim as i64).collect();
8679            expected.sort_unstable();
8680            assert_eq!(xs, expected);
8681        }
8682    }
8683
8684    #[test]
8685    fn remove_body_with_joint_drops_joint_and_rebuilds_islands() {
8686        let mut world = quiet_world();
8687        for i in 0..3 {
8688            world.add_body(RigidBody::new_dynamic(v3(i, 0, 0), Fix128::ONE));
8689        }
8690        let j = crate::joint::BallJoint::new(1, 2, Vec3Fix::ZERO, Vec3Fix::ZERO);
8691        world.add_joint(Joint::Ball(j));
8692        let j2 = crate::joint::BallJoint::new(0, 2, Vec3Fix::ZERO, Vec3Fix::ZERO);
8693        world.add_joint(Joint::Ball(j2));
8694        assert_eq!(world.joint_count(), 2);
8695        world.remove_body(1).expect("in range");
8696        // (1-2) は消え、(0-2) は (0,1) に remap
8697        assert_eq!(world.joint_count(), 1);
8698        assert_eq!(world.joints[0].bodies(), (0, 1));
8699        // island は新 body 数で再構築され、joint で 0 と 1 が同一 island
8700        assert_eq!(world.islands.find(0), world.islands.find(1));
8701    }
8702
8703    // ---- detect_collisions --------------------------------------------
8704
8705    /// |a - b| が 2^-60 未満 (normalize の sqrt 丸め 数 ulp を許容、変異の差は桁違いなので検出力は不変)
8706    fn near(a: Fix128, b: Fix128) -> bool {
8707        let d = (a - b).abs();
8708        d.hi == 0 && d.lo < (1u64 << 4)
8709    }
8710
8711    fn assert_near_vec(a: Vec3Fix, b: Vec3Fix, what: &str) {
8712        assert!(
8713            near(a.x, b.x) && near(a.y, b.y) && near(a.z, b.z),
8714            "{what}: {a:?} vs {b:?}"
8715        );
8716    }
8717
8718    /// 半径 r の dynamic 球 2 個を x 軸上 dist 離して置く
8719    fn two_spheres(dist_num: i64, dist_den: i64, r_a: Fix128, r_b: Fix128) -> PhysicsWorld {
8720        let mut world = quiet_world();
8721        world.add_body_with_radius(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE), r_a);
8722        world.add_body_with_radius(
8723            RigidBody::new_dynamic(
8724                Vec3Fix::new(r(dist_num, dist_den), Fix128::ZERO, Fix128::ZERO),
8725                Fix128::ONE,
8726            ),
8727            r_b,
8728        );
8729        world
8730    }
8731
8732    #[test]
8733    fn detect_collisions_overlapping_spheres_produce_exact_contact() {
8734        // r=1, r=1、距離 1.5 → depth = 0.5、normal = -x (B → A、`Contact::normal` 契約)、
8735        // point_a = pos_a - n·r_a = (1,0,0)、point_b = pos_b + n·r_b = (0.5,0,0)
8736        // (1.2.0 以前は a → b で pin していた = 押し込み方向の bug を test が固定していた)
8737        let mut world = two_spheres(3, 2, Fix128::ONE, Fix128::ONE);
8738        world.detect_collisions();
8739        assert_eq!(world.contact_constraints.len(), 1);
8740        let c = &world.contact_constraints[0];
8741        assert_eq!((c.body_a, c.body_b), (0, 1));
8742        assert!(
8743            near(c.contact.depth, r(1, 2)),
8744            "depth {:?}",
8745            c.contact.depth
8746        );
8747        assert_near_vec(c.contact.normal, v3(-1, 0, 0), "normal");
8748        assert_near_vec(c.contact.point_a, v3(1, 0, 0), "point_a");
8749        assert_near_vec(
8750            c.contact.point_b,
8751            Vec3Fix::new(r(1, 2), Fix128::ZERO, Fix128::ZERO),
8752            "point_b",
8753        );
8754        // event も 1 件、rel_vel は 0
8755        assert_eq!(world.contact_events().len(), 1);
8756        assert!(world.trigger_events().is_empty());
8757    }
8758
8759    #[test]
8760    fn detect_collisions_asymmetric_radii_split_points_correctly() {
8761        // r_a = 2, r_b = 1/2、距離 2 → combined 2.5、depth 0.5
8762        let mut world = two_spheres(2, 1, Fix128::from_int(2), r(1, 2));
8763        world.detect_collisions();
8764        assert_eq!(world.contact_constraints.len(), 1);
8765        let c = world.contact_constraints[0].contact;
8766        assert!(near(c.depth, r(1, 2)), "depth {:?}", c.depth);
8767        assert_near_vec(c.point_a, v3(2, 0, 0), "point_a"); // a.pos + n * r_a
8768        assert_near_vec(
8769            c.point_b,
8770            Vec3Fix::new(r(3, 2), Fix128::ZERO, Fix128::ZERO),
8771            "point_b",
8772        ); // b.pos - n * r_b
8773    }
8774
8775    #[test]
8776    fn detect_collisions_touching_exactly_is_not_a_contact() {
8777        // dist == combined (2 == 1+1): `<` は false → 接触なし (`<=` 変異はここで死ぬ)
8778        let mut world = two_spheres(2, 1, Fix128::ONE, Fix128::ONE);
8779        world.detect_collisions();
8780        assert!(world.contact_constraints.is_empty());
8781        assert!(world.contact_events().is_empty());
8782        // 少しでも近ければ接触
8783        let mut world2 = two_spheres(199, 100, Fix128::ONE, Fix128::ONE);
8784        world2.detect_collisions();
8785        assert_eq!(world2.contact_constraints.len(), 1);
8786    }
8787
8788    #[test]
8789    fn detect_collisions_coincident_centers_are_skipped() {
8790        // dist == 0 → normal 不定なので skip (`!dist.is_zero()`)
8791        let mut world = two_spheres(0, 1, Fix128::ONE, Fix128::ONE);
8792        world.detect_collisions();
8793        assert!(world.contact_constraints.is_empty());
8794    }
8795
8796    #[test]
8797    fn detect_collisions_relative_velocity_is_reported_along_normal() {
8798        let mut world = two_spheres(3, 2, Fix128::ONE, Fix128::ONE);
8799        world.bodies[0].velocity = v3(2, 7, 0); // a → b 方向 +2 (y 成分は normal 直交で無視)
8800        world.bodies[1].velocity = v3(-1, 0, 0);
8801        world.detect_collisions();
8802        let ev = &world.contact_events()[0];
8803        // rel_vel = (v_a - v_b) · n = (3, 7, 0) · (-1,0,0) = -3 (接近中は負、B → A normal)
8804        assert!(
8805            near(ev.relative_velocity, Fix128::from_int(-3)),
8806            "rel_vel {:?}",
8807            ev.relative_velocity
8808        );
8809        assert!(near(ev.depth, r(1, 2)), "depth {:?}", ev.depth);
8810    }
8811
8812    #[test]
8813    fn detect_collisions_sensor_reports_trigger_instead_of_constraint() {
8814        for sensor_side in 0..2 {
8815            let mut world = two_spheres(3, 2, Fix128::ONE, Fix128::ONE);
8816            world.bodies[sensor_side].is_sensor = true;
8817            world.detect_collisions();
8818            assert!(world.contact_constraints.is_empty(), "side {sensor_side}");
8819            assert_eq!(world.trigger_events().len(), 1, "side {sensor_side}");
8820            assert_eq!(world.contact_events().len(), 1, "side {sensor_side}");
8821        }
8822    }
8823
8824    #[test]
8825    fn detect_collisions_static_static_pair_is_skipped_but_static_dynamic_is_not() {
8826        let mut world = quiet_world();
8827        world.add_body_with_radius(RigidBody::new_static(Vec3Fix::ZERO), Fix128::ONE);
8828        world.add_body_with_radius(
8829            RigidBody::new_static(Vec3Fix::new(r(3, 2), Fix128::ZERO, Fix128::ZERO)),
8830            Fix128::ONE,
8831        );
8832        world.detect_collisions();
8833        assert!(world.contact_constraints.is_empty());
8834
8835        let mut mixed = quiet_world();
8836        mixed.add_body_with_radius(RigidBody::new_static(Vec3Fix::ZERO), Fix128::ONE);
8837        mixed.add_body_with_radius(
8838            RigidBody::new_dynamic(
8839                Vec3Fix::new(r(3, 2), Fix128::ZERO, Fix128::ZERO),
8840                Fix128::ONE,
8841            ),
8842            Fix128::ONE,
8843        );
8844        mixed.detect_collisions();
8845        assert_eq!(mixed.contact_constraints.len(), 1);
8846    }
8847
8848    #[test]
8849    fn detect_collisions_filter_blocks_same_group_and_masked_layers() {
8850        // 同一 group (非 0) → 衝突しない
8851        let mut same_group = two_spheres(3, 2, Fix128::ONE, Fix128::ONE);
8852        let g = CollisionFilter {
8853            layer: 1,
8854            mask: u32::MAX,
8855            group: 7,
8856        };
8857        same_group.set_body_filter(0, g);
8858        same_group.set_body_filter(1, g);
8859        same_group.detect_collisions();
8860        assert!(same_group.contact_constraints.is_empty());
8861
8862        // 片方向 mask 不一致 (a.layer & b.mask == 0) → 衝突しない (`&&` の右側も検査)
8863        for side in 0..2 {
8864            let mut masked = two_spheres(3, 2, Fix128::ONE, Fix128::ONE);
8865            masked.set_body_filter(
8866                side,
8867                CollisionFilter {
8868                    layer: 1 << 5,
8869                    mask: u32::MAX,
8870                    group: 0,
8871                },
8872            );
8873            masked.set_body_filter(
8874                1 - side,
8875                CollisionFilter {
8876                    layer: 1,
8877                    mask: !(1 << 5),
8878                    group: 0,
8879                },
8880            );
8881            masked.detect_collisions();
8882            assert!(masked.contact_constraints.is_empty(), "side {side}");
8883        }
8884
8885        // 異なる group + 相互 mask 一致 → 衝突する
8886        let mut ok = two_spheres(3, 2, Fix128::ONE, Fix128::ONE);
8887        ok.set_body_filter(
8888            0,
8889            CollisionFilter {
8890                layer: 1,
8891                mask: u32::MAX,
8892                group: 1,
8893            },
8894        );
8895        ok.set_body_filter(
8896            1,
8897            CollisionFilter {
8898                layer: 1,
8899                mask: u32::MAX,
8900                group: 2,
8901            },
8902        );
8903        ok.detect_collisions();
8904        assert_eq!(ok.contact_constraints.len(), 1);
8905    }
8906
8907    #[test]
8908    fn detect_collisions_both_sleeping_skipped_one_sleeping_wakes() {
8909        let mut both = two_spheres(3, 2, Fix128::ONE, Fix128::ONE);
8910        both.islands.sleep_data[0].state = crate::sleeping::SleepState::Sleeping;
8911        both.islands.sleep_data[1].state = crate::sleeping::SleepState::Sleeping;
8912        both.detect_collisions();
8913        assert!(both.contact_constraints.is_empty());
8914
8915        let mut one = two_spheres(3, 2, Fix128::ONE, Fix128::ONE);
8916        one.islands.sleep_data[0].state = crate::sleeping::SleepState::Sleeping;
8917        one.detect_collisions();
8918        assert_eq!(one.contact_constraints.len(), 1);
8919        // 接触で起こされる
8920        assert!(!one.islands.is_sleeping(0));
8921    }
8922
8923    #[test]
8924    fn detect_collisions_needs_two_collidable_bodies() {
8925        // body 1 個 / radius 付き 1 個だけ → 何も起きない
8926        let mut single = quiet_world();
8927        single.add_body_with_radius(
8928            RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE),
8929            Fix128::ONE,
8930        );
8931        single.detect_collisions();
8932        assert!(single.contact_constraints.is_empty());
8933
8934        let mut one_radius = quiet_world();
8935        one_radius.add_body_with_radius(
8936            RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE),
8937            Fix128::ONE,
8938        );
8939        one_radius.add_body(RigidBody::new_dynamic(
8940            Vec3Fix::new(r(1, 2), Fix128::ZERO, Fix128::ZERO),
8941            Fix128::ONE,
8942        ));
8943        one_radius.detect_collisions();
8944        assert!(one_radius.contact_constraints.is_empty());
8945    }
8946
8947    #[test]
8948    fn detect_collisions_uses_combined_material_of_pair() {
8949        let mut world = two_spheres(3, 2, Fix128::ONE, Fix128::ONE);
8950        world.detect_collisions();
8951        let c = world.contact_constraints[0];
8952        let combined = world.combined_material(0, 1);
8953        assert_eq!(c.friction, combined.friction);
8954        assert_eq!(c.restitution, combined.restitution);
8955    }
8956
8957    // ---- solve_distance_constraints -----------------------------------
8958
8959    #[test]
8960    fn solve_distance_constraints_rigid_splits_error_by_inverse_mass() {
8961        // A (inv 1) at 0、B (inv 3) at x=4、target 2 → error 2、w_sum 4、lambda 1/2
8962        // A += n*λ*1 = +0.5、B -= n*λ*3 = -1.5 → 距離 4 - 2 = 2 で一発収束
8963        let mut world = quiet_world();
8964        let a = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
8965        let b = world.add_body(RigidBody::new_dynamic(v3(4, 0, 0), Fix128::ONE));
8966        world.bodies[b].inv_mass = Fix128::from_int(3);
8967        let mut c = dc(a, b);
8968        c.target_distance = Fix128::from_int(2);
8969        world.add_distance_constraint(c);
8970
8971        world.solve_distance_constraints(r(1, 4));
8972
8973        assert_eq!(
8974            world.bodies[a].position,
8975            Vec3Fix::new(r(1, 2), Fix128::ZERO, Fix128::ZERO)
8976        );
8977        assert_eq!(
8978            world.bodies[b].position,
8979            Vec3Fix::new(r(5, 2), Fix128::ZERO, Fix128::ZERO)
8980        );
8981        assert_eq!(world.distance_constraints[0].cached_lambda, r(1, 2));
8982    }
8983
8984    #[test]
8985    fn solve_distance_constraints_static_side_does_not_move() {
8986        // A static at 0、B (inv 1) at 4、target 1 → error 3、w_sum 1 → B -= 3 → x = 1
8987        let mut world = quiet_world();
8988        let a = world.add_body(RigidBody::new_static(Vec3Fix::ZERO));
8989        let b = world.add_body(RigidBody::new_dynamic(v3(4, 0, 0), Fix128::ONE));
8990        world.add_distance_constraint(dc(a, b)); // target 1
8991
8992        world.solve_distance_constraints(r(1, 4));
8993
8994        assert_eq!(world.bodies[a].position, Vec3Fix::ZERO);
8995        assert_eq!(world.bodies[b].position, v3(1, 0, 0));
8996    }
8997
8998    #[test]
8999    fn solve_distance_constraints_negative_error_pushes_apart() {
9000        // 距離 1 < target 3 → error -2、両 inv 1 → w_sum 2、λ = -1 → A -= 1 (x=-1)、B += 1 (x=2)
9001        let mut world = quiet_world();
9002        let a = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
9003        let b = world.add_body(RigidBody::new_dynamic(v3(1, 0, 0), Fix128::ONE));
9004        let mut c = dc(a, b);
9005        c.target_distance = Fix128::from_int(3);
9006        world.add_distance_constraint(c);
9007
9008        world.solve_distance_constraints(r(1, 4));
9009
9010        assert_eq!(world.bodies[a].position, v3(-1, 0, 0));
9011        assert_eq!(world.bodies[b].position, v3(2, 0, 0));
9012    }
9013
9014    #[test]
9015    fn solve_distance_constraints_compliance_softens_correction() {
9016        // compliance 1/16、dt 1/4 → compliance_term = (1/16)/(1/16) = 1
9017        // A static、B inv 1 at 4、target 2 → error 2、w_sum = 1 + 1 = 2、λ = 1 → B: 4 - 1 = 3
9018        let mut world = quiet_world();
9019        let a = world.add_body(RigidBody::new_static(Vec3Fix::ZERO));
9020        let b = world.add_body(RigidBody::new_dynamic(v3(4, 0, 0), Fix128::ONE));
9021        let mut c = dc(a, b);
9022        c.target_distance = Fix128::from_int(2);
9023        c.compliance = r(1, 16);
9024        world.add_distance_constraint(c);
9025
9026        world.solve_distance_constraints(r(1, 4));
9027
9028        assert_eq!(world.bodies[b].position, v3(3, 0, 0));
9029        assert_eq!(world.distance_constraints[0].cached_lambda, Fix128::ONE);
9030    }
9031
9032    #[test]
9033    fn solve_distance_constraints_accumulates_lambda_xpbd() {
9034        // substep 途中 (前 iteration で λ = 1 が累積済)、compliance_term = 1 (上と同設定)
9035        // dλ = (C - α̃λ) / w_sum = (2 - 1*1) / 2 = 1/2 → B: 4 - 0.5 = 3.5、λ = 1 + 1/2 = 3/2
9036        // (1.2.0 以前は λ を上書きしていたので cached_lambda = 1/2 になっていた)
9037        let mut world = quiet_world();
9038        let a = world.add_body(RigidBody::new_static(Vec3Fix::ZERO));
9039        let b = world.add_body(RigidBody::new_dynamic(v3(4, 0, 0), Fix128::ONE));
9040        let mut c = dc(a, b);
9041        c.target_distance = Fix128::from_int(2);
9042        c.compliance = r(1, 16);
9043        c.cached_lambda = Fix128::ONE;
9044        world.add_distance_constraint(c);
9045
9046        world.solve_distance_constraints(r(1, 4));
9047
9048        assert_eq!(
9049            world.bodies[b].position,
9050            Vec3Fix::new(r(7, 2), Fix128::ZERO, Fix128::ZERO)
9051        );
9052        assert_eq!(world.distance_constraints[0].cached_lambda, r(3, 2));
9053    }
9054
9055    #[test]
9056    fn reset_lambdas_zeroes_distance_lambda() {
9057        let mut world = quiet_world();
9058        let a = world.add_body(RigidBody::new_static(Vec3Fix::ZERO));
9059        let b = world.add_body(RigidBody::new_dynamic(v3(4, 0, 0), Fix128::ONE));
9060        let mut c = dc(a, b);
9061        c.cached_lambda = Fix128::from_int(3);
9062        world.add_distance_constraint(c);
9063        world.reset_lambdas();
9064        assert_eq!(world.distance_constraints[0].cached_lambda, Fix128::ZERO);
9065    }
9066
9067    #[test]
9068    fn xpbd_compliant_extension_is_iteration_independent() {
9069        // 1 kg を compliance 0.01 (k = 100 N/m) の距離拘束で吊る → 定常伸び mg/k = 0.1 m
9070        // iterations を 1..16 と変えても同じ伸びに収束する (1.2.0 以前は iter 倍で伸び半減)
9071        let run = |iters: usize| {
9072            let mut world = quiet_world();
9073            world.config.gravity = v3(0, -10, 0);
9074            world.config.damping = Fix128::ONE;
9075            world.config.substeps = 4;
9076            world.config.iterations = iters;
9077            let a = world.add_body(RigidBody::new_static(Vec3Fix::ZERO));
9078            let b = world.add_body(RigidBody::new_dynamic(v3(0, -1, 0), Fix128::ONE));
9079            world.bodies[b].linear_damping = r(9, 10); // settle the oscillation
9080            let mut c = dc(a, b);
9081            c.target_distance = Fix128::ONE;
9082            c.compliance = r(1, 100);
9083            world.add_distance_constraint(c);
9084            for _ in 0..600 {
9085                world.step(r(1, 60));
9086            }
9087            (-world.bodies[b].position.y - Fix128::ONE).to_f64()
9088        };
9089        let ext: Vec<f64> = [1usize, 2, 4, 8, 16].iter().map(|&i| run(i)).collect();
9090        for (i, e) in ext.iter().enumerate() {
9091            assert!(
9092                (e - 0.1).abs() < 0.01,
9093                "iterations index {i}: extension {e} m, expected 0.1 ± 0.01 (all: {ext:?})"
9094            );
9095        }
9096    }
9097
9098    #[test]
9099    fn solve_distance_constraints_anchor_offsets_are_rotated_into_world() {
9100        // B に local anchor (0,1,0)、B を z 軸 π 回転 → world anchor は B.pos + (0,-1,0)
9101        // A static at 0、B at (0,2,0) → anchor_b = (0,1,0)、距離 1 == target 1 → 補正なし
9102        let mut world = quiet_world();
9103        let a = world.add_body(RigidBody::new_static(Vec3Fix::ZERO));
9104        let b = world.add_body(RigidBody::new_dynamic(v3(0, 2, 0), Fix128::ONE));
9105        world.bodies[b].rotation =
9106            QuatFix::new(Fix128::ZERO, Fix128::ZERO, Fix128::ONE, Fix128::ZERO);
9107        let mut c = dc(a, b);
9108        c.local_anchor_b = v3(0, 1, 0);
9109        world.add_distance_constraint(c);
9110
9111        world.solve_distance_constraints(r(1, 4));
9112
9113        assert_eq!(world.bodies[b].position, v3(0, 2, 0));
9114        // 回転していなければ anchor は (0,3,0)、距離 3 → error 2 → B は -2 動く
9115        let mut unrotated = quiet_world();
9116        let a2 = unrotated.add_body(RigidBody::new_static(Vec3Fix::ZERO));
9117        let b2 = unrotated.add_body(RigidBody::new_dynamic(v3(0, 2, 0), Fix128::ONE));
9118        let mut c2 = dc(a2, b2);
9119        c2.local_anchor_b = v3(0, 1, 0);
9120        unrotated.add_distance_constraint(c2);
9121        unrotated.solve_distance_constraints(r(1, 4));
9122        assert_near_vec(unrotated.bodies[b2].position, v3(0, 0, 0), "unrotated B");
9123        // normalize の sqrt 丸め 数 ulp
9124    }
9125
9126    #[test]
9127    fn solve_distance_constraints_skips_coincident_and_double_static() {
9128        let mut world = quiet_world();
9129        let a = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
9130        let b = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE)); // 同一点
9131        world.add_distance_constraint(dc(a, b));
9132        world.solve_distance_constraints(r(1, 4));
9133        assert_eq!(world.bodies[a].position, Vec3Fix::ZERO);
9134        assert_eq!(world.bodies[b].position, Vec3Fix::ZERO);
9135        assert_eq!(world.distance_constraints[0].cached_lambda, Fix128::ZERO);
9136
9137        let mut statics = quiet_world();
9138        let s1 = statics.add_body(RigidBody::new_static(Vec3Fix::ZERO));
9139        let s2 = statics.add_body(RigidBody::new_static(v3(5, 0, 0)));
9140        statics.add_distance_constraint(dc(s1, s2));
9141        statics.solve_distance_constraints(r(1, 4));
9142        assert_eq!(statics.bodies[s2].position, v3(5, 0, 0));
9143        assert_eq!(statics.distance_constraints[0].cached_lambda, Fix128::ZERO);
9144    }
9145
9146    // ---- solve_contact_constraints ------------------------------------
9147
9148    fn contact_world(inv_a: Fix128, inv_b: Fix128, depth: Fix128) -> PhysicsWorld {
9149        let mut world = quiet_world();
9150        let a = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
9151        let b = world.add_body(RigidBody::new_dynamic(v3(1, 0, 0), Fix128::ONE));
9152        world.bodies[a].inv_mass = inv_a;
9153        world.bodies[b].inv_mass = inv_b;
9154        let contact = Contact {
9155            depth,
9156            normal: v3(1, 0, 0),
9157            point_a: Vec3Fix::ZERO,
9158            point_b: Vec3Fix::ZERO,
9159        };
9160        world
9161            .contact_constraints
9162            .push(ContactConstraint::new(a, b, contact));
9163        world
9164    }
9165
9166    #[test]
9167    fn solve_contact_constraints_pushes_apart_by_depth_weighted_by_inverse_mass() {
9168        // inv 1 / inv 3、depth 1 → λ = 1、inv_w = 1/4 → A += n * 1/4、B -= n * 3/4
9169        let mut world = contact_world(Fix128::ONE, Fix128::from_int(3), Fix128::ONE);
9170        world.solve_contact_constraints(r(1, 4));
9171        assert_eq!(
9172            world.bodies[0].position,
9173            Vec3Fix::new(r(1, 4), Fix128::ZERO, Fix128::ZERO)
9174        );
9175        assert_eq!(
9176            world.bodies[1].position,
9177            Vec3Fix::new(r(1, 4), Fix128::ZERO, Fix128::ZERO)
9178        );
9179        assert_eq!(world.contact_constraints[0].cached_lambda, Fix128::ONE);
9180    }
9181
9182    #[test]
9183    fn solve_contact_constraints_zero_or_negative_depth_is_skipped() {
9184        for depth in [Fix128::ZERO, -r(1, 2)] {
9185            let mut world = contact_world(Fix128::ONE, Fix128::ONE, depth);
9186            world.solve_contact_constraints(r(1, 4));
9187            assert_eq!(world.bodies[0].position, Vec3Fix::ZERO, "depth {depth:?}");
9188            assert_eq!(world.bodies[1].position, v3(1, 0, 0), "depth {depth:?}");
9189            assert_eq!(world.contact_constraints[0].cached_lambda, Fix128::ZERO);
9190        }
9191    }
9192
9193    #[test]
9194    fn solve_contact_constraints_static_side_absorbs_nothing() {
9195        // A static (inv 0)、B inv 1、depth 1/2 → B -= 1/2、A 不動
9196        let mut world = contact_world(Fix128::ZERO, Fix128::ONE, r(1, 2));
9197        world.solve_contact_constraints(r(1, 4));
9198        assert_eq!(world.bodies[0].position, Vec3Fix::ZERO);
9199        assert_eq!(
9200            world.bodies[1].position,
9201            Vec3Fix::new(r(1, 2), Fix128::ZERO, Fix128::ZERO)
9202        );
9203        // 両 static は skip、cached_lambda も 0 のまま
9204        let mut both = contact_world(Fix128::ZERO, Fix128::ZERO, r(1, 2));
9205        both.solve_contact_constraints(r(1, 4));
9206        assert_eq!(both.bodies[1].position, v3(1, 0, 0));
9207        assert_eq!(both.contact_constraints[0].cached_lambda, Fix128::ZERO);
9208    }
9209
9210    #[test]
9211    fn solve_contact_constraints_accumulates_lambda_and_pushes_depth_once() {
9212        // depth 1/2、λ 累積済 1 (≥ depth) → dλ ≤ 0 → 動かない、λ 不変
9213        let mut world = contact_world(Fix128::ONE, Fix128::ONE, r(1, 2));
9214        world.contact_constraints[0].cached_lambda = Fix128::ONE;
9215        world.solve_contact_constraints(r(1, 4));
9216        assert_eq!(world.bodies[0].position, Vec3Fix::ZERO);
9217        assert_eq!(world.contact_constraints[0].cached_lambda, Fix128::ONE);
9218
9219        // λ == depth ちょうど → dλ = 0 → 動かない
9220        let mut edge = contact_world(Fix128::ONE, Fix128::ONE, r(1, 2));
9221        edge.contact_constraints[0].cached_lambda = r(1, 2);
9222        edge.solve_contact_constraints(r(1, 4));
9223        assert_eq!(edge.bodies[0].position, Vec3Fix::ZERO);
9224
9225        // λ 1/4 → dλ = 1/2 - 1/4 = 1/4 → A += 1/8 (w_a / w_sum = 1/2)、λ = 1/2
9226        let mut partial = contact_world(Fix128::ONE, Fix128::ONE, r(1, 2));
9227        partial.contact_constraints[0].cached_lambda = r(1, 4);
9228        partial.solve_contact_constraints(r(1, 4));
9229        assert_eq!(
9230            partial.bodies[0].position,
9231            Vec3Fix::new(r(1, 8), Fix128::ZERO, Fix128::ZERO)
9232        );
9233        assert_eq!(partial.contact_constraints[0].cached_lambda, r(1, 2));
9234
9235        // 4 iteration 回しても総押し出し量は depth 1 回分 (1.2.0 以前は iteration 毎に再 push)
9236        let mut iters = contact_world(Fix128::ONE, Fix128::ONE, r(1, 2));
9237        for _ in 0..4 {
9238            iters.solve_contact_constraints(r(1, 4));
9239        }
9240        assert_eq!(
9241            iters.bodies[0].position,
9242            Vec3Fix::new(r(1, 4), Fix128::ZERO, Fix128::ZERO)
9243        );
9244        assert_eq!(
9245            iters.bodies[1].position,
9246            Vec3Fix::new(r(3, 4), Fix128::ZERO, Fix128::ZERO)
9247        );
9248    }
9249
9250    #[test]
9251    fn solve_contact_constraints_sensor_skips_response() {
9252        for side in 0..2 {
9253            let mut world = contact_world(Fix128::ONE, Fix128::ONE, Fix128::ONE);
9254            world.bodies[side].is_sensor = true;
9255            world.solve_contact_constraints(r(1, 4));
9256            assert_eq!(world.bodies[0].position, Vec3Fix::ZERO, "side {side}");
9257            assert_eq!(world.bodies[1].position, v3(1, 0, 0), "side {side}");
9258        }
9259    }
9260
9261    #[cfg(feature = "std")]
9262    #[test]
9263    fn solve_contact_constraints_pre_solve_hook_can_discard_and_modifier_can_rescale() {
9264        // hook が false → skip
9265        let mut vetoed = contact_world(Fix128::ONE, Fix128::ONE, Fix128::ONE);
9266        vetoed.add_pre_solve_hook(Box::new(|_a, _b, _c| false));
9267        vetoed.apply_contact_filters();
9268        vetoed.solve_contact_constraints(r(1, 4));
9269        assert_eq!(vetoed.bodies[0].position, Vec3Fix::ZERO);
9270
9271        // hook が true → 通常通り
9272        let mut allowed = contact_world(Fix128::ONE, Fix128::ONE, Fix128::ONE);
9273        allowed.add_pre_solve_hook(Box::new(|_a, _b, _c| true));
9274        allowed.apply_contact_filters();
9275        allowed.solve_contact_constraints(r(1, 4));
9276        assert_eq!(
9277            allowed.bodies[0].position,
9278            Vec3Fix::new(r(1, 2), Fix128::ZERO, Fix128::ZERO)
9279        );
9280
9281        // modifier が depth を半分に → λ = 1/2 → A += 1/4
9282        struct Halve;
9283        impl ContactModifier for Halve {
9284            fn modify_contact(
9285                &self,
9286                _a: usize,
9287                _b: usize,
9288                c: &mut Contact,
9289                _f: &mut Fix128,
9290                _r: &mut Fix128,
9291            ) -> bool {
9292                c.depth = c.depth.half();
9293                true
9294            }
9295        }
9296        let mut halved = contact_world(Fix128::ONE, Fix128::ONE, Fix128::ONE);
9297        halved.add_contact_modifier(Box::new(Halve));
9298        halved.apply_contact_filters();
9299        halved.solve_contact_constraints(r(1, 4));
9300        assert_eq!(
9301            halved.bodies[0].position,
9302            Vec3Fix::new(r(1, 4), Fix128::ZERO, Fix128::ZERO)
9303        );
9304
9305        // modifier が false → 捨てる
9306        struct Discard;
9307        impl ContactModifier for Discard {
9308            fn modify_contact(
9309                &self,
9310                _a: usize,
9311                _b: usize,
9312                _c: &mut Contact,
9313                _f: &mut Fix128,
9314                _r: &mut Fix128,
9315            ) -> bool {
9316                false
9317            }
9318        }
9319        let mut discarded = contact_world(Fix128::ONE, Fix128::ONE, Fix128::ONE);
9320        discarded.add_contact_modifier(Box::new(Discard));
9321        discarded.apply_contact_filters();
9322        discarded.solve_contact_constraints(r(1, 4));
9323        assert_eq!(discarded.bodies[0].position, Vec3Fix::ZERO);
9324    }
9325
9326    // ---- RigidBody force / torque / impulse ---------------------------
9327
9328    /// inv_mass 2、inv_inertia (1, 2, 4) の body、初期 velocity / angular を非零にして加算を観測
9329    fn loaded_body() -> RigidBody {
9330        let mut b = RigidBody::new_dynamic(v3(1, 2, 3), Fix128::ONE);
9331        b.inv_mass = Fix128::from_int(2);
9332        b.inv_inertia = v3(1, 2, 4);
9333        b.velocity = v3(10, 20, 30);
9334        b.angular_velocity = v3(-1, -2, -3);
9335        b
9336    }
9337
9338    #[test]
9339    fn add_force_scales_by_inverse_mass_and_dt() {
9340        let mut b = loaded_body();
9341        b.add_force(v3(4, -8, 16), r(1, 4)); // Δv = F * 2 * 1/4 = (2, -4, 8)
9342        assert_eq!(b.velocity, v3(12, 16, 38));
9343        assert_eq!(b.angular_velocity, v3(-1, -2, -3));
9344        let mut st = RigidBody::new_static(Vec3Fix::ZERO);
9345        st.add_force(v3(4, 4, 4), Fix128::ONE);
9346        assert_eq!(st.velocity, Vec3Fix::ZERO);
9347    }
9348
9349    #[test]
9350    fn add_torque_scales_each_axis_by_inverse_inertia_and_dt() {
9351        let mut b = loaded_body();
9352        b.add_torque(v3(8, 8, 8), r(1, 4)); // Δω = (8*1, 8*2, 8*4) * 1/4 = (2, 4, 8)
9353        assert_eq!(b.angular_velocity, v3(1, 2, 5));
9354        assert_eq!(b.velocity, v3(10, 20, 30));
9355        let mut st = RigidBody::new_static(Vec3Fix::ZERO);
9356        st.add_torque(v3(8, 8, 8), Fix128::ONE);
9357        assert_eq!(st.angular_velocity, Vec3Fix::ZERO);
9358    }
9359
9360    #[test]
9361    fn apply_impulse_scales_by_inverse_mass_only() {
9362        let mut b = loaded_body();
9363        b.apply_impulse(v3(1, 2, 3)); // Δv = (2, 4, 6)
9364        assert_eq!(b.velocity, v3(12, 24, 36));
9365        assert_eq!(b.angular_velocity, v3(-1, -2, -3));
9366        let mut st = RigidBody::new_static(Vec3Fix::ZERO);
9367        st.apply_impulse(v3(1, 1, 1));
9368        assert_eq!(st.velocity, Vec3Fix::ZERO);
9369    }
9370
9371    #[test]
9372    fn apply_impulse_at_adds_lever_arm_torque() {
9373        // position (1,2,3)、point (2,2,3) → r = (1,0,0)、impulse (0,3,0) → r × J = (0,0,3)
9374        // Δω = (0*1, 0*2, 3*4) = (0,0,12)、Δv = J * 2 = (0,6,0)
9375        let mut b = loaded_body();
9376        b.apply_impulse_at(v3(0, 3, 0), v3(2, 2, 3));
9377        assert_eq!(b.velocity, v3(10, 26, 30));
9378        assert_eq!(b.angular_velocity, v3(-1, -2, 9));
9379
9380        // r = (0,1,0)、J = (0,0,5) → r × J = (5,0,0) → Δω = (5,0,0)
9381        let mut c = loaded_body();
9382        c.apply_impulse_at(v3(0, 0, 5), v3(1, 3, 3));
9383        assert_eq!(c.angular_velocity, v3(4, -2, -3));
9384        assert_eq!(c.velocity, v3(10, 20, 40));
9385
9386        // r = (0,0,1)、J = (7,0,0) → r × J = (0,7,0) → Δω = (0,14,0)
9387        let mut d = loaded_body();
9388        d.apply_impulse_at(v3(7, 0, 0), v3(1, 2, 4));
9389        assert_eq!(d.angular_velocity, v3(-1, 12, -3));
9390
9391        // 着力点 = 重心 → torque なし
9392        let mut e = loaded_body();
9393        e.apply_impulse_at(v3(7, 0, 0), v3(1, 2, 3));
9394        assert_eq!(e.angular_velocity, v3(-1, -2, -3));
9395        assert_eq!(e.velocity, v3(24, 20, 30));
9396
9397        let mut st = RigidBody::new_static(Vec3Fix::ZERO);
9398        st.apply_impulse_at(v3(1, 1, 1), v3(1, 0, 0));
9399        assert_eq!(st.velocity, Vec3Fix::ZERO);
9400        assert_eq!(st.angular_velocity, Vec3Fix::ZERO);
9401    }
9402
9403    // ---- serialize / deserialize ---------------------------------------
9404
9405    fn snapshot_world() -> PhysicsWorld {
9406        let mut world = quiet_world();
9407        let mut a = RigidBody::new_dynamic(v3(1, 2, 3), Fix128::ONE);
9408        a.velocity = v3(4, 5, 6);
9409        a.angular_velocity = v3(7, 8, 9);
9410        a.rotation = QuatFix::from_axis_angle(v3(1, 1, 0), r(1, 3));
9411        world.add_body(a);
9412        let mut b = RigidBody::new_dynamic(v3(-1, -2, -3), Fix128::ONE);
9413        b.velocity = Vec3Fix::new(r(1, 3), r(-2, 7), r(5, 11));
9414        world.add_body(b);
9415        world
9416    }
9417
9418    #[test]
9419    fn deserialize_state_roundtrip_restores_every_field_bit_exact() {
9420        let src = snapshot_world();
9421        let bytes = src.serialize_state();
9422        // v3: header 12 + body ごと 208 + body ごと sleep 5 + world ごと flag 1 + fingerprint 8
9423        assert_eq!(bytes.len(), 12 + 2 * 208 + 2 * 5 + 1 + 8);
9424        let mut dst = quiet_world();
9425        dst.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
9426        dst.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
9427        assert!(dst.deserialize_state(&bytes));
9428        for i in 0..2 {
9429            assert_eq!(dst.bodies[i].position, src.bodies[i].position, "pos {i}");
9430            assert_eq!(dst.bodies[i].velocity, src.bodies[i].velocity, "vel {i}");
9431            assert_eq!(dst.bodies[i].rotation, src.bodies[i].rotation, "rot {i}");
9432            assert_eq!(
9433                dst.bodies[i].angular_velocity, src.bodies[i].angular_velocity,
9434                "ang {i}"
9435            );
9436        }
9437        // 並列配列は body 数に同期
9438        assert_eq!(dst.body_collision_radii.len(), 2);
9439        assert_eq!(dst.body_filters.len(), 2);
9440        assert_eq!(dst.body_materials.len(), 2);
9441    }
9442
9443    #[test]
9444    fn deserialize_state_every_single_byte_flip_changes_some_field() {
9445        // 1 byte でも壊れた snapshot は必ずどれかの field に反映される (silent 無視 / 別 field 混入を検出)
9446        //
9447        // ⚠️ v3 format は 5 領域に分かれる — **どの領域にも silent に無視される
9448        // byte が無い**ことを領域ごとに確かめる
9449        //   [0..12)                              header (magic / version / reserved) → **拒否される**
9450        //   [12, 12+n*208)                       body の運動状態 → 該当 body の該当 field が変わる
9451        //   [12+n*208, 12+n*208+n*5)             sleep 状態 → 該当 body の sleep_data が変わる
9452        //   [12+n*208+n*5, 12+n*208+n*5+1)       overflow flag (1 byte)
9453        //   [12+n*208+n*5+1, ..+9)               population fingerprint (8 byte) → **拒否される**
9454        let src = snapshot_world();
9455        let bytes = src.serialize_state();
9456        let body_end = 12 + 2 * 208;
9457        let flag_pos = bytes.len() - 9;
9458
9459        // 領域 1: header — 1 byte 壊れたら受け付けない
9460        for pos in 0..12 {
9461            let mut bad = bytes.clone();
9462            bad[pos] ^= 0x01;
9463            let mut dst = snapshot_world();
9464            assert!(
9465                !dst.deserialize_state(&bad),
9466                "header byte {pos} を壊しても受け付けている (silent に無視される byte がある)"
9467            );
9468        }
9469
9470        // 領域 4: flag (fingerprint 直前の 1 byte) — 壊したら overflow_detected が変わる
9471        //
9472        // ⚠️ v2 で足した領域も「silent に無視される byte が無い」不変条件の
9473        // 対象に含める (flag は 0/1 なので 0x01 の flip で必ず値が変わる)
9474        {
9475            let pos = flag_pos;
9476            let mut bad = bytes.clone();
9477            bad[pos] ^= 0x01;
9478            let mut dst = snapshot_world();
9479            assert!(dst.deserialize_state(&bad), "flag byte {pos}");
9480            assert_ne!(
9481                dst.overflow_detected(),
9482                src.overflow_detected(),
9483                "flag byte {pos} を壊しても overflow_detected が変わらない"
9484            );
9485        }
9486
9487        // 領域 5: population fingerprint (末尾 8 byte) — 1 byte でも壊れたら
9488        // 拒否する (= count は一致しているが fingerprint 不一致で fail-fast、
9489        // gap #3 の population 検査そのもの)
9490        for pos in bytes.len() - 8..bytes.len() {
9491            let mut bad = bytes.clone();
9492            bad[pos] ^= 0x01;
9493            let mut dst = snapshot_world();
9494            assert!(
9495                !dst.deserialize_state(&bad),
9496                "fingerprint byte {pos} を壊しても受け付けている"
9497            );
9498            // 拒否時は元の状態を保つ
9499            assert_eq!(dst.bodies[0].position, src.bodies[0].position);
9500        }
9501
9502        // 領域 3: sleep — 1 byte 壊れたら sleep_data が変わる (flag の手前まで)
9503        for pos in body_end..flag_pos {
9504            let mut bad = bytes.clone();
9505            bad[pos] ^= 0x01;
9506            let mut dst = snapshot_world();
9507            assert!(dst.deserialize_state(&bad), "sleep byte {pos}");
9508            let i = (pos - body_end) / 5;
9509            assert_ne!(
9510                dst.islands.sleep_data[i], src.islands.sleep_data[i],
9511                "sleep byte {pos} は body {i} の sleep_data を変えるべき"
9512            );
9513        }
9514
9515        // 領域 2: body の運動状態
9516        for pos in 12..body_end {
9517            let mut bad = bytes.clone();
9518            bad[pos] ^= 0x01;
9519            let mut dst = snapshot_world();
9520            assert!(dst.deserialize_state(&bad), "byte {pos}");
9521            let body = (pos - 12) / 208;
9522            let field = ((pos - 12) % 208) / 48; // 0 pos / 1 vel / 2 rot(64 byte = 2 slot 相当は 128..192) / 3 ang
9523            let changed_body = (0..2)
9524                .filter(|&i| {
9525                    let (s, d) = (&src.bodies[i], &dst.bodies[i]);
9526                    s.position != d.position
9527                        || s.velocity != d.velocity
9528                        || s.rotation != d.rotation
9529                        || s.angular_velocity != d.angular_velocity
9530                })
9531                .collect::<Vec<_>>();
9532            assert_eq!(
9533                changed_body,
9534                vec![body],
9535                "byte {pos} must change only body {body}"
9536            );
9537            let (s, d) = (&src.bodies[body], &dst.bodies[body]);
9538            let off = (pos - 12) % 208;
9539            match off {
9540                0..=47 => assert!(
9541                    s.position != d.position && s.velocity == d.velocity,
9542                    "byte {pos} → position"
9543                ),
9544                48..=95 => assert!(
9545                    s.velocity != d.velocity && s.position == d.position,
9546                    "byte {pos} → velocity"
9547                ),
9548                96..=159 => assert!(
9549                    s.rotation != d.rotation && s.angular_velocity == d.angular_velocity,
9550                    "byte {pos} → rotation"
9551                ),
9552                _ => assert!(
9553                    s.angular_velocity != d.angular_velocity && s.rotation == d.rotation,
9554                    "byte {pos} → angular"
9555                ),
9556            }
9557            let _ = field;
9558        }
9559    }
9560
9561    #[test]
9562    fn deserialize_state_rejects_short_or_mismatched_input() {
9563        let src = snapshot_world();
9564        let bytes = src.serialize_state();
9565        let mut dst = snapshot_world();
9566        assert!(!dst.deserialize_state(&bytes[..11])); // header 未満 (v1 は 12 byte)
9567        assert!(!dst.deserialize_state(&bytes[..12 + 208 + 100])); // 2 体目が途中で切れる
9568        assert!(
9569            !dst.deserialize_state(&bytes[..bytes.len() - 1]),
9570            "末尾 (v3 の fingerprint) が 1 byte 欠けても拒否する (長さ検査が body 数から一意に決まる)"
9571        );
9572        // ⚠️ 旧 format (先頭が body 数、header なし) は magic 不一致で拒否される
9573        // 検査しないと **旧 blob の先頭 4 byte を magic と読んで誤解釈する**
9574        let mut legacy = 2u32.to_le_bytes().to_vec();
9575        legacy.extend_from_slice(&bytes[12..12 + 2 * 208]);
9576        assert!(
9577            !dst.deserialize_state(&legacy),
9578            "旧 format の blob は magic 不一致で拒否されるべき"
9579        );
9580        // ⚠️ v1 blob (magic は合うが version が 1) も拒否される
9581        // version を検査しないと **flag 1 byte 短い blob を v2 として読む**
9582        let mut v1 = bytes.clone();
9583        v1[4] = 1;
9584        v1.truncate(v1.len() - 1);
9585        assert!(
9586            !dst.deserialize_state(&v1),
9587            "v1 blob は version 不一致で拒否されるべき"
9588        );
9589        let mut one_body = quiet_world();
9590        one_body.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
9591        assert!(!one_body.deserialize_state(&bytes)); // count 不一致
9592                                                      // 拒否時は元の状態を保つ (先頭 body だけ書き換わっていない)
9593        assert_eq!(one_body.bodies[0].position, Vec3Fix::ZERO);
9594        // ぴったり 1 body 分 (header + 運動状態 + sleep) なら OK
9595        let mut exact = quiet_world();
9596        exact.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
9597        let mut one = PhysicsWorld::STATE_MAGIC.to_vec();
9598        one.extend_from_slice(&PhysicsWorld::STATE_VERSION.to_le_bytes());
9599        one.extend_from_slice(&0u16.to_le_bytes());
9600        one.extend_from_slice(&1u32.to_le_bytes());
9601        one.extend_from_slice(&bytes[12..12 + 208]);
9602        one.extend_from_slice(&bytes[12 + 2 * 208..12 + 2 * 208 + 5]);
9603        one.push(0); // v2: flag
9604        one.extend_from_slice(&exact.population_fingerprint().to_le_bytes()); // v3: fingerprint
9605        assert!(exact.deserialize_state(&one));
9606        assert_eq!(exact.bodies[0].position, v3(1, 2, 3));
9607    }
9608
9609    // ---- integrate_positions -----------------------------------------
9610
9611    #[test]
9612    fn integrate_positions_applies_gravity_damping_and_predicts_position() {
9613        let mut world = quiet_world();
9614        world.config.gravity = v3(0, -8, 0);
9615        world.config.damping = r(1, 2);
9616        let i = world.add_body(RigidBody::new_dynamic(v3(1, 1, 1), Fix128::ONE));
9617        world.bodies[i].velocity = v3(4, 0, 0);
9618        world.bodies[i].gravity_scale = r(1, 2);
9619        world.bodies[i].linear_damping = r(1, 2);
9620        let dt = r(1, 4);
9621
9622        world.integrate_positions(dt);
9623
9624        // v = (4,0,0) + g*scale*dt = (4, -1, 0)  (damping は integrate では掛からない、1.2.0)
9625        // pos = (1,1,1) + v*dt = (2, 3/4, 1)、prev = (1,1,1)
9626        let b = &world.bodies[i];
9627        assert_eq!(
9628            b.velocity,
9629            Vec3Fix::new(Fix128::from_int(4), -Fix128::ONE, Fix128::ZERO)
9630        );
9631        assert_eq!(
9632            b.position,
9633            Vec3Fix::new(Fix128::from_int(2), r(3, 4), Fix128::ONE)
9634        );
9635        assert_eq!(b.prev_position, v3(1, 1, 1));
9636
9637        // frame damping: global 1/2 * per-body 1/2 → (1, -1/4, 0)
9638        world.apply_frame_damping();
9639        assert_eq!(
9640            world.bodies[i].velocity,
9641            Vec3Fix::new(Fix128::ONE, -r(1, 4), Fix128::ZERO)
9642        );
9643    }
9644
9645    #[test]
9646    fn integrate_positions_angular_damping_and_rotation_prediction() {
9647        let mut world = quiet_world();
9648        world.config.damping = Fix128::ONE;
9649        let i = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
9650        world.bodies[i].angular_velocity = v3(0, 0, 2);
9651        world.bodies[i].angular_damping = r(1, 2); // frame damping → (0,0,1)
9652        let dt = r(1, 4);
9653
9654        world.integrate_positions(dt);
9655
9656        let b = &world.bodies[i];
9657        // integrate は damping を掛けない (1.2.0)、角速度は 2 のまま
9658        assert_eq!(b.angular_velocity, v3(0, 0, 2));
9659        // 回転は z 軸 angle = 2 * 1/4 の quaternion (正規化済) と一致
9660        let expected = QuatFix::from_axis_angle(v3(0, 0, 1), r(1, 2))
9661            .mul(QuatFix::IDENTITY)
9662            .normalize();
9663        assert_eq!(b.rotation, expected);
9664        assert_eq!(b.prev_rotation, QuatFix::IDENTITY);
9665        assert!(b.rotation != QuatFix::IDENTITY);
9666        world.apply_frame_damping();
9667        assert_eq!(world.bodies[i].angular_velocity, v3(0, 0, 1));
9668    }
9669
9670    #[test]
9671    fn apply_frame_damping_skips_static_kinematic_and_sleeping() {
9672        let mut world = quiet_world();
9673        world.config.damping = r(1, 2);
9674        let st = world.add_body(RigidBody::new_static(Vec3Fix::ZERO));
9675        let mut kin = RigidBody::new_static(Vec3Fix::ZERO);
9676        kin.body_type = BodyType::Kinematic;
9677        let ki = world.add_body(kin);
9678        let dy = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
9679        for &i in &[st, ki, dy] {
9680            world.bodies[i].velocity = v3(2, 0, 0);
9681            world.bodies[i].angular_velocity = v3(0, 4, 0);
9682        }
9683        world.apply_frame_damping();
9684        assert_eq!(world.bodies[st].velocity, v3(2, 0, 0));
9685        assert_eq!(world.bodies[ki].velocity, v3(2, 0, 0));
9686        assert_eq!(world.bodies[dy].velocity, v3(1, 0, 0));
9687        assert_eq!(world.bodies[dy].angular_velocity, v3(0, 2, 0));
9688    }
9689
9690    #[test]
9691    fn free_fall_is_substep_independent_with_frame_damping() {
9692        // 重力 -10、damping ONE、1 秒 (60 frame) → y = -5.0、substeps を変えても同じ
9693        // (1.2.0 以前は substep 内 damping で終端速度 g*h*d/(1-d) が substeps 依存だった)
9694        let fall = |substeps: usize| {
9695            let mut world = quiet_world();
9696            world.config.gravity = v3(0, -10, 0);
9697            world.config.damping = Fix128::ONE;
9698            world.config.substeps = substeps;
9699            let b = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
9700            for _ in 0..60 {
9701                world.step(r(1, 60));
9702            }
9703            world.bodies[b].position.y.to_f64()
9704        };
9705        let ys: Vec<f64> = [1usize, 2, 4, 8, 16].iter().map(|&n| fall(n)).collect();
9706        for y in &ys {
9707            // symplectic Euler の離散化誤差 g*dt/2 = 1/12 → -5 - 0.083、substeps 増で -5 に寄る
9708            assert!(
9709                (y + 5.0).abs() < 0.1,
9710                "free fall y = {y}, expected -5.0 ± 0.1 (all: {ys:?})"
9711            );
9712        }
9713        // 既定 config (damping 0.99 / frame) でも 1 秒で 3.5 m 以上落ちる (旧: -1.64 m)
9714        let mut world = PhysicsWorld::new(PhysicsConfig::default());
9715        let b = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
9716        for _ in 0..60 {
9717            world.step(r(1, 60));
9718        }
9719        let y = world.bodies[b].position.y.to_f64();
9720        assert!(y < -3.5, "default config fell only to y = {y}");
9721    }
9722
9723    #[test]
9724    fn integrate_positions_static_kinematic_and_sleeping_paths() {
9725        let mut world = quiet_world();
9726        world.config.gravity = v3(0, -8, 0);
9727        let st = world.add_body(RigidBody::new_static(v3(0, 5, 0)));
9728        let mut kin = RigidBody::new_static(v3(0, 0, 0));
9729        kin.body_type = BodyType::Kinematic;
9730        kin.set_kinematic_target(v3(2, 0, 0), QuatFix::IDENTITY);
9731        let ki = world.add_body(kin);
9732        let sl = world.add_body(RigidBody::new_dynamic(v3(9, 9, 9), Fix128::ONE));
9733        world.islands.sleep_data[sl].state = crate::sleeping::SleepState::Sleeping;
9734        let dt = r(1, 4);
9735
9736        world.integrate_positions(dt);
9737
9738        // static: 完全不動
9739        assert_eq!(world.bodies[st].position, v3(0, 5, 0));
9740        assert_eq!(world.bodies[st].velocity, Vec3Fix::ZERO);
9741        // kinematic: target に snap、velocity = Δ/dt = (8,0,0)、prev は旧位置
9742        assert_eq!(world.bodies[ki].position, v3(2, 0, 0));
9743        assert_eq!(world.bodies[ki].velocity, v3(8, 0, 0));
9744        assert_eq!(world.bodies[ki].prev_position, Vec3Fix::ZERO);
9745        // sleeping: 重力なし、prev == position
9746        assert_eq!(world.bodies[sl].position, v3(9, 9, 9));
9747        assert_eq!(world.bodies[sl].prev_position, v3(9, 9, 9));
9748        assert_eq!(world.bodies[sl].velocity, Vec3Fix::ZERO);
9749    }
9750
9751    // ---- step (substep 分割 / 重力の閉形式) ----------------------------
9752
9753    #[test]
9754    fn step_free_fall_matches_closed_form_over_substeps() {
9755        // gravity -8、damping 1、substeps 4、dt 1 → substep_dt 1/4
9756        // 半陰的 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
9757        let mut world = quiet_world();
9758        world.config.gravity = v3(0, -8, 0);
9759        world.config.damping = Fix128::ONE;
9760        world.config.substeps = 4;
9761        world.config.iterations = 1;
9762        let i = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
9763
9764        world.step(Fix128::ONE);
9765
9766        assert_eq!(world.bodies[i].velocity, v3(0, -8, 0));
9767        assert_eq!(world.bodies[i].position, v3(0, -5, 0));
9768        // substeps 1 なら x = -8 (1 回で落ちる)
9769        let mut single = quiet_world();
9770        single.config.gravity = v3(0, -8, 0);
9771        single.config.damping = Fix128::ONE;
9772        single.config.substeps = 1;
9773        let j = single.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
9774        single.step(Fix128::ONE);
9775        assert_eq!(single.bodies[j].position, v3(0, -8, 0));
9776    }
9777
9778    // ---- setters / counters ------------------------------------------
9779
9780    #[test]
9781    fn combined_material_uses_registered_pair_override_and_ignores_out_of_range() {
9782        let mut world = quiet_world();
9783        let a = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
9784        let b = world.add_body(RigidBody::new_dynamic(v3(1, 0, 0), Fix128::ONE));
9785        let metal = world.material_table.register_metal();
9786        world
9787            .material_table
9788            .set_pair_override(0, metal, r(1, 8), r(7, 8));
9789        world.set_body_material(b, metal);
9790        let c = world.combined_material(a, b);
9791        assert_eq!((c.friction, c.restitution), (r(1, 8), r(7, 8)));
9792        // 順序を入れ替えても同じ (対称)
9793        let c2 = world.combined_material(b, a);
9794        assert_eq!((c2.friction, c2.restitution), (r(1, 8), r(7, 8)));
9795        // 範囲外 index は DEFAULT_MATERIAL 扱い = (default, metal) の組ではなく (default, default)
9796        let d = world.combined_material(a, 99);
9797        let dd = world.material_table.combine(0, 0);
9798        assert_eq!((d.friction, d.restitution), (dd.friction, dd.restitution));
9799        // set_body_material の範囲外は無視
9800        world.set_body_material(99, metal);
9801        assert_eq!(world.body_materials.len(), 2);
9802    }
9803
9804    #[test]
9805    fn body_setters_ignore_out_of_range_and_apply_in_range() {
9806        let mut world = quiet_world();
9807        let a = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
9808        world.set_body_collision_radius(a, r(3, 2));
9809        assert_eq!(world.body_collision_radii[a], Some(r(3, 2)));
9810        world.set_body_collision_radius(5, Fix128::ONE);
9811        assert_eq!(world.body_collision_radii.len(), 1);
9812        world.clear_body_collision_radius(5);
9813        assert_eq!(world.body_collision_radii[a], Some(r(3, 2)));
9814        world.clear_body_collision_radius(a);
9815        assert_eq!(world.body_collision_radii[a], None);
9816
9817        let f = CollisionFilter {
9818            layer: 4,
9819            mask: 8,
9820            group: 2,
9821        };
9822        world.set_body_filter(a, f);
9823        assert_eq!(world.body_filters[a], f);
9824        world.set_body_filter(5, CollisionFilter::DEFAULT);
9825        assert_eq!(world.body_filters[a], f);
9826    }
9827
9828    #[test]
9829    fn active_body_count_excludes_sleeping() {
9830        let mut world = quiet_world();
9831        for i in 0..3 {
9832            world.add_body(RigidBody::new_dynamic(v3(i, 0, 0), Fix128::ONE));
9833        }
9834        assert_eq!(world.active_body_count(), 3);
9835        world.islands.sleep_data[1].state = crate::sleeping::SleepState::Sleeping;
9836        assert_eq!(world.active_body_count(), 2);
9837        world.islands.sleep_data[0].state = crate::sleeping::SleepState::Sleeping;
9838        world.islands.sleep_data[2].state = crate::sleeping::SleepState::Sleeping;
9839        assert_eq!(world.active_body_count(), 0);
9840    }
9841
9842    // ---- hook / modifier clearing, RigidBody builders -------------------
9843
9844    #[cfg(feature = "std")]
9845    #[test]
9846    fn clear_pre_solve_hooks_restores_normal_contact_response() {
9847        // veto hook が入っている間は contact が捨てられて A は動かない
9848        let mut world = contact_world(Fix128::ONE, Fix128::ONE, Fix128::ONE);
9849        world.add_pre_solve_hook(Box::new(|_a, _b, _c| false));
9850        world.add_pre_solve_hook(Box::new(|_a, _b, _c| false));
9851        assert_eq!(world.pre_solve_hooks.len(), 2);
9852        world.apply_contact_filters();
9853        world.solve_contact_constraints(r(1, 4));
9854        assert_eq!(world.bodies[0].position, Vec3Fix::ZERO);
9855
9856        // clear 後は hook 0 個 → 通常通り λ = 1、inv_w = 1/2 → A += 1/2
9857        world.clear_pre_solve_hooks();
9858        assert!(world.pre_solve_hooks.is_empty());
9859        world.apply_contact_filters();
9860        world.solve_contact_constraints(r(1, 4));
9861        assert_eq!(
9862            world.bodies[0].position,
9863            Vec3Fix::new(r(1, 2), Fix128::ZERO, Fix128::ZERO)
9864        );
9865        assert_eq!(
9866            world.bodies[1].position,
9867            Vec3Fix::new(r(1, 2), Fix128::ZERO, Fix128::ZERO)
9868        );
9869    }
9870
9871    #[cfg(feature = "std")]
9872    #[test]
9873    fn clear_contact_modifiers_restores_normal_contact_response() {
9874        struct Discard;
9875        impl ContactModifier for Discard {
9876            fn modify_contact(
9877                &self,
9878                _a: usize,
9879                _b: usize,
9880                _c: &mut Contact,
9881                _f: &mut Fix128,
9882                _r: &mut Fix128,
9883            ) -> bool {
9884                false
9885            }
9886        }
9887        let mut world = contact_world(Fix128::ONE, Fix128::ONE, Fix128::ONE);
9888        world.add_contact_modifier(Box::new(Discard));
9889        assert_eq!(world.contact_modifiers.len(), 1);
9890        world.apply_contact_filters();
9891        world.solve_contact_constraints(r(1, 4));
9892        assert_eq!(world.bodies[0].position, Vec3Fix::ZERO);
9893
9894        world.clear_contact_modifiers();
9895        assert!(world.contact_modifiers.is_empty());
9896        world.apply_contact_filters();
9897        world.solve_contact_constraints(r(1, 4));
9898        assert_eq!(
9899            world.bodies[0].position,
9900            Vec3Fix::new(r(1, 2), Fix128::ZERO, Fix128::ZERO)
9901        );
9902    }
9903
9904    #[test]
9905    fn set_angular_velocity_overwrites_and_drives_rotation_prediction() {
9906        let mut world = quiet_world();
9907        let i = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
9908        world.bodies[i].angular_velocity = v3(5, 5, 5);
9909        world.bodies[i].set_angular_velocity(v3(0, 0, 2));
9910        assert_eq!(world.bodies[i].angular_velocity, v3(0, 0, 2));
9911        // 直接代入 (apply_impulse 系と違い static でも通る)
9912        let mut st = RigidBody::new_static(Vec3Fix::ZERO);
9913        st.set_angular_velocity(v3(1, 0, 0));
9914        assert_eq!(st.angular_velocity, v3(1, 0, 0));
9915
9916        // 設定した ω が積分で使われる: z 軸 angle = 2 * 1/4 の回転
9917        world.integrate_positions(r(1, 4));
9918        let expected = QuatFix::from_axis_angle(v3(0, 0, 1), r(1, 2))
9919            .mul(QuatFix::IDENTITY)
9920            .normalize();
9921        assert_eq!(world.bodies[i].rotation, expected);
9922    }
9923
9924    #[test]
9925    fn with_angular_damping_is_stored_and_applied_once_per_frame_by_step() {
9926        let body = RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE).with_angular_damping(r(1, 2));
9927        assert_eq!(body.angular_damping, r(1, 2));
9928        assert_eq!(body.linear_damping, Fix128::ONE);
9929
9930        // substeps を変えても damping は frame 毎に 1 回だけ: ω_damped == ω_undamped * 1 * 1/2
9931        // (substep 毎なら substeps=4 で 1/16 になる)
9932        for substeps in [1usize, 4] {
9933            let mut undamped = quiet_world();
9934            undamped.config.substeps = substeps;
9935            let u = undamped.add_body(
9936                RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE).with_velocity(v3(3, 0, 0)),
9937            );
9938            undamped.bodies[u].set_angular_velocity(v3(0, 0, 2));
9939
9940            let mut damped = quiet_world();
9941            damped.config.substeps = substeps;
9942            let d = damped.add_body(
9943                RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE)
9944                    .with_velocity(v3(3, 0, 0))
9945                    .with_angular_damping(r(1, 2)),
9946            );
9947            damped.bodies[d].set_angular_velocity(v3(0, 0, 2));
9948
9949            undamped.step(r(1, 4));
9950            damped.step(r(1, 4));
9951
9952            let expected = undamped.bodies[u].angular_velocity * Fix128::ONE * r(1, 2);
9953            assert_eq!(
9954                damped.bodies[d].angular_velocity, expected,
9955                "substeps {substeps}"
9956            );
9957            // 角 damping は線速度に影響しない
9958            assert_eq!(
9959                damped.bodies[d].velocity, undamped.bodies[u].velocity,
9960                "substeps {substeps}"
9961            );
9962            let ratio = damped.bodies[d].angular_velocity.z.to_f64()
9963                / undamped.bodies[u].angular_velocity.z.to_f64();
9964            assert!(
9965                (ratio - 0.5).abs() < 1e-9,
9966                "substeps {substeps}: ratio {ratio}"
9967            );
9968        }
9969    }
9970
9971    #[test]
9972    fn with_sensor_stores_flag_and_sensor_body_skips_contact_response() {
9973        let sensor = RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE).with_sensor(true);
9974        assert!(sensor.is_sensor);
9975        assert!(!sensor.with_sensor(false).is_sensor);
9976
9977        // contact_world と同じ配置だが A を builder で sensor にする
9978        let mut world = quiet_world();
9979        let a =
9980            world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE).with_sensor(true));
9981        let b = world.add_body(RigidBody::new_dynamic(v3(1, 0, 0), Fix128::ONE));
9982        let contact = Contact {
9983            depth: Fix128::ONE,
9984            normal: v3(1, 0, 0),
9985            point_a: Vec3Fix::ZERO,
9986            point_b: Vec3Fix::ZERO,
9987        };
9988        world
9989            .contact_constraints
9990            .push(ContactConstraint::new(a, b, contact));
9991        world.solve_contact_constraints(r(1, 4));
9992        assert_eq!(world.bodies[a].position, Vec3Fix::ZERO);
9993        assert_eq!(world.bodies[b].position, v3(1, 0, 0));
9994
9995        // 自動検出でも sensor は trigger event になり contact constraint を作らない
9996        let mut auto = quiet_world();
9997        auto.add_body_with_radius(
9998            RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE).with_sensor(true),
9999            Fix128::ONE,
10000        );
10001        auto.add_body_with_radius(
10002            RigidBody::new_dynamic(
10003                Vec3Fix::new(r(3, 2), Fix128::ZERO, Fix128::ZERO),
10004                Fix128::ONE,
10005            ),
10006            Fix128::ONE,
10007        );
10008        auto.detect_collisions();
10009        assert!(auto.contact_constraints.is_empty());
10010        assert_eq!(auto.trigger_events().len(), 1);
10011    }
10012
10013    #[test]
10014    fn raycast_far_intersection_off_center_and_max_distance_cull_inside_aabb() {
10015        // 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
10016        let world = sphere_world(&[v3(10, 0, 0)]);
10017        assert_eq!(
10018            world.raycast(v3(9, 0, 0), v3(1, 0, 0), Fix128::from_int(100)),
10019            Some((0, Fix128::from_int(3)))
10020        );
10021        // 球 (10,0,0) r=3 に y=2 の平行 ray: AABB [7,13] は max 7.5 の ray box と重なるが t = 10 - √5 ≈ 7.76 > 7.5 → None
10022        let mut big = quiet_world();
10023        big.add_body_with_radius(RigidBody::new_static(v3(10, 0, 0)), Fix128::from_int(3));
10024        assert_eq!(big.raycast(v3(0, 2, 0), v3(1, 0, 0), r(15, 2)), None);
10025        assert!(big
10026            .raycast(v3(0, 2, 0), v3(1, 0, 0), Fix128::from_int(8))
10027            .is_some());
10028    }
10029
10030    // ---- mutation-kill tests (cargo-mutants missed list, 2026-09-15) ----
10031
10032    /// `RigidBody::set_velocity` / `set_position` / `set_rotation` → `()`,
10033    /// `mass` → `Default` / `/` → `*`, `speed` → `Default`: every setter stores
10034    /// the exact value (position / rotation also reset the XPBD `prev_*`),
10035    /// mass 4 has `inv_mass` 1/4 and `mass()` must invert it back to 4 (the
10036    /// `*` mutant returns 1/4), a static body reports 0, and |(3, -4, 0)| = 5.
10037    #[test]
10038    fn rigid_body_setters_and_mass_speed_report_exact_values() {
10039        let mut body = RigidBody::new(v3(1, 1, 1), Fix128::from_int(4));
10040        assert_eq!(body.inv_mass, r(1, 4));
10041        assert_eq!(body.mass(), Fix128::from_int(4));
10042        assert_eq!(RigidBody::new_static(Vec3Fix::ZERO).mass(), Fix128::ZERO);
10043
10044        body.set_velocity(v3(3, -4, 0));
10045        assert_eq!(body.velocity, v3(3, -4, 0));
10046        assert_eq!(body.speed(), Fix128::from_int(5));
10047
10048        body.set_position(v3(7, -8, 9));
10049        assert_eq!(body.position, v3(7, -8, 9));
10050        assert_eq!(body.prev_position, v3(7, -8, 9));
10051
10052        let q = QuatFix::from_axis_angle(v3(0, 1, 0), r(1, 3));
10053        assert_ne!(q, QuatFix::IDENTITY);
10054        body.set_rotation(q);
10055        assert_eq!(body.rotation, q);
10056        assert_eq!(body.prev_rotation, q);
10057    }
10058
10059    /// `apply_impulse_at` line 297 / `add_torque` line 321 `*` → `/` on the x
10060    /// component: with `inv_inertia = (4, 4, 4)` and torque x = 2 the product
10061    /// is 8 (impulse) / 8 · dt = 4 (torque, dt = 1/2); the quotient would be
10062    /// 1/2 / 1/4. Lever arm r = (0, 1, 0), impulse (0, 0, 2) → r × F = (2, 0, 0).
10063    #[test]
10064    fn apply_impulse_at_and_add_torque_multiply_x_by_inverse_inertia() {
10065        let mut body = RigidBody::new(v3(5, 5, 5), Fix128::ONE);
10066        body.inv_inertia = v3(4, 4, 4);
10067        body.apply_impulse_at(v3(0, 0, 2), v3(5, 6, 5));
10068        assert_eq!(body.velocity, v3(0, 0, 2));
10069        assert_eq!(body.angular_velocity, v3(8, 0, 0));
10070
10071        let mut spun = RigidBody::new(Vec3Fix::ZERO, Fix128::ONE);
10072        spun.inv_inertia = v3(4, 4, 4);
10073        spun.add_torque(v3(2, 0, 0), r(1, 2));
10074        assert_eq!(spun.angular_velocity, v3(4, 0, 0));
10075    }
10076
10077    /// `body_count` → `0` / `1`, `get_body` → `Some(Default)`, `get_body_mut` →
10078    /// `None` / `Some(Default)`: counts 0 → 2 → 1 across add / remove, the
10079    /// accessors return the body at that slot (position (3, 4, 5), not the
10080    /// default origin) and `None` past the end; a write through `get_body_mut`
10081    /// must land in `bodies`.
10082    #[test]
10083    fn body_count_and_get_body_accessors_track_the_body_vector() {
10084        let mut world = quiet_world();
10085        assert_eq!(world.body_count(), 0);
10086        assert!(world.get_body(0).is_none());
10087        assert!(world.get_body_mut(0).is_none());
10088
10089        world.add_body(RigidBody::new_dynamic(v3(3, 4, 5), Fix128::ONE));
10090        world.add_body(RigidBody::new_dynamic(v3(6, 7, 8), Fix128::ONE));
10091        assert_eq!(world.body_count(), 2);
10092        assert_eq!(world.get_body(0).map(|b| b.position), Some(v3(3, 4, 5)));
10093        assert_eq!(world.get_body(1).map(|b| b.position), Some(v3(6, 7, 8)));
10094        assert!(world.get_body(2).is_none());
10095
10096        world
10097            .get_body_mut(1)
10098            .expect("body 1 exists")
10099            .set_velocity(v3(-1, -2, -3));
10100        assert_eq!(world.bodies[1].velocity, v3(-1, -2, -3));
10101        assert!(world.get_body_mut(2).is_none());
10102
10103        assert!(world.remove_body(0).is_some());
10104        assert_eq!(world.body_count(), 1);
10105    }
10106
10107    /// `set_body_material` line 1109, `set_body_collision_radius` line 1213,
10108    /// `clear_body_collision_radius` line 1220, `set_body_filter` line 1227,
10109    /// `combined_material` lines 1413 / 1418 `<` → `<=`: an index equal to the
10110    /// body count is out of range and must be ignored (the mutants index one
10111    /// past the end and panic). `body_filter` → `Default` is killed by reading
10112    /// back a non-default filter.
10113    #[test]
10114    fn index_equal_to_body_count_is_out_of_range_for_every_setter() {
10115        let mut world = quiet_world();
10116        let a = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
10117        let len = world.body_count();
10118        let metal = world.material_table.register_metal();
10119
10120        world.set_body_material(len, metal);
10121        world.set_body_collision_radius(len, Fix128::ONE);
10122        world.clear_body_collision_radius(len);
10123        world.set_body_filter(len, CollisionFilter::NONE);
10124        assert_eq!(
10125            world.body_materials,
10126            vec![crate::material::DEFAULT_MATERIAL]
10127        );
10128        assert_eq!(world.body_collision_radii, vec![None]);
10129        assert_eq!(world.body_filters, vec![CollisionFilter::DEFAULT]);
10130
10131        world.set_body_material(a, metal);
10132        let default_metal = world
10133            .material_table
10134            .combine(crate::material::DEFAULT_MATERIAL, metal);
10135        let default_default = world.material_table.combine(0, 0);
10136        assert_ne!(default_metal.friction, default_default.friction);
10137        for (x, y, want) in [
10138            (len, a, default_metal),
10139            (a, len, default_metal),
10140            (len, len, default_default),
10141        ] {
10142            let c = world.combined_material(x, y);
10143            assert_eq!(
10144                (c.friction, c.restitution),
10145                (want.friction, want.restitution),
10146                "({x}, {y}) must treat the out-of-range side as the default material"
10147            );
10148        }
10149        let custom = CollisionFilter {
10150            layer: 1 << 3,
10151            mask: 1 << 9,
10152            group: 5,
10153        };
10154        world.set_body_filter(a, custom);
10155        assert_eq!(world.body_filter(a), custom);
10156        assert_eq!(world.body_filter(len), CollisionFilter::DEFAULT);
10157    }
10158
10159    /// `begin_frame` / `end_frame` → `()` and `clear_contacts` → `()`: a cached
10160    /// manifold ages by one per `begin_frame` (4 calls → `stale_frames == 4`)
10161    /// and `end_frame` prunes it once it exceeds `max_stale_frames` (3);
10162    /// `clear_contacts` empties the constraint list.
10163    #[test]
10164    fn contact_cache_frame_lifecycle_ages_and_prunes_manifolds() {
10165        let mut world = quiet_world();
10166        let a = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
10167        let b = world.add_body(RigidBody::new_dynamic(v3(1, 0, 0), Fix128::ONE));
10168        push_contact(&mut world, a, b, r(1, 2), r(1, 2));
10169        let c = world.contact_constraints.pop().expect("pushed");
10170        world.add_contact(c);
10171        assert_eq!(world.contact_constraints.len(), 1);
10172        assert_eq!(world.contact_cache.manifold_count(), 1);
10173        assert_eq!(world.contact_cache.manifolds[0].stale_frames, 0);
10174
10175        for _ in 0..4 {
10176            world.begin_frame();
10177        }
10178        assert_eq!(world.contact_cache.manifolds[0].stale_frames, 4);
10179        world.end_frame();
10180        assert_eq!(world.contact_cache.manifold_count(), 0);
10181
10182        world.clear_contacts();
10183        assert!(world.contact_constraints.is_empty());
10184    }
10185
10186    /// `remove_joint` → `None` and line 1179 `>=` → `<`: two joints get
10187    /// indices 0 / 1, index 2 is out of range (`None`), removing 0 returns
10188    /// `Some` and leaves one joint, after which index 1 is out of range.
10189    #[test]
10190    fn add_and_remove_joint_roundtrip() {
10191        use crate::joint::{BallJoint, Joint};
10192        let mut world = quiet_world();
10193        let a = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
10194        let b = world.add_body(RigidBody::new_dynamic(v3(1, 0, 0), Fix128::ONE));
10195        let j0 = world.add_joint(Joint::Ball(BallJoint::new(
10196            a,
10197            b,
10198            Vec3Fix::ZERO,
10199            Vec3Fix::ZERO,
10200        )));
10201        let j1 = world.add_joint(Joint::Ball(BallJoint::new(
10202            b,
10203            a,
10204            v3(1, 0, 0),
10205            Vec3Fix::ZERO,
10206        )));
10207        assert_eq!((j0, j1), (0, 1));
10208        assert_eq!(world.joint_count(), 2);
10209        assert!(world.remove_joint(2).is_none());
10210        assert_eq!(world.joint_count(), 2);
10211        let removed = world.remove_joint(0).expect("joint 0 exists");
10212        assert_eq!(removed.bodies(), (a, b));
10213        assert_eq!(world.joint_count(), 1);
10214        assert_eq!(world.joints[0].bodies(), (b, a));
10215        assert!(world.remove_joint(1).is_none());
10216    }
10217
10218    /// `add_force_field` → `0` / `1`, `remove_force_field` → `None` and line
10219    /// 1203 `>=` → `<`, `step` line 1697 `!` deleted: fields get indices 0 / 1,
10220    /// index 2 is `None`, removing 1 leaves one field, and `step` applies the
10221    /// remaining directional field once per frame: F = (2, 0, 0), m = 1,
10222    /// dt = 1/4 → v = 1/2, then 8 substeps of 1/32 move x by 8 · (1/2)(1/32) =
10223    /// 1/8 (quiet world: no gravity, no damping).
10224    #[test]
10225    fn force_field_add_remove_and_step_applies_directional_force() {
10226        use crate::force::{ForceField, ForceFieldInstance};
10227        let mut world = quiet_world();
10228        let body = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
10229        let wind = ForceFieldInstance::new(ForceField::Directional {
10230            direction: Vec3Fix::UNIT_X,
10231            strength: Fix128::from_int(2),
10232        });
10233        let drag = ForceFieldInstance::new(ForceField::Drag {
10234            coefficient: Fix128::ONE,
10235        });
10236        assert_eq!(world.add_force_field(wind), 0);
10237        assert_eq!(world.add_force_field(drag), 1);
10238        assert_eq!(world.force_fields.len(), 2);
10239        assert!(world.remove_force_field(2).is_none());
10240        assert!(matches!(
10241            world.remove_force_field(1).map(|f| f.field),
10242            Some(ForceField::Drag { .. })
10243        ));
10244        assert!(world.remove_force_field(1).is_none());
10245        assert_eq!(world.force_fields.len(), 1);
10246
10247        world.step(r(1, 4));
10248        assert_eq!(
10249            world.bodies[body].velocity,
10250            Vec3Fix::new(r(1, 2), Fix128::ZERO, Fix128::ZERO)
10251        );
10252        assert_eq!(
10253            world.bodies[body].position,
10254            Vec3Fix::new(r(1, 8), Fix128::ZERO, Fix128::ZERO)
10255        );
10256    }
10257
10258    /// The live-proxy count the broad-phase reads every substep equals the
10259    /// number of proxies in the parked-body tree through parking, a contact
10260    /// unpark, a manual wake, a removal and turning the skip off.
10261    #[test]
10262    fn park_proxy_live_count_tracks_the_tree() {
10263        let mut w = PhysicsWorld::new(PhysicsConfig {
10264            gravity: Vec3Fix::ZERO,
10265            ..PhysicsConfig::default()
10266        });
10267        let r = Fix128::from_ratio(1, 2);
10268        for i in 0..20 {
10269            w.add_body_with_radius(
10270                RigidBody::new_dynamic(Vec3Fix::from_int(i * 3, 0, 0), Fix128::ONE),
10271                r,
10272            );
10273            w.islands.sleep_data[i as usize].state = SleepState::Sleeping;
10274        }
10275        let check = |w: &PhysicsWorld, ctx: &str| {
10276            assert_eq!(w.park.proxy_live, w.park.tree.proxy_count(), "{ctx}");
10277            assert_eq!(
10278                w.park.proxy_live,
10279                w.park.proxies.iter().flatten().count(),
10280                "{ctx}"
10281            );
10282        };
10283        w.step(Fix128::from_ratio(1, 60));
10284        check(&w, "parked");
10285        assert_eq!(w.park.proxy_live, 20);
10286        // Awake body 0 moved into body 1: the contact unparks it.
10287        w.wake_body(0);
10288        w.bodies[0].set_position(Vec3Fix::new(
10289            Fix128::from_ratio(22, 10),
10290            Fix128::ZERO,
10291            Fix128::ZERO,
10292        ));
10293        w.step(Fix128::from_ratio(1, 60));
10294        check(&w, "contact");
10295        assert!(w.stage_work().unparked > 0);
10296        w.wake_body(7);
10297        w.step(Fix128::from_ratio(1, 60));
10298        check(&w, "manual wake");
10299        w.remove_body(3);
10300        w.step(Fix128::from_ratio(1, 60));
10301        check(&w, "remove");
10302        w.set_sleep_skip(false);
10303        check(&w, "off");
10304        assert_eq!(w.park.proxy_live, 0);
10305    }
10306
10307    /// `is_sleeping` → `false` / `true` and `wake_body` → `()`: after one step
10308    /// the static body is asleep (static bodies always are), the moving
10309    /// dynamic body and an out-of-range index are not, and `wake_body` wakes
10310    /// the static one again.
10311    #[test]
10312    fn is_sleeping_and_wake_body_reflect_island_state() {
10313        let mut world = quiet_world();
10314        let floor = world.add_body(RigidBody::new_static(Vec3Fix::ZERO));
10315        let mover = world.add_body(RigidBody::new_dynamic(v3(0, 5, 0), Fix128::ONE));
10316        world.bodies[mover].velocity = v3(5, 0, 0);
10317        world.step(r(1, 4));
10318        assert!(world.is_sleeping(floor));
10319        assert!(!world.is_sleeping(mover));
10320        assert!(!world.is_sleeping(99));
10321        world.wake_body(floor);
10322        assert!(!world.is_sleeping(floor));
10323        assert!(!world.is_sleeping(mover));
10324    }
10325
10326    /// `set_sleep_config` → `()`: with `frames_to_sleep = 2` an idle body is
10327    /// asleep after two frames; under the default 60 frames it is not.
10328    #[test]
10329    fn set_sleep_config_changes_frames_to_sleep() {
10330        let cfg = SleepConfig {
10331            frames_to_sleep: 2,
10332            ..SleepConfig::default()
10333        };
10334        let mut quick = quiet_world();
10335        let idle = quick.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
10336        quick.set_sleep_config(cfg);
10337        assert_eq!(quick.islands.config, cfg);
10338        quick.step(r(1, 4));
10339        assert!(!quick.is_sleeping(idle));
10340        quick.step(r(1, 4));
10341        assert!(quick.is_sleeping(idle));
10342
10343        let mut slow = quiet_world();
10344        let idle2 = slow.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
10345        slow.step(r(1, 4));
10346        slow.step(r(1, 4));
10347        assert!(!slow.is_sleeping(idle2));
10348    }
10349
10350    /// `drain_contact_events` / `drain_trigger_events` → `vec![]`: draining
10351    /// returns the events of the last detection and leaves the collector
10352    /// empty.
10353    #[test]
10354    fn drain_events_return_the_events_and_empty_the_collector() {
10355        let mut world = two_spheres(3, 2, Fix128::ONE, Fix128::ONE);
10356        world.detect_collisions();
10357        assert_eq!(world.contact_events().len(), 1);
10358        let drained = world.drain_contact_events();
10359        assert_eq!(drained.len(), 1);
10360        assert_eq!((drained[0].body_a, drained[0].body_b), (0, 1));
10361        assert_eq!(drained[0].event_type, crate::event::ContactEventType::Begin);
10362        assert!(world.contact_events().is_empty());
10363        assert!(world.drain_contact_events().is_empty());
10364
10365        let mut sensor = two_spheres(3, 2, Fix128::ONE, Fix128::ONE);
10366        sensor.bodies[1].is_sensor = true;
10367        sensor.detect_collisions();
10368        let triggers = sensor.drain_trigger_events();
10369        assert_eq!(triggers.len(), 1);
10370        assert!(triggers[0].entered);
10371        assert!(sensor.trigger_events().is_empty());
10372        assert!(sensor.drain_trigger_events().is_empty());
10373    }
10374
10375    /// `step` line 1691 `<` → `==` / `>`: the joint island rebuild at the start
10376    /// of every step (`reset_unions` then `union` for every in-range joint)
10377    /// must leave both bodies of a joint in one island.
10378    #[test]
10379    fn step_rebuilds_joint_islands_from_scratch() {
10380        use crate::joint::{BallJoint, Joint};
10381        let mut world = quiet_world();
10382        let a = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
10383        let b = world.add_body(RigidBody::new_dynamic(v3(1, 0, 0), Fix128::ONE));
10384        let c = world.add_body(RigidBody::new_dynamic(v3(9, 0, 0), Fix128::ONE));
10385        world.add_joint(Joint::Ball(BallJoint::new(
10386            a,
10387            b,
10388            Vec3Fix::ZERO,
10389            Vec3Fix::ZERO,
10390        )));
10391        // `add_joint` already unions; wipe that so only `step`'s rebuild counts
10392        world.islands.reset_unions();
10393        assert_ne!(world.islands.find(a), world.islands.find(b));
10394        world.step(r(1, 4));
10395        assert_eq!(world.islands.find(a), world.islands.find(b));
10396        assert_ne!(world.islands.find(a), world.islands.find(c));
10397    }
10398
10399    /// `integrate_positions` line 1905 `-` → `+`: a kinematic body's velocity
10400    /// is (target - position) / dt = ((5, 2, 3) - (1, 2, 3)) · 4 = (16, 0, 0)
10401    /// right after integration (the sum would give (24, 16, 24)); the body
10402    /// lands on the target and `prev_position` keeps the old position.
10403    #[test]
10404    fn integrate_positions_kinematic_velocity_is_target_minus_position_over_dt() {
10405        let mut world = quiet_world();
10406        let k = world.add_body(RigidBody::new_kinematic(v3(1, 2, 3)));
10407        world.bodies[k].set_kinematic_target(v3(5, 2, 3), QuatFix::IDENTITY);
10408        world.integrate_positions(r(1, 4));
10409        assert_eq!(world.bodies[k].velocity, v3(16, 0, 0));
10410        assert_eq!(world.bodies[k].position, v3(5, 2, 3));
10411        assert_eq!(world.bodies[k].prev_position, v3(1, 2, 3));
10412    }
10413
10414    /// Same closed form as the `integrate_positions` test immediately above,
10415    /// but through the `Tgs`-path helper instead: velocity is
10416    /// (target - position) / dt = ((5, 2, 3) - (1, 2, 3)) · 4 = (16, 0, 0),
10417    /// the body lands exactly on the target, and `prev_position` keeps the
10418    /// pre-advance position (mirrors `integrate_positions`' `Kinematic`
10419    /// branch bit-for-bit; this is the oracle for
10420    /// `advance_kinematic_targets_for_tgs` itself, not for `step_tgs`).
10421    #[cfg(feature = "std")]
10422    #[test]
10423    fn advance_kinematic_targets_for_tgs_matches_integrate_positions_closed_form() {
10424        let mut world = quiet_world();
10425        let k = world.add_body(RigidBody::new_kinematic(v3(1, 2, 3)));
10426        world.bodies[k].set_kinematic_target(v3(5, 2, 3), QuatFix::IDENTITY);
10427        world.advance_kinematic_targets_for_tgs(r(1, 4));
10428        assert_eq!(world.bodies[k].velocity, v3(16, 0, 0));
10429        assert_eq!(world.bodies[k].position, v3(5, 2, 3));
10430        assert_eq!(world.bodies[k].prev_position, v3(1, 2, 3));
10431    }
10432
10433    /// A dynamic body with no target is untouched by the `Tgs`-path helper
10434    /// (only `BodyType::Kinematic` bodies are visited at all).
10435    #[cfg(feature = "std")]
10436    #[test]
10437    fn advance_kinematic_targets_for_tgs_leaves_dynamic_bodies_untouched() {
10438        let mut world = quiet_world();
10439        let d = world.add_body(RigidBody::new_dynamic(v3(1, 2, 3), Fix128::ONE));
10440        world.bodies[d].velocity = v3(7, 8, 9);
10441        world.advance_kinematic_targets_for_tgs(r(1, 4));
10442        assert_eq!(world.bodies[d].position, v3(1, 2, 3));
10443        assert_eq!(world.bodies[d].velocity, v3(7, 8, 9));
10444    }
10445
10446    /// A kinematic body with no target set (`kinematic_target == None`) is
10447    /// left at rest — only `prev_position`/`prev_rotation` are refreshed,
10448    /// matching `integrate_positions`' `Kinematic` branch (the `if let
10449    /// Some(...)` guards the actual advance in both).
10450    #[cfg(feature = "std")]
10451    #[test]
10452    fn advance_kinematic_targets_for_tgs_without_a_target_stays_at_rest() {
10453        let mut world = quiet_world();
10454        let k = world.add_body(RigidBody::new_kinematic(v3(1, 2, 3)));
10455        world.advance_kinematic_targets_for_tgs(r(1, 4));
10456        assert_eq!(world.bodies[k].position, v3(1, 2, 3));
10457        assert_eq!(world.bodies[k].velocity, Vec3Fix::ZERO);
10458        assert_eq!(world.bodies[k].prev_position, v3(1, 2, 3));
10459    }
10460
10461    /// `update_velocities` lines 2055 / 2073 `-` → `+` (relative velocity),
10462    /// 2089 `/` → `*` (`inv_w`), 2091 / 2093 `*` → `/` and 2093 `+` → `-`
10463    /// (friction split): both bodies dynamic (inv 1 / inv 3 → inv_w = 1/4),
10464    /// contact normal +y (B → A), vA = (3, -4, 0), vB = (1, -2, 0), e = 1/2,
10465    /// μ = 1/2.
10466    /// Restitution: vn = (2, -2, 0)·n = -2 → Δvn = 3 → A += 3/4, B -= 9/4 →
10467    /// A (3, -13/4, 0), B (1, -17/4, 0). Friction: rel = (2, 1, 0), vn2 = 1,
10468    /// tangent (2, 0, 0) speed 2, limit μ·|vn2| = 1/2 → impulse (1/2, 0, 0);
10469    /// A -= 1/2 · 1/4, B += 1/2 · 3/4 → A (23/8, -13/4, 0), B (11/8, -17/4, 0).
10470    /// (`inv_w = w_sum` would give A.x = 1 / B.x = 7, `+` → `-` on B gives 5/8.)
10471    #[test]
10472    fn update_velocities_friction_and_restitution_split_between_two_dynamic_bodies() {
10473        let mut world = quiet_world();
10474        let a = world.add_body(RigidBody::new_dynamic(v3(0, 1, 0), Fix128::ONE));
10475        let b = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
10476        world.bodies[b].inv_mass = Fix128::from_int(3);
10477        let contact = Contact {
10478            depth: r(1, 100),
10479            normal: v3(0, 1, 0),
10480            point_a: Vec3Fix::ZERO,
10481            point_b: Vec3Fix::ZERO,
10482        };
10483        let mut c = ContactConstraint::new(a, b, contact);
10484        c.friction = r(1, 2);
10485        c.restitution = r(1, 2);
10486        // 位置解の法線 λ = 1/4 → Coulomb 上限 μ λ / dt = 1/2 * 1/4 * 4 = 1/2
10487        c.cached_lambda = r(1, 4);
10488        world.contact_constraints.push(c);
10489        freeze_positions(&mut world);
10490        give_velocity(&mut world, a, v3(3, -4, 0));
10491        give_velocity(&mut world, b, v3(1, -2, 0));
10492
10493        world.update_velocities(r(1, 4));
10494
10495        assert_near_vec(
10496            world.bodies[a].velocity,
10497            Vec3Fix::new(r(23, 8), r(-13, 4), Fix128::ZERO),
10498            "A",
10499        );
10500        assert_near_vec(
10501            world.bodies[b].velocity,
10502            Vec3Fix::new(r(11, 8), r(-17, 4), Fix128::ZERO),
10503            "B",
10504        );
10505    }
10506
10507    /// Half of `W_SUM_EPSILON` (an inverse mass strictly below the skip
10508    /// threshold).
10509    fn half_epsilon() -> Fix128 {
10510        Fix128 {
10511            hi: 0,
10512            lo: W_SUM_EPSILON.lo >> 1,
10513        }
10514    }
10515
10516    /// `update_velocities` line 2050 `<` → `<=` / `==`: a contact whose
10517    /// `w_sum` is exactly `W_SUM_EPSILON` is solved (A: inv_mass = ε against a
10518    /// static B, vn = -4, e = 1/2 → Δvn = 6, inv_w = 1/ε, ε · 1/ε = 1 → A.y =
10519    /// -4 + 6 = 2), one with `w_sum = ε/2` is skipped (A keeps (0, -4, 0)).
10520    #[test]
10521    fn update_velocities_w_sum_epsilon_boundary() {
10522        for (inv_mass, expected_y) in [(W_SUM_EPSILON, 2), (half_epsilon(), -4)] {
10523            let mut world = quiet_world();
10524            let a = world.add_body(RigidBody::new_dynamic(v3(0, 1, 0), Fix128::ONE));
10525            let b = world.add_body(RigidBody::new_static(Vec3Fix::ZERO));
10526            world.bodies[a].inv_mass = inv_mass;
10527            let contact = Contact {
10528                depth: r(1, 100),
10529                normal: v3(0, 1, 0),
10530                point_a: Vec3Fix::ZERO,
10531                point_b: Vec3Fix::ZERO,
10532            };
10533            let mut c = ContactConstraint::new(a, b, contact);
10534            c.friction = Fix128::ZERO;
10535            c.restitution = r(1, 2);
10536            world.contact_constraints.push(c);
10537            freeze_positions(&mut world);
10538            give_velocity(&mut world, a, v3(0, -4, 0));
10539
10540            world.update_velocities(r(1, 4));
10541
10542            assert_eq!(
10543                world.bodies[a].velocity,
10544                v3(0, expected_y, 0),
10545                "inv_mass {inv_mass:?}"
10546            );
10547        }
10548    }
10549
10550    /// `solve_distance_constraints` line 2394 `<` → `<=` / `==`: with A at
10551    /// (0, 4, 0), inv_mass = ε, static B at the origin and target 1 the error
10552    /// is 3 and `w_sum == ε` is solved: dlambda = 3/ε, correction · ε moves A
10553    /// by the full error to (0, 1, 0). With inv_mass = ε/2 the constraint is
10554    /// skipped and A stays at (0, 4, 0).
10555    #[test]
10556    fn solve_distance_constraints_w_sum_epsilon_boundary() {
10557        for (inv_mass, expected) in [(W_SUM_EPSILON, v3(0, 1, 0)), (half_epsilon(), v3(0, 4, 0))] {
10558            let mut world = quiet_world();
10559            let a = world.add_body(RigidBody::new_dynamic(v3(0, 4, 0), Fix128::ONE));
10560            let b = world.add_body(RigidBody::new_static(Vec3Fix::ZERO));
10561            world.bodies[a].inv_mass = inv_mass;
10562            world.add_distance_constraint(dc(a, b));
10563            world.solve_distance_constraints(r(1, 4));
10564            assert_near_vec(world.bodies[a].position, expected, "A position");
10565            assert_eq!(world.bodies[b].position, Vec3Fix::ZERO);
10566        }
10567    }
10568
10569    /// `solve_contact_constraints` line 2497 `<` → `<=` / `==`: depth 1 along
10570    /// +x against a static B; `w_sum == ε` pushes A by the full depth
10571    /// (ε · 1/ε = 1) to (1, 0, 0), `w_sum = ε/2` is skipped.
10572    #[test]
10573    fn solve_contact_constraints_w_sum_epsilon_boundary() {
10574        for (inv_mass, expected) in [
10575            (W_SUM_EPSILON, v3(1, 0, 0)),
10576            (half_epsilon(), Vec3Fix::ZERO),
10577        ] {
10578            let mut world = contact_world(inv_mass, Fix128::ZERO, Fix128::ONE);
10579            world.solve_contact_constraints(r(1, 4));
10580            assert_eq!(world.bodies[0].position, expected, "inv_mass {inv_mass:?}");
10581            assert_eq!(world.bodies[1].position, v3(1, 0, 0));
10582        }
10583    }
10584
10585    /// `solve_distance_constraints` line 2377 `+` → `-` (anchor A) and line
10586    /// 2407 `*` → `/` (`delta_a`): A at (0, 2, 0) with local anchor (0, 1, 0)
10587    /// and inv_mass 2, static B at the origin, target 1 → world anchor
10588    /// (0, 3, 0), error 2, w_sum 2, dlambda 1, correction (0, -1, 0) · 2 →
10589    /// A lands on the origin. The `-` anchor gives error 0 (A stays), the `/`
10590    /// gives half the move (A at (0, 3/2, 0)).
10591    #[test]
10592    fn solve_distance_constraints_anchor_a_offset_and_inverse_mass_scaling() {
10593        let mut world = quiet_world();
10594        let a = world.add_body(RigidBody::new_dynamic(v3(0, 2, 0), Fix128::ONE));
10595        let b = world.add_body(RigidBody::new_static(Vec3Fix::ZERO));
10596        world.bodies[a].inv_mass = Fix128::from_int(2);
10597        let mut c = dc(a, b);
10598        c.local_anchor_a = v3(0, 1, 0);
10599        world.add_distance_constraint(c);
10600
10601        world.solve_distance_constraints(r(1, 4));
10602
10603        assert_near_vec(world.bodies[a].position, Vec3Fix::ZERO, "A");
10604        assert_eq!(world.bodies[b].position, Vec3Fix::ZERO);
10605        assert_eq!(world.distance_constraints[0].cached_lambda, Fix128::ONE);
10606    }
10607
10608    /// `detect_collisions` line 2933 / 2952 `<` → `>` (early return with 3
10609    /// bodies / 3 primitives) and line 3009 `*` → `+` (squared-distance early
10610    /// out): three collidable bodies, A at the origin and B at (2, 1, 1)
10611    /// (dist² = 6) with radii 3/2 each (combined 3: 6 < 3² = 9 overlaps, but
10612    /// 6 ≥ 3 + 3 = 6 would be culled), C far away. Exactly one contact, depth
10613    /// 3 - √6.
10614    #[test]
10615    fn detect_collisions_three_bodies_squared_distance_cull_uses_squared_radius() {
10616        let mut world = quiet_world();
10617        let radius = r(3, 2);
10618        world.add_body_with_radius(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE), radius);
10619        world.add_body_with_radius(RigidBody::new_dynamic(v3(2, 1, 1), Fix128::ONE), radius);
10620        world.add_body_with_radius(RigidBody::new_dynamic(v3(50, 50, 50), Fix128::ONE), radius);
10621        world.detect_collisions();
10622        assert_eq!(world.contact_constraints.len(), 1);
10623        let c = world.contact_constraints[0];
10624        assert_eq!((c.body_a, c.body_b), (0, 1));
10625        let expected_depth = Fix128::from_int(3) - Fix128::from_int(6).sqrt();
10626        assert!(
10627            near(c.contact.depth, expected_depth),
10628            "depth {:?} vs {expected_depth:?}",
10629            c.contact.depth
10630        );
10631        assert_eq!(world.contact_events().len(), 1);
10632    }
10633
10634    /// `remove_sdf_collider` → `None` and line 2863 `<` → `<=` / `==` / `>`,
10635    /// `set_sdf_collision_radius` → `()`: index 1 of a single collider is out
10636    /// of range (`None`, no panic), index 0 is removed, and the radius setter
10637    /// stores 3/4.
10638    #[test]
10639    fn remove_sdf_collider_boundary_and_sdf_radius_setter() {
10640        use crate::sdf_collider::{ClosureSdf, SdfCollider};
10641        let mut world = quiet_world();
10642        let ground = ClosureSdf::new(|_x, y, _z| y, |_x, _y, _z| (0.0, 1.0, 0.0));
10643        assert_eq!(
10644            world.add_sdf_collider(SdfCollider::new_static(
10645                Box::new(ground),
10646                Vec3Fix::ZERO,
10647                QuatFix::IDENTITY,
10648            )),
10649            0
10650        );
10651        assert!(world.remove_sdf_collider(1).is_none());
10652        assert_eq!(world.sdf_colliders.len(), 1);
10653        assert!(world.remove_sdf_collider(0).is_some());
10654        assert!(world.sdf_colliders.is_empty());
10655        assert!(world.remove_sdf_collider(0).is_none());
10656
10657        world.set_sdf_collision_radius(r(3, 4));
10658        assert_eq!(world.sdf_collision_radius, r(3, 4));
10659    }
10660
10661    /// `deserialize_state` line 3173 `<` → `<=`: a 4-byte snapshot (header
10662    /// only, count 0) of an empty world is valid.
10663    #[test]
10664    fn deserialize_state_accepts_header_only_snapshot_of_empty_world() {
10665        let src = quiet_world();
10666        let bytes = src.serialize_state();
10667        // v3: body 0 体でも header 12 + flag 1 + fingerprint 8
10668        assert_eq!(bytes.len(), 21);
10669        let mut dst = quiet_world();
10670        assert!(dst.deserialize_state(&bytes));
10671        assert_eq!(dst.body_count(), 0);
10672    }
10673
10674    /// `deserialize_state` lines 3237 / 3241 / 3245 `<` → `==` / `>`: when the
10675    /// parallel arrays are shorter than `bodies` (a body pushed straight into
10676    /// `bodies`), the resync loops must extend them to the body count with
10677    /// default entries.
10678    #[test]
10679    fn deserialize_state_extends_short_parallel_arrays_to_body_count() {
10680        let mut world = quiet_world();
10681        world.add_body(RigidBody::new_dynamic(v3(1, 2, 3), Fix128::ONE));
10682        world.set_body_collision_radius(0, r(1, 2));
10683        world
10684            .bodies
10685            .push(RigidBody::new_dynamic(v3(4, 5, 6), Fix128::ONE));
10686        world
10687            .bodies
10688            .push(RigidBody::new_dynamic(v3(7, 8, 9), Fix128::ONE));
10689        assert_eq!(world.body_collision_radii.len(), 1);
10690        let bytes = world.serialize_state();
10691        assert!(world.deserialize_state(&bytes));
10692        assert_eq!(world.body_collision_radii, vec![Some(r(1, 2)), None, None]);
10693        assert_eq!(world.body_filters, vec![CollisionFilter::DEFAULT; 3]);
10694        assert_eq!(
10695            world.body_materials,
10696            vec![crate::material::DEFAULT_MATERIAL; 3]
10697        );
10698        assert_eq!(world.islands.sleep_data.len(), 3);
10699        assert_eq!(world.bodies[2].position, v3(7, 8, 9));
10700    }
10701
10702    // ---- observe_body / observe_bodies (World Auditor WM-10 Physics 版) ----
10703
10704    #[test]
10705    fn observe_body_returns_none_out_of_range_and_fields_for_in_range() {
10706        let mut world = quiet_world();
10707        let mut a = RigidBody::new_dynamic(v3(1, 2, 3), Fix128::ONE);
10708        a.velocity = v3(4, 5, 6);
10709        a.angular_velocity = v3(7, 8, 9);
10710        world.add_body(a);
10711
10712        assert!(world.observe_body(1).is_none(), "範囲外 index は None");
10713
10714        let obs = world.observe_body(0).expect("範囲内 index は Some");
10715        assert_eq!(obs.body_index, 0);
10716        assert_eq!(obs.position, v3(1, 2, 3));
10717        assert_eq!(obs.velocity, v3(4, 5, 6));
10718        assert_eq!(obs.angular_velocity, v3(7, 8, 9));
10719        assert!(!obs.sleeping);
10720        assert!(!obs.in_contact, "接触していない body は in_contact = false");
10721    }
10722
10723    #[test]
10724    fn observe_bodies_returns_one_entry_per_body_in_index_order() {
10725        let mut world = quiet_world();
10726        world.add_body(RigidBody::new_dynamic(v3(1, 0, 0), Fix128::ONE));
10727        world.add_body(RigidBody::new_dynamic(v3(2, 0, 0), Fix128::ONE));
10728        world.add_body(RigidBody::new_dynamic(v3(3, 0, 0), Fix128::ONE));
10729
10730        let obs = world.observe_bodies();
10731        assert_eq!(obs.len(), 3);
10732        for (i, o) in obs.iter().enumerate() {
10733            assert_eq!(o.body_index, i);
10734            assert_eq!(o.position, world.bodies[i].position);
10735        }
10736    }
10737
10738    #[test]
10739    fn observe_body_reports_in_contact_from_this_frame_events() {
10740        // 重なった 2 球を 1 step 進めると contact event (Begin) が出る
10741        // (`tests/wm07_rollback_event_parity.rs` と同じ idiom — 最初から重なっている配置)
10742        let mut world = PhysicsWorld::new(PhysicsConfig {
10743            gravity: Vec3Fix::ZERO,
10744            ..PhysicsConfig::default()
10745        });
10746        let radius = Fix128::from_int(2);
10747        let a = RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE);
10748        world.add_body_with_radius(a, radius);
10749        let b = RigidBody::new_dynamic(v3(1, 0, 0), Fix128::ONE);
10750        world.add_body_with_radius(b, radius);
10751        let c = RigidBody::new_dynamic(v3(100, 100, 100), Fix128::ONE);
10752        world.add_body_with_radius(c, Fix128::from_ratio(1, 10));
10753
10754        world.step(Fix128::from_ratio(1, 60));
10755
10756        assert!(world.observe_body(0).unwrap().in_contact);
10757        assert!(world.observe_body(1).unwrap().in_contact);
10758        assert!(
10759            !world.observe_body(2).unwrap().in_contact,
10760            "遠くの body は接触していないはず"
10761        );
10762    }
10763
10764    /// `batches_are_body_disjoint` line 1646 `&&` → `||` / `a == b` → `a != b`
10765    /// (conflict through the *second* body of both constraints) and `||` →
10766    /// `&&` (a self-constraint counts its body once): constraints (1, 0) and
10767    /// (2, 0) share body 0 only as `body_b`, so one batch holding both is
10768    /// invalid; a batch holding only the self-constraint (1, 1) is valid.
10769    #[test]
10770    fn batches_are_body_disjoint_checks_second_body_and_self_constraints() {
10771        let mut world = quiet_world();
10772        for i in 0..3i64 {
10773            world.add_body(RigidBody::new_dynamic(v3(i, 0, 0), Fix128::ONE));
10774        }
10775        world.add_distance_constraint(dc(1, 0));
10776        world.add_distance_constraint(dc(2, 0));
10777        world.rebuild_batches();
10778        assert_eq!(world.num_batches(), 2);
10779        assert!(world.batches_are_body_disjoint());
10780
10781        let mut shared_b = ConstraintBatch::default();
10782        shared_b.distance_indices.extend([0usize, 1]);
10783        world.constraint_batches = vec![shared_b];
10784        assert!(!world.batches_are_body_disjoint());
10785
10786        world.distance_constraints.push(dc(1, 1));
10787        let mut self_only = ConstraintBatch::default();
10788        self_only.distance_indices.push(2);
10789        world.constraint_batches = vec![self_only];
10790        assert!(world.batches_are_body_disjoint());
10791
10792        let mut self_and_other = ConstraintBatch::default();
10793        self_and_other.distance_indices.extend([2usize, 0]);
10794        world.constraint_batches = vec![self_and_other];
10795        assert!(!world.batches_are_body_disjoint());
10796    }
10797
10798    /// `raycast` line 1397 `<` → `<=` / `==`: (1) two spheres hit at exactly
10799    /// the same t (mirror images across the ray) — the first BVH candidate
10800    /// wins; the candidates come in Morton order, so the sphere with the
10801    /// smaller y (body 1) is reported. (2) A ray cast in -x meets the far
10802    /// sphere first in Morton order (smaller x) and the nearer sphere second;
10803    /// the nearer one (body 1, t = 10 - 2 = 8) must replace it.
10804    #[test]
10805    fn raycast_ties_keep_first_candidate_and_nearer_later_candidate_replaces() {
10806        let mut tie = quiet_world();
10807        tie.add_body_with_radius(
10808            RigidBody::new_static(Vec3Fix::new(Fix128::from_int(5), r(1, 2), Fix128::ZERO)),
10809            Fix128::ONE,
10810        );
10811        tie.add_body_with_radius(
10812            RigidBody::new_static(Vec3Fix::new(Fix128::from_int(5), r(-1, 2), Fix128::ZERO)),
10813            Fix128::ONE,
10814        );
10815        let hit = tie
10816            .raycast(Vec3Fix::ZERO, v3(1, 0, 0), Fix128::from_int(20))
10817            .expect("both spheres straddle the ray");
10818        assert_eq!(hit.0, 1);
10819        // t = 5 - sqrt(3/4) for both
10820        let expected_t = Fix128::from_int(5) - r(3, 4).sqrt();
10821        assert!(near(hit.1, expected_t), "t {:?} vs {expected_t:?}", hit.1);
10822
10823        let far_first = sphere_world(&[v3(5, 0, 0), v3(10, 0, 0)]);
10824        assert_eq!(
10825            far_first.raycast(v3(20, 0, 0), v3(-1, 0, 0), Fix128::from_int(100)),
10826            Some((1, Fix128::from_int(8)))
10827        );
10828    }
10829
10830    /// `Debug for PhysicsWorld` → `Ok(())`: the output names the type and the
10831    /// body count.
10832    #[test]
10833    fn physics_world_debug_lists_type_and_counts() {
10834        let mut world = quiet_world();
10835        world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
10836        world.add_body(RigidBody::new_dynamic(v3(1, 0, 0), Fix128::ONE));
10837        let text = format!("{world:?}");
10838        assert!(text.starts_with("PhysicsWorld"), "{text}");
10839        assert!(text.contains("bodies: 2"), "{text}");
10840        assert!(text.contains("joints: 0"), "{text}");
10841    }
10842
10843    // ── closed-form unit tests of the shape / motor / contact-force /
10844    //    participant paths (they are otherwise exercised only from tests/) ──
10845
10846    fn half_unit_box() -> crate::shape::Shape {
10847        crate::shape::Shape::Box {
10848            half_extents: Vec3Fix::from_int(1, 1, 1),
10849        }
10850    }
10851
10852    /// oracle: a box of half-extent 1 at the origin and a sphere of radius 1
10853    /// meet when the sphere centre is within `1 + 1 = 2` of the box face along
10854    /// `x` (1.5: overlap, 2.5: clear), whichever side carries the shape; two
10855    /// shaped boxes meet when the centres are under `1 + 1 = 2` apart; two
10856    /// plain spheres of radius 1 meet when the centres are under 2 apart.
10857    /// An index past the bodies, or a body with no collider, never overlaps,
10858    /// and `set_body_shape` refuses an index past the bodies.
10859    #[test]
10860    fn colliders_overlap_follows_the_closed_form_gaps() {
10861        let mut world = quiet_world();
10862        let shaped = world.add_body(RigidBody::new_static(Vec3Fix::ZERO));
10863        assert!(world.set_body_shape(shaped, &half_unit_box()));
10864        assert!(!world.set_body_shape(99, &half_unit_box()));
10865        let near = world.add_body_with_radius(
10866            RigidBody::new(
10867                Vec3Fix::new(Fix128::from_ratio(3, 2), Fix128::ZERO, Fix128::ZERO),
10868                Fix128::ONE,
10869            ),
10870            Fix128::ONE,
10871        );
10872        let far = world.add_body_with_radius(
10873            RigidBody::new(
10874                Vec3Fix::new(Fix128::from_ratio(5, 2), Fix128::ZERO, Fix128::ZERO),
10875                Fix128::ONE,
10876            ),
10877            Fix128::ONE,
10878        );
10879        let bare = world.add_body(RigidBody::new(Vec3Fix::ZERO, Fix128::ONE));
10880        // shape against sphere, both argument orders
10881        assert!(world.colliders_overlap(shaped, near));
10882        assert!(world.colliders_overlap(near, shaped));
10883        assert!(!world.colliders_overlap(shaped, far));
10884        assert!(!world.colliders_overlap(far, shaped));
10885        // two plain spheres 1 apart (< 2) overlap
10886        assert!(world.colliders_overlap(near, far));
10887        // a body without a collider, or an index past the bodies
10888        assert!(!world.colliders_overlap(shaped, bare));
10889        assert!(!world.colliders_overlap(near, bare));
10890        assert!(!world.colliders_overlap(near, 99));
10891        // shape against shape: the far body becomes a box 2.5 away (clear),
10892        // then the near one a box 1.5 away (overlapping)
10893        assert!(world.set_body_shape(far, &half_unit_box()));
10894        assert!(!world.colliders_overlap(shaped, far));
10895        assert!(world.set_body_shape(near, &half_unit_box()));
10896        assert!(world.colliders_overlap(shaped, near));
10897    }
10898
10899    /// oracle: a sphere of radius 1/2 dropped onto the top face (`y = 1`) of a
10900    /// static box of half-extent 1 comes to rest with its centre at
10901    /// `1 + 1/2 = 3/2`, the contact of a shaped body with a plain sphere
10902    /// (the GJK path of the narrow phase), for either body order.
10903    #[test]
10904    fn sphere_rests_on_a_shaped_box_at_face_plus_radius() {
10905        for box_first in [true, false] {
10906            let mut world = PhysicsWorld::new(SolverConfig::default());
10907            let ball = || {
10908                RigidBody::new(
10909                    Vec3Fix::new(Fix128::ZERO, Fix128::from_ratio(8, 5), Fix128::ZERO),
10910                    Fix128::ONE,
10911                )
10912            };
10913            let (floor, b) = if box_first {
10914                let f = world.add_body(RigidBody::new_static(Vec3Fix::ZERO));
10915                let b = world.add_body_with_radius(ball(), Fix128::from_ratio(1, 2));
10916                (f, b)
10917            } else {
10918                let b = world.add_body_with_radius(ball(), Fix128::from_ratio(1, 2));
10919                let f = world.add_body(RigidBody::new_static(Vec3Fix::ZERO));
10920                (f, b)
10921            };
10922            assert!(world.set_body_shape(floor, &half_unit_box()));
10923            for _ in 0..120 {
10924                world.step(Fix128::from_ratio(1, 60));
10925            }
10926            let y = world.bodies[b].position.y.to_f64();
10927            assert!((y - 1.5).abs() < 0.02, "box_first {box_first}: y {y}");
10928            assert!(world.bodies[b].position.x.abs() < Fix128::from_ratio(1, 1000));
10929        }
10930    }
10931
10932    /// oracle: the motor accessors report `false` / `None` for an index that
10933    /// was never returned, set the mode they name, and `remove_joint`
10934    /// drops the motors of the removed joint and re-points those of the
10935    /// last joint to the freed index (swap-remove).
10936    #[test]
10937    fn joint_motor_accessors_and_removal_remap() {
10938        let mut world = quiet_world();
10939        for x in 0..3 {
10940            world.add_body(RigidBody::new(Vec3Fix::from_int(x, 0, 0), Fix128::ONE));
10941        }
10942        let ball = |a, b| {
10943            Joint::Ball(crate::joint::BallJoint::new(
10944                a,
10945                b,
10946                Vec3Fix::ZERO,
10947                Vec3Fix::ZERO,
10948            ))
10949        };
10950        let j0 = world.add_joint(ball(0, 1));
10951        let j1 = world.add_joint(ball(1, 2));
10952        let pd = crate::motor::PdController::new(Fix128::ONE, Fix128::ONE, Fix128::from_int(10));
10953        let m0 = world.add_joint_motor(j0, pd);
10954        let m1 = world.add_joint_motor(j1, pd);
10955        assert!(world.set_joint_motor_velocity_target(m1, Fix128::from_int(2)));
10956        assert!(!world.set_joint_motor_velocity_target(9, Fix128::ONE));
10957        assert_eq!(
10958            world.joint_motor_mut(m1).unwrap().controller.mode,
10959            crate::motor::MotorMode::Velocity
10960        );
10961        assert!(world.disable_joint_motor(m1));
10962        assert!(!world.disable_joint_motor(9));
10963        assert_eq!(
10964            world.joint_motor_mut(m1).unwrap().controller.mode,
10965            crate::motor::MotorMode::Off
10966        );
10967        assert!(world.joint_motor_mut(9).is_none());
10968
10969        let r0 = world.add_joint_motor_3d(
10970            j0,
10971            Vec3Fix::from_int(1, 1, 1),
10972            Vec3Fix::from_int(1, 1, 1),
10973            Fix128::from_int(5),
10974        );
10975        let r1 = world.add_joint_motor_3d(
10976            j1,
10977            Vec3Fix::from_int(1, 1, 1),
10978            Vec3Fix::from_int(1, 1, 1),
10979            Fix128::from_int(5),
10980        );
10981        let target = QuatFix::from_axis_angle(Vec3Fix::from_int(0, 1, 0), Fix128::from_ratio(1, 4));
10982        assert!(world.set_joint_motor_3d_rotation_target(r1, target));
10983        assert!(!world.set_joint_motor_3d_rotation_target(9, target));
10984        assert_eq!(
10985            world.joint_motor_3d_mut(r1).unwrap().target_rotation,
10986            target
10987        );
10988        assert!(world.joint_motor_3d_mut(9).is_none());
10989        let _ = (m0, r0);
10990
10991        assert!(world.remove_joint(7).is_none());
10992        assert!(world.remove_joint(j0).is_some());
10993        // the motors of joint 0 are gone, the ones of joint 1 now name joint 0
10994        assert_eq!(world.joint_motors.len(), 1);
10995        assert_eq!(world.joint_motors[0].joint_index, 0);
10996        assert_eq!(world.joint_motors_3d.len(), 1);
10997        assert_eq!(world.joint_motors_3d[0].0, 0);
10998        assert_eq!(world.joint_motors_3d[0].1.target_rotation, target);
10999    }
11000
11001    /// oracle: a contact carrying `λ` between a body of mass 1 (`w = 1`) and a
11002    /// static body (`w = 0`), in a frame `dt = 1` of 8 substeps (`h = 1/8`),
11003    /// carries the force `F = λ / (w·h²) = 64·λ`; with `λ = 1/2` that is 32.
11004    /// It gives one normal arrow of 32, two friction arrows of `μ·F = 0.3·32`
11005    /// and one cone of height 32. A contact between two static bodies
11006    /// carries none.
11007    #[test]
11008    fn contact_forces_are_lambda_over_w_h_squared() {
11009        let mut world = PhysicsWorld::new(SolverConfig::default());
11010        let a = world.add_body(RigidBody::new(Vec3Fix::from_int(0, 1, 0), Fix128::ONE));
11011        let b = world.add_body(RigidBody::new_static(Vec3Fix::ZERO));
11012        let s = world.add_body(RigidBody::new_static(Vec3Fix::from_int(5, 0, 0)));
11013        let contact = Contact {
11014            depth: Fix128::from_ratio(1, 100),
11015            normal: Vec3Fix::from_int(0, 1, 0),
11016            point_a: Vec3Fix::from_int(0, 1, 0),
11017            point_b: Vec3Fix::ZERO,
11018        };
11019        let mut c = ContactConstraint::new(a, b, contact);
11020        c.cached_lambda = Fix128::from_ratio(1, 2);
11021        let mut dead = ContactConstraint::new(b, s, contact);
11022        dead.cached_lambda = Fix128::ONE;
11023        world.contact_constraints = vec![c, dead];
11024        let dt = Fix128::ONE;
11025        let forces = world.contact_forces(dt);
11026        assert_eq!(forces.len(), 1);
11027        assert_eq!(forces[0].0, Vec3Fix::from_int(0, 1, 0));
11028        assert_eq!(forces[0].1, Vec3Fix::from_int(0, 1, 0));
11029        assert_eq!(forces[0].2, Fix128::from_int(32));
11030        assert_eq!(
11031            world.contact_constraint_force(&world.contact_constraints[0], dt),
11032            Some(Fix128::from_int(32))
11033        );
11034        assert_eq!(
11035            world.contact_constraint_force(&world.contact_constraints[1], dt),
11036            None
11037        );
11038        let arrows = world.contact_arrows(dt);
11039        assert_eq!(arrows.len(), 1);
11040        assert_eq!(arrows[0].force_magnitude, Fix128::from_int(32));
11041        let friction = world.contact_friction_arrows(dt);
11042        assert_eq!(friction.len(), 2);
11043        let mu_f = (Fix128::from_int(32) * Fix128::from_ratio(3, 10)).to_f64();
11044        for f in &friction {
11045            assert!((f.force_magnitude.to_f64() - 9.6).abs() < 1e-9 && (mu_f - 9.6).abs() < 1e-9);
11046        }
11047        let cones = world.contact_friction_cones(dt);
11048        assert_eq!(cones.len(), 1);
11049        assert_eq!(cones[0].height, Fix128::from_int(32));
11050    }
11051
11052    /// oracle: static colliders are indexed in insertion order, `remove`
11053    /// shifts the later ones down and refuses an index past the end.
11054    #[test]
11055    fn static_collider_add_remove_count() {
11056        let mut world = quiet_world();
11057        let plane = |h: i64| {
11058            crate::static_collider::StaticCollider::Plane(
11059                crate::plane_collider::PlaneCollider::new(
11060                    Vec3Fix::from_int(0, 1, 0),
11061                    Fix128::from_int(h),
11062                ),
11063            )
11064        };
11065        assert_eq!(world.add_static_collider(plane(0)), 0);
11066        assert_eq!(world.add_static_collider(plane(1)), 1);
11067        assert_eq!(world.static_collider_count(), 2);
11068        assert!(world.remove_static_collider(2).is_none());
11069        assert!(world.remove_static_collider(0).is_some());
11070        assert_eq!(world.static_collider_count(), 1);
11071        // the plane at height 1 moved to index 0: a sphere of radius 1/2 at
11072        // y = 1 (its centre on that plane) touches it with depth 1/2
11073        let c = world.static_colliders_slice()[0]
11074            .collide_sphere(Vec3Fix::from_int(0, 1, 0), Fix128::from_ratio(1, 2))
11075            .expect("touching the plane at y = 1");
11076        assert!((c.depth.to_f64() - 0.5).abs() < 1e-9, "{c:?}");
11077    }
11078
11079    /// oracle: with the dynamic-tree broad-phase every body with a collision
11080    /// radius has a proxy after a step, and its fattened box contains the
11081    /// sphere's own box (`centre ± r`); a body without a radius has none.
11082    /// With the hybrid broad-phase two overlapping spheres (gap −1/2) are
11083    /// found and pushed apart, while a far one is left at rest.
11084    #[test]
11085    fn dynamic_tree_and_hybrid_broadphases_find_the_pairs() {
11086        for kind in [Broadphase::DynamicTree, Broadphase::Hybrid] {
11087            let mut world = quiet_world();
11088            world.set_broadphase(kind);
11089            assert_eq!(world.broadphase(), kind);
11090            let a =
11091                world.add_body_with_radius(RigidBody::new(Vec3Fix::ZERO, Fix128::ONE), Fix128::ONE);
11092            let b = world.add_body_with_radius(
11093                RigidBody::new(
11094                    Vec3Fix::new(Fix128::from_ratio(3, 2), Fix128::ZERO, Fix128::ZERO),
11095                    Fix128::ONE,
11096                ),
11097                Fix128::ONE,
11098            );
11099            let far = world.add_body_with_radius(
11100                RigidBody::new(Vec3Fix::from_int(50, 0, 0), Fix128::ONE),
11101                Fix128::ONE,
11102            );
11103            let bare = world.add_body(RigidBody::new(Vec3Fix::from_int(-50, 0, 0), Fix128::ONE));
11104            world.step(Fix128::from_ratio(1, 60));
11105            let gap = (world.bodies[b].position - world.bodies[a].position).length();
11106            assert!(gap > Fix128::from_ratio(3, 2), "{kind:?}: gap {gap:?}");
11107            assert_eq!(world.bodies[far].position, Vec3Fix::from_int(50, 0, 0));
11108            if kind == Broadphase::DynamicTree {
11109                assert_eq!(world.broadphase_stats().proxies, 3);
11110                let bx = world.broadphase_proxy_aabb(far).expect("proxy");
11111                assert!(bx.min.x <= Fix128::from_int(49) && bx.max.x >= Fix128::from_int(51));
11112                assert!(world.broadphase_proxy_aabb(bare).is_none());
11113                assert!(world.broadphase_proxy_aabb(99).is_none());
11114            }
11115        }
11116    }
11117
11118    /// A participant adding a constant force to one body (or failing).
11119    struct ConstantPush {
11120        body: usize,
11121        force: Vec3Fix,
11122        fail: bool,
11123    }
11124
11125    impl crate::world_participant::Participant for ConstantPush {
11126        fn kind(&self) -> crate::world_participant::ParticipantKind {
11127            crate::world_participant::ParticipantKind::new(0x5055_5348)
11128        }
11129        fn substep(
11130            &mut self,
11131            ctx: &mut crate::world_participant::SubstepCtx<'_>,
11132            _h: Fix128,
11133        ) -> Result<(), crate::world_participant::ParticipantFault> {
11134            if self.fail {
11135                return Err(crate::world_participant::ParticipantFault::InvalidState);
11136            }
11137            ctx.add_force(self.body, self.force)
11138                .map_err(|_| crate::world_participant::ParticipantFault::InvalidState)
11139        }
11140        fn observe(&self, _: &mut crate::world_participant::ObservationSink) {}
11141        fn write_state(&self, _: &mut Vec<u8>) {}
11142        fn check_state(&self, _: &[u8]) -> Result<(), crate::world_participant::StateError> {
11143            Ok(())
11144        }
11145        fn read_state(&mut self, _: &[u8]) {}
11146    }
11147
11148    /// oracle: under the TGS backend, with no gravity and no damping, a
11149    /// participant applying `F = (4, 0, 0)` to a body of mass 2 for one frame
11150    /// `dt = 1/64` (8 dyadic substeps) gives `Δv = F·dt/m = 1/32` along `x`,
11151    /// with and without a joint in the world; a participant that fails
11152    /// records a participant fault and applies nothing.
11153    #[test]
11154    fn tgs_participant_force_gives_f_dt_over_m() {
11155        for with_joint in [false, true] {
11156            for fail in [false, true] {
11157                let mut world = PhysicsWorld::new(SolverConfig {
11158                    gravity: Vec3Fix::ZERO,
11159                    damping: Fix128::ONE,
11160                    solver_backend: SolverBackend::Tgs,
11161                    ..Default::default()
11162                });
11163                let body = world.add_body(RigidBody::new(Vec3Fix::ZERO, Fix128::from_int(2)));
11164                if with_joint {
11165                    let other =
11166                        world.add_body(RigidBody::new(Vec3Fix::from_int(0, 10, 0), Fix128::ONE));
11167                    let anchor = world.add_body(RigidBody::new_static(Vec3Fix::from_int(0, 12, 0)));
11168                    world.add_joint(Joint::Ball(crate::joint::BallJoint::new(
11169                        other,
11170                        anchor,
11171                        Vec3Fix::ZERO,
11172                        Vec3Fix::from_int(0, -2, 0),
11173                    )));
11174                }
11175                world
11176                    .add_participant(Box::new(ConstantPush {
11177                        body,
11178                        force: Vec3Fix::from_int(4, 0, 0),
11179                        fail,
11180                    }))
11181                    .expect("register");
11182                world.step(Fix128::from_ratio(1, 64));
11183                let v = world.bodies[body].velocity;
11184                if fail {
11185                    assert_eq!(v, Vec3Fix::ZERO, "joint {with_joint}");
11186                    assert!(
11187                        matches!(
11188                            world.fault(),
11189                            Some(crate::world_participant::WorldFault::Participant {
11190                                index: 0,
11191                                ..
11192                            })
11193                        ),
11194                        "joint {with_joint}: {:?}",
11195                        world.fault()
11196                    );
11197                } else {
11198                    assert!(
11199                        (v.x.to_f64() - 1.0 / 32.0).abs() < 1e-12,
11200                        "joint {with_joint}: v {v:?}"
11201                    );
11202                    assert!(
11203                        v.y.is_zero() && v.z.is_zero(),
11204                        "joint {with_joint}: v {v:?}"
11205                    );
11206                    assert_eq!(world.fault(), None);
11207                }
11208            }
11209        }
11210    }
11211
11212    /// oracle: for an isotropic `inv_inertia = (c, c, c)` the checked world
11213    /// inverse inertia is `Some(c · τ)` (component-wise `checked_mul`) for 64
11214    /// non-identity orientations, equal to the unchecked value, and `None`
11215    /// when a product leaves the range.
11216    #[test]
11217    fn checked_world_inv_inertia_apply_of_an_isotropic_body_is_c_tau() {
11218        let c = Fix128::from_ratio(5, 2);
11219        let tau = Vec3Fix::new(
11220            Fix128::from_ratio(3, 7),
11221            Fix128::from_ratio(-11, 13),
11222            Fix128::from_ratio(1, 3),
11223        );
11224        let expected = Vec3Fix::new(tau.x * c, tau.y * c, tau.z * c);
11225        let mut differ = 0;
11226        for i in 0..16_i64 {
11227            let axis = Vec3Fix::new(
11228                Fix128::from_ratio(1 + i, 3),
11229                Fix128::from_ratio(2 - i, 5),
11230                Fix128::from_ratio(3 + 2 * i, 7),
11231            )
11232            .normalize();
11233            for k in 1..=4_i64 {
11234                let mut b = RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE);
11235                b.inv_inertia = Vec3Fix::new(c, c, c);
11236                b.rotation =
11237                    QuatFix::from_axis_angle(axis, Fix128::from_ratio(7 * k + i, 9)).normalize();
11238                let got = b.checked_world_inv_inertia_apply(tau);
11239                assert_eq!(got, Some(b.world_inv_inertia_apply(tau)));
11240                differ += usize::from(got != Some(expected));
11241            }
11242        }
11243        assert_eq!(differ, 0, "{differ}/64 orientations differ from c·τ");
11244        let mut big = RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE);
11245        big.inv_inertia = Vec3Fix::new(
11246            Fix128::from_int(4),
11247            Fix128::from_int(4),
11248            Fix128::from_int(4),
11249        );
11250        let huge = Vec3Fix::new(Fix128::from_int(1_i64 << 62), Fix128::ZERO, Fix128::ZERO);
11251        assert_eq!(big.checked_world_inv_inertia_apply(huge), None);
11252    }
11253}
11254
11255/// loom model of the invariant `BodySlicePtr` / `DistConstraintSlicePtr` /
11256/// `ContactConstraintSlicePtr` rely on: every constraint of one colour batch
11257/// touches disjoint dynamic bodies, so the batch can be written by any
11258/// number of threads without synchronisation.
11259///
11260/// The raw-pointer wrappers themselves are invisible to loom (plain memory);
11261/// what loom can exhaustively check is the *protocol*: take the real batches
11262/// `rebuild_batches` produces for scenes that exercise the hard cases (a
11263/// shared static floor, a dynamic hub, rods under contact), give every
11264/// constraint of a batch to its own loom thread, and let each thread write
11265/// its two body slots through a `loom::cell::UnsafeCell`. If the colouring
11266/// ever placed two constraints sharing a dynamic body in one batch, loom
11267/// reports the unsynchronised concurrent access on one of its interleavings.
11268/// Run with `RUSTFLAGS="--cfg loom" cargo test --lib loom_` (quality-deep).
11269#[cfg(all(test, loom))]
11270mod loom_tests {
11271    use super::*;
11272    use loom::cell::UnsafeCell;
11273    use loom::sync::Arc;
11274    use loom::thread;
11275
11276    fn v3(x: i64, y: i64, z: i64) -> Vec3Fix {
11277        Vec3Fix::from_int(x, y, z)
11278    }
11279
11280    /// (dynamic-body pairs per batch) for a scene: static bodies are excluded
11281    /// because the solver borrows them shared (`BodyRef::Static`).
11282    fn batches_of(world: &mut PhysicsWorld) -> Vec<Vec<(Option<usize>, Option<usize>)>> {
11283        world.rebuild_batches();
11284        let dynamic = |i: usize| (!world.bodies[i].inv_mass.is_zero()).then_some(i);
11285        world
11286            .constraint_batches
11287            .iter()
11288            .map(|b| {
11289                let mut pairs = Vec::new();
11290                for &ci in &b.distance_indices {
11291                    let c = world.distance_constraints[ci];
11292                    pairs.push((dynamic(c.body_a), dynamic(c.body_b)));
11293                }
11294                for &ci in &b.contact_indices {
11295                    let c = world.contact_constraints[ci];
11296                    pairs.push((dynamic(c.body_a), dynamic(c.body_b)));
11297                }
11298                pairs
11299            })
11300            .collect()
11301    }
11302
11303    fn check_batches(batches: Vec<Vec<(Option<usize>, Option<usize>)>>, n_bodies: usize) {
11304        for batch in batches {
11305            // loom explores every interleaving of up to a handful of threads
11306            for chunk in batch.chunks(3) {
11307                let chunk: Vec<_> = chunk.to_vec();
11308                let n = n_bodies;
11309                loom::model(move || {
11310                    let slots: Arc<Vec<UnsafeCell<u32>>> =
11311                        Arc::new((0..n).map(|_| UnsafeCell::new(0)).collect());
11312                    let handles: Vec<_> = chunk
11313                        .iter()
11314                        .copied()
11315                        .map(|(a, b)| {
11316                            let slots = Arc::clone(&slots);
11317                            thread::spawn(move || {
11318                                for body in [a, b].into_iter().flatten() {
11319                                    // the solver writes position / velocity of
11320                                    // each dynamic body of its constraint
11321                                    slots[body].with_mut(|p| unsafe { *p += 1 });
11322                                }
11323                            })
11324                        })
11325                        .collect();
11326                    for h in handles {
11327                        h.join().unwrap();
11328                    }
11329                });
11330            }
11331        }
11332    }
11333
11334    #[test]
11335    fn loom_batches_touch_disjoint_dynamic_bodies_static_floor_hub() {
11336        // 6 dynamic spheres resting on one static sphere + 2 rods
11337        let mut world = PhysicsWorld::new(SolverConfig {
11338            gravity: Vec3Fix::ZERO,
11339            ..SolverConfig::default()
11340        });
11341        let floor =
11342            world.add_body_with_radius(RigidBody::new_static(Vec3Fix::ZERO), Fix128::from_int(4));
11343        let mut ids = Vec::new();
11344        for i in 0..6i64 {
11345            let angle = Fix128::from_ratio(i, 6) * Fix128::TWO_PI;
11346            let (s, c) = angle.sin_cos();
11347            let pos = Vec3Fix::new(
11348                c * Fix128::from_int(4),
11349                s * Fix128::from_int(4),
11350                Fix128::ZERO,
11351            );
11352            ids.push(
11353                world.add_body_with_radius(RigidBody::new_dynamic(pos, Fix128::ONE), Fix128::ONE),
11354            );
11355        }
11356        for w in ids.windows(2).take(2) {
11357            world.add_distance_constraint(DistanceConstraint {
11358                body_a: w[0],
11359                body_b: w[1],
11360                local_anchor_a: Vec3Fix::ZERO,
11361                local_anchor_b: Vec3Fix::ZERO,
11362                target_distance: Fix128::from_int(4),
11363                compliance: Fix128::ZERO,
11364                cached_lambda: Fix128::ZERO,
11365            });
11366        }
11367        let _ = floor;
11368        world.clear_contacts();
11369        world.detect_collisions();
11370        assert!(
11371            !world.contact_constraints.is_empty(),
11372            "spheres must touch the floor"
11373        );
11374        let n = world.bodies.len();
11375        check_batches(batches_of(&mut world), n);
11376    }
11377
11378    #[test]
11379    fn loom_batches_touch_disjoint_dynamic_bodies_dynamic_hub() {
11380        // one dynamic hub connected to 5 dynamic spokes: every spoke rod
11381        // shares the hub, so the colouring must serialise them into 5 batches
11382        let mut world = PhysicsWorld::new(SolverConfig {
11383            gravity: Vec3Fix::ZERO,
11384            ..SolverConfig::default()
11385        });
11386        let hub = world.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
11387        for i in 0..5i64 {
11388            let spoke = world.add_body(RigidBody::new_dynamic(v3(3 * (i + 1), 0, 0), Fix128::ONE));
11389            world.add_distance_constraint(DistanceConstraint {
11390                body_a: hub,
11391                body_b: spoke,
11392                local_anchor_a: Vec3Fix::ZERO,
11393                local_anchor_b: Vec3Fix::ZERO,
11394                target_distance: Fix128::from_int(3 * (i + 1)),
11395                compliance: Fix128::ZERO,
11396                cached_lambda: Fix128::ZERO,
11397            });
11398        }
11399        let n = world.bodies.len();
11400        let batches = batches_of(&mut world);
11401        assert_eq!(batches.len(), 5, "hub forces one batch per spoke");
11402        check_batches(batches, n);
11403    }
11404}