Skip to main content

loam_physics/
world.rs

1use std::collections::{BTreeMap, HashMap};
2use std::ops::{Add, Mul};
3
4use loam_math::Bivector;
5use loam_time::par;
6use loam_time::StateHash;
7
8use crate::body::{BodyArena, BodyDef, BodyId, RigidBody};
9use crate::collider::Collider;
10use crate::collision::VectorOps;
11use crate::dirty::{DirtyBodies, DirtyDrain};
12use crate::edit::EditError;
13use crate::field_contact::FieldNarrowphase;
14use crate::geometry::GeometryStore;
15use crate::integrator::{integrate_body, BroadphaseBound, PhysicsSpace};
16use crate::manifold::{
17    ContactPoint, Manifold, BAUMGARTE_BETA, DEFAULT_PGS_ITERS, MAX_LINEAR_CORRECTION,
18    PENETRATION_SLOP, RESTITUTION_THRESHOLD,
19};
20use crate::narrowphase::Narrowphase;
21use crate::response::Contact;
22use crate::response::FRICTION_COEFF;
23use crate::state::WorldState;
24
25/// Handles in ascending order.
26pub type PairKey = (BodyId, BodyId);
27
28fn canonical_pair(a: BodyId, b: BodyId) -> PairKey {
29    debug_assert_ne!(a, b, "a body cannot pair with itself");
30    if a < b {
31        (a, b)
32    } else {
33        (b, a)
34    }
35}
36
37/// A connected component of the contact graph, over dynamic bodies only.
38#[derive(Clone, Debug, PartialEq, Eq)]
39pub struct Island {
40    /// Lowest handle among [`Self::bodies`].
41    pub id: BodyId,
42    /// Ascending.
43    pub bodies: Vec<BodyId>,
44    /// Ascending pairs; static contacts belong to their dynamic side.
45    pub constraints: Vec<PairKey>,
46}
47
48pub const DEFAULT_SOLVER_TOLERANCE: f32 = 1e-3;
49
50const WAKE_APPROACH_SPEED: f32 = 0.01;
51
52/// Measured, not derived; the sweep is in docs/PERF.md.
53pub const ISLANDS_PER_SOLVE_WORKER: usize = 256;
54
55const STALE_CONSTRAINT_KEY: &str = "constraint buffer outlived its manifold";
56const STALE_MANIFOLD_BODY: &str = "manifold key names a body that is gone";
57
58// do Carmo, Riemannian Geometry, 1992, ch. 7, prop. 3.6.
59const BROADPHASE_TRIANGLE_SLACK: f32 = 4.0 * f32::EPSILON;
60
61#[derive(Clone, Copy)]
62struct RadialInterval {
63    lo: f32,
64    hi: f32,
65    radius: f32,
66    dense: u32,
67    id: BodyId,
68    dynamic: bool,
69    group: u32,
70    mask: u32,
71}
72
73fn bounding_radius(collider: Option<&Collider>) -> f32 {
74    let Some(collider) = collider else {
75        return 0.0;
76    };
77    match collider {
78        Collider::Sphere { radius, .. } | Collider::HyperSphere4D { radius, .. } => *radius,
79        Collider::Box3 { half_extents } => half_extents.length(),
80        Collider::Polygon2D { vertices } => max_norm(vertices.iter().map(|v| v.length_squared())),
81        Collider::ConvexPolytope3D { vertices } => {
82            max_norm(vertices.iter().map(|v| v.length_squared()))
83        }
84        Collider::ConvexPolytope4D { vertices } => {
85            max_norm(vertices.iter().map(|v| v.length_squared()))
86        }
87        Collider::HalfSpace { .. } | Collider::HalfSpace4D { .. } => f32::INFINITY,
88    }
89}
90
91fn max_norm(norms_squared: impl Iterator<Item = f32>) -> f32 {
92    norms_squared.fold(0.0_f32, f32::max).sqrt()
93}
94
95/// `residual` is the largest normal-impulse change of the last sweep; `converged` compares it to `World::solver_tolerance`.
96#[derive(Clone, Copy, Debug, PartialEq)]
97pub struct SolveReport {
98    pub residual: f32,
99    pub converged: bool,
100}
101
102impl Default for SolveReport {
103    fn default() -> Self {
104        Self {
105            residual: 0.0,
106            converged: true,
107        }
108    }
109}
110
111struct ScratchUnit {
112    key: PairKey,
113    a: u32,
114    b: u32,
115    first: u32,
116    count: u32,
117}
118
119struct IslandSolve<S: PhysicsSpace> {
120    bodies: Vec<RigidBody<S>>,
121    dense: Vec<u32>,
122    units: Vec<ScratchUnit>,
123    points: Vec<ContactPoint<S>>,
124    residual: f32,
125    push: Vec<f32>,
126    saved: Vec<RigidBody<S>>,
127}
128
129impl<S: PhysicsSpace> Default for IslandSolve<S> {
130    fn default() -> Self {
131        Self {
132            bodies: Vec::new(),
133            dense: Vec::new(),
134            units: Vec::new(),
135            points: Vec::new(),
136            residual: 0.0,
137            push: Vec::new(),
138            saved: Vec::new(),
139        }
140    }
141}
142
143const SCATTERED_NOWHERE: u32 = u32::MAX;
144
145#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
146pub struct StepCounters {
147    pub index_visits: u32,
148    pub distance_evals: u32,
149    pub candidates: u32,
150    pub contacts: u32,
151}
152
153#[derive(Clone, Copy)]
154struct ConstraintUnit {
155    island: BodyId,
156    key: PairKey,
157    dense: (usize, usize),
158}
159
160#[cfg_attr(feature = "persist", derive(serde::Serialize, serde::Deserialize))]
161#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
162pub struct FieldId(u32);
163
164struct FieldEntry {
165    anchor: BodyId,
166    field: Box<dyn loam_shape::field::DistanceField>,
167}
168
169pub struct World<S: PhysicsSpace> {
170    space: S,
171    pub(crate) bodies: BodyArena<S>,
172    gravity: Option<S::Vector>,
173    pub narrowphase: Narrowphase<S>,
174    pub field_narrowphase: FieldNarrowphase<S>,
175    fields: Vec<FieldEntry>,
176    field_bindings: Vec<(BodyId, FieldId)>,
177    geometry: GeometryStore,
178    pub(crate) manifolds: HashMap<PairKey, Manifold<S>>,
179    manifold_order: Vec<PairKey>,
180    manifold_pool: Vec<Manifold<S>>,
181    pending_contacts: Vec<(PairKey, Contact<S>)>,
182    pgs_iters: usize,
183    time: f32,
184    dirty: DirtyBodies,
185    pair_order: Vec<PairKey>,
186    constraints: Vec<ConstraintUnit>,
187    broadphase_intervals: Vec<RadialInterval>,
188    broadphase_active: Vec<u32>,
189    touched_pairs: Vec<PairKey>,
190    island_parent: Vec<u32>,
191    island_labels: Vec<BodyId>,
192    counters: StepCounters,
193    solver_tolerance: f32,
194    report: SolveReport,
195    scratch: Vec<IslandSolve<S>>,
196    scratch_islands: usize,
197    scratch_local: Vec<u32>,
198}
199
200const _: () = {
201    const fn assert_send_sync<T: Send + Sync>() {}
202    const fn assert_no_allocation<T: Copy>() {}
203    #[cfg(feature = "r2")]
204    {
205        assert_send_sync::<World<loam_math::EuclideanR2>>();
206        assert_no_allocation::<RigidBody<loam_math::EuclideanR2>>();
207    }
208    #[cfg(feature = "r3")]
209    {
210        assert_send_sync::<World<loam_math::EuclideanR3>>();
211        assert_no_allocation::<RigidBody<loam_math::EuclideanR3>>();
212    }
213    #[cfg(feature = "r4")]
214    {
215        assert_send_sync::<World<loam_math::EuclideanR4>>();
216        assert_no_allocation::<RigidBody<loam_math::EuclideanR4>>();
217    }
218};
219
220impl<S: PhysicsSpace> World<S> {
221    pub fn new(space: S) -> Self {
222        Self {
223            space,
224            bodies: BodyArena::new(),
225            gravity: None,
226            narrowphase: Narrowphase::new(),
227            field_narrowphase: FieldNarrowphase::new(),
228            fields: Vec::new(),
229            field_bindings: Vec::new(),
230            geometry: GeometryStore::default(),
231            manifolds: HashMap::new(),
232            manifold_order: Vec::new(),
233            manifold_pool: Vec::new(),
234            pending_contacts: Vec::new(),
235            pgs_iters: DEFAULT_PGS_ITERS,
236            time: 0.0,
237            dirty: DirtyBodies::default(),
238            pair_order: Vec::new(),
239            constraints: Vec::new(),
240            broadphase_intervals: Vec::new(),
241            broadphase_active: Vec::new(),
242            touched_pairs: Vec::new(),
243            island_parent: Vec::new(),
244            island_labels: Vec::new(),
245            counters: StepCounters::default(),
246            solver_tolerance: DEFAULT_SOLVER_TOLERANCE,
247            report: SolveReport::default(),
248            scratch: Vec::new(),
249            scratch_islands: 0,
250            scratch_local: Vec::new(),
251        }
252    }
253
254    /// From the last `step` or `broadphase_into`.
255    pub fn counters(&self) -> StepCounters {
256        self.counters
257    }
258
259    pub fn bodies(&self) -> &BodyArena<S> {
260        &self.bodies
261    }
262
263    pub fn space(&self) -> &S {
264        &self.space
265    }
266
267    pub fn gravity(&self) -> Option<S::Vector> {
268        self.gravity
269    }
270
271    pub fn set_gravity(&mut self, gravity: Option<S::Vector>) -> Result<(), EditError> {
272        if gravity.is_some_and(|gravity| !self.space.valid_vector(gravity)) {
273            return Err(EditError::InvalidGravity);
274        }
275        self.gravity = gravity;
276        Ok(())
277    }
278
279    pub fn solver_iterations(&self) -> usize {
280        self.pgs_iters
281    }
282
283    pub fn set_solver_iterations(&mut self, iterations: usize) {
284        self.pgs_iters = iterations;
285    }
286
287    pub fn solver_tolerance(&self) -> f32 {
288        self.solver_tolerance
289    }
290
291    pub fn set_solver_tolerance(&mut self, tolerance: f32) -> Result<(), EditError> {
292        if tolerance.is_nan() || tolerance < 0.0 {
293            return Err(EditError::InvalidSolverTolerance);
294        }
295        self.solver_tolerance = tolerance;
296        Ok(())
297    }
298
299    pub fn time(&self) -> f32 {
300        self.time
301    }
302
303    pub fn body(&self, id: BodyId) -> Option<&RigidBody<S>> {
304        self.bodies.get(id)
305    }
306
307    pub fn manifolds(&self) -> impl ExactSizeIterator<Item = (&PairKey, &Manifold<S>)> {
308        self.manifold_order
309            .iter()
310            .map(|key| (key, &self.manifolds[key]))
311    }
312
313    pub fn manifold(&self, key: PairKey) -> Option<&Manifold<S>> {
314        self.manifolds.get(&key)
315    }
316
317    pub fn solve_report(&self) -> SolveReport {
318        self.report
319    }
320
321    pub fn push_body(&mut self, body: BodyDef<S>) -> BodyId {
322        let row = body.into_row(&mut self.geometry, &self.space);
323        let id = self.bodies.spawn(row);
324        self.dirty.mark(id);
325        id
326    }
327
328    /// `anchor` must be static; it leaves every collision group so the broadphase never pairs it, and it owns the field's contacts.
329    pub fn insert_field(
330        &mut self,
331        anchor: BodyId,
332        field: Box<dyn loam_shape::field::DistanceField>,
333    ) -> Result<FieldId, EditError> {
334        let Some(body) = self.bodies.get_mut(anchor) else {
335            return Err(EditError::StaleHandle);
336        };
337        if !body.is_static() {
338            return Err(EditError::DynamicFieldAnchor);
339        }
340        body.collision_group = 0;
341        body.collision_mask = 0;
342        self.drop_contacts_of(anchor);
343        let id = FieldId(self.fields.len() as u32);
344        self.fields.push(FieldEntry { anchor, field });
345        Ok(id)
346    }
347
348    /// Every step queries the field for `body`; a contact lands in the manifold keyed by the body and the field's anchor.
349    pub fn bind_field(&mut self, body: BodyId, field: FieldId) -> Result<(), EditError> {
350        let Some(entry) = self.fields.get(field.0 as usize) else {
351            return Err(EditError::StaleHandle);
352        };
353        if entry.anchor == body {
354            return Err(EditError::AnchorBindsOwnField);
355        }
356        if self.bodies.get(body).is_none() {
357            return Err(EditError::StaleHandle);
358        }
359        if let Err(at) = self.field_bindings.binary_search(&(body, field)) {
360            self.field_bindings.insert(at, (body, field));
361        }
362        Ok(())
363    }
364
365    pub fn field_bindings(&self) -> &[(BodyId, FieldId)] {
366        &self.field_bindings
367    }
368
369    pub fn geometry(&self) -> &GeometryStore {
370        &self.geometry
371    }
372
373    /// Pops the newest of the last two released shapes so its buffer can be reused.
374    pub fn reclaim_geometry(&mut self) -> Option<Collider> {
375        self.geometry.take_released()
376    }
377
378    pub fn collider(&self, body: &RigidBody<S>) -> Option<&Collider> {
379        self.geometry.get(body.collider())
380    }
381
382    /// Also removes every manifold the body takes part in.
383    pub fn despawn_body(&mut self, id: BodyId) -> Result<(), EditError> {
384        if self.fields.iter().any(|field| field.anchor == id) {
385            return Err(EditError::FieldAnchorRemoval);
386        }
387        let Some(removed) = self.bodies.despawn(id) else {
388            return Err(EditError::StaleHandle);
389        };
390        self.field_bindings.retain(|&(body, _)| body != id);
391        self.geometry.release(removed.collider());
392        self.drop_contacts_of(id);
393        self.dirty.forget(id);
394        Ok(())
395    }
396
397    /// Drops the body's contacts.
398    pub fn set_pose(
399        &mut self,
400        id: BodyId,
401        position: S::Point,
402        orientation: S::Iso,
403    ) -> Result<(), EditError> {
404        if self.bodies.get(id).is_none() {
405            return Err(EditError::StaleHandle);
406        }
407        if !self.space.valid_point(position) || !self.space.valid_orientation(orientation) {
408            return Err(EditError::NotFinite);
409        }
410        let body = &mut self.bodies[id];
411        body.position = position;
412        body.orientation = orientation;
413        body.wake();
414        self.drop_contacts_of(id);
415        self.dirty.mark(id);
416        Ok(())
417    }
418
419    pub fn set_velocity(
420        &mut self,
421        id: BodyId,
422        velocity: S::Vector,
423        angular_velocity: S::AngVel,
424    ) -> Result<(), EditError> {
425        if self.bodies.get(id).is_none() {
426            return Err(EditError::StaleHandle);
427        }
428        if !self.space.valid_vector(velocity)
429            || !self.space.valid_angular_velocity(angular_velocity)
430        {
431            return Err(EditError::NotFinite);
432        }
433        let body = &mut self.bodies[id];
434        body.velocity = velocity;
435        body.angular_velocity = angular_velocity;
436        body.wake();
437        self.dirty.mark(id);
438        Ok(())
439    }
440
441    pub fn set_restitution(&mut self, id: BodyId, restitution: f32) -> Result<(), EditError> {
442        let Some(body) = self.bodies.get_mut(id) else {
443            return Err(EditError::StaleHandle);
444        };
445        if !restitution.is_finite() || restitution < 0.0 {
446            return Err(EditError::InvalidRestitution);
447        }
448        body.restitution = restitution;
449        self.drop_contacts_of(id);
450        Ok(())
451    }
452
453    pub fn set_collision_filter(
454        &mut self,
455        id: BodyId,
456        group: u32,
457        mask: u32,
458    ) -> Result<(), EditError> {
459        if self.fields.iter().any(|field| field.anchor == id) && (group != 0 || mask != 0) {
460            return Err(EditError::InvalidFieldBinding);
461        }
462        let Some(body) = self.bodies.get_mut(id) else {
463            return Err(EditError::StaleHandle);
464        };
465        body.collision_group = group;
466        body.collision_mask = mask;
467        self.drop_contacts_of(id);
468        Ok(())
469    }
470
471    /// Drops the body's contacts.
472    pub fn set_mass_properties(
473        &mut self,
474        id: BodyId,
475        mass: f32,
476        inertia: S::Inertia,
477    ) -> Result<(), EditError> {
478        if mass > 0.0 && self.fields.iter().any(|field| field.anchor == id) {
479            return Err(EditError::DynamicFieldAnchor);
480        }
481        if self.bodies.get(id).is_none() {
482            return Err(EditError::StaleHandle);
483        }
484        if !self.space.valid_inertia(inertia) {
485            return Err(EditError::InvalidInertia);
486        }
487        self.bodies[id].set_mass_properties(mass, inertia)?;
488        self.drop_contacts_of(id);
489        self.dirty.mark(id);
490        Ok(())
491    }
492
493    /// Drops the body's contacts.
494    pub fn set_collider(
495        &mut self,
496        id: BodyId,
497        collider: Collider,
498        inertia: S::Inertia,
499    ) -> Result<(), EditError> {
500        if self.bodies.get(id).is_none() {
501            self.geometry.stash(collider);
502            return Err(EditError::StaleHandle);
503        }
504        if !self.space.valid_inertia(inertia) {
505            self.geometry.stash(collider);
506            return Err(EditError::InvalidInertia);
507        }
508        let Self {
509            space,
510            bodies,
511            geometry,
512            ..
513        } = self;
514        bodies[id].replace_collider(geometry, space, collider, inertia)?;
515        self.drop_contacts_of(id);
516        self.dirty.mark(id);
517        Ok(())
518    }
519
520    pub fn apply_impulse(&mut self, id: BodyId, impulse: S::Vector) -> Result<(), EditError>
521    where
522        S::Vector: Add<Output = S::Vector> + Mul<f32, Output = S::Vector>,
523    {
524        if self.bodies.get(id).is_none() {
525            return Err(EditError::StaleHandle);
526        }
527        if !self.space.valid_vector(impulse) {
528            return Err(EditError::NotFinite);
529        }
530        self.bodies[id].apply_impulse(impulse);
531        self.dirty.mark(id);
532        Ok(())
533    }
534
535    pub fn apply_impulse_at_point(
536        &mut self,
537        id: BodyId,
538        impulse: S::Vector,
539        point: S::Point,
540    ) -> Result<(), EditError>
541    where
542        S::Vector: Add<Output = S::Vector> + Mul<f32, Output = S::Vector>,
543    {
544        if self.bodies.get(id).is_none() {
545            return Err(EditError::StaleHandle);
546        }
547        if !self.space.valid_vector(impulse) || !self.space.valid_point(point) {
548            return Err(EditError::NotFinite);
549        }
550        let space = &self.space;
551        self.bodies[id].apply_impulse_at_point(space, impulse, point);
552        self.dirty.mark(id);
553        Ok(())
554    }
555
556    pub fn sleep_body(&mut self, id: BodyId) -> Result<(), EditError>
557    where
558        S::Vector: Default,
559    {
560        let Some(body) = self.bodies.get_mut(id) else {
561            return Err(EditError::StaleHandle);
562        };
563        body.sleep();
564        self.dirty.mark(id);
565        Ok(())
566    }
567
568    pub fn wake_body(&mut self, id: BodyId) -> Result<(), EditError> {
569        let Some(body) = self.bodies.get_mut(id) else {
570            return Err(EditError::StaleHandle);
571        };
572        body.wake();
573        self.dirty.mark(id);
574        Ok(())
575    }
576
577    fn drop_contacts_of(&mut self, id: BodyId) {
578        let mut write = 0;
579        for read in 0..self.manifold_order.len() {
580            let key = self.manifold_order[read];
581            if key.0 == id || key.1 == id {
582                if let Some(manifold) = self.manifolds.remove(&key) {
583                    self.manifold_pool.push(manifold);
584                }
585            } else {
586                self.manifold_order[write] = key;
587                write += 1;
588            }
589        }
590        self.manifold_order.truncate(write);
591    }
592
593    /// Yields each body spawned, integrated, edited, or restored since the last drain, skipping despawned ones.
594    pub fn drain_dirty(&mut self) -> DirtyDrain<'_, S> {
595        let Self { bodies, dirty, .. } = self;
596        dirty.drain(bodies)
597    }
598
599    pub fn snapshot(&self) -> WorldState<S> {
600        WorldState {
601            bodies: self.bodies.clone(),
602            geometry: self.geometry.clone(),
603            manifolds: self
604                .manifolds()
605                .map(|(&key, manifold)| (key, manifold.clone()))
606                .collect(),
607            time: self.time,
608            field_bindings: self.field_bindings.clone(),
609            field_anchors: self.fields.iter().map(|entry| entry.anchor).collect(),
610            registrations: self.narrowphase.registrations().to_vec(),
611            field_registrations: self.field_narrowphase.registrations().to_vec(),
612        }
613    }
614
615    pub fn check_restore(&self, state: &WorldState<S>) -> Result<(), EditError>
616    where
617        S::Vector: VectorOps,
618        S::AngVel: PartialEq,
619        S::Point: Copy + std::ops::Sub<Output = S::Vector>,
620    {
621        if self
622            .gravity
623            .is_some_and(|gravity| !self.space.valid_vector(gravity))
624        {
625            return Err(EditError::InvalidGravity);
626        }
627        if self.solver_tolerance.is_nan() || self.solver_tolerance < 0.0 {
628            return Err(EditError::InvalidSolverTolerance);
629        }
630        let field_registrations = self.field_narrowphase.registrations();
631        if self.narrowphase.registrations() != state.registrations
632            || field_registrations.len() != state.field_registrations.len()
633            || state
634                .field_registrations
635                .iter()
636                .any(|kind| !field_registrations.contains(kind))
637        {
638            return Err(EditError::RegistrationMismatch);
639        }
640        if self.fields.len() != state.field_anchors.len()
641            || self
642                .fields
643                .iter()
644                .zip(&state.field_anchors)
645                .any(|(entry, &anchor)| entry.anchor != anchor)
646        {
647            return Err(EditError::FieldMismatch);
648        }
649        if !state.time.is_finite() || state.time < 0.0 {
650            return Err(EditError::InvalidTime);
651        }
652        state.bodies.validate(&self.space, &state.geometry)?;
653        for (&anchor, entry) in state.field_anchors.iter().zip(&self.fields) {
654            let Some(body) = state.bodies.get(anchor) else {
655                return Err(EditError::InvalidFieldBinding);
656            };
657            if entry.anchor != anchor
658                || !body.is_static()
659                || body.collision_group != 0
660                || body.collision_mask != 0
661            {
662                return Err(EditError::InvalidFieldBinding);
663            }
664        }
665        if state
666            .field_bindings
667            .windows(2)
668            .any(|pair| pair[0] >= pair[1])
669        {
670            return Err(EditError::InvalidFieldBinding);
671        }
672        for &(body, field) in &state.field_bindings {
673            let Some(entry) = self.fields.get(field.0 as usize) else {
674                return Err(EditError::InvalidFieldBinding);
675            };
676            if body == entry.anchor || state.bodies.get(body).is_none() {
677                return Err(EditError::InvalidFieldBinding);
678            }
679        }
680        for (&key, manifold) in &state.manifolds {
681            let (Some(body_a), Some(body_b)) = (state.bodies.get(key.0), state.bodies.get(key.1))
682            else {
683                return Err(EditError::InvalidManifold);
684            };
685            if key.0 >= key.1 || manifold.body_a != key.0 || manifold.body_b != key.1 {
686                return Err(EditError::InvalidManifold);
687            }
688            let field_pair = state.field_bindings.iter().any(|&(body, field)| {
689                let anchor = self.fields[field.0 as usize].anchor;
690                canonical_pair(body, anchor) == key
691                    && state.bodies.get(body).is_some_and(|body| {
692                        state.field_registrations.contains(&body.collider().kind())
693                    })
694            });
695            let registered_pair = self.narrowphase.registrations().iter().any(|&(a, b)| {
696                let pair = (body_a.collider().kind(), body_b.collider().kind());
697                (a, b) == pair || (b, a) == pair
698            });
699            let body_pair = registered_pair
700                && !(body_a.is_static() && body_b.is_static())
701                && body_a.collision_mask & body_b.collision_group != 0
702                && body_b.collision_mask & body_a.collision_group != 0;
703            if (!field_pair && !body_pair) || !manifold.is_valid(&self.space, body_a, body_b) {
704                return Err(EditError::InvalidManifold);
705            }
706        }
707        Ok(())
708    }
709
710    /// Refuses, unchanged, whatever `check_restore` refuses.
711    pub fn restore(&mut self, state: &WorldState<S>) -> Result<(), EditError>
712    where
713        S::Vector: VectorOps,
714        S::AngVel: PartialEq,
715        S::Point: Copy + std::ops::Sub<Output = S::Vector>,
716    {
717        self.check_restore(state)?;
718        self.bodies = state.bodies.clone();
719        self.geometry = state.geometry.clone();
720        let required_pool = state
721            .manifolds
722            .len()
723            .max(self.manifold_pool.len() + self.manifold_order.len());
724        if self.manifold_pool.capacity() < required_pool {
725            self.manifold_pool
726                .reserve_exact(required_pool - self.manifold_pool.len());
727        }
728        for key in self.manifold_order.drain(..) {
729            if let Some(manifold) = self.manifolds.remove(&key) {
730                self.manifold_pool.push(manifold);
731            }
732        }
733        for (&key, manifold) in &state.manifolds {
734            let restored = if let Some(mut restored) = self.manifold_pool.pop() {
735                restored.clone_from(manifold);
736                restored
737            } else {
738                manifold.clone()
739            };
740            self.manifolds.insert(key, restored);
741            self.manifold_order.push(key);
742        }
743        self.time = state.time;
744        self.field_bindings.clear();
745        self.field_bindings.extend_from_slice(&state.field_bindings);
746        self.dirty.mark_every(&self.bodies);
747        self.pair_order.clear();
748        self.constraints.clear();
749        self.touched_pairs.clear();
750        self.pending_contacts.clear();
751        self.broadphase_intervals.clear();
752        self.broadphase_active.clear();
753        self.island_parent.clear();
754        self.island_labels.clear();
755        self.scratch_islands = 0;
756        self.report = SolveReport::default();
757        Ok(())
758    }
759
760    /// `dt` is in seconds.
761    pub fn step(&mut self, dt: f32) -> Result<(), EditError>
762    where
763        S::Vector: VectorOps,
764        S::Point: Copy + std::ops::Sub<Output = S::Vector>,
765    {
766        if !dt.is_finite() || dt < 0.0 {
767            return Err(EditError::InvalidTimeStep);
768        }
769        let next_time = self.time + dt;
770        if !next_time.is_finite() {
771            return Err(EditError::InvalidTimeStep);
772        }
773        self.counters = StepCounters::default();
774        // Test approach before gravity adds support-load velocity.
775        self.wake_approaching_contacts();
776        self.apply_forces(dt);
777        self.integrate(dt);
778        self.update_manifolds();
779        self.collect_constraints();
780        self.prepare_solve();
781        self.warm_start();
782        self.solve(dt);
783
784        self.time = next_time;
785        Ok(())
786    }
787
788    fn apply_forces(&mut self, dt: f32)
789    where
790        S::Vector: VectorOps,
791    {
792        let Some(acceleration) = self.gravity else {
793            return;
794        };
795        for body in self.bodies.iter_mut() {
796            if body.inv_mass() != 0.0 {
797                body.velocity = body.velocity + acceleration * dt;
798            }
799        }
800    }
801
802    fn integrate(&mut self, dt: f32)
803    where
804        S::Vector: VectorOps,
805    {
806        for i in 0..self.bodies.len() {
807            if self.bodies[i].inv_mass() == 0.0 {
808                continue;
809            }
810            integrate_body(&self.space, &mut self.bodies[i], dt);
811            let id = self.bodies.id_at(i);
812            self.dirty.mark(id);
813        }
814    }
815
816    fn update_manifolds(&mut self)
817    where
818        S::Vector: VectorOps,
819        S::Point: Copy + std::ops::Sub<Output = S::Vector>,
820    {
821        let mut pairs = std::mem::take(&mut self.pair_order);
822        Self::fill_broadphase(
823            &self.bodies,
824            &self.geometry,
825            &self.space,
826            &mut self.broadphase_intervals,
827            &mut self.broadphase_active,
828            &mut pairs,
829            &mut self.counters,
830        );
831        let mut touched = std::mem::take(&mut self.touched_pairs);
832        touched.clear();
833        let mut pending = std::mem::take(&mut self.pending_contacts);
834        pending.clear();
835
836        for &key in &pairs {
837            let (i, j) = self.dense_pair(key);
838            let (a, b) = split_two_mut(self.bodies.dense_mut(), i, j);
839            // Refresh anchors before the narrowphase merges contacts.
840            if let Some(manifold) = self.manifolds.get_mut(&key) {
841                manifold.refresh(&self.space, a, b);
842            }
843            let Some(contact) = self.narrowphase.test(a, b, &self.geometry, &self.space) else {
844                continue;
845            };
846            if !self.manifolds.contains_key(&key)
847                && a.is_sleeping() != b.is_sleeping()
848                && !a.is_static()
849                && !b.is_static()
850            {
851                a.wake();
852                b.wake();
853            }
854            pending.push((key, contact));
855        }
856
857        self.field_manifolds(&mut pending);
858
859        touched.extend(pending.iter().map(|(key, _)| *key));
860        touched.sort_unstable();
861        touched.dedup();
862        self.counters.contacts = pending.len() as u32;
863        self.recycle_untouched_manifolds(&touched);
864        for &(key, contact) in &pending {
865            self.ensure_manifold(key, contact.restitution);
866            let Self {
867                bodies,
868                manifolds,
869                space,
870                ..
871            } = self;
872            let i = bodies
873                .dense_index(key.0)
874                .unwrap_or_else(|| panic!("{STALE_MANIFOLD_BODY}"));
875            let j = bodies
876                .dense_index(key.1)
877                .unwrap_or_else(|| panic!("{STALE_MANIFOLD_BODY}"));
878            let (a, b) = split_two_mut(bodies.dense_mut(), i, j);
879            manifolds
880                .get_mut(&key)
881                .unwrap_or_else(|| panic!("{STALE_CONSTRAINT_KEY}"))
882                .add_or_update(space, a, b, contact);
883        }
884        self.touched_pairs = touched;
885        self.pending_contacts = pending;
886        self.pair_order = pairs;
887    }
888
889    fn ensure_manifold(&mut self, key: PairKey, restitution: f32)
890    where
891        S::Vector: VectorOps,
892    {
893        if self.manifolds.contains_key(&key) {
894            return;
895        }
896        let manifold = if let Some(mut manifold) = self.manifold_pool.pop() {
897            manifold.reset(key.0, key.1, restitution);
898            manifold
899        } else {
900            let required = self.manifolds.len() + 1;
901            if self.manifold_pool.capacity() < required {
902                self.manifold_pool
903                    .reserve_exact(required - self.manifold_pool.len());
904            }
905            Manifold::new(key.0, key.1, restitution)
906        };
907        self.manifolds.insert(key, manifold);
908    }
909
910    fn recycle_untouched_manifolds(&mut self, touched: &[PairKey]) {
911        for &key in &self.manifold_order {
912            if touched.binary_search(&key).is_err() {
913                if let Some(manifold) = self.manifolds.remove(&key) {
914                    self.manifold_pool.push(manifold);
915                }
916            }
917        }
918        self.manifold_order.clear();
919        self.manifold_order.extend_from_slice(touched);
920    }
921
922    fn wake_approaching_contacts(&mut self)
923    where
924        S::Vector: VectorOps,
925    {
926        for key in &self.manifold_order {
927            let manifold = &self.manifolds[key];
928            let (Some(a), Some(b)) = (
929                self.bodies.get(manifold.body_a),
930                self.bodies.get(manifold.body_b),
931            ) else {
932                continue;
933            };
934            if a.is_static() || b.is_static() || a.is_sleeping() == b.is_sleeping() {
935                continue;
936            }
937            let approaching = manifold.points.iter().any(|point| {
938                let relative = self.space.velocity_at_point(b, point.world_point)
939                    - self.space.velocity_at_point(a, point.world_point);
940                VectorOps::dot(relative, point.normal) < -WAKE_APPROACH_SPEED
941            });
942            if approaching {
943                let id = if a.is_sleeping() {
944                    manifold.body_a
945                } else {
946                    manifold.body_b
947                };
948                if let Some(body) = self.bodies.get_mut(id) {
949                    body.wake();
950                }
951            }
952        }
953    }
954
955    fn field_manifolds(&mut self, pending: &mut Vec<(PairKey, Contact<S>)>)
956    where
957        S::Vector: VectorOps,
958        S::Point: Copy + std::ops::Sub<Output = S::Vector>,
959    {
960        for index in 0..self.field_bindings.len() {
961            let (body, field) = self.field_bindings[index];
962            let Some(entry) = self.fields.get(field.0 as usize) else {
963                continue;
964            };
965            let anchor = entry.anchor;
966            let (Some(row), Some(_)) = (self.bodies.get(body), self.bodies.get(anchor)) else {
967                continue;
968            };
969            let Ok(found) =
970                self.field_narrowphase
971                    .test(row, &self.geometry, entry.field.as_ref(), &self.space)
972            else {
973                continue;
974            };
975            if !found.separation.is_finite() || !found.error.is_finite() || found.error < 0.0 {
976                continue;
977            }
978            let certified_separation = (found.separation as f64 + found.error as f64).next_up();
979            if certified_separation >= 0.0 || -certified_separation > f32::MAX as f64 {
980                continue;
981            }
982            let mut penetration = (-certified_separation) as f32;
983            if penetration as f64 > -certified_separation {
984                penetration = penetration.next_down();
985            }
986            let key = canonical_pair(body, anchor);
987            let normal = if key.0 == anchor {
988                found.normal
989            } else {
990                -found.normal
991            };
992            let contact = Contact {
993                normal,
994                point: found.witness,
995                penetration,
996                restitution: (self.bodies[body].restitution + self.bodies[anchor].restitution)
997                    * 0.5,
998            };
999            let (i, j) = self.dense_pair(key);
1000            let (a, b) = split_two_mut(self.bodies.dense_mut(), i, j);
1001            if let Some(manifold) = self.manifolds.get_mut(&key) {
1002                manifold.refresh(&self.space, a, b);
1003            }
1004            pending.push((key, contact));
1005        }
1006    }
1007
1008    // Manifold membership must stay fixed until the solve ends.
1009    fn collect_constraints(&mut self) {
1010        let mut parent = std::mem::take(&mut self.island_parent);
1011        let mut labels = std::mem::take(&mut self.island_labels);
1012        Self::fill_islands(
1013            &self.bodies,
1014            self.manifold_order.iter().copied(),
1015            &mut parent,
1016            &mut labels,
1017        );
1018        let mut units = std::mem::take(&mut self.constraints);
1019        units.clear();
1020        units.extend(self.manifold_order.iter().map(|&key| {
1021            let dense = self.dense_pair(key);
1022            ConstraintUnit {
1023                island: constraint_island(&self.bodies, &labels, dense),
1024                key,
1025                dense,
1026            }
1027        }));
1028        units.sort_unstable_by_key(|unit| (unit.island, unit.key));
1029        self.island_parent = parent;
1030        self.island_labels = labels;
1031        self.constraints = units;
1032    }
1033
1034    // Restitution uses velocities from before the warm-start impulse.
1035    fn prepare_solve(&mut self)
1036    where
1037        S::Vector: VectorOps,
1038    {
1039        for unit in &self.constraints {
1040            debug_assert_eq!(unit.dense, self.dense_pair(unit.key));
1041            let (i, j) = unit.dense;
1042            let manifold = self
1043                .manifolds
1044                .get_mut(&unit.key)
1045                .unwrap_or_else(|| panic!("{STALE_CONSTRAINT_KEY}"));
1046            let (a, b) = split_two_mut(self.bodies.dense_mut(), i, j);
1047            for cp in &mut manifold.points {
1048                let v_rel = self.space.velocity_at_point(b, cp.world_point)
1049                    - self.space.velocity_at_point(a, cp.world_point);
1050                let v_n = VectorOps::dot(v_rel, cp.normal);
1051
1052                cp.velocity_bias = if v_n < -RESTITUTION_THRESHOLD {
1053                    manifold.restitution * v_n
1054                } else {
1055                    0.0
1056                };
1057
1058                cp.tangent_impulse = 0.0;
1059                cp.tangent_dir = VectorOps::zero();
1060            }
1061        }
1062    }
1063
1064    fn warm_start(&mut self)
1065    where
1066        S::Vector: VectorOps,
1067    {
1068        for unit in &self.constraints {
1069            debug_assert_eq!(unit.dense, self.dense_pair(unit.key));
1070            let (i, j) = unit.dense;
1071            let manifold = self
1072                .manifolds
1073                .get(&unit.key)
1074                .unwrap_or_else(|| panic!("{STALE_CONSTRAINT_KEY}"));
1075            let (a, b) = split_two_mut(self.bodies.dense_mut(), i, j);
1076            for cp in &manifold.points {
1077                if cp.normal_impulse > 0.0 {
1078                    self.space.apply_contact_impulse(
1079                        a,
1080                        b,
1081                        cp.world_point,
1082                        cp.normal,
1083                        cp.normal_impulse,
1084                    );
1085                }
1086            }
1087        }
1088    }
1089
1090    fn solve(&mut self, dt: f32)
1091    where
1092        S::Vector: VectorOps,
1093    {
1094        self.gather_islands();
1095        let Self {
1096            space,
1097            scratch,
1098            scratch_islands,
1099            pgs_iters,
1100            ..
1101        } = self;
1102        let iterations = *pgs_iters;
1103        let space = &*space;
1104        let islands = &mut scratch[..*scratch_islands];
1105        let workers = islands.len() / ISLANDS_PER_SOLVE_WORKER;
1106        if workers < 2 {
1107            for island in islands.iter_mut() {
1108                solve_island(space, iterations, dt, island);
1109            }
1110        } else {
1111            let chunk = islands.len().div_ceil(workers);
1112            par::for_each_chunk(islands, chunk, |chunk| {
1113                for island in chunk {
1114                    solve_island(space, iterations, dt, island);
1115                }
1116            });
1117        }
1118        self.scatter_islands();
1119    }
1120
1121    fn gather_islands(&mut self) {
1122        self.scratch_local.clear();
1123        self.scratch_local
1124            .resize(self.bodies.len(), SCATTERED_NOWHERE);
1125        self.scratch_islands = 0;
1126        let mut current = None;
1127        for unit in &self.constraints {
1128            if current != Some(unit.island) {
1129                current = Some(unit.island);
1130                if self.scratch.len() == self.scratch_islands {
1131                    self.scratch.push(IslandSolve::default());
1132                }
1133                let island = &mut self.scratch[self.scratch_islands];
1134                island.bodies.clear();
1135                island.dense.clear();
1136                island.units.clear();
1137                island.points.clear();
1138                self.scratch_islands += 1;
1139            }
1140            let island = &mut self.scratch[self.scratch_islands - 1];
1141            let (i, j) = unit.dense;
1142            let a = gather_body(island, &mut self.scratch_local, &self.bodies, i);
1143            let b = gather_body(island, &mut self.scratch_local, &self.bodies, j);
1144            let manifold = self
1145                .manifolds
1146                .get(&unit.key)
1147                .unwrap_or_else(|| panic!("{STALE_CONSTRAINT_KEY}"));
1148            let first = island.points.len() as u32;
1149            island.points.extend_from_slice(&manifold.points);
1150            island.units.push(ScratchUnit {
1151                key: unit.key,
1152                a,
1153                b,
1154                first,
1155                count: manifold.points.len() as u32,
1156            });
1157        }
1158    }
1159
1160    fn scatter_islands(&mut self) {
1161        let mut residual = 0.0_f32;
1162        for island in &self.scratch[..self.scratch_islands] {
1163            residual = residual.max(island.residual);
1164            for (local, &dense) in island.dense.iter().enumerate() {
1165                if dense != SCATTERED_NOWHERE {
1166                    self.bodies[dense as usize] = island.bodies[local];
1167                }
1168            }
1169            for unit in &island.units {
1170                let manifold = self
1171                    .manifolds
1172                    .get_mut(&unit.key)
1173                    .unwrap_or_else(|| panic!("{STALE_CONSTRAINT_KEY}"));
1174                let first = unit.first as usize;
1175                manifold
1176                    .points
1177                    .copy_from_slice(&island.points[first..first + unit.count as usize]);
1178            }
1179        }
1180        self.report = SolveReport {
1181            residual,
1182            converged: residual <= self.solver_tolerance,
1183        };
1184    }
1185
1186    /// Replaces `pairs` with the sorted candidates: bounding-ball overlaps under a certified bound, every masked non-static pair otherwise.
1187    pub fn broadphase_into(&mut self, pairs: &mut Vec<PairKey>) {
1188        self.counters = StepCounters::default();
1189        Self::fill_broadphase(
1190            &self.bodies,
1191            &self.geometry,
1192            &self.space,
1193            &mut self.broadphase_intervals,
1194            &mut self.broadphase_active,
1195            pairs,
1196            &mut self.counters,
1197        );
1198    }
1199
1200    // Cohen, Lin, Manocha, Ponamgi, I-COLLIDE, 1995, sec. 3; sweep radial distances.
1201    fn fill_broadphase(
1202        bodies: &BodyArena<S>,
1203        geometry: &GeometryStore,
1204        space: &S,
1205        intervals: &mut Vec<RadialInterval>,
1206        active: &mut Vec<u32>,
1207        pairs: &mut Vec<PairKey>,
1208        counters: &mut StepCounters,
1209    ) {
1210        pairs.clear();
1211        intervals.clear();
1212        active.clear();
1213        let n = bodies.len();
1214        if n < 2 {
1215            return;
1216        }
1217        let certified = space.broadphase_bound() == BroadphaseBound::Certified;
1218
1219        let anchor = (1..n).fold(0, |lowest, dense| {
1220            if bodies.id_at(dense) < bodies.id_at(lowest) {
1221                dense
1222            } else {
1223                lowest
1224            }
1225        });
1226        let origin = bodies[anchor].position;
1227
1228        for dense in 0..n {
1229            let body = &bodies[dense];
1230            let radius = bounding_radius(geometry.get(body.collider()));
1231            let (lo, hi) = if certified {
1232                counters.distance_evals += 1;
1233                let d = space.distance(origin, body.position);
1234                let slack = d * BROADPHASE_TRIANGLE_SLACK;
1235                (d - radius - slack, d + radius + slack)
1236            } else {
1237                (f32::NEG_INFINITY, f32::INFINITY)
1238            };
1239            intervals.push(RadialInterval {
1240                lo,
1241                hi,
1242                radius,
1243                dense: dense as u32,
1244                id: bodies.id_at(dense),
1245                dynamic: body.inv_mass() != 0.0,
1246                group: body.collision_group,
1247                mask: body.collision_mask,
1248            });
1249        }
1250
1251        intervals.sort_unstable_by(|a, b| a.lo.total_cmp(&b.lo).then(a.id.cmp(&b.id)));
1252
1253        for i in 0..n {
1254            let entry = intervals[i];
1255
1256            let mut visits = counters.index_visits;
1257            active.retain(|&open| {
1258                visits += 1;
1259                intervals[open as usize].hi >= entry.lo
1260            });
1261            counters.index_visits = visits;
1262            for &open in active.iter() {
1263                counters.index_visits += 1;
1264                let other = intervals[open as usize];
1265                if !entry.dynamic && !other.dynamic {
1266                    continue;
1267                }
1268
1269                if entry.group & other.mask == 0 || other.group & entry.mask == 0 {
1270                    continue;
1271                }
1272                if certified {
1273                    counters.distance_evals += 1;
1274                    let gap = space.distance(
1275                        bodies[other.dense as usize].position,
1276                        bodies[entry.dense as usize].position,
1277                    );
1278                    if gap > other.radius + entry.radius {
1279                        continue;
1280                    }
1281                }
1282                pairs.push(canonical_pair(other.id, entry.id));
1283            }
1284            active.push(i as u32);
1285        }
1286
1287        pairs.sort_unstable();
1288        counters.candidates = pairs.len() as u32;
1289    }
1290
1291    /// Hashes contact keys, point counts, and normal impulses in key order.
1292    pub fn hash_contacts(&self, hash: &mut StateHash) {
1293        for (key, manifold) in self.manifolds() {
1294            for id in [key.0, key.1] {
1295                hash.write_u32(id.slot());
1296                hash.write_u32(id.generation());
1297            }
1298            hash.write_u32(manifold.points.len() as u32);
1299            for point in &manifold.points {
1300                hash.write_f32(point.normal_impulse);
1301            }
1302        }
1303    }
1304
1305    /// Hashes bodies by handle, then contacts; `sample_body` must emit fixed-width records.
1306    pub fn state_hash(&self, sample_body: impl Fn(&RigidBody<S>, &mut Vec<u32>)) -> u64 {
1307        let mut order: Vec<BodyId> = (0..self.bodies.len())
1308            .map(|dense| self.bodies.id_at(dense))
1309            .collect();
1310        order.sort_unstable();
1311
1312        let mut words = Vec::new();
1313        for id in order {
1314            sample_body(&self.bodies[id], &mut words);
1315        }
1316        let mut hash = StateHash::new();
1317        hash.write_u32s(&words);
1318        self.hash_contacts(&mut hash);
1319        hash.finish()
1320    }
1321
1322    /// Returns islands by id; stale manifold handles panic after a bare [`BodyArena::despawn`].
1323    pub fn islands(&self) -> Vec<Island> {
1324        let mut parent = Vec::new();
1325        let mut labels = Vec::new();
1326        Self::fill_islands(
1327            &self.bodies,
1328            self.manifold_order.iter().copied(),
1329            &mut parent,
1330            &mut labels,
1331        );
1332
1333        let mut by_id: BTreeMap<BodyId, Island> = BTreeMap::new();
1334        for &key in &self.manifold_order {
1335            if self.bodies[key.0].inv_mass() == 0.0 && self.bodies[key.1].inv_mass() == 0.0 {
1336                continue;
1337            }
1338            let id = constraint_island(&self.bodies, &labels, self.dense_pair(key));
1339            let island = by_id.entry(id).or_insert_with(|| Island {
1340                id,
1341                bodies: Vec::new(),
1342                constraints: Vec::new(),
1343            });
1344            island.constraints.push(key);
1345            for member in [key.0, key.1] {
1346                if self.bodies[member].inv_mass() != 0.0 {
1347                    island.bodies.push(member);
1348                }
1349            }
1350        }
1351
1352        let mut islands: Vec<Island> = by_id.into_values().collect();
1353        for island in &mut islands {
1354            island.bodies.sort_unstable();
1355            island.bodies.dedup();
1356        }
1357        islands
1358    }
1359
1360    fn fill_islands(
1361        bodies: &BodyArena<S>,
1362        touched: impl Iterator<Item = PairKey>,
1363        parent: &mut Vec<u32>,
1364        labels: &mut Vec<BodyId>,
1365    ) {
1366        let n = bodies.len();
1367        parent.clear();
1368        parent.extend(0..n as u32);
1369        labels.clear();
1370        labels.extend((0..n).map(|dense| bodies.id_at(dense)));
1371
1372        for key in touched {
1373            let (i, j) = (
1374                bodies
1375                    .dense_index(key.0)
1376                    .unwrap_or_else(|| panic!("{STALE_MANIFOLD_BODY}")),
1377                bodies
1378                    .dense_index(key.1)
1379                    .unwrap_or_else(|| panic!("{STALE_MANIFOLD_BODY}")),
1380            );
1381            if bodies[i].inv_mass() == 0.0 || bodies[j].inv_mass() == 0.0 {
1382                continue;
1383            }
1384            let (a, b) = (find_root(parent, i), find_root(parent, j));
1385            if a != b {
1386                parent[a.max(b)] = a.min(b) as u32;
1387            }
1388        }
1389
1390        for dense in 0..n {
1391            let root = find_root(parent, dense);
1392            labels[root] = labels[root].min(bodies.id_at(dense));
1393        }
1394        for dense in 0..n {
1395            let label = labels[find_root(parent, dense)];
1396            labels[dense] = label;
1397        }
1398    }
1399
1400    fn dense_pair(&self, key: PairKey) -> (usize, usize) {
1401        (
1402            self.bodies
1403                .dense_index(key.0)
1404                .unwrap_or_else(|| panic!("{STALE_MANIFOLD_BODY}")),
1405            self.bodies
1406                .dense_index(key.1)
1407                .unwrap_or_else(|| panic!("{STALE_MANIFOLD_BODY}")),
1408        )
1409    }
1410}
1411
1412// Tarjan and van Leeuwen, JACM 31(2), 1984, sec. 2.
1413fn find_root(parent: &mut [u32], mut dense: usize) -> usize {
1414    while parent[dense] as usize != dense {
1415        parent[dense] = parent[parent[dense] as usize];
1416        dense = parent[dense] as usize;
1417    }
1418    dense
1419}
1420
1421fn constraint_island<S: PhysicsSpace>(
1422    bodies: &BodyArena<S>,
1423    labels: &[BodyId],
1424    dense: (usize, usize),
1425) -> BodyId {
1426    let (i, j) = dense;
1427    debug_assert!(
1428        bodies[i].inv_mass() != 0.0 || bodies[j].inv_mass() != 0.0,
1429        "a contact between two static bodies has no island to solve in",
1430    );
1431    if bodies[i].inv_mass() != 0.0 {
1432        labels[i]
1433    } else {
1434        labels[j]
1435    }
1436}
1437
1438// Handle order can differ from dense storage order.
1439fn split_two_mut<T>(slice: &mut [T], i: usize, j: usize) -> (&mut T, &mut T) {
1440    debug_assert_ne!(i, j, "split_two_mut requires distinct indices");
1441    if i < j {
1442        let (left, right) = slice.split_at_mut(j);
1443        (&mut left[i], &mut right[0])
1444    } else {
1445        let (left, right) = slice.split_at_mut(i);
1446        (&mut right[0], &mut left[j])
1447    }
1448}
1449
1450fn gather_body<S: PhysicsSpace>(
1451    island: &mut IslandSolve<S>,
1452    local: &mut [u32],
1453    bodies: &BodyArena<S>,
1454    dense: usize,
1455) -> u32 {
1456    if bodies[dense].inv_mass() == 0.0 {
1457        island.bodies.push(bodies[dense]);
1458        island.dense.push(SCATTERED_NOWHERE);
1459        return island.bodies.len() as u32 - 1;
1460    }
1461    if local[dense] == SCATTERED_NOWHERE {
1462        local[dense] = island.bodies.len() as u32;
1463        island.bodies.push(bodies[dense]);
1464        island.dense.push(dense as u32);
1465    }
1466    local[dense]
1467}
1468
1469// Catto 2005, "Iterative Dynamics with Temporal Coherence", accumulated impulses with warm start.
1470fn solve_island<S>(space: &S, iterations: usize, dt: f32, island: &mut IslandSolve<S>)
1471where
1472    S: PhysicsSpace,
1473    S::Vector: VectorOps,
1474{
1475    let IslandSolve {
1476        bodies,
1477        units,
1478        points,
1479        residual,
1480        push,
1481        saved,
1482        ..
1483    } = island;
1484    *residual = 0.0;
1485    for _ in 0..iterations {
1486        *residual = 0.0;
1487        for unit in units.iter() {
1488            let (a, b) = split_two_mut(bodies, unit.a as usize, unit.b as usize);
1489            let first = unit.first as usize;
1490            for cp in &mut points[first..first + unit.count as usize] {
1491                *residual = residual.max(solve_normal_then_tangent(space, a, b, cp));
1492            }
1493        }
1494    }
1495    if dt <= 0.0 {
1496        return;
1497    }
1498    // Split impulse: a dropped pseudo velocity translates bodies apart, adding no energy or creep.
1499    saved.clear();
1500    saved.extend_from_slice(bodies);
1501    for body in bodies.iter_mut() {
1502        body.velocity = VectorOps::zero();
1503        body.angular_velocity = S::AngVel::zero();
1504    }
1505    push.clear();
1506    push.resize(points.len(), 0.0);
1507    for _ in 0..iterations {
1508        for unit in units.iter() {
1509            let (a, b) = split_two_mut(bodies, unit.a as usize, unit.b as usize);
1510            let range = unit.first as usize..(unit.first + unit.count) as usize;
1511            for (cp, accumulated) in points[range.clone()].iter().zip(&mut push[range]) {
1512                let depth = (cp.penetration - PENETRATION_SLOP).max(0.0);
1513                let target = (depth * BAUMGARTE_BETA).min(MAX_LINEAR_CORRECTION) / dt;
1514                let k_n = a.inv_mass() + b.inv_mass();
1515                if k_n <= 0.0 {
1516                    continue;
1517                }
1518                let v_rel = b.velocity - a.velocity;
1519                let next =
1520                    (*accumulated + (target - VectorOps::dot(v_rel, cp.normal)) / k_n).max(0.0);
1521                let impulse = next - *accumulated;
1522                *accumulated = next;
1523                if impulse != 0.0 {
1524                    a.velocity = a.velocity - cp.normal * (impulse * a.inv_mass());
1525                    b.velocity = b.velocity + cp.normal * (impulse * b.inv_mass());
1526                }
1527            }
1528        }
1529    }
1530    for (body, before) in bodies.iter_mut().zip(saved.iter()) {
1531        let from = body.position;
1532        integrate_body(space, body, dt);
1533        body.velocity = space.parallel_transport(from, body.position, before.velocity);
1534        body.angular_velocity = before.angular_velocity;
1535    }
1536}
1537
1538fn solve_normal_then_tangent<S>(
1539    space: &S,
1540    a: &mut RigidBody<S>,
1541    b: &mut RigidBody<S>,
1542    cp: &mut ContactPoint<S>,
1543) -> f32
1544where
1545    S: PhysicsSpace,
1546    S::Vector: VectorOps,
1547{
1548    debug_assert!(
1549        VectorOps::is_finite(cp.normal) && cp.penetration.is_finite(),
1550        "non-finite contact in solve_normal_then_tangent",
1551    );
1552
1553    let v_rel_n_vec =
1554        space.velocity_at_point(b, cp.world_point) - space.velocity_at_point(a, cp.world_point);
1555    let v_n = VectorOps::dot(v_rel_n_vec, cp.normal);
1556    let k_n = space.effective_mass_inv(a, b, cp.world_point, cp.normal);
1557
1558    let mut change = 0.0;
1559    if k_n > 0.0 {
1560        let dj = -(v_n + cp.velocity_bias) / k_n;
1561        let new_acc = (cp.normal_impulse + dj).max(0.0);
1562        let actual = new_acc - cp.normal_impulse;
1563        cp.normal_impulse = new_acc;
1564        change = actual.abs();
1565        if actual.abs() > 0.0 {
1566            space.apply_contact_impulse(a, b, cp.world_point, cp.normal, actual);
1567        }
1568    }
1569
1570    let previous = cp.tangent_dir * cp.tangent_impulse;
1571    let v_rel =
1572        space.velocity_at_point(b, cp.world_point) - space.velocity_at_point(a, cp.world_point);
1573    let slip = v_rel - cp.normal * VectorOps::dot(v_rel, cp.normal);
1574    let speed = VectorOps::length(slip);
1575    let mut impulse = previous;
1576    if speed > 1e-8 {
1577        let direction = slip * (1.0 / speed);
1578        let inverse_mass = space.effective_mass_inv(a, b, cp.world_point, direction);
1579        if inverse_mass > 0.0 {
1580            impulse = impulse + slip * (1.0 / inverse_mass);
1581        }
1582    }
1583    let magnitude = VectorOps::length(impulse);
1584    let limit = cp.normal_impulse * FRICTION_COEFF;
1585    if magnitude > limit {
1586        impulse = impulse * (limit / magnitude);
1587    }
1588    cp.tangent_impulse = VectorOps::length(impulse);
1589    cp.tangent_dir = if cp.tangent_impulse > 0.0 {
1590        impulse * (1.0 / cp.tangent_impulse)
1591    } else {
1592        VectorOps::zero()
1593    };
1594
1595    let delta = impulse - previous;
1596    let delta_magnitude = VectorOps::length(delta);
1597    if delta_magnitude > 0.0 {
1598        space.apply_contact_impulse(
1599            a,
1600            b,
1601            cp.world_point,
1602            delta * (1.0 / delta_magnitude),
1603            -delta_magnitude,
1604        );
1605    }
1606    change
1607}
1608
1609#[cfg(all(test, feature = "r2", feature = "r3", feature = "r4"))]
1610mod tests {
1611    use std::alloc::System;
1612    use std::collections::BTreeSet;
1613
1614    use super::*;
1615    use crate::determinism_fixture::{
1616        multi_island_world, record_flick_chamber_tape, replay_flick_chamber_tape, sample_body_r3,
1617        MULTI_ISLAND_DT, MULTI_ISLAND_STEPS, REPLAY_SEED,
1618    };
1619    use crate::euclidean_r3::{
1620        box_body, halfspace_body_r3, register_default_narrowphase, sphere_body_r3,
1621    };
1622    use glam::{Quat, Vec3};
1623    use loam_math::{Bivector3, EuclideanR3, Space};
1624    use loam_time::alloc::{bytes_allocated_by, CountingAllocator};
1625    use loam_time::Tape;
1626
1627    const PERMUTATION_SEEDS: [u64; 4] = [1, 0x9e37_79b9_7f4a_7c15, 0xdead_beef_cafe_f00d, 424_242];
1628
1629    fn constraint_order<S: PhysicsSpace>(world: &World<S>) -> Vec<PairKey> {
1630        world.constraints.iter().map(|unit| unit.key).collect()
1631    }
1632
1633    #[global_allocator]
1634    static COUNTING_ALLOCATOR: CountingAllocator<System> = CountingAllocator::new(System);
1635
1636    #[test]
1637    fn a_group_outside_the_others_mask_never_reaches_the_narrowphase() {
1638        let mut world = World::new(EuclideanR3);
1639        register_default_narrowphase(&mut world.narrowphase);
1640        let a = world.push_body(sphere_body_r3(Vec3::ZERO, Vec3::ZERO, 1.0, 1.0).unwrap());
1641        let b = world
1642            .push_body(sphere_body_r3(Vec3::new(0.5, 0.0, 0.0), Vec3::ZERO, 1.0, 1.0).unwrap());
1643        let mut pairs = Vec::new();
1644        world.broadphase_into(&mut pairs);
1645        assert_eq!(pairs.len(), 1, "the pair does not overlap to begin with");
1646
1647        world.bodies[a].collision_group = 0b01;
1648        world.bodies[a].collision_mask = 0b01;
1649        world.bodies[b].collision_group = 0b10;
1650        world.bodies[b].collision_mask = 0b10;
1651        world.broadphase_into(&mut pairs);
1652        assert!(
1653            pairs.is_empty(),
1654            "a filtered pair still reached the narrowphase"
1655        );
1656
1657        world.bodies[b].collision_mask = 0b11;
1658        world.broadphase_into(&mut pairs);
1659        assert!(
1660            pairs.is_empty(),
1661            "a one-sided mask edit produced a pair that collides in one direction"
1662        );
1663
1664        world.bodies[a].collision_mask = 0b11;
1665        world.broadphase_into(&mut pairs);
1666        assert_eq!(pairs.len(), 1, "agreement did not restore the pair");
1667    }
1668
1669    #[test]
1670    fn a_tape_replays_the_same_after_a_round_trip_through_its_byte_format() {
1671        let recorded = record_flick_chamber_tape(REPLAY_SEED);
1672        let decoded = Tape::decode(&recorded.encode()).expect("own encoding decodes");
1673        assert_eq!(decoded, recorded);
1674        assert_eq!(replay_flick_chamber_tape(&decoded), decoded.checkpoints());
1675    }
1676
1677    #[test]
1678    fn a_flipped_input_word_moves_the_replayed_state_hash() {
1679        let recorded = record_flick_chamber_tape(REPLAY_SEED);
1680        let throw = (0..recorded.ticks())
1681            .find(|&tick| recorded.input(tick).expect("inside the tape")[0] != u32::MAX)
1682            .expect("the scripted stream throws at least once");
1683
1684        let mut tampered = Tape::new(
1685            recorded.tick_hz(),
1686            recorded.seed(),
1687            recorded.words_per_tick(),
1688        );
1689        for tick in 0..recorded.ticks() {
1690            let mut frame: Vec<u32> = recorded.input(tick).expect("inside the tape").to_vec();
1691            if tick == throw {
1692                frame[2] ^= 1;
1693            }
1694            tampered.push_tick(&frame);
1695        }
1696
1697        assert_ne!(
1698            replay_flick_chamber_tape(&tampered),
1699            recorded.checkpoints(),
1700            "one ulp of input made no difference, so the tape is not driving \
1701             the simulation"
1702        );
1703    }
1704
1705    #[test]
1706    fn state_hash_is_invariant_under_arena_compaction() {
1707        let radii = [0.3_f32, 0.4, 0.5];
1708        let body = |i: usize| {
1709            sphere_body_r3(
1710                Vec3::new(i as f32, 2.0 * i as f32, 0.0),
1711                Vec3::new(0.0, -1.0, 0.5 * i as f32),
1712                radii[i],
1713                1.0 + i as f32,
1714            )
1715            .unwrap()
1716        };
1717
1718        let mut direct = World::new(EuclideanR3);
1719        for i in 0..3 {
1720            direct.push_body(body(i));
1721        }
1722
1723        let mut compacted = World::new(EuclideanR3);
1724        compacted.push_body(body(0));
1725        let doomed = compacted.push_body(sphere_body_r3(Vec3::ZERO, Vec3::ZERO, 9.0, 4.0).unwrap());
1726        compacted.push_body(body(1));
1727        compacted.push_body(body(2));
1728        assert!(compacted.despawn_body(doomed).is_ok());
1729
1730        let dense_order = |world: &World<EuclideanR3>| {
1731            world
1732                .bodies
1733                .iter()
1734                .map(|body| body.mass().to_bits())
1735                .collect::<Vec<_>>()
1736        };
1737        assert_ne!(
1738            dense_order(&direct),
1739            dense_order(&compacted),
1740            "the two worlds must differ in storage order or the pin is vacuous"
1741        );
1742        assert_eq!(
1743            direct.state_hash(sample_body_r3),
1744            compacted.state_hash(sample_body_r3),
1745        );
1746    }
1747
1748    #[test]
1749    fn state_hash_covers_carried_contact_impulses() {
1750        let mut world = multi_island_world();
1751        for _ in 0..MULTI_ISLAND_STEPS {
1752            world.step(MULTI_ISLAND_DT).unwrap();
1753        }
1754        let settled = world.state_hash(sample_body_r3);
1755
1756        let point = world
1757            .manifolds
1758            .values_mut()
1759            .flat_map(|manifold| manifold.points.iter_mut())
1760            .next()
1761            .expect("the fixture rests in contact");
1762        point.normal_impulse = f32::from_bits(point.normal_impulse.to_bits() ^ 1);
1763
1764        assert_ne!(
1765            settled,
1766            world.state_hash(sample_body_r3),
1767            "one ulp of warm-start impulse left the state hash unmoved"
1768        );
1769    }
1770
1771    const SPHERE_RADIUS: f32 = 0.5;
1772    const GRAVITY_Y: f32 = -9.8;
1773
1774    fn settled_sphere_stack(dt: f32, settle_steps: usize) -> World<EuclideanR3> {
1775        let mut world = World::new(EuclideanR3);
1776        register_default_narrowphase(&mut world.narrowphase);
1777        world
1778            .set_gravity(Some(Vec3::new(0.0, GRAVITY_Y, 0.0)))
1779            .unwrap();
1780
1781        for level in 0..3 {
1782            let y = SPHERE_RADIUS + level as f32 * 2.0 * SPHERE_RADIUS;
1783            let id = world.push_body(
1784                sphere_body_r3(Vec3::new(0.0, y, 0.0), Vec3::ZERO, SPHERE_RADIUS, 1.0).unwrap(),
1785            );
1786            world.bodies[id].restitution = 0.0;
1787        }
1788        let floor = world.push_body(halfspace_body_r3(Vec3::Y, 0.0).unwrap());
1789        world.bodies[floor].restitution = 0.0;
1790
1791        for _ in 0..settle_steps {
1792            world.step(dt).unwrap();
1793        }
1794        world
1795    }
1796
1797    #[test]
1798    fn a_one_iteration_budget_separates_a_resting_contact_from_a_fast_impact() {
1799        const DT: f32 = 1.0 / 240.0;
1800        let mut resting = settled_sphere_stack(DT, 240);
1801        resting.set_solver_iterations(1);
1802        resting.step(DT).unwrap();
1803        let settled = resting.solve_report();
1804        assert!(
1805            resting.counters().contacts > 0,
1806            "the resting stack lost its contacts"
1807        );
1808        assert!(settled.converged, "resting residual {}", settled.residual);
1809
1810        let mut struck = settled_sphere_stack(DT, 240);
1811        struck.set_solver_iterations(1);
1812        let top = struck.bodies.id_at(2);
1813        struck
1814            .set_velocity(top, Vec3::new(0.0, -20.0, 0.0), Bivector3::ZERO)
1815            .unwrap();
1816        struck.step(DT).unwrap();
1817        let impact = struck.solve_report();
1818        assert!(!impact.converged, "impact residual {}", impact.residual);
1819    }
1820
1821    mod solver_contracts {
1822        use glam::{Vec2, Vec4};
1823        use loam_math::{EuclideanR2, EuclideanR4};
1824
1825        use super::*;
1826        use crate::euclidean_r2::{sphere_body, static_wall};
1827        use crate::euclidean_r4::{halfspace4_body_r4, sphere_body_r4};
1828
1829        fn kinetic_energy<S: PhysicsSpace<Inertia = f32>>(body: &RigidBody<S>, omega: f32) -> f32
1830        where
1831            S::Vector: VectorOps,
1832        {
1833            0.5 * body.mass() * VectorOps::length_squared(body.velocity)
1834                + 0.5 * body.inertia * omega * omega
1835        }
1836
1837        const FLOOR_HALF: Vec2 = Vec2::new(50.0, 1.0);
1838
1839        fn floor_r2() -> BodyDef<EuclideanR2> {
1840            static_wall(Vec2::new(0.0, -FLOOR_HALF.y), FLOOR_HALF).unwrap()
1841        }
1842
1843        const REBOUND_APPROACH: f32 = 2.0;
1844        const REBOUND_DT: f32 = 1.0 / 1000.0;
1845        const REBOUND_GAP: f32 = 0.01;
1846
1847        fn assert_elastic_rebound_conserves_kinetic_energy<S>(
1848            mut world: World<S>,
1849            faller: BodyId,
1850            up: S::Vector,
1851            angular_speed: impl Fn(S::AngVel) -> f32,
1852        ) where
1853            S: PhysicsSpace<Inertia = f32>,
1854            S::Vector: VectorOps,
1855            S::Point: Copy + std::ops::Sub<Output = S::Vector>,
1856        {
1857            let energy_before = kinetic_energy(
1858                &world.bodies[faller],
1859                angular_speed(world.bodies[faller].angular_velocity),
1860            );
1861            let steps = (4.0 * REBOUND_GAP / (REBOUND_APPROACH * REBOUND_DT)).ceil() as usize;
1862            for _ in 0..steps {
1863                world.step(REBOUND_DT).unwrap();
1864            }
1865
1866            let body = &world.bodies[faller];
1867            let rebound = VectorOps::dot(body.velocity, up);
1868            assert!(rebound > 0.0, "body did not rebound: v·up = {rebound}");
1869            let spin = angular_speed(body.angular_velocity);
1870            assert!(spin < 1e-6, "central impact spun the body: |ω| = {spin}");
1871
1872            let energy_after = kinetic_energy(body, spin);
1873            let ratio = energy_after / energy_before;
1874            assert!(
1875                ratio <= 1.0 + 1e-4,
1876                "e = 1 impact added energy: {energy_before} -> {energy_after}"
1877            );
1878            assert!(
1879                ratio >= 1.0 - 1e-4,
1880                "e = 1 impact lost energy: {energy_before} -> {energy_after}"
1881            );
1882        }
1883
1884        #[test]
1885        fn perfectly_elastic_rebound_conserves_kinetic_energy_r2() {
1886            let mut world = World::new(EuclideanR2);
1887            crate::euclidean_r2::register_default_narrowphase(&mut world.narrowphase);
1888            let disk = world.push_body(
1889                sphere_body(
1890                    Vec2::new(0.0, SPHERE_RADIUS + REBOUND_GAP),
1891                    Vec2::new(0.0, -REBOUND_APPROACH),
1892                    SPHERE_RADIUS,
1893                    1.0,
1894                )
1895                .unwrap(),
1896            );
1897            let floor = world.push_body(floor_r2());
1898            world.bodies[disk].restitution = 1.0;
1899            world.bodies[floor].restitution = 1.0;
1900
1901            assert_elastic_rebound_conserves_kinetic_energy(world, disk, Vec2::Y, |omega| {
1902                omega.0.abs()
1903            });
1904        }
1905
1906        #[test]
1907        fn perfectly_elastic_rebound_conserves_kinetic_energy_r3() {
1908            let mut world = World::new(EuclideanR3);
1909            register_default_narrowphase(&mut world.narrowphase);
1910            let sphere = world.push_body(
1911                sphere_body_r3(
1912                    Vec3::new(0.0, SPHERE_RADIUS + REBOUND_GAP, 0.0),
1913                    Vec3::new(0.0, -REBOUND_APPROACH, 0.0),
1914                    SPHERE_RADIUS,
1915                    1.0,
1916                )
1917                .unwrap(),
1918            );
1919            let floor = world.push_body(halfspace_body_r3(Vec3::Y, 0.0).unwrap());
1920            world.bodies[sphere].restitution = 1.0;
1921            world.bodies[floor].restitution = 1.0;
1922
1923            assert_elastic_rebound_conserves_kinetic_energy(world, sphere, Vec3::Y, |omega| {
1924                omega.magnitude()
1925            });
1926        }
1927
1928        #[test]
1929        fn perfectly_elastic_rebound_conserves_kinetic_energy_r4() {
1930            let mut world = World::new(EuclideanR4);
1931            crate::euclidean_r4::register_default_narrowphase(&mut world.narrowphase);
1932            let sphere = world.push_body(
1933                sphere_body_r4(
1934                    Vec4::new(0.0, SPHERE_RADIUS + REBOUND_GAP, 0.0, 0.0),
1935                    Vec4::new(0.0, -REBOUND_APPROACH, 0.0, 0.0),
1936                    SPHERE_RADIUS,
1937                    1.0,
1938                )
1939                .unwrap(),
1940            );
1941            let floor = world.push_body(halfspace4_body_r4(Vec4::Y, 0.0).unwrap());
1942            world.bodies[sphere].restitution = 1.0;
1943            world.bodies[floor].restitution = 1.0;
1944
1945            assert_elastic_rebound_conserves_kinetic_energy(world, sphere, Vec4::Y, |omega| {
1946                omega.magnitude()
1947            });
1948        }
1949
1950        const SLIDE_SPEED: f32 = 5.0;
1951        const SLIDE_DT: f32 = 1.0 / 240.0;
1952        const SLIDE_STEPS: usize = 240;
1953
1954        fn assert_tangent_impulse_stays_inside_the_coulomb_cone<S>(
1955            mut world: World<S>,
1956            slider: BodyId,
1957            slide: S::Vector,
1958            up: S::Vector,
1959        ) where
1960            S: PhysicsSpace,
1961            S::Vector: VectorOps,
1962            S::Point: Copy + std::ops::Sub<Output = S::Vector>,
1963        {
1964            let mut cone_ever_binds = false;
1965            for _ in 0..SLIDE_STEPS {
1966                world.step(SLIDE_DT).unwrap();
1967                for (_, manifold) in world.manifolds() {
1968                    for cp in &manifold.points {
1969                        let cap = cp.normal_impulse * FRICTION_COEFF;
1970                        assert!(
1971                            cp.tangent_impulse <= cap + 1e-6,
1972                            "friction escaped the cone: jt = {}, μ·jn = {cap}",
1973                            cp.tangent_impulse
1974                        );
1975                        assert!(
1976                            cp.tangent_impulse >= 0.0,
1977                            "tangent accumulator went negative: {}",
1978                            cp.tangent_impulse
1979                        );
1980                        if cap > 1e-6 && cp.tangent_impulse >= cap - 1e-6 {
1981                            cone_ever_binds = true;
1982                        }
1983                    }
1984                }
1985            }
1986
1987            assert!(
1988                cone_ever_binds,
1989                "friction never saturated, so the clamp was never exercised"
1990            );
1991            let body = &world.bodies[slider];
1992            let along = VectorOps::dot(body.velocity, slide);
1993            assert!(
1994                along < SLIDE_SPEED,
1995                "friction did not brake the slide: v·slide = {along}"
1996            );
1997
1998            let contact = world.space.exp(body.position, up * -SPHERE_RADIUS);
1999            let angular_at_contact = world.space.velocity_at_point(body, contact) - body.velocity;
2000            let backspin = VectorOps::dot(angular_at_contact, slide);
2001            assert!(
2002                backspin < -1e-3,
2003                "friction spun the body away from rolling: contact-point \
2004                 angular velocity along the slide is {backspin}"
2005            );
2006        }
2007
2008        #[test]
2009        fn tangent_impulse_stays_inside_the_coulomb_cone_r2() {
2010            let mut world = World::new(EuclideanR2);
2011            crate::euclidean_r2::register_default_narrowphase(&mut world.narrowphase);
2012            world.set_gravity(Some(Vec2::new(0.0, GRAVITY_Y))).unwrap();
2013            let disk = world.push_body(
2014                sphere_body(
2015                    Vec2::new(0.0, SPHERE_RADIUS),
2016                    Vec2::new(SLIDE_SPEED, 0.0),
2017                    SPHERE_RADIUS,
2018                    1.0,
2019                )
2020                .unwrap(),
2021            );
2022            let floor = world.push_body(floor_r2());
2023            world.bodies[disk].restitution = 0.0;
2024            world.bodies[floor].restitution = 0.0;
2025
2026            assert_tangent_impulse_stays_inside_the_coulomb_cone(world, disk, Vec2::X, Vec2::Y);
2027        }
2028
2029        #[test]
2030        fn tangent_impulse_stays_inside_the_coulomb_cone_r3() {
2031            let mut world = World::new(EuclideanR3);
2032            register_default_narrowphase(&mut world.narrowphase);
2033            world
2034                .set_gravity(Some(Vec3::new(0.0, GRAVITY_Y, 0.0)))
2035                .unwrap();
2036            let sphere = world.push_body(
2037                sphere_body_r3(
2038                    Vec3::new(0.0, SPHERE_RADIUS, 0.0),
2039                    Vec3::new(SLIDE_SPEED, 0.0, 0.0),
2040                    SPHERE_RADIUS,
2041                    1.0,
2042                )
2043                .unwrap(),
2044            );
2045            let floor = world.push_body(halfspace_body_r3(Vec3::Y, 0.0).unwrap());
2046            world.bodies[sphere].restitution = 0.0;
2047            world.bodies[floor].restitution = 0.0;
2048
2049            assert_tangent_impulse_stays_inside_the_coulomb_cone(world, sphere, Vec3::X, Vec3::Y);
2050        }
2051
2052        #[test]
2053        fn tangent_impulse_stays_inside_the_coulomb_cone_r4() {
2054            let mut world = World::new(EuclideanR4);
2055            crate::euclidean_r4::register_default_narrowphase(&mut world.narrowphase);
2056            world
2057                .set_gravity(Some(Vec4::new(0.0, GRAVITY_Y, 0.0, 0.0)))
2058                .unwrap();
2059            let sphere = world.push_body(
2060                sphere_body_r4(
2061                    Vec4::new(0.0, SPHERE_RADIUS, 0.0, 0.0),
2062                    Vec4::new(SLIDE_SPEED, 0.0, 0.0, 0.0),
2063                    SPHERE_RADIUS,
2064                    1.0,
2065                )
2066                .unwrap(),
2067            );
2068            let floor = world.push_body(halfspace4_body_r4(Vec4::Y, 0.0).unwrap());
2069            world.bodies[sphere].restitution = 0.0;
2070            world.bodies[floor].restitution = 0.0;
2071
2072            assert_tangent_impulse_stays_inside_the_coulomb_cone(world, sphere, Vec4::X, Vec4::Y);
2073        }
2074
2075        const STACK_DT: f32 = 1.0 / 240.0;
2076        const STACK_SETTLE_STEPS: usize = 400;
2077        const STACK_LEVELS: usize = 3;
2078
2079        fn clear_warm_start<S: PhysicsSpace>(world: &mut World<S>) {
2080            for manifold in world.manifolds.values_mut() {
2081                for cp in &mut manifold.points {
2082                    cp.normal_impulse = 0.0;
2083                }
2084            }
2085        }
2086
2087        fn velocities<S: PhysicsSpace>(world: &World<S>) -> Vec<S::Vector>
2088        where
2089            S::Vector: VectorOps,
2090        {
2091            world.bodies.iter().map(|b| b.velocity).collect()
2092        }
2093
2094        fn normal_impulses<S: PhysicsSpace>(world: &World<S>) -> Vec<f32> {
2095            world
2096                .manifolds()
2097                .map(|(_, manifold)| manifold)
2098                .flat_map(|m| m.points.iter().map(|cp| cp.normal_impulse))
2099                .collect()
2100        }
2101
2102        fn max_velocity_gap<V: VectorOps>(a: &[V], b: &[V]) -> f32 {
2103            assert_eq!(a.len(), b.len(), "body layouts diverged");
2104            a.iter()
2105                .zip(b)
2106                .map(|(x, y)| VectorOps::length(*x - *y))
2107                .fold(0.0_f32, f32::max)
2108        }
2109
2110        fn max_scalar_gap(a: &[f32], b: &[f32]) -> f32 {
2111            assert_eq!(a.len(), b.len(), "manifold layouts diverged");
2112            a.iter()
2113                .zip(b)
2114                .map(|(x, y)| (x - y).abs())
2115                .fold(0.0_f32, f32::max)
2116        }
2117
2118        fn assert_warm_started_step_matches_cold_started_converged_step<S>(
2119            fixture: impl Fn() -> World<S>,
2120        ) where
2121            S: PhysicsSpace,
2122            S::Vector: VectorOps,
2123            S::Point: Copy + std::ops::Sub<Output = S::Vector>,
2124        {
2125            let mut warm = fixture();
2126            let mut cold_converged = fixture();
2127            let mut cold_default = fixture();
2128
2129            assert!(
2130                warm.manifolds()
2131                    .map(|(_, manifold)| manifold)
2132                    .flat_map(|m| &m.points)
2133                    .any(|cp| cp.normal_impulse > 0.0),
2134                "fixture carries no warm-start payload"
2135            );
2136
2137            clear_warm_start(&mut cold_converged);
2138            clear_warm_start(&mut cold_default);
2139            cold_converged.set_solver_iterations(400);
2140
2141            warm.step(STACK_DT).unwrap();
2142            cold_converged.step(STACK_DT).unwrap();
2143            cold_default.step(STACK_DT).unwrap();
2144
2145            let converged = velocities(&cold_converged);
2146            let warm_gap = max_velocity_gap(&velocities(&warm), &converged);
2147            let cold_gap = max_velocity_gap(&velocities(&cold_default), &converged);
2148
2149            assert!(
2150                warm_gap < 1e-5,
2151                "warm-started step diverged from the converged solve by {warm_gap} m/s"
2152            );
2153            assert!(
2154                warm_gap < cold_gap,
2155                "warm start bought no convergence: warm {warm_gap} vs cold {cold_gap}"
2156            );
2157
2158            let impulse_gap =
2159                max_scalar_gap(&normal_impulses(&warm), &normal_impulses(&cold_converged));
2160            let reference = normal_impulses(&cold_converged)
2161                .into_iter()
2162                .fold(0.0_f32, f32::max);
2163            assert!(
2164                impulse_gap < 1e-3 * reference,
2165                "warm-started accumulator diverged by {impulse_gap} against a peak impulse of {reference}"
2166            );
2167        }
2168
2169        fn settled_disk_stack_r2() -> World<EuclideanR2> {
2170            let mut world = World::new(EuclideanR2);
2171            crate::euclidean_r2::register_default_narrowphase(&mut world.narrowphase);
2172            world.set_gravity(Some(Vec2::new(0.0, GRAVITY_Y))).unwrap();
2173
2174            for level in 0..STACK_LEVELS {
2175                let y = SPHERE_RADIUS + level as f32 * 2.0 * SPHERE_RADIUS;
2176                let id = world.push_body(
2177                    sphere_body(Vec2::new(0.0, y), Vec2::ZERO, SPHERE_RADIUS, 1.0).unwrap(),
2178                );
2179                world.bodies[id].restitution = 0.0;
2180            }
2181            let floor = world.push_body(floor_r2());
2182            world.bodies[floor].restitution = 0.0;
2183
2184            for _ in 0..STACK_SETTLE_STEPS {
2185                world.step(STACK_DT).unwrap();
2186            }
2187            world
2188        }
2189
2190        fn settled_sphere_stack_r4() -> World<EuclideanR4> {
2191            let mut world = World::new(EuclideanR4);
2192            crate::euclidean_r4::register_default_narrowphase(&mut world.narrowphase);
2193            world
2194                .set_gravity(Some(Vec4::new(0.0, GRAVITY_Y, 0.0, 0.0)))
2195                .unwrap();
2196
2197            for level in 0..STACK_LEVELS {
2198                let y = SPHERE_RADIUS + level as f32 * 2.0 * SPHERE_RADIUS;
2199                let id = world.push_body(
2200                    sphere_body_r4(Vec4::new(0.0, y, 0.0, 0.0), Vec4::ZERO, SPHERE_RADIUS, 1.0)
2201                        .unwrap(),
2202                );
2203                world.bodies[id].restitution = 0.0;
2204            }
2205            let floor = world.push_body(halfspace4_body_r4(Vec4::Y, 0.0).unwrap());
2206            world.bodies[floor].restitution = 0.0;
2207
2208            for _ in 0..STACK_SETTLE_STEPS {
2209                world.step(STACK_DT).unwrap();
2210            }
2211            world
2212        }
2213
2214        #[test]
2215        fn warm_started_step_matches_cold_started_converged_step_r2() {
2216            assert_warm_started_step_matches_cold_started_converged_step(settled_disk_stack_r2);
2217        }
2218
2219        #[test]
2220        fn warm_started_step_matches_cold_started_converged_step_r3() {
2221            assert_warm_started_step_matches_cold_started_converged_step(|| {
2222                settled_sphere_stack(STACK_DT, STACK_SETTLE_STEPS)
2223            });
2224        }
2225
2226        #[test]
2227        fn warm_started_step_matches_cold_started_converged_step_r4() {
2228            assert_warm_started_step_matches_cold_started_converged_step(settled_sphere_stack_r4);
2229        }
2230    }
2231
2232    #[test]
2233    fn shrinking_normal_impulse_releases_applied_friction_even_without_slip() {
2234        use crate::euclidean_r2::sphere_body;
2235        use glam::Vec2;
2236        use loam_math::EuclideanR2;
2237
2238        for slip in [0.0, 1.0] {
2239            let mut geometry = crate::geometry::GeometryStore::default();
2240            let mut a = sphere_body(Vec2::ZERO, Vec2::ZERO, 1.0, 0.0)
2241                .unwrap()
2242                .into_row(&mut geometry, &EuclideanR2);
2243            let mut b = sphere_body(Vec2::ZERO, Vec2::new(slip, 2.0), 1.0, 1.0)
2244                .unwrap()
2245                .into_row(&mut geometry, &EuclideanR2);
2246            let previous_friction = 2.0 * FRICTION_COEFF;
2247            let mut contact = ContactPoint {
2248                world_point: Vec2::ZERO,
2249                anchor_a: Vec2::ZERO,
2250                anchor_b: Vec2::ZERO,
2251                normal: Vec2::Y,
2252                penetration: 0.0,
2253                normal_impulse: 2.0,
2254                tangent_dir: Vec2::X,
2255                tangent_impulse: previous_friction,
2256                velocity_bias: 0.0,
2257            };
2258            solve_normal_then_tangent(&EuclideanR2, &mut a, &mut b, &mut contact);
2259            assert_eq!(contact.normal_impulse, 0.0);
2260            assert_eq!(contact.tangent_impulse, 0.0);
2261            assert!((b.velocity - Vec2::X * (slip + previous_friction)).length() < 1e-6);
2262        }
2263    }
2264
2265    const ISLAND_X: [f32; 4] = [-4.0, 0.0, 4.0, 8.0];
2266
2267    fn settled_islands(dt: f32, settle_steps: usize) -> (World<EuclideanR3>, BodyId, Vec<BodyId>) {
2268        let mut world = World::new(EuclideanR3);
2269        register_default_narrowphase(&mut world.narrowphase);
2270        world
2271            .set_gravity(Some(Vec3::new(0.0, GRAVITY_Y, 0.0)))
2272            .unwrap();
2273
2274        let floor = world.push_body(halfspace_body_r3(Vec3::Y, 0.0).unwrap());
2275        world.bodies[floor].restitution = 0.0;
2276        let mut spheres = Vec::with_capacity(ISLAND_X.len());
2277        for x in ISLAND_X {
2278            let id = world.push_body(island_sphere(x));
2279            world.bodies[id].restitution = 0.0;
2280            spheres.push(id);
2281        }
2282
2283        for _ in 0..settle_steps {
2284            world.step(dt).unwrap();
2285        }
2286        (world, floor, spheres)
2287    }
2288
2289    fn island_sphere(x: f32) -> BodyDef<EuclideanR3> {
2290        sphere_body_r3(
2291            Vec3::new(x, SPHERE_RADIUS, 0.0),
2292            Vec3::ZERO,
2293            SPHERE_RADIUS,
2294            1.0,
2295        )
2296        .unwrap()
2297    }
2298
2299    fn body_state(world: &World<EuclideanR3>, id: BodyId) -> (Vec3, Vec3, Bivector3) {
2300        let body = &world.bodies[id];
2301        (body.position, body.velocity, body.angular_velocity)
2302    }
2303
2304    fn normal_impulses(world: &World<EuclideanR3>, key: PairKey) -> Vec<f32> {
2305        world.manifolds[&key]
2306            .points
2307            .iter()
2308            .map(|cp| cp.normal_impulse)
2309            .collect()
2310    }
2311
2312    #[test]
2313    fn sleeping_after_a_step_removes_inactive_islands_immediately() {
2314        let (mut world, floor, spheres) = settled_islands(1.0 / 240.0, 1);
2315        for id in &spheres {
2316            assert!(world.manifolds.contains_key(&(floor, *id)));
2317            world.bodies[*id].sleep();
2318        }
2319        assert!(world.islands().is_empty());
2320        world.bodies[spheres[0]].wake();
2321        let islands = world.islands();
2322        assert_eq!(islands.len(), 1);
2323        assert_eq!(islands[0].bodies, vec![spheres[0]]);
2324    }
2325
2326    #[test]
2327    fn despawn_preserves_surviving_manifold_keys_and_warm_start_impulses() {
2328        let dt = 1.0 / 240.0;
2329        let settle_steps = 400;
2330        let (mut world, floor, spheres) = settled_islands(dt, settle_steps);
2331        let (mut control, _, control_spheres) = settled_islands(dt, settle_steps);
2332
2333        let doomed = spheres[1];
2334        let keeper = spheres[3];
2335        let keeper_key = (floor, keeper);
2336        let control_key = (floor, control_spheres[3]);
2337
2338        let impulses_before = normal_impulses(&world, keeper_key);
2339        assert!(
2340            impulses_before.iter().any(|&jn| jn > 0.0),
2341            "fixture carries no warm-start payload, so nothing below is being preserved"
2342        );
2343        let keeper_position_before = world.bodies.dense_index(keeper).unwrap();
2344
2345        assert!(world.despawn_body(doomed).is_ok());
2346
2347        assert_ne!(
2348            world.bodies.dense_index(keeper).unwrap(),
2349            keeper_position_before,
2350            "despawn moved no surviving body, so this test never reached the compaction case"
2351        );
2352        assert!(
2353            !world
2354                .manifolds
2355                .keys()
2356                .any(|&(a, b)| a == doomed || b == doomed),
2357            "the removed body's manifolds outlived it"
2358        );
2359        assert_eq!(
2360            normal_impulses(&world, keeper_key),
2361            impulses_before,
2362            "compaction disturbed a surviving manifold's warm-start impulses"
2363        );
2364
2365        for _ in 0..60 {
2366            world.step(dt).unwrap();
2367            control.step(dt).unwrap();
2368        }
2369
2370        assert_eq!(
2371            body_state(&world, keeper),
2372            body_state(&control, control_spheres[3]),
2373            "an unrelated despawn perturbed a surviving island"
2374        );
2375        let impulses_after = normal_impulses(&world, keeper_key);
2376        assert_eq!(
2377            impulses_after,
2378            normal_impulses(&control, control_key),
2379            "an unrelated despawn moved a surviving manifold's impulses"
2380        );
2381        assert!(
2382            impulses_after.iter().any(|&jn| jn > 0.0),
2383            "the surviving contact stopped carrying an impulse"
2384        );
2385    }
2386
2387    #[test]
2388    fn spawn_mid_simulation_leaves_existing_islands_bit_identical() {
2389        let dt = 1.0 / 240.0;
2390        let settle_steps = 400;
2391        let (mut world, floor, spheres) = settled_islands(dt, settle_steps);
2392        let (mut control, _, control_spheres) = settled_islands(dt, settle_steps);
2393
2394        let keeper = spheres[0];
2395        let keeper_key = (floor, keeper);
2396        let newcomer = world.push_body(island_sphere(12.0));
2397        world.bodies[newcomer].restitution = 0.0;
2398
2399        for _ in 0..60 {
2400            world.step(dt).unwrap();
2401            control.step(dt).unwrap();
2402        }
2403
2404        assert!(
2405            world.manifolds.contains_key(&(floor, newcomer)),
2406            "the spawned body never made contact, so it exercised no solver state"
2407        );
2408        assert_eq!(
2409            body_state(&world, keeper),
2410            body_state(&control, control_spheres[0]),
2411            "a spawn perturbed an existing island"
2412        );
2413        assert_eq!(
2414            normal_impulses(&world, keeper_key),
2415            normal_impulses(&control, (floor, control_spheres[0])),
2416            "a spawn moved an existing manifold's warm-start impulses"
2417        );
2418    }
2419
2420    #[test]
2421    fn a_recycled_slot_inherits_no_manifold_from_the_previous_body() {
2422        let dt = 1.0 / 240.0;
2423        let (mut world, floor, spheres) = settled_islands(dt, 400);
2424        let doomed = spheres[1];
2425        assert!(world.manifolds.contains_key(&(floor, doomed)));
2426
2427        assert!(world.despawn_body(doomed).is_ok());
2428        assert_eq!(
2429            world.despawn_body(doomed),
2430            Err(EditError::StaleHandle),
2431            "a stale handle despawned a second body"
2432        );
2433        assert!(world.bodies.get(doomed).is_none());
2434
2435        let reborn = world.push_body(island_sphere(ISLAND_X[1]));
2436        assert_eq!(
2437            reborn.slot(),
2438            doomed.slot(),
2439            "the slot was not recycled, so this test is not exercising aliasing"
2440        );
2441        assert_ne!(reborn, doomed);
2442        assert!(
2443            world.bodies.get(doomed).is_none(),
2444            "the stale handle resolved to the body that took its slot"
2445        );
2446
2447        world.step(dt).unwrap();
2448        assert!(
2449            world.manifolds.contains_key(&(floor, reborn)),
2450            "the respawned body made no contact"
2451        );
2452        assert!(
2453            !world.manifolds.contains_key(&(floor, doomed)),
2454            "a manifold keyed on the despawned body came back with the slot"
2455        );
2456    }
2457
2458    const EDIT_DT: f32 = 1.0 / 240.0;
2459    const EDIT_SETTLE_STEPS: usize = 400;
2460
2461    #[test]
2462    fn a_rejected_edit_leaves_the_body_its_inertia_and_its_contacts_untouched() {
2463        let (mut world, floor, spheres) = settled_islands(EDIT_DT, EDIT_SETTLE_STEPS);
2464        let id = spheres[0];
2465        let key = (floor, id);
2466        let before = body_state(&world, id);
2467        let mass = world.bodies[id].mass();
2468        let inv_mass = world.bodies[id].inv_mass();
2469        let inertia = world.bodies[id].inertia;
2470        let orientation = world.bodies[id].orientation;
2471        let stale = BodyId::forge(u32::MAX, 0);
2472        let impulses = normal_impulses(&world, key);
2473        let _ = world.drain_dirty().count();
2474        assert!(
2475            impulses.iter().any(|&jn| jn > 0.0),
2476            "the fixture carries no warm start, so a preserved contact proves nothing"
2477        );
2478
2479        assert_eq!(
2480            world.set_mass_properties(id, f32::NAN, 1.0),
2481            Err(EditError::InvalidMass)
2482        );
2483        assert_eq!(
2484            world.set_mass_properties(id, 3.0, f32::NAN),
2485            Err(EditError::InvalidInertia)
2486        );
2487        assert_eq!(
2488            world.set_restitution(id, -1.0),
2489            Err(EditError::InvalidRestitution)
2490        );
2491        assert_eq!(
2492            world.set_collider(
2493                id,
2494                Collider::Box3 {
2495                    half_extents: Vec3::ONE
2496                },
2497                2.0
2498            ),
2499            Err(EditError::UnsupportedCollider)
2500        );
2501        while world.reclaim_geometry().is_some() {}
2502        assert_eq!(
2503            world.set_collider(id, Collider::sphere_at_origin(0.5), f32::INFINITY),
2504            Err(EditError::InvalidInertia)
2505        );
2506        assert!(
2507            world.reclaim_geometry().is_some(),
2508            "a rejected inertia swallowed the caller's collider"
2509        );
2510        assert_eq!(
2511            world.set_pose(id, Vec3::splat(f32::NAN), orientation),
2512            Err(EditError::NotFinite)
2513        );
2514        let spun = loam_math::Iso3 {
2515            rotation: glam::Quat::from_xyzw(f32::NAN, 0.0, 0.0, 1.0),
2516            translation: Vec3::ZERO,
2517        };
2518        assert_eq!(
2519            world.set_pose(id, Vec3::ZERO, spun),
2520            Err(EditError::NotFinite)
2521        );
2522        let zero_rotation = loam_math::Iso3 {
2523            rotation: glam::Quat::from_xyzw(0.0, 0.0, 0.0, 0.0),
2524            translation: Vec3::ZERO,
2525        };
2526        assert_eq!(
2527            world.set_pose(id, Vec3::ZERO, zero_rotation),
2528            Err(EditError::NotFinite)
2529        );
2530        assert_eq!(
2531            world.set_velocity(id, Vec3::ZERO, Bivector3::new(f32::NAN, 0.0, 0.0)),
2532            Err(EditError::NotFinite)
2533        );
2534        assert_eq!(
2535            world.set_velocity(stale, Vec3::ZERO, Bivector3::ZERO),
2536            Err(EditError::StaleHandle)
2537        );
2538        assert_eq!(
2539            world.drain_dirty().count(),
2540            0,
2541            "a rejected edit published a pose change"
2542        );
2543
2544        assert_eq!(body_state(&world, id), before);
2545        assert_eq!(world.bodies[id].mass(), mass);
2546        assert_eq!(world.bodies[id].inv_mass(), inv_mass);
2547        assert_eq!(world.bodies[id].inertia, inertia);
2548        assert_eq!(normal_impulses(&world, key), impulses);
2549
2550        let mut world4 = World::new(loam_math::EuclideanR4);
2551        let id4 = world4.push_body(
2552            crate::euclidean_r4::sphere_body_r4(glam::Vec4::ZERO, glam::Vec4::ZERO, 0.5, 1.0)
2553                .unwrap(),
2554        );
2555        let before4 = world4.bodies[id4].orientation;
2556        let scale = std::f32::consts::FRAC_1_SQRT_2;
2557        let invalid4 = loam_math::Iso4Flat {
2558            rotation: loam_math::Rotor4 {
2559                s: scale,
2560                xy: 0.0,
2561                xz: 0.0,
2562                xw: 0.0,
2563                yz: 0.0,
2564                yw: 0.0,
2565                zw: 0.0,
2566                xyzw: scale,
2567            },
2568            translation: glam::Vec4::ZERO,
2569        };
2570        assert_eq!(
2571            world4.set_pose(id4, glam::Vec4::ZERO, invalid4),
2572            Err(EditError::NotFinite)
2573        );
2574        assert_eq!(world4.bodies[id4].orientation, before4);
2575    }
2576
2577    #[test]
2578    fn warm_start_impulses_survive_a_step_and_a_snapshot_restore() {
2579        let (mut world, floor, spheres) = settled_islands(EDIT_DT, EDIT_SETTLE_STEPS);
2580        let key = (floor, spheres[0]);
2581        let settled = normal_impulses(&world, key);
2582        assert!(settled.iter().any(|&jn| jn > 0.0));
2583
2584        let state = world.snapshot();
2585        world.step(EDIT_DT).unwrap();
2586
2587        let stepped = normal_impulses(&world, key);
2588        assert_eq!(
2589            stepped.len(),
2590            settled.len(),
2591            "a step dropped a contact slot"
2592        );
2593        for (after, before) in stepped.iter().zip(&settled) {
2594            assert!(
2595                *after > 0.0 && (after - before).abs() < 1e-3,
2596                "a step reset the accumulator: {before} became {after}"
2597            );
2598        }
2599
2600        assert!(world
2601            .apply_impulse(spheres[0], Vec3::new(0.0, 20.0, 0.0))
2602            .is_ok());
2603        for _ in 0..30 {
2604            world.step(EDIT_DT).unwrap();
2605        }
2606        assert!(
2607            !world.manifolds.contains_key(&key),
2608            "the launched sphere kept its contact, so the restore has nothing to undo"
2609        );
2610
2611        assert!(world.restore(&state).is_ok());
2612        assert_eq!(normal_impulses(&world, key), settled);
2613        assert_eq!(world.time, state.time);
2614    }
2615
2616    #[test]
2617    fn invalid_snapshot_state_is_refused_before_the_world_changes() {
2618        let (mut world, floor, spheres) = settled_islands(EDIT_DT, EDIT_SETTLE_STEPS);
2619        let key = (floor, spheres[0]);
2620        let before = world.state_hash(sample_body_r3);
2621        let time = world.time;
2622        let impulses = normal_impulses(&world, key);
2623        let unchanged = |world: &World<EuclideanR3>| {
2624            assert_eq!(world.state_hash(sample_body_r3), before);
2625            assert_eq!(world.time, time);
2626            assert_eq!(normal_impulses(world, key), impulses);
2627        };
2628
2629        let mut invalid_body = world.snapshot();
2630        invalid_body.bodies[spheres[0]].orientation.rotation = Quat::from_xyzw(0.0, 0.0, 0.0, 2.0);
2631        assert_eq!(world.restore(&invalid_body), Err(EditError::InvalidBody));
2632        unchanged(&world);
2633
2634        let mut invalid_time = world.snapshot();
2635        invalid_time.time = f32::NAN;
2636        assert_eq!(world.restore(&invalid_time), Err(EditError::InvalidTime));
2637        unchanged(&world);
2638
2639        let mut invalid_manifold = world.snapshot();
2640        invalid_manifold.manifolds.get_mut(&key).unwrap().points[0].normal = Vec3::splat(f32::NAN);
2641        assert_eq!(
2642            world.restore(&invalid_manifold),
2643            Err(EditError::InvalidManifold)
2644        );
2645        unchanged(&world);
2646    }
2647
2648    #[test]
2649    fn invalid_gravity_and_time_steps_leave_body_state_and_time_unchanged() {
2650        let mut world = World::new(EuclideanR3);
2651        let body = world.push_body(sphere_body_r3(Vec3::X, Vec3::Y, 0.5, 1.0).unwrap());
2652        let before = world.state_hash(sample_body_r3);
2653
2654        assert_eq!(
2655            world.set_gravity(Some(Vec3::splat(f32::NAN))),
2656            Err(EditError::InvalidGravity)
2657        );
2658        assert_eq!(world.gravity(), None);
2659        for dt in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, -1.0] {
2660            assert_eq!(world.step(dt), Err(EditError::InvalidTimeStep));
2661            assert!(world.body(body).is_some());
2662            assert_eq!(world.state_hash(sample_body_r3), before);
2663            assert_eq!(world.time(), 0.0);
2664        }
2665
2666        world.step(0.0).unwrap();
2667        assert!(world.body(body).is_some());
2668        assert_eq!(world.state_hash(sample_body_r3), before);
2669        assert_eq!(world.time(), 0.0);
2670
2671        world.time = f32::MAX;
2672        assert_eq!(world.step(f32::MAX), Err(EditError::InvalidTimeStep));
2673        assert_eq!(world.state_hash(sample_body_r3), before);
2674        assert_eq!(world.time(), f32::MAX);
2675    }
2676
2677    #[test]
2678    fn a_teleported_sleeping_body_publishes_its_pose_change() {
2679        let (mut world, _, spheres) = settled_islands(EDIT_DT, EDIT_SETTLE_STEPS);
2680        let id = spheres[0];
2681        assert!(world.sleep_body(id).is_ok());
2682        assert!(world.drain_dirty().count() > 0);
2683
2684        world.step(EDIT_DT).unwrap();
2685        assert!(
2686            !world.drain_dirty().any(|(dirty, _)| dirty == id),
2687            "a sleeping body was published as if it had moved"
2688        );
2689
2690        let orientation = world.bodies[id].orientation;
2691        let elsewhere = Vec3::new(40.0, 9.0, 0.0);
2692        assert!(world.set_pose(id, elsewhere, orientation).is_ok());
2693        assert!(
2694            world.drain_dirty().any(|(dirty, _)| dirty == id),
2695            "the teleport never reached the dirty set"
2696        );
2697        assert_eq!(world.bodies[id].position, elsewhere);
2698    }
2699
2700    #[test]
2701    fn a_restored_world_rebuilds_its_configuration_and_reproduces_the_snapshot() {
2702        let (mut world, floor, spheres) = settled_islands(EDIT_DT, EDIT_SETTLE_STEPS);
2703        let key = (floor, spheres[0]);
2704        let state = world.snapshot();
2705
2706        let mut unregistered = World::new(EuclideanR3);
2707        assert_eq!(
2708            unregistered.restore(&state),
2709            Err(EditError::RegistrationMismatch),
2710            "restore accepted a world whose dispatch table cannot serve the snapshot"
2711        );
2712        assert_eq!(unregistered.bodies.len(), 0);
2713
2714        let mut rebuilt = World::new(EuclideanR3);
2715        register_default_narrowphase(&mut rebuilt.narrowphase);
2716        rebuilt.set_gravity(world.gravity()).unwrap();
2717        assert!(rebuilt.restore(&state).is_ok());
2718
2719        assert_eq!(
2720            rebuilt.state_hash(sample_body_r3),
2721            world.state_hash(sample_body_r3)
2722        );
2723        assert_eq!(normal_impulses(&rebuilt, key), normal_impulses(&world, key));
2724
2725        for _ in 0..60 {
2726            world.step(EDIT_DT).unwrap();
2727            rebuilt.step(EDIT_DT).unwrap();
2728        }
2729        assert_eq!(
2730            rebuilt.state_hash(sample_body_r3),
2731            world.state_hash(sample_body_r3),
2732            "the restored world solved with a different dispatch table"
2733        );
2734    }
2735
2736    #[test]
2737    fn a_collider_replacement_drops_the_previous_shapes_inertia() {
2738        let mut world = World::new(EuclideanR3);
2739        register_default_narrowphase(&mut world.narrowphase);
2740        let id = world.push_body(box_body(Vec3::ZERO, Vec3::ZERO, Vec3::ONE, 2.0).unwrap());
2741        let previous = world.bodies[id].inertia;
2742        let replacement = 0.5;
2743        assert_ne!(
2744            previous, replacement,
2745            "the two shapes share an inertia, so the swap cannot be seen"
2746        );
2747
2748        assert!(world
2749            .set_collider(id, Collider::sphere_at_origin(0.25), replacement)
2750            .is_ok());
2751        assert!(matches!(
2752            world.collider(&world.bodies[id]),
2753            Some(Collider::Sphere { .. })
2754        ));
2755        assert!(world
2756            .apply_impulse_at_point(id, Vec3::new(3.0, 0.0, 0.0), Vec3::new(0.0, 2.0, 0.0))
2757            .is_ok());
2758
2759        assert_eq!(world.bodies[id].velocity, Vec3::new(1.5, 0.0, 0.0));
2760        assert_eq!(
2761            world.bodies[id].angular_velocity,
2762            Bivector3::new(-12.0, 0.0, 0.0)
2763        );
2764    }
2765
2766    #[test]
2767    fn a_despawn_leaves_no_contact_or_dirty_row_naming_the_removed_body() {
2768        let (mut world, floor, spheres) = settled_islands(EDIT_DT, EDIT_SETTLE_STEPS);
2769        let doomed = spheres[1];
2770        assert!(world.manifolds.contains_key(&(floor, doomed)));
2771        assert!(world
2772            .set_velocity(doomed, Vec3::new(0.0, 3.0, 0.0), Bivector3::ZERO)
2773            .is_ok());
2774
2775        assert!(world.despawn_body(doomed).is_ok());
2776
2777        assert!(
2778            !world
2779                .manifolds
2780                .keys()
2781                .any(|&(a, b)| a == doomed || b == doomed),
2782            "a contact outlived the body it names"
2783        );
2784        let reborn = world.push_body(island_sphere(ISLAND_X[1]));
2785        assert_eq!(
2786            reborn.slot(),
2787            doomed.slot(),
2788            "the slot was not recycled, so this test is not exercising aliasing"
2789        );
2790        let published: Vec<BodyId> = world.drain_dirty().map(|(id, _)| id).collect();
2791        assert!(
2792            !published.contains(&doomed),
2793            "a removed row was published for mirroring"
2794        );
2795        assert!(
2796            published.contains(&reborn),
2797            "the row that took the removed slot was never published"
2798        );
2799
2800        world.step(EDIT_DT).unwrap();
2801        assert!(!world
2802            .manifolds
2803            .keys()
2804            .any(|&(a, b)| a == doomed || b == doomed));
2805    }
2806
2807    #[test]
2808    fn bodies_that_share_a_hull_hold_one_copy_of_its_vertices() {
2809        use crate::euclidean_r4::{polytope_body_r4, tesseract_vertices};
2810        use glam::Vec4;
2811        use loam_math::EuclideanR4;
2812
2813        let hull = tesseract_vertices(1.0);
2814        let mut world = World::new(EuclideanR4);
2815        let a =
2816            world.push_body(polytope_body_r4(Vec4::ZERO, Vec4::ZERO, hull.clone(), 1.0).unwrap());
2817        let b = world.push_body(polytope_body_r4(Vec4::X * 8.0, Vec4::ZERO, hull, 1.0).unwrap());
2818
2819        assert_eq!(world.bodies[a].collider(), world.bodies[b].collider());
2820        assert_eq!(world.geometry().prepared(), 1);
2821
2822        let vertices = |id: BodyId| match world.collider(&world.bodies[id]) {
2823            Some(Collider::ConvexPolytope4D { vertices }) => vertices.as_ptr(),
2824            other => panic!("the hull is not prepared: {other:?}"),
2825        };
2826        assert_eq!(
2827            vertices(a),
2828            vertices(b),
2829            "each body kept its own copy of the hull"
2830        );
2831
2832        assert!(world.despawn_body(a).is_ok());
2833        assert_eq!(
2834            world.geometry().prepared(),
2835            1,
2836            "a shared hull was released while another body still used it"
2837        );
2838        assert!(world.despawn_body(b).is_ok());
2839        assert_eq!(
2840            world.geometry().prepared(),
2841            0,
2842            "the last body's hull outlived it"
2843        );
2844    }
2845
2846    #[test]
2847    fn a_restored_sleeping_body_publishes_the_pose_the_restore_gave_it() {
2848        let (mut world, _, spheres) = settled_islands(EDIT_DT, EDIT_SETTLE_STEPS);
2849        let id = spheres[0];
2850        assert!(world.sleep_body(id).is_ok());
2851        let settled = world.bodies[id].position;
2852
2853        let state = world.snapshot();
2854        let orientation = world.bodies[id].orientation;
2855        assert!(world
2856            .set_pose(id, Vec3::new(40.0, 9.0, 0.0), orientation)
2857            .is_ok());
2858        assert!(world.sleep_body(id).is_ok());
2859        let _ = world.drain_dirty().count();
2860
2861        assert!(world.restore(&state).is_ok());
2862        assert_eq!(world.bodies[id].position, settled);
2863        assert!(world.bodies[id].is_sleeping());
2864        assert!(
2865            world.drain_dirty().any(|(dirty, _)| dirty == id),
2866            "a restore moved a sleeping body without publishing it"
2867        );
2868    }
2869
2870    #[test]
2871    fn a_drained_row_is_readable_without_copying_the_ids_out_first() {
2872        let (mut world, _, spheres) = settled_islands(EDIT_DT, 4);
2873        let _ = world.drain_dirty().count();
2874        world.step(EDIT_DT).unwrap();
2875
2876        let mut mirror: BTreeMap<BodyId, Vec3> = BTreeMap::new();
2877        for (id, body) in world.drain_dirty() {
2878            mirror.insert(id, body.position);
2879        }
2880
2881        assert_eq!(mirror.len(), spheres.len());
2882        for id in &spheres {
2883            assert_eq!(mirror.get(id), Some(&world.bodies[*id].position));
2884        }
2885    }
2886
2887    #[test]
2888    fn a_half_space_on_a_body_with_mass_is_not_reported_as_an_unsupported_collider() {
2889        let mut world = World::new(EuclideanR3);
2890        let id = world.push_body(sphere_body_r3(Vec3::ZERO, Vec3::ZERO, 0.5, 2.0).unwrap());
2891        let floor = Collider::HalfSpace {
2892            normal: Vec3::Y,
2893            offset: 0.0,
2894        };
2895        assert!(
2896            world.space.supports_collider(floor.kind()),
2897            "the space rejects half-spaces outright, so this cannot tell the two apart"
2898        );
2899
2900        assert_eq!(
2901            world.set_collider(id, floor, 1.0),
2902            Err(EditError::DynamicHalfSpace)
2903        );
2904        assert_eq!(world.bodies[id].mass(), 2.0);
2905
2906        let ground = world.push_body(halfspace_body_r3(Vec3::Y, 0.0).unwrap());
2907        assert_eq!(
2908            world.set_mass_properties(ground, 3.0, 1.0),
2909            Err(EditError::DynamicHalfSpace)
2910        );
2911        assert_eq!(world.bodies[ground].mass(), 0.0);
2912    }
2913
2914    #[test]
2915    fn registration_order_does_not_refuse_a_snapshot_of_the_same_pairs() {
2916        fn never(
2917            _a: &RigidBody<EuclideanR3>,
2918            _b: &RigidBody<EuclideanR3>,
2919            _geometry: &crate::geometry::GeometryStore,
2920            _space: &EuclideanR3,
2921        ) -> Option<crate::response::Contact<EuclideanR3>> {
2922            None
2923        }
2924
2925        use crate::collider::ColliderKind;
2926
2927        let pairs = [
2928            (ColliderKind::Sphere, ColliderKind::HalfSpace),
2929            (ColliderKind::Sphere, ColliderKind::ConvexPolytope3D),
2930            (ColliderKind::ConvexPolytope3D, ColliderKind::HalfSpace),
2931        ];
2932        assert_ne!(
2933            pairs.first(),
2934            pairs.last(),
2935            "the registration sequence is symmetric, so reversing it changes nothing"
2936        );
2937
2938        let mut forward = World::new(EuclideanR3);
2939        for &(a, b) in &pairs {
2940            forward.narrowphase.register(a, b, never);
2941        }
2942        let mut reversed = World::new(EuclideanR3);
2943        for &(a, b) in pairs.iter().rev() {
2944            reversed.narrowphase.register(a, b, never);
2945        }
2946        forward.push_body(sphere_body_r3(Vec3::ZERO, Vec3::ZERO, 0.5, 1.0).unwrap());
2947
2948        assert!(reversed.restore(&forward.snapshot()).is_ok());
2949        assert!(forward.restore(&reversed.snapshot()).is_ok());
2950        assert_eq!(reversed.bodies.len(), 1);
2951    }
2952
2953    #[test]
2954    fn dirty_publication_allocates_nothing_on_a_warmed_world() {
2955        let (mut world, _, spheres) = settled_islands(EDIT_DT, 4);
2956        for _ in 0..2 {
2957            world.step(EDIT_DT).unwrap();
2958            assert!(world.drain_dirty().count() > 0);
2959        }
2960
2961        let bytes = bytes_allocated_by(|| {
2962            for _ in 0..16 {
2963                for &id in &spheres {
2964                    assert!(world.wake_body(id).is_ok());
2965                }
2966                assert_eq!(world.drain_dirty().count(), spheres.len());
2967            }
2968        })
2969        .expect("the counting allocator is installed");
2970        assert_eq!(
2971            bytes, 0,
2972            "16 dirty publications over a steady body set asked the allocator for {bytes} bytes"
2973        );
2974    }
2975
2976    #[test]
2977    fn split_two_mut_returns_borrows_in_argument_order() {
2978        let mut slice = [0u32, 1, 2, 3];
2979        for (i, j) in [(1usize, 3usize), (3, 1)] {
2980            let (a, b) = split_two_mut(&mut slice, i, j);
2981            assert_eq!((*a, *b), (i as u32, j as u32), "split_two_mut({i}, {j})");
2982        }
2983    }
2984
2985    fn stacked_pair_world(doomed_first: bool) -> (World<EuclideanR3>, BodyId, BodyId, BodyId) {
2986        const LOWER_HALF_EXTENT: f32 = 0.5;
2987        const UPPER_HALF_EXTENT: f32 = 0.35;
2988        const DOOMED_X: f32 = -6.0;
2989
2990        let mut world = World::new(EuclideanR3);
2991        register_default_narrowphase(&mut world.narrowphase);
2992        world
2993            .set_gravity(Some(Vec3::new(0.0, GRAVITY_Y, 0.0)))
2994            .unwrap();
2995
2996        let floor = world.push_body(halfspace_body_r3(Vec3::Y, 0.0).unwrap());
2997        world.bodies[floor].restitution = 0.0;
2998
2999        let spawned_first = doomed_first.then(|| world.push_body(island_sphere(DOOMED_X)));
3000        let lower = world.push_body(
3001            box_body(
3002                Vec3::new(0.0, LOWER_HALF_EXTENT, 0.0),
3003                Vec3::ZERO,
3004                Vec3::splat(LOWER_HALF_EXTENT),
3005                1.0,
3006            )
3007            .unwrap(),
3008        );
3009        let upper = world.push_body(
3010            box_body(
3011                Vec3::new(0.0, 2.0 * LOWER_HALF_EXTENT + UPPER_HALF_EXTENT, 0.0),
3012                Vec3::ZERO,
3013                Vec3::splat(UPPER_HALF_EXTENT),
3014                3.0,
3015            )
3016            .unwrap(),
3017        );
3018        let doomed = spawned_first.unwrap_or_else(|| world.push_body(island_sphere(DOOMED_X)));
3019
3020        for id in [lower, upper, doomed] {
3021            world.bodies[id].restitution = 0.0;
3022        }
3023        (world, lower, upper, doomed)
3024    }
3025
3026    #[test]
3027    fn gravity_does_not_wake_a_sleeping_support_until_its_neighbor_is_driven() {
3028        let (mut world, lower, upper, distant) = stacked_pair_world(false);
3029        world.despawn_body(distant).unwrap();
3030        let dt = 1.0 / 240.0;
3031        for _ in 0..600 {
3032            world.step(dt).unwrap();
3033        }
3034        world.sleep_body(lower).unwrap();
3035        for _ in 0..120 {
3036            world.step(dt).unwrap();
3037            assert!(world.bodies[lower].is_sleeping());
3038        }
3039        assert!(world.manifolds.contains_key(&canonical_pair(lower, upper)));
3040        world
3041            .set_velocity(upper, -Vec3::Y, Bivector3::ZERO)
3042            .unwrap();
3043        world.step(dt).unwrap();
3044        assert!(!world.bodies[lower].is_sleeping());
3045    }
3046
3047    fn despawned_pair_world(
3048        doomed_first: bool,
3049        dt: f32,
3050    ) -> (World<EuclideanR3>, BodyId, BodyId, PairKey) {
3051        const SETTLE_STEPS: usize = 40;
3052
3053        let (mut world, lower, upper, doomed) = stacked_pair_world(doomed_first);
3054        for _ in 0..SETTLE_STEPS {
3055            world.step(dt).unwrap();
3056        }
3057
3058        let key = canonical_pair(lower, upper);
3059        assert!(
3060            world.manifolds.contains_key(&key),
3061            "the two dynamic bodies never settled into contact"
3062        );
3063        assert!(world.despawn_body(doomed).is_ok());
3064
3065        let (i, j) = world.dense_pair(key);
3066        assert_eq!(
3067            i > j,
3068            doomed_first,
3069            "the pair is stored at {i}, {j}, which is not the order this fixture \
3070             was built to produce"
3071        );
3072        (world, lower, upper, key)
3073    }
3074
3075    #[test]
3076    fn contact_normal_points_from_the_pair_key_low_body_to_the_high_one() {
3077        let dt = 1.0 / 240.0;
3078        for doomed_first in [false, true] {
3079            let (mut world, _, _, key) = despawned_pair_world(doomed_first, dt);
3080            world.step(dt).unwrap();
3081
3082            let manifold = world.manifolds.get(&key).expect("the pair separated");
3083            let key_axis = world.bodies[key.1].position - world.bodies[key.0].position;
3084            assert!(!manifold.points.is_empty(), "manifold carries no contact");
3085            for cp in &manifold.points {
3086                assert!(
3087                    cp.normal.dot(key_axis) > 0.0,
3088                    "normal {} points back toward the key's low body",
3089                    cp.normal
3090                );
3091            }
3092        }
3093    }
3094
3095    #[test]
3096    fn storage_order_does_not_reach_a_contacting_pairs_trajectory() {
3097        let dt = 1.0 / 240.0;
3098        let steps = 120;
3099        let mut trajectories = Vec::new();
3100        for doomed_first in [false, true] {
3101            let (mut world, lower, upper, key) = despawned_pair_world(doomed_first, dt);
3102            let mut contact_steps = 0;
3103            let mut trajectory = Vec::with_capacity(steps);
3104            for _ in 0..steps {
3105                world.step(dt).unwrap();
3106                if world.manifolds.contains_key(&key) {
3107                    contact_steps += 1;
3108                }
3109                trajectory.push((body_state(&world, lower), body_state(&world, upper)));
3110            }
3111            assert_eq!(
3112                contact_steps, steps,
3113                "the pair held contact for only {contact_steps} of {steps} steps"
3114            );
3115            trajectories.push(trajectory);
3116        }
3117
3118        let step = trajectories[0]
3119            .iter()
3120            .zip(&trajectories[1])
3121            .position(|(a, b)| a != b);
3122        assert!(
3123            step.is_none(),
3124            "storage order reached the solve: the pair diverged from the control \
3125             at step {step:?}"
3126        );
3127    }
3128
3129    // Marsaglia 2003, "Xorshift RNGs", Journal of Statistical Software 8(14), 13/7/17.
3130    struct Xorshift(u64);
3131
3132    impl Xorshift {
3133        fn new(seed: u64) -> Self {
3134            Self(seed | 1)
3135        }
3136
3137        fn next_u64(&mut self) -> u64 {
3138            self.0 ^= self.0 << 13;
3139            self.0 ^= self.0 >> 7;
3140            self.0 ^= self.0 << 17;
3141            self.0
3142        }
3143
3144        fn range(&mut self, lo: f32, hi: f32) -> f32 {
3145            let unit = (self.next_u64() >> 40) as f32 / (1u32 << 24) as f32;
3146            lo + (hi - lo) * unit
3147        }
3148    }
3149
3150    fn random_scene(seed: u64, count: usize, spread: f32) -> World<EuclideanR3> {
3151        let mut rng = Xorshift::new(seed);
3152        let mut world = World::new(EuclideanR3);
3153        register_default_narrowphase(&mut world.narrowphase);
3154        world
3155            .set_gravity(Some(Vec3::new(0.0, GRAVITY_Y, 0.0)))
3156            .unwrap();
3157        world.push_body(halfspace_body_r3(Vec3::Y, 0.0).unwrap());
3158
3159        let mut spawned = Vec::with_capacity(count);
3160        for _ in 0..count {
3161            let position = Vec3::new(
3162                rng.range(-spread, spread),
3163                rng.range(0.5, spread + 0.5),
3164                rng.range(-spread, spread),
3165            );
3166            let id = if rng.next_u64() & 1 == 0 {
3167                world.push_body(
3168                    sphere_body_r3(position, Vec3::ZERO, rng.range(0.2, 0.8), 1.0).unwrap(),
3169                )
3170            } else {
3171                world.push_body(
3172                    box_body(position, Vec3::ZERO, Vec3::splat(rng.range(0.2, 0.6)), 1.0).unwrap(),
3173                )
3174            };
3175            if rng.next_u64().is_multiple_of(8) {
3176                assert!(world.bodies[id].set_mass(0.0));
3177            }
3178            world.bodies[id].restitution = 0.0;
3179            spawned.push(id);
3180        }
3181        for doomed in spawned.iter().step_by(5) {
3182            assert!(world.despawn_body(*doomed).is_ok());
3183        }
3184        world
3185    }
3186
3187    fn all_pairs_reference(world: &World<EuclideanR3>) -> Vec<PairKey> {
3188        let mut pairs = Vec::new();
3189        let n = world.bodies.len();
3190        for i in 0..n {
3191            for j in (i + 1)..n {
3192                let (a, b) = (&world.bodies[i], &world.bodies[j]);
3193                if a.inv_mass() == 0.0 && b.inv_mass() == 0.0 {
3194                    continue;
3195                }
3196                let reach = bounding_radius(world.collider(a)) + bounding_radius(world.collider(b));
3197                if world.space.distance(a.position, b.position) <= reach {
3198                    pairs.push(canonical_pair(world.bodies.id_at(i), world.bodies.id_at(j)));
3199                }
3200            }
3201        }
3202        pairs.sort_unstable();
3203        pairs
3204    }
3205
3206    fn dynamic_body_count(world: &World<EuclideanR3>) -> usize {
3207        world.bodies.iter().filter(|b| b.inv_mass() != 0.0).count()
3208    }
3209
3210    const RANDOM_SCENE_SHAPES: [(usize, f32); 3] = [(12, 2.0), (40, 6.0), (80, 3.0)];
3211
3212    #[test]
3213    fn sweep_broadphase_emits_exactly_the_all_pairs_candidate_set() {
3214        let mut ever_beyond_the_floor = false;
3215        for seed in PERMUTATION_SEEDS {
3216            for (count, spread) in RANDOM_SCENE_SHAPES {
3217                let mut world = random_scene(seed, count, spread);
3218                let mut emitted = Vec::new();
3219                for step in 0..8 {
3220                    let expected = all_pairs_reference(&world);
3221                    world.broadphase_into(&mut emitted);
3222                    assert_eq!(
3223                        emitted, expected,
3224                        "seed {seed}, {count} bodies, spread {spread}, step {step}"
3225                    );
3226                    ever_beyond_the_floor |= expected.len() > dynamic_body_count(&world);
3227                    world.step(1.0 / 240.0).unwrap();
3228                }
3229            }
3230        }
3231        assert!(
3232            ever_beyond_the_floor,
3233            "no scene ever produced a candidate pair between two finite colliders"
3234        );
3235    }
3236
3237    #[test]
3238    fn broadphase_culls_only_pairs_the_narrowphase_would_reject() {
3239        for seed in PERMUTATION_SEEDS {
3240            let mut world = random_scene(seed, 40, 3.0);
3241            let mut emitted = Vec::new();
3242            for step in 0..8 {
3243                world.broadphase_into(&mut emitted);
3244                let n = world.bodies.len();
3245                let mut culled = 0usize;
3246                for i in 0..n {
3247                    for j in (i + 1)..n {
3248                        if world.bodies[i].inv_mass() == 0.0 && world.bodies[j].inv_mass() == 0.0 {
3249                            continue;
3250                        }
3251                        let key = canonical_pair(world.bodies.id_at(i), world.bodies.id_at(j));
3252                        if emitted.binary_search(&key).is_ok() {
3253                            continue;
3254                        }
3255                        culled += 1;
3256                        let contact = world.narrowphase.test(
3257                            &world.bodies[i],
3258                            &world.bodies[j],
3259                            world.geometry(),
3260                            &world.space,
3261                        );
3262                        assert!(
3263                            contact.is_none(),
3264                            "seed {seed} step {step}: the sweep culled {key:?}, which the \
3265                             narrowphase reports in contact"
3266                        );
3267                    }
3268                }
3269                assert!(
3270                    culled > 0,
3271                    "seed {seed} step {step}: nothing was culled, so this pass proved nothing"
3272                );
3273                world.step(1.0 / 240.0).unwrap();
3274            }
3275        }
3276    }
3277
3278    #[test]
3279    fn broadphase_emits_strictly_ascending_keys_under_disagreeing_storage_order() {
3280        for seed in PERMUTATION_SEEDS {
3281            let mut world = random_scene(seed, 40, 3.0);
3282            let disagrees = (1..world.bodies.len())
3283                .any(|dense| world.bodies.id_at(dense) < world.bodies.id_at(dense - 1));
3284            assert!(
3285                disagrees,
3286                "seed {seed}: storage order still agrees with handle order, so this \
3287                 scene cannot tell the two apart"
3288            );
3289
3290            let mut pairs = Vec::new();
3291            world.broadphase_into(&mut pairs);
3292            assert!(pairs.len() > 1, "seed {seed}: too few pairs to be ordered");
3293            assert!(
3294                pairs.windows(2).all(|w| w[0] < w[1]),
3295                "seed {seed}: emission order is not strictly ascending in BodyId"
3296            );
3297        }
3298    }
3299
3300    #[test]
3301    fn broadphase_prunes_the_quadratic_pair_set_at_scale() {
3302        let mut world = random_scene(PERMUTATION_SEEDS[1], 200, 20.0);
3303        let n = world.bodies.len();
3304        assert!(
3305            n >= 100,
3306            "the scale case needs at least 100 bodies, got {n}"
3307        );
3308        let all_pairs = n * (n - 1) / 2;
3309        let mut pairs = Vec::new();
3310        world.broadphase_into(&mut pairs);
3311        let emitted = pairs.len();
3312        assert!(
3313            emitted * 10 < all_pairs,
3314            "the sweep emitted {emitted} of {all_pairs} pairs, which is no better than \
3315             a constant-factor cull"
3316        );
3317    }
3318
3319    #[test]
3320    fn broadphase_fill_allocates_nothing_after_the_first_pass() {
3321        let world = random_scene(PERMUTATION_SEEDS[0], 120, 8.0);
3322        let mut intervals = Vec::new();
3323        let mut active = Vec::new();
3324        let mut pairs = Vec::new();
3325
3326        for _ in 0..2 {
3327            World::fill_broadphase(
3328                &world.bodies,
3329                world.geometry(),
3330                &world.space,
3331                &mut intervals,
3332                &mut active,
3333                &mut pairs,
3334                &mut StepCounters::default(),
3335            );
3336        }
3337        assert!(!pairs.is_empty(), "the fixture produced no pairs to emit");
3338
3339        let bytes = bytes_allocated_by(|| {
3340            for _ in 0..16 {
3341                World::fill_broadphase(
3342                    &world.bodies,
3343                    world.geometry(),
3344                    &world.space,
3345                    &mut intervals,
3346                    &mut active,
3347                    &mut pairs,
3348                    &mut StepCounters::default(),
3349                );
3350            }
3351        })
3352        .expect("the counting allocator is installed");
3353        assert_eq!(
3354            bytes, 0,
3355            "16 sweeps over a steady body set asked the allocator for {bytes} bytes"
3356        );
3357    }
3358
3359    #[test]
3360    fn step_counters_report_the_sweep_work_of_a_four_body_line() {
3361        let mut world = World::new(EuclideanR3);
3362        register_default_narrowphase(&mut world.narrowphase);
3363        for x in [0.0, 0.75, 1.75, 5.0] {
3364            world.push_body(sphere_body_r3(Vec3::new(x, 0.0, 0.0), Vec3::ZERO, 0.5, 1.0).unwrap());
3365        }
3366        world.step(1.0 / 240.0).unwrap();
3367        assert_eq!(
3368            world.counters(),
3369            StepCounters {
3370                index_visits: 7,
3371                distance_evals: 6,
3372                candidates: 2,
3373                contacts: 1,
3374            }
3375        );
3376    }
3377
3378    #[test]
3379    fn exactly_tangent_spheres_are_a_candidate_pair() {
3380        const RADIUS: f32 = 0.5;
3381        let mut world = World::new(EuclideanR3);
3382        let anchor = world.push_body(sphere_body_r3(Vec3::ZERO, Vec3::ZERO, RADIUS, 1.0).unwrap());
3383        let tangent = world.push_body(sphere_body_r3(Vec3::X, Vec3::ZERO, RADIUS, 1.0).unwrap());
3384        let separated = world.push_body(
3385            sphere_body_r3(
3386                Vec3::new(-(1.0 + f32::EPSILON), 0.0, 0.0),
3387                Vec3::ZERO,
3388                RADIUS,
3389                1.0,
3390            )
3391            .unwrap(),
3392        );
3393
3394        let position = |id: BodyId| {
3395            let dense = world.bodies.dense_index(id).expect("nothing despawned");
3396            world.bodies[dense].position
3397        };
3398        assert_eq!(
3399            world.space.distance(position(anchor), position(tangent)),
3400            RADIUS + RADIUS,
3401            "the fixture is not exactly tangent, so it cannot state the boundary"
3402        );
3403        assert!(
3404            world.space.distance(position(anchor), position(separated)) > RADIUS + RADIUS,
3405            "the separated sphere is not past the boundary"
3406        );
3407
3408        let mut pairs = Vec::new();
3409        world.broadphase_into(&mut pairs);
3410        assert_eq!(pairs, vec![canonical_pair(anchor, tangent)]);
3411    }
3412
3413    #[test]
3414    fn coincident_point_colliders_are_a_candidate_pair() {
3415        let mut world = World::new(EuclideanR3);
3416        let a = world.push_body(sphere_body_r3(Vec3::ZERO, Vec3::ZERO, 0.0, 1.0).unwrap());
3417        let b = world.push_body(sphere_body_r3(Vec3::ZERO, Vec3::ZERO, 0.0, 1.0).unwrap());
3418        let mut pairs = Vec::new();
3419        world.broadphase_into(&mut pairs);
3420        assert_eq!(pairs, vec![canonical_pair(a, b)]);
3421    }
3422
3423    #[test]
3424    fn bounding_radius_contains_every_posed_vertex_of_its_collider() {
3425        let half_extents = Vec3::new(0.5, 1.25, 0.25);
3426        let vertices = crate::euclidean_r3::box_vertices(half_extents);
3427        let radius = bounding_radius(Some(&Collider::ConvexPolytope3D {
3428            vertices: vertices.clone(),
3429        }));
3430        assert_eq!(radius, half_extents.length());
3431
3432        let rotation = glam::Quat::from_axis_angle(Vec3::new(1.0, 2.0, 3.0).normalize(), 0.7);
3433        for v in &vertices {
3434            let posed = rotation * *v;
3435            assert!(
3436                posed.length() <= radius + 1e-6,
3437                "posed vertex {posed} escaped the bounding radius {radius}"
3438            );
3439        }
3440
3441        assert_eq!(
3442            bounding_radius(Some(&Collider::sphere_at_origin(0.75))),
3443            0.75
3444        );
3445        assert_eq!(
3446            bounding_radius(Some(&Collider::HalfSpace {
3447                normal: Vec3::Y,
3448                offset: 0.0
3449            })),
3450            f32::INFINITY,
3451            "a half-space is unbounded and must never be culled"
3452        );
3453    }
3454
3455    const ISLAND_SETTLE_STEPS: usize = 400;
3456    const ISLAND_COLUMN_PITCH: f32 = 4.0;
3457    const ISLAND_COLUMNS: usize = 6;
3458    const ISLAND_COLUMN_HEIGHT: usize = 3;
3459    const ISLAND_PINNED_COLUMN: usize = 2;
3460
3461    fn settled_columns(seed: u64) -> World<EuclideanR3> {
3462        const GAP: f32 = 0.05;
3463        const PINNED_TOUCH: f32 = 0.01;
3464
3465        let mut rng = Xorshift::new(seed);
3466        let mut world = World::new(EuclideanR3);
3467        register_default_narrowphase(&mut world.narrowphase);
3468        world
3469            .set_gravity(Some(Vec3::new(0.0, GRAVITY_Y, 0.0)))
3470            .unwrap();
3471        world.push_body(halfspace_body_r3(Vec3::Y, 0.0).unwrap());
3472
3473        let mut columns: Vec<Vec<BodyId>> = Vec::with_capacity(ISLAND_COLUMNS);
3474        for column in 0..ISLAND_COLUMNS {
3475            let x = column as f32 * ISLAND_COLUMN_PITCH + rng.range(-0.5, 0.5);
3476            let radius = rng.range(0.3, 0.6);
3477            let mut ids = Vec::with_capacity(ISLAND_COLUMN_HEIGHT);
3478            for level in 0..ISLAND_COLUMN_HEIGHT {
3479                let pinned = column == ISLAND_PINNED_COLUMN && level == 1;
3480                let y = if pinned {
3481                    3.0 * radius - PINNED_TOUCH
3482                } else {
3483                    radius + GAP + level as f32 * (2.0 * radius + GAP)
3484                };
3485                let id = world.push_body(
3486                    sphere_body_r3(Vec3::new(x, y, 0.0), Vec3::ZERO, radius, 1.0).unwrap(),
3487                );
3488                world.bodies[id].restitution = 0.0;
3489                if pinned {
3490                    assert!(world.bodies[id].set_mass(0.0));
3491                }
3492                ids.push(id);
3493            }
3494            columns.push(ids);
3495        }
3496
3497        for column in columns.iter().take(2) {
3498            assert!(world.despawn_body(column[0]).is_ok());
3499        }
3500        for _ in 0..ISLAND_SETTLE_STEPS {
3501            world.step(1.0 / 240.0).unwrap();
3502        }
3503        world
3504    }
3505
3506    #[test]
3507    fn island_ids_are_the_lowest_body_id_not_the_lowest_storage_position() {
3508        let mut orders_disagreed = 0usize;
3509        for seed in PERMUTATION_SEEDS {
3510            let mut world = settled_columns(seed);
3511            for step in 0..8 {
3512                world.step(1.0 / 240.0).unwrap();
3513                let islands = world.islands();
3514                for island in &islands {
3515                    let lowest_handle = island.bodies.iter().copied().min();
3516                    assert_eq!(
3517                        Some(island.id),
3518                        lowest_handle,
3519                        "seed {seed} step {step}: island {:?} is not named by its \
3520                         lowest handle",
3521                        island.id
3522                    );
3523                    let lowest_stored =
3524                        island.bodies.iter().copied().min_by_key(|&id| {
3525                            world.bodies.dense_index(id).expect(STALE_MANIFOLD_BODY)
3526                        });
3527                    if lowest_stored != lowest_handle {
3528                        orders_disagreed += 1;
3529                    }
3530                }
3531                assert!(
3532                    islands.windows(2).all(|w| w[0].id < w[1].id),
3533                    "seed {seed} step {step}: islands are not strictly ascending in id"
3534                );
3535            }
3536        }
3537        assert!(
3538            orders_disagreed > 0,
3539            "no island ever held a body whose handle order disagreed with its \
3540             storage order, so the two labelling rules were never told apart"
3541        );
3542    }
3543
3544    #[test]
3545    fn a_static_body_joins_no_island_and_merges_none() {
3546        for seed in PERMUTATION_SEEDS {
3547            let world = settled_columns(seed);
3548            let pinned = world
3549                .bodies
3550                .iter()
3551                .position(|body| {
3552                    body.inv_mass() == 0.0
3553                        && matches!(world.collider(body), Some(Collider::Sphere { .. }))
3554                })
3555                .map(|dense| world.bodies.id_at(dense))
3556                .expect("the fixture lost its static sphere");
3557
3558            let neighbours: Vec<BodyId> = world
3559                .manifolds
3560                .keys()
3561                .filter_map(|&(a, b)| match (a == pinned, b == pinned) {
3562                    (true, false) => Some(b),
3563                    (false, true) => Some(a),
3564                    _ => None,
3565                })
3566                .collect();
3567            assert_eq!(
3568                neighbours.len(),
3569                2,
3570                "seed {seed}: the static sphere touches {} bodies, so it is \
3571                 not wedged between two",
3572                neighbours.len()
3573            );
3574
3575            let islands = world.islands();
3576            let island_of = |id: BodyId| {
3577                islands
3578                    .iter()
3579                    .find(|island| island.bodies.contains(&id))
3580                    .map(|island| island.id)
3581            };
3582            assert!(
3583                island_of(pinned).is_none(),
3584                "seed {seed}: a static body joined an island"
3585            );
3586            assert_ne!(
3587                island_of(neighbours[0]),
3588                island_of(neighbours[1]),
3589                "seed {seed}: two bodies that meet only through a static one \
3590                 were merged into one island"
3591            );
3592        }
3593    }
3594
3595    fn flood_fill_islands(world: &World<EuclideanR3>) -> Vec<Island> {
3596        let dynamic = |id: BodyId| world.bodies[id].inv_mass() != 0.0;
3597        let mut adjacency: BTreeMap<BodyId, Vec<BodyId>> = BTreeMap::new();
3598        for (&(a, b), _) in world.manifolds() {
3599            for id in [a, b].into_iter().filter(|&id| dynamic(id)) {
3600                adjacency.entry(id).or_default();
3601            }
3602            if dynamic(a) && dynamic(b) {
3603                adjacency.entry(a).or_default().push(b);
3604                adjacency.entry(b).or_default().push(a);
3605            }
3606        }
3607
3608        let mut seen: BTreeSet<BodyId> = BTreeSet::new();
3609        let mut islands = Vec::new();
3610        for &seed in adjacency.keys() {
3611            if !seen.insert(seed) {
3612                continue;
3613            }
3614            let mut bodies = vec![seed];
3615            let mut frontier = vec![seed];
3616            while let Some(body) = frontier.pop() {
3617                for &next in &adjacency[&body] {
3618                    if seen.insert(next) {
3619                        bodies.push(next);
3620                        frontier.push(next);
3621                    }
3622                }
3623            }
3624            bodies.sort_unstable();
3625            let constraints = world
3626                .manifolds()
3627                .map(|(&key, _)| key)
3628                .filter(|&(a, b)| {
3629                    bodies.binary_search(&a).is_ok() || bodies.binary_search(&b).is_ok()
3630                })
3631                .collect();
3632            islands.push(Island {
3633                id: bodies[0],
3634                bodies,
3635                constraints,
3636            });
3637        }
3638        islands.sort_unstable_by_key(|island| island.id);
3639        islands
3640    }
3641
3642    #[test]
3643    fn islands_match_a_flood_fill_of_the_contact_graph() {
3644        let mut ever_multi_body = false;
3645        for seed in PERMUTATION_SEEDS {
3646            let mut world = settled_columns(seed);
3647            for step in 0..8 {
3648                world.step(1.0 / 240.0).unwrap();
3649                let islands = world.islands();
3650                ever_multi_body |= islands.iter().any(|island| island.bodies.len() > 1);
3651                assert_eq!(
3652                    islands,
3653                    flood_fill_islands(&world),
3654                    "seed {seed} step {step}: union-find disagreed with the flood fill"
3655                );
3656            }
3657        }
3658        assert!(
3659            ever_multi_body,
3660            "no island ever held two bodies, so the comparison never covered a union"
3661        );
3662    }
3663
3664    #[test]
3665    fn a_single_island_solves_in_the_global_ascending_key_order() {
3666        let world = settled_sphere_stack(1.0 / 240.0, 200);
3667        let islands = world.islands();
3668        assert_eq!(
3669            islands.len(),
3670            1,
3671            "the stack is not one island, so this fixture cannot state the \
3672             single-island case"
3673        );
3674
3675        let ascending: Vec<PairKey> = world.manifolds().map(|(&key, _)| key).collect();
3676        assert!(ascending.len() > 1, "too few constraints to be ordered");
3677        assert_eq!(
3678            constraint_order(&world),
3679            ascending,
3680            "grouping moved a constraint in a world with a single island"
3681        );
3682        assert_eq!(islands[0].constraints, ascending);
3683        assert_eq!(
3684            islands[0].bodies.len(),
3685            3,
3686            "the island should hold the three spheres and not the static floor"
3687        );
3688    }
3689
3690    #[test]
3691    fn constraint_buffer_runs_island_by_island() {
3692        let mut world = multi_island_world();
3693        for _ in 0..MULTI_ISLAND_STEPS {
3694            world.step(MULTI_ISLAND_DT).unwrap();
3695        }
3696
3697        let islands = world.islands();
3698        assert_eq!(
3699            islands.len(),
3700            3,
3701            "the groups share only the static floor, so they are three islands"
3702        );
3703        let grouped: Vec<PairKey> = islands
3704            .iter()
3705            .flat_map(|island| island.constraints.iter().copied())
3706            .collect();
3707        assert_eq!(
3708            constraint_order(&world),
3709            grouped,
3710            "the solved buffer is not the islands in order"
3711        );
3712
3713        let ascending: Vec<PairKey> = world.manifolds().map(|(&key, _)| key).collect();
3714        assert_ne!(
3715            grouped, ascending,
3716            "the fixture's islands happen to be contiguous in ascending key \
3717             order, so it cannot show that grouping reorders anything"
3718        );
3719    }
3720
3721    #[test]
3722    fn island_scratch_allocates_nothing_after_the_first_solve() {
3723        const DT: f32 = 1.0 / 240.0;
3724        let (mut world, _, _) = settled_islands(DT, 120);
3725        world.step(DT).unwrap();
3726        assert!(!world.constraints.is_empty(), "the fixture solves nothing");
3727        world.solve(DT);
3728
3729        let bytes = bytes_allocated_by(|| {
3730            for _ in 0..16 {
3731                world.solve(DT);
3732            }
3733        })
3734        .expect("the counting allocator is installed");
3735        assert_eq!(
3736            bytes, 0,
3737            "16 solves over a steady island set asked the allocator for {bytes} bytes"
3738        );
3739    }
3740
3741    #[test]
3742    fn island_grouping_allocates_nothing_after_the_first_pass() {
3743        let mut world = settled_columns(PERMUTATION_SEEDS[0]);
3744        for _ in 0..2 {
3745            world.collect_constraints();
3746        }
3747        assert!(world.constraints.len() > 1);
3748
3749        let bytes = bytes_allocated_by(|| {
3750            for _ in 0..16 {
3751                world.collect_constraints();
3752            }
3753        })
3754        .expect("the counting allocator is installed");
3755        assert_eq!(
3756            bytes, 0,
3757            "16 island passes over a steady contact set asked the allocator for \
3758             {bytes} bytes"
3759        );
3760    }
3761
3762    #[test]
3763    fn bounded_contact_churn_allocates_nothing_after_warmup() {
3764        let mut world = World::new(EuclideanR3);
3765        register_default_narrowphase(&mut world.narrowphase);
3766        let floor = world.push_body(halfspace_body_r3(Vec3::Y, 0.0).unwrap());
3767        let mut active =
3768            world.push_body(sphere_body_r3(Vec3::Y * 0.49, Vec3::ZERO, 0.5, 1.0).unwrap());
3769        let mut inactive =
3770            world.push_body(sphere_body_r3(Vec3::Y * 4.0, Vec3::ZERO, 0.5, 1.0).unwrap());
3771        world.update_manifolds();
3772        for _ in 0..4 {
3773            world.despawn_body(inactive).unwrap();
3774            let next =
3775                world.push_body(sphere_body_r3(Vec3::Y * 4.0, Vec3::ZERO, 0.5, 1.0).unwrap());
3776            let active_orientation = world.bodies[active].orientation;
3777            let next_orientation = world.bodies[next].orientation;
3778            world
3779                .set_pose(active, Vec3::Y * 4.0, active_orientation)
3780                .unwrap();
3781            world
3782                .set_pose(next, Vec3::Y * 0.49, next_orientation)
3783                .unwrap();
3784            world.update_manifolds();
3785            inactive = active;
3786            active = next;
3787        }
3788        assert!(world.manifolds.contains_key(&canonical_pair(floor, active)));
3789
3790        let state = world.snapshot();
3791        world.restore(&state).unwrap();
3792        let retained = world.manifold_pool.len();
3793        for _ in 0..4 {
3794            world.restore(&state).unwrap();
3795            assert_eq!(world.manifold_pool.len(), retained);
3796        }
3797        let first_measured_generation = active.generation();
3798
3799        let mut bytes = 0;
3800        for _ in 0..16 {
3801            world.despawn_body(inactive).unwrap();
3802            let next =
3803                world.push_body(sphere_body_r3(Vec3::Y * 4.0, Vec3::ZERO, 0.5, 1.0).unwrap());
3804            let active_orientation = world.bodies[active].orientation;
3805            let next_orientation = world.bodies[next].orientation;
3806            world
3807                .set_pose(active, Vec3::Y * 4.0, active_orientation)
3808                .unwrap();
3809            world
3810                .set_pose(next, Vec3::Y * 0.49, next_orientation)
3811                .unwrap();
3812            bytes += bytes_allocated_by(|| world.update_manifolds())
3813                .expect("the counting allocator is installed");
3814            inactive = active;
3815            active = next;
3816        }
3817        assert!(active.generation() > first_measured_generation);
3818        assert!(world.manifolds.contains_key(&canonical_pair(floor, active)));
3819        assert_eq!(
3820            bytes, 0,
3821            "16 new-generation recontacts asked the allocator for {bytes} bytes"
3822        );
3823    }
3824
3825    #[test]
3826    fn posed_halfspaces_move_sphere_and_hull_contacts_in_r3_and_r4() {
3827        let mut r3 = World::new(EuclideanR3);
3828        register_default_narrowphase(&mut r3.narrowphase);
3829        let wall3 = r3.push_body(halfspace_body_r3(Vec3::Y, 0.0).unwrap());
3830        let sphere3 =
3831            r3.push_body(sphere_body_r3(Vec3::new(2.25, 3.0, -2.0), Vec3::ZERO, 0.5, 1.0).unwrap());
3832        let hull3 = r3.push_body(
3833            box_body(Vec3::new(2.1, 3.0, 2.0), Vec3::ZERO, Vec3::splat(0.2), 1.0).unwrap(),
3834        );
3835        let pose3 = loam_math::Iso3 {
3836            rotation: glam::Quat::from_rotation_arc(Vec3::Y, Vec3::X),
3837            translation: Vec3::ZERO,
3838        };
3839        r3.set_pose(wall3, Vec3::X * 2.0, pose3).unwrap();
3840        r3.update_manifolds();
3841        for body in [sphere3, hull3] {
3842            let manifold = r3.manifold(canonical_pair(wall3, body)).unwrap();
3843            assert!(manifold.points.iter().any(|point| point.normal.x > 0.99));
3844        }
3845
3846        let mut r4 = World::new(loam_math::EuclideanR4);
3847        crate::euclidean_r4::register_default_narrowphase(&mut r4.narrowphase);
3848        let wall4 =
3849            r4.push_body(crate::euclidean_r4::halfspace4_body_r4(glam::Vec4::Y, 0.0).unwrap());
3850        let sphere4 = r4.push_body(
3851            crate::euclidean_r4::sphere_body_r4(
3852                glam::Vec4::new(2.25, 3.0, -2.0, 0.0),
3853                glam::Vec4::ZERO,
3854                0.5,
3855                1.0,
3856            )
3857            .unwrap(),
3858        );
3859        let hull4 = r4.push_body(
3860            crate::euclidean_r4::polytope_body_r4(
3861                glam::Vec4::new(2.1, 3.0, 2.0, 0.0),
3862                glam::Vec4::ZERO,
3863                crate::euclidean_r4::tesseract_vertices(0.2),
3864                1.0,
3865            )
3866            .unwrap(),
3867        );
3868        let pose4 = loam_math::Iso4Flat {
3869            rotation: loam_math::Rotor4::from_rotation_arc(glam::Vec4::Y, glam::Vec4::X),
3870            translation: glam::Vec4::ZERO,
3871        };
3872        r4.set_pose(wall4, glam::Vec4::X * 2.0, pose4).unwrap();
3873        r4.update_manifolds();
3874        for body in [sphere4, hull4] {
3875            let manifold = r4.manifold(canonical_pair(wall4, body)).unwrap();
3876            assert!(manifold.points.iter().any(|point| point.normal.x > 0.99));
3877        }
3878    }
3879
3880    #[test]
3881    fn r4_wall_correction_pushes_toward_the_near_face() {
3882        use crate::euclidean_r4::{sphere_body_r4, tesseract_vertices};
3883        use glam::Vec4;
3884        use loam_math::EuclideanR4;
3885        for x in [-0.092, -0.09, -0.088] {
3886            let mut world = World::new(EuclideanR4);
3887            crate::euclidean_r4::register_default_narrowphase(&mut world.narrowphase);
3888            world.push_body(
3889                BodyDef::fixed(
3890                    Vec4::ZERO,
3891                    Collider::ConvexPolytope4D {
3892                        vertices: tesseract_vertices(2.0)
3893                            .into_iter()
3894                            .map(|v| v * Vec4::new(0.05, 2.0, 2.0, 2.0))
3895                            .collect(),
3896                    },
3897                    1.0,
3898                    &EuclideanR4,
3899                )
3900                .unwrap(),
3901            );
3902            let ball = world.push_body(
3903                sphere_body_r4(Vec4::new(x, 0.0, 0.0, 0.0), Vec4::ZERO, 0.1, 1.0).unwrap(),
3904            );
3905            world.step(1.0 / 240.0).unwrap();
3906            assert!(world.bodies[ball].position.x < x, "x={x}");
3907        }
3908    }
3909}