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
343fn clamp(x: Fix128, lo: Fix128, hi: Fix128) -> Fix128 {
344    max_fix(lo, min_fix(x, hi))
345}
346
347/// The point of segment `a`–`b` nearest `p`.
348fn closest_on_segment(a: Vec3Fix, b: Vec3Fix, p: Vec3Fix) -> Vec3Fix {
349    let ab = b - a;
350    let len2 = ab.length_squared();
351    if len2.is_zero() {
352        return a;
353    }
354    a + ab * clamp((p - a).dot(ab) / len2, Fix128::ZERO, Fix128::ONE)
355}
356
357/// The point of `aabb` nearest `p` (the point itself inside).
358fn closest_on_aabb(aabb: &AABB, p: Vec3Fix) -> Vec3Fix {
359    Vec3Fix::new(
360        clamp(p.x, aabb.min.x, aabb.max.x),
361        clamp(p.y, aabb.min.y, aabb.max.y),
362        clamp(p.z, aabb.min.z, aabb.max.z),
363    )
364}
365
366/// A point, for GJK.
367struct PointSupport(Vec3Fix);
368
369impl Support for PointSupport {
370    fn support(&self, _direction: Vec3Fix) -> Vec3Fix {
371        self.0
372    }
373}
374
375/// A segment, for GJK.
376struct SegmentSupport(Vec3Fix, Vec3Fix);
377
378impl Support for SegmentSupport {
379    fn support(&self, direction: Vec3Fix) -> Vec3Fix {
380        if self.1.dot(direction) > self.0.dot(direction) {
381            self.1
382        } else {
383            self.0
384        }
385    }
386}
387
388/// A triangle, for GJK.
389struct TriangleSupport<'a>(&'a Triangle);
390
391impl Support for TriangleSupport<'_> {
392    fn support(&self, direction: Vec3Fix) -> Vec3Fix {
393        let t = self.0;
394        let (d0, d1, d2) = (
395            t.v0.dot(direction),
396            t.v1.dot(direction),
397            t.v2.dot(direction),
398        );
399        if d1 > d0 && d1 >= d2 {
400            t.v1
401        } else if d2 > d0 && d2 > d1 {
402            t.v2
403        } else {
404            t.v0
405        }
406    }
407}
408
409/// A compound child placed in the world, for GJK.
410struct ChildSupport<'a> {
411    child: &'a CompoundChild,
412    position: Vec3Fix,
413    rotation: QuatFix,
414}
415
416impl Support for ChildSupport<'_> {
417    fn support(&self, direction: Vec3Fix) -> Vec3Fix {
418        self.child
419            .support_world(direction, self.position, self.rotation)
420    }
421}
422
423/// One vertex of a GJK simplex: the Minkowski-difference point and the two
424/// support points it came from.
425#[derive(Clone, Copy)]
426struct Vertex {
427    w: Vec3Fix,
428    a: Vec3Fix,
429    b: Vec3Fix,
430}
431
432fn vertex<A: Support, B: Support>(a: &A, b: &B, direction: Vec3Fix) -> Vertex {
433    let pa = a.support(direction);
434    let pb = b.support(-direction);
435    Vertex {
436        w: pa - pb,
437        a: pa,
438        b: pb,
439    }
440}
441
442/// The point of the simplex nearest the origin, the sub-simplex it lies on and
443/// its barycentric weights; `None` when the origin is inside a tetrahedron.
444fn closest_on_simplex(s: &[Vertex]) -> Option<(Vec<Vertex>, Vec<Fix128>)> {
445    match s.len() {
446        1 => Some((s.to_vec(), vec![Fix128::ONE])),
447        2 => Some(closest_on_edge(s[0], s[1])),
448        3 => Some(closest_on_triangle(s[0], s[1], s[2])),
449        _ => closest_on_tetrahedron(s[0], s[1], s[2], s[3]),
450    }
451}
452
453fn closest_on_edge(a: Vertex, b: Vertex) -> (Vec<Vertex>, Vec<Fix128>) {
454    let ab = b.w - a.w;
455    let len2 = ab.length_squared();
456    let t = if len2.is_zero() {
457        Fix128::ZERO
458    } else {
459        -a.w.dot(ab) / len2
460    };
461    if t <= Fix128::ZERO {
462        (vec![a], vec![Fix128::ONE])
463    } else if t >= Fix128::ONE {
464        (vec![b], vec![Fix128::ONE])
465    } else {
466        (vec![a, b], vec![Fix128::ONE - t, t])
467    }
468}
469
470/// Ericson, *Real-Time Collision Detection* §5.1.5, for the origin.
471fn closest_on_triangle(a: Vertex, b: Vertex, c: Vertex) -> (Vec<Vertex>, Vec<Fix128>) {
472    let ab = b.w - a.w;
473    let ac = c.w - a.w;
474    let ap = -a.w;
475    let d1 = ab.dot(ap);
476    let d2 = ac.dot(ap);
477    if d1 <= Fix128::ZERO && d2 <= Fix128::ZERO {
478        return (vec![a], vec![Fix128::ONE]);
479    }
480    let bp = -b.w;
481    let d3 = ab.dot(bp);
482    let d4 = ac.dot(bp);
483    if d3 >= Fix128::ZERO && d4 <= d3 {
484        return (vec![b], vec![Fix128::ONE]);
485    }
486    let vc = d1 * d4 - d3 * d2;
487    if vc <= Fix128::ZERO && d1 >= Fix128::ZERO && d3 <= Fix128::ZERO {
488        let v = d1 / (d1 - d3);
489        return (vec![a, b], vec![Fix128::ONE - v, v]);
490    }
491    let cp = -c.w;
492    let d5 = ab.dot(cp);
493    let d6 = ac.dot(cp);
494    if d6 >= Fix128::ZERO && d5 <= d6 {
495        return (vec![c], vec![Fix128::ONE]);
496    }
497    let vb = d5 * d2 - d1 * d6;
498    if vb <= Fix128::ZERO && d2 >= Fix128::ZERO && d6 <= Fix128::ZERO {
499        let w = d2 / (d2 - d6);
500        return (vec![a, c], vec![Fix128::ONE - w, w]);
501    }
502    let va = d3 * d6 - d5 * d4;
503    if va <= Fix128::ZERO && d4 - d3 >= Fix128::ZERO && d5 - d6 >= Fix128::ZERO {
504        let w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
505        return (vec![b, c], vec![Fix128::ONE - w, w]);
506    }
507    let denom = va + vb + vc;
508    if denom <= Fix128::ZERO {
509        // A degenerate (flat) triangle: the nearest of its edges.
510        let mut best = closest_on_edge(a, b);
511        for cand in [closest_on_edge(b, c), closest_on_edge(a, c)] {
512            if weighted(&cand).length_squared() < weighted(&best).length_squared() {
513                best = cand;
514            }
515        }
516        return best;
517    }
518    let v = vb / denom;
519    let w = vc / denom;
520    (vec![a, b, c], vec![Fix128::ONE - v - w, v, w])
521}
522
523fn closest_on_tetrahedron(
524    a: Vertex,
525    b: Vertex,
526    c: Vertex,
527    d: Vertex,
528) -> Option<(Vec<Vertex>, Vec<Fix128>)> {
529    let faces = [(a, b, c, d), (a, c, d, b), (a, d, b, c), (b, d, c, a)];
530    let mut best: Option<(Vec<Vertex>, Vec<Fix128>)> = None;
531    let mut inside = true;
532    for (p, q, r, opposite) in faces {
533        let n = (q.w - p.w).cross(r.w - p.w);
534        let origin_side = n.dot(-p.w);
535        let other_side = n.dot(opposite.w - p.w);
536        // The origin is outside this face when it is strictly on the other side
537        // from the fourth vertex; every face of a flat tetrahedron counts.
538        let outside = other_side.is_zero()
539            || (!origin_side.is_zero() && origin_side.is_negative() != other_side.is_negative());
540        if outside {
541            inside = false;
542            let cand = closest_on_triangle(p, q, r);
543            if best
544                .as_ref()
545                .is_none_or(|b| weighted(&cand).length_squared() < weighted(b).length_squared())
546            {
547                best = Some(cand);
548            }
549        }
550    }
551    if inside {
552        None
553    } else {
554        best
555    }
556}
557
558fn weighted(s: &(Vec<Vertex>, Vec<Fix128>)) -> Vec3Fix {
559    s.0.iter()
560        .zip(&s.1)
561        .fold(Vec3Fix::ZERO, |acc, (v, &l)| acc + v.w * l)
562}
563
564/// The distance between two convex sets by GJK: `(distance, point on a, point on
565/// b)`, or `None` when they intersect (closer than `2⁻³²`).
566fn gjk_distance<A: Support, B: Support>(a: &A, b: &B) -> Option<(Fix128, Vec3Fix, Vec3Fix)> {
567    let mut simplex = vec![vertex(a, b, Vec3Fix::UNIT_X)];
568    let mut weights = vec![Fix128::ONE];
569    let mut v = simplex[0].w;
570    for _ in 0..GJK_MAX_ITERATIONS {
571        let vv = v.length_squared();
572        if vv <= GJK_INTERSECT {
573            return None;
574        }
575        let w = vertex(a, b, -v);
576        if vv - v.dot(w.w) <= vv * GJK_RELATIVE {
577            break;
578        }
579        if simplex.iter().any(|s| s.w == w.w) {
580            break;
581        }
582        let mut grown = simplex.clone();
583        grown.push(w);
584        let (sub, lambda) = closest_on_simplex(&grown)?;
585        let next = weighted(&(sub.clone(), lambda.clone()));
586        if next.length_squared() >= vv {
587            // No progress (rounding): keep the previous simplex.
588            break;
589        }
590        simplex = sub;
591        weights = lambda;
592        v = next;
593    }
594    let pa = simplex
595        .iter()
596        .zip(&weights)
597        .fold(Vec3Fix::ZERO, |acc, (s, &l)| acc + s.a * l);
598    let pb = simplex
599        .iter()
600        .zip(&weights)
601        .fold(Vec3Fix::ZERO, |acc, (s, &l)| acc + s.b * l);
602    let dist = v.length();
603    if dist.is_zero() {
604        return None;
605    }
606    Some((dist, pa, pb))
607}
608
609/// The distance from the core `a`–`b` to a convex set, by GJK.
610fn convex_dist<S: Support>(a: Vec3Fix, b: Vec3Fix, solid: &S) -> Dist {
611    let found = if a == b {
612        gjk_distance(&PointSupport(a), solid)
613    } else {
614        gjk_distance(&SegmentSupport(a, b), solid)
615    };
616    match found {
617        None => Dist::Inside,
618        Some((dist, pa, pb)) => Dist::Outside {
619            dist,
620            point: pb,
621            normal: (pa - pb) / dist,
622        },
623    }
624}
625
626/// The distance between two segments `p0`–`p1` and `q0`–`q1`, by GJK on the two
627/// segments: `(distance, point on p, point on q)`, `None` when they cross.
628fn segment_segment(
629    p0: Vec3Fix,
630    p1: Vec3Fix,
631    q0: Vec3Fix,
632    q1: Vec3Fix,
633) -> Option<(Fix128, Vec3Fix, Vec3Fix)> {
634    if p0 == p1 && q0 == q1 {
635        let (_, d) = (p0 - q0).normalize_with_length();
636        return (!d.is_zero()).then_some((d, p0, q0));
637    }
638    if p0 == p1 {
639        let q = closest_on_segment(q0, q1, p0);
640        let d = (p0 - q).length();
641        return (!d.is_zero()).then_some((d, p0, q));
642    }
643    if q0 == q1 {
644        let p = closest_on_segment(p0, p1, q0);
645        let d = (p - q0).length();
646        return (!d.is_zero()).then_some((d, p, q0));
647    }
648    gjk_distance(&SegmentSupport(p0, p1), &SegmentSupport(q0, q1))
649}
650
651/// The distance from the core `a`–`b` to a solid sphere.
652fn sphere_dist(a: Vec3Fix, b: Vec3Fix, center: Vec3Fix, radius: Fix128) -> Dist {
653    let s = closest_on_segment(a, b, center);
654    let (n, d) = (s - center).normalize_with_length();
655    if d < radius || d.is_zero() {
656        return Dist::Inside;
657    }
658    Dist::Outside {
659        dist: d - radius,
660        point: center + n * radius,
661        normal: n,
662    }
663}
664
665/// The distance from the core `a`–`b` to a solid capsule.
666fn capsule_dist(a: Vec3Fix, b: Vec3Fix, capsule: &Capsule) -> Dist {
667    match segment_segment(a, b, capsule.a, capsule.b) {
668        None => Dist::Inside,
669        Some((d, pa, pb)) => Dist::Outside {
670            dist: d,
671            point: pb,
672            normal: (pa - pb) / d,
673        }
674        .shrunk(capsule.radius),
675    }
676}
677
678/// The distance from `p` to a solid box of half-extents `h` about the origin, in
679/// its frame.
680fn point_box_local(p: Vec3Fix, h: Vec3Fix) -> Dist {
681    if p.x.abs() < h.x && p.y.abs() < h.y && p.z.abs() < h.z {
682        return Dist::Inside;
683    }
684    let q = Vec3Fix::new(
685        clamp(p.x, -h.x, h.x),
686        clamp(p.y, -h.y, h.y),
687        clamp(p.z, -h.z, h.z),
688    );
689    if q == p {
690        // On the boundary: touching, with the normal of a face it lies on.
691        let axis = |v: Fix128, e: Fix128, unit: Vec3Fix| {
692            (v.abs() == e).then(|| if v.is_negative() { -unit } else { unit })
693        };
694        let z = Vec3Fix::new(Fix128::ZERO, Fix128::ZERO, Fix128::ONE);
695        let normal = axis(p.x, h.x, Vec3Fix::UNIT_X)
696            .or_else(|| axis(p.y, h.y, Vec3Fix::UNIT_Y))
697            .or_else(|| axis(p.z, h.z, z))
698            .unwrap_or(Vec3Fix::UNIT_Y);
699        return Dist::Outside {
700            dist: Fix128::ZERO,
701            point: p,
702            normal,
703        };
704    }
705    Dist::toward(p, q)
706}
707
708/// The distance from `p` to a solid cylinder along `Y` (radius `radius`, half
709/// height `hh`), in its frame.
710fn point_cylinder_local(p: Vec3Fix, radius: Fix128, hh: Fix128) -> Dist {
711    let (dir, rho) = Vec3Fix::new(p.x, Fix128::ZERO, p.z).normalize_with_length();
712    let qy = clamp(p.y, -hh, hh);
713    if rho < radius && p.y.abs() < hh {
714        return Dist::Inside;
715    }
716    let qr = min_fix(rho, radius);
717    let q = if rho.is_zero() {
718        Vec3Fix::new(Fix128::ZERO, qy, Fix128::ZERO)
719    } else {
720        Vec3Fix::new(dir.x * qr, qy, dir.z * qr)
721    };
722    if (q - p).length_squared().is_zero() {
723        // On the boundary: touching, with the cap or side normal there.
724        let normal = if p.y.abs() == hh {
725            if p.y.is_negative() {
726                -Vec3Fix::UNIT_Y
727            } else {
728                Vec3Fix::UNIT_Y
729            }
730        } else {
731            dir
732        };
733        return Dist::Outside {
734            dist: Fix128::ZERO,
735            point: p,
736            normal,
737        };
738    }
739    Dist::toward(p, q)
740}
741
742/// The distance from `p` to a solid torus (ring `major` in `XZ`, tube `minor`), in
743/// its frame.
744fn point_torus_local(p: Vec3Fix, major: Fix128, minor: Fix128) -> Dist {
745    if torus_distance(p, major, minor).is_negative() {
746        return Dist::Inside;
747    }
748    let (ring_dir, rho) = Vec3Fix::new(p.x, Fix128::ZERO, p.z).normalize_with_length();
749    let ring_dir = if rho.is_zero() {
750        Vec3Fix::UNIT_X
751    } else {
752        ring_dir
753    };
754    let ring = ring_dir * major;
755    match Dist::toward(p, ring) {
756        Dist::Outside { dist, normal, .. } => Dist::Outside {
757            dist,
758            point: ring,
759            normal,
760        }
761        .shrunk(minor),
762        other => other,
763    }
764}
765
766/// A distance in a solid's frame turned back into the world.
767fn dist_to_world(d: Dist, center: Vec3Fix, rotation: QuatFix) -> Dist {
768    match d {
769        Dist::Outside {
770            dist,
771            point,
772            normal,
773        } => Dist::Outside {
774            dist,
775            point: center + rotation.rotate_vec(point),
776            normal: rotation.rotate_vec(normal),
777        },
778        other => other,
779    }
780}
781
782/// The minimum over `s ∈ [0, 1]` of an `L`-Lipschitz distance `f(s)`:
783/// branch-and-bound to [`CURVE_COARSE`], then golden-section refinement about
784/// the best node. `f` may report [`Dist::AtLeast`] (a lower bound).
785fn curve_min(lipschitz: Fix128, f: impl Fn(Fix128) -> Dist) -> Dist {
786    const SEEDS: i64 = 16;
787    // (s, half width of its node, distance)
788    let mut best: Option<(Fix128, Fix128, Dist)> = None;
789    // Open nodes: (midpoint, half width, lower bound).
790    let mut stack: Vec<(Fix128, Fix128, Fix128)> = Vec::new();
791    let seed_half = Fix128::from_ratio(1, 2 * SEEDS);
792    let mut nodes = 0usize;
793    let mut todo: Vec<(Fix128, Fix128)> = (0..SEEDS)
794        .map(|k| (Fix128::from_ratio(2 * k + 1, 2 * SEEDS), seed_half))
795        .collect();
796    loop {
797        for (m, hw) in todo.drain(..) {
798            let d = f(m);
799            if matches!(d, Dist::Inside) {
800                return Dist::Inside;
801            }
802            if best.as_ref().is_none_or(|(_, _, b)| d.value() < b.value()) {
803                best = Some((m, hw, d));
804            }
805            // A node's lower bound: the Lipschitz cone below its midpoint value.
806            stack.push((m, hw, d.value() - lipschitz * hw));
807        }
808        let best_value = best.as_ref().map_or(Fix128::ZERO, |(_, _, b)| b.value());
809        // Drop the nodes that cannot hold a value below the best by more than the
810        // coarse tolerance, and the ones already narrow enough.
811        stack.retain(|&(_, hw, bound)| {
812            bound < best_value - CURVE_COARSE && lipschitz * hw > CURVE_COARSE
813        });
814        // Best first: the lowest bound (the first such node on a tie).
815        let Some(k) = (0..stack.len()).min_by_key(|&k| stack[k].2) else {
816            break;
817        };
818        nodes += 1;
819        if nodes > CURVE_MAX_NODES {
820            break;
821        }
822        let (mid, hw, _) = stack.remove(k);
823        let quarter = hw.half();
824        todo.push((mid - quarter, quarter));
825        todo.push((mid + quarter, quarter));
826    }
827    let Some((s_best, hw_best, d_best)) = best else {
828        return Dist::AtLeast(Fix128::ZERO);
829    };
830    // Golden-section over the best node and its neighbours.
831    let mut lo = max_fix(Fix128::ZERO, s_best - hw_best.double());
832    let mut hi = min_fix(Fix128::ONE, s_best + hw_best.double());
833    let ratio = Fix128::from_ratio(618_033_988_749_895, 1_000_000_000_000_000);
834    let mut x1 = hi - (hi - lo) * ratio;
835    let mut x2 = lo + (hi - lo) * ratio;
836    let mut f1 = f(x1);
837    let mut f2 = f(x2);
838    let mut refined = d_best;
839    for _ in 0..GOLDEN_STEPS {
840        for d in [f1, f2] {
841            if matches!(d, Dist::Inside) {
842                return Dist::Inside;
843            }
844            if d.value() < refined.value() {
845                refined = d;
846            }
847        }
848        if f1.value() <= f2.value() {
849            hi = x2;
850            x2 = x1;
851            f2 = f1;
852            x1 = hi - (hi - lo) * ratio;
853            f1 = f(x1);
854        } else {
855            lo = x1;
856            x1 = x2;
857            f1 = f2;
858            x2 = lo + (hi - lo) * ratio;
859            f2 = f(x2);
860        }
861    }
862    refined
863}
864
865/// The distance from the segment `a`–`b` to a geometry given by its point
866/// distance `f`.
867fn segment_min(a: Vec3Fix, b: Vec3Fix, f: impl Fn(Vec3Fix) -> Dist) -> Dist {
868    if a == b {
869        return f(a);
870    }
871    let ab = b - a;
872    curve_min(ab.length(), |s| f(a + ab * s))
873}
874
875// ============================================================================
876// Height fields
877// ============================================================================
878
879/// Whether the field has a surface (see [`crate::shape_raycast`]).
880fn field_has_surface(field: &HeightField) -> bool {
881    field.width >= 2 && field.depth >= 2 && field.spacing > Fix128::ZERO
882}
883
884/// The cell range `[lo, hi]` (clamped to the grid) covering `[x0, x1]` along an
885/// axis with `n` points.
886fn cell_range(
887    x0: Fix128,
888    x1: Fix128,
889    origin: Fix128,
890    spacing: Fix128,
891    n: u32,
892) -> Option<(u32, u32)> {
893    let last = i64::from(n) - 2;
894    let lo = ((x0 - origin) / spacing).floor().hi;
895    let hi = ((x1 - origin) / spacing).floor().hi;
896    if hi < 0 || lo > last {
897        return None;
898    }
899    Some((lo.clamp(0, last) as u32, hi.clamp(0, last) as u32))
900}
901
902/// The world `(x, z)` of cell `(gx, gz)`'s first corner.
903fn cell_origin(field: &HeightField, gx: u32, gz: u32) -> (Fix128, Fix128) {
904    (
905        field.origin.x + field.spacing * Fix128::from_int(i64::from(gx)),
906        field.origin.z + field.spacing * Fix128::from_int(i64::from(gz)),
907    )
908}
909
910/// The four corner heights of cell `(gx, gz)`: `(h00, h10, h01, h11)`.
911fn cell_heights(field: &HeightField, gx: u32, gz: u32) -> [Fix128; 4] {
912    [
913        field.get_height(gx, gz),
914        field.get_height(gx + 1, gz),
915        field.get_height(gx, gz + 1),
916        field.get_height(gx + 1, gz + 1),
917    ]
918}
919
920/// Four points, for GJK: their convex hull. The control points of a bilinear
921/// patch, whose hull contains the patch.
922struct QuadSupport([Vec3Fix; 4]);
923
924impl Support for QuadSupport {
925    fn support(&self, direction: Vec3Fix) -> Vec3Fix {
926        let mut best = self.0[0];
927        let mut best_dot = best.dot(direction);
928        for &p in &self.0[1..] {
929            let d = p.dot(direction);
930            if d > best_dot {
931                best = p;
932                best_dot = d;
933            }
934        }
935        best
936    }
937}
938
939/// One bilinear height-field cell: first corner `(x0, z0)`, spacing `s`, corner
940/// heights `(h00, h10, h01, h11)`.
941#[derive(Clone, Copy)]
942struct Cell {
943    x0: Fix128,
944    z0: Fix128,
945    s: Fix128,
946    h: [Fix128; 4],
947}
948
949impl Cell {
950    fn new(field: &HeightField, gx: u32, gz: u32) -> Self {
951        let (x0, z0) = cell_origin(field, gx, gz);
952        Self {
953            x0,
954            z0,
955            s: field.spacing,
956            h: cell_heights(field, gx, gz),
957        }
958    }
959
960    /// The bilinear height at `(u, v) ∈ [0, 1]²`.
961    fn height(&self, u: Fix128, v: Fix128) -> Fix128 {
962        let [h00, h10, h01, h11] = self.h;
963        let one = Fix128::ONE;
964        h00 * (one - u) * (one - v) + h10 * u * (one - v) + h01 * (one - u) * v + h11 * u * v
965    }
966
967    fn point(&self, u: Fix128, v: Fix128) -> Vec3Fix {
968        Vec3Fix::new(
969            self.x0 + self.s * u,
970            self.height(u, v),
971            self.z0 + self.s * v,
972        )
973    }
974
975    /// `h00 − h10 − h01 + h11`: zero for a planar cell.
976    fn twist(&self) -> Fix128 {
977        let [h00, h10, h01, h11] = self.h;
978        h00 - h10 - h01 + h11
979    }
980
981    /// The unit upward normal of the surface at `(u, v)`.
982    fn normal(&self, u: Fix128, v: Fix128) -> Vec3Fix {
983        let [h00, h10, h01, h11] = self.h;
984        let one = Fix128::ONE;
985        let hu = (h10 - h00) * (one - v) + (h11 - h01) * v;
986        let hv = (h01 - h00) * (one - u) + (h11 - h10) * u;
987        Vec3Fix::new(-hu, self.s, -hv).normalize()
988    }
989
990    /// The `(u, v)` of the world `(x, z)` of `p`, clamped to `[u0, u1] × [v0, v1]`.
991    fn uv(&self, p: Vec3Fix, part: &Patch) -> (Fix128, Fix128) {
992        (
993            clamp((p.x - self.x0) / self.s, part.u0, part.u1),
994            clamp((p.z - self.z0) / self.s, part.v0, part.v1),
995        )
996    }
997}
998
999/// The part `u ∈ [u0, u1]`, `v ∈ [v0, v1]` of a [`Cell`]. A bilinear patch
1000/// restricted to a rectangle of `(u, v)` is again bilinear, with its four corners
1001/// as control points, so it lies inside the convex hull of those corners.
1002#[derive(Clone, Copy)]
1003struct Patch {
1004    u0: Fix128,
1005    u1: Fix128,
1006    v0: Fix128,
1007    v1: Fix128,
1008}
1009
1010impl Patch {
1011    const WHOLE: Self = Self {
1012        u0: Fix128::ZERO,
1013        u1: Fix128::ONE,
1014        v0: Fix128::ZERO,
1015        v1: Fix128::ONE,
1016    };
1017
1018    fn hull(&self, cell: &Cell) -> QuadSupport {
1019        QuadSupport([
1020            cell.point(self.u0, self.v0),
1021            cell.point(self.u1, self.v0),
1022            cell.point(self.u0, self.v1),
1023            cell.point(self.u1, self.v1),
1024        ])
1025    }
1026
1027    /// A bound on how far a point of the hull is from the patch: the hull lies
1028    /// between the two triangulations of the corners, which differ from the
1029    /// bilinear surface by at most a quarter of the part's twist
1030    /// `|k|·(u1 − u0)·(v1 − v0)` along `Y`.
1031    fn thickness(&self, cell: &Cell) -> Fix128 {
1032        (cell.twist().abs() * (self.u1 - self.u0) * (self.v1 - self.v0))
1033            .half()
1034            .half()
1035    }
1036
1037    fn split(&self) -> [Self; 4] {
1038        let um = (self.u0 + self.u1).half();
1039        let vm = (self.v0 + self.v1).half();
1040        [
1041            Self {
1042                u1: um,
1043                v1: vm,
1044                ..*self
1045            },
1046            Self {
1047                u0: um,
1048                v1: vm,
1049                ..*self
1050            },
1051            Self {
1052                u1: um,
1053                v0: vm,
1054                ..*self
1055            },
1056            Self {
1057                u0: um,
1058                v0: vm,
1059                ..*self
1060            },
1061        ]
1062    }
1063
1064    /// The surface point of this part over the `(x, z)` of `p`.
1065    fn surface_under(&self, cell: &Cell, p: Vec3Fix) -> Vec3Fix {
1066        let (u, v) = cell.uv(p, self);
1067        cell.point(u, v)
1068    }
1069}
1070
1071/// The distance from the core `a`–`b` to one cell and the nearest surface point
1072/// found, by branch-and-bound over parts of the cell: the distance to a part's
1073/// hull is a lower bound for the part, the distance to the surface point under
1074/// the hull's nearest point an upper bound, and parts are split until the two
1075/// meet within [`TRACE_TOLERANCE`]. The result is never above the true distance
1076/// by more than that tolerance; if the node budget runs out it is the lowest
1077/// open lower bound (below the true distance).
1078fn cell_dist(cell: &Cell, a: Vec3Fix, b: Vec3Fix) -> (Fix128, Vec3Fix) {
1079    let to_core = |q: Vec3Fix| (closest_on_segment(a, b, q) - q).length();
1080    let eval = |part: &Patch| {
1081        let hull = part.hull(cell);
1082        let found = if a == b {
1083            gjk_distance(&PointSupport(a), &hull)
1084        } else {
1085            gjk_distance(&SegmentSupport(a, b), &hull)
1086        };
1087        let (lower, near) = match found {
1088            Some((d, _, pb)) => (d, pb),
1089            None => (
1090                Fix128::ZERO,
1091                cell.point((part.u0 + part.u1).half(), (part.v0 + part.v1).half()),
1092            ),
1093        };
1094        let q = part.surface_under(cell, near);
1095        (lower, q, to_core(q))
1096    };
1097    let (lower, q, upper) = eval(&Patch::WHOLE);
1098    let mut best = (upper, q);
1099    let mut open = vec![(lower, Patch::WHOLE)];
1100    let mut nodes = 0usize;
1101    while let Some(k) = (0..open.len()).min_by_key(|&k| open[k].0) {
1102        let (lower, part) = open.remove(k);
1103        // Every open part is at least `lower` away: nothing can beat the best by
1104        // more than the tolerance.
1105        if lower + TRACE_TOLERANCE >= best.0 {
1106            break;
1107        }
1108        nodes += 1;
1109        if nodes > CELL_MAX_NODES {
1110            return (lower, best.1);
1111        }
1112        for child in part.split() {
1113            let (lower, q, upper) = eval(&child);
1114            if upper < best.0 {
1115                best = (upper, q);
1116            }
1117            // A part thinner than the tolerance has its upper bound within the
1118            // tolerance of its lower bound, so it is settled by `best`.
1119            if lower + TRACE_TOLERANCE < best.0 && child.thickness(cell) > TRACE_TOLERANCE {
1120                open.push((lower, child));
1121            }
1122        }
1123    }
1124    best
1125}
1126
1127/// The distance from the core `a`–`b` to the height-field surface, searching the
1128/// cells within `cap` of it ([`Dist::AtLeast`] beyond). A core within
1129/// [`TRACE_TOLERANCE`] of the surface is touching: a point gets distance `0` and
1130/// the surface normal on its side, a segment is [`Dist::Inside`] (it meets the
1131/// surface).
1132fn heightfield_dist(field: &HeightField, a: Vec3Fix, b: Vec3Fix, cap: Fix128) -> Dist {
1133    if !field_has_surface(field) {
1134        return Dist::AtLeast(cap);
1135    }
1136    let core = core_box(a, b, Fix128::ZERO);
1137    let far = aabb_gap(&core, &field.aabb());
1138    if far >= cap {
1139        return Dist::AtLeast(far);
1140    }
1141    let reach = core_box(a, b, cap);
1142    let s = field.spacing;
1143    let (Some((gx0, gx1)), Some((gz0, gz1))) = (
1144        cell_range(reach.min.x, reach.max.x, field.origin.x, s, field.width),
1145        cell_range(reach.min.z, reach.max.z, field.origin.z, s, field.depth),
1146    ) else {
1147        return Dist::AtLeast(cap);
1148    };
1149    // The cells by their box's distance from the core, nearest first: once a
1150    // cell's box is farther than the best distance found, no later cell is nearer.
1151    let mut cells: Vec<(Fix128, u32, u32)> = Vec::new();
1152    for gz in gz0..=gz1 {
1153        for gx in gx0..=gx1 {
1154            let lower = aabb_gap(&core, &cell_box(field, gx, gz));
1155            if lower < cap {
1156                cells.push((lower, gz, gx));
1157            }
1158        }
1159    }
1160    cells.sort_unstable();
1161    let mut best: Option<(Fix128, Vec3Fix, Cell)> = None;
1162    for (lower, gz, gx) in cells {
1163        if best.is_some_and(|(d, _, _)| lower >= d) {
1164            break;
1165        }
1166        let cell = Cell::new(field, gx, gz);
1167        let (d, q) = cell_dist(&cell, a, b);
1168        if best.is_none_or(|(bd, _, _)| d < bd) {
1169            best = Some((d, q, cell));
1170        }
1171    }
1172    let Some((d, q, cell)) = best.filter(|&(d, _, _)| d < cap) else {
1173        return Dist::AtLeast(cap);
1174    };
1175    let core_point = closest_on_segment(a, b, q);
1176    if d <= TRACE_TOLERANCE {
1177        if a != b {
1178            return Dist::Inside;
1179        }
1180        let (u, v) = cell.uv(q, &Patch::WHOLE);
1181        let n = cell.normal(u, v);
1182        let side = if (a - q).dot(n).is_negative() { -n } else { n };
1183        return Dist::Outside {
1184            dist: d,
1185            point: q,
1186            normal: side,
1187        };
1188    }
1189    let normal = match (core_point - q).try_normalize() {
1190        Some(n) => n,
1191        None => return Dist::Inside,
1192    };
1193    Dist::Outside {
1194        dist: d,
1195        point: q,
1196        normal,
1197    }
1198}
1199
1200/// The box of cell `(gx, gz)`: its `XZ` square and the range of its corner
1201/// heights (the bilinear surface stays within them).
1202fn cell_box(field: &HeightField, gx: u32, gz: u32) -> AABB {
1203    let h = cell_heights(field, gx, gz);
1204    let (x0, z0) = cell_origin(field, gx, gz);
1205    let s = field.spacing;
1206    let lo = min_fix(min_fix(h[0], h[1]), min_fix(h[2], h[3]));
1207    let hi = max_fix(max_fix(h[0], h[1]), max_fix(h[2], h[3]));
1208    AABB::new(Vec3Fix::new(x0, lo, z0), Vec3Fix::new(x0 + s, hi, z0 + s))
1209}
1210
1211/// The first time the core `a`–`b` grown by `r`, moving along the unit `d`,
1212/// touches one cell: against the hull of the whole cell when it is planar (the
1213/// hull is the cell), otherwise by [`toi_tree`] over its parts, which accepts a
1214/// part once its hull is within [`TRACE_TOLERANCE`] of its surface.
1215fn cell_toi(
1216    cell: &Cell,
1217    a: Vec3Fix,
1218    b: Vec3Fix,
1219    r: Fix128,
1220    d: Vec3Fix,
1221    max_t: Fix128,
1222) -> Option<Contact> {
1223    let toi = |part: &Patch| toi_convex(a, b, r, Fix128::ZERO, d, max_t, &part.hull(cell));
1224    if cell.twist().is_zero() {
1225        return toi(&Patch::WHOLE);
1226    }
1227    let found = toi_tree(
1228        &[Patch::WHOLE],
1229        toi,
1230        |part| part.thickness(cell) <= TRACE_TOLERANCE,
1231        |part| part.split().to_vec(),
1232    )?;
1233    let off = d * found.t;
1234    Some(refine_on_cell(cell, a + off, b + off, found))
1235}
1236
1237/// A contact found on a part's hull moved onto the surface: Newton's method on
1238/// the squared distance from the core to the bilinear surface, started at the
1239/// contact's `(u, v)`. The hull contact is within the part (a few `2⁻¹⁶` of the
1240/// cell), where the distance has a single minimum; the refined point is taken
1241/// only if it is not farther from the core than the hull contact.
1242fn refine_on_cell(cell: &Cell, a: Vec3Fix, b: Vec3Fix, found: Contact) -> Contact {
1243    let [h00, h10, h01, h11] = cell.h;
1244    let k = cell.twist();
1245    let s = cell.s;
1246    let one = Fix128::ONE;
1247    let (mut u, mut v) = cell.uv(found.point, &Patch::WHOLE);
1248    for _ in 0..8 {
1249        let q = cell.point(u, v);
1250        let e = q - closest_on_segment(a, b, q);
1251        let hu = (h10 - h00) * (one - v) + (h11 - h01) * v;
1252        let hv = (h01 - h00) * (one - u) + (h11 - h10) * u;
1253        let pu = Vec3Fix::new(s, hu, Fix128::ZERO);
1254        let pv = Vec3Fix::new(Fix128::ZERO, hv, s);
1255        let (gu, gv) = (e.dot(pu), e.dot(pv));
1256        let (huu, hvv) = (pu.dot(pu), pv.dot(pv));
1257        let huv = pu.dot(pv) + e.y * k;
1258        let det = huu * hvv - huv * huv;
1259        if det <= Fix128::ZERO {
1260            return found;
1261        }
1262        u = clamp(u - (hvv * gu - huv * gv) / det, Fix128::ZERO, one);
1263        v = clamp(v - (huu * gv - huv * gu) / det, Fix128::ZERO, one);
1264    }
1265    let q = cell.point(u, v);
1266    let core = closest_on_segment(a, b, q);
1267    let before = (closest_on_segment(a, b, found.point) - found.point).length();
1268    match (core - q).normalize_with_length() {
1269        (n, dist)
1270            if !dist.is_zero()
1271                && dist <= before + TRACE_TOLERANCE + TRACE_TOLERANCE
1272                && n.dot(found.normal) > Fix128::ZERO =>
1273        {
1274            Contact {
1275                t: found.t,
1276                point: q,
1277                normal: n,
1278            }
1279        }
1280        _ => found,
1281    }
1282}
1283
1284/// The first contact of the core `a`–`b` grown by `r` moving along the unit `d`
1285/// with a height field: every cell its swept box crosses, in order of the time
1286/// the moving box first reaches the cell's box, stopping once that time is past
1287/// the best contact.
1288fn sweep_heightfield(
1289    field: &HeightField,
1290    a: Vec3Fix,
1291    b: Vec3Fix,
1292    r: Fix128,
1293    d: Vec3Fix,
1294    max_t: Fix128,
1295) -> Option<Contact> {
1296    if !field_has_surface(field) {
1297        return None;
1298    }
1299    let start = core_box(a, b, r);
1300    let swept = swept_box(a, b, r, d, max_t);
1301    let s = field.spacing;
1302    let (Some((gx0, gx1)), Some((gz0, gz1))) = (
1303        cell_range(swept.min.x, swept.max.x, field.origin.x, s, field.width),
1304        cell_range(swept.min.z, swept.max.z, field.origin.z, s, field.depth),
1305    ) else {
1306        return None;
1307    };
1308    let mut cells: Vec<(Fix128, u32, u32)> = Vec::new();
1309    for gz in gz0..=gz1 {
1310        for gx in gx0..=gx1 {
1311            if let Some(entry) = box_entry(&start, d, max_t, &cell_box(field, gx, gz)) {
1312                cells.push((entry, gz, gx));
1313            }
1314        }
1315    }
1316    cells.sort_unstable();
1317    let mut best: Option<Contact> = None;
1318    for (entry, gz, gx) in cells {
1319        if best.is_some_and(|c| c.t < entry) {
1320            break;
1321        }
1322        if let Some(c) = cell_toi(&Cell::new(field, gx, gz), a, b, r, d, max_t) {
1323            if best.is_none_or(|bc| c.t < bc.t) {
1324                best = Some(c);
1325            }
1326        }
1327    }
1328    best
1329}
1330
1331/// `num / den` limited to `[−big, big]` (no overflow for a tiny `den`).
1332fn ratio_within(num: Fix128, den: Fix128, big: Fix128) -> Fix128 {
1333    if num.abs() >= den.abs() * big {
1334        if num.is_negative() == den.is_negative() {
1335            big
1336        } else {
1337            -big
1338        }
1339    } else {
1340        num / den
1341    }
1342}
1343
1344/// The first `t ∈ [0, max_t]` at which `moving` shifted by `t·d` meets `target`
1345/// (boxes touching count), or `None`.
1346fn box_entry(moving: &AABB, d: Vec3Fix, max_t: Fix128, target: &AABB) -> Option<Fix128> {
1347    let big = max_t + Fix128::ONE;
1348    let mut lo = Fix128::ZERO;
1349    let mut hi = max_t;
1350    for (m0, m1, t0, t1, di) in [
1351        (moving.min.x, moving.max.x, target.min.x, target.max.x, d.x),
1352        (moving.min.y, moving.max.y, target.min.y, target.max.y, d.y),
1353        (moving.min.z, moving.max.z, target.min.z, target.max.z, d.z),
1354    ] {
1355        if di.abs() < PARALLEL_EPSILON {
1356            // Nearly no motion on this axis: at most max_t·|di| either way.
1357            let slack = max_t * di.abs();
1358            if m1 + slack < t0 || m0 - slack > t1 {
1359                return None;
1360            }
1361            continue;
1362        }
1363        let (enter, leave) = if di.is_negative() {
1364            (
1365                ratio_within(t1 - m0, di, big),
1366                ratio_within(t0 - m1, di, big),
1367            )
1368        } else {
1369            (
1370                ratio_within(t0 - m1, di, big),
1371                ratio_within(t1 - m0, di, big),
1372            )
1373        };
1374        lo = max_fix(lo, enter);
1375        hi = min_fix(hi, leave);
1376    }
1377    (lo <= hi).then_some(lo)
1378}
1379
1380/// The box swept by the core `a`–`b` grown by `r` moving along `d` for `max_t`.
1381fn swept_box(a: Vec3Fix, b: Vec3Fix, r: Fix128, d: Vec3Fix, max_t: Fix128) -> AABB {
1382    let start = core_box(a, b, r);
1383    let end = core_box(a + d * max_t, b + d * max_t, r);
1384    AABB::new(
1385        Vec3Fix::new(
1386            min_fix(start.min.x, end.min.x),
1387            min_fix(start.min.y, end.min.y),
1388            min_fix(start.min.z, end.min.z),
1389        ),
1390        Vec3Fix::new(
1391            max_fix(start.max.x, end.max.x),
1392            max_fix(start.max.y, end.max.y),
1393            max_fix(start.max.z, end.max.z),
1394        ),
1395    )
1396}
1397
1398/// The first contact of the core `a`–`b` grown by `r` moving along the unit `d`
1399/// with a triangle mesh: a convex time of impact per triangle the swept box
1400/// meets, in order of the time the moving box reaches the triangle's box. Each
1401/// triangle is convex, so running along one triangle (a floor) is not a step
1402/// budget spent against another (a wall).
1403fn sweep_mesh(
1404    mesh: &TriMesh,
1405    a: Vec3Fix,
1406    b: Vec3Fix,
1407    r: Fix128,
1408    d: Vec3Fix,
1409    max_t: Fix128,
1410) -> Option<Contact> {
1411    let start = core_box(a, b, r);
1412    let mut order: Vec<(Fix128, u32)> = mesh_candidates(mesh, &swept_box(a, b, r, d, max_t))
1413        .into_iter()
1414        .filter_map(|i| {
1415            let tri = &mesh.triangles[i as usize];
1416            let tri_box = AABB::new(
1417                Vec3Fix::new(
1418                    min_fix(min_fix(tri.v0.x, tri.v1.x), tri.v2.x),
1419                    min_fix(min_fix(tri.v0.y, tri.v1.y), tri.v2.y),
1420                    min_fix(min_fix(tri.v0.z, tri.v1.z), tri.v2.z),
1421                ),
1422                Vec3Fix::new(
1423                    max_fix(max_fix(tri.v0.x, tri.v1.x), tri.v2.x),
1424                    max_fix(max_fix(tri.v0.y, tri.v1.y), tri.v2.y),
1425                    max_fix(max_fix(tri.v0.z, tri.v1.z), tri.v2.z),
1426                ),
1427            );
1428            box_entry(&start, d, max_t, &tri_box).map(|entry| (entry, i))
1429        })
1430        .collect();
1431    order.sort_unstable();
1432    let mut best: Option<Contact> = None;
1433    for (entry, i) in order {
1434        if best.is_some_and(|c| c.t < entry) {
1435            break;
1436        }
1437        let tri = &mesh.triangles[i as usize];
1438        if let Some(c) = toi_convex(a, b, r, Fix128::ZERO, d, max_t, &TriangleSupport(tri)) {
1439            if best.is_none_or(|bc| c.t < bc.t) {
1440                best = Some(c);
1441            }
1442        }
1443    }
1444    best
1445}
1446
1447/// The ring of a torus (radius `major` in its local `XZ` plane) and its tube
1448/// radius: the solid torus is the ring grown by `minor`.
1449#[derive(Clone, Copy)]
1450struct Ring {
1451    center: Vec3Fix,
1452    rotation: QuatFix,
1453    major: Fix128,
1454    minor: Fix128,
1455}
1456
1457/// The arc of a [`Ring`] from the unit local direction `e0` counter-clockwise
1458/// (from `+X` toward `+Z`) to `e1`, less than half a turn.
1459#[derive(Clone, Copy)]
1460struct RingArc {
1461    e0: Vec3Fix,
1462    e1: Vec3Fix,
1463}
1464
1465/// `(a × b)·Y` for vectors in the `XZ` plane: positive when `b` is
1466/// counter-clockwise from `a` (from `+X` toward `+Z`).
1467fn turn(a: Vec3Fix, b: Vec3Fix) -> Fix128 {
1468    a.x * b.z - a.z * b.x
1469}
1470
1471impl RingArc {
1472    /// How far the chord is inside the arc (the sagitta): every point of the
1473    /// arc's hull is within it of the arc.
1474    fn thickness(&self, major: Fix128) -> Fix128 {
1475        let half_chord = ((self.e1 - self.e0).length() * major).half();
1476        major - (major * major - half_chord * half_chord).sqrt()
1477    }
1478
1479    fn split(&self) -> [Self; 2] {
1480        let mid = (self.e0 + self.e1).normalize();
1481        [
1482            Self {
1483                e0: self.e0,
1484                e1: mid,
1485            },
1486            Self {
1487                e0: mid,
1488                e1: self.e1,
1489            },
1490        ]
1491    }
1492}
1493
1494/// An arc of a ring placed in the world, for GJK: its convex hull (the circular
1495/// segment).
1496struct ArcSupport {
1497    ring: Ring,
1498    arc: RingArc,
1499}
1500
1501impl Support for ArcSupport {
1502    fn support(&self, direction: Vec3Fix) -> Vec3Fix {
1503        let local = self.ring.rotation.conjugate().rotate_vec(direction);
1504        let flat = Vec3Fix::new(local.x, Fix128::ZERO, local.z);
1505        let (e0, e1) = (self.arc.e0, self.arc.e1);
1506        let e = match flat.try_normalize() {
1507            Some(u) if !turn(e0, u).is_negative() && !turn(u, e1).is_negative() => u,
1508            _ if e1.dot(flat) > e0.dot(flat) => e1,
1509            _ => e0,
1510        };
1511        self.ring.center + self.ring.rotation.rotate_vec(e * self.ring.major)
1512    }
1513}
1514
1515impl Ring {
1516    /// The first contact of the core `a`–`b` grown by `r` with the solid torus:
1517    /// [`toi_tree`] over arcs of the ring, each arc's hull grown by the tube
1518    /// radius (a convex set containing that part of the torus), split until the
1519    /// hull is within [`TRACE_TOLERANCE`] of the arc.
1520    fn sweep(
1521        &self,
1522        a: Vec3Fix,
1523        b: Vec3Fix,
1524        r: Fix128,
1525        d: Vec3Fix,
1526        max_t: Fix128,
1527    ) -> Option<Contact> {
1528        let x = Vec3Fix::UNIT_X;
1529        let z = Vec3Fix::new(Fix128::ZERO, Fix128::ZERO, Fix128::ONE);
1530        let quarters = [
1531            RingArc { e0: x, e1: z },
1532            RingArc { e0: z, e1: -x },
1533            RingArc { e0: -x, e1: -z },
1534            RingArc { e0: -z, e1: x },
1535        ];
1536        toi_tree(
1537            &quarters,
1538            |arc| {
1539                let hull = ArcSupport {
1540                    ring: *self,
1541                    arc: *arc,
1542                };
1543                toi_convex(a, b, r, self.minor, d, max_t, &hull)
1544            },
1545            |arc| arc.thickness(self.major) <= TRACE_TOLERANCE,
1546            |arc| arc.split().to_vec(),
1547        )
1548        .map(|c| self.refine(a + d * c.t, b + d * c.t, c))
1549    }
1550
1551    /// A contact found on an arc's hull moved onto the torus: the ring point
1552    /// nearest the core point nearest the contact, grown by the tube radius
1553    /// (the hull contact is within the arc's sagitta of it).
1554    fn refine(&self, a: Vec3Fix, b: Vec3Fix, found: Contact) -> Contact {
1555        let core = closest_on_segment(a, b, found.point);
1556        let inv = self.rotation.conjugate();
1557        let local = inv.rotate_vec(core - self.center);
1558        let Some(dir) = Vec3Fix::new(local.x, Fix128::ZERO, local.z).try_normalize() else {
1559            return found;
1560        };
1561        let ring = self.center + self.rotation.rotate_vec(dir * self.major);
1562        match (core - ring).try_normalize() {
1563            Some(n) if n.dot(found.normal) > Fix128::ZERO => Contact {
1564                t: found.t,
1565                point: ring + n * self.minor,
1566                normal: n,
1567            },
1568            _ => found,
1569        }
1570    }
1571}
1572
1573// ============================================================================
1574// Time of impact
1575// ============================================================================
1576
1577/// The first time the core `a`–`b` grown by `r`, moving along the unit `d` for at
1578/// most `max_t`, touches a convex `solid` grown by `inflate`.
1579///
1580/// The gap `g(t) = dist(core + t·d, solid) − r − inflate` of two convex sets
1581/// moving apart linearly is convex in `t`, and `g′(t) = d·n` with `n` the unit
1582/// normal from the solid toward the core. Newton's step from below,
1583/// `t + g / (−g′)`, then never passes the first root (the tangent of a convex
1584/// function lies below it) and converges quadratically (linearly at a grazing,
1585/// double root); a gap that is not decreasing (`g′ ≥ 0`) never decreases again,
1586/// which proves there is no contact. A step that lands past the root (the
1587/// distance and normal are computed to within GJK's tolerance) is undone by
1588/// bisection between the last clear time and that time.
1589///
1590/// Returns the contact once the gap is within [`TRACE_TOLERANCE`] and the core
1591/// moves into the solid (see [`touch_is_hit`]): a core touching the solid and
1592/// moving away from it or along it does not hit. A core that overlaps the solid at `t = 0` by
1593/// more than the tolerance hits at `t = 0` with normal `−d`. If the step budget
1594/// runs out (it does not in practice: Newton needs a few dozen steps even at a
1595/// grazing approach), the last clear time is reported, which is never after the
1596/// true contact.
1597fn toi_convex<S: Support>(
1598    a: Vec3Fix,
1599    b: Vec3Fix,
1600    r: Fix128,
1601    inflate: Fix128,
1602    d: Vec3Fix,
1603    max_t: Fix128,
1604    solid: &S,
1605) -> Option<Contact> {
1606    let reach = r + inflate;
1607    let mut t = Fix128::ZERO;
1608    // The last time known clear (gap above the tolerance) and its near contact.
1609    let mut clear: Option<Contact> = None;
1610    // A time known to overlap, once a step has gone past the root.
1611    let mut deep: Option<Fix128> = None;
1612    for _ in 0..TRACE_MAX_STEPS {
1613        let off = d * t;
1614        let state = match convex_dist(a + off, b + off, solid) {
1615            Dist::Outside {
1616                dist,
1617                point,
1618                normal,
1619            } if dist - reach >= -TRACE_TOLERANCE => {
1620                let gap = dist - reach;
1621                let slope = d.dot(normal);
1622                let contact = Contact {
1623                    t,
1624                    point: point + normal * inflate,
1625                    normal,
1626                };
1627                if gap <= TRACE_TOLERANCE {
1628                    let gap_at = |t: Fix128| {
1629                        let off = d * t;
1630                        match convex_dist(a + off, b + off, solid) {
1631                            Dist::Outside { dist, .. } => Some(dist - reach),
1632                            _ => None,
1633                        }
1634                    };
1635                    return touch_is_hit(contact, d, max_t, gap_at).then_some(contact);
1636                }
1637                if !slope.is_negative() {
1638                    return None;
1639                }
1640                Some((gap, -slope, contact))
1641            }
1642            _ => None,
1643        };
1644        match state {
1645            Some((gap, speed, contact)) => {
1646                clear = Some(contact);
1647                let limit = deep.unwrap_or(max_t);
1648                // The tangent's root, unless it is at or past the limit.
1649                if gap >= (limit - t) * speed {
1650                    match deep {
1651                        None if gap > (limit - t) * speed => return None,
1652                        None => t = limit,
1653                        Some(hi) => t = (t + hi).half(),
1654                    }
1655                } else {
1656                    t = t + gap / speed;
1657                }
1658            }
1659            None => {
1660                if t.is_zero() {
1661                    return Some(Contact {
1662                        t,
1663                        point: half_vec(a + b),
1664                        normal: -d,
1665                    });
1666                }
1667                deep = Some(t);
1668                let lo = clear.map_or(Fix128::ZERO, |c| c.t);
1669                if t - lo <= TRACE_TOLERANCE {
1670                    return clear;
1671                }
1672                t = (lo + t).half();
1673            }
1674        }
1675    }
1676    clear
1677}
1678
1679/// Whether a contact found touching a convex piece (gap at most `2⁻³²`, normal
1680/// `n` toward the core) is a hit for the motion along the unit `d`: moving away
1681/// or along the surface (`d·n ≥ 0`) is not; moving into it is, except that when
1682/// the motion is within [`NEAR_TANGENT`] of the tangent plane the contact is a
1683/// hit only if `gap_at` (the gap along the path, `None` inside) then drops
1684/// below `−2⁻³²` before `max_t`. A shape resting on a mesh floor or a flat
1685/// height field touches the next triangle or cell only tangentially at their
1686/// shared edge, and must not stop there; a grazing approach that really goes
1687/// in is still a hit.
1688fn touch_is_hit(
1689    contact: Contact,
1690    d: Vec3Fix,
1691    max_t: Fix128,
1692    gap_at: impl Fn(Fix128) -> Option<Fix128>,
1693) -> bool {
1694    let slope = d.dot(contact.normal);
1695    if slope < -NEAR_TANGENT {
1696        return true;
1697    }
1698    if !slope.is_negative() {
1699        return false;
1700    }
1701    match gap_at(contact.t) {
1702        None => true,
1703        Some(gap) => dips_below(gap_at, contact.t, gap, -slope, max_t),
1704    }
1705}
1706
1707/// Whether a convex gap function (`None` inside) that is `f0` at `t0` and
1708/// decreasing at `speed` drops below `−2⁻³²` in `[t0, max_t]`: steps doubling
1709/// from the tangent's root until the gap goes below that (yes), grows again
1710/// (its minimum is then bracketed and found by golden-section search) or
1711/// `max_t` is reached.
1712fn dips_below(
1713    gap_at: impl Fn(Fix128) -> Option<Fix128>,
1714    t0: Fix128,
1715    f0: Fix128,
1716    speed: Fix128,
1717    max_t: Fix128,
1718) -> bool {
1719    let deep = |g: Option<Fix128>| g.is_none_or(|g| g < -TRACE_TOLERANCE);
1720    let big = max_t + Fix128::ONE;
1721    let mut step = max_fix(
1722        ratio_within(max_fix(f0, Fix128::ZERO), speed, big),
1723        TRACE_TOLERANCE,
1724    );
1725    let (mut before, mut last, mut last_gap) = (t0, t0, f0);
1726    for _ in 0..GOLDEN_STEPS {
1727        let t1 = min_fix(t0 + step, max_t);
1728        let g1 = gap_at(t1);
1729        if deep(g1) {
1730            return true;
1731        }
1732        let g1 = g1.unwrap_or(Fix128::ZERO);
1733        if g1 > last_gap || t1 >= max_t {
1734            // Convex: the minimum is in [before, t1].
1735            let (mut lo, mut hi) = (before, t1);
1736            let ratio = Fix128::from_ratio(618_033_988_749_895, 1_000_000_000_000_000);
1737            for _ in 0..GOLDEN_STEPS {
1738                let x1 = hi - (hi - lo) * ratio;
1739                let x2 = lo + (hi - lo) * ratio;
1740                let (g1, g2) = (gap_at(x1), gap_at(x2));
1741                if deep(g1) || deep(g2) {
1742                    return true;
1743                }
1744                if g1 <= g2 {
1745                    hi = x2;
1746                } else {
1747                    lo = x1;
1748                }
1749            }
1750            return false;
1751        }
1752        before = last;
1753        last = t1;
1754        last_gap = g1;
1755        step = step.double();
1756    }
1757    false
1758}
1759
1760/// The first contact with a non-convex surface described as a hierarchy of parts
1761/// whose convex hulls contain them: `toi` is the contact with a part's hull,
1762/// `leaf` says a part's hull is within tolerance of the part, `split` divides a
1763/// part. Parts are taken best first (earliest hull contact); a hull contact is
1764/// never after the contact with the part it contains, so the first leaf taken is
1765/// the first contact of the whole surface to within the leaf tolerance. If the
1766/// node budget runs out, the earliest open hull contact is reported (never after
1767/// the true contact).
1768fn toi_tree<N: Copy>(
1769    roots: &[N],
1770    toi: impl Fn(&N) -> Option<Contact>,
1771    leaf: impl Fn(&N) -> bool,
1772    split: impl Fn(&N) -> Vec<N>,
1773) -> Option<Contact> {
1774    let mut open: Vec<(Contact, N)> = roots
1775        .iter()
1776        .filter_map(|n| toi(n).map(|c| (c, *n)))
1777        .collect();
1778    let mut nodes = 0usize;
1779    loop {
1780        let k = (0..open.len()).min_by_key(|&k| open[k].0.t)?;
1781        let (contact, node) = open.remove(k);
1782        if leaf(&node) {
1783            return Some(contact);
1784        }
1785        nodes += 1;
1786        if nodes > TREE_MAX_NODES {
1787            return Some(contact);
1788        }
1789        for child in split(&node) {
1790            if let Some(c) = toi(&child) {
1791                open.push((c, child));
1792            }
1793        }
1794    }
1795}
1796
1797/// Whether the height-field surface passes through `aabb`: over each cell's part
1798/// of the box's `XZ` footprint the bilinear surface takes its extremes at the
1799/// corners of that part.
1800fn heightfield_meets_aabb(field: &HeightField, aabb: &AABB) -> bool {
1801    if !field_has_surface(field) {
1802        return false;
1803    }
1804    let s = field.spacing;
1805    let x_end = field.origin.x + s * Fix128::from_int(i64::from(field.width) - 1);
1806    let z_end = field.origin.z + s * Fix128::from_int(i64::from(field.depth) - 1);
1807    let fx0 = max_fix(aabb.min.x, field.origin.x);
1808    let fx1 = min_fix(aabb.max.x, x_end);
1809    let fz0 = max_fix(aabb.min.z, field.origin.z);
1810    let fz1 = min_fix(aabb.max.z, z_end);
1811    if fx0 > fx1 || fz0 > fz1 {
1812        return false;
1813    }
1814    let (Some((gx0, gx1)), Some((gz0, gz1))) = (
1815        cell_range(fx0, fx1, field.origin.x, s, field.width),
1816        cell_range(fz0, fz1, field.origin.z, s, field.depth),
1817    ) else {
1818        return false;
1819    };
1820    for gz in gz0..=gz1 {
1821        for gx in gx0..=gx1 {
1822            let x0 = field.origin.x + s * Fix128::from_int(i64::from(gx));
1823            let z0 = field.origin.z + s * Fix128::from_int(i64::from(gz));
1824            let ux0 = max_fix(fx0, x0);
1825            let ux1 = min_fix(fx1, x0 + s);
1826            let uz0 = max_fix(fz0, z0);
1827            let uz1 = min_fix(fz1, z0 + s);
1828            if ux0 > ux1 || uz0 > uz1 {
1829                continue;
1830            }
1831            let [h00, h10, h01, h11] = cell_heights(field, gx, gz);
1832            let mut lo: Option<Fix128> = None;
1833            let mut hi: Option<Fix128> = None;
1834            for x in [ux0, ux1] {
1835                for z in [uz0, uz1] {
1836                    let u = (x - x0) / s;
1837                    let v = (z - z0) / s;
1838                    let one = Fix128::ONE;
1839                    let h = h00 * (one - u) * (one - v)
1840                        + h10 * u * (one - v)
1841                        + h01 * (one - u) * v
1842                        + h11 * u * v;
1843                    lo = Some(lo.map_or(h, |l| min_fix(l, h)));
1844                    hi = Some(hi.map_or(h, |m| max_fix(m, h)));
1845                }
1846            }
1847            if let (Some(lo), Some(hi)) = (lo, hi) {
1848                if lo < aabb.max.y && hi > aabb.min.y {
1849                    return true;
1850                }
1851            }
1852        }
1853    }
1854    false
1855}
1856
1857// ============================================================================
1858// Triangle meshes
1859// ============================================================================
1860
1861/// The triangles of `mesh` whose box meets `query`, in BVH order.
1862fn mesh_candidates(mesh: &TriMesh, query: &AABB) -> Vec<u32> {
1863    mesh.bvh.query(query)
1864}
1865
1866/// The distance from the core `a`–`b` to the mesh, searching the triangles within
1867/// `cap`.
1868fn mesh_dist(mesh: &TriMesh, a: Vec3Fix, b: Vec3Fix, cap: Fix128) -> Dist {
1869    if mesh.triangles.is_empty() {
1870        return Dist::AtLeast(cap);
1871    }
1872    let core = core_box(a, b, Fix128::ZERO);
1873    let far = aabb_gap(&core, &mesh.bounds);
1874    if far >= cap {
1875        return Dist::AtLeast(far);
1876    }
1877    let mut best: Option<Dist> = None;
1878    for i in mesh_candidates(mesh, &core_box(a, b, cap)) {
1879        let tri = &mesh.triangles[i as usize];
1880        let d = if a == b {
1881            match Dist::toward(a, tri.closest_point(a)) {
1882                // On the triangle: touching, with its normal.
1883                Dist::Inside => Dist::Outside {
1884                    dist: Fix128::ZERO,
1885                    point: a,
1886                    normal: tri.normal().try_normalize().unwrap_or(Vec3Fix::UNIT_Y),
1887                },
1888                other => other,
1889            }
1890        } else {
1891            convex_dist(a, b, &TriangleSupport(tri))
1892        };
1893        if matches!(d, Dist::Inside) {
1894            return Dist::Inside;
1895        }
1896        if best.as_ref().is_none_or(|x| d.value() < x.value()) {
1897            best = Some(d);
1898        }
1899    }
1900    match best {
1901        Some(d) if d.value() < cap => d,
1902        _ => Dist::AtLeast(cap),
1903    }
1904}
1905
1906/// A sphere swept against one triangle: the two copies of the triangle moved
1907/// `±r` along its normal and the three edges grown into capsules of `r`.
1908fn sweep_triangle(
1909    o: Vec3Fix,
1910    d: Vec3Fix,
1911    tri: &Triangle,
1912    r: Fix128,
1913    max_t: Fix128,
1914) -> Option<LocalHit> {
1915    let ray = crate::raycast::Ray {
1916        origin: o,
1917        direction: d,
1918    };
1919    let mut best = None;
1920    if let Some(n) = tri.normal().try_normalize() {
1921        for off in [n * r, -(n * r)] {
1922            let moved = Triangle::new(tri.v0 + off, tri.v1 + off, tri.v2 + off);
1923            best = nearer(
1924                best,
1925                ray_triangle(&ray, &moved, max_t).map(|h| LocalHit {
1926                    t: h.t,
1927                    normal: h.normal,
1928                }),
1929            );
1930        }
1931    }
1932    for (e0, e1) in [(tri.v0, tri.v1), (tri.v1, tri.v2), (tri.v2, tri.v0)] {
1933        best = nearer(
1934            best,
1935            ray_solid_capsule(o, d, &Capsule::new(e0, e1, r), max_t),
1936        );
1937    }
1938    best
1939}
1940
1941// ============================================================================
1942// Geometry pieces
1943// ============================================================================
1944
1945/// One piece of geometry a query tests, placed in the world.
1946enum Piece<'a> {
1947    Sphere {
1948        center: Vec3Fix,
1949        radius: Fix128,
1950    },
1951    Capsule(Capsule),
1952    Box {
1953        center: Vec3Fix,
1954        half: Vec3Fix,
1955        rotation: QuatFix,
1956    },
1957    Posed(PosedShape),
1958    Hull(ChildSupport<'a>),
1959    Plane(&'a PlaneCollider),
1960    Height(&'a HeightField),
1961    Mesh(&'a TriMesh),
1962    #[cfg(feature = "std")]
1963    Sdf(&'a crate::sdf_collider::SdfCollider),
1964}
1965
1966/// The pieces of a body (see [`crate::shape_raycast`] for which geometry).
1967fn body_pieces(world: &PhysicsWorld, i: usize) -> Vec<Piece<'_>> {
1968    let Some((radius, collider)) = world.ray_geometry(i) else {
1969        return Vec::new();
1970    };
1971    let body = &world.bodies[i];
1972    match collider {
1973        None => vec![Piece::Sphere {
1974            center: body.position,
1975            radius,
1976        }],
1977        Some(BodyCollider::Shape(shape)) => {
1978            let posed = PosedShape {
1979                shape: *shape,
1980                position: body.position,
1981                rotation: body.rotation,
1982            };
1983            vec![match *shape {
1984                Shape::Box { half_extents } => {
1985                    let rotation = body.rotation.unit_rotation();
1986                    Piece::Box {
1987                        center: body.position - rotation.rotate_vec(shape.center_of_mass_offset()),
1988                        half: half_extents,
1989                        rotation,
1990                    }
1991                }
1992                _ => Piece::Posed(posed),
1993            }]
1994        }
1995        Some(BodyCollider::Compound(compound)) => compound
1996            .children
1997            .iter()
1998            .map(|child| child_piece(child, body.position, body.rotation))
1999            .collect(),
2000    }
2001}
2002
2003/// One compound child, placed the way [`CompoundChild::support_world`] places it.
2004fn child_piece(child: &CompoundChild, body_pos: Vec3Fix, body_rot: QuatFix) -> Piece<'_> {
2005    let body_rot = body_rot.unit_rotation();
2006    let child_rot = body_rot.mul(child.local_rotation);
2007    let child_pos = body_pos + body_rot.rotate_vec(child.local_position);
2008    match &child.shape {
2009        ShapeRef::Sphere(s) => Piece::Sphere {
2010            center: child_pos + child_rot.rotate_vec(s.center),
2011            radius: s.radius,
2012        },
2013        ShapeRef::Capsule(c) => Piece::Capsule(Capsule::new(
2014            child_pos + child_rot.rotate_vec(c.a),
2015            child_pos + child_rot.rotate_vec(c.b),
2016            c.radius,
2017        )),
2018        ShapeRef::Box(b) => Piece::Box {
2019            center: child_pos + child_rot.rotate_vec(b.center),
2020            half: b.half_extents,
2021            rotation: child_rot.mul(b.rotation),
2022        },
2023        ShapeRef::ConvexHull(_) => Piece::Hull(ChildSupport {
2024            child,
2025            position: body_pos,
2026            rotation: body_rot,
2027        }),
2028    }
2029}
2030
2031/// The geometric centre and frame of a posed shape.
2032fn posed_frame(posed: &PosedShape) -> (Vec3Fix, QuatFix) {
2033    let rotation = posed.rotation.unit_rotation();
2034    (
2035        posed.position - rotation.rotate_vec(posed.shape.center_of_mass_offset()),
2036        rotation,
2037    )
2038}
2039
2040/// The query settings that pieces need.
2041struct Settings {
2042    #[cfg(feature = "std")]
2043    sdf: SdfCcdConfig,
2044}
2045
2046impl Piece<'_> {
2047    /// The distance from the core `a`–`b`; `cap` bounds the search where a piece
2048    /// searches cells or triangles (it may report [`Dist::AtLeast`] beyond).
2049    fn dist(&self, a: Vec3Fix, b: Vec3Fix, cap: Fix128) -> Dist {
2050        match self {
2051            Self::Sphere { center, radius } => sphere_dist(a, b, *center, *radius),
2052            Self::Capsule(c) => capsule_dist(a, b, c),
2053            Self::Box {
2054                center,
2055                half,
2056                rotation,
2057            } => {
2058                if a == b {
2059                    let inv = rotation.conjugate();
2060                    let p = inv.rotate_vec(a - *center);
2061                    dist_to_world(point_box_local(p, *half), *center, *rotation)
2062                } else {
2063                    let obb = crate::box_collider::OrientedBox::new(*center, *half, *rotation);
2064                    convex_dist(a, b, &obb)
2065                }
2066            }
2067            Self::Posed(posed) => {
2068                let (center, rotation) = posed_frame(posed);
2069                let inv = rotation.conjugate();
2070                match posed.shape {
2071                    Shape::Cylinder {
2072                        radius,
2073                        half_height,
2074                    } if a == b => dist_to_world(
2075                        point_cylinder_local(inv.rotate_vec(a - center), radius, half_height),
2076                        center,
2077                        rotation,
2078                    ),
2079                    Shape::Torus {
2080                        major_radius,
2081                        minor_radius,
2082                    } => {
2083                        let f = |p: Vec3Fix| {
2084                            dist_to_world(
2085                                point_torus_local(
2086                                    inv.rotate_vec(p - center),
2087                                    major_radius,
2088                                    minor_radius,
2089                                ),
2090                                center,
2091                                rotation,
2092                            )
2093                        };
2094                        segment_min(a, b, f)
2095                    }
2096                    _ => convex_dist(a, b, posed),
2097                }
2098            }
2099            Self::Hull(child) => convex_dist(a, b, child),
2100            Self::Plane(plane) => plane_dist(plane, a, b),
2101            Self::Height(field) => heightfield_dist(field, a, b, cap),
2102            Self::Mesh(mesh) => mesh_dist(mesh, a, b, cap),
2103            #[cfg(feature = "std")]
2104            Self::Sdf(sdf) => segment_min(a, b, |p| point_sdf(sdf, p)),
2105        }
2106    }
2107
2108    /// The first time the core `a`–`b` grown by `r` touches this piece moving
2109    /// along the unit `d`, when it does not overlap it at the start: closed forms
2110    /// for a point core against spheres, capsules, boxes, cylinders, tori,
2111    /// planes and triangles, the convex time of impact ([`toi_convex`]) for a
2112    /// segment core and the other convex pieces, a hierarchy of hulls for
2113    /// height-field cells and torus arcs ([`toi_tree`]), one time of impact per
2114    /// triangle for meshes, and sphere tracing for SDFs.
2115    fn sweep(
2116        &self,
2117        a: Vec3Fix,
2118        b: Vec3Fix,
2119        r: Fix128,
2120        d: Vec3Fix,
2121        max_t: Fix128,
2122        settings: &Settings,
2123    ) -> Option<Contact> {
2124        #[cfg(not(feature = "std"))]
2125        let _ = settings;
2126        let point_core = a == b;
2127        match self {
2128            Self::Sphere { center, radius } => {
2129                let c = if point_core {
2130                    let h = ray_solid_sphere(a, d, *center, *radius + r, max_t)?;
2131                    Contact::from_ray(a, d, r, h)
2132                } else {
2133                    // The capsule moving +d meets the sphere when the sphere's
2134                    // centre, moving −d, meets the capsule grown by its radius.
2135                    let grown = Capsule::new(a, b, r + *radius);
2136                    let h = ray_solid_capsule(*center, -d, &grown, max_t)?;
2137                    let normal = -h.normal;
2138                    Contact {
2139                        t: h.t,
2140                        point: *center + normal * *radius,
2141                        normal,
2142                    }
2143                };
2144                self.entering(c, a, b, r, d, max_t)
2145            }
2146            Self::Capsule(c) => {
2147                if point_core {
2148                    let grown = Capsule::new(c.a, c.b, c.radius + r);
2149                    let h = ray_solid_capsule(a, d, &grown, max_t)?;
2150                    self.entering(Contact::from_ray(a, d, r, h), a, b, r, d, max_t)
2151                } else {
2152                    toi_convex(a, b, r, c.radius, d, max_t, &SegmentSupport(c.a, c.b))
2153                }
2154            }
2155            Self::Box {
2156                center,
2157                half,
2158                rotation,
2159            } => {
2160                if point_core {
2161                    let (o, dl) = to_local(a, d, *center, *rotation);
2162                    let h = to_world(sweep_local_box(o, dl, *half, r, max_t), *rotation)?;
2163                    self.entering(Contact::from_ray(a, d, r, h), a, b, r, d, max_t)
2164                } else {
2165                    let obb = crate::box_collider::OrientedBox::new(*center, *half, *rotation);
2166                    toi_convex(a, b, r, Fix128::ZERO, d, max_t, &obb)
2167                }
2168            }
2169            Self::Posed(posed) => {
2170                let (center, rotation) = posed_frame(posed);
2171                match posed.shape {
2172                    Shape::Cylinder {
2173                        radius,
2174                        half_height,
2175                    } if point_core => {
2176                        let (o, dl) = to_local(a, d, center, rotation);
2177                        let h = to_world(
2178                            Some(sweep_local_cylinder(o, dl, radius, half_height, r, max_t)?),
2179                            rotation,
2180                        )?;
2181                        self.entering(Contact::from_ray(a, d, r, h), a, b, r, d, max_t)
2182                    }
2183                    Shape::Torus {
2184                        major_radius,
2185                        minor_radius,
2186                    } => {
2187                        let ring = Ring {
2188                            center,
2189                            rotation,
2190                            major: major_radius,
2191                            minor: minor_radius,
2192                        };
2193                        if point_core {
2194                            let (o, dl) = to_local(a, d, center, rotation);
2195                            let h = ray_local_torus(o, dl, major_radius, minor_radius + r, max_t);
2196                            let c = Contact::from_ray(a, d, r, to_world(h, rotation)?);
2197                            // A torus is not convex: leaving it where the sphere
2198                            // touches it, the path can still enter it further on.
2199                            self.entering(c, a, b, r, d, max_t)
2200                                .or_else(|| ring.sweep(a, b, r, d, max_t))
2201                        } else {
2202                            ring.sweep(a, b, r, d, max_t)
2203                        }
2204                    }
2205                    _ => toi_convex(a, b, r, Fix128::ZERO, d, max_t, posed),
2206                }
2207            }
2208            Self::Hull(child) => toi_convex(a, b, r, Fix128::ZERO, d, max_t, child),
2209            Self::Plane(plane) => sweep_plane(plane, a, b, r, d, max_t),
2210            Self::Height(field) => sweep_heightfield(field, a, b, r, d, max_t),
2211            Self::Mesh(mesh) => {
2212                if point_core {
2213                    let swept = core_box(a, a + d * max_t, r);
2214                    let mut best: Option<Contact> = None;
2215                    for i in mesh_candidates(mesh, &swept) {
2216                        let tri = &mesh.triangles[i as usize];
2217                        let Some(h) = sweep_triangle(a, d, tri, r, max_t) else {
2218                            continue;
2219                        };
2220                        let gap_at = |t: Fix128| {
2221                            let p = a + d * t;
2222                            match Dist::toward(p, tri.closest_point(p)) {
2223                                Dist::Outside { dist, .. } => Some(dist - r),
2224                                _ => None,
2225                            }
2226                        };
2227                        let mut c = Contact::from_ray(a, d, r, h);
2228                        if c.t <= TRACE_TOLERANCE {
2229                            // Touching this triangle at the start: its own
2230                            // nearest point and normal (see `entering`).
2231                            if let Dist::Outside { point, normal, .. } =
2232                                Dist::toward(a, tri.closest_point(a))
2233                            {
2234                                c = Contact {
2235                                    t: Fix128::ZERO,
2236                                    point,
2237                                    normal,
2238                                };
2239                            }
2240                        }
2241                        if !touch_is_hit(c, d, max_t, gap_at) {
2242                            continue;
2243                        }
2244                        if best.is_none_or(|bc| c.t < bc.t) {
2245                            best = Some(c);
2246                        }
2247                    }
2248                    best
2249                } else {
2250                    sweep_mesh(mesh, a, b, r, d, max_t)
2251                }
2252            }
2253            #[cfg(feature = "std")]
2254            Self::Sdf(_) => {
2255                let tol = Fix128::from_f32(settings.sdf.tolerance);
2256                self.trace(a, b, r, d, max_t, tol)
2257            }
2258        }
2259        .filter(|c| c.t >= Fix128::ZERO && c.t <= max_t)
2260    }
2261
2262    /// A closed-form contact is a hit by [`touch_is_hit`]. One at `t ≤ 2⁻³²`
2263    /// comes from a cast that starts touching the piece (an overlap is reported
2264    /// before the sweep): its normal and point are the piece's nearest point and
2265    /// normal toward the core (a closed form may report a touching origin as
2266    /// inside). Moving away or along the surface is not a hit, nor is a tangent
2267    /// arrival.
2268    fn entering(
2269        &self,
2270        c: Contact,
2271        a: Vec3Fix,
2272        b: Vec3Fix,
2273        r: Fix128,
2274        d: Vec3Fix,
2275        max_t: Fix128,
2276    ) -> Option<Contact> {
2277        let cap = r + r + Fix128::ONE;
2278        let gap_at = |t: Fix128| {
2279            let off = d * t;
2280            match self.dist(a + off, b + off, cap) {
2281                Dist::Outside { dist, .. } => Some(dist - r),
2282                Dist::AtLeast(bound) => Some(bound - r),
2283                Dist::Inside => None,
2284            }
2285        };
2286        let c = if c.t > TRACE_TOLERANCE {
2287            c
2288        } else {
2289            match self.dist(a, b, cap) {
2290                Dist::Outside { point, normal, .. } => Contact {
2291                    t: Fix128::ZERO,
2292                    point,
2293                    normal,
2294                },
2295                _ => return Some(c),
2296            }
2297        };
2298        touch_is_hit(c, d, max_t, gap_at).then_some(c)
2299    }
2300
2301    /// Sphere tracing (for SDFs): step by the gap (the distance less `r`) until
2302    /// it is below `tol`. A field that is a true distance never lets a step pass
2303    /// the surface; if the step budget runs out the last position is reported
2304    /// (never after the contact), not "no hit".
2305    #[cfg(feature = "std")]
2306    fn trace(
2307        &self,
2308        a: Vec3Fix,
2309        b: Vec3Fix,
2310        r: Fix128,
2311        d: Vec3Fix,
2312        max_t: Fix128,
2313        tol: Fix128,
2314    ) -> Option<Contact> {
2315        let mut t = Fix128::ZERO;
2316        let cap = r + r + Fix128::ONE;
2317        let mut last: Option<Contact> = None;
2318        for _ in 0..SDF_MAX_STEPS {
2319            let off = d * t;
2320            let gap = match self.dist(a + off, b + off, cap) {
2321                Dist::Inside => {
2322                    return Some(Contact {
2323                        t,
2324                        point: half_vec(a + b) + off,
2325                        normal: -d,
2326                    })
2327                }
2328                Dist::AtLeast(bound) => bound - r,
2329                Dist::Outside {
2330                    dist,
2331                    point,
2332                    normal,
2333                } => {
2334                    let gap = dist - r;
2335                    let contact = Contact { t, point, normal };
2336                    last = Some(contact);
2337                    if gap <= tol {
2338                        // Touching: a hit only when moving into the surface;
2339                        // moving away or along it, step past by the tolerance.
2340                        if d.dot(normal).is_negative() {
2341                            return Some(contact);
2342                        }
2343                        tol
2344                    } else {
2345                        gap
2346                    }
2347                }
2348            };
2349            if gap <= Fix128::ZERO {
2350                return None;
2351            }
2352            t = t + gap;
2353            if t > max_t {
2354                return None;
2355            }
2356        }
2357        last
2358    }
2359
2360    /// Whether this piece overlaps `aabb` (see the module doc).
2361    fn meets_aabb(&self, aabb: &AABB) -> bool {
2362        match self {
2363            Self::Sphere { center, radius } => {
2364                (closest_on_aabb(aabb, *center) - *center).length_squared() < *radius * *radius
2365            }
2366            Self::Capsule(c) => match gjk_distance(&SegmentSupport(c.a, c.b), aabb) {
2367                None => true,
2368                Some((dist, _, _)) => dist < c.radius,
2369            },
2370            Self::Box {
2371                center,
2372                half,
2373                rotation,
2374            } => obb_meets_aabb(*center, *half, *rotation, aabb),
2375            Self::Posed(posed) => match posed.shape {
2376                Shape::Torus {
2377                    major_radius,
2378                    minor_radius,
2379                } => torus_meets_aabb(posed, major_radius, minor_radius, aabb),
2380                _ => gjk_distance(posed, aabb).is_none(),
2381            },
2382            Self::Hull(child) => gjk_distance(child, aabb).is_none(),
2383            Self::Plane(plane) => {
2384                let c = half_vec(aabb.min + aabb.max);
2385                let h = half_vec(aabb.max - aabb.min);
2386                let n = plane.normal;
2387                let reach = n.x.abs() * h.x + n.y.abs() * h.y + n.z.abs() * h.z;
2388                (n.dot(c) - plane.offset).abs() < reach
2389            }
2390            Self::Height(field) => heightfield_meets_aabb(field, aabb),
2391            Self::Mesh(mesh) => mesh_candidates(mesh, aabb).into_iter().any(|i| {
2392                gjk_distance(&TriangleSupport(&mesh.triangles[i as usize]), aabb).is_none()
2393            }),
2394            #[cfg(feature = "std")]
2395            Self::Sdf(sdf) => sdf_meets_aabb(sdf, aabb),
2396        }
2397    }
2398}
2399
2400/// A contact of a cast: distance along the direction, contact point, normal.
2401#[derive(Clone, Copy, Debug)]
2402struct Contact {
2403    t: Fix128,
2404    point: Vec3Fix,
2405    normal: Vec3Fix,
2406}
2407
2408impl Contact {
2409    /// A ray hit on the Minkowski sum turned into the contact of the sphere of `r`
2410    /// whose centre moved from `o` along `d`.
2411    fn from_ray(o: Vec3Fix, d: Vec3Fix, r: Fix128, h: LocalHit) -> Self {
2412        Self {
2413            t: h.t,
2414            point: o + d * h.t - h.normal * r,
2415            normal: h.normal,
2416        }
2417    }
2418}
2419
2420/// A box of half-extents `h` grown by `r` (rounded), in its frame: the three
2421/// slabs (the box pushed out by `r` along one axis) and the 12 edges as capsules.
2422fn sweep_local_box(
2423    o: Vec3Fix,
2424    d: Vec3Fix,
2425    h: Vec3Fix,
2426    r: Fix128,
2427    max_t: Fix128,
2428) -> Option<LocalHit> {
2429    let mut best = None;
2430    for grow in [
2431        Vec3Fix::new(r, Fix128::ZERO, Fix128::ZERO),
2432        Vec3Fix::new(Fix128::ZERO, r, Fix128::ZERO),
2433        Vec3Fix::new(Fix128::ZERO, Fix128::ZERO, r),
2434    ] {
2435        best = nearer(best, ray_planes(o, d, &box_planes(h + grow), max_t));
2436    }
2437    let signs = [Fix128::ONE, Fix128::NEG_ONE];
2438    for &s1 in &signs {
2439        for &s2 in &signs {
2440            for (a, b) in [
2441                (
2442                    Vec3Fix::new(-h.x, s1 * h.y, s2 * h.z),
2443                    Vec3Fix::new(h.x, s1 * h.y, s2 * h.z),
2444                ),
2445                (
2446                    Vec3Fix::new(s1 * h.x, -h.y, s2 * h.z),
2447                    Vec3Fix::new(s1 * h.x, h.y, s2 * h.z),
2448                ),
2449                (
2450                    Vec3Fix::new(s1 * h.x, s2 * h.y, -h.z),
2451                    Vec3Fix::new(s1 * h.x, s2 * h.y, h.z),
2452                ),
2453            ] {
2454                best = nearer(best, ray_solid_capsule(o, d, &Capsule::new(a, b, r), max_t));
2455            }
2456        }
2457    }
2458    best
2459}
2460
2461/// A cylinder along `Y` grown by `r` (rounded), in its frame: the cylinder of
2462/// radius `radius + r`, the one of half height `hh + r`, and the two rim tori.
2463fn sweep_local_cylinder(
2464    o: Vec3Fix,
2465    d: Vec3Fix,
2466    radius: Fix128,
2467    hh: Fix128,
2468    r: Fix128,
2469    max_t: Fix128,
2470) -> Option<LocalHit> {
2471    let mut best = ray_local_cylinder(o, d, radius + r, hh, max_t);
2472    best = nearer(best, ray_local_cylinder(o, d, radius, hh + r, max_t));
2473    for y in [hh, -hh] {
2474        let shift = Vec3Fix::new(Fix128::ZERO, y, Fix128::ZERO);
2475        best = nearer(best, ray_local_torus(o - shift, d, radius, r, max_t));
2476    }
2477    best
2478}
2479
2480/// Whether an oriented box overlaps an axis-aligned one, by the separating axis
2481/// theorem (the 3 + 3 face axes and the 9 edge cross products): they overlap
2482/// when no axis separates them, and boxes that only touch (projections meeting
2483/// at a point) do not overlap. Cross products of parallel edges are zero and
2484/// separate nothing; they are skipped.
2485fn obb_meets_aabb(center: Vec3Fix, half: Vec3Fix, rotation: QuatFix, aabb: &AABB) -> bool {
2486    let z = Vec3Fix::new(Fix128::ZERO, Fix128::ZERO, Fix128::ONE);
2487    let a_axes = [
2488        rotation.rotate_vec(Vec3Fix::UNIT_X),
2489        rotation.rotate_vec(Vec3Fix::UNIT_Y),
2490        rotation.rotate_vec(z),
2491    ];
2492    let a_half = [half.x, half.y, half.z];
2493    let b_axes = [Vec3Fix::UNIT_X, Vec3Fix::UNIT_Y, z];
2494    let b_center = half_vec(aabb.min + aabb.max);
2495    let b_ext = half_vec(aabb.max - aabb.min);
2496    let b_half = [b_ext.x, b_ext.y, b_ext.z];
2497    let between = b_center - center;
2498    let separates = |l: Vec3Fix| {
2499        let reach_a: Fix128 = (0..3).fold(Fix128::ZERO, |acc, i| {
2500            acc + a_half[i] * a_axes[i].dot(l).abs()
2501        });
2502        let reach_b: Fix128 = (0..3).fold(Fix128::ZERO, |acc, i| {
2503            acc + b_half[i] * b_axes[i].dot(l).abs()
2504        });
2505        between.dot(l).abs() >= reach_a + reach_b
2506    };
2507    let mut axes: Vec<Vec3Fix> = Vec::with_capacity(15);
2508    axes.extend_from_slice(&a_axes);
2509    axes.extend_from_slice(&b_axes);
2510    for ea in a_axes {
2511        for eb in b_axes {
2512            let l = ea.cross(eb);
2513            if !l.length_squared().is_zero() {
2514                axes.push(l);
2515            }
2516        }
2517    }
2518    !axes.into_iter().any(separates)
2519}
2520
2521/// The distance from the core `a`–`b` to a two-sided plane.
2522fn plane_dist(plane: &PlaneCollider, a: Vec3Fix, b: Vec3Fix) -> Dist {
2523    let n = plane.normal;
2524    let sa = n.dot(a) - plane.offset;
2525    let sb = n.dot(b) - plane.offset;
2526    if !sa.is_zero() && !sb.is_zero() && sa.is_negative() != sb.is_negative() {
2527        // The segment crosses the plane.
2528        return Dist::Inside;
2529    }
2530    let (p, s) = if sb.abs() < sa.abs() {
2531        (b, sb)
2532    } else {
2533        (a, sa)
2534    };
2535    // An end on the plane is touching it, on the side of the other end.
2536    let other = if sb.abs() < sa.abs() { sa } else { sb };
2537    let side = if s.is_negative() || (s.is_zero() && other.is_negative()) {
2538        -n
2539    } else {
2540        n
2541    };
2542    Dist::Outside {
2543        dist: s.abs(),
2544        point: p - n * s,
2545        normal: side,
2546    }
2547}
2548
2549/// A capsule (or sphere) swept against a two-sided plane: the nearer end reaches
2550/// distance `r`.
2551fn sweep_plane(
2552    plane: &PlaneCollider,
2553    a: Vec3Fix,
2554    b: Vec3Fix,
2555    r: Fix128,
2556    d: Vec3Fix,
2557    max_t: Fix128,
2558) -> Option<Contact> {
2559    let n = plane.normal;
2560    let sa = n.dot(a) - plane.offset;
2561    let sb = n.dot(b) - plane.offset;
2562    if sa.is_zero() || sb.is_zero() || sa.is_negative() != sb.is_negative() {
2563        return None;
2564    }
2565    // The nearer end (the first on a tie).
2566    let (p, s) = if sb.abs() < sa.abs() {
2567        (b, sb)
2568    } else {
2569        (a, sa)
2570    };
2571    let normal = if s.is_negative() { -n } else { n };
2572    let speed = -normal.dot(d);
2573    if speed < PARALLEL_EPSILON {
2574        return None;
2575    }
2576    // A start within the tolerance inside the plane's reach (deeper is a start
2577    // overlap, reported before the sweep) is touching: contact at once.
2578    let t = max_fix((s.abs() - r) / speed, Fix128::ZERO);
2579    if t > max_t {
2580        return None;
2581    }
2582    Some(Contact {
2583        t,
2584        point: p + d * t - normal * r,
2585        normal,
2586    })
2587}
2588
2589/// Whether a torus meets `aabb`: its ring comes within `minor` of the box.
2590fn torus_meets_aabb(posed: &PosedShape, major: Fix128, minor: Fix128, aabb: &AABB) -> bool {
2591    let (center, rotation) = posed_frame(posed);
2592    // Quick rejection by the bounding sphere.
2593    if (closest_on_aabb(aabb, center) - center).length() >= major + minor {
2594        return false;
2595    }
2596    let ring = |s: Fix128| {
2597        let (sin, cos) = (Fix128::TWO_PI * s).sin_cos();
2598        center + rotation.rotate_vec(Vec3Fix::new(major * cos, Fix128::ZERO, major * sin))
2599    };
2600    let d = curve_min(Fix128::TWO_PI * major, |s| {
2601        let p = ring(s);
2602        let q = closest_on_aabb(aabb, p);
2603        if q == p {
2604            Dist::Inside
2605        } else {
2606            Dist::toward(p, q)
2607        }
2608    });
2609    d.within(minor)
2610}
2611
2612#[cfg(feature = "std")]
2613fn point_sdf(sdf: &crate::sdf_collider::SdfCollider, p: Vec3Fix) -> Dist {
2614    let (lx, ly, lz) = sdf.world_to_local(p);
2615    let (d, (nx, ny, nz)) = sdf.field.distance_and_normal(lx, ly, lz);
2616    let d = d * sdf.scale_f32;
2617    if !d.is_finite() || d < 0.0 {
2618        return Dist::Inside;
2619    }
2620    let dist = Fix128::from_f32(d);
2621    let normal = sdf.local_normal_to_world(nx, ny, nz);
2622    Dist::Outside {
2623        dist,
2624        point: p - normal * dist,
2625        normal,
2626    }
2627}
2628
2629/// Whether an SDF solid meets `aabb`: an octree subdivision of the box, pruning
2630/// cells whose centre is farther than their half-diagonal from the surface and
2631/// accepting a centre inside; a leaf (depth `SDF_BOX_DEPTH`) within its
2632/// half-diagonal is counted as meeting.
2633#[cfg(feature = "std")]
2634fn sdf_meets_aabb(sdf: &crate::sdf_collider::SdfCollider, aabb: &AABB) -> bool {
2635    let mut stack = vec![(aabb.min, aabb.max, 0u32)];
2636    while let Some((lo, hi, depth)) = stack.pop() {
2637        let c = half_vec(lo + hi);
2638        let half_diag = half_vec(hi - lo).length();
2639        let d = match point_sdf(sdf, c) {
2640            Dist::Inside => return true,
2641            Dist::Outside { dist, .. } => dist,
2642            Dist::AtLeast(d) => d,
2643        };
2644        if d >= half_diag {
2645            continue;
2646        }
2647        if depth >= SDF_BOX_DEPTH {
2648            return true;
2649        }
2650        for k in 0..8u8 {
2651            let pick = |bit: u8, l: Fix128, m: Fix128, h: Fix128| {
2652                if k & bit == 0 {
2653                    (l, m)
2654                } else {
2655                    (m, h)
2656                }
2657            };
2658            let (x0, x1) = pick(1, lo.x, c.x, hi.x);
2659            let (y0, y1) = pick(2, lo.y, c.y, hi.y);
2660            let (z0, z1) = pick(4, lo.z, c.z, hi.z);
2661            stack.push((
2662                Vec3Fix::new(x0, y0, z0),
2663                Vec3Fix::new(x1, y1, z1),
2664                depth + 1,
2665            ));
2666        }
2667    }
2668    false
2669}
2670
2671/// The box of the segment `a`–`b` grown by `r`.
2672fn core_box(a: Vec3Fix, b: Vec3Fix, r: Fix128) -> AABB {
2673    let lo = Vec3Fix::new(min_fix(a.x, b.x), min_fix(a.y, b.y), min_fix(a.z, b.z));
2674    let hi = Vec3Fix::new(max_fix(a.x, b.x), max_fix(a.y, b.y), max_fix(a.z, b.z));
2675    let g = Vec3Fix::new(r, r, r);
2676    AABB::new(lo - g, hi + g)
2677}
2678
2679/// The distance between two boxes (zero when they meet).
2680fn aabb_gap(a: &AABB, b: &AABB) -> Fix128 {
2681    let gap = |lo1: Fix128, hi1: Fix128, lo2: Fix128, hi2: Fix128| {
2682        max_fix(Fix128::ZERO, max_fix(lo2 - hi1, lo1 - hi2))
2683    };
2684    Vec3Fix::new(
2685        gap(a.min.x, a.max.x, b.min.x, b.max.x),
2686        gap(a.min.y, a.max.y, b.min.y, b.max.y),
2687        gap(a.min.z, a.max.z, b.min.z, b.max.z),
2688    )
2689    .length()
2690}
2691
2692/// `direction` scaled down by its largest component when that is above `2²⁰`,
2693/// so that normalizing it does not overflow the squared length (`i64::MAX / 2`
2694/// squared wraps); smaller directions are returned as they are.
2695fn tame_direction(direction: Vec3Fix) -> Vec3Fix {
2696    let m = max_fix(
2697        max_fix(direction.x.abs(), direction.y.abs()),
2698        direction.z.abs(),
2699    );
2700    if m > Fix128::from_int(1 << 20) {
2701        direction / m
2702    } else {
2703        direction
2704    }
2705}
2706
2707fn box_is_valid(aabb: &AABB) -> bool {
2708    aabb.min.x <= aabb.max.x && aabb.min.y <= aabb.max.y && aabb.min.z <= aabb.max.z
2709}
2710
2711// ============================================================================
2712// World queries
2713// ============================================================================
2714
2715/// The colliders a query sees whose box meets `query`: each target with its body
2716/// and its pieces, in target order.
2717fn for_each_target<'w>(
2718    world: &'w PhysicsWorld,
2719    filter: &RayFilter,
2720    query: &AABB,
2721    mut visit: impl FnMut(RayTarget, Option<usize>, &[Piece<'w>]),
2722) {
2723    let (bvh, _) = world.query_body_bvh(filter);
2724    let mut bodies: Vec<u32> = bvh.query(query);
2725    bodies.sort_unstable();
2726    bodies.dedup();
2727    for i in bodies {
2728        let i = i as usize;
2729        let pieces = body_pieces(world, i);
2730        visit(RayTarget::Body(i), Some(i), &pieces);
2731    }
2732    if filter.include_static {
2733        for (j, collider) in world.static_colliders_slice().iter().enumerate() {
2734            let piece = match collider {
2735                StaticCollider::Plane(p) => Piece::Plane(p),
2736                StaticCollider::HeightField(f) => Piece::Height(f),
2737                StaticCollider::TriMesh(m) => Piece::Mesh(m),
2738            };
2739            visit(
2740                RayTarget::StaticCollider(j),
2741                None,
2742                core::slice::from_ref(&piece),
2743            );
2744        }
2745    }
2746    #[cfg(feature = "std")]
2747    if filter.include_sdf {
2748        for (k, sdf) in world.sdf_colliders.iter().enumerate() {
2749            let body = (sdf.body_index < world.bodies.len()).then_some(sdf.body_index);
2750            if body.is_some_and(|b| !filter.sees_body(world, b)) {
2751                continue;
2752            }
2753            let piece = Piece::Sdf(sdf);
2754            visit(RayTarget::Sdf(k), body, core::slice::from_ref(&piece));
2755        }
2756    }
2757}
2758
2759impl PhysicsWorld {
2760    /// The nearest collider a sphere of `radius` touches when its centre moves
2761    /// from `center` along `direction` for at most `max_t` (see
2762    /// [`crate::world_shape_query`] for the conventions). Radius `0` is
2763    /// [`PhysicsWorld::cast_ray`]; a negative radius, a zero direction or
2764    /// `max_t ≤ 0` gives `None`.
2765    #[must_use]
2766    pub fn cast_sphere(
2767        &self,
2768        center: Vec3Fix,
2769        radius: Fix128,
2770        direction: Vec3Fix,
2771        max_t: Fix128,
2772        filter: &RayFilter,
2773    ) -> Option<WorldShapeHit> {
2774        if radius.is_negative() {
2775            return None;
2776        }
2777        let direction = tame_direction(direction);
2778        if radius.is_zero() {
2779            return self
2780                .cast_ray(center, direction, max_t, filter)
2781                .map(|h| WorldShapeHit {
2782                    t: h.t,
2783                    point: h.point,
2784                    normal: h.normal,
2785                    target: h.target,
2786                    body: h.body,
2787                });
2788        }
2789        self.cast_core(center, center, radius, direction, max_t, filter)
2790    }
2791
2792    /// The nearest collider a capsule (segment `a`–`b` grown by `radius`) touches
2793    /// when it moves along `direction` for at most `max_t`. Coinciding ends are
2794    /// [`PhysicsWorld::cast_sphere`]; a negative radius, a zero direction or
2795    /// `max_t ≤ 0` gives `None`.
2796    #[must_use]
2797    pub fn cast_capsule(
2798        &self,
2799        a: Vec3Fix,
2800        b: Vec3Fix,
2801        radius: Fix128,
2802        direction: Vec3Fix,
2803        max_t: Fix128,
2804        filter: &RayFilter,
2805    ) -> Option<WorldShapeHit> {
2806        if a == b {
2807            return self.cast_sphere(a, radius, direction, max_t, filter);
2808        }
2809        if radius.is_negative() {
2810            return None;
2811        }
2812        self.cast_core(a, b, radius, tame_direction(direction), max_t, filter)
2813    }
2814
2815    /// Every collider a sphere of `radius` about `center` overlaps, sorted by
2816    /// target. A negative radius gives an empty list; radius `0` finds the solids
2817    /// `center` is strictly inside.
2818    #[must_use]
2819    pub fn overlap_sphere(
2820        &self,
2821        center: Vec3Fix,
2822        radius: Fix128,
2823        filter: &RayFilter,
2824    ) -> Vec<RayTarget> {
2825        let mut out = Vec::new();
2826        if radius.is_negative() {
2827            return out;
2828        }
2829        let query = core_box(center, center, radius);
2830        let cap = radius + radius + Fix128::ONE;
2831        for_each_target(self, filter, &query, |target, _, pieces| {
2832            if pieces
2833                .iter()
2834                .any(|p| p.dist(center, center, cap).within(radius))
2835            {
2836                out.push(target);
2837            }
2838        });
2839        out.sort_unstable();
2840        out
2841    }
2842
2843    /// Every collider `aabb` overlaps, sorted by target (see
2844    /// [`crate::world_shape_query`]). A box with `min > max` on any axis gives an
2845    /// empty list.
2846    #[must_use]
2847    pub fn overlap_aabb(&self, aabb: &AABB, filter: &RayFilter) -> Vec<RayTarget> {
2848        let mut out = Vec::new();
2849        if !box_is_valid(aabb) {
2850            return out;
2851        }
2852        for_each_target(self, filter, aabb, |target, _, pieces| {
2853            if pieces.iter().any(|p| p.meets_aabb(aabb)) {
2854                out.push(target);
2855            }
2856        });
2857        out.sort_unstable();
2858        out
2859    }
2860
2861    /// The colliders the capsule (segment `a`–`b` grown by `r`) overlaps, each
2862    /// with its penetration along the normal at the segment point nearest it,
2863    /// sorted by target (the pieces of one target, such as compound children, in
2864    /// their order); `None` when the segment itself meets a solid (no distance, so
2865    /// no push-out direction). Used by [`PhysicsWorld::move_character`] to push a capsule out.
2866    pub(crate) fn capsule_penetrations(
2867        &self,
2868        a: Vec3Fix,
2869        b: Vec3Fix,
2870        r: Fix128,
2871        filter: &RayFilter,
2872    ) -> Option<Vec<Penetration>> {
2873        let query = core_box(a, b, r);
2874        let cap = r + r + Fix128::ONE;
2875        let mut out = Vec::new();
2876        let mut core_inside = false;
2877        for_each_target(self, filter, &query, |target, _, pieces| {
2878            for piece in pieces {
2879                match piece.dist(a, b, cap) {
2880                    Dist::Inside => core_inside = true,
2881                    Dist::Outside { dist, normal, .. } if dist + TRACE_TOLERANCE < r => {
2882                        out.push(Penetration {
2883                            target,
2884                            depth: r - dist,
2885                            normal,
2886                        })
2887                    }
2888                    _ => {}
2889                }
2890            }
2891        });
2892        if core_inside {
2893            return None;
2894        }
2895        out.sort_by_key(|x| x.target);
2896        Some(out)
2897    }
2898
2899    /// The nearest hit of the core `a`–`b` grown by `r > 0` (or `r = 0` with
2900    /// `a ≠ b`) moving along `direction`.
2901    ///
2902    /// `direction` must have gone through `tame_direction` (both callers do):
2903    /// the `try_normalize` below squares it and wraps for components of `2³¹·⁵`
2904    /// or more.
2905    fn cast_core(
2906        &self,
2907        a: Vec3Fix,
2908        b: Vec3Fix,
2909        r: Fix128,
2910        direction: Vec3Fix,
2911        max_t: Fix128,
2912        filter: &RayFilter,
2913    ) -> Option<WorldShapeHit> {
2914        if max_t <= Fix128::ZERO {
2915            return None;
2916        }
2917        let d = direction.try_normalize()?;
2918        let settings = Settings {
2919            #[cfg(feature = "std")]
2920            sdf: filter.sdf,
2921        };
2922        let swept = {
2923            let start = core_box(a, b, r);
2924            let end = core_box(a + d * max_t, b + d * max_t, r);
2925            AABB::new(
2926                Vec3Fix::new(
2927                    min_fix(start.min.x, end.min.x),
2928                    min_fix(start.min.y, end.min.y),
2929                    min_fix(start.min.z, end.min.z),
2930                ),
2931                Vec3Fix::new(
2932                    max_fix(start.max.x, end.max.x),
2933                    max_fix(start.max.y, end.max.y),
2934                    max_fix(start.max.z, end.max.z),
2935                ),
2936            )
2937        };
2938        let start_cap = r + r + Fix128::ONE;
2939        let centre = half_vec(a + b);
2940        let mut best: Option<WorldShapeHit> = None;
2941        for_each_target(self, filter, &swept, |target, body, pieces| {
2942            let mut nearest: Option<Contact> = None;
2943            for piece in pieces {
2944                let c = if starts_overlapping(&piece.dist(a, b, start_cap), r) {
2945                    Some(Contact {
2946                        t: Fix128::ZERO,
2947                        point: centre,
2948                        normal: -d,
2949                    })
2950                } else {
2951                    piece.sweep(a, b, r, d, max_t, &settings)
2952                };
2953                if let Some(c) = c {
2954                    if nearest.is_none_or(|n| c.t < n.t) {
2955                        nearest = Some(c);
2956                    }
2957                }
2958            }
2959            if let Some(c) = nearest {
2960                let hit = WorldShapeHit {
2961                    t: c.t,
2962                    point: c.point,
2963                    normal: c.normal,
2964                    target,
2965                    body,
2966                };
2967                if best.is_none_or(|b| (hit.t, hit.target) < (b.t, b.target)) {
2968                    best = Some(hit);
2969                }
2970            }
2971        });
2972        best
2973    }
2974}
2975
2976#[cfg(test)]
2977mod tests {
2978    //! Closed-form unit tests of the swept and overlap queries: every expected
2979    //! distance, normal and point is written from the geometry by hand (the
2980    //! closed form is in a comment next to each assertion); none calls the code
2981    //! under test. A sphere of radius `r` swept along a unit direction first
2982    //! touches a solid `S` where its centre reaches the boundary of
2983    //! `S ⊕ ball(r)`.
2984    //!
2985    //! `EXACT = 1e-12` for closed-form paths (spheres, boxes against a point
2986    //! core, planes, triangles, cylinders); `ITER = 1e-8` for iterative ones (GJK,
2987    //! convex time of impact, height-field parts, torus arcs).
2988
2989    use super::*;
2990    use crate::box_collider::OrientedBox;
2991    use crate::collider::{ConvexHull, Sphere};
2992    use crate::compound::CompoundShape;
2993    use crate::solver::{PhysicsConfig, RigidBody};
2994
2995    const EXACT: f64 = 1e-12;
2996    const ITER: f64 = 1e-8;
2997
2998    fn fx(v: f64) -> Fix128 {
2999        Fix128::from_f64(v)
3000    }
3001
3002    fn v3(x: f64, y: f64, z: f64) -> Vec3Fix {
3003        Vec3Fix::new(fx(x), fx(y), fx(z))
3004    }
3005
3006    fn p3(p: [f64; 3]) -> Vec3Fix {
3007        v3(p[0], p[1], p[2])
3008    }
3009
3010    fn world() -> PhysicsWorld {
3011        PhysicsWorld::new(PhysicsConfig::default())
3012    }
3013
3014    fn all() -> RayFilter {
3015        RayFilter::default()
3016    }
3017
3018    fn sphere_cast(
3019        w: &PhysicsWorld,
3020        c: [f64; 3],
3021        r: f64,
3022        d: [f64; 3],
3023        max: f64,
3024    ) -> Option<WorldShapeHit> {
3025        w.cast_sphere(p3(c), fx(r), p3(d), fx(max), &all())
3026    }
3027
3028    fn capsule_cast(
3029        w: &PhysicsWorld,
3030        a: [f64; 3],
3031        b: [f64; 3],
3032        r: f64,
3033        d: [f64; 3],
3034        max: f64,
3035    ) -> Option<WorldShapeHit> {
3036        w.cast_capsule(p3(a), p3(b), fx(r), p3(d), fx(max), &all())
3037    }
3038
3039    fn overlap_s(w: &PhysicsWorld, c: [f64; 3], r: f64) -> Vec<RayTarget> {
3040        w.overlap_sphere(p3(c), fx(r), &all())
3041    }
3042
3043    fn overlap_b(w: &PhysicsWorld, lo: [f64; 3], hi: [f64; 3]) -> Vec<RayTarget> {
3044        w.overlap_aabb(&AABB::new(p3(lo), p3(hi)), &all())
3045    }
3046
3047    fn unit(v: [f64; 3]) -> [f64; 3] {
3048        let l = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
3049        [v[0] / l, v[1] / l, v[2] / l]
3050    }
3051
3052    #[track_caller]
3053    fn assert_vec(got: Vec3Fix, want: [f64; 3], tol: f64, what: &str) {
3054        let g = [got.x.to_f64(), got.y.to_f64(), got.z.to_f64()];
3055        for k in 0..3 {
3056            assert!(
3057                (g[k] - want[k]).abs() < tol,
3058                "{what} {g:?} but the closed form is {want:?}"
3059            );
3060        }
3061    }
3062
3063    /// A hit on `target` at `t` with `normal` (normalized here) and `point`.
3064    #[track_caller]
3065    fn assert_hit(
3066        hit: Option<WorldShapeHit>,
3067        target: RayTarget,
3068        t: f64,
3069        normal: [f64; 3],
3070        point: [f64; 3],
3071        tol: f64,
3072    ) {
3073        let h = hit.unwrap_or_else(|| panic!("expected a hit at t = {t}, got none"));
3074        assert_eq!(h.target, target, "target");
3075        assert!(
3076            (h.t.to_f64() - t).abs() < tol,
3077            "t = {} but the closed form is {t}",
3078            h.t.to_f64()
3079        );
3080        assert_vec(h.normal, unit(normal), tol, "normal");
3081        assert_vec(h.point, point, tol, "point");
3082    }
3083
3084    /// A capsule cast onto a flat face parallel to its segment: every point under
3085    /// the segment is a contact, so only `t`, the normal and the contact's height
3086    /// `y` are unique; its `x` lies between the ends' `x0..x1`.
3087    #[track_caller]
3088    fn assert_face_hit(
3089        hit: Option<WorldShapeHit>,
3090        target: RayTarget,
3091        t: f64,
3092        y: f64,
3093        x0: f64,
3094        x1: f64,
3095    ) {
3096        let h = hit.unwrap_or_else(|| panic!("expected a hit at t = {t}, got none"));
3097        assert_eq!(h.target, target, "target");
3098        assert!((h.t.to_f64() - t).abs() < ITER, "t = {}", h.t.to_f64());
3099        assert_vec(h.normal, [0.0, 1.0, 0.0], ITER, "normal");
3100        assert!(
3101            (h.point.y.to_f64() - y).abs() < ITER,
3102            "y = {}",
3103            h.point.y.to_f64()
3104        );
3105        let x = h.point.x.to_f64();
3106        assert!(x > x0 - ITER && x < x1 + ITER, "x = {x}");
3107    }
3108
3109    fn shaped(w: &mut PhysicsWorld, shape: Shape, pos: Vec3Fix) -> usize {
3110        w.add_shaped_body(&shape, Fix128::ONE, pos)
3111            .expect("valid shape")
3112    }
3113
3114    fn unit_box(w: &mut PhysicsWorld) -> usize {
3115        shaped(
3116            w,
3117            Shape::Box {
3118                half_extents: v3(1.0, 1.0, 1.0),
3119            },
3120            Vec3Fix::ZERO,
3121        )
3122    }
3123
3124    fn sphere_body(w: &mut PhysicsWorld, pos: Vec3Fix, r: f64) -> usize {
3125        w.add_body_with_radius(RigidBody::new_static(pos), fx(r))
3126    }
3127
3128    fn plane(w: &mut PhysicsWorld, normal: Vec3Fix, offset: f64) -> usize {
3129        w.add_static_collider(StaticCollider::Plane(PlaneCollider::new(
3130            normal,
3131            fx(offset),
3132        )))
3133    }
3134
3135    /// The two triangles of the floor square `[0, 4]²` at `y = 0`.
3136    fn floor_mesh(w: &mut PhysicsWorld) -> usize {
3137        let verts = [
3138            v3(0.0, 0.0, 0.0),
3139            v3(4.0, 0.0, 0.0),
3140            v3(4.0, 0.0, 4.0),
3141            v3(0.0, 0.0, 4.0),
3142        ];
3143        w.add_static_collider(StaticCollider::TriMesh(TriMesh::from_indexed(
3144            &verts,
3145            &[0, 1, 2, 0, 2, 3],
3146        )))
3147    }
3148
3149    /// One bilinear cell over `[0, 1]²` with corner heights `0, 0, 0, 1`: the
3150    /// twisted surface `y = x·z`.
3151    fn saddle_field(w: &mut PhysicsWorld) -> usize {
3152        let heights = vec![Fix128::ZERO, Fix128::ZERO, Fix128::ZERO, Fix128::ONE];
3153        w.add_static_collider(StaticCollider::HeightField(HeightField::new(
3154            heights,
3155            2,
3156            2,
3157            Fix128::ONE,
3158            Vec3Fix::ZERO,
3159        )))
3160    }
3161
3162    // ------------------------------------------------------------ sphere cast
3163
3164    #[test]
3165    fn sphere_cast_against_a_sphere_body() {
3166        let mut w = world();
3167        let b = sphere_body(&mut w, Vec3Fix::ZERO, 1.0);
3168        // oracle: head-on, the centre reaches |c| = R + r = 1.5: t = 10 − 1.5;
3169        // the direction (2, 0, 0) is normalized first.
3170        assert_hit(
3171            sphere_cast(&w, [-10.0, 0.0, 0.0], 0.5, [2.0, 0.0, 0.0], 100.0),
3172            RayTarget::Body(b),
3173            8.5,
3174            [-1.0, 0.0, 0.0],
3175            [-1.0, 0.0, 0.0],
3176            EXACT,
3177        );
3178        // oracle: on the line y = 0.6, x = −√(1.5² − 0.36) = −√1.89 at contact,
3179        // t = 10 − √1.89, normal c/1.5, contact R·normal.
3180        let s = 1.89f64.sqrt();
3181        let n = [-s / 1.5, 0.6 / 1.5, 0.0];
3182        assert_hit(
3183            sphere_cast(&w, [-10.0, 0.6, 0.0], 0.5, [1.0, 0.0, 0.0], 100.0),
3184            RayTarget::Body(b),
3185            10.0 - s,
3186            n,
3187            n,
3188            EXACT,
3189        );
3190        // oracle: the line y = 1.6 passes 1.6 > 1.5 from the centre: no contact.
3191        assert_eq!(
3192            sphere_cast(&w, [-10.0, 1.6, 0.0], 0.5, [1.0, 0.0, 0.0], 100.0),
3193            None
3194        );
3195        // oracle: the contact at t = 8.5 is beyond max_t = 5.
3196        assert_eq!(
3197            sphere_cast(&w, [-10.0, 0.0, 0.0], 0.5, [1.0, 0.0, 0.0], 5.0),
3198            None
3199        );
3200        let h = sphere_cast(&w, [-10.0, 0.0, 0.0], 0.5, [1.0, 0.0, 0.0], 100.0).expect("hit");
3201        assert_eq!(h.body, Some(b));
3202    }
3203
3204    #[test]
3205    fn degenerate_casts_give_no_hit() {
3206        let mut w = world();
3207        sphere_body(&mut w, Vec3Fix::ZERO, 1.0);
3208        let c = [-10.0, 0.0, 0.0];
3209        // A negative radius, a zero direction and max_t ≤ 0 give no hit.
3210        assert_eq!(sphere_cast(&w, c, -0.5, [1.0, 0.0, 0.0], 100.0), None);
3211        assert_eq!(sphere_cast(&w, c, 0.5, [0.0, 0.0, 0.0], 100.0), None);
3212        assert_eq!(sphere_cast(&w, c, 0.5, [1.0, 0.0, 0.0], 0.0), None);
3213        assert_eq!(sphere_cast(&w, c, 0.5, [1.0, 0.0, 0.0], -1.0), None);
3214        assert_eq!(
3215            capsule_cast(&w, c, [-10.0, 1.0, 0.0], -0.5, [1.0, 0.0, 0.0], 100.0),
3216            None
3217        );
3218        assert_eq!(
3219            capsule_cast(&w, c, [-10.0, 1.0, 0.0], 0.5, [1.0, 0.0, 0.0], 0.0),
3220            None
3221        );
3222    }
3223
3224    #[test]
3225    fn radius_zero_is_a_ray_and_a_huge_direction_is_normalized() {
3226        let mut w = world();
3227        let b = sphere_body(&mut w, Vec3Fix::ZERO, 1.0);
3228        // oracle: a ray from x = −10 meets the unit sphere at x = −1: t = 9.
3229        assert_hit(
3230            sphere_cast(&w, [-10.0, 0.0, 0.0], 0.0, [1.0, 0.0, 0.0], 100.0),
3231            RayTarget::Body(b),
3232            9.0,
3233            [-1.0, 0.0, 0.0],
3234            [-1.0, 0.0, 0.0],
3235            EXACT,
3236        );
3237        // oracle: the direction (1e7, 0, 0) is the unit +X: t = 8.5.
3238        assert_hit(
3239            sphere_cast(&w, [-10.0, 0.0, 0.0], 0.5, [1e7, 0.0, 0.0], 100.0),
3240            RayTarget::Body(b),
3241            8.5,
3242            [-1.0, 0.0, 0.0],
3243            [-1.0, 0.0, 0.0],
3244            EXACT,
3245        );
3246    }
3247
3248    #[test]
3249    fn a_cast_that_starts_overlapping_hits_at_zero() {
3250        let mut w = world();
3251        let b = sphere_body(&mut w, Vec3Fix::ZERO, 1.0);
3252        // oracle: the centre 0.5 from a body of radius 1: overlapping, t = 0,
3253        // normal −direction, point the cast centre.
3254        assert_hit(
3255            sphere_cast(&w, [0.5, 0.0, 0.0], 0.5, [0.0, 1.0, 0.0], 100.0),
3256            RayTarget::Body(b),
3257            0.0,
3258            [0.0, -1.0, 0.0],
3259            [0.5, 0.0, 0.0],
3260            EXACT,
3261        );
3262        // oracle: a capsule with an end inside: t = 0, point the segment midpoint.
3263        assert_hit(
3264            capsule_cast(
3265                &w,
3266                [0.0, 0.0, 0.0],
3267                [0.0, 4.0, 0.0],
3268                0.5,
3269                [1.0, 0.0, 0.0],
3270                100.0,
3271            ),
3272            RayTarget::Body(b),
3273            0.0,
3274            [-1.0, 0.0, 0.0],
3275            [0.0, 2.0, 0.0],
3276            EXACT,
3277        );
3278    }
3279
3280    #[test]
3281    fn a_cast_that_starts_touching_hits_only_when_it_moves_in() {
3282        let mut w = world();
3283        let b = sphere_body(&mut w, Vec3Fix::ZERO, 1.0);
3284        // The cast sphere of radius 0.5 at (−1.5, 0, 0) touches the body.
3285        let c = [-1.5, 0.0, 0.0];
3286        // oracle: moving in (+X): t = 0, normal −X, contact (−1, 0, 0).
3287        assert_hit(
3288            sphere_cast(&w, c, 0.5, [1.0, 0.0, 0.0], 100.0),
3289            RayTarget::Body(b),
3290            0.0,
3291            [-1.0, 0.0, 0.0],
3292            [-1.0, 0.0, 0.0],
3293            EXACT,
3294        );
3295        // oracle: moving away (−X) or along the tangent plane (+Y): no hit.
3296        assert_eq!(sphere_cast(&w, c, 0.5, [-1.0, 0.0, 0.0], 100.0), None);
3297        assert_eq!(sphere_cast(&w, c, 0.5, [0.0, 1.0, 0.0], 100.0), None);
3298
3299        // A sphere resting on a plane: down is a hit at 0, sideways and up not.
3300        let mut w = world();
3301        let p = plane(&mut w, Vec3Fix::UNIT_Y, 0.0);
3302        let c = [1.0, 0.5, 2.0];
3303        assert_hit(
3304            sphere_cast(&w, c, 0.5, [0.0, -1.0, 0.0], 100.0),
3305            RayTarget::StaticCollider(p),
3306            0.0,
3307            [0.0, 1.0, 0.0],
3308            [1.0, 0.0, 2.0],
3309            EXACT,
3310        );
3311        assert_eq!(sphere_cast(&w, c, 0.5, [1.0, 0.0, 0.0], 100.0), None);
3312        assert_eq!(sphere_cast(&w, c, 0.5, [0.0, 1.0, 0.0], 100.0), None);
3313
3314        // A sphere resting on the unit box top: down hits at 0, sideways not.
3315        let mut w = world();
3316        let bx = unit_box(&mut w);
3317        let c = [0.2, 1.5, 0.3];
3318        assert_hit(
3319            sphere_cast(&w, c, 0.5, [0.0, -1.0, 0.0], 100.0),
3320            RayTarget::Body(bx),
3321            0.0,
3322            [0.0, 1.0, 0.0],
3323            [0.2, 1.0, 0.3],
3324            EXACT,
3325        );
3326        assert_eq!(sphere_cast(&w, c, 0.5, [1.0, 0.0, 0.0], 100.0), None);
3327    }
3328
3329    #[test]
3330    fn sphere_cast_against_box_face_edge_and_turned_box() {
3331        let mut w = world();
3332        let b = unit_box(&mut w);
3333        // oracle: face x = −1 pushed out to x = −1.5: t = 8.5, contact on the face.
3334        assert_hit(
3335            sphere_cast(&w, [-10.0, 0.2, 0.3], 0.5, [1.0, 0.0, 0.0], 100.0),
3336            RayTarget::Body(b),
3337            8.5,
3338            [-1.0, 0.0, 0.0],
3339            [-1.0, 0.2, 0.3],
3340            EXACT,
3341        );
3342        // oracle: edge (x, y) = (−1, 1): the centre (x, 1.3) is 0.5 from it at
3343        // x = −1 − √(0.25 − 0.09) = −1.4, t = 8.6, normal (−0.4, 0.3)/0.5.
3344        assert_hit(
3345            sphere_cast(&w, [-10.0, 1.3, 0.0], 0.5, [1.0, 0.0, 0.0], 100.0),
3346            RayTarget::Body(b),
3347            8.6,
3348            [-0.8, 0.6, 0.0],
3349            [-1.0, 1.0, 0.0],
3350            EXACT,
3351        );
3352        // The same box turned 45° about Y: its vertical edge points at −X, at
3353        // x = −√2. oracle: t = 10 − √2 − 0.5, normal −X, contact (−√2, 0, 0).
3354        let mut w = world();
3355        let b = unit_box(&mut w);
3356        w.bodies[b].rotation = QuatFix::from_axis_angle(Vec3Fix::UNIT_Y, Fix128::HALF_PI.half());
3357        let s2 = 2f64.sqrt();
3358        assert_hit(
3359            sphere_cast(&w, [-10.0, 0.0, 0.0], 0.5, [1.0, 0.0, 0.0], 100.0),
3360            RayTarget::Body(b),
3361            9.5 - s2,
3362            [-1.0, 0.0, 0.0],
3363            [-s2, 0.0, 0.0],
3364            ITER,
3365        );
3366    }
3367
3368    #[test]
3369    fn sphere_cast_against_cylinder_side_cap_and_rim() {
3370        let mut w = world();
3371        let b = shaped(
3372            &mut w,
3373            Shape::Cylinder {
3374                radius: Fix128::ONE,
3375                half_height: Fix128::ONE,
3376            },
3377            Vec3Fix::ZERO,
3378        );
3379        // oracle: side x = −1 pushed out to −1.5: t = 8.5.
3380        assert_hit(
3381            sphere_cast(&w, [-10.0, 0.0, 0.0], 0.5, [1.0, 0.0, 0.0], 100.0),
3382            RayTarget::Body(b),
3383            8.5,
3384            [-1.0, 0.0, 0.0],
3385            [-1.0, 0.0, 0.0],
3386            EXACT,
3387        );
3388        // oracle: top cap y = 1 pushed up to 1.5: t = 8.5.
3389        assert_hit(
3390            sphere_cast(&w, [0.2, 10.0, 0.0], 0.5, [0.0, -1.0, 0.0], 100.0),
3391            RayTarget::Body(b),
3392            8.5,
3393            [0.0, 1.0, 0.0],
3394            [0.2, 1.0, 0.0],
3395            EXACT,
3396        );
3397        // oracle: rim (1, 1, 0): the centre (1.3, y, 0) is 0.5 from it at
3398        // y = 1 + √(0.25 − 0.09) = 1.4, t = 8.6, normal (0.3, 0.4)/0.5.
3399        assert_hit(
3400            sphere_cast(&w, [1.3, 10.0, 0.0], 0.5, [0.0, -1.0, 0.0], 100.0),
3401            RayTarget::Body(b),
3402            8.6,
3403            [0.6, 0.8, 0.0],
3404            [1.0, 1.0, 0.0],
3405            EXACT,
3406        );
3407        // oracle: a horizontal capsule over the cap: t = 5 − 1 − 0.5.
3408        assert_face_hit(
3409            capsule_cast(
3410                &w,
3411                [-0.5, 5.0, 0.0],
3412                [0.5, 5.0, 0.0],
3413                0.5,
3414                [0.0, -1.0, 0.0],
3415                100.0,
3416            ),
3417            RayTarget::Body(b),
3418            3.5,
3419            1.0,
3420            -0.5,
3421            0.5,
3422        );
3423        // Overlaps: 0.2 above the cap is within 0.3, not within 0.1.
3424        assert_eq!(
3425            overlap_s(&w, [0.0, 1.2, 0.0], 0.3),
3426            vec![RayTarget::Body(b)]
3427        );
3428        assert!(overlap_s(&w, [0.0, 1.2, 0.0], 0.1).is_empty());
3429        // Radius 0: a point strictly inside overlaps, one on the surface does not.
3430        assert_eq!(
3431            overlap_s(&w, [0.0, 0.0, 0.0], 0.0),
3432            vec![RayTarget::Body(b)]
3433        );
3434        assert!(overlap_s(&w, [0.0, 1.0, 0.0], 0.0).is_empty());
3435    }
3436
3437    #[test]
3438    fn sphere_and_capsule_casts_against_an_ellipsoid() {
3439        let mut w = world();
3440        let b = shaped(
3441            &mut w,
3442            Shape::Ellipsoid {
3443                radii: v3(1.0, 2.0, 1.0),
3444            },
3445            Vec3Fix::ZERO,
3446        );
3447        // oracle: the top (0, 2, 0) reached at y = 2.5: t = 10 − 2.5.
3448        assert_hit(
3449            sphere_cast(&w, [0.0, 10.0, 0.0], 0.5, [0.0, -1.0, 0.0], 100.0),
3450            RayTarget::Body(b),
3451            7.5,
3452            [0.0, 1.0, 0.0],
3453            [0.0, 2.0, 0.0],
3454            ITER,
3455        );
3456        // oracle: the side (1, 0, 0) reached by a vertical capsule at x = 1.5.
3457        assert_hit(
3458            capsule_cast(
3459                &w,
3460                [-10.0, -0.5, 0.0],
3461                [-10.0, 0.5, 0.0],
3462                0.5,
3463                [1.0, 0.0, 0.0],
3464                100.0,
3465            ),
3466            RayTarget::Body(b),
3467            8.5,
3468            [-1.0, 0.0, 0.0],
3469            [-1.0, 0.0, 0.0],
3470            ITER,
3471        );
3472        // oracle: the line x = 1.6 is 0.6 > 0.5 from the widest point: no contact.
3473        assert_eq!(
3474            sphere_cast(&w, [1.6, 10.0, 0.0], 0.5, [0.0, -1.0, 0.0], 100.0),
3475            None
3476        );
3477        // Overlaps: the side point (1, 0, 0) is 0.2 from (1.2, 0, 0).
3478        assert_eq!(
3479            overlap_s(&w, [1.2, 0.0, 0.0], 0.3),
3480            vec![RayTarget::Body(b)]
3481        );
3482        assert!(overlap_s(&w, [1.2, 0.0, 0.0], 0.1).is_empty());
3483        assert_eq!(
3484            overlap_b(&w, [0.9, -0.1, -0.1], [1.5, 0.1, 0.1]),
3485            vec![RayTarget::Body(b)]
3486        );
3487        assert!(overlap_b(&w, [1.1, -0.1, -0.1], [1.5, 0.1, 0.1]).is_empty());
3488    }
3489
3490    #[test]
3491    fn casts_and_overlaps_against_a_torus() {
3492        let mut w = world();
3493        // Ring of radius 2 in XZ, tube 0.5.
3494        let b = shaped(
3495            &mut w,
3496            Shape::Torus {
3497                major_radius: fx(2.0),
3498                minor_radius: fx(0.5),
3499            },
3500            Vec3Fix::ZERO,
3501        );
3502        // oracle: the top of the tube (2, 0.5, 0) reached at y = 0.75: t = 9.25.
3503        assert_hit(
3504            sphere_cast(&w, [2.0, 10.0, 0.0], 0.25, [0.0, -1.0, 0.0], 100.0),
3505            RayTarget::Body(b),
3506            9.25,
3507            [0.0, 1.0, 0.0],
3508            [2.0, 0.5, 0.0],
3509            EXACT,
3510        );
3511        // oracle: down the hole, 1.5 > 0.25 from the tube: no contact.
3512        assert_eq!(
3513            sphere_cast(&w, [0.0, 10.0, 0.0], 0.25, [0.0, -1.0, 0.0], 100.0),
3514            None
3515        );
3516        // oracle: a capsule across the tube, its point (2, y, 0) nearest the ring:
3517        // y = 0.5 + 0.25, t = 4.25.
3518        assert_hit(
3519            capsule_cast(
3520                &w,
3521                [1.5, 5.0, 0.0],
3522                [2.5, 5.0, 0.0],
3523                0.25,
3524                [0.0, -1.0, 0.0],
3525                100.0,
3526            ),
3527            RayTarget::Body(b),
3528            4.25,
3529            [0.0, 1.0, 0.0],
3530            [2.0, 0.5, 0.0],
3531            ITER,
3532        );
3533        // Overlaps: the axis is 1.5 from the tube.
3534        assert!(overlap_s(&w, [0.0, 0.0, 0.0], 1.4).is_empty());
3535        assert_eq!(
3536            overlap_s(&w, [0.0, 0.0, 0.0], 1.6),
3537            vec![RayTarget::Body(b)]
3538        );
3539        assert_eq!(
3540            overlap_s(&w, [2.0, 0.0, 0.0], 0.1),
3541            vec![RayTarget::Body(b)]
3542        );
3543        // oracle: the ring at 45° (√2, 0, √2) is (√2 − c)·√2 from the box corner
3544        // (c, ·, c): c = 1 gives 2 − √2 = 0.59 > 0.5 (no overlap), c = 1.2 gives
3545        // 0.30 < 0.5 (overlap).
3546        assert!(overlap_b(&w, [-1.0, -1.0, -1.0], [1.0, 1.0, 1.0]).is_empty());
3547        assert_eq!(
3548            overlap_b(&w, [-1.2, -1.0, -1.2], [1.2, 1.0, 1.2]),
3549            vec![RayTarget::Body(b)]
3550        );
3551        // oracle: a box far outside the bounding sphere (radius 2.5).
3552        assert!(overlap_b(&w, [5.0, 5.0, 5.0], [6.0, 6.0, 6.0]).is_empty());
3553    }
3554
3555    #[test]
3556    fn casts_and_overlaps_against_compound_children() {
3557        let mut w = world();
3558        // Box children at x = ±3, sphere children at z = ±3, a capsule child
3559        // along X and cube hull children at y = ±4: symmetric, centre of mass at
3560        // the origin.
3561        let mut c = CompoundShape::new();
3562        for s in [3.0, -3.0] {
3563            c.add_box(
3564                OrientedBox::new(Vec3Fix::ZERO, v3(0.5, 0.5, 0.5), QuatFix::IDENTITY),
3565                v3(s, 0.0, 0.0),
3566                QuatFix::IDENTITY,
3567            );
3568            c.add_sphere(
3569                Sphere::new(Vec3Fix::ZERO, fx(0.5)),
3570                v3(0.0, 0.0, s),
3571                QuatFix::IDENTITY,
3572            );
3573            let mut cube = Vec::new();
3574            for x in [-0.5, 0.5] {
3575                for y in [-0.5, 0.5] {
3576                    for z in [-0.5, 0.5] {
3577                        cube.push(v3(x, y, z));
3578                    }
3579                }
3580            }
3581            c.add_convex_hull(
3582                ConvexHull::new(cube),
3583                v3(0.0, s + s / 3.0, 0.0),
3584                QuatFix::IDENTITY,
3585            );
3586        }
3587        c.add_capsule(
3588            Capsule::new(v3(-1.5, 0.0, 0.0), v3(1.5, 0.0, 0.0), fx(0.5)),
3589            Vec3Fix::ZERO,
3590            QuatFix::IDENTITY,
3591        );
3592        let b = w
3593            .add_compound_body(&c, Fix128::ONE, Vec3Fix::ZERO)
3594            .expect("valid compound");
3595        let body = RayTarget::Body(b);
3596        // oracle: box child top y = 0.5 reached at 0.75: t = 9.25.
3597        assert_hit(
3598            sphere_cast(&w, [3.0, 10.0, 0.0], 0.25, [0.0, -1.0, 0.0], 100.0),
3599            body,
3600            9.25,
3601            [0.0, 1.0, 0.0],
3602            [3.0, 0.5, 0.0],
3603            EXACT,
3604        );
3605        // oracle: sphere child at (0, 0, 3): radius 0.75 above it, t = 9.25.
3606        assert_hit(
3607            sphere_cast(&w, [0.0, 10.0, 3.0], 0.25, [0.0, -1.0, 0.0], 100.0),
3608            body,
3609            9.25,
3610            [0.0, 1.0, 0.0],
3611            [0.0, 0.5, 3.0],
3612            EXACT,
3613        );
3614        // oracle: capsule child side at x = 1, from +Z: 0.75 from the segment,
3615        // t = 10 − 0.75.
3616        assert_hit(
3617            sphere_cast(&w, [1.0, 0.0, 10.0], 0.25, [0.0, 0.0, -1.0], 100.0),
3618            body,
3619            9.25,
3620            [0.0, 0.0, 1.0],
3621            [1.0, 0.0, 0.5],
3622            EXACT,
3623        );
3624        // oracle: hull cube at y = 4 (top 4.5) from the side: its face x = 0.5
3625        // reached at x = 0.75, t = 10 − 0.75.
3626        assert_hit(
3627            sphere_cast(&w, [10.0, 4.0, 0.0], 0.25, [-1.0, 0.0, 0.0], 100.0),
3628            body,
3629            9.25,
3630            [1.0, 0.0, 0.0],
3631            [0.5, 4.0, 0.0],
3632            ITER,
3633        );
3634        // oracle: a capsule over the box child at x = 3: t = 5 − 0.5 − 0.25.
3635        assert_face_hit(
3636            capsule_cast(
3637                &w,
3638                [2.8, 5.0, 0.0],
3639                [3.2, 5.0, 0.0],
3640                0.25,
3641                [0.0, -1.0, 0.0],
3642                100.0,
3643            ),
3644            body,
3645            4.25,
3646            0.5,
3647            2.8,
3648            3.2,
3649        );
3650        // oracle: a capsule crossing over the capsule child (below the hull cube
3651        // at y = 4): t = 2 − 0.75.
3652        assert_hit(
3653            capsule_cast(
3654                &w,
3655                [0.0, 2.0, -1.0],
3656                [0.0, 2.0, 1.0],
3657                0.25,
3658                [0.0, -1.0, 0.0],
3659                100.0,
3660            ),
3661            body,
3662            1.25,
3663            [0.0, 1.0, 0.0],
3664            [0.0, 0.5, 0.0],
3665            ITER,
3666        );
3667        // oracle: a capsule over the sphere child at (0, 0, −3): t = 5 − 0.75.
3668        assert_hit(
3669            capsule_cast(
3670                &w,
3671                [-1.0, 5.0, -3.0],
3672                [1.0, 5.0, -3.0],
3673                0.25,
3674                [0.0, -1.0, 0.0],
3675                100.0,
3676            ),
3677            body,
3678            4.25,
3679            [0.0, 1.0, 0.0],
3680            [0.0, 0.5, -3.0],
3681            EXACT,
3682        );
3683        // oracle: (2.1, 0, 0) is 0.1 from the capsule child (its end cap reaches
3684        // x = 2) and 0.4 from the box child (face x = 2.5): not within 0.05,
3685        // within 0.45.
3686        assert!(overlap_s(&w, [2.1, 0.0, 0.0], 0.05).is_empty());
3687        assert_eq!(overlap_s(&w, [2.1, 0.0, 0.0], 0.45), vec![body]);
3688        // Box overlaps: each child alone, and the empty space between them.
3689        assert_eq!(overlap_b(&w, [3.4, 0.4, -0.1], [3.6, 0.6, 0.1]), vec![body]);
3690        assert_eq!(overlap_b(&w, [-0.1, 0.4, 2.9], [0.1, 0.6, 3.1]), vec![body]);
3691        assert_eq!(overlap_b(&w, [1.0, 0.4, -0.1], [1.2, 0.6, 0.1]), vec![body]);
3692        assert_eq!(overlap_b(&w, [0.4, 4.4, -0.1], [0.6, 4.6, 0.1]), vec![body]);
3693        assert!(overlap_b(&w, [1.0, 2.0, 1.0], [2.0, 3.0, 2.0]).is_empty());
3694    }
3695
3696    #[test]
3697    fn casts_and_overlaps_against_a_plane() {
3698        let mut w = world();
3699        let p = plane(&mut w, Vec3Fix::UNIT_Y, 0.0);
3700        let sp = RayTarget::StaticCollider(p);
3701        // oracle: oblique (1, −1, 0)/√2: the height drops 4.5 after 4.5·√2.
3702        assert_hit(
3703            sphere_cast(&w, [0.0, 5.0, 0.0], 0.5, [1.0, -1.0, 0.0], 100.0),
3704            sp,
3705            4.5 * 2f64.sqrt(),
3706            [0.0, 1.0, 0.0],
3707            [4.5, 0.0, 0.0],
3708            EXACT,
3709        );
3710        // oracle: two-sided: from below the centre reaches y = −0.5.
3711        assert_hit(
3712            sphere_cast(&w, [0.0, -5.0, 0.0], 0.5, [0.0, 1.0, 0.0], 100.0),
3713            sp,
3714            4.5,
3715            [0.0, -1.0, 0.0],
3716            [0.0, 0.0, 0.0],
3717            EXACT,
3718        );
3719        // oracle: parallel 1 above: never closer than 1 > 0.5.
3720        assert_eq!(
3721            sphere_cast(&w, [0.0, 1.0, 0.0], 0.5, [1.0, 0.0, 0.0], 100.0),
3722            None
3723        );
3724        // oracle: capsule, the lower end (−1, 5, 0) reaches y = 0.5: t = 4.5.
3725        assert_hit(
3726            capsule_cast(
3727                &w,
3728                [-1.0, 5.0, 0.0],
3729                [1.0, 6.0, 0.0],
3730                0.5,
3731                [0.0, -1.0, 0.0],
3732                100.0,
3733            ),
3734            sp,
3735            4.5,
3736            [0.0, 1.0, 0.0],
3737            [-1.0, 0.0, 0.0],
3738            EXACT,
3739        );
3740        // oracle: a capsule whose segment crosses the plane starts overlapping.
3741        assert_hit(
3742            capsule_cast(
3743                &w,
3744                [0.0, -1.0, 0.0],
3745                [0.0, 1.0, 0.0],
3746                0.5,
3747                [1.0, 0.0, 0.0],
3748                100.0,
3749            ),
3750            sp,
3751            0.0,
3752            [-1.0, 0.0, 0.0],
3753            [0.0, 0.0, 0.0],
3754            EXACT,
3755        );
3756        // Overlaps: 0.4 above is within 0.5; 0.6 is not.
3757        assert_eq!(overlap_s(&w, [0.0, 0.4, 0.0], 0.5), vec![sp]);
3758        assert!(overlap_s(&w, [0.0, 0.6, 0.0], 0.5).is_empty());
3759        // oracle: a box straddling y = 0 overlaps, one resting on it does not.
3760        assert_eq!(overlap_b(&w, [0.0, -0.1, 0.0], [1.0, 0.1, 1.0]), vec![sp]);
3761        assert!(overlap_b(&w, [0.0, 0.0, 0.0], [1.0, 1.0, 1.0]).is_empty());
3762    }
3763
3764    #[test]
3765    fn casts_along_a_mesh_floor_cross_its_edges() {
3766        let mut w = world();
3767        let m = floor_mesh(&mut w);
3768        let sm = RayTarget::StaticCollider(m);
3769        // oracle: face region: the plane y = 0 reached at y = 0.5: t = 2.5.
3770        assert_hit(
3771            sphere_cast(&w, [1.0, 3.0, 3.0], 0.5, [0.0, -1.0, 0.0], 100.0),
3772            sm,
3773            2.5,
3774            [0.0, 1.0, 0.0],
3775            [1.0, 0.0, 3.0],
3776            EXACT,
3777        );
3778        // oracle: edge x = 0: the centre (x, 0.3, 1) is 0.5 from (0, 0, 1) at
3779        // x = −0.4, t = 9.6, normal (−0.8, 0.6, 0).
3780        assert_hit(
3781            sphere_cast(&w, [-10.0, 0.3, 1.0], 0.5, [1.0, 0.0, 0.0], 100.0),
3782            sm,
3783            9.6,
3784            [-0.8, 0.6, 0.0],
3785            [0.0, 0.0, 1.0],
3786            EXACT,
3787        );
3788        // oracle: a sphere resting on the floor sliding across the diagonal
3789        // edge, and a capsule doing the same: tangent, no hit.
3790        assert_eq!(
3791            sphere_cast(&w, [1.0, 0.5, 3.0], 0.5, [1.0, 0.0, -1.0], 2.0),
3792            None
3793        );
3794        assert_eq!(
3795            capsule_cast(
3796                &w,
3797                [1.0, 0.5, 2.0],
3798                [1.0, 1.5, 2.0],
3799                0.5,
3800                [1.0, 0.0, 0.0],
3801                2.0
3802            ),
3803            None
3804        );
3805        // oracle: resting and moving down: t = 0 on the floor.
3806        assert_hit(
3807            sphere_cast(&w, [1.0, 0.5, 3.0], 0.5, [0.0, -1.0, 0.0], 100.0),
3808            sm,
3809            0.0,
3810            [0.0, 1.0, 0.0],
3811            [1.0, 0.0, 3.0],
3812            EXACT,
3813        );
3814        // oracle: tilted capsule, lower end (2, 3, 1) reaches y = 0.5: t = 2.5.
3815        assert_hit(
3816            capsule_cast(
3817                &w,
3818                [2.0, 3.0, 1.0],
3819                [3.0, 4.0, 1.0],
3820                0.5,
3821                [0.0, -1.0, 0.0],
3822                100.0,
3823            ),
3824            sm,
3825            2.5,
3826            [0.0, 1.0, 0.0],
3827            [2.0, 0.0, 1.0],
3828            ITER,
3829        );
3830        // Overlaps: 0.3 above the floor is within 0.5, outside it 0.6 is not.
3831        assert_eq!(overlap_s(&w, [2.0, 0.3, 2.0], 0.5), vec![sm]);
3832        assert!(overlap_s(&w, [-0.6, 0.0, 2.0], 0.5).is_empty());
3833        assert_eq!(overlap_b(&w, [1.0, -0.1, 1.0], [2.0, 0.1, 2.0]), vec![sm]);
3834        assert!(overlap_b(&w, [1.0, 0.1, 1.0], [2.0, 1.0, 2.0]).is_empty());
3835    }
3836
3837    #[test]
3838    fn casts_and_overlaps_against_height_fields() {
3839        // oracle: flat field at 0.25 over [0, 4]²: t = 5 − 0.25 − 0.5.
3840        let mut w = world();
3841        let h = w.add_static_collider(StaticCollider::HeightField(HeightField::flat(
3842            5,
3843            5,
3844            Fix128::ONE,
3845            Vec3Fix::ZERO,
3846            fx(0.25),
3847        )));
3848        let sh = RayTarget::StaticCollider(h);
3849        assert_hit(
3850            sphere_cast(&w, [2.0, 5.0, 2.0], 0.5, [0.0, -1.0, 0.0], 100.0),
3851            sh,
3852            4.25,
3853            [0.0, 1.0, 0.0],
3854            [2.0, 0.25, 2.0],
3855            ITER,
3856        );
3857        // oracle: a horizontal capsule over it: the same t.
3858        assert_face_hit(
3859            capsule_cast(
3860                &w,
3861                [1.5, 5.0, 2.0],
3862                [2.5, 5.0, 2.0],
3863                0.5,
3864                [0.0, -1.0, 0.0],
3865                100.0,
3866            ),
3867            sh,
3868            4.25,
3869            0.25,
3870            1.5,
3871            2.5,
3872        );
3873        // oracle: resting on it and sliding across cells: no hit.
3874        assert_eq!(
3875            sphere_cast(&w, [0.5, 0.75, 0.5], 0.5, [1.0, 0.0, 0.0], 3.0),
3876            None
3877        );
3878        // oracle: outside the field's footprint: no hit.
3879        assert_eq!(
3880            sphere_cast(&w, [10.0, 5.0, 10.0], 0.5, [0.0, -1.0, 0.0], 100.0),
3881            None
3882        );
3883        assert_eq!(overlap_s(&w, [2.0, 0.5, 2.0], 0.3), vec![sh]);
3884        assert!(overlap_s(&w, [2.0, 0.6, 2.0], 0.3).is_empty());
3885        assert_eq!(overlap_b(&w, [1.0, 0.2, 1.0], [3.0, 0.3, 3.0]), vec![sh]);
3886        assert!(overlap_b(&w, [1.0, 0.3, 1.0], [3.0, 1.0, 3.0]).is_empty());
3887        assert!(overlap_b(&w, [5.0, 0.0, 5.0], [6.0, 1.0, 6.0]).is_empty());
3888    }
3889
3890    #[test]
3891    fn casts_and_overlaps_against_a_twisted_height_field_cell() {
3892        let mut w = world();
3893        let h = saddle_field(&mut w);
3894        let sh = RayTarget::StaticCollider(h);
3895        // The surface y = x·z at (0.5, 0.25, 0.5) has the unit normal
3896        // n = (−0.5, 1, −0.5)/√1.5. A sphere of radius 0.1 (its curvature 10
3897        // beats the surface's ±1, so the contact is unique) whose centre moves
3898        // down the line through (0.5, 0.25, 0.5) + 0.1·n touches there.
3899        let k = 1.5f64.sqrt();
3900        let n = [-0.5 / k, 1.0 / k, -0.5 / k];
3901        let r = 0.1;
3902        let cx = 0.5 + r * n[0];
3903        // oracle: t = 5 − (0.25 + r·n.y).
3904        assert_hit(
3905            sphere_cast(&w, [cx, 5.0, cx], r, [0.0, -1.0, 0.0], 100.0),
3906            sh,
3907            5.0 - (0.25 + r * n[1]),
3908            n,
3909            [0.5, 0.25, 0.5],
3910            ITER,
3911        );
3912        // oracle: a point 0.09 along n from the surface point is within 0.1 of
3913        // the surface, one 0.11 along n is not.
3914        let at = |s: f64| [0.5 + s * n[0], 0.25 + s * n[1], 0.5 + s * n[2]];
3915        assert_eq!(overlap_s(&w, at(0.09), 0.1), vec![sh]);
3916        assert!(overlap_s(&w, at(0.11), 0.1).is_empty());
3917        // oracle: the surface spans y ∈ [0, 1]: a box at y ∈ [0.9, 2] over the
3918        // corner (1, 1) meets it (height 1 there), one over (0, 0) does not.
3919        assert_eq!(overlap_b(&w, [0.9, 0.9, 0.9], [1.0, 2.0, 1.0]), vec![sh]);
3920        assert!(overlap_b(&w, [0.0, 0.9, 0.0], [0.1, 2.0, 0.1]).is_empty());
3921    }
3922
3923    #[cfg(feature = "std")]
3924    #[test]
3925    fn casts_and_overlaps_against_an_sdf() {
3926        use crate::sdf_collider::{ClosureSdf, SdfCollider};
3927        let mut w = world();
3928        // The unit-sphere field about the origin.
3929        w.sdf_colliders.push(SdfCollider::new_static(
3930            Box::new(ClosureSdf::new(
3931                |x, y, z| (x * x + y * y + z * z).sqrt() - 1.0,
3932                |x, y, z| {
3933                    let l = (x * x + y * y + z * z).sqrt();
3934                    (x / l, y / l, z / l)
3935                },
3936            )),
3937            Vec3Fix::ZERO,
3938            QuatFix::IDENTITY,
3939        ));
3940        let tol = f64::from(all().sdf.tolerance);
3941        // oracle: the centre reaches |c| = 1.5: t ∈ [3.5 − tol, 3.5].
3942        let h = sphere_cast(&w, [-5.0, 0.0, 0.0], 0.5, [1.0, 0.0, 0.0], 100.0).expect("hit");
3943        assert_eq!(h.target, RayTarget::Sdf(0));
3944        assert!(h.t.to_f64() > 3.5 - tol - 1e-5 && h.t.to_f64() < 3.5 + 1e-5);
3945        assert_vec(h.normal, [-1.0, 0.0, 0.0], 1e-3, "normal");
3946        // oracle: a vertical capsule at the same height: the same t.
3947        let h = capsule_cast(
3948            &w,
3949            [-5.0, -0.5, 0.0],
3950            [-5.0, 0.5, 0.0],
3951            0.5,
3952            [1.0, 0.0, 0.0],
3953            100.0,
3954        )
3955        .expect("hit");
3956        assert!(h.t.to_f64() > 3.5 - tol - 1e-4 && h.t.to_f64() < 3.5 + 1e-4);
3957        // oracle: the line y = 2 stays 2 − 1 = 1 > 0.5 from the surface.
3958        assert_eq!(
3959            sphere_cast(&w, [-5.0, 2.0, 0.0], 0.5, [1.0, 0.0, 0.0], 100.0),
3960            None
3961        );
3962        // Overlaps: (1.2, 0, 0) is 0.2 from the surface.
3963        assert_eq!(overlap_s(&w, [1.2, 0.0, 0.0], 0.3), vec![RayTarget::Sdf(0)]);
3964        assert!(overlap_s(&w, [1.2, 0.0, 0.0], 0.1).is_empty());
3965        assert_eq!(
3966            overlap_b(&w, [0.8, -0.1, -0.1], [1.2, 0.1, 0.1]),
3967            vec![RayTarget::Sdf(0)]
3968        );
3969        assert!(overlap_b(&w, [2.0, 2.0, 2.0], [3.0, 3.0, 3.0]).is_empty());
3970        // The filter can hide it.
3971        assert!(w
3972            .overlap_sphere(v3(1.2, 0.0, 0.0), fx(0.3), &all().with_sdf(false))
3973            .is_empty());
3974    }
3975
3976    #[test]
3977    fn capsule_cast_against_a_sphere_body_and_a_box() {
3978        let mut w = world();
3979        let b = sphere_body(&mut w, Vec3Fix::ZERO, 1.0);
3980        // oracle: the segment (1..3, y, 0): its end (1, y, 0) reaches 1.5 from
3981        // the centre at y = √1.25: t = 5 − √1.25, normal (1, √1.25, 0)/1.5.
3982        let s = 1.25f64.sqrt();
3983        let n = [1.0 / 1.5, s / 1.5, 0.0];
3984        assert_hit(
3985            capsule_cast(
3986                &w,
3987                [1.0, 5.0, 0.0],
3988                [3.0, 5.0, 0.0],
3989                0.5,
3990                [0.0, -1.0, 0.0],
3991                100.0,
3992            ),
3993            RayTarget::Body(b),
3994            5.0 - s,
3995            n,
3996            n,
3997            EXACT,
3998        );
3999        // oracle: coinciding ends are the sphere cast: t = 8.5.
4000        assert_hit(
4001            capsule_cast(
4002                &w,
4003                [-10.0, 0.0, 0.0],
4004                [-10.0, 0.0, 0.0],
4005                0.5,
4006                [1.0, 0.0, 0.0],
4007                100.0,
4008            ),
4009            RayTarget::Body(b),
4010            8.5,
4011            [-1.0, 0.0, 0.0],
4012            [-1.0, 0.0, 0.0],
4013            EXACT,
4014        );
4015        let mut w = world();
4016        let b = unit_box(&mut w);
4017        // oracle: horizontal capsule over the top face y = 1: t = 5 − 1 − 0.5.
4018        assert_face_hit(
4019            capsule_cast(
4020                &w,
4021                [-0.5, 5.0, 0.0],
4022                [0.5, 5.0, 0.0],
4023                0.5,
4024                [0.0, -1.0, 0.0],
4025                100.0,
4026            ),
4027            RayTarget::Body(b),
4028            3.5,
4029            1.0,
4030            -0.5,
4031            0.5,
4032        );
4033        // oracle: moving away from the box: no hit.
4034        assert_eq!(
4035            capsule_cast(
4036                &w,
4037                [-0.5, 5.0, 0.0],
4038                [0.5, 5.0, 0.0],
4039                0.5,
4040                [0.0, 1.0, 0.0],
4041                100.0
4042            ),
4043            None
4044        );
4045    }
4046
4047    #[test]
4048    fn overlaps_of_bodies_and_the_box_edge_gap() {
4049        let mut w = world();
4050        let s = sphere_body(&mut w, Vec3Fix::ZERO, 1.0);
4051        // oracle: (1.4, 0, 0) is 0.4 from the sphere: within 0.5, not 0.3.
4052        assert_eq!(
4053            overlap_s(&w, [1.4, 0.0, 0.0], 0.5),
4054            vec![RayTarget::Body(s)]
4055        );
4056        assert!(overlap_s(&w, [1.4, 0.0, 0.0], 0.3).is_empty());
4057        // A negative radius overlaps nothing.
4058        assert!(overlap_s(&w, [0.0, 0.0, 0.0], -1.0).is_empty());
4059        // oracle: the box [0.5, 2]³'s nearest point (0.5, 0.5, 0.5) is √0.75 =
4060        // 0.866 from the centre: inside radius 1.
4061        assert_eq!(
4062            overlap_b(&w, [0.5, 0.5, 0.5], [2.0, 2.0, 2.0]),
4063            vec![RayTarget::Body(s)]
4064        );
4065        // oracle: the box [0.6, 2]³: √1.08 = 1.04 > 1, no overlap.
4066        assert!(overlap_b(&w, [0.6, 0.6, 0.6], [2.0, 2.0, 2.0]).is_empty());
4067        // A box with min > max contains nothing.
4068        assert!(overlap_b(&w, [1.0, -1.0, -1.0], [-1.0, 1.0, 1.0]).is_empty());
4069
4070        let mut w = world();
4071        let b = unit_box(&mut w);
4072        // oracle: (1.3, 1.3, 0) is 0.3·√2 = 0.424 from the edge (1, 1, z): within
4073        // 0.5, not 0.4.
4074        assert_eq!(
4075            overlap_s(&w, [1.3, 1.3, 0.0], 0.5),
4076            vec![RayTarget::Body(b)]
4077        );
4078        assert!(overlap_s(&w, [1.3, 1.3, 0.0], 0.4).is_empty());
4079        // The box turned 45° about Y reaches x = √2 at z = 0: an AABB from
4080        // x = 1.3 meets it, one from x = 1.5 does not.
4081        w.bodies[b].rotation = QuatFix::from_axis_angle(Vec3Fix::UNIT_Y, Fix128::HALF_PI.half());
4082        assert_eq!(
4083            overlap_b(&w, [1.3, -0.5, -0.1], [2.0, 0.5, 0.1]),
4084            vec![RayTarget::Body(b)]
4085        );
4086        assert!(overlap_b(&w, [1.5, -0.5, -0.1], [2.0, 0.5, 0.1]).is_empty());
4087    }
4088
4089    #[test]
4090    fn filters_and_ties() {
4091        let mut w = world();
4092        let b = sphere_body(&mut w, Vec3Fix::ZERO, 1.0);
4093        let p = plane(&mut w, Vec3Fix::UNIT_Y, -5.0);
4094        // oracle: down from (0, 10, 0): the sphere first (t = 10 − 1.5), the
4095        // plane y = −5 at t = 14.5 when the body is excluded, nothing when the
4096        // static colliders are hidden too.
4097        let c = v3(0.0, 10.0, 0.0);
4098        let down = -Vec3Fix::UNIT_Y;
4099        let h = w
4100            .cast_sphere(c, fx(0.5), down, fx(100.0), &all())
4101            .expect("hit");
4102        assert_eq!(h.target, RayTarget::Body(b));
4103        let h = w
4104            .cast_sphere(c, fx(0.5), down, fx(100.0), &all().excluding_body(b))
4105            .expect("hit");
4106        assert_eq!(h.target, RayTarget::StaticCollider(p));
4107        assert!((h.t.to_f64() - 14.5).abs() < EXACT);
4108        assert_eq!(
4109            w.cast_sphere(
4110                c,
4111                fx(0.5),
4112                down,
4113                fx(100.0),
4114                &all().excluding_body(b).with_static(false)
4115            ),
4116            None
4117        );
4118        // Two identical bodies: the tie goes to the lower index; overlaps are
4119        // sorted by target.
4120        let mut w = world();
4121        let b0 = sphere_body(&mut w, Vec3Fix::ZERO, 1.0);
4122        let b1 = sphere_body(&mut w, Vec3Fix::ZERO, 1.0);
4123        let h = sphere_cast(&w, [-10.0, 0.0, 0.0], 0.5, [1.0, 0.0, 0.0], 100.0).expect("hit");
4124        assert_eq!(h.target, RayTarget::Body(b0));
4125        assert_eq!(
4126            overlap_s(&w, [0.0, 0.0, 0.0], 0.1),
4127            vec![RayTarget::Body(b0), RayTarget::Body(b1)]
4128        );
4129    }
4130
4131    #[test]
4132    fn capsule_penetrations_closed_forms() {
4133        let mut w = world();
4134        let p = plane(&mut w, Vec3Fix::UNIT_Y, 0.0);
4135        // oracle: the segment's lower end is 0.2 above the plane: depth
4136        // 0.3 − 0.2 = 0.1 along +Y.
4137        let pens = w
4138            .capsule_penetrations(v3(0.0, 0.2, 0.0), v3(0.0, 1.4, 0.0), fx(0.3), &all())
4139            .expect("the segment is outside");
4140        assert_eq!(pens.len(), 1);
4141        assert_eq!(pens[0].target, RayTarget::StaticCollider(p));
4142        assert!((pens[0].depth.to_f64() - 0.1).abs() < EXACT);
4143        assert_vec(pens[0].normal, [0.0, 1.0, 0.0], EXACT, "normal");
4144        // oracle: 0.4 above: no overlap.
4145        assert!(w
4146            .capsule_penetrations(v3(0.0, 0.4, 0.0), v3(0.0, 1.4, 0.0), fx(0.3), &all())
4147            .expect("outside")
4148            .is_empty());
4149        // oracle: a segment crossing the plane has no push-out direction.
4150        assert!(w
4151            .capsule_penetrations(v3(0.0, -0.2, 0.0), v3(0.0, 1.4, 0.0), fx(0.3), &all())
4152            .is_none());
4153    }
4154
4155    #[test]
4156    fn helper_closed_forms() {
4157        // aabb_gap: x gap 1, z gap 2, y overlapping: √5.
4158        let a = AABB::new(v3(0.0, 0.0, 0.0), v3(1.0, 1.0, 1.0));
4159        let b = AABB::new(v3(2.0, 0.5, 3.0), v3(3.0, 2.0, 4.0));
4160        assert!((aabb_gap(&a, &b).to_f64() - 5f64.sqrt()).abs() < EXACT);
4161        assert_eq!(aabb_gap(&a, &a), Fix128::ZERO);
4162        // box_entry: [0, 1]³ moving +X meets [3, 4] × [0, 1]² at t = 2; not
4163        // within max_t = 1; never when the Y ranges are apart.
4164        let target = AABB::new(v3(3.0, 0.0, 0.0), v3(4.0, 1.0, 1.0));
4165        let entry = box_entry(&a, Vec3Fix::UNIT_X, fx(10.0), &target).expect("meets");
4166        assert!((entry.to_f64() - 2.0).abs() < EXACT);
4167        assert_eq!(box_entry(&a, Vec3Fix::UNIT_X, Fix128::ONE, &target), None);
4168        let above = AABB::new(v3(3.0, 5.0, 0.0), v3(4.0, 6.0, 1.0));
4169        assert_eq!(box_entry(&a, Vec3Fix::UNIT_X, fx(10.0), &above), None);
4170        // oracle: moving −X toward [−4, −3]: t = 3.
4171        let behind = AABB::new(v3(-4.0, 0.0, 0.0), v3(-3.0, 1.0, 1.0));
4172        let entry = box_entry(&a, -Vec3Fix::UNIT_X, fx(10.0), &behind).expect("meets");
4173        assert!((entry.to_f64() - 3.0).abs() < EXACT);
4174        // ratio_within: 1/4 = 0.25; a tiny denominator is clamped to ±big.
4175        let tiny = Fix128 { hi: 0, lo: 1 };
4176        assert!((ratio_within(Fix128::ONE, fx(4.0), fx(5.0)).to_f64() - 0.25).abs() < EXACT);
4177        assert_eq!(ratio_within(Fix128::ONE, tiny, fx(5.0)), fx(5.0));
4178        assert_eq!(ratio_within(Fix128::NEG_ONE, tiny, fx(5.0)), fx(-5.0));
4179        // tame_direction: (2²¹, 0, 0) is scaled to (1, 0, 0); (3, 4, 0) is kept.
4180        assert_eq!(
4181            tame_direction(Vec3Fix::new(
4182                Fix128::from_int(1 << 21),
4183                Fix128::ZERO,
4184                Fix128::ZERO
4185            )),
4186            Vec3Fix::UNIT_X
4187        );
4188        assert_eq!(tame_direction(v3(3.0, 4.0, 0.0)), v3(3.0, 4.0, 0.0));
4189        assert!(box_is_valid(&a));
4190        assert!(!box_is_valid(&AABB::new(
4191            v3(0.0, 1.0, 0.0),
4192            v3(1.0, 0.0, 1.0)
4193        )));
4194        // closest_on_segment: clamped to the ends, degenerate segment.
4195        let (s0, s1) = (v3(0.0, 0.0, 0.0), v3(2.0, 0.0, 0.0));
4196        assert_eq!(
4197            closest_on_segment(s0, s1, v3(1.0, 5.0, 0.0)),
4198            v3(1.0, 0.0, 0.0)
4199        );
4200        assert_eq!(closest_on_segment(s0, s1, v3(-3.0, 1.0, 0.0)), s0);
4201        assert_eq!(closest_on_segment(s0, s0, v3(5.0, 5.0, 5.0)), s0);
4202        // segment_segment: crossing → None; skew at distance 1; point cases.
4203        assert!(segment_segment(
4204            v3(-1.0, 0.0, 0.0),
4205            v3(1.0, 0.0, 0.0),
4206            v3(0.0, -1.0, 0.0),
4207            v3(0.0, 1.0, 0.0)
4208        )
4209        .is_none());
4210        let (d, _, _) = segment_segment(
4211            v3(-1.0, 0.0, 0.0),
4212            v3(1.0, 0.0, 0.0),
4213            v3(0.0, -1.0, 1.0),
4214            v3(0.0, 1.0, 1.0),
4215        )
4216        .expect("apart");
4217        assert!((d.to_f64() - 1.0).abs() < ITER);
4218        let (d, _, _) =
4219            segment_segment(s0, s0, v3(3.0, 4.0, 0.0), v3(3.0, 4.0, 0.0)).expect("apart");
4220        assert!((d.to_f64() - 5.0).abs() < EXACT);
4221        let (d, _, _) =
4222            segment_segment(v3(1.0, 3.0, 0.0), v3(1.0, 3.0, 0.0), s0, s1).expect("apart");
4223        assert!((d.to_f64() - 3.0).abs() < EXACT);
4224        let (d, _, _) =
4225            segment_segment(s0, s1, v3(1.0, 0.0, 2.0), v3(1.0, 0.0, 2.0)).expect("apart");
4226        assert!((d.to_f64() - 2.0).abs() < EXACT);
4227        // point_box_local: on the face x = 1 it touches with normal +X; outside
4228        // it is the distance to the clamped point; inside it is Inside.
4229        let h = v3(1.0, 1.0, 1.0);
4230        match point_box_local(v3(1.0, 0.5, 0.5), h) {
4231            Dist::Outside { dist, normal, .. } => {
4232                assert_eq!(dist, Fix128::ZERO);
4233                assert_eq!(normal, Vec3Fix::UNIT_X);
4234            }
4235            other => panic!("expected touching, got {other:?}"),
4236        }
4237        assert!((point_box_local(v3(4.0, 5.0, 0.0), h).value().to_f64() - 5.0).abs() < EXACT);
4238        assert!(matches!(point_box_local(Vec3Fix::ZERO, h), Dist::Inside));
4239        // point_cylinder_local: on the cap; 2 beside the side.
4240        match point_cylinder_local(v3(0.0, -1.0, 0.0), Fix128::ONE, Fix128::ONE) {
4241            Dist::Outside { dist, normal, .. } => {
4242                assert_eq!(dist, Fix128::ZERO);
4243                assert_eq!(normal, -Vec3Fix::UNIT_Y);
4244            }
4245            other => panic!("expected touching, got {other:?}"),
4246        }
4247        assert!(
4248            (point_cylinder_local(v3(3.0, 0.0, 0.0), Fix128::ONE, Fix128::ONE)
4249                .value()
4250                .to_f64()
4251                - 2.0)
4252                .abs()
4253                < EXACT
4254        );
4255        // Dist: a distance shrunk past zero is Inside; a bound shrinks by it.
4256        assert!(matches!(
4257            Dist::toward(v3(2.0, 0.0, 0.0), Vec3Fix::ZERO).shrunk(fx(3.0)),
4258            Dist::Inside
4259        ));
4260        assert_eq!(
4261            Dist::AtLeast(fx(2.0)).shrunk(Fix128::ONE).value(),
4262            Fix128::ONE
4263        );
4264        assert!(!Dist::AtLeast(Fix128::ZERO).within(fx(10.0)));
4265        assert!(Dist::Inside.within(Fix128::ZERO));
4266        assert_eq!(Dist::Inside.value(), Fix128::NEG_ONE);
4267        assert!(matches!(
4268            Dist::toward(Vec3Fix::ZERO, Vec3Fix::ZERO),
4269            Dist::Inside
4270        ));
4271    }
4272}