Skip to main content

alice_physics/
world_shape_query.rs

1//! Swept and overlap queries against the geometry a [`PhysicsWorld`] actually
2//! collides with.
3//!
4//! [`crate::query::sphere_cast`], [`crate::query::capsule_cast`],
5//! [`crate::query::overlap_sphere`] and [`crate::query::overlap_aabb`] test each
6//! body's bounding sphere (or its position) and see no static or SDF collider.
7//! The world methods here test the same geometry as
8//! [`PhysicsWorld::cast_ray`] (see [`crate::shape_raycast`]): body shapes,
9//! compound children, planes, height fields, triangle meshes and (`std` only) SDF
10//! colliders.
11//!
12//! | method | query |
13//! |---|---|
14//! | [`PhysicsWorld::cast_sphere`] | a sphere swept along a direction, nearest hit |
15//! | [`PhysicsWorld::cast_capsule`] | a capsule (segment `a`–`b` grown by a radius) swept along a direction, nearest hit |
16//! | [`PhysicsWorld::overlap_sphere`] | every collider a sphere overlaps |
17//! | [`PhysicsWorld::overlap_aabb`] | every collider an axis-aligned box overlaps |
18//!
19//! # How each geometry is tested
20//!
21//! A swept sphere of radius `r` first touches a solid `S` where its centre
22//! reaches the boundary of the Minkowski sum `S ⊕ ball(r)`; a ray against that sum
23//! is used wherever it has a closed form:
24//!
25//! | geometry | sphere cast | capsule cast |
26//! |---|---|---|
27//! | sphere (body radius, compound child) | ray vs sphere of `R + r` | ray from the sphere's centre along `−direction` vs the capsule grown by `R` |
28//! | box (shape, compound child) | ray vs rounded box (3 slabs + 12 edge capsules) | convex time of impact |
29//! | cylinder | ray vs rounded cylinder (2 cylinders + 2 rim tori) | convex time of impact |
30//! | torus | ray vs torus of tube `minor + r` | arcs of the ring: time of impact against each arc's hull grown by `minor`, arcs split until the hull is within `2⁻³²` of the arc |
31//! | compound capsule | ray vs capsule of `radius + r` | convex time of impact |
32//! | cone, ellipsoid, wedge, convex hull child | convex time of impact | convex time of impact |
33//! | plane | closed form (two-sided) | closed form at the nearer end |
34//! | triangle mesh | per triangle: the two offset triangles + 3 edge capsules | convex time of impact per triangle |
35//! | height field | per cell: time of impact against the hull of the cell's corners, the cell split into parts until each hull is within `2⁻³²` of the surface (a planar cell is its own hull) | the same, with the segment |
36//! | SDF (`std`) | sphere tracing of `field − r` | sphere tracing of the field minimum along the segment |
37//!
38//! Overlaps use the distance from the sphere's centre to the geometry (closed form
39//! for spheres, capsules, boxes, cylinders, tori, planes and triangles, GJK for the
40//! other convex pieces). A box overlaps a convex piece when GJK finds them
41//! intersecting, a plane when the plane passes between its corners, a height field
42//! when the surface heights over the box's footprint (the bilinear surface takes
43//! its extremes at the corners of each cell's part of the footprint) reach into
44//! its `Y` range, and an SDF when an octree subdivision of the box finds a point
45//! inside.
46//!
47//! # Conventions
48//!
49//! Those of [`crate::shape_raycast`], with these additions:
50//!
51//! - The direction need not be unit length; it is normalized and `t` is a
52//!   **distance** along it. A zero direction or `max_t ≤ 0` gives no hit.
53//! - A negative radius is not a sphere: no hit, no overlap. Radius `0` is allowed:
54//!   [`PhysicsWorld::cast_sphere`] with radius `0` **is** [`PhysicsWorld::cast_ray`]
55//!   (same target, `t`, point and normal), and an overlap of radius `0` is a point
56//!   strictly inside a solid.
57//! - A capsule whose two ends coincide is a sphere: [`PhysicsWorld::cast_capsule`]
58//!   then returns [`PhysicsWorld::cast_sphere`].
59//! - A cast that starts overlapping a collider by more than `2⁻³²` reports it at
60//!   `t = 0`, normal `−direction`, point the cast shape's centre (the sphere's
61//!   centre, the capsule segment's midpoint). Otherwise the hit's point is the
62//!   contact point on the collider and the normal is the collider's surface
63//!   normal there, pointing toward the cast shape (for surfaces: toward the side
64//!   the shape comes from).
65//! - A cast that starts touching a collider (within `2⁻³²`) hits it at `t = 0`
66//!   only when the direction goes into the surface (`direction·normal < 0`);
67//!   moving away from it or along it is not a hit, and the cast goes on to what
68//!   lies further along the path. The same holds wherever a cast touches a
69//!   surface on its way: a contact whose direction is within `2⁻⁶` of the
70//!   tangent plane is a hit only if the path then goes more than `2⁻³²` into
71//!   that piece, so a shape resting on a mesh floor or a flat height field
72//!   crosses the edges between triangles and cells.
73//! - Touching is not overlapping: an overlap needs a distance below the radius
74//!   (or, for boxes against convex pieces, a GJK intersection).
75//! - Ties in `t` are broken by target (bodies, then static colliders, then SDF
76//!   colliders, each by index); overlaps are returned sorted by target. Neither
77//!   depends on the order the BVH visits bodies in.
78//! - An [`AABB`] with `min > max` on any axis contains nothing.
79//!
80//! # Precision
81//!
82//! Closed-form paths are exact up to `Fix128` truncation and the CORDIC rotations,
83//! like the ray queries (`tests/analytic_world_shape_query.rs` checks them to
84//! `1e-12`).
85//!
86//! The convex time of impact steps by Newton's method on the gap `g(t)` (GJK
87//! distance less the radii; GJK stops when its bound is within `2⁻⁴⁰` of the
88//! distance). For two convex sets moving apart linearly `g` is convex in `t`,
89//! so the tangent's root never passes the first contact, and a gap that is not
90//! decreasing proves there is none. It stops when the gap is below `2⁻³²`
91//! (checked to `1e-9`); a grazing approach converges linearly instead of
92//! quadratically but still in a few dozen steps (an approach `0.1°` from
93//! tangent is checked). Meshes, height-field cells and torus arcs are split into
94//! convex parts, so moving along one part (a mesh floor) costs nothing against
95//! another (a wall of the same mesh).
96//!
97//! **No missed hits:** a cast reports no hit only when there is none within
98//! `max_t` (to the `2⁻³²` gap tolerance). If a step budget runs out (a part
99//! hierarchy deeper than its node budget, an SDF traced for more than 4096
100//! steps), the cast reports the earliest position it could not rule out, which
101//! is never after the true contact.
102//!
103//! A bilinear height-field cell restricted to a rectangle of its `(u, v)` is
104//! again bilinear with its four corners as control points, so it lies inside
105//! their convex hull, within a quarter of the part's twist `|h00 − h10 − h01 +
106//! h11|·Δu·Δv` of it. Distances to a cell are found by branch-and-bound over
107//! such parts (the hull distance bounds a part from below), so they never
108//! exceed the true distance by more than `2⁻³²`; casts take the earliest hull
109//! contact over parts, splitting a part until its hull is that close to it, so
110//! a cast never moves past the surface. The contact is then moved onto the
111//! surface by Newton's method. Cells are visited in the order the swept shape
112//! reaches their boxes. Segment distances to tori and
113//! SDFs minimise a 1-Lipschitz point distance along the segment by
114//! branch-and-bound to `2⁻¹⁶` and golden-section refinement after it. SDF fields
115//! are `f32`: a hit is where `field − r` drops below [`crate::sdf_ccd::SdfCcdConfig::tolerance`].
116//!
117//! Author: Moroya Sakamoto
118
119use crate::body_collider::BodyCollider;
120use crate::collider::{Capsule, Support, AABB};
121use crate::compound::{CompoundChild, ShapeRef};
122use crate::heightfield::HeightField;
123use crate::math::{Fix128, QuatFix, Vec3Fix};
124use crate::plane_collider::PlaneCollider;
125#[cfg(feature = "std")]
126use crate::sdf_ccd::SdfCcdConfig;
127use crate::shape::{PosedShape, Shape};
128use crate::shape_raycast::{
129    box_planes, nearer, ray_local_cylinder, ray_local_torus, ray_planes, ray_solid_capsule,
130    ray_solid_sphere, to_local, to_world, torus_distance, LocalHit, RayFilter, RayTarget,
131    PARALLEL_EPSILON,
132};
133use crate::solver::PhysicsWorld;
134use crate::static_collider::StaticCollider;
135use crate::trimesh::{ray_triangle, TriMesh, Triangle};
136
137#[cfg(not(feature = "std"))]
138use alloc::{vec, vec::Vec};
139
140/// Conservative advancement stops when the gap is below this: `2⁻³²`.
141const TRACE_TOLERANCE: Fix128 = Fix128 {
142    hi: 0,
143    lo: 0x0000_0001_0000_0000,
144};
145
146/// A touching contact whose motion is closer than this to the surface's tangent
147/// plane (`|d·n| < 2⁻⁶`) is a hit only if the path then goes more than
148/// [`TRACE_TOLERANCE`] into the piece (see [`dips_below`]); a steeper one is.
149const NEAR_TANGENT: Fix128 = Fix128 {
150    hi: 0,
151    lo: 0x0400_0000_0000_0000,
152};
153
154/// Newton steps of a convex time of impact before the last clear time is
155/// reported (it needs a few dozen at most, even at a grazing approach).
156const TRACE_MAX_STEPS: usize = 1024;
157
158/// Sphere-tracing steps for an SDF before the last position is reported.
159#[cfg(feature = "std")]
160const SDF_MAX_STEPS: usize = 4096;
161
162/// GJK stops when `|v|² − v·w ≤ |v|² · 2⁻⁴⁰`.
163const GJK_RELATIVE: Fix128 = Fix128 {
164    hi: 0,
165    lo: 0x0000_0000_0100_0000,
166};
167
168/// GJK treats `|v|² ≤ 2⁻⁶⁴` as intersecting: `|v| ≤ 2⁻³²`, the conservative
169/// advancement tolerance. `lo` is a 64-bit fraction (`lo = 1` is `2⁻⁶⁴`, the
170/// smallest positive `Fix128`), so this is the smallest threshold a squared
171/// length can be compared with; a larger one (`lo = 1 << 48` is `2⁻¹⁶`, `|v| ≤
172/// 2⁻⁸`) would count any pair nearer than about `0.0039` as intersecting.
173const GJK_INTERSECT: Fix128 = Fix128 { hi: 0, lo: 1 };
174
175const GJK_MAX_ITERATIONS: usize = 64;
176
177/// Branch-and-bound along a segment stops subdividing below this bound gap:
178/// `2⁻¹⁶`.
179const CURVE_COARSE: Fix128 = Fix128 {
180    hi: 0,
181    lo: 0x0001_0000_0000_0000,
182};
183
184/// Golden-section steps after the branch-and-bound (shrinks by `0.618⁶⁴`).
185const GOLDEN_STEPS: usize = 64;
186
187/// Branch-and-bound node budget per minimisation.
188const CURVE_MAX_NODES: usize = 256;
189
190/// Branch-and-bound parts per height-field cell distance.
191const CELL_MAX_NODES: usize = 512;
192
193/// Parts expanded per hierarchical time of impact (one height-field cell).
194const TREE_MAX_NODES: usize = 1024;
195
196/// SDF box overlap: octree depth.
197#[cfg(feature = "std")]
198const SDF_BOX_DEPTH: u32 = 10;
199
200// ============================================================================
201// Public types
202// ============================================================================
203
204/// A hit of [`PhysicsWorld::cast_sphere`] or [`PhysicsWorld::cast_capsule`].
205#[derive(Clone, Copy, Debug, PartialEq, Eq)]
206#[non_exhaustive]
207pub struct WorldShapeHit {
208    /// Distance the shape travels along the (normalized) direction before it
209    /// touches.
210    pub t: Fix128,
211    /// The contact point on the collider (the cast shape's centre for a cast that
212    /// starts overlapping; see the module conventions).
213    pub point: Vec3Fix,
214    /// Unit surface normal of the collider at the contact, toward the cast shape
215    /// (`−direction` for a cast that starts overlapping).
216    pub normal: Vec3Fix,
217    /// The collider hit.
218    pub target: RayTarget,
219    /// The body the hit belongs to, as in [`crate::shape_raycast::WorldRayHit::body`].
220    pub body: Option<usize>,
221}
222
223/// One collider a capsule overlaps (see [`PhysicsWorld::capsule_penetrations`]).
224#[derive(Clone, Copy, Debug)]
225pub(crate) struct Penetration {
226    /// The collider.
227    pub(crate) target: RayTarget,
228    /// How far the capsule reaches into it, `r −` the segment's distance (`> 0`).
229    pub(crate) depth: Fix128,
230    /// Unit normal of the collider at the nearest point, toward the segment:
231    /// moving the capsule by `depth · normal` makes it touch.
232    pub(crate) normal: Vec3Fix,
233}
234
235// ============================================================================
236// Distances
237// ============================================================================
238
239/// The distance from a query core (a point or a segment) to one piece of geometry.
240#[derive(Clone, Copy, Debug)]
241enum Dist {
242    /// The core meets the inside of a solid (or lies on a surface).
243    Inside,
244    /// The nearest point of the geometry, its distance and the unit normal there
245    /// pointing toward the core.
246    Outside {
247        dist: Fix128,
248        point: Vec3Fix,
249        normal: Vec3Fix,
250    },
251    /// Nothing nearer than this.
252    AtLeast(Fix128),
253}
254
255impl Dist {
256    /// A lower bound on the distance (`−1` inside), for comparisons.
257    fn value(&self) -> Fix128 {
258        match *self {
259            Self::Inside => Fix128::NEG_ONE,
260            Self::Outside { dist, .. } => dist,
261            Self::AtLeast(d) => d,
262        }
263    }
264
265    /// Whether a sphere of `r` about the core overlaps the geometry.
266    fn within(&self, r: Fix128) -> bool {
267        match *self {
268            Self::Inside => true,
269            Self::Outside { dist, .. } => dist < r,
270            Self::AtLeast(_) => false,
271        }
272    }
273
274    /// The outside distance from `core` to `point`, the normal toward the core.
275    fn toward(core: Vec3Fix, point: Vec3Fix) -> Self {
276        let (n, dist) = (core - point).normalize_with_length();
277        if dist.is_zero() {
278            return Self::Inside;
279        }
280        Self::Outside {
281            dist,
282            point,
283            normal: n,
284        }
285    }
286
287    /// This distance less `amount` (the core grown, a solid's radius).
288    fn shrunk(self, amount: Fix128) -> Self {
289        match self {
290            Self::Outside {
291                dist,
292                point,
293                normal,
294            } => {
295                let d = dist - amount;
296                if d.is_negative() {
297                    Self::Inside
298                } else {
299                    Self::Outside {
300                        dist: d,
301                        point: point + normal * amount,
302                        normal,
303                    }
304                }
305            }
306            Self::AtLeast(d) => Self::AtLeast(d - amount),
307            Self::Inside => Self::Inside,
308        }
309    }
310}
311
312/// Whether a cast of radius `r` starts overlapping a piece at distance `d`: more
313/// than [`TRACE_TOLERANCE`] inside it. Within the tolerance it is touching, and
314/// the sweep decides by the direction of motion.
315fn starts_overlapping(d: &Dist, r: Fix128) -> bool {
316    match *d {
317        Dist::Inside => true,
318        Dist::Outside { dist, .. } => dist + TRACE_TOLERANCE < r,
319        Dist::AtLeast(_) => false,
320    }
321}
322
323fn half_vec(v: Vec3Fix) -> Vec3Fix {
324    Vec3Fix::new(v.x.half(), v.y.half(), v.z.half())
325}
326
327fn min_fix(a: Fix128, b: Fix128) -> Fix128 {
328    if b < a {
329        b
330    } else {
331        a
332    }
333}
334
335fn max_fix(a: Fix128, b: Fix128) -> Fix128 {
336    if b > a {
337        b
338    } else {
339        a
340    }
341}
342
343/// Below this squared length (`|v| < 2⁻¹⁶`) a length or direction is taken from
344/// `v` scaled up by a power of two: `length_squared` keeps only multiples of
345/// `2⁻⁶⁴`, so `|v|` near `2⁻³²` would have one or two significant bits.
346const FINE_LENGTH_SQUARED: Fix128 = Fix128 {
347    hi: 0,
348    lo: 0x0000_0001_0000_0000,
349};
350
351/// `2⁻¹⁶`, the square root of [`FINE_LENGTH_SQUARED`].
352const FINE_LENGTH: Fix128 = Fix128 {
353    hi: 0,
354    lo: 0x0001_0000_0000_0000,
355};
356
357/// `v · 2ᵏ` with the largest component in `[1, 2)` and that `k`, for
358/// `0 < |v| < 1` (`k > 0`, an exact scaling); zero gives `(ZERO, 0)`.
359fn scaled_up(v: Vec3Fix) -> (Vec3Fix, u32) {
360    let raw = |f: Fix128| ((f.hi as i128) << 64) | (f.lo as i128);
361    let (x, y, z) = (raw(v.x), raw(v.y), raw(v.z));
362    let m = x.unsigned_abs().max(y.unsigned_abs()).max(z.unsigned_abs());
363    if m == 0 {
364        return (Vec3Fix::ZERO, 0);
365    }
366    // bit 64 is 1.0
367    let msb = 127 - m.leading_zeros();
368    if msb >= 64 {
369        return (v, 0);
370    }
371    let k = 64 - msb;
372    let up = |r: i128| {
373        let s = r << k;
374        Fix128::from_raw((s >> 64) as i64, s as u64)
375    };
376    (Vec3Fix::new(up(x), up(y), up(z)), k)
377}
378
379/// `|v|`, accurate for a short `v` too (measured on `v` scaled up, see
380/// [`FINE_LENGTH_SQUARED`]); `None` for zero. A `v` not shorter than `2⁻¹⁶`
381/// gives `v.length()`.
382fn fine_length(v: Vec3Fix) -> Option<Fix128> {
383    if v.length_squared() >= FINE_LENGTH_SQUARED {
384        let len = v.length();
385        return (!len.is_zero()).then_some(len);
386    }
387    let (s, k) = scaled_up(v);
388    let len = s.length();
389    if len.is_zero() {
390        return None;
391    }
392    let back = (((len.hi as i128) << 64) | (len.lo as i128)) >> k;
393    Some(Fix128::from_raw((back >> 64) as i64, back as u64))
394}
395
396fn clamp(x: Fix128, lo: Fix128, hi: Fix128) -> Fix128 {
397    max_fix(lo, min_fix(x, hi))
398}
399
400/// The point of segment `a`–`b` nearest `p`.
401fn closest_on_segment(a: Vec3Fix, b: Vec3Fix, p: Vec3Fix) -> Vec3Fix {
402    let ab = b - a;
403    let len2 = ab.length_squared();
404    if len2.is_zero() {
405        return a;
406    }
407    a + ab * clamp((p - a).dot(ab) / len2, Fix128::ZERO, Fix128::ONE)
408}
409
410/// The point of `aabb` nearest `p` (the point itself inside).
411fn closest_on_aabb(aabb: &AABB, p: Vec3Fix) -> Vec3Fix {
412    Vec3Fix::new(
413        clamp(p.x, aabb.min.x, aabb.max.x),
414        clamp(p.y, aabb.min.y, aabb.max.y),
415        clamp(p.z, aabb.min.z, aabb.max.z),
416    )
417}
418
419/// A point, for GJK.
420struct PointSupport(Vec3Fix);
421
422impl Support for PointSupport {
423    fn support(&self, _direction: Vec3Fix) -> Vec3Fix {
424        self.0
425    }
426}
427
428/// A segment, for GJK.
429struct SegmentSupport(Vec3Fix, Vec3Fix);
430
431impl Support for SegmentSupport {
432    fn support(&self, direction: Vec3Fix) -> Vec3Fix {
433        if self.1.dot(direction) > self.0.dot(direction) {
434            self.1
435        } else {
436            self.0
437        }
438    }
439}
440
441/// A triangle, for GJK.
442struct TriangleSupport<'a>(&'a Triangle);
443
444impl Support for TriangleSupport<'_> {
445    fn support(&self, direction: Vec3Fix) -> Vec3Fix {
446        let t = self.0;
447        let (d0, d1, d2) = (
448            t.v0.dot(direction),
449            t.v1.dot(direction),
450            t.v2.dot(direction),
451        );
452        if d1 > d0 && d1 >= d2 {
453            t.v1
454        } else if d2 > d0 && d2 > d1 {
455            t.v2
456        } else {
457            t.v0
458        }
459    }
460}
461
462/// A compound child placed in the world, for GJK.
463struct ChildSupport<'a> {
464    child: &'a CompoundChild,
465    position: Vec3Fix,
466    rotation: QuatFix,
467}
468
469/// An ellipsoid, whose support mapping normalizes its direction, given that
470/// direction scaled up to a largest component
471/// in `[1, 2)`: GJK's direction near a contact is about as long as the gap, and
472/// such a mapping (which leaves directions down to `2⁻²⁴` unscaled) loses that
473/// many bits of the support point, enough to put it about `2⁻³²` off the
474/// surface. The scaling is exact and the direction unchanged. Polytopes are not
475/// wrapped: their support compares dot products, and a different rounding would
476/// only pick another of two tied vertices. Cylinders and cones are not wrapped
477/// either: their support jumps between the rims (and the apex), and with the
478/// scaled direction casts onto their sides went further in, and some stopped
479/// early, so they keep the previous support direction.
480struct RoundSupport<'a>(&'a PosedShape);
481
482impl Support for RoundSupport<'_> {
483    fn support(&self, direction: Vec3Fix) -> Vec3Fix {
484        self.0.support(scaled_up(direction).0)
485    }
486}
487
488/// Whether a posed shape is wrapped in [`RoundSupport`] (an ellipsoid).
489fn is_round(posed: &PosedShape) -> bool {
490    matches!(posed.shape, Shape::Ellipsoid { .. })
491}
492
493impl Support for ChildSupport<'_> {
494    fn support(&self, direction: Vec3Fix) -> Vec3Fix {
495        self.child
496            .support_world(direction, self.position, self.rotation)
497    }
498}
499
500/// One vertex of a GJK simplex: the Minkowski-difference point and the two
501/// support points it came from.
502#[derive(Clone, Copy)]
503struct Vertex {
504    w: Vec3Fix,
505    a: Vec3Fix,
506    b: Vec3Fix,
507}
508
509fn vertex<A: Support, B: Support>(a: &A, b: &B, direction: Vec3Fix) -> Vertex {
510    let pa = a.support(direction);
511    let pb = b.support(-direction);
512    Vertex {
513        w: pa - pb,
514        a: pa,
515        b: pb,
516    }
517}
518
519/// A sub-simplex, its weights, and whether it is a face whose barycentric
520/// weights are not precise (see [`closest_on_triangle`] and [`gjk_distance`]).
521type Nearest = (Vec<Vertex>, Vec<Fix128>, bool);
522
523/// The point of the simplex nearest the origin, the sub-simplex it lies on and
524/// its barycentric weights; `None` when the origin is inside a tetrahedron.
525///
526/// The weights are the same for the simplex scaled about the origin, and are
527/// computed on it scaled up by `2ᵏ` (exactly) when its edges are shorter than
528/// [`SMALL_SIMPLEX`]: their dot products, of the size of an edge to the fourth
529/// power, would otherwise keep few significant bits (on a curved surface the
530/// simplex shrinks around the nearest point as GJK converges).
531fn closest_on_simplex(s: &[Vertex]) -> Option<Nearest> {
532    let k = simplex_scale(s);
533    if k == 0 {
534        return closest_on_scaled_simplex(s, 0);
535    }
536    let up: Vec<Vertex> = s
537        .iter()
538        .map(|v| Vertex {
539            w: shift_vec(v.w, k as i32),
540            ..*v
541        })
542        .collect();
543    let (sub, lambda, face) = closest_on_scaled_simplex(&up, k)?;
544    let sub = sub
545        .into_iter()
546        .map(|v| Vertex {
547            w: shift_vec(v.w, -(k as i32)),
548            ..v
549        })
550        .collect();
551    Some((sub, lambda, face))
552}
553
554/// [`closest_on_simplex`] for `s` scaled up by `2ᵏ`.
555fn closest_on_scaled_simplex(s: &[Vertex], k: u32) -> Option<Nearest> {
556    match s.len() {
557        1 => Some((s.to_vec(), vec![Fix128::ONE], false)),
558        2 => {
559            let (sub, lambda) = closest_on_edge(s[0], s[1]);
560            Some((sub, lambda, false))
561        }
562        3 => Some(closest_on_triangle(s[0], s[1], s[2])),
563        _ => closest_on_tetrahedron(s[0], s[1], s[2], s[3], k),
564    }
565}
566
567/// A simplex whose edges have every component below this (`2⁻⁸`) is scaled up
568/// for its barycentric weights (see [`closest_on_simplex`]).
569const SMALL_SIMPLEX_BITS: u32 = 56;
570
571/// The `k` that brings the largest edge component of `s` to `[1, 2)`, `0` when
572/// it is at least `2⁻⁸` (or the simplex is a point), and limited so that no
573/// vertex gets a component of `2²⁴` or more.
574fn simplex_scale(s: &[Vertex]) -> u32 {
575    let raw = |f: Fix128| ((f.hi as i128) << 64) | (f.lo as i128);
576    let mag = |v: Vec3Fix| {
577        raw(v.x)
578            .unsigned_abs()
579            .max(raw(v.y).unsigned_abs())
580            .max(raw(v.z).unsigned_abs())
581    };
582    let Some(first) = s.first() else {
583        return 0;
584    };
585    let edge = s.iter().map(|v| mag(v.w - first.w)).max().unwrap_or(0);
586    if edge == 0 {
587        return 0;
588    }
589    // bit 64 is 1.0
590    let msb = 127 - edge.leading_zeros();
591    if msb >= SMALL_SIMPLEX_BITS {
592        return 0;
593    }
594    let far = s.iter().map(|v| mag(v.w)).max().unwrap_or(0);
595    let far_msb = 127 - far.leading_zeros();
596    // keep components below 2²⁴ (bit 88)
597    (64 - msb).min(88u32.saturating_sub(far_msb))
598}
599
600/// `v · 2ᵏ` (`k` may be negative), exact when no bit leaves the range.
601fn shift_vec(v: Vec3Fix, k: i32) -> Vec3Fix {
602    let raw = |f: Fix128| ((f.hi as i128) << 64) | (f.lo as i128);
603    let sh = |f: Fix128| {
604        let r = raw(f);
605        let s = if k >= 0 { r << k } else { r >> (-k) };
606        Fix128::from_raw((s >> 64) as i64, s as u64)
607    };
608    Vec3Fix::new(sh(v.x), sh(v.y), sh(v.z))
609}
610
611fn closest_on_edge(a: Vertex, b: Vertex) -> (Vec<Vertex>, Vec<Fix128>) {
612    let ab = b.w - a.w;
613    let len2 = ab.length_squared();
614    let t = if len2.is_zero() {
615        Fix128::ZERO
616    } else {
617        -a.w.dot(ab) / len2
618    };
619    if t <= Fix128::ZERO {
620        (vec![a], vec![Fix128::ONE])
621    } else if t >= Fix128::ONE {
622        (vec![b], vec![Fix128::ONE])
623    } else {
624        (vec![a, b], vec![Fix128::ONE - t, t])
625    }
626}
627
628/// [`log2_size`] of zero.
629const NO_SIZE: i32 = i32::MIN / 4;
630
631/// `⌊log₂⌋` of the largest component of `v` ([`NO_SIZE`] for zero).
632fn log2_size(v: Vec3Fix) -> i32 {
633    let raw = |f: Fix128| ((f.hi as i128) << 64) | (f.lo as i128);
634    let m = raw(v.x)
635        .unsigned_abs()
636        .max(raw(v.y).unsigned_abs())
637        .max(raw(v.z).unsigned_abs());
638    if m == 0 {
639        return NO_SIZE;
640    }
641    // bit 64 is 1.0
642    63 - m.leading_zeros() as i32
643}
644
645/// Whether the barycentric form gives `point`, the nearest point of triangle
646/// `a b c`, to within `2⁻³²` of its length, so that its direction (GJK's next
647/// search direction) is within `2⁻³²` rad. The ratio does not depend on a
648/// scaling of the simplex.
649///
650/// Its region tests and weights are differences of products of two dot
651/// products: each dot product of vectors of sizes `L` (an edge) and `P` (a
652/// vertex) is rounded to about `2⁻⁶²·(L + P)`, so a weight, such a difference
653/// over `|n|²` (`n = (b − a) × (c − a)`), is off by about `2⁻⁶²·L·P / |n|²`,
654/// and the point by `L` times that. On a curved solid the simplex near the
655/// surface is a long thin triangle of support points far from the origin
656/// (`|n|` a small fraction of `L²`): the point was off by up to `2⁻²⁶` there,
657/// its direction turned the next support back to a vertex already in the
658/// simplex, and GJK stopped up to `2⁻²⁶` away from the surface. The faces of
659/// the polytopes cast at (boxes, wedges, meshes) stay well below the bound and
660/// keep the barycentric form. The sizes are taken from the leading bits of
661/// the components, so the estimate is within a factor of 8.
662fn barycentric_is_precise(a: Vertex, b: Vertex, c: Vertex, point: Vec3Fix) -> bool {
663    let ab = b.w - a.w;
664    let ac = c.w - a.w;
665    let n = log2_size(ab.cross(ac));
666    let near = log2_size(point);
667    if n == NO_SIZE || near == NO_SIZE {
668        return false;
669    }
670    let l = log2_size(ab).max(log2_size(ac)).max(log2_size(c.w - b.w));
671    let p = log2_size(a.w).max(log2_size(b.w)).max(log2_size(c.w));
672    // log₂ of the point's error over its length
673    -62 + 2 * l + p - 2 * n - near <= -32
674}
675
676/// The point of triangle `a b c` nearest the origin: by
677/// [`closest_on_triangle_barycentric`] when that is precise (see
678/// [`barycentric_is_precise`]), otherwise by [`closest_on_triangle_by_area`]
679/// (the third element tells [`gjk_distance`] that it is such a face).
680fn closest_on_triangle(a: Vertex, b: Vertex, c: Vertex) -> Nearest {
681    let (sub, lambda) = closest_on_triangle_barycentric(a, b, c);
682    if barycentric_is_precise(a, b, c, weighted(&(sub.clone(), lambda.clone()))) {
683        return (sub, lambda, false);
684    }
685    let (sub, lambda) = closest_on_triangle_by_area(a, b, c);
686    let face = sub.len() == 3;
687    (sub, lambda, face)
688}
689
690/// The point of triangle `a b c` nearest the origin, its sub-simplex and
691/// weights, for a triangle whose barycentric form is not precise (see
692/// [`barycentric_is_precise`]). The regions are decided by the signed areas
693/// `n·((q − p) × (o − p))` of the origin `o` projected on the plane, one per
694/// edge `p q` (`n = (b − a) × (c − a)`, scaled up to a largest component in
695/// `[1, 2)`): each is one cross product of edge vectors and a dot product, so
696/// its rounding is about `2⁻⁶²` of the size of the triangle, and a weight,
697/// such an area over their sum `n·n`, is off by that over `|n|`. The
698/// barycentric form ([`closest_on_triangle_barycentric`]) takes the same
699/// quantities as differences of products of dot products, whose weights are
700/// off by that size squared over `|n|²`.
701///
702/// When the origin is outside an edge (a negative area), the nearest point is
703/// on such an edge (the nearest point of a convex polygon to a point outside it
704/// is on an edge whose line separates them), and the nearest of those edges is
705/// taken.
706fn closest_on_triangle_by_area(a: Vertex, b: Vertex, c: Vertex) -> (Vec<Vertex>, Vec<Fix128>) {
707    let ab = b.w - a.w;
708    let ac = c.w - a.w;
709    let bc = c.w - b.w;
710    let n = scaled_up(ab.cross(ac)).0;
711    // the area for the edge opposite each vertex (positive inside)
712    let area_a = n.dot(bc.cross(-b.w));
713    let area_b = n.dot((-ac).cross(-c.w));
714    let area_c = n.dot(ab.cross(-a.w));
715    let sum = area_a + area_b + area_c;
716    if n.length_squared().is_zero() || sum <= Fix128::ZERO {
717        // A degenerate (flat) triangle: the nearest of its edges.
718        let mut best = closest_on_edge(a, b);
719        for cand in [closest_on_edge(b, c), closest_on_edge(a, c)] {
720            if shorter(weighted(&cand), weighted(&best)) {
721                best = cand;
722            }
723        }
724        return best;
725    }
726    if !area_a.is_negative() && !area_b.is_negative() && !area_c.is_negative() {
727        let (wb, wc) = (area_b / sum, area_c / sum);
728        return (vec![a, b, c], vec![Fix128::ONE - wb - wc, wb, wc]);
729    }
730    let mut best: Option<(Vec<Vertex>, Vec<Fix128>)> = None;
731    for (area, p, q) in [(area_c, a, b), (area_a, b, c), (area_b, a, c)] {
732        if !area.is_negative() {
733            continue;
734        }
735        let cand = closest_on_edge(p, q);
736        if best
737            .as_ref()
738            .is_none_or(|b| shorter(weighted(&cand), weighted(b)))
739        {
740            best = Some(cand);
741        }
742    }
743    // not reached: one of the areas is negative here
744    best.unwrap_or_else(|| (vec![a], vec![Fix128::ONE]))
745}
746
747/// Ericson, *Real-Time Collision Detection* §5.1.5, for the origin.
748fn closest_on_triangle_barycentric(a: Vertex, b: Vertex, c: Vertex) -> (Vec<Vertex>, Vec<Fix128>) {
749    let ab = b.w - a.w;
750    let ac = c.w - a.w;
751    let ap = -a.w;
752    let d1 = ab.dot(ap);
753    let d2 = ac.dot(ap);
754    if d1 <= Fix128::ZERO && d2 <= Fix128::ZERO {
755        return (vec![a], vec![Fix128::ONE]);
756    }
757    let bp = -b.w;
758    let d3 = ab.dot(bp);
759    let d4 = ac.dot(bp);
760    if d3 >= Fix128::ZERO && d4 <= d3 {
761        return (vec![b], vec![Fix128::ONE]);
762    }
763    let vc = d1 * d4 - d3 * d2;
764    if vc <= Fix128::ZERO && d1 >= Fix128::ZERO && d3 <= Fix128::ZERO {
765        let v = d1 / (d1 - d3);
766        return (vec![a, b], vec![Fix128::ONE - v, v]);
767    }
768    let cp = -c.w;
769    let d5 = ab.dot(cp);
770    let d6 = ac.dot(cp);
771    if d6 >= Fix128::ZERO && d5 <= d6 {
772        return (vec![c], vec![Fix128::ONE]);
773    }
774    let vb = d5 * d2 - d1 * d6;
775    if vb <= Fix128::ZERO && d2 >= Fix128::ZERO && d6 <= Fix128::ZERO {
776        let w = d2 / (d2 - d6);
777        return (vec![a, c], vec![Fix128::ONE - w, w]);
778    }
779    let va = d3 * d6 - d5 * d4;
780    if va <= Fix128::ZERO && d4 - d3 >= Fix128::ZERO && d5 - d6 >= Fix128::ZERO {
781        let w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
782        return (vec![b, c], vec![Fix128::ONE - w, w]);
783    }
784    let denom = va + vb + vc;
785    if denom <= Fix128::ZERO {
786        // A degenerate (flat) triangle: the nearest of its edges.
787        let mut best = closest_on_edge(a, b);
788        for cand in [closest_on_edge(b, c), closest_on_edge(a, c)] {
789            if weighted(&cand).length_squared() < weighted(&best).length_squared() {
790                best = cand;
791            }
792        }
793        return best;
794    }
795    let v = vb / denom;
796    let w = vc / denom;
797    (vec![a, b, c], vec![Fix128::ONE - v - w, v, w])
798}
799
800/// The nearest point of tetrahedron `a b c d` (scaled up by `2ᵏ`), `None` when
801/// the origin is inside it.
802///
803/// A tetrahedron whose least height (the volume `n·(d − a)` over its largest
804/// face's `|n|`, both from differences of its vertices) is at most `h = 2⁻³²`
805/// (in unscaled units) is taken as flat: four support points on one circle or
806/// one generator of a curved solid are coplanar but for rounding, and the
807/// sign of such a volume (and of the origin's side of a face) is that
808/// rounding, which counted the origin inside and reported an intersection far
809/// from the surface. Its nearest face is returned instead: every point of the
810/// flat tetrahedron is within `h` of a face, so an origin inside it is within
811/// `h` of that face, and GJK then stops at its own intersection tolerance
812/// (`|v| ≤ 2⁻³²`).
813fn closest_on_tetrahedron(a: Vertex, b: Vertex, c: Vertex, d: Vertex, k: u32) -> Option<Nearest> {
814    let faces = [(a, b, c, d), (a, c, d, b), (a, d, b, c), (b, d, c, a)];
815    let flat_height = if k >= 32 {
816        Fix128::from_raw(1i64 << (k - 32), 0)
817    } else {
818        Fix128::from_raw(0, 1u64 << (32 + k))
819    };
820    let flat = faces.iter().any(|&(p, q, r, opposite)| {
821        let n = (q.w - p.w).cross(r.w - p.w);
822        n.dot(opposite.w - p.w).abs() <= n.length() * flat_height
823    });
824    let mut best: Option<Nearest> = None;
825    let mut inside = true;
826    for (p, q, r, opposite) in faces {
827        let n = (q.w - p.w).cross(r.w - p.w);
828        let origin_side = n.dot(-p.w);
829        let other_side = n.dot(opposite.w - p.w);
830        // The origin is outside this face when it is strictly on the other side
831        // from the fourth vertex.
832        let outside = other_side.is_zero()
833            || (!origin_side.is_zero() && origin_side.is_negative() != other_side.is_negative());
834        if outside {
835            inside = false;
836        }
837        if outside || flat {
838            let cand = closest_on_triangle(p, q, r);
839            if best
840                .as_ref()
841                .is_none_or(|b| shorter(nearest_point(&cand), nearest_point(b)))
842            {
843                best = Some(cand);
844            }
845        }
846    }
847    if inside && !flat {
848        None
849    } else {
850        best
851    }
852}
853
854/// Whether `x` is shorter than `y`. Squared lengths keep multiples of `2⁻⁶⁴`
855/// only, so two candidate points about `2⁻³²` from the origin (GJK's last
856/// steps) differ by less than one unit there and the first was kept whichever
857/// was nearer: below `2⁻¹⁶` the lengths are compared on the vectors scaled up
858/// ([`fine_length`]). Longer vectors are compared by their squared lengths as
859/// before.
860fn shorter(x: Vec3Fix, y: Vec3Fix) -> bool {
861    let (xx, yy) = (x.length_squared(), y.length_squared());
862    if xx >= FINE_LENGTH_SQUARED && yy >= FINE_LENGTH_SQUARED {
863        return xx < yy;
864    }
865    let len = |v: Vec3Fix| fine_length(v).unwrap_or(Fix128::ZERO);
866    len(x) < len(y)
867}
868
869/// The point of a [`Nearest`] by its weights.
870fn nearest_point(n: &Nearest) -> Vec3Fix {
871    n.0.iter()
872        .zip(&n.1)
873        .fold(Vec3Fix::ZERO, |acc, (v, &l)| acc + v.w * l)
874}
875
876/// The point of the line or plane through the sub-simplex `s` (1 to 3 vertices,
877/// the one [`closest_on_simplex`] chose) nearest the origin, by projection: when
878/// the simplex is near the origin its barycentric combination cancels terms of
879/// the size of its vertices and keeps few significant bits, while `a − (a·ê)ê`
880/// and `(a·n̂)n̂` lose only rounding of that size.
881fn nearest_on_affine_hull(s: &[Vertex]) -> Vec3Fix {
882    match *s {
883        [a] => a.w,
884        [a, b] => {
885            let e = scaled_up(b.w - a.w).0;
886            let ee = e.length_squared();
887            if ee.is_zero() {
888                return a.w;
889            }
890            a.w - e * (a.w.dot(e) / ee)
891        }
892        [a, b, c, ..] => {
893            let n = scaled_up((b.w - a.w).cross(c.w - a.w)).0;
894            let nn = n.length_squared();
895            if nn.is_zero() {
896                return a.w;
897            }
898            n * (a.w.dot(n) / nn)
899        }
900        [] => Vec3Fix::ZERO,
901    }
902}
903
904fn weighted(s: &(Vec<Vertex>, Vec<Fix128>)) -> Vec3Fix {
905    s.0.iter()
906        .zip(&s.1)
907        .fold(Vec3Fix::ZERO, |acc, (v, &l)| acc + v.w * l)
908}
909
910/// The distance between two convex sets by GJK: `(distance, point on a, point on
911/// b)`, or `None` when they intersect (closer than `2⁻³²`).
912///
913/// # A face whose barycentric weights are not precise
914///
915/// Near a curved surface the simplex is a long thin triangle of support points
916/// (see [`barycentric_is_precise`]); its weights come from signed areas
917/// ([`closest_on_triangle_by_area`]). `v` is then its projection on the
918/// face's plane ([`nearest_on_affine_hull`]), both as the next search
919/// direction and as the distance (`u = 2⁻⁶⁴`, `L` its longest edge, `P` its
920/// farthest vertex, `N` twice its area, `D` the distance; measured on the
921/// faces of the casts in `tests/analytic_world_query_curved_casts.rs` and
922/// `tests/analytic_world_query_precision.rs` against the nearest point in
923/// exact rational arithmetic): the plane's normal is off by less than
924/// `2√3·u / N` rad (each component of the cross product of two exact edges is
925/// truncated by less than `2u`), `9·10⁻¹¹` rad or less on those faces, and the
926/// projected length by up to `2√3·P·u / N` either way, within `10⁻⁶·2⁻³²` on
927/// them. The point within the face, from the area weights, is off along the
928/// face by `δ < 60·L·u / N`, so its direction by `δ / D`, which grows as `D`
929/// drops: `9·10⁻⁷` rad at `D ≈ 1.3·2⁻³²`, where support points along it
930/// repeated and GJK stopped at `1.3·2⁻³²` from a segment that reached
931/// `2.7·2⁻³²` into an ellipsoid. Where the projected length's error could
932/// matter (a time of impact near its tolerance), the lower bound below and
933/// an exact distance decide (see [`toi_convex_refined`]).
934///
935/// The witness `pa − pb` is `v`.
936///
937/// # A lower bound
938///
939/// [`gjk_bounds`] also returns `lower`, the largest `v̂·w` over the
940/// iterations (`w` the support point toward `−v`), capped by the distance. In
941/// exact arithmetic every point `x` of the Minkowski difference has
942/// `v̂·x ≥ v̂·w`, since `w` minimises `v̂·x` over it, so
943///
944/// `dist ≥ v̂·w`
945///
946/// for every `v`, whatever GJK did afterwards (a simplex that stops on a face
947/// without enclosing the origin leaves the distance it reports above the true
948/// one, not this). Only the support points can lift `v̂·w` above it: a
949/// computed support point `w̃` that lies `e` farther along `v̂` than the true
950/// minimiser gives `v̂·w̃ ≤ dist + e`, so `lower − e ≤ dist` with `e` the
951/// support's error toward the core. For a posed cylinder or cone it is the rim
952/// point taken on the axis for a direction within `2⁻⁴²` of it
953/// (`Vec3Fix::rescaled_xz`: lower by `radius·√2·2⁻⁴²` along it) and a few
954/// `2⁻⁶⁴` of rounding per unit of coordinate, `e ≤ radius·2⁻⁴¹ + 2⁻⁵⁶`
955/// ([`round_support_slack`]), which [`convex_bounds`] subtracts; the products
956/// `u·w / |u|` add a few `2⁻⁶⁴`, inside that. The test
957/// `the_lower_bound_is_not_above_the_distance` checks `lower ≤ dist` against an
958/// independent `f64` distance.
959///
960/// # Stopping
961///
962/// The loop stops as before: at `|v| ≤ 2⁻³²` (intersecting), when the support
963/// point gains less than `2⁻⁴⁰` of `|v|²`, repeats a vertex, or the new nearest
964/// point is no nearer (`v` compared as before: its length, on the scaled
965/// vector below `2⁻¹⁶`). A flat tetrahedron counts as intersecting exactly when
966/// the origin is within `2⁻³²` of one of its faces ([`closest_on_tetrahedron`]),
967/// and candidate points are compared by [`shorter`]. Callers:
968/// [`convex_dist`] (casts, overlaps of spheres and capsules), [`segment_segment`],
969/// [`cell_dist`] (height-field cells) and the box overlaps of
970/// [`Piece::meets_aabb`] (a capsule's against its radius, the others only
971/// `None`, an intersection).
972fn gjk_distance<A: Support, B: Support>(a: &A, b: &B) -> Option<(Fix128, Vec3Fix, Vec3Fix)> {
973    gjk_bounds(a, b).map(|g| (g.dist, g.pa, g.pb))
974}
975
976/// The result of [`gjk_bounds`].
977struct GjkBounds {
978    /// The distance, as [`gjk_distance`] reports it.
979    dist: Fix128,
980    /// The witness points on `a` and `b`.
981    pa: Vec3Fix,
982    pb: Vec3Fix,
983    /// A lower bound on the distance (see [`gjk_distance`] § A lower bound).
984    lower: Fix128,
985    /// Whether the distance was taken on a face whose weights are not
986    /// precise.
987    face: bool,
988}
989
990/// [`gjk_distance`] with a lower bound on the distance.
991fn gjk_bounds<A: Support, B: Support>(a: &A, b: &B) -> Option<GjkBounds> {
992    let mut simplex = vec![vertex(a, b, Vec3Fix::UNIT_X)];
993    let mut weights = vec![Fix128::ONE];
994    let mut v = simplex[0].w;
995    // `v` was projected on a face whose weights are not precise (see the doc
996    // above).
997    let mut on_face = false;
998    // The largest `v̂·w` seen (see § A lower bound).
999    let mut lower = Fix128::ZERO;
1000    for _ in 0..GJK_MAX_ITERATIONS {
1001        let vv = v.length_squared();
1002        if vv <= GJK_INTERSECT {
1003            return None;
1004        }
1005        // A short `v` is compared through `u`, `v` scaled up: `|v|² − v·w ≤
1006        // |v|²·ε` is `u·(v − w) ≤ (u·v)·ε`, whose terms keep their precision
1007        // where `|v|²` is a few multiples of `2⁻⁶⁴`.
1008        let fine = vv < FINE_LENGTH_SQUARED;
1009        let u = if fine { scaled_up(v).0 } else { v };
1010        let w = vertex(a, b, -u);
1011        if let Some(len) = fine_length(u) {
1012            let support = u.dot(w.w) / len;
1013            if support > lower {
1014                lower = support;
1015            }
1016        }
1017        let converged = if fine {
1018            u.dot(v - w.w) <= u.dot(v) * GJK_RELATIVE
1019        } else {
1020            vv - v.dot(w.w) <= vv * GJK_RELATIVE
1021        };
1022        if converged {
1023            break;
1024        }
1025        if simplex.iter().any(|s| s.w == w.w) {
1026            break;
1027        }
1028        let mut grown = simplex.clone();
1029        grown.push(w);
1030        let (sub, lambda, face) = closest_on_simplex(&grown)?;
1031        let next = if fine || face {
1032            nearest_on_affine_hull(&sub)
1033        } else {
1034            weighted(&(sub.clone(), lambda.clone()))
1035        };
1036        let no_progress = if fine {
1037            // Both short: compare the lengths measured on the scaled vectors.
1038            let len = |x: Vec3Fix| fine_length(x).unwrap_or(Fix128::ZERO);
1039            len(next) >= len(v)
1040        } else {
1041            next.length_squared() >= vv
1042        };
1043        if no_progress {
1044            // No progress (rounding): keep the previous simplex.
1045            break;
1046        }
1047        on_face = face;
1048        simplex = sub;
1049        weights = lambda;
1050        v = next;
1051    }
1052    let pa = simplex
1053        .iter()
1054        .zip(&weights)
1055        .fold(Vec3Fix::ZERO, |acc, (s, &l)| acc + s.a * l);
1056    let pb = simplex
1057        .iter()
1058        .zip(&weights)
1059        .fold(Vec3Fix::ZERO, |acc, (s, &l)| acc + s.b * l);
1060    let len = fine_length(v)?;
1061    if on_face || v.length_squared() < FINE_LENGTH_SQUARED {
1062        // `v` is projected (see `nearest_on_affine_hull`), the weights are not
1063        // as precise: the witness on `a` is taken as `pb + v`, so that `pa − pb`
1064        // is `v` exactly.
1065        return Some(GjkBounds {
1066            dist: len,
1067            pa: pb + v,
1068            pb,
1069            lower: min_fix(lower, len),
1070            face: on_face,
1071        });
1072    }
1073    Some(GjkBounds {
1074        dist: len,
1075        pa,
1076        pb,
1077        lower: min_fix(lower, len),
1078        face: false,
1079    })
1080}
1081
1082/// The distance from the core `a`–`b` to a convex set, by GJK.
1083fn convex_dist<S: Support>(a: Vec3Fix, b: Vec3Fix, solid: &S) -> Dist {
1084    convex_bounds(a, b, solid, Fix128::ZERO).0
1085}
1086
1087/// [`convex_dist`] and its [`Bound`], the lower bound less `slack`: how far
1088/// `solid`'s support points can be from its true ones toward the core (see
1089/// [`gjk_distance`] § A lower bound).
1090fn convex_bounds<S: Support>(a: Vec3Fix, b: Vec3Fix, solid: &S, slack: Fix128) -> (Dist, Bound) {
1091    let found = if a == b {
1092        gjk_bounds(&PointSupport(a), solid)
1093    } else {
1094        gjk_bounds(&SegmentSupport(a, b), solid)
1095    };
1096    match found {
1097        None => (
1098            Dist::Inside,
1099            Bound {
1100                lower: Fix128::ZERO,
1101                step_lower: false,
1102            },
1103        ),
1104        Some(g) => (
1105            Dist::Outside {
1106                dist: g.dist,
1107                point: g.pb,
1108                normal: gjk_normal(g.pa - g.pb, g.dist),
1109            },
1110            Bound {
1111                lower: max_fix(g.lower - slack, Fix128::ZERO),
1112                step_lower: g.face,
1113            },
1114        ),
1115    }
1116}
1117
1118/// A lower bound on a piece's distance from the core, for [`toi_by`].
1119#[derive(Clone, Copy)]
1120struct Bound {
1121    /// Not above the distance (see [`gjk_distance`] § A lower bound).
1122    lower: Fix128,
1123    /// Whether the time of impact steps by `lower` rather than by the distance:
1124    /// for a distance taken on a face whose weights are not precise.
1125    step_lower: bool,
1126}
1127
1128impl Bound {
1129    /// For a distance known exactly (a closed form): the distance itself.
1130    fn exact(d: &Dist) -> Self {
1131        let lower = match *d {
1132            Dist::Outside { dist, .. } => dist,
1133            _ => Fix128::ZERO,
1134        };
1135        Self {
1136            lower,
1137            step_lower: false,
1138        }
1139    }
1140}
1141
1142/// The unit normal along `diff` (`pa − pb` of [`gjk_distance`]) of length
1143/// `dist`: `diff / dist`, or for a short `diff` the scaled-up `diff` normalized,
1144/// since a quotient of two lengths near `2⁻³²` is unit only to about `2⁻³³`.
1145fn gjk_normal(diff: Vec3Fix, dist: Fix128) -> Vec3Fix {
1146    if diff.length_squared() >= FINE_LENGTH_SQUARED {
1147        return diff / dist;
1148    }
1149    let s = scaled_up(diff).0;
1150    s / s.length()
1151}
1152
1153/// The distance between two segments `p0`–`p1` and `q0`–`q1`, by GJK on the two
1154/// segments: `(distance, point on p, point on q)`, `None` when they cross.
1155fn segment_segment(
1156    p0: Vec3Fix,
1157    p1: Vec3Fix,
1158    q0: Vec3Fix,
1159    q1: Vec3Fix,
1160) -> Option<(Fix128, Vec3Fix, Vec3Fix)> {
1161    if p0 == p1 && q0 == q1 {
1162        let (_, d) = (p0 - q0).normalize_with_length();
1163        return (!d.is_zero()).then_some((d, p0, q0));
1164    }
1165    if p0 == p1 {
1166        let q = closest_on_segment(q0, q1, p0);
1167        let d = (p0 - q).length();
1168        return (!d.is_zero()).then_some((d, p0, q));
1169    }
1170    if q0 == q1 {
1171        let p = closest_on_segment(p0, p1, q0);
1172        let d = (p - q0).length();
1173        return (!d.is_zero()).then_some((d, p, q0));
1174    }
1175    gjk_distance(&SegmentSupport(p0, p1), &SegmentSupport(q0, q1))
1176}
1177
1178/// The distance from the core `a`–`b` to a solid sphere.
1179fn sphere_dist(a: Vec3Fix, b: Vec3Fix, center: Vec3Fix, radius: Fix128) -> Dist {
1180    let s = closest_on_segment(a, b, center);
1181    let (n, d) = (s - center).normalize_with_length();
1182    if d < radius || d.is_zero() {
1183        return Dist::Inside;
1184    }
1185    Dist::Outside {
1186        dist: d - radius,
1187        point: center + n * radius,
1188        normal: n,
1189    }
1190}
1191
1192/// The distance from the core `a`–`b` to a solid capsule.
1193fn capsule_dist(a: Vec3Fix, b: Vec3Fix, capsule: &Capsule) -> Dist {
1194    match segment_segment(a, b, capsule.a, capsule.b) {
1195        None => Dist::Inside,
1196        Some((d, pa, pb)) => Dist::Outside {
1197            dist: d,
1198            point: pb,
1199            normal: gjk_normal(pa - pb, d),
1200        }
1201        .shrunk(capsule.radius),
1202    }
1203}
1204
1205/// The distance from `p` to a solid box of half-extents `h` about the origin, in
1206/// its frame.
1207fn point_box_local(p: Vec3Fix, h: Vec3Fix) -> Dist {
1208    if p.x.abs() < h.x && p.y.abs() < h.y && p.z.abs() < h.z {
1209        return Dist::Inside;
1210    }
1211    let q = Vec3Fix::new(
1212        clamp(p.x, -h.x, h.x),
1213        clamp(p.y, -h.y, h.y),
1214        clamp(p.z, -h.z, h.z),
1215    );
1216    if q == p {
1217        // On the boundary: touching, with the normal of a face it lies on.
1218        let axis = |v: Fix128, e: Fix128, unit: Vec3Fix| {
1219            (v.abs() == e).then(|| if v.is_negative() { -unit } else { unit })
1220        };
1221        let z = Vec3Fix::new(Fix128::ZERO, Fix128::ZERO, Fix128::ONE);
1222        let normal = axis(p.x, h.x, Vec3Fix::UNIT_X)
1223            .or_else(|| axis(p.y, h.y, Vec3Fix::UNIT_Y))
1224            .or_else(|| axis(p.z, h.z, z))
1225            .unwrap_or(Vec3Fix::UNIT_Y);
1226        return Dist::Outside {
1227            dist: Fix128::ZERO,
1228            point: p,
1229            normal,
1230        };
1231    }
1232    Dist::toward(p, q)
1233}
1234
1235/// The distance from `p` to a solid cylinder along `Y` (radius `radius`, half
1236/// height `hh`), in its frame.
1237fn point_cylinder_local(p: Vec3Fix, radius: Fix128, hh: Fix128) -> Dist {
1238    let (dir, rho) = Vec3Fix::new(p.x, Fix128::ZERO, p.z).normalize_with_length();
1239    let qy = clamp(p.y, -hh, hh);
1240    if rho < radius && p.y.abs() < hh {
1241        return Dist::Inside;
1242    }
1243    let qr = min_fix(rho, radius);
1244    let q = if rho.is_zero() {
1245        Vec3Fix::new(Fix128::ZERO, qy, Fix128::ZERO)
1246    } else {
1247        Vec3Fix::new(dir.x * qr, qy, dir.z * qr)
1248    };
1249    if (q - p).length_squared().is_zero() {
1250        // On the boundary: touching, with the cap or side normal there.
1251        let normal = if p.y.abs() == hh {
1252            if p.y.is_negative() {
1253                -Vec3Fix::UNIT_Y
1254            } else {
1255                Vec3Fix::UNIT_Y
1256            }
1257        } else {
1258            dir
1259        };
1260        return Dist::Outside {
1261            dist: Fix128::ZERO,
1262            point: p,
1263            normal,
1264        };
1265    }
1266    Dist::toward(p, q)
1267}
1268
1269/// The distance from `core` to `point` with a length accurate for a short
1270/// difference ([`fine_length`]), `Inside` when they coincide.
1271fn toward_fine(core: Vec3Fix, point: Vec3Fix) -> Dist {
1272    let diff = core - point;
1273    match fine_length(diff) {
1274        None => Dist::Inside,
1275        Some(dist) => Dist::Outside {
1276            dist,
1277            point,
1278            normal: gjk_normal(diff, dist),
1279        },
1280    }
1281}
1282
1283/// `p`'s part across the `Y` axis turned to length `len` (`0 ≤ len ≤ ρ`, `ρ`
1284/// that part's length): `(x, z)·len/ρ`, or `p`'s own `(x, z)` when `len = ρ`.
1285fn toward_axis(p: Vec3Fix, rho: Fix128, len: Fix128) -> (Fix128, Fix128) {
1286    if len >= rho || rho.is_zero() {
1287        return (p.x, p.z);
1288    }
1289    let k = len / rho;
1290    (p.x * k, p.z * k)
1291}
1292
1293/// The exact distance from `p` to a solid cylinder along `Y` (radius `radius`,
1294/// half height `hh`), in its frame: the nearest point is `p` clamped to the
1295/// cylinder, its part across the axis shortened to `radius` (taken as `p`'s own
1296/// when within it, so no rounding of a length enters), and the length is
1297/// [`fine_length`] of the difference.
1298fn point_cylinder_exact(p: Vec3Fix, radius: Fix128, hh: Fix128) -> Dist {
1299    let rho = fine_length(Vec3Fix::new(p.x, Fix128::ZERO, p.z)).unwrap_or(Fix128::ZERO);
1300    if rho < radius && p.y.abs() < hh {
1301        return Dist::Inside;
1302    }
1303    let (x, z) = toward_axis(p, rho, min_fix(rho, radius));
1304    toward_fine(p, Vec3Fix::new(x, clamp(p.y, -hh, hh), z))
1305}
1306
1307/// The exact distance from `p` to a solid cone along `Y` (apex at `hh`, base of
1308/// `radius` at `−hh`), in its frame. In the half plane `(ρ, y)` of `p` the cone
1309/// is the triangle `(0, −hh)`, `(0, hh)`, `(radius, −hh)`; a point outside it is
1310/// nearest to its side `(0, hh)`–`(radius, −hh)` or its base, whichever is
1311/// nearer, and that point is turned back about the axis.
1312fn point_cone_exact(p: Vec3Fix, radius: Fix128, hh: Fix128) -> Dist {
1313    let rho = fine_length(Vec3Fix::new(p.x, Fix128::ZERO, p.z)).unwrap_or(Fix128::ZERO);
1314    let height = hh.double();
1315    if p.y >= -hh && rho * height <= radius * (hh - p.y) {
1316        return Dist::Inside;
1317    }
1318    let at = |q_rho: Fix128, q_y: Fix128| {
1319        let (x, z) = toward_axis(p, rho, q_rho);
1320        Vec3Fix::new(x, q_y, z)
1321    };
1322    // the base disc
1323    let base = at(min_fix(rho, radius), -hh);
1324    // the side: (0, hh) + s·(radius, −2hh), s clamped to [0, 1]
1325    let len2 = radius * radius + height * height;
1326    let s = clamp(
1327        (rho * radius - (p.y - hh) * height) / len2,
1328        Fix128::ZERO,
1329        Fix128::ONE,
1330    );
1331    let side = at(radius * s, hh - height * s);
1332    if shorter(p - side, p - base) {
1333        toward_fine(p, side)
1334    } else {
1335        toward_fine(p, base)
1336    }
1337}
1338
1339/// How far a posed cylinder's or cone's support point can be from the true one
1340/// toward a direction: its rim point is replaced by the point on the axis for
1341/// a direction within `2⁻⁴²` of the axis (`Vec3Fix::rescaled_xz`), which is
1342/// `radius·√2·2⁻⁴²` lower along it, and the rim point is rounded by a few
1343/// `2⁻⁶⁴` per unit of coordinate (the normalization and the turn, below
1344/// `2⁻⁵⁶` for coordinates up to `2⁴`): `radius·2⁻⁴¹ + 2⁻⁵⁶`.
1345fn round_support_slack(posed: &PosedShape) -> Fix128 {
1346    let radius = match posed.shape {
1347        Shape::Cylinder { radius, .. } | Shape::Cone { radius, .. } => radius.abs(),
1348        _ => Fix128::ZERO,
1349    };
1350    let scaled = |r: Fix128, k: i32| {
1351        let raw = ((r.hi as i128) << 64) | (r.lo as i128);
1352        let s = raw >> k;
1353        Fix128::from_raw((s >> 64) as i64, s as u64)
1354    };
1355    scaled(radius, 41) + Fix128::from_raw(0, 1 << 8)
1356}
1357
1358/// The exact distance from the core `a`–`b` to a posed cylinder or cone, `None`
1359/// for other shapes: the point distance in the solid's frame, minimised along
1360/// the segment by [`segment_min`].
1361fn exact_round_dist(posed: &PosedShape, a: Vec3Fix, b: Vec3Fix) -> Option<Dist> {
1362    let (center, rotation) = posed_frame(posed);
1363    let inv = rotation.conjugate();
1364    let point: fn(Vec3Fix, Fix128, Fix128) -> Dist = match posed.shape {
1365        Shape::Cylinder { .. } => point_cylinder_exact,
1366        Shape::Cone { .. } => point_cone_exact,
1367        _ => return None,
1368    };
1369    let (radius, hh) = match posed.shape {
1370        Shape::Cylinder {
1371            radius,
1372            half_height,
1373        }
1374        | Shape::Cone {
1375            radius,
1376            half_height,
1377        } => (radius, half_height),
1378        _ => return None,
1379    };
1380    // `rotation` is unit only to `2⁻³²` in its squared norm `n²`, and the
1381    // support mappings place the solid at `center + q v q*` = `n²·R v`: the
1382    // solid's own frame is `q* (x − center) q / n⁴` and its lengths are `n²`
1383    // times the world's (a turn from `from_axis_angle` has `n²` within about
1384    // `2⁻⁴⁶` of 1, which moved a point on a cap by `2⁻¹⁴·2⁻³²`).
1385    let n2 = rotation.length_squared();
1386    let n4 = n2 * n2;
1387    let f = |q: Vec3Fix| match point(inv.rotate_vec(q - center) / n4, radius, hh) {
1388        Dist::Outside {
1389            dist,
1390            point,
1391            normal,
1392        } => Dist::Outside {
1393            dist: dist * n2,
1394            point: center + rotation.rotate_vec(point),
1395            normal: rotation.rotate_vec(normal) / n2,
1396        },
1397        other => other,
1398    };
1399    Some(segment_min(a, b, f))
1400}
1401
1402/// The distance from `p` to a solid torus (ring `major` in `XZ`, tube `minor`), in
1403/// its frame.
1404fn point_torus_local(p: Vec3Fix, major: Fix128, minor: Fix128) -> Dist {
1405    if torus_distance(p, major, minor).is_negative() {
1406        return Dist::Inside;
1407    }
1408    let (ring_dir, rho) = Vec3Fix::new(p.x, Fix128::ZERO, p.z).normalize_with_length();
1409    let ring_dir = if rho.is_zero() {
1410        Vec3Fix::UNIT_X
1411    } else {
1412        ring_dir
1413    };
1414    let ring = ring_dir * major;
1415    match Dist::toward(p, ring) {
1416        Dist::Outside { dist, normal, .. } => Dist::Outside {
1417            dist,
1418            point: ring,
1419            normal,
1420        }
1421        .shrunk(minor),
1422        other => other,
1423    }
1424}
1425
1426/// A distance in a solid's frame turned back into the world.
1427fn dist_to_world(d: Dist, center: Vec3Fix, rotation: QuatFix) -> Dist {
1428    match d {
1429        Dist::Outside {
1430            dist,
1431            point,
1432            normal,
1433        } => Dist::Outside {
1434            dist,
1435            point: center + rotation.rotate_vec(point),
1436            normal: rotation.rotate_vec(normal),
1437        },
1438        other => other,
1439    }
1440}
1441
1442/// The minimum over `s ∈ [0, 1]` of an `L`-Lipschitz distance `f(s)`:
1443/// branch-and-bound to [`CURVE_COARSE`], then golden-section refinement about
1444/// the best node. `f` may report [`Dist::AtLeast`] (a lower bound).
1445fn curve_min(lipschitz: Fix128, f: impl Fn(Fix128) -> Dist) -> Dist {
1446    const SEEDS: i64 = 16;
1447    // (s, half width of its node, distance)
1448    let mut best: Option<(Fix128, Fix128, Dist)> = None;
1449    // Open nodes: (midpoint, half width, lower bound).
1450    let mut stack: Vec<(Fix128, Fix128, Fix128)> = Vec::new();
1451    let seed_half = Fix128::from_ratio(1, 2 * SEEDS);
1452    let mut nodes = 0usize;
1453    let mut todo: Vec<(Fix128, Fix128)> = (0..SEEDS)
1454        .map(|k| (Fix128::from_ratio(2 * k + 1, 2 * SEEDS), seed_half))
1455        .collect();
1456    loop {
1457        for (m, hw) in todo.drain(..) {
1458            let d = f(m);
1459            if matches!(d, Dist::Inside) {
1460                return Dist::Inside;
1461            }
1462            if best.as_ref().is_none_or(|(_, _, b)| d.value() < b.value()) {
1463                best = Some((m, hw, d));
1464            }
1465            // A node's lower bound: the Lipschitz cone below its midpoint value.
1466            stack.push((m, hw, d.value() - lipschitz * hw));
1467        }
1468        let best_value = best.as_ref().map_or(Fix128::ZERO, |(_, _, b)| b.value());
1469        // Drop the nodes that cannot hold a value below the best by more than the
1470        // coarse tolerance, and the ones already narrow enough.
1471        stack.retain(|&(_, hw, bound)| {
1472            bound < best_value - CURVE_COARSE && lipschitz * hw > CURVE_COARSE
1473        });
1474        // Best first: the lowest bound (the first such node on a tie).
1475        let Some(k) = (0..stack.len()).min_by_key(|&k| stack[k].2) else {
1476            break;
1477        };
1478        nodes += 1;
1479        if nodes > CURVE_MAX_NODES {
1480            break;
1481        }
1482        let (mid, hw, _) = stack.remove(k);
1483        let quarter = hw.half();
1484        todo.push((mid - quarter, quarter));
1485        todo.push((mid + quarter, quarter));
1486    }
1487    let Some((s_best, hw_best, d_best)) = best else {
1488        return Dist::AtLeast(Fix128::ZERO);
1489    };
1490    // Golden-section over the best node and its neighbours.
1491    let mut lo = max_fix(Fix128::ZERO, s_best - hw_best.double());
1492    let mut hi = min_fix(Fix128::ONE, s_best + hw_best.double());
1493    let ratio = Fix128::from_ratio(618_033_988_749_895, 1_000_000_000_000_000);
1494    let mut x1 = hi - (hi - lo) * ratio;
1495    let mut x2 = lo + (hi - lo) * ratio;
1496    let mut f1 = f(x1);
1497    let mut f2 = f(x2);
1498    let mut refined = d_best;
1499    for _ in 0..GOLDEN_STEPS {
1500        for d in [f1, f2] {
1501            if matches!(d, Dist::Inside) {
1502                return Dist::Inside;
1503            }
1504            if d.value() < refined.value() {
1505                refined = d;
1506            }
1507        }
1508        if f1.value() <= f2.value() {
1509            hi = x2;
1510            x2 = x1;
1511            f2 = f1;
1512            x1 = hi - (hi - lo) * ratio;
1513            f1 = f(x1);
1514        } else {
1515            lo = x1;
1516            x1 = x2;
1517            f1 = f2;
1518            x2 = lo + (hi - lo) * ratio;
1519            f2 = f(x2);
1520        }
1521    }
1522    refined
1523}
1524
1525/// The distance from the segment `a`–`b` to a geometry given by its point
1526/// distance `f`.
1527fn segment_min(a: Vec3Fix, b: Vec3Fix, f: impl Fn(Vec3Fix) -> Dist) -> Dist {
1528    if a == b {
1529        return f(a);
1530    }
1531    let ab = b - a;
1532    curve_min(ab.length(), |s| f(a + ab * s))
1533}
1534
1535// ============================================================================
1536// Height fields
1537// ============================================================================
1538
1539/// Whether the field has a surface (see [`crate::shape_raycast`]).
1540fn field_has_surface(field: &HeightField) -> bool {
1541    field.width >= 2 && field.depth >= 2 && field.spacing > Fix128::ZERO
1542}
1543
1544/// The cell range `[lo, hi]` (clamped to the grid) covering `[x0, x1]` along an
1545/// axis with `n` points.
1546fn cell_range(
1547    x0: Fix128,
1548    x1: Fix128,
1549    origin: Fix128,
1550    spacing: Fix128,
1551    n: u32,
1552) -> Option<(u32, u32)> {
1553    let last = i64::from(n) - 2;
1554    let lo = ((x0 - origin) / spacing).floor().hi;
1555    let hi = ((x1 - origin) / spacing).floor().hi;
1556    if hi < 0 || lo > last {
1557        return None;
1558    }
1559    Some((lo.clamp(0, last) as u32, hi.clamp(0, last) as u32))
1560}
1561
1562/// The world `(x, z)` of cell `(gx, gz)`'s first corner.
1563fn cell_origin(field: &HeightField, gx: u32, gz: u32) -> (Fix128, Fix128) {
1564    (
1565        field.origin.x + field.spacing * Fix128::from_int(i64::from(gx)),
1566        field.origin.z + field.spacing * Fix128::from_int(i64::from(gz)),
1567    )
1568}
1569
1570/// The four corner heights of cell `(gx, gz)`: `(h00, h10, h01, h11)`.
1571fn cell_heights(field: &HeightField, gx: u32, gz: u32) -> [Fix128; 4] {
1572    [
1573        field.get_height(gx, gz),
1574        field.get_height(gx + 1, gz),
1575        field.get_height(gx, gz + 1),
1576        field.get_height(gx + 1, gz + 1),
1577    ]
1578}
1579
1580/// Four points, for GJK: their convex hull. The control points of a bilinear
1581/// patch, whose hull contains the patch.
1582struct QuadSupport([Vec3Fix; 4]);
1583
1584impl Support for QuadSupport {
1585    fn support(&self, direction: Vec3Fix) -> Vec3Fix {
1586        let mut best = self.0[0];
1587        let mut best_dot = best.dot(direction);
1588        for &p in &self.0[1..] {
1589            let d = p.dot(direction);
1590            if d > best_dot {
1591                best = p;
1592                best_dot = d;
1593            }
1594        }
1595        best
1596    }
1597}
1598
1599/// One bilinear height-field cell: first corner `(x0, z0)`, spacing `s`, corner
1600/// heights `(h00, h10, h01, h11)`.
1601#[derive(Clone, Copy)]
1602struct Cell {
1603    x0: Fix128,
1604    z0: Fix128,
1605    s: Fix128,
1606    h: [Fix128; 4],
1607}
1608
1609impl Cell {
1610    fn new(field: &HeightField, gx: u32, gz: u32) -> Self {
1611        let (x0, z0) = cell_origin(field, gx, gz);
1612        Self {
1613            x0,
1614            z0,
1615            s: field.spacing,
1616            h: cell_heights(field, gx, gz),
1617        }
1618    }
1619
1620    /// The bilinear height at `(u, v) ∈ [0, 1]²`.
1621    fn height(&self, u: Fix128, v: Fix128) -> Fix128 {
1622        let [h00, h10, h01, h11] = self.h;
1623        let one = Fix128::ONE;
1624        h00 * (one - u) * (one - v) + h10 * u * (one - v) + h01 * (one - u) * v + h11 * u * v
1625    }
1626
1627    fn point(&self, u: Fix128, v: Fix128) -> Vec3Fix {
1628        Vec3Fix::new(
1629            self.x0 + self.s * u,
1630            self.height(u, v),
1631            self.z0 + self.s * v,
1632        )
1633    }
1634
1635    /// `h00 − h10 − h01 + h11`: zero for a planar cell.
1636    fn twist(&self) -> Fix128 {
1637        let [h00, h10, h01, h11] = self.h;
1638        h00 - h10 - h01 + h11
1639    }
1640
1641    /// The unit upward normal of the surface at `(u, v)`.
1642    fn normal(&self, u: Fix128, v: Fix128) -> Vec3Fix {
1643        let [h00, h10, h01, h11] = self.h;
1644        let one = Fix128::ONE;
1645        let hu = (h10 - h00) * (one - v) + (h11 - h01) * v;
1646        let hv = (h01 - h00) * (one - u) + (h11 - h10) * u;
1647        Vec3Fix::new(-hu, self.s, -hv).normalize()
1648    }
1649
1650    /// The `(u, v)` of the world `(x, z)` of `p`, clamped to `[u0, u1] × [v0, v1]`.
1651    fn uv(&self, p: Vec3Fix, part: &Patch) -> (Fix128, Fix128) {
1652        (
1653            clamp((p.x - self.x0) / self.s, part.u0, part.u1),
1654            clamp((p.z - self.z0) / self.s, part.v0, part.v1),
1655        )
1656    }
1657}
1658
1659/// The part `u ∈ [u0, u1]`, `v ∈ [v0, v1]` of a [`Cell`]. A bilinear patch
1660/// restricted to a rectangle of `(u, v)` is again bilinear, with its four corners
1661/// as control points, so it lies inside the convex hull of those corners.
1662#[derive(Clone, Copy)]
1663struct Patch {
1664    u0: Fix128,
1665    u1: Fix128,
1666    v0: Fix128,
1667    v1: Fix128,
1668}
1669
1670impl Patch {
1671    const WHOLE: Self = Self {
1672        u0: Fix128::ZERO,
1673        u1: Fix128::ONE,
1674        v0: Fix128::ZERO,
1675        v1: Fix128::ONE,
1676    };
1677
1678    fn hull(&self, cell: &Cell) -> QuadSupport {
1679        QuadSupport([
1680            cell.point(self.u0, self.v0),
1681            cell.point(self.u1, self.v0),
1682            cell.point(self.u0, self.v1),
1683            cell.point(self.u1, self.v1),
1684        ])
1685    }
1686
1687    /// A bound on how far a point of the hull is from the patch: the hull lies
1688    /// between the two triangulations of the corners, which differ from the
1689    /// bilinear surface by at most a quarter of the part's twist
1690    /// `|k|·(u1 − u0)·(v1 − v0)` along `Y`.
1691    fn thickness(&self, cell: &Cell) -> Fix128 {
1692        (cell.twist().abs() * (self.u1 - self.u0) * (self.v1 - self.v0))
1693            .half()
1694            .half()
1695    }
1696
1697    fn split(&self) -> [Self; 4] {
1698        let um = (self.u0 + self.u1).half();
1699        let vm = (self.v0 + self.v1).half();
1700        [
1701            Self {
1702                u1: um,
1703                v1: vm,
1704                ..*self
1705            },
1706            Self {
1707                u0: um,
1708                v1: vm,
1709                ..*self
1710            },
1711            Self {
1712                u1: um,
1713                v0: vm,
1714                ..*self
1715            },
1716            Self {
1717                u0: um,
1718                v0: vm,
1719                ..*self
1720            },
1721        ]
1722    }
1723
1724    /// The surface point of this part over the `(x, z)` of `p`.
1725    fn surface_under(&self, cell: &Cell, p: Vec3Fix) -> Vec3Fix {
1726        let (u, v) = cell.uv(p, self);
1727        cell.point(u, v)
1728    }
1729}
1730
1731/// The distance from the core `a`–`b` to one cell and the nearest surface point
1732/// found, by branch-and-bound over parts of the cell: the distance to a part's
1733/// hull is a lower bound for the part, the distance to the surface point under
1734/// the hull's nearest point an upper bound, and parts are split until the two
1735/// meet within [`TRACE_TOLERANCE`]. The result is never above the true distance
1736/// by more than that tolerance; if the node budget runs out it is the lowest
1737/// open lower bound (below the true distance).
1738fn cell_dist(cell: &Cell, a: Vec3Fix, b: Vec3Fix) -> (Fix128, Vec3Fix) {
1739    let to_core = |q: Vec3Fix| (closest_on_segment(a, b, q) - q).length();
1740    let eval = |part: &Patch| {
1741        let hull = part.hull(cell);
1742        let found = if a == b {
1743            gjk_distance(&PointSupport(a), &hull)
1744        } else {
1745            gjk_distance(&SegmentSupport(a, b), &hull)
1746        };
1747        let (lower, near) = match found {
1748            Some((d, _, pb)) => (d, pb),
1749            None => (
1750                Fix128::ZERO,
1751                cell.point((part.u0 + part.u1).half(), (part.v0 + part.v1).half()),
1752            ),
1753        };
1754        let q = part.surface_under(cell, near);
1755        (lower, q, to_core(q))
1756    };
1757    let (lower, q, upper) = eval(&Patch::WHOLE);
1758    let mut best = (upper, q);
1759    let mut open = vec![(lower, Patch::WHOLE)];
1760    let mut nodes = 0usize;
1761    while let Some(k) = (0..open.len()).min_by_key(|&k| open[k].0) {
1762        let (lower, part) = open.remove(k);
1763        // Every open part is at least `lower` away: nothing can beat the best by
1764        // more than the tolerance.
1765        if lower + TRACE_TOLERANCE >= best.0 {
1766            break;
1767        }
1768        nodes += 1;
1769        if nodes > CELL_MAX_NODES {
1770            return (lower, best.1);
1771        }
1772        for child in part.split() {
1773            let (lower, q, upper) = eval(&child);
1774            if upper < best.0 {
1775                best = (upper, q);
1776            }
1777            // A part thinner than the tolerance has its upper bound within the
1778            // tolerance of its lower bound, so it is settled by `best`.
1779            if lower + TRACE_TOLERANCE < best.0 && child.thickness(cell) > TRACE_TOLERANCE {
1780                open.push((lower, child));
1781            }
1782        }
1783    }
1784    best
1785}
1786
1787/// The distance from the core `a`–`b` to the height-field surface, searching the
1788/// cells within `cap` of it ([`Dist::AtLeast`] beyond). A core within
1789/// [`TRACE_TOLERANCE`] of the surface is touching: a point gets distance `0` and
1790/// the surface normal on its side, a segment is [`Dist::Inside`] (it meets the
1791/// surface).
1792fn heightfield_dist(field: &HeightField, a: Vec3Fix, b: Vec3Fix, cap: Fix128) -> Dist {
1793    if !field_has_surface(field) {
1794        return Dist::AtLeast(cap);
1795    }
1796    let core = core_box(a, b, Fix128::ZERO);
1797    let far = aabb_gap(&core, &field.aabb());
1798    if far >= cap {
1799        return Dist::AtLeast(far);
1800    }
1801    let reach = core_box(a, b, cap);
1802    let s = field.spacing;
1803    let (Some((gx0, gx1)), Some((gz0, gz1))) = (
1804        cell_range(reach.min.x, reach.max.x, field.origin.x, s, field.width),
1805        cell_range(reach.min.z, reach.max.z, field.origin.z, s, field.depth),
1806    ) else {
1807        return Dist::AtLeast(cap);
1808    };
1809    // The cells by their box's distance from the core, nearest first: once a
1810    // cell's box is farther than the best distance found, no later cell is nearer.
1811    let mut cells: Vec<(Fix128, u32, u32)> = Vec::new();
1812    for gz in gz0..=gz1 {
1813        for gx in gx0..=gx1 {
1814            let lower = aabb_gap(&core, &cell_box(field, gx, gz));
1815            if lower < cap {
1816                cells.push((lower, gz, gx));
1817            }
1818        }
1819    }
1820    cells.sort_unstable();
1821    let mut best: Option<(Fix128, Vec3Fix, Cell)> = None;
1822    for (lower, gz, gx) in cells {
1823        if best.is_some_and(|(d, _, _)| lower >= d) {
1824            break;
1825        }
1826        let cell = Cell::new(field, gx, gz);
1827        let (d, q) = cell_dist(&cell, a, b);
1828        if best.is_none_or(|(bd, _, _)| d < bd) {
1829            best = Some((d, q, cell));
1830        }
1831    }
1832    let Some((d, q, cell)) = best.filter(|&(d, _, _)| d < cap) else {
1833        return Dist::AtLeast(cap);
1834    };
1835    let core_point = closest_on_segment(a, b, q);
1836    if d <= TRACE_TOLERANCE {
1837        if a != b {
1838            return Dist::Inside;
1839        }
1840        let (u, v) = cell.uv(q, &Patch::WHOLE);
1841        let n = cell.normal(u, v);
1842        let side = if (a - q).dot(n).is_negative() { -n } else { n };
1843        return Dist::Outside {
1844            dist: d,
1845            point: q,
1846            normal: side,
1847        };
1848    }
1849    let normal = match (core_point - q).try_normalize() {
1850        Some(n) => n,
1851        None => return Dist::Inside,
1852    };
1853    Dist::Outside {
1854        dist: d,
1855        point: q,
1856        normal,
1857    }
1858}
1859
1860/// The box of cell `(gx, gz)`: its `XZ` square and the range of its corner
1861/// heights (the bilinear surface stays within them).
1862fn cell_box(field: &HeightField, gx: u32, gz: u32) -> AABB {
1863    let h = cell_heights(field, gx, gz);
1864    let (x0, z0) = cell_origin(field, gx, gz);
1865    let s = field.spacing;
1866    let lo = min_fix(min_fix(h[0], h[1]), min_fix(h[2], h[3]));
1867    let hi = max_fix(max_fix(h[0], h[1]), max_fix(h[2], h[3]));
1868    AABB::new(Vec3Fix::new(x0, lo, z0), Vec3Fix::new(x0 + s, hi, z0 + s))
1869}
1870
1871/// The first time the core `a`–`b` grown by `r`, moving along the unit `d`,
1872/// touches one cell: against the hull of the whole cell when it is planar (the
1873/// hull is the cell), otherwise by [`toi_tree`] over its parts, which accepts a
1874/// part once its hull is within [`TRACE_TOLERANCE`] of its surface.
1875fn cell_toi(
1876    cell: &Cell,
1877    a: Vec3Fix,
1878    b: Vec3Fix,
1879    r: Fix128,
1880    d: Vec3Fix,
1881    max_t: Fix128,
1882) -> Option<Contact> {
1883    let toi = |part: &Patch| toi_convex(a, b, r, Fix128::ZERO, d, max_t, &part.hull(cell));
1884    if cell.twist().is_zero() {
1885        return toi(&Patch::WHOLE);
1886    }
1887    let found = toi_tree(
1888        &[Patch::WHOLE],
1889        toi,
1890        |part| part.thickness(cell) <= TRACE_TOLERANCE,
1891        |part| part.split().to_vec(),
1892    )?;
1893    let off = d * found.t;
1894    Some(refine_on_cell(cell, a + off, b + off, found))
1895}
1896
1897/// A contact found on a part's hull moved onto the surface: Newton's method on
1898/// the squared distance from the core to the bilinear surface, started at the
1899/// contact's `(u, v)`. The hull contact is within the part (a few `2⁻¹⁶` of the
1900/// cell), where the distance has a single minimum; the refined point is taken
1901/// only if it is not farther from the core than the hull contact.
1902fn refine_on_cell(cell: &Cell, a: Vec3Fix, b: Vec3Fix, found: Contact) -> Contact {
1903    let [h00, h10, h01, h11] = cell.h;
1904    let k = cell.twist();
1905    let s = cell.s;
1906    let one = Fix128::ONE;
1907    let (mut u, mut v) = cell.uv(found.point, &Patch::WHOLE);
1908    for _ in 0..8 {
1909        let q = cell.point(u, v);
1910        let e = q - closest_on_segment(a, b, q);
1911        let hu = (h10 - h00) * (one - v) + (h11 - h01) * v;
1912        let hv = (h01 - h00) * (one - u) + (h11 - h10) * u;
1913        let pu = Vec3Fix::new(s, hu, Fix128::ZERO);
1914        let pv = Vec3Fix::new(Fix128::ZERO, hv, s);
1915        let (gu, gv) = (e.dot(pu), e.dot(pv));
1916        let (huu, hvv) = (pu.dot(pu), pv.dot(pv));
1917        let huv = pu.dot(pv) + e.y * k;
1918        let det = huu * hvv - huv * huv;
1919        if det <= Fix128::ZERO {
1920            return found;
1921        }
1922        u = clamp(u - (hvv * gu - huv * gv) / det, Fix128::ZERO, one);
1923        v = clamp(v - (huu * gv - huv * gu) / det, Fix128::ZERO, one);
1924    }
1925    let q = cell.point(u, v);
1926    let core = closest_on_segment(a, b, q);
1927    let before = (closest_on_segment(a, b, found.point) - found.point).length();
1928    match (core - q).normalize_with_length() {
1929        (n, dist)
1930            if !dist.is_zero()
1931                && dist <= before + TRACE_TOLERANCE + TRACE_TOLERANCE
1932                && n.dot(found.normal) > Fix128::ZERO =>
1933        {
1934            Contact {
1935                t: found.t,
1936                point: q,
1937                normal: n,
1938            }
1939        }
1940        _ => found,
1941    }
1942}
1943
1944/// The first contact of the core `a`–`b` grown by `r` moving along the unit `d`
1945/// with a height field: every cell its swept box crosses, in order of the time
1946/// the moving box first reaches the cell's box, stopping once that time is past
1947/// the best contact.
1948fn sweep_heightfield(
1949    field: &HeightField,
1950    a: Vec3Fix,
1951    b: Vec3Fix,
1952    r: Fix128,
1953    d: Vec3Fix,
1954    max_t: Fix128,
1955) -> Option<Contact> {
1956    if !field_has_surface(field) {
1957        return None;
1958    }
1959    let start = core_box(a, b, r);
1960    let swept = swept_box(a, b, r, d, max_t);
1961    let s = field.spacing;
1962    let (Some((gx0, gx1)), Some((gz0, gz1))) = (
1963        cell_range(swept.min.x, swept.max.x, field.origin.x, s, field.width),
1964        cell_range(swept.min.z, swept.max.z, field.origin.z, s, field.depth),
1965    ) else {
1966        return None;
1967    };
1968    let mut cells: Vec<(Fix128, u32, u32)> = Vec::new();
1969    for gz in gz0..=gz1 {
1970        for gx in gx0..=gx1 {
1971            if let Some(entry) = box_entry(&start, d, max_t, &cell_box(field, gx, gz)) {
1972                cells.push((entry, gz, gx));
1973            }
1974        }
1975    }
1976    cells.sort_unstable();
1977    let mut best: Option<Contact> = None;
1978    for (entry, gz, gx) in cells {
1979        if best.is_some_and(|c| c.t < entry) {
1980            break;
1981        }
1982        if let Some(c) = cell_toi(&Cell::new(field, gx, gz), a, b, r, d, max_t) {
1983            if best.is_none_or(|bc| c.t < bc.t) {
1984                best = Some(c);
1985            }
1986        }
1987    }
1988    best
1989}
1990
1991/// `num / den` limited to `[−big, big]` (no overflow for a tiny `den`).
1992fn ratio_within(num: Fix128, den: Fix128, big: Fix128) -> Fix128 {
1993    if num.abs() >= den.abs() * big {
1994        if num.is_negative() == den.is_negative() {
1995            big
1996        } else {
1997            -big
1998        }
1999    } else {
2000        num / den
2001    }
2002}
2003
2004/// The first `t ∈ [0, max_t]` at which `moving` shifted by `t·d` meets `target`
2005/// (boxes touching count), or `None`.
2006fn box_entry(moving: &AABB, d: Vec3Fix, max_t: Fix128, target: &AABB) -> Option<Fix128> {
2007    let big = max_t + Fix128::ONE;
2008    let mut lo = Fix128::ZERO;
2009    let mut hi = max_t;
2010    for (m0, m1, t0, t1, di) in [
2011        (moving.min.x, moving.max.x, target.min.x, target.max.x, d.x),
2012        (moving.min.y, moving.max.y, target.min.y, target.max.y, d.y),
2013        (moving.min.z, moving.max.z, target.min.z, target.max.z, d.z),
2014    ] {
2015        if di.abs() < PARALLEL_EPSILON {
2016            // Nearly no motion on this axis: at most max_t·|di| either way.
2017            let slack = max_t * di.abs();
2018            if m1 + slack < t0 || m0 - slack > t1 {
2019                return None;
2020            }
2021            continue;
2022        }
2023        let (enter, leave) = if di.is_negative() {
2024            (
2025                ratio_within(t1 - m0, di, big),
2026                ratio_within(t0 - m1, di, big),
2027            )
2028        } else {
2029            (
2030                ratio_within(t0 - m1, di, big),
2031                ratio_within(t1 - m0, di, big),
2032            )
2033        };
2034        lo = max_fix(lo, enter);
2035        hi = min_fix(hi, leave);
2036    }
2037    (lo <= hi).then_some(lo)
2038}
2039
2040/// The box swept by the core `a`–`b` grown by `r` moving along `d` for `max_t`.
2041fn swept_box(a: Vec3Fix, b: Vec3Fix, r: Fix128, d: Vec3Fix, max_t: Fix128) -> AABB {
2042    let start = core_box(a, b, r);
2043    let end = core_box(a + d * max_t, b + d * max_t, r);
2044    AABB::new(
2045        Vec3Fix::new(
2046            min_fix(start.min.x, end.min.x),
2047            min_fix(start.min.y, end.min.y),
2048            min_fix(start.min.z, end.min.z),
2049        ),
2050        Vec3Fix::new(
2051            max_fix(start.max.x, end.max.x),
2052            max_fix(start.max.y, end.max.y),
2053            max_fix(start.max.z, end.max.z),
2054        ),
2055    )
2056}
2057
2058/// The first contact of the core `a`–`b` grown by `r` moving along the unit `d`
2059/// with a triangle mesh: a convex time of impact per triangle the swept box
2060/// meets, in order of the time the moving box reaches the triangle's box. Each
2061/// triangle is convex, so running along one triangle (a floor) is not a step
2062/// budget spent against another (a wall).
2063fn sweep_mesh(
2064    mesh: &TriMesh,
2065    a: Vec3Fix,
2066    b: Vec3Fix,
2067    r: Fix128,
2068    d: Vec3Fix,
2069    max_t: Fix128,
2070) -> Option<Contact> {
2071    let start = core_box(a, b, r);
2072    let mut order: Vec<(Fix128, u32)> = mesh_candidates(mesh, &swept_box(a, b, r, d, max_t))
2073        .into_iter()
2074        .filter_map(|i| {
2075            let tri = &mesh.triangles[i as usize];
2076            let tri_box = AABB::new(
2077                Vec3Fix::new(
2078                    min_fix(min_fix(tri.v0.x, tri.v1.x), tri.v2.x),
2079                    min_fix(min_fix(tri.v0.y, tri.v1.y), tri.v2.y),
2080                    min_fix(min_fix(tri.v0.z, tri.v1.z), tri.v2.z),
2081                ),
2082                Vec3Fix::new(
2083                    max_fix(max_fix(tri.v0.x, tri.v1.x), tri.v2.x),
2084                    max_fix(max_fix(tri.v0.y, tri.v1.y), tri.v2.y),
2085                    max_fix(max_fix(tri.v0.z, tri.v1.z), tri.v2.z),
2086                ),
2087            );
2088            box_entry(&start, d, max_t, &tri_box).map(|entry| (entry, i))
2089        })
2090        .collect();
2091    order.sort_unstable();
2092    let mut best: Option<Contact> = None;
2093    for (entry, i) in order {
2094        if best.is_some_and(|c| c.t < entry) {
2095            break;
2096        }
2097        let tri = &mesh.triangles[i as usize];
2098        if let Some(c) = toi_convex(a, b, r, Fix128::ZERO, d, max_t, &TriangleSupport(tri)) {
2099            if best.is_none_or(|bc| c.t < bc.t) {
2100                best = Some(c);
2101            }
2102        }
2103    }
2104    best
2105}
2106
2107/// The ring of a torus (radius `major` in its local `XZ` plane) and its tube
2108/// radius: the solid torus is the ring grown by `minor`.
2109#[derive(Clone, Copy)]
2110struct Ring {
2111    center: Vec3Fix,
2112    rotation: QuatFix,
2113    major: Fix128,
2114    minor: Fix128,
2115}
2116
2117/// The arc of a [`Ring`] from the unit local direction `e0` counter-clockwise
2118/// (from `+X` toward `+Z`) to `e1`, less than half a turn.
2119#[derive(Clone, Copy)]
2120struct RingArc {
2121    e0: Vec3Fix,
2122    e1: Vec3Fix,
2123}
2124
2125/// `(a × b)·Y` for vectors in the `XZ` plane: positive when `b` is
2126/// counter-clockwise from `a` (from `+X` toward `+Z`).
2127fn turn(a: Vec3Fix, b: Vec3Fix) -> Fix128 {
2128    a.x * b.z - a.z * b.x
2129}
2130
2131impl RingArc {
2132    /// How far the chord is inside the arc (the sagitta): every point of the
2133    /// arc's hull is within it of the arc.
2134    fn thickness(&self, major: Fix128) -> Fix128 {
2135        let half_chord = ((self.e1 - self.e0).length() * major).half();
2136        major - (major * major - half_chord * half_chord).sqrt()
2137    }
2138
2139    fn split(&self) -> [Self; 2] {
2140        let mid = (self.e0 + self.e1).normalize();
2141        [
2142            Self {
2143                e0: self.e0,
2144                e1: mid,
2145            },
2146            Self {
2147                e0: mid,
2148                e1: self.e1,
2149            },
2150        ]
2151    }
2152}
2153
2154/// An arc of a ring placed in the world, for GJK: its convex hull (the circular
2155/// segment).
2156struct ArcSupport {
2157    ring: Ring,
2158    arc: RingArc,
2159}
2160
2161impl Support for ArcSupport {
2162    fn support(&self, direction: Vec3Fix) -> Vec3Fix {
2163        let local = self.ring.rotation.conjugate().rotate_vec(direction);
2164        let flat = Vec3Fix::new(local.x, Fix128::ZERO, local.z);
2165        let (e0, e1) = (self.arc.e0, self.arc.e1);
2166        let e = match flat.try_normalize() {
2167            Some(u) if !turn(e0, u).is_negative() && !turn(u, e1).is_negative() => u,
2168            _ if e1.dot(flat) > e0.dot(flat) => e1,
2169            _ => e0,
2170        };
2171        self.ring.center + self.ring.rotation.rotate_vec(e * self.ring.major)
2172    }
2173}
2174
2175impl Ring {
2176    /// The first contact of the core `a`–`b` grown by `r` with the solid torus:
2177    /// [`toi_tree`] over arcs of the ring, each arc's hull grown by the tube
2178    /// radius (a convex set containing that part of the torus), split until the
2179    /// hull is within [`TRACE_TOLERANCE`] of the arc.
2180    fn sweep(
2181        &self,
2182        a: Vec3Fix,
2183        b: Vec3Fix,
2184        r: Fix128,
2185        d: Vec3Fix,
2186        max_t: Fix128,
2187    ) -> Option<Contact> {
2188        let x = Vec3Fix::UNIT_X;
2189        let z = Vec3Fix::new(Fix128::ZERO, Fix128::ZERO, Fix128::ONE);
2190        let quarters = [
2191            RingArc { e0: x, e1: z },
2192            RingArc { e0: z, e1: -x },
2193            RingArc { e0: -x, e1: -z },
2194            RingArc { e0: -z, e1: x },
2195        ];
2196        toi_tree(
2197            &quarters,
2198            |arc| {
2199                let hull = ArcSupport {
2200                    ring: *self,
2201                    arc: *arc,
2202                };
2203                toi_convex(a, b, r, self.minor, d, max_t, &hull)
2204            },
2205            |arc| arc.thickness(self.major) <= TRACE_TOLERANCE,
2206            |arc| arc.split().to_vec(),
2207        )
2208        .map(|c| self.refine(a + d * c.t, b + d * c.t, c))
2209    }
2210
2211    /// A contact found on an arc's hull moved onto the torus: the ring point
2212    /// nearest the core point nearest the contact, grown by the tube radius
2213    /// (the hull contact is within the arc's sagitta of it).
2214    fn refine(&self, a: Vec3Fix, b: Vec3Fix, found: Contact) -> Contact {
2215        let core = closest_on_segment(a, b, found.point);
2216        let inv = self.rotation.conjugate();
2217        let local = inv.rotate_vec(core - self.center);
2218        let Some(dir) = Vec3Fix::new(local.x, Fix128::ZERO, local.z).try_normalize() else {
2219            return found;
2220        };
2221        let ring = self.center + self.rotation.rotate_vec(dir * self.major);
2222        match (core - ring).try_normalize() {
2223            Some(n) if n.dot(found.normal) > Fix128::ZERO => Contact {
2224                t: found.t,
2225                point: ring + n * self.minor,
2226                normal: n,
2227            },
2228            _ => found,
2229        }
2230    }
2231}
2232
2233// ============================================================================
2234// Time of impact
2235// ============================================================================
2236
2237/// The first time the core `a`–`b` grown by `r`, moving along the unit `d` for at
2238/// most `max_t`, touches a convex `solid` grown by `inflate`.
2239///
2240/// The gap `g(t) = dist(core + t·d, solid) − r − inflate` of two convex sets
2241/// moving apart linearly is convex in `t`, and `g′(t) = d·n` with `n` the unit
2242/// normal from the solid toward the core. Newton's step from below,
2243/// `t + g / (−g′)`, then never passes the first root (the tangent of a convex
2244/// function lies below it) and converges quadratically (linearly at a grazing,
2245/// double root); a gap that is not decreasing (`g′ ≥ 0`) never decreases again,
2246/// which proves there is no contact. A step that lands past the root (the
2247/// distance and normal are computed to within GJK's tolerance) is undone by
2248/// bisection between the last clear time and that time; once the two are within
2249/// the tolerance, the tangent's root from the clear time is the contact (a core
2250/// of reach `0` always ends this way, since it is touching only within GJK's
2251/// intersection tolerance).
2252///
2253/// Returns the contact once the gap is within [`TRACE_TOLERANCE`] and the core
2254/// moves into the solid (see [`touch_is_hit`]): a core touching the solid and
2255/// moving away from it or along it does not hit. A core that overlaps the solid at `t = 0` by
2256/// more than the tolerance hits at `t = 0` with normal `−d`. If the step budget
2257/// runs out (it does not in practice: Newton needs a few dozen steps even at a
2258/// grazing approach), the last clear time is reported, which is never after the
2259/// true contact.
2260fn toi_convex<S: Support>(
2261    a: Vec3Fix,
2262    b: Vec3Fix,
2263    r: Fix128,
2264    inflate: Fix128,
2265    d: Vec3Fix,
2266    max_t: Fix128,
2267    solid: &S,
2268) -> Option<Contact> {
2269    toi_convex_refined(a, b, r, inflate, d, max_t, solid, Fix128::ZERO, |_, _| None)
2270}
2271
2272/// [`toi_convex`] with an exact distance `exact` (`None` where there is none).
2273///
2274/// GJK's distance is an upper bound and its lower bound (see [`gjk_distance`]
2275/// § A lower bound) a lower one; where they straddle a decision of the time of
2276/// impact (touching at `2⁻³²`, going in at `−2⁻³²`, see [`straddles`]) GJK
2277/// cannot tell which side the gap is on. A segment of radius 0 that enters a
2278/// cylinder through its side just below a cap left GJK on a face `2` to
2279/// `25·2⁻³²` from the origin with the segment already `1.25·2⁻³²` in: taken as
2280/// clear, the cast went past the contact; taken as touching (by the lower
2281/// bound), other casts stopped up to `10·2⁻³²` early. There the exact distance
2282/// decides. Pieces with an exact distance: a posed cylinder and cone, point
2283/// and segment cores ([`exact_round_dist`]); the other convex pieces (boxes,
2284/// wedges, ellipsoids, hulls, mesh triangles, height-field cells) keep GJK's
2285/// bounds (`slack` is zero for them; their support points are not moved
2286/// onto an axis).
2287#[allow(clippy::too_many_arguments)]
2288fn toi_convex_refined<S: Support>(
2289    a: Vec3Fix,
2290    b: Vec3Fix,
2291    r: Fix128,
2292    inflate: Fix128,
2293    d: Vec3Fix,
2294    max_t: Fix128,
2295    solid: &S,
2296    slack: Fix128,
2297    exact: impl Fn(Vec3Fix, Vec3Fix) -> Option<Dist>,
2298) -> Option<Contact> {
2299    let reach = r + inflate;
2300    toi_by(a, b, r, inflate, d, max_t, |a, b, along| {
2301        if along {
2302            // the search along a near-tangent path takes the exact distance
2303            // where there is one (one source of distance, no mixed rounding)
2304            if let Some(e) = exact(a, b) {
2305                let exact_bound = Bound::exact(&e);
2306                return (e, exact_bound);
2307            }
2308        }
2309        let (found, bound) = convex_bounds(a, b, solid, slack);
2310        if let Dist::Outside { dist, .. } = found {
2311            if straddles(bound.lower - reach, dist - reach) {
2312                if let Some(e) = exact(a, b) {
2313                    let exact_bound = Bound::exact(&e);
2314                    return (e, exact_bound);
2315                }
2316            }
2317        }
2318        (found, bound)
2319    })
2320}
2321
2322/// Whether a gap known within `[lower, upper]` straddles one of the decisions
2323/// of a time of impact: touching (`2⁻³²`) or going in (`−2⁻³²`).
2324fn straddles(lower: Fix128, upper: Fix128) -> bool {
2325    let crosses = |x: Fix128| lower <= x && x < upper;
2326    crosses(TRACE_TOLERANCE) || crosses(-TRACE_TOLERANCE)
2327}
2328
2329/// [`toi_convex`] for a convex piece given by its distance from a core:
2330/// `core_dist(a, b, along)` is GJK for [`toi_convex`] (with its lower bound,
2331/// see [`Bound`]), or a closed form; `along` is set by the search along a
2332/// near-tangent path ([`dips_below`]), which takes an exact distance where
2333/// there is one.
2334///
2335/// The step from a clear time goes by the gap (the tangent's root), from
2336/// the distance as before, or for a distance taken on an imprecise face
2337/// from the lower bound, and at least by the tolerance (a step of the tolerance
2338/// goes at most that far in). Touching is decided by the distance.
2339fn toi_by(
2340    a: Vec3Fix,
2341    b: Vec3Fix,
2342    r: Fix128,
2343    inflate: Fix128,
2344    d: Vec3Fix,
2345    max_t: Fix128,
2346    core_dist: impl Fn(Vec3Fix, Vec3Fix, bool) -> (Dist, Bound),
2347) -> Option<Contact> {
2348    let reach = r + inflate;
2349    let mut t = Fix128::ZERO;
2350    // The last time known clear (gap above the tolerance), its near contact, and
2351    // the gap and closing speed there.
2352    let mut clear: Option<Contact> = None;
2353    let mut clear_gap = (Fix128::ZERO, Fix128::ZERO, false);
2354    // A time known to overlap, once a step has gone past the root.
2355    let mut deep: Option<Fix128> = None;
2356    for _ in 0..TRACE_MAX_STEPS {
2357        let off = d * t;
2358        let (found, bound) = core_dist(a + off, b + off, false);
2359        let state = match found {
2360            Dist::Outside {
2361                dist,
2362                point,
2363                normal,
2364            } if dist - reach >= -TRACE_TOLERANCE => {
2365                // The distance decides touching, as before; the step goes by
2366                // the lower bound (see `gjk_distance`), and at least by the
2367                // tolerance: the lower bound can be below it while the distance
2368                // is not, and a step of the tolerance goes at most that far in.
2369                let gap = if bound.step_lower {
2370                    max_fix(bound.lower - reach, TRACE_TOLERANCE)
2371                } else {
2372                    dist - reach
2373                };
2374                let touching = dist - reach <= TRACE_TOLERANCE;
2375                let slope = d.dot(normal);
2376                let contact = Contact {
2377                    t,
2378                    point: point + normal * inflate,
2379                    normal,
2380                };
2381                if touching {
2382                    let gap_at = |t: Fix128| {
2383                        let off = d * t;
2384                        match core_dist(a + off, b + off, true).0 {
2385                            Dist::Outside { dist, .. } => Some(dist - reach),
2386                            _ => None,
2387                        }
2388                    };
2389                    return touch_is_hit(contact, d, max_t, gap_at).then_some(contact);
2390                }
2391                if !slope.is_negative() {
2392                    if slope < NEAR_TANGENT && gap <= GRAZE_GAP {
2393                        // Not a proof of separation: see `GRAZE_GAP`.
2394                        let probe = |t: Fix128| {
2395                            let off = d * t;
2396                            core_dist(a + off, b + off, true).0
2397                        };
2398                        return graze_contact(probe, reach, inflate, t, max_t);
2399                    }
2400                    return None;
2401                }
2402                Some((gap, -slope, contact, dist < FINE_LENGTH))
2403            }
2404            _ => None,
2405        };
2406        match state {
2407            Some((gap, speed, contact, fine)) => {
2408                clear = Some(contact);
2409                clear_gap = (gap, speed, fine);
2410                let limit = deep.unwrap_or(max_t);
2411                // The tangent's root, unless it is at or past the limit.
2412                if gap >= (limit - t) * speed {
2413                    match deep {
2414                        None if gap > (limit - t) * speed => return None,
2415                        None => t = limit,
2416                        Some(hi) => t = (t + hi).half(),
2417                    }
2418                } else {
2419                    t = t + gap / speed;
2420                }
2421            }
2422            None => {
2423                if t.is_zero() {
2424                    return Some(Contact {
2425                        t,
2426                        point: half_vec(a + b),
2427                        normal: -d,
2428                    });
2429                }
2430                deep = Some(t);
2431                let lo = clear.map_or(Fix128::ZERO, |c| c.t);
2432                if t - lo <= TRACE_TOLERANCE {
2433                    // Bracketed to within the tolerance. When the clear end is
2434                    // nearer than 2⁻¹⁶ (a core of reach below that), report the
2435                    // tangent's root from it, which is not past the contact
2436                    // (convexity): a core of reach 0 never sees a touching gap
2437                    // (GJK counts `|v| ≤ 2⁻³²` as intersecting), so every such
2438                    // cast ends here; at `t` its gap is at most 2⁻³², not
2439                    // necessarily below 0, so the contact is before
2440                    // `t + 2⁻³² / speed`. Otherwise the clear end itself.
2441                    let (gap, speed, fine) = clear_gap;
2442                    if !fine {
2443                        return clear;
2444                    }
2445                    return clear.map(|c| {
2446                        let slack = ratio_within(TRACE_TOLERANCE, speed, max_t);
2447                        let step = ratio_within(gap, speed, t - lo + slack);
2448                        Contact {
2449                            t: min_fix(lo + max_fix(step, Fix128::ZERO), max_t),
2450                            ..c
2451                        }
2452                    });
2453                }
2454                t = (lo + t).half();
2455            }
2456        }
2457    }
2458    clear
2459}
2460
2461/// A clear gap up to this (`2⁻²⁴`) with the motion within [`NEAR_TANGENT`] of
2462/// the tangent plane but not into it: the slope `d·n` then is not a proof that
2463/// the gap only grows. The normal is known to about the error of the distance
2464/// over the distance, and a path at `9·10⁻¹⁰` rad to a cone's base showed a
2465/// slope of `+3·10⁻⁹` where it was `−1.5·10⁻⁹`, at a gap of
2466/// `3·2⁻³²`, and went `1.7·2⁻³²` in further on: `2⁻²⁸` of slope error over a
2467/// path of 16 is `2⁻²⁴` of gap.
2468const GRAZE_GAP: Fix128 = Fix128 {
2469    hi: 0,
2470    lo: 0x0000_0100_0000_0000,
2471};
2472
2473/// The contact of a path that is near tangent at `t0` (see [`GRAZE_GAP`]): the
2474/// gap (`dist` less `reach`, convex) is minimised on `[t0, max_t]` by
2475/// golden-section search, and if it goes below `−2⁻³²` the time it first comes
2476/// within `2⁻³²` is found by bisection between `t0` and that point (a clear
2477/// end, never after the contact, if the bisection does not land on it); `None`
2478/// if it does not go in.
2479fn graze_contact(
2480    dist: impl Fn(Fix128) -> Dist,
2481    reach: Fix128,
2482    inflate: Fix128,
2483    t0: Fix128,
2484    max_t: Fix128,
2485) -> Option<Contact> {
2486    let gap_at = |t: Fix128| match dist(t) {
2487        Dist::Outside { dist, .. } => Some(dist - reach),
2488        _ => None,
2489    };
2490    let deep = |g: Option<Fix128>| g.is_none_or(|g| g < -TRACE_TOLERANCE);
2491    let mut inside = deep(gap_at(max_t)).then_some(max_t);
2492    if inside.is_none() {
2493        let ratio = Fix128::from_ratio(618_033_988_749_895, 1_000_000_000_000_000);
2494        let (mut lo, mut hi) = (t0, max_t);
2495        for _ in 0..GOLDEN_STEPS {
2496            let x1 = hi - (hi - lo) * ratio;
2497            let x2 = lo + (hi - lo) * ratio;
2498            let (g1, g2) = (gap_at(x1), gap_at(x2));
2499            if deep(g1) {
2500                inside = Some(x1);
2501                break;
2502            }
2503            if deep(g2) {
2504                inside = Some(x2);
2505                break;
2506            }
2507            if g1 <= g2 {
2508                hi = x2;
2509            } else {
2510                lo = x1;
2511            }
2512        }
2513    }
2514    let (mut lo, mut hi) = (t0, inside?);
2515    for _ in 0..GOLDEN_STEPS {
2516        let mid = (lo + hi).half();
2517        match gap_at(mid) {
2518            Some(g) if g > TRACE_TOLERANCE => lo = mid,
2519            Some(g) if g >= -TRACE_TOLERANCE => {
2520                lo = mid;
2521                break;
2522            }
2523            _ => hi = mid,
2524        }
2525    }
2526    match dist(lo) {
2527        Dist::Outside { point, normal, .. } => Some(Contact {
2528            t: lo,
2529            point: point + normal * inflate,
2530            normal,
2531        }),
2532        _ => None,
2533    }
2534}
2535
2536/// Whether a contact found touching a convex piece (gap at most `2⁻³²`, normal
2537/// `n` toward the core) is a hit for the motion along the unit `d`: moving away
2538/// or along the surface (`d·n ≥ 0`) is not; moving into it is, except that when
2539/// the motion is within [`NEAR_TANGENT`] of the tangent plane the contact is a
2540/// hit only if `gap_at` (the gap along the path, `None` inside) then drops
2541/// below `−2⁻³²` before `max_t`. A shape resting on a mesh floor or a flat
2542/// height field touches the next triangle or cell only tangentially at their
2543/// shared edge, and must not stop there; a grazing approach that really goes
2544/// in is still a hit.
2545fn touch_is_hit(
2546    contact: Contact,
2547    d: Vec3Fix,
2548    max_t: Fix128,
2549    gap_at: impl Fn(Fix128) -> Option<Fix128>,
2550) -> bool {
2551    let slope = d.dot(contact.normal);
2552    if slope < -NEAR_TANGENT {
2553        return true;
2554    }
2555    if !slope.is_negative() {
2556        return false;
2557    }
2558    match gap_at(contact.t) {
2559        None => true,
2560        Some(gap) => dips_below(gap_at, contact.t, gap, -slope, max_t),
2561    }
2562}
2563
2564/// A gap has grown in [`dips_below`] only when it is above the previous one by
2565/// this (`2⁻⁵⁶`, `2⁻²⁴` of the tolerance): an exact distance
2566/// ([`exact_round_dist`]) is still off by its rounding, the turn into the
2567/// solid's frame (`12·2⁻⁶⁴` per unit of coordinate, see
2568/// `Vec3Fix::rescaled_xz`) and a few truncated products, below `2⁻⁵⁷` for
2569/// coordinates up to `2⁴`. A path that runs along a face sinking by
2570/// `4·10⁻⁵·2⁻³²` over its length changed by less than that between two steps,
2571/// a rounding-level rise bracketed a minimum that is not there, and a path
2572/// that went `1.00004·2⁻³²` in was not a hit.
2573const DIPS_GROWTH: Fix128 = Fix128 { hi: 0, lo: 1 << 8 };
2574
2575/// Whether a convex gap function (`None` inside) that is `f0` at `t0` and
2576/// decreasing at `speed` drops below `−2⁻³²` in `[t0, max_t]`: steps doubling
2577/// from the tangent's root until the gap goes below that (yes), grows again
2578/// (its minimum is then bracketed and found by golden-section search) or
2579/// `max_t` is reached.
2580fn dips_below(
2581    gap_at: impl Fn(Fix128) -> Option<Fix128>,
2582    t0: Fix128,
2583    f0: Fix128,
2584    speed: Fix128,
2585    max_t: Fix128,
2586) -> bool {
2587    let deep = |g: Option<Fix128>| g.is_none_or(|g| g < -TRACE_TOLERANCE);
2588    // The far end first: a path that sinks so slowly that a doubling step
2589    // changes the gap by less than its rounding would see no decrease and stop
2590    // the search before it reaches the depth it has by `max_t`.
2591    if deep(gap_at(max_t)) {
2592        return true;
2593    }
2594    let big = max_t + Fix128::ONE;
2595    let mut step = max_fix(
2596        ratio_within(max_fix(f0, Fix128::ZERO), speed, big),
2597        TRACE_TOLERANCE,
2598    );
2599    let (mut before, mut last, mut last_gap) = (t0, t0, f0);
2600    for _ in 0..GOLDEN_STEPS {
2601        let t1 = min_fix(t0 + step, max_t);
2602        let g1 = gap_at(t1);
2603        if deep(g1) {
2604            return true;
2605        }
2606        let g1 = g1.unwrap_or(Fix128::ZERO);
2607        if g1 > last_gap + DIPS_GROWTH || t1 >= max_t {
2608            // Convex: the minimum is in [before, t1].
2609            let (mut lo, mut hi) = (before, t1);
2610            let ratio = Fix128::from_ratio(618_033_988_749_895, 1_000_000_000_000_000);
2611            for _ in 0..GOLDEN_STEPS {
2612                let x1 = hi - (hi - lo) * ratio;
2613                let x2 = lo + (hi - lo) * ratio;
2614                let (g1, g2) = (gap_at(x1), gap_at(x2));
2615                if deep(g1) || deep(g2) {
2616                    return true;
2617                }
2618                let (g1, g2) = (g1.unwrap_or(Fix128::ZERO), g2.unwrap_or(Fix128::ZERO));
2619                if g1 <= g2 {
2620                    hi = x2;
2621                } else {
2622                    lo = x1;
2623                }
2624            }
2625            return false;
2626        }
2627        before = last;
2628        last = t1;
2629        last_gap = g1;
2630        step = step.double();
2631    }
2632    false
2633}
2634
2635/// The first contact with a non-convex surface described as a hierarchy of parts
2636/// whose convex hulls contain them: `toi` is the contact with a part's hull,
2637/// `leaf` says a part's hull is within tolerance of the part, `split` divides a
2638/// part. Parts are taken best first (earliest hull contact); a hull contact is
2639/// never after the contact with the part it contains, so the first leaf taken is
2640/// the first contact of the whole surface to within the leaf tolerance. If the
2641/// node budget runs out, the earliest open hull contact is reported (never after
2642/// the true contact).
2643fn toi_tree<N: Copy>(
2644    roots: &[N],
2645    toi: impl Fn(&N) -> Option<Contact>,
2646    leaf: impl Fn(&N) -> bool,
2647    split: impl Fn(&N) -> Vec<N>,
2648) -> Option<Contact> {
2649    let mut open: Vec<(Contact, N)> = roots
2650        .iter()
2651        .filter_map(|n| toi(n).map(|c| (c, *n)))
2652        .collect();
2653    let mut nodes = 0usize;
2654    loop {
2655        let k = (0..open.len()).min_by_key(|&k| open[k].0.t)?;
2656        let (contact, node) = open.remove(k);
2657        if leaf(&node) {
2658            return Some(contact);
2659        }
2660        nodes += 1;
2661        if nodes > TREE_MAX_NODES {
2662            return Some(contact);
2663        }
2664        for child in split(&node) {
2665            if let Some(c) = toi(&child) {
2666                open.push((c, child));
2667            }
2668        }
2669    }
2670}
2671
2672/// Whether the height-field surface passes through `aabb`: over each cell's part
2673/// of the box's `XZ` footprint the bilinear surface takes its extremes at the
2674/// corners of that part.
2675fn heightfield_meets_aabb(field: &HeightField, aabb: &AABB) -> bool {
2676    if !field_has_surface(field) {
2677        return false;
2678    }
2679    let s = field.spacing;
2680    let x_end = field.origin.x + s * Fix128::from_int(i64::from(field.width) - 1);
2681    let z_end = field.origin.z + s * Fix128::from_int(i64::from(field.depth) - 1);
2682    let fx0 = max_fix(aabb.min.x, field.origin.x);
2683    let fx1 = min_fix(aabb.max.x, x_end);
2684    let fz0 = max_fix(aabb.min.z, field.origin.z);
2685    let fz1 = min_fix(aabb.max.z, z_end);
2686    if fx0 > fx1 || fz0 > fz1 {
2687        return false;
2688    }
2689    let (Some((gx0, gx1)), Some((gz0, gz1))) = (
2690        cell_range(fx0, fx1, field.origin.x, s, field.width),
2691        cell_range(fz0, fz1, field.origin.z, s, field.depth),
2692    ) else {
2693        return false;
2694    };
2695    for gz in gz0..=gz1 {
2696        for gx in gx0..=gx1 {
2697            let x0 = field.origin.x + s * Fix128::from_int(i64::from(gx));
2698            let z0 = field.origin.z + s * Fix128::from_int(i64::from(gz));
2699            let ux0 = max_fix(fx0, x0);
2700            let ux1 = min_fix(fx1, x0 + s);
2701            let uz0 = max_fix(fz0, z0);
2702            let uz1 = min_fix(fz1, z0 + s);
2703            if ux0 > ux1 || uz0 > uz1 {
2704                continue;
2705            }
2706            let [h00, h10, h01, h11] = cell_heights(field, gx, gz);
2707            let mut lo: Option<Fix128> = None;
2708            let mut hi: Option<Fix128> = None;
2709            for x in [ux0, ux1] {
2710                for z in [uz0, uz1] {
2711                    let u = (x - x0) / s;
2712                    let v = (z - z0) / s;
2713                    let one = Fix128::ONE;
2714                    let h = h00 * (one - u) * (one - v)
2715                        + h10 * u * (one - v)
2716                        + h01 * (one - u) * v
2717                        + h11 * u * v;
2718                    lo = Some(lo.map_or(h, |l| min_fix(l, h)));
2719                    hi = Some(hi.map_or(h, |m| max_fix(m, h)));
2720                }
2721            }
2722            if let (Some(lo), Some(hi)) = (lo, hi) {
2723                if lo < aabb.max.y && hi > aabb.min.y {
2724                    return true;
2725                }
2726            }
2727        }
2728    }
2729    false
2730}
2731
2732// ============================================================================
2733// Triangle meshes
2734// ============================================================================
2735
2736/// The triangles of `mesh` whose box meets `query`, in BVH order.
2737fn mesh_candidates(mesh: &TriMesh, query: &AABB) -> Vec<u32> {
2738    mesh.bvh.query(query)
2739}
2740
2741/// The distance from the core `a`–`b` to the mesh, searching the triangles within
2742/// `cap`.
2743fn mesh_dist(mesh: &TriMesh, a: Vec3Fix, b: Vec3Fix, cap: Fix128) -> Dist {
2744    if mesh.triangles.is_empty() {
2745        return Dist::AtLeast(cap);
2746    }
2747    let core = core_box(a, b, Fix128::ZERO);
2748    let far = aabb_gap(&core, &mesh.bounds);
2749    if far >= cap {
2750        return Dist::AtLeast(far);
2751    }
2752    let mut best: Option<Dist> = None;
2753    for i in mesh_candidates(mesh, &core_box(a, b, cap)) {
2754        let tri = &mesh.triangles[i as usize];
2755        let d = if a == b {
2756            match Dist::toward(a, tri.closest_point(a)) {
2757                // On the triangle: touching, with its normal.
2758                Dist::Inside => Dist::Outside {
2759                    dist: Fix128::ZERO,
2760                    point: a,
2761                    normal: tri.normal().try_normalize().unwrap_or(Vec3Fix::UNIT_Y),
2762                },
2763                other => other,
2764            }
2765        } else {
2766            convex_dist(a, b, &TriangleSupport(tri))
2767        };
2768        if matches!(d, Dist::Inside) {
2769            return Dist::Inside;
2770        }
2771        if best.as_ref().is_none_or(|x| d.value() < x.value()) {
2772            best = Some(d);
2773        }
2774    }
2775    match best {
2776        Some(d) if d.value() < cap => d,
2777        _ => Dist::AtLeast(cap),
2778    }
2779}
2780
2781/// A sphere swept against one triangle: the two copies of the triangle moved
2782/// `±r` along its normal and the three edges grown into capsules of `r`.
2783fn sweep_triangle(
2784    o: Vec3Fix,
2785    d: Vec3Fix,
2786    tri: &Triangle,
2787    r: Fix128,
2788    max_t: Fix128,
2789) -> Option<LocalHit> {
2790    let ray = crate::raycast::Ray {
2791        origin: o,
2792        direction: d,
2793    };
2794    let mut best = None;
2795    if let Some(n) = tri.normal().try_normalize() {
2796        for off in [n * r, -(n * r)] {
2797            let moved = Triangle::new(tri.v0 + off, tri.v1 + off, tri.v2 + off);
2798            best = nearer(
2799                best,
2800                ray_triangle(&ray, &moved, max_t).map(|h| LocalHit {
2801                    t: h.t,
2802                    normal: h.normal,
2803                }),
2804            );
2805        }
2806    }
2807    for (e0, e1) in [(tri.v0, tri.v1), (tri.v1, tri.v2), (tri.v2, tri.v0)] {
2808        best = nearer(
2809            best,
2810            ray_solid_capsule(o, d, &Capsule::new(e0, e1, r), max_t),
2811        );
2812    }
2813    best
2814}
2815
2816// ============================================================================
2817// Geometry pieces
2818// ============================================================================
2819
2820/// One piece of geometry a query tests, placed in the world.
2821enum Piece<'a> {
2822    Sphere {
2823        center: Vec3Fix,
2824        radius: Fix128,
2825    },
2826    Capsule(Capsule),
2827    Box {
2828        center: Vec3Fix,
2829        half: Vec3Fix,
2830        rotation: QuatFix,
2831    },
2832    Posed(PosedShape),
2833    Hull(ChildSupport<'a>),
2834    Plane(&'a PlaneCollider),
2835    Height(&'a HeightField),
2836    Mesh(&'a TriMesh),
2837    #[cfg(feature = "std")]
2838    Sdf(&'a crate::sdf_collider::SdfCollider),
2839}
2840
2841/// The pieces of a body (see [`crate::shape_raycast`] for which geometry).
2842fn body_pieces(world: &PhysicsWorld, i: usize) -> Vec<Piece<'_>> {
2843    let Some((radius, collider)) = world.ray_geometry(i) else {
2844        return Vec::new();
2845    };
2846    let body = &world.bodies[i];
2847    match collider {
2848        None => vec![Piece::Sphere {
2849            center: body.position,
2850            radius,
2851        }],
2852        Some(BodyCollider::Shape(shape)) => {
2853            let posed = PosedShape {
2854                shape: *shape,
2855                position: body.position,
2856                rotation: body.rotation,
2857            };
2858            vec![match *shape {
2859                Shape::Box { half_extents } => {
2860                    let rotation = body.rotation.unit_rotation();
2861                    Piece::Box {
2862                        center: body.position - rotation.rotate_vec(shape.center_of_mass_offset()),
2863                        half: half_extents,
2864                        rotation,
2865                    }
2866                }
2867                _ => Piece::Posed(posed),
2868            }]
2869        }
2870        Some(BodyCollider::Compound(compound)) => compound
2871            .children
2872            .iter()
2873            .map(|child| child_piece(child, body.position, body.rotation))
2874            .collect(),
2875    }
2876}
2877
2878/// One compound child, placed the way [`CompoundChild::support_world`] places it.
2879fn child_piece(child: &CompoundChild, body_pos: Vec3Fix, body_rot: QuatFix) -> Piece<'_> {
2880    let body_rot = body_rot.unit_rotation();
2881    let child_rot = body_rot.mul(child.local_rotation);
2882    let child_pos = body_pos + body_rot.rotate_vec(child.local_position);
2883    match &child.shape {
2884        ShapeRef::Sphere(s) => Piece::Sphere {
2885            center: child_pos + child_rot.rotate_vec(s.center),
2886            radius: s.radius,
2887        },
2888        ShapeRef::Capsule(c) => Piece::Capsule(Capsule::new(
2889            child_pos + child_rot.rotate_vec(c.a),
2890            child_pos + child_rot.rotate_vec(c.b),
2891            c.radius,
2892        )),
2893        ShapeRef::Box(b) => Piece::Box {
2894            center: child_pos + child_rot.rotate_vec(b.center),
2895            half: b.half_extents,
2896            rotation: child_rot.mul(b.rotation),
2897        },
2898        ShapeRef::ConvexHull(_) => Piece::Hull(ChildSupport {
2899            child,
2900            position: body_pos,
2901            rotation: body_rot,
2902        }),
2903    }
2904}
2905
2906/// The geometric centre and frame of a posed shape.
2907fn posed_frame(posed: &PosedShape) -> (Vec3Fix, QuatFix) {
2908    let rotation = posed.rotation.unit_rotation();
2909    (
2910        posed.position - rotation.rotate_vec(posed.shape.center_of_mass_offset()),
2911        rotation,
2912    )
2913}
2914
2915/// The query settings that pieces need.
2916struct Settings {
2917    #[cfg(feature = "std")]
2918    sdf: SdfCcdConfig,
2919}
2920
2921impl Piece<'_> {
2922    /// The distance from the core `a`–`b`; `cap` bounds the search where a piece
2923    /// searches cells or triangles (it may report [`Dist::AtLeast`] beyond).
2924    fn dist(&self, a: Vec3Fix, b: Vec3Fix, cap: Fix128) -> Dist {
2925        match self {
2926            Self::Sphere { center, radius } => sphere_dist(a, b, *center, *radius),
2927            Self::Capsule(c) => capsule_dist(a, b, c),
2928            Self::Box {
2929                center,
2930                half,
2931                rotation,
2932            } => {
2933                if a == b {
2934                    let inv = rotation.conjugate();
2935                    let p = inv.rotate_vec(a - *center);
2936                    dist_to_world(point_box_local(p, *half), *center, *rotation)
2937                } else {
2938                    let obb = crate::box_collider::OrientedBox::new(*center, *half, *rotation);
2939                    convex_dist(a, b, &obb)
2940                }
2941            }
2942            Self::Posed(posed) => {
2943                let (center, rotation) = posed_frame(posed);
2944                let inv = rotation.conjugate();
2945                match posed.shape {
2946                    Shape::Cylinder {
2947                        radius,
2948                        half_height,
2949                    } if a == b => dist_to_world(
2950                        point_cylinder_local(inv.rotate_vec(a - center), radius, half_height),
2951                        center,
2952                        rotation,
2953                    ),
2954                    Shape::Torus {
2955                        major_radius,
2956                        minor_radius,
2957                    } => {
2958                        let f = |p: Vec3Fix| {
2959                            dist_to_world(
2960                                point_torus_local(
2961                                    inv.rotate_vec(p - center),
2962                                    major_radius,
2963                                    minor_radius,
2964                                ),
2965                                center,
2966                                rotation,
2967                            )
2968                        };
2969                        segment_min(a, b, f)
2970                    }
2971                    _ if is_round(posed) => convex_dist(a, b, &RoundSupport(posed)),
2972                    _ => convex_dist(a, b, posed),
2973                }
2974            }
2975            Self::Hull(child) => convex_dist(a, b, child),
2976            Self::Plane(plane) => plane_dist(plane, a, b),
2977            Self::Height(field) => heightfield_dist(field, a, b, cap),
2978            Self::Mesh(mesh) => mesh_dist(mesh, a, b, cap),
2979            #[cfg(feature = "std")]
2980            Self::Sdf(sdf) => segment_min(a, b, |p| point_sdf(sdf, p)),
2981        }
2982    }
2983
2984    /// The first time the core `a`–`b` grown by `r` touches this piece moving
2985    /// along the unit `d`, when it does not overlap it at the start: closed forms
2986    /// for a point core against spheres, capsules, boxes, cylinders, tori,
2987    /// planes and triangles, the convex time of impact ([`toi_convex`]) for a
2988    /// segment core and the other convex pieces, a hierarchy of hulls for
2989    /// height-field cells and torus arcs ([`toi_tree`]), one time of impact per
2990    /// triangle for meshes, and sphere tracing for SDFs.
2991    fn sweep(
2992        &self,
2993        a: Vec3Fix,
2994        b: Vec3Fix,
2995        r: Fix128,
2996        d: Vec3Fix,
2997        max_t: Fix128,
2998        settings: &Settings,
2999    ) -> Option<Contact> {
3000        #[cfg(not(feature = "std"))]
3001        let _ = settings;
3002        let point_core = a == b;
3003        match self {
3004            Self::Sphere { center, radius } => {
3005                let c = if point_core {
3006                    let h = ray_solid_sphere(a, d, *center, *radius + r, max_t)?;
3007                    Contact::from_ray(a, d, r, h)
3008                } else {
3009                    // The capsule moving +d meets the sphere when the sphere's
3010                    // centre, moving −d, meets the capsule grown by its radius.
3011                    let grown = Capsule::new(a, b, r + *radius);
3012                    let h = ray_solid_capsule(*center, -d, &grown, max_t)?;
3013                    let normal = -h.normal;
3014                    Contact {
3015                        t: h.t,
3016                        point: *center + normal * *radius,
3017                        normal,
3018                    }
3019                };
3020                self.entering(c, a, b, r, d, max_t)
3021            }
3022            Self::Capsule(c) => {
3023                if point_core {
3024                    let grown = Capsule::new(c.a, c.b, c.radius + r);
3025                    let h = ray_solid_capsule(a, d, &grown, max_t)?;
3026                    self.entering(Contact::from_ray(a, d, r, h), a, b, r, d, max_t)
3027                } else {
3028                    toi_convex(a, b, r, c.radius, d, max_t, &SegmentSupport(c.a, c.b))
3029                }
3030            }
3031            Self::Box {
3032                center,
3033                half,
3034                rotation,
3035            } => {
3036                if point_core {
3037                    let (o, dl) = to_local(a, d, *center, *rotation);
3038                    let h = to_world(sweep_local_box(o, dl, *half, r, max_t), *rotation)?;
3039                    self.entering(Contact::from_ray(a, d, r, h), a, b, r, d, max_t)
3040                } else {
3041                    let obb = crate::box_collider::OrientedBox::new(*center, *half, *rotation);
3042                    toi_convex(a, b, r, Fix128::ZERO, d, max_t, &obb)
3043                }
3044            }
3045            Self::Posed(posed) => {
3046                let (center, rotation) = posed_frame(posed);
3047                match posed.shape {
3048                    Shape::Cylinder {
3049                        radius,
3050                        half_height,
3051                    } if point_core => {
3052                        let (o, dl) = to_local(a, d, center, rotation);
3053                        let found = to_world(
3054                            sweep_local_cylinder(o, dl, radius, half_height, r, max_t),
3055                            rotation,
3056                        );
3057                        let cap = r + r + Fix128::ONE;
3058                        // The closed form is the first ray hit on the cylinder
3059                        // grown by `r` (two cylinders and two tori); taken only
3060                        // where the exact distance agrees (see
3061                        // `sweep_local_cylinder`), with the contact's point and
3062                        // normal from the nearest point of the cylinder.
3063                        let checked = found.and_then(|h| {
3064                            if h.t.is_zero() {
3065                                return Some(Contact::from_ray(a, d, r, h));
3066                            }
3067                            let c = a + d * h.t;
3068                            match self.dist(c, c, cap) {
3069                                Dist::Outside {
3070                                    dist,
3071                                    point,
3072                                    normal,
3073                                } if dist - r >= -TRACE_TOLERANCE => Some(Contact {
3074                                    t: h.t,
3075                                    point,
3076                                    normal,
3077                                }),
3078                                _ => None,
3079                            }
3080                        });
3081                        match checked {
3082                            Some(c) => self.entering(c, a, b, r, d, max_t),
3083                            None => toi_by(a, b, r, Fix128::ZERO, d, max_t, |p, q, _| {
3084                                let found = self.dist(p, q, cap);
3085                                let bound = Bound::exact(&found);
3086                                (found, bound)
3087                            }),
3088                        }
3089                    }
3090                    Shape::Torus {
3091                        major_radius,
3092                        minor_radius,
3093                    } => {
3094                        let ring = Ring {
3095                            center,
3096                            rotation,
3097                            major: major_radius,
3098                            minor: minor_radius,
3099                        };
3100                        if point_core {
3101                            let (o, dl) = to_local(a, d, center, rotation);
3102                            let h = ray_local_torus(o, dl, major_radius, minor_radius + r, max_t);
3103                            let c = Contact::from_ray(a, d, r, to_world(h, rotation)?);
3104                            // A torus is not convex: leaving it where the sphere
3105                            // touches it, the path can still enter it further on.
3106                            self.entering(c, a, b, r, d, max_t)
3107                                .or_else(|| ring.sweep(a, b, r, d, max_t))
3108                        } else {
3109                            ring.sweep(a, b, r, d, max_t)
3110                        }
3111                    }
3112                    _ if is_round(posed) => {
3113                        toi_convex(a, b, r, Fix128::ZERO, d, max_t, &RoundSupport(posed))
3114                    }
3115                    Shape::Cylinder { .. } | Shape::Cone { .. } => toi_convex_refined(
3116                        a,
3117                        b,
3118                        r,
3119                        Fix128::ZERO,
3120                        d,
3121                        max_t,
3122                        posed,
3123                        round_support_slack(posed),
3124                        |p, q| exact_round_dist(posed, p, q),
3125                    ),
3126                    _ => toi_convex(a, b, r, Fix128::ZERO, d, max_t, posed),
3127                }
3128            }
3129            Self::Hull(child) => toi_convex(a, b, r, Fix128::ZERO, d, max_t, child),
3130            Self::Plane(plane) => sweep_plane(plane, a, b, r, d, max_t),
3131            Self::Height(field) => sweep_heightfield(field, a, b, r, d, max_t),
3132            Self::Mesh(mesh) => {
3133                if point_core {
3134                    let swept = core_box(a, a + d * max_t, r);
3135                    let mut best: Option<Contact> = None;
3136                    for i in mesh_candidates(mesh, &swept) {
3137                        let tri = &mesh.triangles[i as usize];
3138                        let Some(h) = sweep_triangle(a, d, tri, r, max_t) else {
3139                            continue;
3140                        };
3141                        let gap_at = |t: Fix128| {
3142                            let p = a + d * t;
3143                            match Dist::toward(p, tri.closest_point(p)) {
3144                                Dist::Outside { dist, .. } => Some(dist - r),
3145                                _ => None,
3146                            }
3147                        };
3148                        let mut c = Contact::from_ray(a, d, r, h);
3149                        if c.t <= TRACE_TOLERANCE {
3150                            // Touching this triangle at the start: its own
3151                            // nearest point and normal (see `entering`).
3152                            if let Dist::Outside { point, normal, .. } =
3153                                Dist::toward(a, tri.closest_point(a))
3154                            {
3155                                c = Contact {
3156                                    t: Fix128::ZERO,
3157                                    point,
3158                                    normal,
3159                                };
3160                            }
3161                        }
3162                        if !touch_is_hit(c, d, max_t, gap_at) {
3163                            continue;
3164                        }
3165                        if best.is_none_or(|bc| c.t < bc.t) {
3166                            best = Some(c);
3167                        }
3168                    }
3169                    best
3170                } else {
3171                    sweep_mesh(mesh, a, b, r, d, max_t)
3172                }
3173            }
3174            #[cfg(feature = "std")]
3175            Self::Sdf(_) => {
3176                let tol = Fix128::from_f32(settings.sdf.tolerance);
3177                self.trace(a, b, r, d, max_t, tol)
3178            }
3179        }
3180        .filter(|c| c.t >= Fix128::ZERO && c.t <= max_t)
3181    }
3182
3183    /// A closed-form contact is a hit by [`touch_is_hit`]. One at `t ≤ 2⁻³²`
3184    /// comes from a cast that starts touching the piece (an overlap is reported
3185    /// before the sweep): its normal and point are the piece's nearest point and
3186    /// normal toward the core (a closed form may report a touching origin as
3187    /// inside). Moving away or along the surface is not a hit, nor is a tangent
3188    /// arrival.
3189    fn entering(
3190        &self,
3191        c: Contact,
3192        a: Vec3Fix,
3193        b: Vec3Fix,
3194        r: Fix128,
3195        d: Vec3Fix,
3196        max_t: Fix128,
3197    ) -> Option<Contact> {
3198        let cap = r + r + Fix128::ONE;
3199        let gap_at = |t: Fix128| {
3200            let off = d * t;
3201            match self.dist(a + off, b + off, cap) {
3202                Dist::Outside { dist, .. } => Some(dist - r),
3203                Dist::AtLeast(bound) => Some(bound - r),
3204                Dist::Inside => None,
3205            }
3206        };
3207        let c = if c.t > TRACE_TOLERANCE {
3208            c
3209        } else {
3210            match self.dist(a, b, cap) {
3211                Dist::Outside { point, normal, .. } => Contact {
3212                    t: Fix128::ZERO,
3213                    point,
3214                    normal,
3215                },
3216                _ => return Some(c),
3217            }
3218        };
3219        touch_is_hit(c, d, max_t, gap_at).then_some(c)
3220    }
3221
3222    /// Sphere tracing (for SDFs): step by the gap (the distance less `r`) until
3223    /// it is below `tol`. A field that is a true distance never lets a step pass
3224    /// the surface; if the step budget runs out the last position is reported
3225    /// (never after the contact), not "no hit".
3226    #[cfg(feature = "std")]
3227    fn trace(
3228        &self,
3229        a: Vec3Fix,
3230        b: Vec3Fix,
3231        r: Fix128,
3232        d: Vec3Fix,
3233        max_t: Fix128,
3234        tol: Fix128,
3235    ) -> Option<Contact> {
3236        let mut t = Fix128::ZERO;
3237        let cap = r + r + Fix128::ONE;
3238        let mut last: Option<Contact> = None;
3239        for _ in 0..SDF_MAX_STEPS {
3240            let off = d * t;
3241            let gap = match self.dist(a + off, b + off, cap) {
3242                Dist::Inside => {
3243                    return Some(Contact {
3244                        t,
3245                        point: half_vec(a + b) + off,
3246                        normal: -d,
3247                    })
3248                }
3249                Dist::AtLeast(bound) => bound - r,
3250                Dist::Outside {
3251                    dist,
3252                    point,
3253                    normal,
3254                } => {
3255                    let gap = dist - r;
3256                    let contact = Contact { t, point, normal };
3257                    last = Some(contact);
3258                    if gap <= tol {
3259                        // Touching: a hit only when moving into the surface;
3260                        // moving away or along it, step past by the tolerance.
3261                        if d.dot(normal).is_negative() {
3262                            return Some(contact);
3263                        }
3264                        tol
3265                    } else {
3266                        gap
3267                    }
3268                }
3269            };
3270            if gap <= Fix128::ZERO {
3271                return None;
3272            }
3273            t = t + gap;
3274            if t > max_t {
3275                return None;
3276            }
3277        }
3278        last
3279    }
3280
3281    /// Whether this piece overlaps `aabb` (see the module doc).
3282    fn meets_aabb(&self, aabb: &AABB) -> bool {
3283        match self {
3284            Self::Sphere { center, radius } => {
3285                (closest_on_aabb(aabb, *center) - *center).length_squared() < *radius * *radius
3286            }
3287            Self::Capsule(c) => match gjk_distance(&SegmentSupport(c.a, c.b), aabb) {
3288                None => true,
3289                Some((dist, _, _)) => dist < c.radius,
3290            },
3291            Self::Box {
3292                center,
3293                half,
3294                rotation,
3295            } => obb_meets_aabb(*center, *half, *rotation, aabb),
3296            Self::Posed(posed) => match posed.shape {
3297                Shape::Torus {
3298                    major_radius,
3299                    minor_radius,
3300                } => torus_meets_aabb(posed, major_radius, minor_radius, aabb),
3301                _ => gjk_distance(posed, aabb).is_none(),
3302            },
3303            Self::Hull(child) => gjk_distance(child, aabb).is_none(),
3304            Self::Plane(plane) => {
3305                let c = half_vec(aabb.min + aabb.max);
3306                let h = half_vec(aabb.max - aabb.min);
3307                let n = plane.normal;
3308                let reach = n.x.abs() * h.x + n.y.abs() * h.y + n.z.abs() * h.z;
3309                (n.dot(c) - plane.offset).abs() < reach
3310            }
3311            Self::Height(field) => heightfield_meets_aabb(field, aabb),
3312            Self::Mesh(mesh) => mesh_candidates(mesh, aabb).into_iter().any(|i| {
3313                gjk_distance(&TriangleSupport(&mesh.triangles[i as usize]), aabb).is_none()
3314            }),
3315            #[cfg(feature = "std")]
3316            Self::Sdf(sdf) => sdf_meets_aabb(sdf, aabb),
3317        }
3318    }
3319}
3320
3321/// A contact of a cast: distance along the direction, contact point, normal.
3322#[derive(Clone, Copy, Debug)]
3323struct Contact {
3324    t: Fix128,
3325    point: Vec3Fix,
3326    normal: Vec3Fix,
3327}
3328
3329impl Contact {
3330    /// A ray hit on the Minkowski sum turned into the contact of the sphere of `r`
3331    /// whose centre moved from `o` along `d`.
3332    fn from_ray(o: Vec3Fix, d: Vec3Fix, r: Fix128, h: LocalHit) -> Self {
3333        Self {
3334            t: h.t,
3335            point: o + d * h.t - h.normal * r,
3336            normal: h.normal,
3337        }
3338    }
3339}
3340
3341/// A box of half-extents `h` grown by `r` (rounded), in its frame: the three
3342/// slabs (the box pushed out by `r` along one axis) and the 12 edges as capsules.
3343fn sweep_local_box(
3344    o: Vec3Fix,
3345    d: Vec3Fix,
3346    h: Vec3Fix,
3347    r: Fix128,
3348    max_t: Fix128,
3349) -> Option<LocalHit> {
3350    let mut best = None;
3351    for grow in [
3352        Vec3Fix::new(r, Fix128::ZERO, Fix128::ZERO),
3353        Vec3Fix::new(Fix128::ZERO, r, Fix128::ZERO),
3354        Vec3Fix::new(Fix128::ZERO, Fix128::ZERO, r),
3355    ] {
3356        best = nearer(best, ray_planes(o, d, &box_planes(h + grow), max_t));
3357    }
3358    let signs = [Fix128::ONE, Fix128::NEG_ONE];
3359    for &s1 in &signs {
3360        for &s2 in &signs {
3361            for (a, b) in [
3362                (
3363                    Vec3Fix::new(-h.x, s1 * h.y, s2 * h.z),
3364                    Vec3Fix::new(h.x, s1 * h.y, s2 * h.z),
3365                ),
3366                (
3367                    Vec3Fix::new(s1 * h.x, -h.y, s2 * h.z),
3368                    Vec3Fix::new(s1 * h.x, h.y, s2 * h.z),
3369                ),
3370                (
3371                    Vec3Fix::new(s1 * h.x, s2 * h.y, -h.z),
3372                    Vec3Fix::new(s1 * h.x, s2 * h.y, h.z),
3373                ),
3374            ] {
3375                best = nearer(best, ray_solid_capsule(o, d, &Capsule::new(a, b, r), max_t));
3376            }
3377        }
3378    }
3379    best
3380}
3381
3382/// A cylinder along `Y` grown by `r` (rounded), in its frame: the cylinder of
3383/// radius `radius + r`, the one of half height `hh + r`, and the two rim tori.
3384///
3385/// The faces of the two cylinders beyond the rims (the side of the tall one
3386/// above a cap, the caps of the wide one outside the radius) are inside the
3387/// tori; they are the first hit only when a torus is not found. The torus ray
3388/// is sphere traced, and a path at a small angle to a cap (`10⁻⁶` rad) or
3389/// along its plane through the rounded rim takes more than its step budget:
3390/// it was then reported on the tall cylinder's side, `r` off the surface with a
3391/// horizontal normal, or deep in the cap, or not at all. The caller checks a
3392/// hit against the exact distance and falls back to the time of impact on that
3393/// distance.
3394fn sweep_local_cylinder(
3395    o: Vec3Fix,
3396    d: Vec3Fix,
3397    radius: Fix128,
3398    hh: Fix128,
3399    r: Fix128,
3400    max_t: Fix128,
3401) -> Option<LocalHit> {
3402    let mut best = ray_local_cylinder(o, d, radius + r, hh, max_t);
3403    best = nearer(best, ray_local_cylinder(o, d, radius, hh + r, max_t));
3404    for y in [hh, -hh] {
3405        let shift = Vec3Fix::new(Fix128::ZERO, y, Fix128::ZERO);
3406        best = nearer(best, ray_local_torus(o - shift, d, radius, r, max_t));
3407    }
3408    best
3409}
3410
3411/// Whether an oriented box overlaps an axis-aligned one, by the separating axis
3412/// theorem (the 3 + 3 face axes and the 9 edge cross products): they overlap
3413/// when no axis separates them, and boxes that only touch (projections meeting
3414/// at a point) do not overlap. Cross products of parallel edges are zero and
3415/// separate nothing; they are skipped.
3416fn obb_meets_aabb(center: Vec3Fix, half: Vec3Fix, rotation: QuatFix, aabb: &AABB) -> bool {
3417    let z = Vec3Fix::new(Fix128::ZERO, Fix128::ZERO, Fix128::ONE);
3418    let a_axes = [
3419        rotation.rotate_vec(Vec3Fix::UNIT_X),
3420        rotation.rotate_vec(Vec3Fix::UNIT_Y),
3421        rotation.rotate_vec(z),
3422    ];
3423    let a_half = [half.x, half.y, half.z];
3424    let b_axes = [Vec3Fix::UNIT_X, Vec3Fix::UNIT_Y, z];
3425    let b_center = half_vec(aabb.min + aabb.max);
3426    let b_ext = half_vec(aabb.max - aabb.min);
3427    let b_half = [b_ext.x, b_ext.y, b_ext.z];
3428    let between = b_center - center;
3429    let separates = |l: Vec3Fix| {
3430        let reach_a: Fix128 = (0..3).fold(Fix128::ZERO, |acc, i| {
3431            acc + a_half[i] * a_axes[i].dot(l).abs()
3432        });
3433        let reach_b: Fix128 = (0..3).fold(Fix128::ZERO, |acc, i| {
3434            acc + b_half[i] * b_axes[i].dot(l).abs()
3435        });
3436        between.dot(l).abs() >= reach_a + reach_b
3437    };
3438    let mut axes: Vec<Vec3Fix> = Vec::with_capacity(15);
3439    axes.extend_from_slice(&a_axes);
3440    axes.extend_from_slice(&b_axes);
3441    for ea in a_axes {
3442        for eb in b_axes {
3443            let l = ea.cross(eb);
3444            if !l.length_squared().is_zero() {
3445                axes.push(l);
3446            }
3447        }
3448    }
3449    !axes.into_iter().any(separates)
3450}
3451
3452/// The distance from the core `a`–`b` to a two-sided plane.
3453fn plane_dist(plane: &PlaneCollider, a: Vec3Fix, b: Vec3Fix) -> Dist {
3454    let n = plane.normal;
3455    let sa = n.dot(a) - plane.offset;
3456    let sb = n.dot(b) - plane.offset;
3457    if !sa.is_zero() && !sb.is_zero() && sa.is_negative() != sb.is_negative() {
3458        // The segment crosses the plane.
3459        return Dist::Inside;
3460    }
3461    let (p, s) = if sb.abs() < sa.abs() {
3462        (b, sb)
3463    } else {
3464        (a, sa)
3465    };
3466    // An end on the plane is touching it, on the side of the other end.
3467    let other = if sb.abs() < sa.abs() { sa } else { sb };
3468    let side = if s.is_negative() || (s.is_zero() && other.is_negative()) {
3469        -n
3470    } else {
3471        n
3472    };
3473    Dist::Outside {
3474        dist: s.abs(),
3475        point: p - n * s,
3476        normal: side,
3477    }
3478}
3479
3480/// A capsule (or sphere) swept against a two-sided plane: the nearer end reaches
3481/// distance `r`.
3482fn sweep_plane(
3483    plane: &PlaneCollider,
3484    a: Vec3Fix,
3485    b: Vec3Fix,
3486    r: Fix128,
3487    d: Vec3Fix,
3488    max_t: Fix128,
3489) -> Option<Contact> {
3490    let n = plane.normal;
3491    let sa = n.dot(a) - plane.offset;
3492    let sb = n.dot(b) - plane.offset;
3493    if sa.is_zero() || sb.is_zero() || sa.is_negative() != sb.is_negative() {
3494        return None;
3495    }
3496    // The nearer end (the first on a tie).
3497    let (p, s) = if sb.abs() < sa.abs() {
3498        (b, sb)
3499    } else {
3500        (a, sa)
3501    };
3502    let normal = if s.is_negative() { -n } else { n };
3503    let speed = -normal.dot(d);
3504    if speed <= Fix128::ZERO {
3505        return None;
3506    }
3507    let gap = s.abs() - r;
3508    // A sink slower than 2⁻³² per unit of travel is a hit when the core is more
3509    // than 2⁻³² inside the plane by `max_t` (the rule of `touch_is_hit` for a
3510    // motion near the tangent plane); its time is bounded by `ratio_within`.
3511    if speed < PARALLEL_EPSILON && gap - speed * max_t >= -TRACE_TOLERANCE {
3512        return None;
3513    }
3514    // A start within the tolerance inside the plane's reach (deeper is a start
3515    // overlap, reported before the sweep) is touching: contact at once.
3516    let t = max_fix(ratio_within(gap, speed, max_t + Fix128::ONE), Fix128::ZERO);
3517    if t > max_t {
3518        return None;
3519    }
3520    Some(Contact {
3521        t,
3522        point: p + d * t - normal * r,
3523        normal,
3524    })
3525}
3526
3527/// Whether a torus meets `aabb`: its ring comes within `minor` of the box.
3528fn torus_meets_aabb(posed: &PosedShape, major: Fix128, minor: Fix128, aabb: &AABB) -> bool {
3529    let (center, rotation) = posed_frame(posed);
3530    // Quick rejection by the bounding sphere.
3531    if (closest_on_aabb(aabb, center) - center).length() >= major + minor {
3532        return false;
3533    }
3534    let ring = |s: Fix128| {
3535        let (sin, cos) = (Fix128::TWO_PI * s).sin_cos();
3536        center + rotation.rotate_vec(Vec3Fix::new(major * cos, Fix128::ZERO, major * sin))
3537    };
3538    let d = curve_min(Fix128::TWO_PI * major, |s| {
3539        let p = ring(s);
3540        let q = closest_on_aabb(aabb, p);
3541        if q == p {
3542            Dist::Inside
3543        } else {
3544            Dist::toward(p, q)
3545        }
3546    });
3547    d.within(minor)
3548}
3549
3550#[cfg(feature = "std")]
3551fn point_sdf(sdf: &crate::sdf_collider::SdfCollider, p: Vec3Fix) -> Dist {
3552    let (lx, ly, lz) = sdf.world_to_local(p);
3553    let (d, (nx, ny, nz)) = sdf.field.distance_and_normal(lx, ly, lz);
3554    let d = d * sdf.scale_f32;
3555    if !d.is_finite() || d < 0.0 {
3556        return Dist::Inside;
3557    }
3558    let dist = Fix128::from_f32(d);
3559    let normal = sdf.local_normal_to_world(nx, ny, nz);
3560    Dist::Outside {
3561        dist,
3562        point: p - normal * dist,
3563        normal,
3564    }
3565}
3566
3567/// Whether an SDF solid meets `aabb`: an octree subdivision of the box, pruning
3568/// cells whose centre is farther than their half-diagonal from the surface and
3569/// accepting a centre inside; a leaf (depth `SDF_BOX_DEPTH`) within its
3570/// half-diagonal is counted as meeting.
3571#[cfg(feature = "std")]
3572fn sdf_meets_aabb(sdf: &crate::sdf_collider::SdfCollider, aabb: &AABB) -> bool {
3573    let mut stack = vec![(aabb.min, aabb.max, 0u32)];
3574    while let Some((lo, hi, depth)) = stack.pop() {
3575        let c = half_vec(lo + hi);
3576        let half_diag = half_vec(hi - lo).length();
3577        let d = match point_sdf(sdf, c) {
3578            Dist::Inside => return true,
3579            Dist::Outside { dist, .. } => dist,
3580            Dist::AtLeast(d) => d,
3581        };
3582        if d >= half_diag {
3583            continue;
3584        }
3585        if depth >= SDF_BOX_DEPTH {
3586            return true;
3587        }
3588        for k in 0..8u8 {
3589            let pick = |bit: u8, l: Fix128, m: Fix128, h: Fix128| {
3590                if k & bit == 0 {
3591                    (l, m)
3592                } else {
3593                    (m, h)
3594                }
3595            };
3596            let (x0, x1) = pick(1, lo.x, c.x, hi.x);
3597            let (y0, y1) = pick(2, lo.y, c.y, hi.y);
3598            let (z0, z1) = pick(4, lo.z, c.z, hi.z);
3599            stack.push((
3600                Vec3Fix::new(x0, y0, z0),
3601                Vec3Fix::new(x1, y1, z1),
3602                depth + 1,
3603            ));
3604        }
3605    }
3606    false
3607}
3608
3609/// The box of the segment `a`–`b` grown by `r`.
3610fn core_box(a: Vec3Fix, b: Vec3Fix, r: Fix128) -> AABB {
3611    let lo = Vec3Fix::new(min_fix(a.x, b.x), min_fix(a.y, b.y), min_fix(a.z, b.z));
3612    let hi = Vec3Fix::new(max_fix(a.x, b.x), max_fix(a.y, b.y), max_fix(a.z, b.z));
3613    let g = Vec3Fix::new(r, r, r);
3614    AABB::new(lo - g, hi + g)
3615}
3616
3617/// The distance between two boxes (zero when they meet).
3618fn aabb_gap(a: &AABB, b: &AABB) -> Fix128 {
3619    let gap = |lo1: Fix128, hi1: Fix128, lo2: Fix128, hi2: Fix128| {
3620        max_fix(Fix128::ZERO, max_fix(lo2 - hi1, lo1 - hi2))
3621    };
3622    Vec3Fix::new(
3623        gap(a.min.x, a.max.x, b.min.x, b.max.x),
3624        gap(a.min.y, a.max.y, b.min.y, b.max.y),
3625        gap(a.min.z, a.max.z, b.min.z, b.max.z),
3626    )
3627    .length()
3628}
3629
3630/// `direction` scaled down by its largest component when that is above `2²⁰`,
3631/// so that normalizing it does not overflow the squared length (`i64::MAX / 2`
3632/// squared wraps); smaller directions are returned as they are.
3633fn tame_direction(direction: Vec3Fix) -> Vec3Fix {
3634    let m = max_fix(
3635        max_fix(direction.x.abs(), direction.y.abs()),
3636        direction.z.abs(),
3637    );
3638    if m > Fix128::from_int(1 << 20) {
3639        direction / m
3640    } else {
3641        direction
3642    }
3643}
3644
3645fn box_is_valid(aabb: &AABB) -> bool {
3646    aabb.min.x <= aabb.max.x && aabb.min.y <= aabb.max.y && aabb.min.z <= aabb.max.z
3647}
3648
3649// ============================================================================
3650// World queries
3651// ============================================================================
3652
3653/// The colliders a query sees whose box meets `query`: each target with its body
3654/// and its pieces, in target order.
3655fn for_each_target<'w>(
3656    world: &'w PhysicsWorld,
3657    filter: &RayFilter,
3658    query: &AABB,
3659    mut visit: impl FnMut(RayTarget, Option<usize>, &[Piece<'w>]),
3660) {
3661    let (bvh, _) = world.query_body_bvh(filter);
3662    let mut bodies: Vec<u32> = bvh.query(query);
3663    bodies.sort_unstable();
3664    bodies.dedup();
3665    for i in bodies {
3666        let i = i as usize;
3667        let pieces = body_pieces(world, i);
3668        visit(RayTarget::Body(i), Some(i), &pieces);
3669    }
3670    if filter.include_static {
3671        for (j, collider) in world.static_colliders_slice().iter().enumerate() {
3672            let piece = match collider {
3673                StaticCollider::Plane(p) => Piece::Plane(p),
3674                StaticCollider::HeightField(f) => Piece::Height(f),
3675                StaticCollider::TriMesh(m) => Piece::Mesh(m),
3676            };
3677            visit(
3678                RayTarget::StaticCollider(j),
3679                None,
3680                core::slice::from_ref(&piece),
3681            );
3682        }
3683    }
3684    #[cfg(feature = "std")]
3685    if filter.include_sdf {
3686        for (k, sdf) in world.sdf_colliders.iter().enumerate() {
3687            let body = (sdf.body_index < world.bodies.len()).then_some(sdf.body_index);
3688            if body.is_some_and(|b| !filter.sees_body(world, b)) {
3689                continue;
3690            }
3691            let piece = Piece::Sdf(sdf);
3692            visit(RayTarget::Sdf(k), body, core::slice::from_ref(&piece));
3693        }
3694    }
3695}
3696
3697impl PhysicsWorld {
3698    /// The nearest collider a sphere of `radius` touches when its centre moves
3699    /// from `center` along `direction` for at most `max_t` (see
3700    /// [`crate::world_shape_query`] for the conventions). Radius `0` is
3701    /// [`PhysicsWorld::cast_ray`]; a negative radius, a zero direction or
3702    /// `max_t ≤ 0` gives `None`.
3703    #[must_use]
3704    pub fn cast_sphere(
3705        &self,
3706        center: Vec3Fix,
3707        radius: Fix128,
3708        direction: Vec3Fix,
3709        max_t: Fix128,
3710        filter: &RayFilter,
3711    ) -> Option<WorldShapeHit> {
3712        if radius.is_negative() {
3713            return None;
3714        }
3715        let direction = tame_direction(direction);
3716        if radius.is_zero() {
3717            return self
3718                .cast_ray(center, direction, max_t, filter)
3719                .map(|h| WorldShapeHit {
3720                    t: h.t,
3721                    point: h.point,
3722                    normal: h.normal,
3723                    target: h.target,
3724                    body: h.body,
3725                });
3726        }
3727        self.cast_core(center, center, radius, direction, max_t, filter)
3728    }
3729
3730    /// The nearest collider a capsule (segment `a`–`b` grown by `radius`) touches
3731    /// when it moves along `direction` for at most `max_t`. Coinciding ends are
3732    /// [`PhysicsWorld::cast_sphere`]; a negative radius, a zero direction or
3733    /// `max_t ≤ 0` gives `None`.
3734    #[must_use]
3735    pub fn cast_capsule(
3736        &self,
3737        a: Vec3Fix,
3738        b: Vec3Fix,
3739        radius: Fix128,
3740        direction: Vec3Fix,
3741        max_t: Fix128,
3742        filter: &RayFilter,
3743    ) -> Option<WorldShapeHit> {
3744        if a == b {
3745            return self.cast_sphere(a, radius, direction, max_t, filter);
3746        }
3747        if radius.is_negative() {
3748            return None;
3749        }
3750        self.cast_core(a, b, radius, tame_direction(direction), max_t, filter)
3751    }
3752
3753    /// Every collider a sphere of `radius` about `center` overlaps, sorted by
3754    /// target. A negative radius gives an empty list; radius `0` finds the solids
3755    /// `center` is strictly inside.
3756    #[must_use]
3757    pub fn overlap_sphere(
3758        &self,
3759        center: Vec3Fix,
3760        radius: Fix128,
3761        filter: &RayFilter,
3762    ) -> Vec<RayTarget> {
3763        let mut out = Vec::new();
3764        if radius.is_negative() {
3765            return out;
3766        }
3767        let query = core_box(center, center, radius);
3768        let cap = radius + radius + Fix128::ONE;
3769        for_each_target(self, filter, &query, |target, _, pieces| {
3770            if pieces
3771                .iter()
3772                .any(|p| p.dist(center, center, cap).within(radius))
3773            {
3774                out.push(target);
3775            }
3776        });
3777        out.sort_unstable();
3778        out
3779    }
3780
3781    /// Every collider `aabb` overlaps, sorted by target (see
3782    /// [`crate::world_shape_query`]). A box with `min > max` on any axis gives an
3783    /// empty list.
3784    #[must_use]
3785    pub fn overlap_aabb(&self, aabb: &AABB, filter: &RayFilter) -> Vec<RayTarget> {
3786        let mut out = Vec::new();
3787        if !box_is_valid(aabb) {
3788            return out;
3789        }
3790        for_each_target(self, filter, aabb, |target, _, pieces| {
3791            if pieces.iter().any(|p| p.meets_aabb(aabb)) {
3792                out.push(target);
3793            }
3794        });
3795        out.sort_unstable();
3796        out
3797    }
3798
3799    /// The colliders the capsule (segment `a`–`b` grown by `r`) overlaps, each
3800    /// with its penetration along the normal at the segment point nearest it,
3801    /// sorted by target (the pieces of one target, such as compound children, in
3802    /// their order); `None` when the segment itself meets a solid (no distance, so
3803    /// no push-out direction). Used by [`PhysicsWorld::move_character`] to push a capsule out.
3804    pub(crate) fn capsule_penetrations(
3805        &self,
3806        a: Vec3Fix,
3807        b: Vec3Fix,
3808        r: Fix128,
3809        filter: &RayFilter,
3810    ) -> Option<Vec<Penetration>> {
3811        let query = core_box(a, b, r);
3812        let cap = r + r + Fix128::ONE;
3813        let mut out = Vec::new();
3814        let mut core_inside = false;
3815        for_each_target(self, filter, &query, |target, _, pieces| {
3816            for piece in pieces {
3817                match piece.dist(a, b, cap) {
3818                    Dist::Inside => core_inside = true,
3819                    Dist::Outside { dist, normal, .. } if dist + TRACE_TOLERANCE < r => {
3820                        out.push(Penetration {
3821                            target,
3822                            depth: r - dist,
3823                            normal,
3824                        })
3825                    }
3826                    _ => {}
3827                }
3828            }
3829        });
3830        if core_inside {
3831            return None;
3832        }
3833        out.sort_by_key(|x| x.target);
3834        Some(out)
3835    }
3836
3837    /// The nearest hit of the core `a`–`b` grown by `r > 0` (or `r = 0` with
3838    /// `a ≠ b`) moving along `direction`.
3839    ///
3840    /// `direction` must have gone through `tame_direction` (both callers do):
3841    /// the `try_normalize` below squares it and wraps for components of `2³¹·⁵`
3842    /// or more.
3843    fn cast_core(
3844        &self,
3845        a: Vec3Fix,
3846        b: Vec3Fix,
3847        r: Fix128,
3848        direction: Vec3Fix,
3849        max_t: Fix128,
3850        filter: &RayFilter,
3851    ) -> Option<WorldShapeHit> {
3852        if max_t <= Fix128::ZERO {
3853            return None;
3854        }
3855        let d = direction.try_normalize()?;
3856        let settings = Settings {
3857            #[cfg(feature = "std")]
3858            sdf: filter.sdf,
3859        };
3860        let swept = {
3861            let start = core_box(a, b, r);
3862            let end = core_box(a + d * max_t, b + d * max_t, r);
3863            AABB::new(
3864                Vec3Fix::new(
3865                    min_fix(start.min.x, end.min.x),
3866                    min_fix(start.min.y, end.min.y),
3867                    min_fix(start.min.z, end.min.z),
3868                ),
3869                Vec3Fix::new(
3870                    max_fix(start.max.x, end.max.x),
3871                    max_fix(start.max.y, end.max.y),
3872                    max_fix(start.max.z, end.max.z),
3873                ),
3874            )
3875        };
3876        let start_cap = r + r + Fix128::ONE;
3877        let centre = half_vec(a + b);
3878        let mut best: Option<WorldShapeHit> = None;
3879        for_each_target(self, filter, &swept, |target, body, pieces| {
3880            let mut nearest: Option<Contact> = None;
3881            for piece in pieces {
3882                let c = if starts_overlapping(&piece.dist(a, b, start_cap), r) {
3883                    Some(Contact {
3884                        t: Fix128::ZERO,
3885                        point: centre,
3886                        normal: -d,
3887                    })
3888                } else {
3889                    piece.sweep(a, b, r, d, max_t, &settings)
3890                };
3891                if let Some(c) = c {
3892                    if nearest.is_none_or(|n| c.t < n.t) {
3893                        nearest = Some(c);
3894                    }
3895                }
3896            }
3897            if let Some(c) = nearest {
3898                let hit = WorldShapeHit {
3899                    t: c.t,
3900                    point: c.point,
3901                    normal: c.normal,
3902                    target,
3903                    body,
3904                };
3905                if best.is_none_or(|b| (hit.t, hit.target) < (b.t, b.target)) {
3906                    best = Some(hit);
3907                }
3908            }
3909        });
3910        best
3911    }
3912}
3913
3914#[cfg(test)]
3915mod tests {
3916    //! Closed-form unit tests of the swept and overlap queries: every expected
3917    //! distance, normal and point is written from the geometry by hand (the
3918    //! closed form is in a comment next to each assertion); none calls the code
3919    //! under test. A sphere of radius `r` swept along a unit direction first
3920    //! touches a solid `S` where its centre reaches the boundary of
3921    //! `S ⊕ ball(r)`.
3922    //!
3923    //! `EXACT = 1e-12` for closed-form paths (spheres, boxes against a point
3924    //! core, planes, triangles, cylinders); `ITER = 1e-8` for iterative ones (GJK,
3925    //! convex time of impact, height-field parts, torus arcs).
3926
3927    use super::*;
3928    use crate::box_collider::OrientedBox;
3929    use crate::collider::{ConvexHull, Sphere};
3930    use crate::compound::CompoundShape;
3931    use crate::solver::{PhysicsConfig, RigidBody};
3932
3933    const EXACT: f64 = 1e-12;
3934    const ITER: f64 = 1e-8;
3935
3936    fn fx(v: f64) -> Fix128 {
3937        Fix128::from_f64(v)
3938    }
3939
3940    fn v3(x: f64, y: f64, z: f64) -> Vec3Fix {
3941        Vec3Fix::new(fx(x), fx(y), fx(z))
3942    }
3943
3944    fn p3(p: [f64; 3]) -> Vec3Fix {
3945        v3(p[0], p[1], p[2])
3946    }
3947
3948    fn world() -> PhysicsWorld {
3949        PhysicsWorld::new(PhysicsConfig::default())
3950    }
3951
3952    fn all() -> RayFilter {
3953        RayFilter::default()
3954    }
3955
3956    fn sphere_cast(
3957        w: &PhysicsWorld,
3958        c: [f64; 3],
3959        r: f64,
3960        d: [f64; 3],
3961        max: f64,
3962    ) -> Option<WorldShapeHit> {
3963        w.cast_sphere(p3(c), fx(r), p3(d), fx(max), &all())
3964    }
3965
3966    fn capsule_cast(
3967        w: &PhysicsWorld,
3968        a: [f64; 3],
3969        b: [f64; 3],
3970        r: f64,
3971        d: [f64; 3],
3972        max: f64,
3973    ) -> Option<WorldShapeHit> {
3974        w.cast_capsule(p3(a), p3(b), fx(r), p3(d), fx(max), &all())
3975    }
3976
3977    fn overlap_s(w: &PhysicsWorld, c: [f64; 3], r: f64) -> Vec<RayTarget> {
3978        w.overlap_sphere(p3(c), fx(r), &all())
3979    }
3980
3981    fn overlap_b(w: &PhysicsWorld, lo: [f64; 3], hi: [f64; 3]) -> Vec<RayTarget> {
3982        w.overlap_aabb(&AABB::new(p3(lo), p3(hi)), &all())
3983    }
3984
3985    fn unit(v: [f64; 3]) -> [f64; 3] {
3986        let l = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
3987        [v[0] / l, v[1] / l, v[2] / l]
3988    }
3989
3990    #[track_caller]
3991    fn assert_vec(got: Vec3Fix, want: [f64; 3], tol: f64, what: &str) {
3992        let g = [got.x.to_f64(), got.y.to_f64(), got.z.to_f64()];
3993        for k in 0..3 {
3994            assert!(
3995                (g[k] - want[k]).abs() < tol,
3996                "{what} {g:?} but the closed form is {want:?}"
3997            );
3998        }
3999    }
4000
4001    /// A hit on `target` at `t` with `normal` (normalized here) and `point`.
4002    #[track_caller]
4003    fn assert_hit(
4004        hit: Option<WorldShapeHit>,
4005        target: RayTarget,
4006        t: f64,
4007        normal: [f64; 3],
4008        point: [f64; 3],
4009        tol: f64,
4010    ) {
4011        let h = hit.unwrap_or_else(|| panic!("expected a hit at t = {t}, got none"));
4012        assert_eq!(h.target, target, "target");
4013        assert!(
4014            (h.t.to_f64() - t).abs() < tol,
4015            "t = {} but the closed form is {t}",
4016            h.t.to_f64()
4017        );
4018        assert_vec(h.normal, unit(normal), tol, "normal");
4019        assert_vec(h.point, point, tol, "point");
4020    }
4021
4022    /// A capsule cast onto a flat face parallel to its segment: every point under
4023    /// the segment is a contact, so only `t`, the normal and the contact's height
4024    /// `y` are unique; its `x` lies between the ends' `x0..x1`.
4025    #[track_caller]
4026    fn assert_face_hit(
4027        hit: Option<WorldShapeHit>,
4028        target: RayTarget,
4029        t: f64,
4030        y: f64,
4031        x0: f64,
4032        x1: f64,
4033    ) {
4034        let h = hit.unwrap_or_else(|| panic!("expected a hit at t = {t}, got none"));
4035        assert_eq!(h.target, target, "target");
4036        assert!((h.t.to_f64() - t).abs() < ITER, "t = {}", h.t.to_f64());
4037        assert_vec(h.normal, [0.0, 1.0, 0.0], ITER, "normal");
4038        assert!(
4039            (h.point.y.to_f64() - y).abs() < ITER,
4040            "y = {}",
4041            h.point.y.to_f64()
4042        );
4043        let x = h.point.x.to_f64();
4044        assert!(x > x0 - ITER && x < x1 + ITER, "x = {x}");
4045    }
4046
4047    fn shaped(w: &mut PhysicsWorld, shape: Shape, pos: Vec3Fix) -> usize {
4048        w.add_shaped_body(&shape, Fix128::ONE, pos)
4049            .expect("valid shape")
4050    }
4051
4052    fn unit_box(w: &mut PhysicsWorld) -> usize {
4053        shaped(
4054            w,
4055            Shape::Box {
4056                half_extents: v3(1.0, 1.0, 1.0),
4057            },
4058            Vec3Fix::ZERO,
4059        )
4060    }
4061
4062    fn sphere_body(w: &mut PhysicsWorld, pos: Vec3Fix, r: f64) -> usize {
4063        w.add_body_with_radius(RigidBody::new_static(pos), fx(r))
4064    }
4065
4066    fn plane(w: &mut PhysicsWorld, normal: Vec3Fix, offset: f64) -> usize {
4067        w.add_static_collider(StaticCollider::Plane(PlaneCollider::new(
4068            normal,
4069            fx(offset),
4070        )))
4071    }
4072
4073    /// The two triangles of the floor square `[0, 4]²` at `y = 0`.
4074    fn floor_mesh(w: &mut PhysicsWorld) -> usize {
4075        let verts = [
4076            v3(0.0, 0.0, 0.0),
4077            v3(4.0, 0.0, 0.0),
4078            v3(4.0, 0.0, 4.0),
4079            v3(0.0, 0.0, 4.0),
4080        ];
4081        w.add_static_collider(StaticCollider::TriMesh(TriMesh::from_indexed(
4082            &verts,
4083            &[0, 1, 2, 0, 2, 3],
4084        )))
4085    }
4086
4087    /// One bilinear cell over `[0, 1]²` with corner heights `0, 0, 0, 1`: the
4088    /// twisted surface `y = x·z`.
4089    fn saddle_field(w: &mut PhysicsWorld) -> usize {
4090        let heights = vec![Fix128::ZERO, Fix128::ZERO, Fix128::ZERO, Fix128::ONE];
4091        w.add_static_collider(StaticCollider::HeightField(HeightField::new(
4092            heights,
4093            2,
4094            2,
4095            Fix128::ONE,
4096            Vec3Fix::ZERO,
4097        )))
4098    }
4099
4100    // ------------------------------------------------------------ sphere cast
4101
4102    #[test]
4103    fn sphere_cast_against_a_sphere_body() {
4104        let mut w = world();
4105        let b = sphere_body(&mut w, Vec3Fix::ZERO, 1.0);
4106        // oracle: head-on, the centre reaches |c| = R + r = 1.5: t = 10 − 1.5;
4107        // the direction (2, 0, 0) is normalized first.
4108        assert_hit(
4109            sphere_cast(&w, [-10.0, 0.0, 0.0], 0.5, [2.0, 0.0, 0.0], 100.0),
4110            RayTarget::Body(b),
4111            8.5,
4112            [-1.0, 0.0, 0.0],
4113            [-1.0, 0.0, 0.0],
4114            EXACT,
4115        );
4116        // oracle: on the line y = 0.6, x = −√(1.5² − 0.36) = −√1.89 at contact,
4117        // t = 10 − √1.89, normal c/1.5, contact R·normal.
4118        let s = 1.89f64.sqrt();
4119        let n = [-s / 1.5, 0.6 / 1.5, 0.0];
4120        assert_hit(
4121            sphere_cast(&w, [-10.0, 0.6, 0.0], 0.5, [1.0, 0.0, 0.0], 100.0),
4122            RayTarget::Body(b),
4123            10.0 - s,
4124            n,
4125            n,
4126            EXACT,
4127        );
4128        // oracle: the line y = 1.6 passes 1.6 > 1.5 from the centre: no contact.
4129        assert_eq!(
4130            sphere_cast(&w, [-10.0, 1.6, 0.0], 0.5, [1.0, 0.0, 0.0], 100.0),
4131            None
4132        );
4133        // oracle: the contact at t = 8.5 is beyond max_t = 5.
4134        assert_eq!(
4135            sphere_cast(&w, [-10.0, 0.0, 0.0], 0.5, [1.0, 0.0, 0.0], 5.0),
4136            None
4137        );
4138        let h = sphere_cast(&w, [-10.0, 0.0, 0.0], 0.5, [1.0, 0.0, 0.0], 100.0).expect("hit");
4139        assert_eq!(h.body, Some(b));
4140    }
4141
4142    #[test]
4143    fn degenerate_casts_give_no_hit() {
4144        let mut w = world();
4145        sphere_body(&mut w, Vec3Fix::ZERO, 1.0);
4146        let c = [-10.0, 0.0, 0.0];
4147        // A negative radius, a zero direction and max_t ≤ 0 give no hit.
4148        assert_eq!(sphere_cast(&w, c, -0.5, [1.0, 0.0, 0.0], 100.0), None);
4149        assert_eq!(sphere_cast(&w, c, 0.5, [0.0, 0.0, 0.0], 100.0), None);
4150        assert_eq!(sphere_cast(&w, c, 0.5, [1.0, 0.0, 0.0], 0.0), None);
4151        assert_eq!(sphere_cast(&w, c, 0.5, [1.0, 0.0, 0.0], -1.0), None);
4152        assert_eq!(
4153            capsule_cast(&w, c, [-10.0, 1.0, 0.0], -0.5, [1.0, 0.0, 0.0], 100.0),
4154            None
4155        );
4156        assert_eq!(
4157            capsule_cast(&w, c, [-10.0, 1.0, 0.0], 0.5, [1.0, 0.0, 0.0], 0.0),
4158            None
4159        );
4160    }
4161
4162    #[test]
4163    fn radius_zero_is_a_ray_and_a_huge_direction_is_normalized() {
4164        let mut w = world();
4165        let b = sphere_body(&mut w, Vec3Fix::ZERO, 1.0);
4166        // oracle: a ray from x = −10 meets the unit sphere at x = −1: t = 9.
4167        assert_hit(
4168            sphere_cast(&w, [-10.0, 0.0, 0.0], 0.0, [1.0, 0.0, 0.0], 100.0),
4169            RayTarget::Body(b),
4170            9.0,
4171            [-1.0, 0.0, 0.0],
4172            [-1.0, 0.0, 0.0],
4173            EXACT,
4174        );
4175        // oracle: the direction (1e7, 0, 0) is the unit +X: t = 8.5.
4176        assert_hit(
4177            sphere_cast(&w, [-10.0, 0.0, 0.0], 0.5, [1e7, 0.0, 0.0], 100.0),
4178            RayTarget::Body(b),
4179            8.5,
4180            [-1.0, 0.0, 0.0],
4181            [-1.0, 0.0, 0.0],
4182            EXACT,
4183        );
4184    }
4185
4186    #[test]
4187    fn a_cast_that_starts_overlapping_hits_at_zero() {
4188        let mut w = world();
4189        let b = sphere_body(&mut w, Vec3Fix::ZERO, 1.0);
4190        // oracle: the centre 0.5 from a body of radius 1: overlapping, t = 0,
4191        // normal −direction, point the cast centre.
4192        assert_hit(
4193            sphere_cast(&w, [0.5, 0.0, 0.0], 0.5, [0.0, 1.0, 0.0], 100.0),
4194            RayTarget::Body(b),
4195            0.0,
4196            [0.0, -1.0, 0.0],
4197            [0.5, 0.0, 0.0],
4198            EXACT,
4199        );
4200        // oracle: a capsule with an end inside: t = 0, point the segment midpoint.
4201        assert_hit(
4202            capsule_cast(
4203                &w,
4204                [0.0, 0.0, 0.0],
4205                [0.0, 4.0, 0.0],
4206                0.5,
4207                [1.0, 0.0, 0.0],
4208                100.0,
4209            ),
4210            RayTarget::Body(b),
4211            0.0,
4212            [-1.0, 0.0, 0.0],
4213            [0.0, 2.0, 0.0],
4214            EXACT,
4215        );
4216    }
4217
4218    #[test]
4219    fn a_cast_that_starts_touching_hits_only_when_it_moves_in() {
4220        let mut w = world();
4221        let b = sphere_body(&mut w, Vec3Fix::ZERO, 1.0);
4222        // The cast sphere of radius 0.5 at (−1.5, 0, 0) touches the body.
4223        let c = [-1.5, 0.0, 0.0];
4224        // oracle: moving in (+X): t = 0, normal −X, contact (−1, 0, 0).
4225        assert_hit(
4226            sphere_cast(&w, c, 0.5, [1.0, 0.0, 0.0], 100.0),
4227            RayTarget::Body(b),
4228            0.0,
4229            [-1.0, 0.0, 0.0],
4230            [-1.0, 0.0, 0.0],
4231            EXACT,
4232        );
4233        // oracle: moving away (−X) or along the tangent plane (+Y): no hit.
4234        assert_eq!(sphere_cast(&w, c, 0.5, [-1.0, 0.0, 0.0], 100.0), None);
4235        assert_eq!(sphere_cast(&w, c, 0.5, [0.0, 1.0, 0.0], 100.0), None);
4236
4237        // A sphere resting on a plane: down is a hit at 0, sideways and up not.
4238        let mut w = world();
4239        let p = plane(&mut w, Vec3Fix::UNIT_Y, 0.0);
4240        let c = [1.0, 0.5, 2.0];
4241        assert_hit(
4242            sphere_cast(&w, c, 0.5, [0.0, -1.0, 0.0], 100.0),
4243            RayTarget::StaticCollider(p),
4244            0.0,
4245            [0.0, 1.0, 0.0],
4246            [1.0, 0.0, 2.0],
4247            EXACT,
4248        );
4249        assert_eq!(sphere_cast(&w, c, 0.5, [1.0, 0.0, 0.0], 100.0), None);
4250        assert_eq!(sphere_cast(&w, c, 0.5, [0.0, 1.0, 0.0], 100.0), None);
4251
4252        // A sphere resting on the unit box top: down hits at 0, sideways not.
4253        let mut w = world();
4254        let bx = unit_box(&mut w);
4255        let c = [0.2, 1.5, 0.3];
4256        assert_hit(
4257            sphere_cast(&w, c, 0.5, [0.0, -1.0, 0.0], 100.0),
4258            RayTarget::Body(bx),
4259            0.0,
4260            [0.0, 1.0, 0.0],
4261            [0.2, 1.0, 0.3],
4262            EXACT,
4263        );
4264        assert_eq!(sphere_cast(&w, c, 0.5, [1.0, 0.0, 0.0], 100.0), None);
4265    }
4266
4267    #[test]
4268    fn sphere_cast_against_box_face_edge_and_turned_box() {
4269        let mut w = world();
4270        let b = unit_box(&mut w);
4271        // oracle: face x = −1 pushed out to x = −1.5: t = 8.5, contact on the face.
4272        assert_hit(
4273            sphere_cast(&w, [-10.0, 0.2, 0.3], 0.5, [1.0, 0.0, 0.0], 100.0),
4274            RayTarget::Body(b),
4275            8.5,
4276            [-1.0, 0.0, 0.0],
4277            [-1.0, 0.2, 0.3],
4278            EXACT,
4279        );
4280        // oracle: edge (x, y) = (−1, 1): the centre (x, 1.3) is 0.5 from it at
4281        // x = −1 − √(0.25 − 0.09) = −1.4, t = 8.6, normal (−0.4, 0.3)/0.5.
4282        assert_hit(
4283            sphere_cast(&w, [-10.0, 1.3, 0.0], 0.5, [1.0, 0.0, 0.0], 100.0),
4284            RayTarget::Body(b),
4285            8.6,
4286            [-0.8, 0.6, 0.0],
4287            [-1.0, 1.0, 0.0],
4288            EXACT,
4289        );
4290        // The same box turned 45° about Y: its vertical edge points at −X, at
4291        // x = −√2. oracle: t = 10 − √2 − 0.5, normal −X, contact (−√2, 0, 0).
4292        let mut w = world();
4293        let b = unit_box(&mut w);
4294        w.bodies[b].rotation = QuatFix::from_axis_angle(Vec3Fix::UNIT_Y, Fix128::HALF_PI.half());
4295        let s2 = 2f64.sqrt();
4296        assert_hit(
4297            sphere_cast(&w, [-10.0, 0.0, 0.0], 0.5, [1.0, 0.0, 0.0], 100.0),
4298            RayTarget::Body(b),
4299            9.5 - s2,
4300            [-1.0, 0.0, 0.0],
4301            [-s2, 0.0, 0.0],
4302            ITER,
4303        );
4304    }
4305
4306    #[test]
4307    fn sphere_cast_against_cylinder_side_cap_and_rim() {
4308        let mut w = world();
4309        let b = shaped(
4310            &mut w,
4311            Shape::Cylinder {
4312                radius: Fix128::ONE,
4313                half_height: Fix128::ONE,
4314            },
4315            Vec3Fix::ZERO,
4316        );
4317        // oracle: side x = −1 pushed out to −1.5: t = 8.5.
4318        assert_hit(
4319            sphere_cast(&w, [-10.0, 0.0, 0.0], 0.5, [1.0, 0.0, 0.0], 100.0),
4320            RayTarget::Body(b),
4321            8.5,
4322            [-1.0, 0.0, 0.0],
4323            [-1.0, 0.0, 0.0],
4324            EXACT,
4325        );
4326        // oracle: top cap y = 1 pushed up to 1.5: t = 8.5.
4327        assert_hit(
4328            sphere_cast(&w, [0.2, 10.0, 0.0], 0.5, [0.0, -1.0, 0.0], 100.0),
4329            RayTarget::Body(b),
4330            8.5,
4331            [0.0, 1.0, 0.0],
4332            [0.2, 1.0, 0.0],
4333            EXACT,
4334        );
4335        // oracle: rim (1, 1, 0): the centre (1.3, y, 0) is 0.5 from it at
4336        // y = 1 + √(0.25 − 0.09) = 1.4, t = 8.6, normal (0.3, 0.4)/0.5.
4337        assert_hit(
4338            sphere_cast(&w, [1.3, 10.0, 0.0], 0.5, [0.0, -1.0, 0.0], 100.0),
4339            RayTarget::Body(b),
4340            8.6,
4341            [0.6, 0.8, 0.0],
4342            [1.0, 1.0, 0.0],
4343            EXACT,
4344        );
4345        // oracle: a horizontal capsule over the cap: t = 5 − 1 − 0.5.
4346        assert_face_hit(
4347            capsule_cast(
4348                &w,
4349                [-0.5, 5.0, 0.0],
4350                [0.5, 5.0, 0.0],
4351                0.5,
4352                [0.0, -1.0, 0.0],
4353                100.0,
4354            ),
4355            RayTarget::Body(b),
4356            3.5,
4357            1.0,
4358            -0.5,
4359            0.5,
4360        );
4361        // Overlaps: 0.2 above the cap is within 0.3, not within 0.1.
4362        assert_eq!(
4363            overlap_s(&w, [0.0, 1.2, 0.0], 0.3),
4364            vec![RayTarget::Body(b)]
4365        );
4366        assert!(overlap_s(&w, [0.0, 1.2, 0.0], 0.1).is_empty());
4367        // Radius 0: a point strictly inside overlaps, one on the surface does not.
4368        assert_eq!(
4369            overlap_s(&w, [0.0, 0.0, 0.0], 0.0),
4370            vec![RayTarget::Body(b)]
4371        );
4372        assert!(overlap_s(&w, [0.0, 1.0, 0.0], 0.0).is_empty());
4373    }
4374
4375    #[test]
4376    fn sphere_and_capsule_casts_against_an_ellipsoid() {
4377        let mut w = world();
4378        let b = shaped(
4379            &mut w,
4380            Shape::Ellipsoid {
4381                radii: v3(1.0, 2.0, 1.0),
4382            },
4383            Vec3Fix::ZERO,
4384        );
4385        // oracle: the top (0, 2, 0) reached at y = 2.5: t = 10 − 2.5.
4386        assert_hit(
4387            sphere_cast(&w, [0.0, 10.0, 0.0], 0.5, [0.0, -1.0, 0.0], 100.0),
4388            RayTarget::Body(b),
4389            7.5,
4390            [0.0, 1.0, 0.0],
4391            [0.0, 2.0, 0.0],
4392            ITER,
4393        );
4394        // oracle: the side (1, 0, 0) reached by a vertical capsule at x = 1.5.
4395        assert_hit(
4396            capsule_cast(
4397                &w,
4398                [-10.0, -0.5, 0.0],
4399                [-10.0, 0.5, 0.0],
4400                0.5,
4401                [1.0, 0.0, 0.0],
4402                100.0,
4403            ),
4404            RayTarget::Body(b),
4405            8.5,
4406            [-1.0, 0.0, 0.0],
4407            [-1.0, 0.0, 0.0],
4408            ITER,
4409        );
4410        // oracle: the line x = 1.6 is 0.6 > 0.5 from the widest point: no contact.
4411        assert_eq!(
4412            sphere_cast(&w, [1.6, 10.0, 0.0], 0.5, [0.0, -1.0, 0.0], 100.0),
4413            None
4414        );
4415        // Overlaps: the side point (1, 0, 0) is 0.2 from (1.2, 0, 0).
4416        assert_eq!(
4417            overlap_s(&w, [1.2, 0.0, 0.0], 0.3),
4418            vec![RayTarget::Body(b)]
4419        );
4420        assert!(overlap_s(&w, [1.2, 0.0, 0.0], 0.1).is_empty());
4421        assert_eq!(
4422            overlap_b(&w, [0.9, -0.1, -0.1], [1.5, 0.1, 0.1]),
4423            vec![RayTarget::Body(b)]
4424        );
4425        assert!(overlap_b(&w, [1.1, -0.1, -0.1], [1.5, 0.1, 0.1]).is_empty());
4426    }
4427
4428    #[test]
4429    fn casts_and_overlaps_against_a_torus() {
4430        let mut w = world();
4431        // Ring of radius 2 in XZ, tube 0.5.
4432        let b = shaped(
4433            &mut w,
4434            Shape::Torus {
4435                major_radius: fx(2.0),
4436                minor_radius: fx(0.5),
4437            },
4438            Vec3Fix::ZERO,
4439        );
4440        // oracle: the top of the tube (2, 0.5, 0) reached at y = 0.75: t = 9.25.
4441        assert_hit(
4442            sphere_cast(&w, [2.0, 10.0, 0.0], 0.25, [0.0, -1.0, 0.0], 100.0),
4443            RayTarget::Body(b),
4444            9.25,
4445            [0.0, 1.0, 0.0],
4446            [2.0, 0.5, 0.0],
4447            EXACT,
4448        );
4449        // oracle: down the hole, 1.5 > 0.25 from the tube: no contact.
4450        assert_eq!(
4451            sphere_cast(&w, [0.0, 10.0, 0.0], 0.25, [0.0, -1.0, 0.0], 100.0),
4452            None
4453        );
4454        // oracle: a capsule across the tube, its point (2, y, 0) nearest the ring:
4455        // y = 0.5 + 0.25, t = 4.25.
4456        assert_hit(
4457            capsule_cast(
4458                &w,
4459                [1.5, 5.0, 0.0],
4460                [2.5, 5.0, 0.0],
4461                0.25,
4462                [0.0, -1.0, 0.0],
4463                100.0,
4464            ),
4465            RayTarget::Body(b),
4466            4.25,
4467            [0.0, 1.0, 0.0],
4468            [2.0, 0.5, 0.0],
4469            ITER,
4470        );
4471        // Overlaps: the axis is 1.5 from the tube.
4472        assert!(overlap_s(&w, [0.0, 0.0, 0.0], 1.4).is_empty());
4473        assert_eq!(
4474            overlap_s(&w, [0.0, 0.0, 0.0], 1.6),
4475            vec![RayTarget::Body(b)]
4476        );
4477        assert_eq!(
4478            overlap_s(&w, [2.0, 0.0, 0.0], 0.1),
4479            vec![RayTarget::Body(b)]
4480        );
4481        // oracle: the ring at 45° (√2, 0, √2) is (√2 − c)·√2 from the box corner
4482        // (c, ·, c): c = 1 gives 2 − √2 = 0.59 > 0.5 (no overlap), c = 1.2 gives
4483        // 0.30 < 0.5 (overlap).
4484        assert!(overlap_b(&w, [-1.0, -1.0, -1.0], [1.0, 1.0, 1.0]).is_empty());
4485        assert_eq!(
4486            overlap_b(&w, [-1.2, -1.0, -1.2], [1.2, 1.0, 1.2]),
4487            vec![RayTarget::Body(b)]
4488        );
4489        // oracle: a box far outside the bounding sphere (radius 2.5).
4490        assert!(overlap_b(&w, [5.0, 5.0, 5.0], [6.0, 6.0, 6.0]).is_empty());
4491    }
4492
4493    #[test]
4494    fn casts_and_overlaps_against_compound_children() {
4495        let mut w = world();
4496        // Box children at x = ±3, sphere children at z = ±3, a capsule child
4497        // along X and cube hull children at y = ±4: symmetric, centre of mass at
4498        // the origin.
4499        let mut c = CompoundShape::new();
4500        for s in [3.0, -3.0] {
4501            c.add_box(
4502                OrientedBox::new(Vec3Fix::ZERO, v3(0.5, 0.5, 0.5), QuatFix::IDENTITY),
4503                v3(s, 0.0, 0.0),
4504                QuatFix::IDENTITY,
4505            );
4506            c.add_sphere(
4507                Sphere::new(Vec3Fix::ZERO, fx(0.5)),
4508                v3(0.0, 0.0, s),
4509                QuatFix::IDENTITY,
4510            );
4511            let mut cube = Vec::new();
4512            for x in [-0.5, 0.5] {
4513                for y in [-0.5, 0.5] {
4514                    for z in [-0.5, 0.5] {
4515                        cube.push(v3(x, y, z));
4516                    }
4517                }
4518            }
4519            c.add_convex_hull(
4520                ConvexHull::new(cube),
4521                v3(0.0, s + s / 3.0, 0.0),
4522                QuatFix::IDENTITY,
4523            );
4524        }
4525        c.add_capsule(
4526            Capsule::new(v3(-1.5, 0.0, 0.0), v3(1.5, 0.0, 0.0), fx(0.5)),
4527            Vec3Fix::ZERO,
4528            QuatFix::IDENTITY,
4529        );
4530        let b = w
4531            .add_compound_body(&c, Fix128::ONE, Vec3Fix::ZERO)
4532            .expect("valid compound");
4533        let body = RayTarget::Body(b);
4534        // oracle: box child top y = 0.5 reached at 0.75: t = 9.25.
4535        assert_hit(
4536            sphere_cast(&w, [3.0, 10.0, 0.0], 0.25, [0.0, -1.0, 0.0], 100.0),
4537            body,
4538            9.25,
4539            [0.0, 1.0, 0.0],
4540            [3.0, 0.5, 0.0],
4541            EXACT,
4542        );
4543        // oracle: sphere child at (0, 0, 3): radius 0.75 above it, t = 9.25.
4544        assert_hit(
4545            sphere_cast(&w, [0.0, 10.0, 3.0], 0.25, [0.0, -1.0, 0.0], 100.0),
4546            body,
4547            9.25,
4548            [0.0, 1.0, 0.0],
4549            [0.0, 0.5, 3.0],
4550            EXACT,
4551        );
4552        // oracle: capsule child side at x = 1, from +Z: 0.75 from the segment,
4553        // t = 10 − 0.75.
4554        assert_hit(
4555            sphere_cast(&w, [1.0, 0.0, 10.0], 0.25, [0.0, 0.0, -1.0], 100.0),
4556            body,
4557            9.25,
4558            [0.0, 0.0, 1.0],
4559            [1.0, 0.0, 0.5],
4560            EXACT,
4561        );
4562        // oracle: hull cube at y = 4 (top 4.5) from the side: its face x = 0.5
4563        // reached at x = 0.75, t = 10 − 0.75.
4564        assert_hit(
4565            sphere_cast(&w, [10.0, 4.0, 0.0], 0.25, [-1.0, 0.0, 0.0], 100.0),
4566            body,
4567            9.25,
4568            [1.0, 0.0, 0.0],
4569            [0.5, 4.0, 0.0],
4570            ITER,
4571        );
4572        // oracle: a capsule over the box child at x = 3: t = 5 − 0.5 − 0.25.
4573        assert_face_hit(
4574            capsule_cast(
4575                &w,
4576                [2.8, 5.0, 0.0],
4577                [3.2, 5.0, 0.0],
4578                0.25,
4579                [0.0, -1.0, 0.0],
4580                100.0,
4581            ),
4582            body,
4583            4.25,
4584            0.5,
4585            2.8,
4586            3.2,
4587        );
4588        // oracle: a capsule crossing over the capsule child (below the hull cube
4589        // at y = 4): t = 2 − 0.75.
4590        assert_hit(
4591            capsule_cast(
4592                &w,
4593                [0.0, 2.0, -1.0],
4594                [0.0, 2.0, 1.0],
4595                0.25,
4596                [0.0, -1.0, 0.0],
4597                100.0,
4598            ),
4599            body,
4600            1.25,
4601            [0.0, 1.0, 0.0],
4602            [0.0, 0.5, 0.0],
4603            ITER,
4604        );
4605        // oracle: a capsule over the sphere child at (0, 0, −3): t = 5 − 0.75.
4606        assert_hit(
4607            capsule_cast(
4608                &w,
4609                [-1.0, 5.0, -3.0],
4610                [1.0, 5.0, -3.0],
4611                0.25,
4612                [0.0, -1.0, 0.0],
4613                100.0,
4614            ),
4615            body,
4616            4.25,
4617            [0.0, 1.0, 0.0],
4618            [0.0, 0.5, -3.0],
4619            EXACT,
4620        );
4621        // oracle: (2.1, 0, 0) is 0.1 from the capsule child (its end cap reaches
4622        // x = 2) and 0.4 from the box child (face x = 2.5): not within 0.05,
4623        // within 0.45.
4624        assert!(overlap_s(&w, [2.1, 0.0, 0.0], 0.05).is_empty());
4625        assert_eq!(overlap_s(&w, [2.1, 0.0, 0.0], 0.45), vec![body]);
4626        // Box overlaps: each child alone, and the empty space between them.
4627        assert_eq!(overlap_b(&w, [3.4, 0.4, -0.1], [3.6, 0.6, 0.1]), vec![body]);
4628        assert_eq!(overlap_b(&w, [-0.1, 0.4, 2.9], [0.1, 0.6, 3.1]), vec![body]);
4629        assert_eq!(overlap_b(&w, [1.0, 0.4, -0.1], [1.2, 0.6, 0.1]), vec![body]);
4630        assert_eq!(overlap_b(&w, [0.4, 4.4, -0.1], [0.6, 4.6, 0.1]), vec![body]);
4631        assert!(overlap_b(&w, [1.0, 2.0, 1.0], [2.0, 3.0, 2.0]).is_empty());
4632    }
4633
4634    #[test]
4635    fn casts_and_overlaps_against_a_plane() {
4636        let mut w = world();
4637        let p = plane(&mut w, Vec3Fix::UNIT_Y, 0.0);
4638        let sp = RayTarget::StaticCollider(p);
4639        // oracle: oblique (1, −1, 0)/√2: the height drops 4.5 after 4.5·√2.
4640        assert_hit(
4641            sphere_cast(&w, [0.0, 5.0, 0.0], 0.5, [1.0, -1.0, 0.0], 100.0),
4642            sp,
4643            4.5 * 2f64.sqrt(),
4644            [0.0, 1.0, 0.0],
4645            [4.5, 0.0, 0.0],
4646            EXACT,
4647        );
4648        // oracle: two-sided: from below the centre reaches y = −0.5.
4649        assert_hit(
4650            sphere_cast(&w, [0.0, -5.0, 0.0], 0.5, [0.0, 1.0, 0.0], 100.0),
4651            sp,
4652            4.5,
4653            [0.0, -1.0, 0.0],
4654            [0.0, 0.0, 0.0],
4655            EXACT,
4656        );
4657        // oracle: parallel 1 above: never closer than 1 > 0.5.
4658        assert_eq!(
4659            sphere_cast(&w, [0.0, 1.0, 0.0], 0.5, [1.0, 0.0, 0.0], 100.0),
4660            None
4661        );
4662        // oracle: capsule, the lower end (−1, 5, 0) reaches y = 0.5: t = 4.5.
4663        assert_hit(
4664            capsule_cast(
4665                &w,
4666                [-1.0, 5.0, 0.0],
4667                [1.0, 6.0, 0.0],
4668                0.5,
4669                [0.0, -1.0, 0.0],
4670                100.0,
4671            ),
4672            sp,
4673            4.5,
4674            [0.0, 1.0, 0.0],
4675            [-1.0, 0.0, 0.0],
4676            EXACT,
4677        );
4678        // oracle: a capsule whose segment crosses the plane starts overlapping.
4679        assert_hit(
4680            capsule_cast(
4681                &w,
4682                [0.0, -1.0, 0.0],
4683                [0.0, 1.0, 0.0],
4684                0.5,
4685                [1.0, 0.0, 0.0],
4686                100.0,
4687            ),
4688            sp,
4689            0.0,
4690            [-1.0, 0.0, 0.0],
4691            [0.0, 0.0, 0.0],
4692            EXACT,
4693        );
4694        // Overlaps: 0.4 above is within 0.5; 0.6 is not.
4695        assert_eq!(overlap_s(&w, [0.0, 0.4, 0.0], 0.5), vec![sp]);
4696        assert!(overlap_s(&w, [0.0, 0.6, 0.0], 0.5).is_empty());
4697        // oracle: a box straddling y = 0 overlaps, one resting on it does not.
4698        assert_eq!(overlap_b(&w, [0.0, -0.1, 0.0], [1.0, 0.1, 1.0]), vec![sp]);
4699        assert!(overlap_b(&w, [0.0, 0.0, 0.0], [1.0, 1.0, 1.0]).is_empty());
4700    }
4701
4702    #[test]
4703    fn casts_along_a_mesh_floor_cross_its_edges() {
4704        let mut w = world();
4705        let m = floor_mesh(&mut w);
4706        let sm = RayTarget::StaticCollider(m);
4707        // oracle: face region: the plane y = 0 reached at y = 0.5: t = 2.5.
4708        assert_hit(
4709            sphere_cast(&w, [1.0, 3.0, 3.0], 0.5, [0.0, -1.0, 0.0], 100.0),
4710            sm,
4711            2.5,
4712            [0.0, 1.0, 0.0],
4713            [1.0, 0.0, 3.0],
4714            EXACT,
4715        );
4716        // oracle: edge x = 0: the centre (x, 0.3, 1) is 0.5 from (0, 0, 1) at
4717        // x = −0.4, t = 9.6, normal (−0.8, 0.6, 0).
4718        assert_hit(
4719            sphere_cast(&w, [-10.0, 0.3, 1.0], 0.5, [1.0, 0.0, 0.0], 100.0),
4720            sm,
4721            9.6,
4722            [-0.8, 0.6, 0.0],
4723            [0.0, 0.0, 1.0],
4724            EXACT,
4725        );
4726        // oracle: a sphere resting on the floor sliding across the diagonal
4727        // edge, and a capsule doing the same: tangent, no hit.
4728        assert_eq!(
4729            sphere_cast(&w, [1.0, 0.5, 3.0], 0.5, [1.0, 0.0, -1.0], 2.0),
4730            None
4731        );
4732        assert_eq!(
4733            capsule_cast(
4734                &w,
4735                [1.0, 0.5, 2.0],
4736                [1.0, 1.5, 2.0],
4737                0.5,
4738                [1.0, 0.0, 0.0],
4739                2.0
4740            ),
4741            None
4742        );
4743        // oracle: resting and moving down: t = 0 on the floor.
4744        assert_hit(
4745            sphere_cast(&w, [1.0, 0.5, 3.0], 0.5, [0.0, -1.0, 0.0], 100.0),
4746            sm,
4747            0.0,
4748            [0.0, 1.0, 0.0],
4749            [1.0, 0.0, 3.0],
4750            EXACT,
4751        );
4752        // oracle: tilted capsule, lower end (2, 3, 1) reaches y = 0.5: t = 2.5.
4753        assert_hit(
4754            capsule_cast(
4755                &w,
4756                [2.0, 3.0, 1.0],
4757                [3.0, 4.0, 1.0],
4758                0.5,
4759                [0.0, -1.0, 0.0],
4760                100.0,
4761            ),
4762            sm,
4763            2.5,
4764            [0.0, 1.0, 0.0],
4765            [2.0, 0.0, 1.0],
4766            ITER,
4767        );
4768        // Overlaps: 0.3 above the floor is within 0.5, outside it 0.6 is not.
4769        assert_eq!(overlap_s(&w, [2.0, 0.3, 2.0], 0.5), vec![sm]);
4770        assert!(overlap_s(&w, [-0.6, 0.0, 2.0], 0.5).is_empty());
4771        assert_eq!(overlap_b(&w, [1.0, -0.1, 1.0], [2.0, 0.1, 2.0]), vec![sm]);
4772        assert!(overlap_b(&w, [1.0, 0.1, 1.0], [2.0, 1.0, 2.0]).is_empty());
4773    }
4774
4775    #[test]
4776    fn casts_and_overlaps_against_height_fields() {
4777        // oracle: flat field at 0.25 over [0, 4]²: t = 5 − 0.25 − 0.5.
4778        let mut w = world();
4779        let h = w.add_static_collider(StaticCollider::HeightField(HeightField::flat(
4780            5,
4781            5,
4782            Fix128::ONE,
4783            Vec3Fix::ZERO,
4784            fx(0.25),
4785        )));
4786        let sh = RayTarget::StaticCollider(h);
4787        assert_hit(
4788            sphere_cast(&w, [2.0, 5.0, 2.0], 0.5, [0.0, -1.0, 0.0], 100.0),
4789            sh,
4790            4.25,
4791            [0.0, 1.0, 0.0],
4792            [2.0, 0.25, 2.0],
4793            ITER,
4794        );
4795        // oracle: a horizontal capsule over it: the same t.
4796        assert_face_hit(
4797            capsule_cast(
4798                &w,
4799                [1.5, 5.0, 2.0],
4800                [2.5, 5.0, 2.0],
4801                0.5,
4802                [0.0, -1.0, 0.0],
4803                100.0,
4804            ),
4805            sh,
4806            4.25,
4807            0.25,
4808            1.5,
4809            2.5,
4810        );
4811        // oracle: resting on it and sliding across cells: no hit.
4812        assert_eq!(
4813            sphere_cast(&w, [0.5, 0.75, 0.5], 0.5, [1.0, 0.0, 0.0], 3.0),
4814            None
4815        );
4816        // oracle: outside the field's footprint: no hit.
4817        assert_eq!(
4818            sphere_cast(&w, [10.0, 5.0, 10.0], 0.5, [0.0, -1.0, 0.0], 100.0),
4819            None
4820        );
4821        assert_eq!(overlap_s(&w, [2.0, 0.5, 2.0], 0.3), vec![sh]);
4822        assert!(overlap_s(&w, [2.0, 0.6, 2.0], 0.3).is_empty());
4823        assert_eq!(overlap_b(&w, [1.0, 0.2, 1.0], [3.0, 0.3, 3.0]), vec![sh]);
4824        assert!(overlap_b(&w, [1.0, 0.3, 1.0], [3.0, 1.0, 3.0]).is_empty());
4825        assert!(overlap_b(&w, [5.0, 0.0, 5.0], [6.0, 1.0, 6.0]).is_empty());
4826    }
4827
4828    #[test]
4829    fn casts_and_overlaps_against_a_twisted_height_field_cell() {
4830        let mut w = world();
4831        let h = saddle_field(&mut w);
4832        let sh = RayTarget::StaticCollider(h);
4833        // The surface y = x·z at (0.5, 0.25, 0.5) has the unit normal
4834        // n = (−0.5, 1, −0.5)/√1.5. A sphere of radius 0.1 (its curvature 10
4835        // beats the surface's ±1, so the contact is unique) whose centre moves
4836        // down the line through (0.5, 0.25, 0.5) + 0.1·n touches there.
4837        let k = 1.5f64.sqrt();
4838        let n = [-0.5 / k, 1.0 / k, -0.5 / k];
4839        let r = 0.1;
4840        let cx = 0.5 + r * n[0];
4841        // oracle: t = 5 − (0.25 + r·n.y).
4842        assert_hit(
4843            sphere_cast(&w, [cx, 5.0, cx], r, [0.0, -1.0, 0.0], 100.0),
4844            sh,
4845            5.0 - (0.25 + r * n[1]),
4846            n,
4847            [0.5, 0.25, 0.5],
4848            ITER,
4849        );
4850        // oracle: a point 0.09 along n from the surface point is within 0.1 of
4851        // the surface, one 0.11 along n is not.
4852        let at = |s: f64| [0.5 + s * n[0], 0.25 + s * n[1], 0.5 + s * n[2]];
4853        assert_eq!(overlap_s(&w, at(0.09), 0.1), vec![sh]);
4854        assert!(overlap_s(&w, at(0.11), 0.1).is_empty());
4855        // oracle: the surface spans y ∈ [0, 1]: a box at y ∈ [0.9, 2] over the
4856        // corner (1, 1) meets it (height 1 there), one over (0, 0) does not.
4857        assert_eq!(overlap_b(&w, [0.9, 0.9, 0.9], [1.0, 2.0, 1.0]), vec![sh]);
4858        assert!(overlap_b(&w, [0.0, 0.9, 0.0], [0.1, 2.0, 0.1]).is_empty());
4859    }
4860
4861    #[cfg(feature = "std")]
4862    #[test]
4863    fn casts_and_overlaps_against_an_sdf() {
4864        use crate::sdf_collider::{ClosureSdf, SdfCollider};
4865        let mut w = world();
4866        // The unit-sphere field about the origin.
4867        w.sdf_colliders.push(SdfCollider::new_static(
4868            Box::new(ClosureSdf::new(
4869                |x, y, z| (x * x + y * y + z * z).sqrt() - 1.0,
4870                |x, y, z| {
4871                    let l = (x * x + y * y + z * z).sqrt();
4872                    (x / l, y / l, z / l)
4873                },
4874            )),
4875            Vec3Fix::ZERO,
4876            QuatFix::IDENTITY,
4877        ));
4878        let tol = f64::from(all().sdf.tolerance);
4879        // oracle: the centre reaches |c| = 1.5: t ∈ [3.5 − tol, 3.5].
4880        let h = sphere_cast(&w, [-5.0, 0.0, 0.0], 0.5, [1.0, 0.0, 0.0], 100.0).expect("hit");
4881        assert_eq!(h.target, RayTarget::Sdf(0));
4882        assert!(h.t.to_f64() > 3.5 - tol - 1e-5 && h.t.to_f64() < 3.5 + 1e-5);
4883        assert_vec(h.normal, [-1.0, 0.0, 0.0], 1e-3, "normal");
4884        // oracle: a vertical capsule at the same height: the same t.
4885        let h = capsule_cast(
4886            &w,
4887            [-5.0, -0.5, 0.0],
4888            [-5.0, 0.5, 0.0],
4889            0.5,
4890            [1.0, 0.0, 0.0],
4891            100.0,
4892        )
4893        .expect("hit");
4894        assert!(h.t.to_f64() > 3.5 - tol - 1e-4 && h.t.to_f64() < 3.5 + 1e-4);
4895        // oracle: the line y = 2 stays 2 − 1 = 1 > 0.5 from the surface.
4896        assert_eq!(
4897            sphere_cast(&w, [-5.0, 2.0, 0.0], 0.5, [1.0, 0.0, 0.0], 100.0),
4898            None
4899        );
4900        // Overlaps: (1.2, 0, 0) is 0.2 from the surface.
4901        assert_eq!(overlap_s(&w, [1.2, 0.0, 0.0], 0.3), vec![RayTarget::Sdf(0)]);
4902        assert!(overlap_s(&w, [1.2, 0.0, 0.0], 0.1).is_empty());
4903        assert_eq!(
4904            overlap_b(&w, [0.8, -0.1, -0.1], [1.2, 0.1, 0.1]),
4905            vec![RayTarget::Sdf(0)]
4906        );
4907        assert!(overlap_b(&w, [2.0, 2.0, 2.0], [3.0, 3.0, 3.0]).is_empty());
4908        // The filter can hide it.
4909        assert!(w
4910            .overlap_sphere(v3(1.2, 0.0, 0.0), fx(0.3), &all().with_sdf(false))
4911            .is_empty());
4912    }
4913
4914    #[test]
4915    fn capsule_cast_against_a_sphere_body_and_a_box() {
4916        let mut w = world();
4917        let b = sphere_body(&mut w, Vec3Fix::ZERO, 1.0);
4918        // oracle: the segment (1..3, y, 0): its end (1, y, 0) reaches 1.5 from
4919        // the centre at y = √1.25: t = 5 − √1.25, normal (1, √1.25, 0)/1.5.
4920        let s = 1.25f64.sqrt();
4921        let n = [1.0 / 1.5, s / 1.5, 0.0];
4922        assert_hit(
4923            capsule_cast(
4924                &w,
4925                [1.0, 5.0, 0.0],
4926                [3.0, 5.0, 0.0],
4927                0.5,
4928                [0.0, -1.0, 0.0],
4929                100.0,
4930            ),
4931            RayTarget::Body(b),
4932            5.0 - s,
4933            n,
4934            n,
4935            EXACT,
4936        );
4937        // oracle: coinciding ends are the sphere cast: t = 8.5.
4938        assert_hit(
4939            capsule_cast(
4940                &w,
4941                [-10.0, 0.0, 0.0],
4942                [-10.0, 0.0, 0.0],
4943                0.5,
4944                [1.0, 0.0, 0.0],
4945                100.0,
4946            ),
4947            RayTarget::Body(b),
4948            8.5,
4949            [-1.0, 0.0, 0.0],
4950            [-1.0, 0.0, 0.0],
4951            EXACT,
4952        );
4953        let mut w = world();
4954        let b = unit_box(&mut w);
4955        // oracle: horizontal capsule over the top face y = 1: t = 5 − 1 − 0.5.
4956        assert_face_hit(
4957            capsule_cast(
4958                &w,
4959                [-0.5, 5.0, 0.0],
4960                [0.5, 5.0, 0.0],
4961                0.5,
4962                [0.0, -1.0, 0.0],
4963                100.0,
4964            ),
4965            RayTarget::Body(b),
4966            3.5,
4967            1.0,
4968            -0.5,
4969            0.5,
4970        );
4971        // oracle: moving away from the box: no hit.
4972        assert_eq!(
4973            capsule_cast(
4974                &w,
4975                [-0.5, 5.0, 0.0],
4976                [0.5, 5.0, 0.0],
4977                0.5,
4978                [0.0, 1.0, 0.0],
4979                100.0
4980            ),
4981            None
4982        );
4983    }
4984
4985    #[test]
4986    fn overlaps_of_bodies_and_the_box_edge_gap() {
4987        let mut w = world();
4988        let s = sphere_body(&mut w, Vec3Fix::ZERO, 1.0);
4989        // oracle: (1.4, 0, 0) is 0.4 from the sphere: within 0.5, not 0.3.
4990        assert_eq!(
4991            overlap_s(&w, [1.4, 0.0, 0.0], 0.5),
4992            vec![RayTarget::Body(s)]
4993        );
4994        assert!(overlap_s(&w, [1.4, 0.0, 0.0], 0.3).is_empty());
4995        // A negative radius overlaps nothing.
4996        assert!(overlap_s(&w, [0.0, 0.0, 0.0], -1.0).is_empty());
4997        // oracle: the box [0.5, 2]³'s nearest point (0.5, 0.5, 0.5) is √0.75 =
4998        // 0.866 from the centre: inside radius 1.
4999        assert_eq!(
5000            overlap_b(&w, [0.5, 0.5, 0.5], [2.0, 2.0, 2.0]),
5001            vec![RayTarget::Body(s)]
5002        );
5003        // oracle: the box [0.6, 2]³: √1.08 = 1.04 > 1, no overlap.
5004        assert!(overlap_b(&w, [0.6, 0.6, 0.6], [2.0, 2.0, 2.0]).is_empty());
5005        // A box with min > max contains nothing.
5006        assert!(overlap_b(&w, [1.0, -1.0, -1.0], [-1.0, 1.0, 1.0]).is_empty());
5007
5008        let mut w = world();
5009        let b = unit_box(&mut w);
5010        // oracle: (1.3, 1.3, 0) is 0.3·√2 = 0.424 from the edge (1, 1, z): within
5011        // 0.5, not 0.4.
5012        assert_eq!(
5013            overlap_s(&w, [1.3, 1.3, 0.0], 0.5),
5014            vec![RayTarget::Body(b)]
5015        );
5016        assert!(overlap_s(&w, [1.3, 1.3, 0.0], 0.4).is_empty());
5017        // The box turned 45° about Y reaches x = √2 at z = 0: an AABB from
5018        // x = 1.3 meets it, one from x = 1.5 does not.
5019        w.bodies[b].rotation = QuatFix::from_axis_angle(Vec3Fix::UNIT_Y, Fix128::HALF_PI.half());
5020        assert_eq!(
5021            overlap_b(&w, [1.3, -0.5, -0.1], [2.0, 0.5, 0.1]),
5022            vec![RayTarget::Body(b)]
5023        );
5024        assert!(overlap_b(&w, [1.5, -0.5, -0.1], [2.0, 0.5, 0.1]).is_empty());
5025    }
5026
5027    #[test]
5028    fn filters_and_ties() {
5029        let mut w = world();
5030        let b = sphere_body(&mut w, Vec3Fix::ZERO, 1.0);
5031        let p = plane(&mut w, Vec3Fix::UNIT_Y, -5.0);
5032        // oracle: down from (0, 10, 0): the sphere first (t = 10 − 1.5), the
5033        // plane y = −5 at t = 14.5 when the body is excluded, nothing when the
5034        // static colliders are hidden too.
5035        let c = v3(0.0, 10.0, 0.0);
5036        let down = -Vec3Fix::UNIT_Y;
5037        let h = w
5038            .cast_sphere(c, fx(0.5), down, fx(100.0), &all())
5039            .expect("hit");
5040        assert_eq!(h.target, RayTarget::Body(b));
5041        let h = w
5042            .cast_sphere(c, fx(0.5), down, fx(100.0), &all().excluding_body(b))
5043            .expect("hit");
5044        assert_eq!(h.target, RayTarget::StaticCollider(p));
5045        assert!((h.t.to_f64() - 14.5).abs() < EXACT);
5046        assert_eq!(
5047            w.cast_sphere(
5048                c,
5049                fx(0.5),
5050                down,
5051                fx(100.0),
5052                &all().excluding_body(b).with_static(false)
5053            ),
5054            None
5055        );
5056        // Two identical bodies: the tie goes to the lower index; overlaps are
5057        // sorted by target.
5058        let mut w = world();
5059        let b0 = sphere_body(&mut w, Vec3Fix::ZERO, 1.0);
5060        let b1 = sphere_body(&mut w, Vec3Fix::ZERO, 1.0);
5061        let h = sphere_cast(&w, [-10.0, 0.0, 0.0], 0.5, [1.0, 0.0, 0.0], 100.0).expect("hit");
5062        assert_eq!(h.target, RayTarget::Body(b0));
5063        assert_eq!(
5064            overlap_s(&w, [0.0, 0.0, 0.0], 0.1),
5065            vec![RayTarget::Body(b0), RayTarget::Body(b1)]
5066        );
5067    }
5068
5069    #[test]
5070    fn capsule_penetrations_closed_forms() {
5071        let mut w = world();
5072        let p = plane(&mut w, Vec3Fix::UNIT_Y, 0.0);
5073        // oracle: the segment's lower end is 0.2 above the plane: depth
5074        // 0.3 − 0.2 = 0.1 along +Y.
5075        let pens = w
5076            .capsule_penetrations(v3(0.0, 0.2, 0.0), v3(0.0, 1.4, 0.0), fx(0.3), &all())
5077            .expect("the segment is outside");
5078        assert_eq!(pens.len(), 1);
5079        assert_eq!(pens[0].target, RayTarget::StaticCollider(p));
5080        assert!((pens[0].depth.to_f64() - 0.1).abs() < EXACT);
5081        assert_vec(pens[0].normal, [0.0, 1.0, 0.0], EXACT, "normal");
5082        // oracle: 0.4 above: no overlap.
5083        assert!(w
5084            .capsule_penetrations(v3(0.0, 0.4, 0.0), v3(0.0, 1.4, 0.0), fx(0.3), &all())
5085            .expect("outside")
5086            .is_empty());
5087        // oracle: a segment crossing the plane has no push-out direction.
5088        assert!(w
5089            .capsule_penetrations(v3(0.0, -0.2, 0.0), v3(0.0, 1.4, 0.0), fx(0.3), &all())
5090            .is_none());
5091    }
5092
5093    #[test]
5094    fn helper_closed_forms() {
5095        // aabb_gap: x gap 1, z gap 2, y overlapping: √5.
5096        let a = AABB::new(v3(0.0, 0.0, 0.0), v3(1.0, 1.0, 1.0));
5097        let b = AABB::new(v3(2.0, 0.5, 3.0), v3(3.0, 2.0, 4.0));
5098        assert!((aabb_gap(&a, &b).to_f64() - 5f64.sqrt()).abs() < EXACT);
5099        assert_eq!(aabb_gap(&a, &a), Fix128::ZERO);
5100        // box_entry: [0, 1]³ moving +X meets [3, 4] × [0, 1]² at t = 2; not
5101        // within max_t = 1; never when the Y ranges are apart.
5102        let target = AABB::new(v3(3.0, 0.0, 0.0), v3(4.0, 1.0, 1.0));
5103        let entry = box_entry(&a, Vec3Fix::UNIT_X, fx(10.0), &target).expect("meets");
5104        assert!((entry.to_f64() - 2.0).abs() < EXACT);
5105        assert_eq!(box_entry(&a, Vec3Fix::UNIT_X, Fix128::ONE, &target), None);
5106        let above = AABB::new(v3(3.0, 5.0, 0.0), v3(4.0, 6.0, 1.0));
5107        assert_eq!(box_entry(&a, Vec3Fix::UNIT_X, fx(10.0), &above), None);
5108        // oracle: moving −X toward [−4, −3]: t = 3.
5109        let behind = AABB::new(v3(-4.0, 0.0, 0.0), v3(-3.0, 1.0, 1.0));
5110        let entry = box_entry(&a, -Vec3Fix::UNIT_X, fx(10.0), &behind).expect("meets");
5111        assert!((entry.to_f64() - 3.0).abs() < EXACT);
5112        // ratio_within: 1/4 = 0.25; a tiny denominator is clamped to ±big.
5113        let tiny = Fix128 { hi: 0, lo: 1 };
5114        assert!((ratio_within(Fix128::ONE, fx(4.0), fx(5.0)).to_f64() - 0.25).abs() < EXACT);
5115        assert_eq!(ratio_within(Fix128::ONE, tiny, fx(5.0)), fx(5.0));
5116        assert_eq!(ratio_within(Fix128::NEG_ONE, tiny, fx(5.0)), fx(-5.0));
5117        // tame_direction: (2²¹, 0, 0) is scaled to (1, 0, 0); (3, 4, 0) is kept.
5118        assert_eq!(
5119            tame_direction(Vec3Fix::new(
5120                Fix128::from_int(1 << 21),
5121                Fix128::ZERO,
5122                Fix128::ZERO
5123            )),
5124            Vec3Fix::UNIT_X
5125        );
5126        assert_eq!(tame_direction(v3(3.0, 4.0, 0.0)), v3(3.0, 4.0, 0.0));
5127        assert!(box_is_valid(&a));
5128        assert!(!box_is_valid(&AABB::new(
5129            v3(0.0, 1.0, 0.0),
5130            v3(1.0, 0.0, 1.0)
5131        )));
5132        // closest_on_segment: clamped to the ends, degenerate segment.
5133        let (s0, s1) = (v3(0.0, 0.0, 0.0), v3(2.0, 0.0, 0.0));
5134        assert_eq!(
5135            closest_on_segment(s0, s1, v3(1.0, 5.0, 0.0)),
5136            v3(1.0, 0.0, 0.0)
5137        );
5138        assert_eq!(closest_on_segment(s0, s1, v3(-3.0, 1.0, 0.0)), s0);
5139        assert_eq!(closest_on_segment(s0, s0, v3(5.0, 5.0, 5.0)), s0);
5140        // segment_segment: crossing → None; skew at distance 1; point cases.
5141        assert!(segment_segment(
5142            v3(-1.0, 0.0, 0.0),
5143            v3(1.0, 0.0, 0.0),
5144            v3(0.0, -1.0, 0.0),
5145            v3(0.0, 1.0, 0.0)
5146        )
5147        .is_none());
5148        let (d, _, _) = segment_segment(
5149            v3(-1.0, 0.0, 0.0),
5150            v3(1.0, 0.0, 0.0),
5151            v3(0.0, -1.0, 1.0),
5152            v3(0.0, 1.0, 1.0),
5153        )
5154        .expect("apart");
5155        assert!((d.to_f64() - 1.0).abs() < ITER);
5156        let (d, _, _) =
5157            segment_segment(s0, s0, v3(3.0, 4.0, 0.0), v3(3.0, 4.0, 0.0)).expect("apart");
5158        assert!((d.to_f64() - 5.0).abs() < EXACT);
5159        let (d, _, _) =
5160            segment_segment(v3(1.0, 3.0, 0.0), v3(1.0, 3.0, 0.0), s0, s1).expect("apart");
5161        assert!((d.to_f64() - 3.0).abs() < EXACT);
5162        let (d, _, _) =
5163            segment_segment(s0, s1, v3(1.0, 0.0, 2.0), v3(1.0, 0.0, 2.0)).expect("apart");
5164        assert!((d.to_f64() - 2.0).abs() < EXACT);
5165        // point_box_local: on the face x = 1 it touches with normal +X; outside
5166        // it is the distance to the clamped point; inside it is Inside.
5167        let h = v3(1.0, 1.0, 1.0);
5168        match point_box_local(v3(1.0, 0.5, 0.5), h) {
5169            Dist::Outside { dist, normal, .. } => {
5170                assert_eq!(dist, Fix128::ZERO);
5171                assert_eq!(normal, Vec3Fix::UNIT_X);
5172            }
5173            other => panic!("expected touching, got {other:?}"),
5174        }
5175        assert!((point_box_local(v3(4.0, 5.0, 0.0), h).value().to_f64() - 5.0).abs() < EXACT);
5176        assert!(matches!(point_box_local(Vec3Fix::ZERO, h), Dist::Inside));
5177        // point_cylinder_local: on the cap; 2 beside the side.
5178        match point_cylinder_local(v3(0.0, -1.0, 0.0), Fix128::ONE, Fix128::ONE) {
5179            Dist::Outside { dist, normal, .. } => {
5180                assert_eq!(dist, Fix128::ZERO);
5181                assert_eq!(normal, -Vec3Fix::UNIT_Y);
5182            }
5183            other => panic!("expected touching, got {other:?}"),
5184        }
5185        assert!(
5186            (point_cylinder_local(v3(3.0, 0.0, 0.0), Fix128::ONE, Fix128::ONE)
5187                .value()
5188                .to_f64()
5189                - 2.0)
5190                .abs()
5191                < EXACT
5192        );
5193        // Dist: a distance shrunk past zero is Inside; a bound shrinks by it.
5194        assert!(matches!(
5195            Dist::toward(v3(2.0, 0.0, 0.0), Vec3Fix::ZERO).shrunk(fx(3.0)),
5196            Dist::Inside
5197        ));
5198        assert_eq!(
5199            Dist::AtLeast(fx(2.0)).shrunk(Fix128::ONE).value(),
5200            Fix128::ONE
5201        );
5202        assert!(!Dist::AtLeast(Fix128::ZERO).within(fx(10.0)));
5203        assert!(Dist::Inside.within(Fix128::ZERO));
5204        assert_eq!(Dist::Inside.value(), Fix128::NEG_ONE);
5205        assert!(matches!(
5206            Dist::toward(Vec3Fix::ZERO, Vec3Fix::ZERO),
5207            Dist::Inside
5208        ));
5209    }
5210
5211    /// The lower bound of [`gjk_bounds`] (less the support slack) is never
5212    /// above the distance: against an independent `f64` distance (the
5213    /// meridian half-plane of a cone or a cylinder) for points and segments
5214    /// near their sides, caps, rims and apex, aligned and turned, including
5215    /// segments of radius 0 through a cap's edge, where GJK stops on a face
5216    /// whose length is up to `25·2⁻³²` above the distance.
5217    #[test]
5218    #[allow(clippy::disallowed_methods)] // f64 oracle, not production arithmetic
5219    fn the_lower_bound_is_not_above_the_distance() {
5220        struct Rng(u64);
5221        impl Rng {
5222            fn u(&mut self) -> f64 {
5223                self.0 = self
5224                    .0
5225                    .wrapping_mul(6_364_136_223_846_793_005)
5226                    .wrapping_add(1_442_695_040_888_963_407);
5227                ((self.0 >> 11) as f64) / ((1u64 << 53) as f64)
5228            }
5229            fn r(&mut self, a: f64, b: f64) -> f64 {
5230                a + (b - a) * self.u()
5231            }
5232        }
5233        fn seg2(p: (f64, f64), a: (f64, f64), b: (f64, f64)) -> f64 {
5234            let (ex, ey) = (b.0 - a.0, b.1 - a.1);
5235            let t = (((p.0 - a.0) * ex + (p.1 - a.1) * ey) / (ex * ex + ey * ey)).clamp(0.0, 1.0);
5236            ((p.0 - a.0 - t * ex).powi(2) + (p.1 - a.1 - t * ey).powi(2)).sqrt()
5237        }
5238        // outside distance in the frame of the geometric centre (0 inside)
5239        let dist_local = |cone: bool, r: f64, h: f64, l: [f64; 3]| {
5240            let rho = (l[0] * l[0] + l[2] * l[2]).sqrt();
5241            if cone {
5242                if l[1] >= -h && rho * 2.0 * h <= r * (h - l[1]) {
5243                    return 0.0;
5244                }
5245                let p = (rho, l[1]);
5246                seg2(p, (0.0, h), (r, -h)).min(seg2(p, (r, -h), (0.0, -h)))
5247            } else {
5248                let (dx, dy) = ((rho - r).max(0.0), (l[1].abs() - h).max(0.0));
5249                (dx * dx + dy * dy).sqrt()
5250            }
5251        };
5252        let rot_inv = |q: [f64; 4], v: [f64; 3]| {
5253            let n = q.iter().map(|x| x * x).sum::<f64>();
5254            let (w, x, y, z) = (q[0], -q[1], -q[2], -q[3]);
5255            let t = [
5256                2.0 * (y * v[2] - z * v[1]),
5257                2.0 * (z * v[0] - x * v[2]),
5258                2.0 * (x * v[1] - y * v[0]),
5259            ];
5260            let o = [
5261                v[0] + w * t[0] + (y * t[2] - z * t[1]),
5262                v[1] + w * t[1] + (z * t[0] - x * t[2]),
5263                v[2] + w * t[2] + (x * t[1] - y * t[0]),
5264            ];
5265            [o[0] / (n * n), o[1] / (n * n), o[2] / (n * n)]
5266        };
5267        let rot = |q: [f64; 4], v: [f64; 3]| {
5268            let (w, x, y, z) = (q[0], q[1], q[2], q[3]);
5269            let t = [
5270                2.0 * (y * v[2] - z * v[1]),
5271                2.0 * (z * v[0] - x * v[2]),
5272                2.0 * (x * v[1] - y * v[0]),
5273            ];
5274            [
5275                v[0] + w * t[0] + (y * t[2] - z * t[1]),
5276                v[1] + w * t[1] + (z * t[0] - x * t[2]),
5277                v[2] + w * t[2] + (x * t[1] - y * t[0]),
5278            ]
5279        };
5280        let mut rng = Rng(0x1046_b0d5);
5281        let (mut checked, mut stalled) = (0usize, 0usize);
5282        for cone in [false, true] {
5283            for turned in [false, true] {
5284                let (r, h) = if cone { (1.0, 0.8) } else { (0.9, 0.7) };
5285                let shape = if cone {
5286                    Shape::Cone {
5287                        radius: fx(r),
5288                        half_height: fx(h),
5289                    }
5290                } else {
5291                    Shape::Cylinder {
5292                        radius: fx(r),
5293                        half_height: fx(h),
5294                    }
5295                };
5296                let rotation = if turned {
5297                    let k = (0.36f64 + 0.49 + 0.04).sqrt();
5298                    QuatFix::from_axis_angle(v3(0.6 / k, 0.7 / k, -0.2 / k), fx(0.9))
5299                } else {
5300                    QuatFix::IDENTITY
5301                };
5302                let posed = PosedShape {
5303                    shape,
5304                    position: v3(0.25, -0.5, 0.75),
5305                    rotation,
5306                };
5307                let (center, _) = posed_frame(&posed);
5308                let c = [center.x.to_f64(), center.y.to_f64(), center.z.to_f64()];
5309                let q = [
5310                    rotation.w.to_f64(),
5311                    rotation.x.to_f64(),
5312                    rotation.y.to_f64(),
5313                    rotation.z.to_f64(),
5314                ];
5315                let local = |p: [f64; 3]| rot_inv(q, [p[0] - c[0], p[1] - c[1], p[2] - c[2]]);
5316                let world = |l: [f64; 3]| {
5317                    let w = rot(q, l);
5318                    [w[0] + c[0], w[1] + c[1], w[2] + c[2]]
5319                };
5320                let slack = round_support_slack(&posed);
5321                for _ in 0..300 {
5322                    // a point near the side, a cap or base, a rim or the apex, just
5323                    // outside or just inside, and a segment along the axis from it
5324                    // (radius 0 through a cap's edge) or across
5325                    let phi = rng.r(0.0, std::f64::consts::TAU);
5326                    let pick = rng.u();
5327                    let (rho, y) = if pick < 0.3 {
5328                        (
5329                            if cone { r * rng.u() } else { r },
5330                            if cone { 0.0 } else { rng.r(-h, h) },
5331                        )
5332                    } else if pick < 0.6 {
5333                        (rng.r(0.0, r), -h)
5334                    } else if pick < 0.9 {
5335                        (r, -h)
5336                    } else if cone {
5337                        (0.0, h)
5338                    } else {
5339                        (r, h)
5340                    };
5341                    let y = if cone && pick < 0.3 {
5342                        h - 2.0 * h * rho / r
5343                    } else {
5344                        y
5345                    };
5346                    let off = [rng.r(-1.0, 1.0), rng.r(-1.0, 1.0), rng.r(-1.0, 1.0)];
5347                    let e = 2f64.powi(-(20 + (rng.u() * 14.0) as i32));
5348                    let p = [
5349                        rho * phi.cos() + e * off[0],
5350                        y + e * off[1],
5351                        rho * phi.sin() + e * off[2],
5352                    ];
5353                    let (pa, pb) = if rng.u() < 0.5 {
5354                        (p, p)
5355                    } else if rng.u() < 0.5 {
5356                        (p, [p[0], p[1] + 0.3 * y.signum(), p[2]])
5357                    } else {
5358                        let s = [rng.r(-0.2, 0.2), rng.r(-0.2, 0.2), rng.r(-0.2, 0.2)];
5359                        (p, [p[0] + s[0], p[1] + s[1], p[2] + s[2]])
5360                    };
5361                    let (wa, wb) = (world(pa), world(pb));
5362                    let (a, b) = (v3(wa[0], wa[1], wa[2]), v3(wb[0], wb[1], wb[2]));
5363                    let (fa, fb) = (
5364                        local([a.x.to_f64(), a.y.to_f64(), a.z.to_f64()]),
5365                        local([b.x.to_f64(), b.y.to_f64(), b.z.to_f64()]),
5366                    );
5367                    // the segment's distance: its minimum along it (convex), by
5368                    // ternary search
5369                    let at = |s: f64| {
5370                        dist_local(
5371                            cone,
5372                            r,
5373                            h,
5374                            [
5375                                fa[0] + s * (fb[0] - fa[0]),
5376                                fa[1] + s * (fb[1] - fa[1]),
5377                                fa[2] + s * (fb[2] - fa[2]),
5378                            ],
5379                        )
5380                    };
5381                    let (mut lo, mut hi) = (0.0f64, 1.0f64);
5382                    for _ in 0..200 {
5383                        let (m1, m2) = (lo + (hi - lo) / 3.0, hi - (hi - lo) / 3.0);
5384                        if at(m1) <= at(m2) {
5385                            hi = m2
5386                        } else {
5387                            lo = m1
5388                        }
5389                    }
5390                    let truth = at(0.5 * (lo + hi)).min(at(0.0)).min(at(1.0));
5391                    let (found, bound) = convex_bounds(a, b, &posed, slack);
5392                    if let Dist::Outside { dist, .. } = found {
5393                        checked += 1;
5394                        let (lower, dist) = (bound.lower.to_f64(), dist.to_f64());
5395                        if dist - lower > 2f64.powi(-32) {
5396                            stalled += 1;
5397                        }
5398                        // f64 rounding of the inputs and the truth: below 2⁻⁵⁰
5399                        assert!(
5400                            lower <= truth + 2f64.powi(-50),
5401                            "lower {lower:e} above the distance {truth:e} by {:e} (cone {cone}, turned {turned})",
5402                            lower - truth
5403                        );
5404                    }
5405                }
5406            }
5407        }
5408        // not vacuous: most samples are outside, and some have the bounds apart
5409        assert!(checked > 500, "{checked} outside");
5410        assert!(
5411            stalled > 0,
5412            "no sample with the lower bound below the distance"
5413        );
5414    }
5415
5416    /// A tetrahedron just thicker than the flatness `h = 2⁻³²` of
5417    /// [`closest_on_tetrahedron`] is not flat: one around the origin is
5418    /// inside (`None`), whatever its least height between `2⁻³⁰` and `2⁻¹⁶`.
5419    /// Taken as flat, its nearest face would be returned at a distance up to
5420    /// half its height, and GJK would report the shapes apart by that much.
5421    #[test]
5422    fn a_tetrahedron_just_thicker_than_flat_holds_the_origin() {
5423        let vertex = |w: Vec3Fix| Vertex {
5424            w,
5425            a: w,
5426            b: Vec3Fix::ZERO,
5427        };
5428        for e in [17, 20, 24, 27] {
5429            let h = 1.0 / ((1u64 << e) as f64);
5430            // a base triangle below the origin and an apex above it
5431            let pts = [
5432                v3(-1.0, -1.0, -0.5 * h),
5433                v3(1.0, -1.0, -0.5 * h),
5434                v3(0.0, 1.0, -0.5 * h),
5435                v3(0.0, 0.0, 0.5 * h),
5436            ];
5437            // precondition: every height is above 2⁻³² (and not by much)
5438            let least = [(0, 1, 2, 3), (0, 2, 3, 1), (0, 3, 1, 2), (1, 3, 2, 0)]
5439                .iter()
5440                .map(|&(p, q, r, o)| {
5441                    let n = (pts[q] - pts[p]).cross(pts[r] - pts[p]);
5442                    (n.dot(pts[o] - pts[p]).abs() / n.length()).to_f64()
5443                })
5444                .fold(f64::MAX, f64::min);
5445            assert!(
5446                least > 2.0 / 4_294_967_296.0 && least < 1.0 / 32768.0,
5447                "least height {least:e} for h = 2^-{e}"
5448            );
5449            let [a, b, c, d] = pts.map(vertex);
5450            let got = closest_on_tetrahedron(a, b, c, d, 0);
5451            assert!(
5452                got.is_none(),
5453                "h = 2^-{e} (least height {least:e}): the origin is inside, got the face {:?}",
5454                got.map(|n| nearest_point(&n).length().to_f64())
5455            );
5456        }
5457    }
5458
5459    /// An independent `f64` reference for a cone and a cylinder (solids of
5460    /// revolution): the distance of a point is the distance in its meridian
5461    /// half-plane `(ρ, y)` to a triangle or a rectangle, a segment's is the
5462    /// minimum of that convex function along it (ternary search), and along a
5463    /// straight path the gap is convex in time, so its minimum and first zero
5464    /// are found by ternary search and bisection. `f64` is exact to about
5465    /// `2⁻⁵²` here, far below the `2⁻³²` of the casts.
5466    #[allow(clippy::disallowed_methods)] // f64 oracle, not production arithmetic
5467    mod round_reference {
5468        use super::*;
5469
5470        /// `2⁻³²`.
5471        pub(super) const TOL: f64 = 1.0 / 4_294_967_296.0;
5472
5473        /// A cone (apex at `hh`, base of radius `r` at `−hh` about its
5474        /// geometric centre, which is `hh/2` above the body position) or a
5475        /// cylinder (about the body position).
5476        #[derive(Clone, Copy, Debug)]
5477        pub(super) struct Solid {
5478            pub(super) cone: bool,
5479            pub(super) r: f64,
5480            pub(super) hh: f64,
5481        }
5482
5483        fn seg2(p: (f64, f64), a: (f64, f64), b: (f64, f64)) -> f64 {
5484            let (ex, ey) = (b.0 - a.0, b.1 - a.1);
5485            let u = (((p.0 - a.0) * ex + (p.1 - a.1) * ey) / (ex * ex + ey * ey)).clamp(0.0, 1.0);
5486            ((p.0 - a.0 - u * ex).powi(2) + (p.1 - a.1 - u * ey).powi(2)).sqrt()
5487        }
5488
5489        impl Solid {
5490            pub(super) fn shape(self) -> Shape {
5491                let (radius, half_height) = (fx(self.r), fx(self.hh));
5492                if self.cone {
5493                    Shape::Cone {
5494                        radius,
5495                        half_height,
5496                    }
5497                } else {
5498                    Shape::Cylinder {
5499                        radius,
5500                        half_height,
5501                    }
5502                }
5503            }
5504
5505            /// The signed distance of `l` in the frame of the geometric
5506            /// centre (negative inside).
5507            pub(super) fn signed(self, l: [f64; 3]) -> f64 {
5508                let (r, h) = (self.r, self.hh);
5509                let rho = (l[0] * l[0] + l[2] * l[2]).sqrt();
5510                if self.cone {
5511                    let p = (rho, l[1]);
5512                    let (a, b, c) = ((0.0, -h), (0.0, h), (r, -h));
5513                    let to_side = seg2(p, b, c);
5514                    let to_base = seg2(p, c, a);
5515                    if p.1 >= -h && p.0 * 2.0 * h <= r * (h - p.1) {
5516                        -to_side.min(to_base)
5517                    } else {
5518                        seg2(p, a, b).min(to_side).min(to_base)
5519                    }
5520                } else {
5521                    let (dx, dy) = (rho - r, l[1].abs() - h);
5522                    let out = (dx.max(0.0).powi(2) + dy.max(0.0).powi(2)).sqrt();
5523                    out + dx.max(dy).min(0.0)
5524                }
5525            }
5526        }
5527
5528        /// `v` turned by the quaternion `q = (w, x, y, z)`, or by its inverse.
5529        pub(super) fn rotate(q: [f64; 4], v: [f64; 3], inverse: bool) -> [f64; 3] {
5530            let s = if inverse { -1.0 } else { 1.0 };
5531            let (w, x, y, z) = (q[0], s * q[1], s * q[2], s * q[3]);
5532            let t = [
5533                2.0 * (y * v[2] - z * v[1]),
5534                2.0 * (z * v[0] - x * v[2]),
5535                2.0 * (x * v[1] - y * v[0]),
5536            ];
5537            [
5538                v[0] + w * t[0] + (y * t[2] - z * t[1]),
5539                v[1] + w * t[1] + (z * t[0] - x * t[2]),
5540                v[2] + w * t[2] + (x * t[1] - y * t[0]),
5541            ]
5542        }
5543
5544        pub(super) fn f3(v: Vec3Fix) -> [f64; 3] {
5545            [v.x.to_f64(), v.y.to_f64(), v.z.to_f64()]
5546        }
5547
5548        /// A solid posed at `position` turned by `turn`, in a world and in `f64`.
5549        pub(super) struct Scene {
5550            pub(super) solid: Solid,
5551            pub(super) world: PhysicsWorld,
5552            pub(super) posed: PosedShape,
5553            position: [f64; 3],
5554            q: [f64; 4],
5555        }
5556
5557        impl Scene {
5558            pub(super) fn new(solid: Solid, position: [f64; 3], turn: QuatFix) -> Self {
5559                let mut world = world();
5560                let i = world.add_body(RigidBody::new_static(p3(position)));
5561                assert!(world.set_body_shape(i, &solid.shape()));
5562                world.bodies[i].rotation = turn;
5563                let r = world.bodies[i].rotation;
5564                Self {
5565                    solid,
5566                    world,
5567                    posed: PosedShape {
5568                        shape: solid.shape(),
5569                        position: p3(position),
5570                        rotation: r,
5571                    },
5572                    position: f3(p3(position)),
5573                    q: [r.w.to_f64(), r.x.to_f64(), r.y.to_f64(), r.z.to_f64()],
5574                }
5575            }
5576
5577            /// Turned about `axis` by `angle`.
5578            pub(super) fn turned(
5579                solid: Solid,
5580                position: [f64; 3],
5581                axis: [f64; 3],
5582                angle: f64,
5583            ) -> Self {
5584                let turn = QuatFix::from_axis_angle(p3(unit(axis)), fx(angle));
5585                Self::new(solid, position, turn)
5586            }
5587
5588            /// The signed distance of the world point `p`.
5589            pub(super) fn point(&self, p: [f64; 3]) -> f64 {
5590                let o = self.position;
5591                let mut l = rotate(self.q, [p[0] - o[0], p[1] - o[1], p[2] - o[2]], true);
5592                if self.solid.cone {
5593                    l[1] -= 0.5 * self.solid.hh;
5594                }
5595                self.solid.signed(l)
5596            }
5597
5598            /// The signed distance of the segment `a`–`b`.
5599            pub(super) fn segment(&self, a: [f64; 3], b: [f64; 3]) -> f64 {
5600                let at = |s: f64| {
5601                    self.point([
5602                        a[0] + s * (b[0] - a[0]),
5603                        a[1] + s * (b[1] - a[1]),
5604                        a[2] + s * (b[2] - a[2]),
5605                    ])
5606                };
5607                if a == b {
5608                    return at(0.0);
5609                }
5610                let (mut lo, mut hi) = (0.0f64, 1.0f64);
5611                for _ in 0..100 {
5612                    let m1 = lo + (hi - lo) / 3.0;
5613                    let m2 = hi - (hi - lo) / 3.0;
5614                    if at(m1) <= at(m2) {
5615                        hi = m2;
5616                    } else {
5617                        lo = m1;
5618                    }
5619                }
5620                at(0.5 * (lo + hi)).min(at(0.0)).min(at(1.0))
5621            }
5622        }
5623
5624        /// A cast of the core `a`–`b` grown by `r` along `d` (normalized by
5625        /// the cast) up to `max_t`: the inputs given to the code under test and
5626        /// the same in `f64`.
5627        pub(super) struct Cast {
5628            fixed: [Vec3Fix; 3],
5629            a: [f64; 3],
5630            b: [f64; 3],
5631            d: [f64; 3],
5632            r: f64,
5633            max_t: f64,
5634        }
5635
5636        impl Cast {
5637            pub(super) fn new(a: [f64; 3], b: [f64; 3], d: [f64; 3], r: f64, max_t: f64) -> Self {
5638                let fixed = [p3(a), p3(b), p3(d)];
5639                Self {
5640                    a: f3(fixed[0]),
5641                    b: f3(fixed[1]),
5642                    d: unit(f3(fixed[2])),
5643                    fixed,
5644                    r,
5645                    max_t,
5646                }
5647            }
5648
5649            /// The gap at `t`.
5650            pub(super) fn gap(&self, s: &Scene, t: f64) -> f64 {
5651                let mv = |p: [f64; 3]| {
5652                    [
5653                        p[0] + t * self.d[0],
5654                        p[1] + t * self.d[1],
5655                        p[2] + t * self.d[2],
5656                    ]
5657                };
5658                s.segment(mv(self.a), mv(self.b)) - self.r
5659            }
5660
5661            /// The least gap on `[0, max_t]`.
5662            pub(super) fn least(&self, s: &Scene) -> f64 {
5663                let (mut lo, mut hi) = (0.0f64, self.max_t);
5664                for _ in 0..120 {
5665                    let m1 = lo + (hi - lo) / 3.0;
5666                    let m2 = hi - (hi - lo) / 3.0;
5667                    if self.gap(s, m1) <= self.gap(s, m2) {
5668                        hi = m2;
5669                    } else {
5670                        lo = m1;
5671                    }
5672                }
5673                self.gap(s, 0.5 * (lo + hi))
5674            }
5675
5676            /// The time of the code under test's hit.
5677            pub(super) fn run(&self, s: &Scene) -> Option<f64> {
5678                let f = RayFilter::default();
5679                let (r, max_t) = (fx(self.r), fx(self.max_t));
5680                let h = if self.fixed[0] == self.fixed[1] {
5681                    s.world
5682                        .cast_sphere(self.fixed[0], r, self.fixed[2], max_t, &f)
5683                } else {
5684                    s.world
5685                        .cast_capsule(self.fixed[0], self.fixed[1], r, self.fixed[2], max_t, &f)
5686                };
5687                h.map(|h| h.t.to_f64())
5688            }
5689        }
5690    }
5691
5692    /// The exact distance of a point to a cylinder and a cone in their frame
5693    /// ([`point_cylinder_exact`], [`point_cone_exact`]) against the meridian
5694    /// closed form: outside, the distance, a nearest point on the surface and
5695    /// the normal from it toward the point; inside or on the surface, `Inside`.
5696    /// Points beside, above and below each, at their rims, on the axis (where
5697    /// the part across it is 0) and beyond the cone's apex.
5698    #[test]
5699    #[allow(clippy::disallowed_methods)] // f64 oracle, not production arithmetic
5700    fn point_distances_to_a_cylinder_and_a_cone_are_their_meridian_closed_forms() {
5701        use round_reference::Solid;
5702        let solids = [
5703            Solid {
5704                cone: false,
5705                r: 0.9,
5706                hh: 0.7,
5707            },
5708            Solid {
5709                cone: true,
5710                r: 1.0,
5711                hh: 0.8,
5712            },
5713        ];
5714        // (ρ, y) in the meridian half-plane, turned about the axis by (c, s)
5715        let meridian = [
5716            (1.5, 0.1),
5717            (0.3, 1.4),
5718            (0.3, -1.6),
5719            (1.3, 1.2),
5720            (1.4, -1.1),
5721            (0.0, 2.0),
5722            (0.0, -2.0),
5723            (0.2, 0.0),
5724            (0.0, 0.0),
5725            (0.7, 0.5),
5726            (2.0, 2.0),
5727            (0.05, 1.3),
5728            (1.0, -0.79),
5729        ];
5730        let turns = [(1.0, 0.0), (0.6, 0.8), (-0.28, 0.96), (0.0, -1.0)];
5731        let (mut outside, mut inside) = (0, 0);
5732        for solid in solids {
5733            let (radius, hh) = (fx(solid.r), fx(solid.hh));
5734            for &(rho, y) in &meridian {
5735                for &(c, s) in &turns {
5736                    let p = [rho * c, y, rho * s];
5737                    let want = solid.signed(p);
5738                    let got = if solid.cone {
5739                        point_cone_exact(p3(p), radius, hh)
5740                    } else {
5741                        point_cylinder_exact(p3(p), radius, hh)
5742                    };
5743                    match got {
5744                        Dist::Outside {
5745                            dist,
5746                            point,
5747                            normal,
5748                        } => {
5749                            outside += 1;
5750                            assert!(want > 0.0, "{solid:?} {p:?}: outside, closed form {want}");
5751                            assert!(
5752                                (dist.to_f64() - want).abs() < EXACT,
5753                                "{solid:?} {p:?}: distance {} but the closed form is {want}",
5754                                dist.to_f64()
5755                            );
5756                            let q = round_reference::f3(point);
5757                            assert!(
5758                                solid.signed(q).abs() < EXACT,
5759                                "{solid:?} {p:?}: nearest point {q:?} is {} off the surface",
5760                                solid.signed(q)
5761                            );
5762                            let to = [
5763                                (p[0] - q[0]) / want,
5764                                (p[1] - q[1]) / want,
5765                                (p[2] - q[2]) / want,
5766                            ];
5767                            assert_vec(normal, to, 1e-10, "normal");
5768                        }
5769                        Dist::Inside => {
5770                            inside += 1;
5771                            assert!(want <= 0.0, "{solid:?} {p:?}: inside, closed form {want}");
5772                        }
5773                        Dist::AtLeast(_) => panic!("{solid:?} {p:?}: a bound, not a distance"),
5774                    }
5775                }
5776            }
5777            // on the surface: the cylinder's cap, the cone's base
5778            let on = if solid.cone {
5779                point_cone_exact(v3(0.5, -0.8, 0.0), radius, hh)
5780            } else {
5781                point_cylinder_exact(v3(0.3, 0.7, 0.0), radius, hh)
5782            };
5783            assert!(matches!(on, Dist::Inside), "{solid:?}: on the surface");
5784        }
5785        // not vacuous: both kinds of answer were checked
5786        assert!(
5787            outside >= 70 && inside >= 10,
5788            "{outside} outside, {inside} inside"
5789        );
5790    }
5791
5792    /// [`exact_round_dist`] of points and segments to a posed cylinder and cone
5793    /// (aligned, turned, moved) against the `f64` reference: the least distance
5794    /// along the segment, and a nearest point on the surface; `None` for a solid
5795    /// that is not a cylinder or a cone.
5796    #[test]
5797    fn exact_round_distances_are_the_least_along_the_segment() {
5798        use round_reference::{Scene, Solid};
5799        let cylinder = Solid {
5800            cone: false,
5801            r: 0.9,
5802            hh: 0.7,
5803        };
5804        let cone = Solid {
5805            cone: true,
5806            r: 1.0,
5807            hh: 0.8,
5808        };
5809        let position = [0.25, -0.5, 0.75];
5810        let scenes = [
5811            Scene::new(cylinder, position, QuatFix::IDENTITY),
5812            Scene::turned(cylinder, position, [0.6, 0.7, -0.2], 0.9),
5813            Scene::new(cone, position, QuatFix::IDENTITY),
5814            Scene::turned(cone, position, [0.6, 0.7, -0.2], 0.9),
5815        ];
5816        let cores = [
5817            ([2.5, -0.4, 0.8], [2.5, -0.4, 0.8]),
5818            ([0.1, 2.0, 0.9], [0.6, 2.2, 0.4]),
5819            ([-1.5, -2.5, 1.0], [-1.0, -2.0, 2.0]),
5820            ([1.6, 1.0, -0.5], [1.4, 1.6, -0.9]),
5821            ([-2.0, 0.0, 0.75], [2.5, 0.0, 3.0]),
5822            ([0.25, 3.0, 0.75], [0.25, 3.0, 0.75]),
5823        ];
5824        let mut checked = 0;
5825        for s in &scenes {
5826            for &(a, b) in &cores {
5827                let (pa, pb) = (p3(a), p3(b));
5828                let (a, b) = (round_reference::f3(pa), round_reference::f3(pb));
5829                let want = s.segment(a, b);
5830                let got = exact_round_dist(&s.posed, pa, pb).expect("a cylinder or a cone");
5831                match got {
5832                    Dist::Outside { dist, point, .. } => {
5833                        checked += 1;
5834                        assert!(
5835                            (dist.to_f64() - want).abs() < 1e-10,
5836                            "{:?} {a:?}–{b:?}: distance {} but the reference is {want}",
5837                            s.solid,
5838                            dist.to_f64()
5839                        );
5840                        let q = round_reference::f3(point);
5841                        assert!(
5842                            s.point(q).abs() < 1e-10,
5843                            "{:?}: nearest point {q:?} is {} off the surface",
5844                            s.solid,
5845                            s.point(q)
5846                        );
5847                    }
5848                    _ => assert!(
5849                        want <= 0.0,
5850                        "{:?} {a:?}–{b:?}: inside, reference {want}",
5851                        s.solid
5852                    ),
5853                }
5854            }
5855        }
5856        assert!(checked >= 20, "only {checked} distances checked");
5857        let ball = PosedShape {
5858            shape: Shape::Box {
5859                half_extents: v3(1.0, 1.0, 1.0),
5860            },
5861            position: Vec3Fix::ZERO,
5862            rotation: QuatFix::IDENTITY,
5863        };
5864        assert!(exact_round_dist(&ball, v3(3.0, 0.0, 0.0), v3(3.0, 0.0, 0.0)).is_none());
5865    }
5866
5867    /// Casts onto a cone and a cylinder at the edge of what a time of impact
5868    /// can tell apart (paths that graze a rim, a cap or a base, or go into the
5869    /// side just below a cap, `1` to `40·2⁻³²` deep): each path goes more than
5870    /// `2⁻³²` in, and the hit is within `[−1, +2]·2⁻³²` of the surface by the
5871    /// `f64` reference. These go through the lower bound's step, the exact
5872    /// distance where GJK's bounds straddle a decision, and the search along a
5873    /// near-tangent path.
5874    #[test]
5875    fn casts_at_the_edge_of_the_tolerance_stop_at_the_curved_surface() {
5876        use round_reference::{Cast, Scene, Solid, TOL};
5877        let cyl = |r, hh| Solid { cone: false, r, hh };
5878        let cone = |r, hh| Solid { cone: true, r, hh };
5879        let p = [0.25, -0.5, 0.75];
5880        let tilt = [0.6, 0.7, -0.2];
5881        let cases: Vec<(&str, Scene, Cast)> = vec![
5882            (
5883                "segment over a rim",
5884                Scene::new(cyl(0.9, 0.7), p, QuatFix::IDENTITY),
5885                Cast::new(
5886                    [-1.6640986290561979, 0.19999999975304572, 1.8206196513237867],
5887                    [-1.834506720909574, 0.41213203410901, 1.6942824745669378],
5888                    [0.5955591626715423, 0.0, -0.8033114487905494],
5889                    0.0,
5890                    4.0,
5891                ),
5892            ),
5893            (
5894                "segment over a rim, stepping by the lower bound",
5895                Scene::new(cyl(0.9, 0.7), p, QuatFix::IDENTITY),
5896                Cast::new(
5897                    [1.8673886946205231, 0.19999999967072768, -0.7312338808982968],
5898                    [2.0622378155658776, 0.41213203402669196, -0.6473660082245325],
5899                    [-0.3953569432753914, 0.0, 0.9185275648579845],
5900                    0.0,
5901                    4.0,
5902                ),
5903            ),
5904            (
5905                "segment over a turned rim",
5906                Scene::turned(cyl(0.9, 0.7), p, tilt, 0.9),
5907                Cast::new(
5908                    [-0.21852465418609512, -0.6507157640535539, 2.998948511156243],
5909                    [
5910                        -0.34405370476815655,
5911                        -0.41720885309588485,
5912                        3.139365675331331,
5913                    ],
5914                    [-0.06648810702703647, 0.487707135982694, -0.8704717578046512],
5915                    0.0,
5916                    4.0,
5917                ),
5918            ),
5919            (
5920                "sphere over a cone's rim",
5921                Scene::new(cone(1.0, 0.8), p, QuatFix::IDENTITY),
5922                Cast::new(
5923                    [-1.7015908184354425, -0.9027621356993638, 1.8439950540065662],
5924                    [-1.7015908184354425, -0.9027621356993638, 1.8439950540065662],
5925                    [0.5606127132316385, 0.0, -0.8280781278134695],
5926                    1.0 / 256.0,
5927                    4.0,
5928                ),
5929            ),
5930            (
5931                "sphere onto a turned cone",
5932                Scene::turned(cone(1.0, 0.8), p, tilt, 0.9),
5933                Cast::new(
5934                    [0.634936017399154, 0.008205912075554433, -1.4320403873864982],
5935                    [0.634936017399154, 0.008205912075554433, -1.4320403873864982],
5936                    [0.19818663470094447, -0.5210934105185997, 0.8301708952619384],
5937                    1.0 / 256.0,
5938                    4.0,
5939                ),
5940            ),
5941            (
5942                "sphere along a turned cone's base",
5943                Scene::turned(cone(1.0, 0.8), p, tilt, 0.9),
5944                Cast::new(
5945                    [1.4096423077353353, -1.9430159060992016, 1.6571642739508496],
5946                    [1.4096423077353353, -1.9430159060992016, 1.6571642739508496],
5947                    [-0.7361296676461404, 0.528908761536265, -0.422337109879486],
5948                    1.0 / 65536.0,
5949                    4.0,
5950                ),
5951            ),
5952            (
5953                "segment into a cylinder's side below its cap",
5954                Scene::new(cyl(0.9, 0.7), p, QuatFix::IDENTITY),
5955                Cast::new(
5956                    [-1.695761194159812, 0.19999999973806548, 0.2143570449433858],
5957                    [-1.695761194159812, 0.4999999997380655, 0.2143570449433858],
5958                    [0.9909759596810406, 0.0, 0.1340397229713666],
5959                    0.0,
5960                    4.0,
5961                ),
5962            ),
5963            (
5964                "segment into a turned cylinder's side below its cap",
5965                Scene::turned(cyl(0.9, 0.7), p, tilt, 0.9),
5966                Cast::new(
5967                    [
5968                        -0.7916938670894744,
5969                        1.1912886520345445,
5970                        -0.03569496775777914,
5971                    ],
5972                    [-0.6883045505593128, 1.4402697713519372, 0.09590689944358055],
5973                    [0.7223971058724328, -0.5328868033551468, 0.4406518764706059],
5974                    0.0,
5975                    4.0,
5976                ),
5977            ),
5978            (
5979                "sphere grazing into a turned cone",
5980                Scene::turned(cone(1.0, 0.8), [0.0; 3], tilt, 0.9),
5981                Cast::new(
5982                    [3.49676953785352, -1.9311419641957024, -0.20807093324643489],
5983                    [3.49676953785352, -1.9311419641957024, -0.20807093324643489],
5984                    [
5985                        -0.8417749934677569,
5986                        0.49646776600120957,
5987                        -0.21197787076514807,
5988                    ],
5989                    1.0 / 65536.0,
5990                    10.0,
5991                ),
5992            ),
5993            (
5994                "sphere at 9·10⁻¹⁰ rad to a turned cone's base",
5995                Scene::turned(
5996                    cone(0.6428203322684567, 0.7475816177966408),
5997                    [
5998                        -1.6526587531452606,
5999                        -1.2186799371102097,
6000                        0.18938048544987396,
6001                    ],
6002                    [
6003                        -0.5803070933934578,
6004                        0.7790505106677301,
6005                        -0.23732673508311564,
6006                    ],
6007                    1.8984897774453202,
6008                ),
6009                Cast::new(
6010                    [
6011                        -0.8965821042720563,
6012                        -3.3486672559565704,
6013                        -0.9839086836373099,
6014                    ],
6015                    [
6016                        -0.8965821042720563,
6017                        -3.3486672559565704,
6018                        -0.9839086836373099,
6019                    ],
6020                    [-0.1482674077997217, 0.8136896687128683, 0.5620728589901773],
6021                    1.0 / 65536.0,
6022                    10.0,
6023                ),
6024            ),
6025            (
6026                "capsule sliding onto a turned cylinder's cap at 9·10⁻¹⁰ rad",
6027                Scene::turned(
6028                    cyl(0.9012093363780878, 0.5179403340707722),
6029                    [
6030                        -0.4023466290173019,
6031                        -0.46594330930383876,
6032                        -1.2957395342946256,
6033                    ],
6034                    [
6035                        -0.6714485362126652,
6036                        -0.09612558018852724,
6037                        -0.7347902667097514,
6038                    ],
6039                    0.5010374581070209,
6040                ),
6041                Cast::new(
6042                    [1.8608221708009296, -2.0500185708660865, -1.4757067685941365],
6043                    [1.3062351220451092, -1.5734718199873896, -0.7798910803267063],
6044                    [
6045                        -0.9249401521738037,
6046                        0.2939600468880599,
6047                        -0.24098382877309632,
6048                    ],
6049                    1.0 / 65536.0,
6050                    10.0,
6051                ),
6052            ),
6053        ];
6054        for (what, scene, cast) in &cases {
6055            let least = cast.least(scene);
6056            assert!(
6057                least < -TOL,
6058                "{what}: the path goes only {} in",
6059                least / TOL
6060            );
6061            let t = cast
6062                .run(scene)
6063                .unwrap_or_else(|| panic!("{what}: no hit, the path goes {} in", least / TOL));
6064            let g = cast.gap(scene, t) / TOL;
6065            assert!(
6066                (-1.0..=2.0).contains(&g),
6067                "{what}: a hit at t = {t} with gap {g}·2⁻³², expected within [−1, +2]"
6068            );
6069        }
6070    }
6071
6072    /// Paths that pass a cylinder's side tangentially, `2⁻²⁷` (32 tolerances)
6073    /// outside it, inside the gap where the slope is not a proof of separation
6074    /// (`2⁻²⁴`) and well outside it: none goes in, and none is a hit; the
6075    /// same paths `2⁻²⁷` inside the side are hits at its surface.
6076    #[test]
6077    fn paths_tangent_to_a_cylinder_hit_only_when_they_go_in() {
6078        use round_reference::{Cast, Scene, Solid, TOL};
6079        let solid = Solid {
6080            cone: false,
6081            r: 0.9,
6082            hh: 0.7,
6083        };
6084        let scene = Scene::new(solid, [0.0; 3], QuatFix::IDENTITY);
6085        let off = 1.0 / 134_217_728.0; // 2⁻²⁷
6086        for (g, hit) in [(off, false), (64.0 * off, false), (-off, true)] {
6087            for r in [0.0, 1.0 / 65536.0] {
6088                let z = 0.9 + r + g;
6089                let cast = if r == 0.0 {
6090                    // a short vertical segment of radius 0 across the axis' height
6091                    Cast::new([-3.0, 0.1, z], [-3.0, 0.3, z], [1.0, 0.0, 0.0], 0.0, 6.0)
6092                } else {
6093                    Cast::new([-3.0, 0.2, z], [-3.0, 0.2, z], [1.0, 0.0, 0.0], r, 6.0)
6094                };
6095                // the least gap is g, at x = 0 (`t = 3`)
6096                let least = cast.least(&scene);
6097                assert!(
6098                    (least - g).abs() < 1e-3 * TOL,
6099                    "g = {g:e}, r = {r}: least gap {least:e}"
6100                );
6101                match (cast.run(&scene), hit) {
6102                    (None, false) => {}
6103                    (Some(t), true) => {
6104                        let gt = cast.gap(&scene, t) / TOL;
6105                        assert!(
6106                            (-1.0..=2.0).contains(&gt),
6107                            "g = {g:e}, r = {r}: a hit at t = {t} with gap {gt}·2⁻³²"
6108                        );
6109                    }
6110                    (got, _) => panic!("g = {g:e}, r = {r}: {got:?}, expected a hit: {hit}"),
6111                }
6112            }
6113        }
6114    }
6115
6116    /// [`graze_contact`] and [`dips_below`] on gap functions whose roots are
6117    /// known: `(t − 1)² − c` (into the solid by `c` at `t = 1`, first in at
6118    /// `1 − √c`), `(t − 1)² + c` (never in), a gap that is inside (`None`)
6119    /// below `−1/8`, and one that falls linearly and is in only at the far end.
6120    #[test]
6121    fn the_near_tangent_searches_find_the_roots_of_a_convex_gap() {
6122        type Gap<'a> = &'a dyn Fn(f64) -> Option<f64>;
6123        /// A gap function in `f64` given to the searches as `Fix128`.
6124        fn as_dist(g: Gap<'_>) -> impl Fn(Fix128) -> Dist + '_ {
6125            move |t: Fix128| match g(t.to_f64()) {
6126                Some(v) => Dist::Outside {
6127                    dist: fx(v),
6128                    point: Vec3Fix::ZERO,
6129                    normal: Vec3Fix::UNIT_Y,
6130                },
6131                None => Dist::Inside,
6132            }
6133        }
6134        let tol = 1.0 / 4_294_967_296.0;
6135        let quadratic = |c: f64| move |t: f64| Some((t - 1.0) * (t - 1.0) - c);
6136        let deep_inside = |t: f64| {
6137            let g = (t - 1.0) * (t - 1.0) - 0.25;
6138            (g >= -0.125).then_some(g)
6139        };
6140        let falling = |t: f64| Some(0.5 - 0.25 * t);
6141        let (t0, max_t) = (Fix128::ZERO, fx(3.0));
6142        // first in (the gap within ±2⁻³²) at 1 − √c
6143        let (q4, q20, never) = (
6144            quadratic(0.25),
6145            quadratic(1.0 / 1_048_576.0),
6146            quadratic(-1e-3),
6147        );
6148        let cases: [(&str, Gap<'_>, Option<f64>); 5] = [
6149            ("into by 1/4", &q4, Some(0.5)),
6150            ("into by 2⁻²⁰", &q20, Some(1.0 - 1.0 / 1024.0)),
6151            ("inside below −1/8", &deep_inside, Some(0.5)),
6152            ("in only at the far end", &falling, Some(2.0)),
6153            ("never in", &never, None),
6154        ];
6155        for (name, g, root) in cases {
6156            let got = graze_contact(as_dist(g), Fix128::ZERO, Fix128::ZERO, t0, max_t);
6157            match (got, root) {
6158                (None, None) => {}
6159                (Some(c), Some(root)) => {
6160                    let t = c.t.to_f64();
6161                    // the clear end of a bisection to within the tolerance
6162                    let gap = g(t).expect("the contact is outside");
6163                    assert!(
6164                        (-tol..=tol).contains(&gap) || (gap > 0.0 && (t - root).abs() < 1e-9),
6165                        "{name}: contact at t = {t} with gap {gap:e}, the root is {root}"
6166                    );
6167                    assert!(
6168                        (t - root).abs() < 1e-6,
6169                        "{name}: t = {t}, the root is {root}"
6170                    );
6171                    assert_vec(c.normal, [0.0, 1.0, 0.0], EXACT, "normal");
6172                }
6173                (got, _) => panic!(
6174                    "{name}: {:?}, the root is {root:?}",
6175                    got.map(|c| c.t.to_f64())
6176                ),
6177            }
6178            // dips_below from t0 with the gap and speed there
6179            let gap_at = |t: Fix128| g(t.to_f64()).map(fx);
6180            let f0 = g(0.0).expect("clear at the start");
6181            let speed = match name {
6182                "in only at the far end" => 0.25,
6183                _ => 2.0,
6184            };
6185            assert_eq!(
6186                dips_below(gap_at, t0, fx(f0), fx(speed), max_t),
6187                root.is_some(),
6188                "{name}: dips below"
6189            );
6190        }
6191        // a path that sinks too little to go in by the far end
6192        let shallow = |t: f64| Some((t - 1.0) * (t - 1.0) - 0.5 * tol);
6193        let gap_at = |t: Fix128| shallow(t.to_f64()).map(fx);
6194        assert!(
6195            !dips_below(gap_at, t0, fx(1.0), fx(2.0), max_t),
6196            "half a tolerance in"
6197        );
6198    }
6199}