Skip to main content

alice_physics/
ccd.rs

1//! Continuous Collision Detection (CCD)
2//!
3//! Prevents fast-moving objects from tunneling through thin geometry.
4//!
5//! # Algorithms
6//!
7//! - **Conservative Advancement**: Iteratively advance along trajectory
8//! - **Sphere-Sphere TOI**: Exact time of impact for two moving spheres
9//! - **Sphere-Plane TOI**: Exact time of impact for sphere vs infinite plane
10
11use crate::collider::AABB;
12use crate::math::{Fix128, Vec3Fix};
13
14/// Time of Impact result
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub struct TOI {
17    /// Time of first impact [0, 1], where 0=start, 1=end of timestep
18    pub t: Fix128,
19    /// Contact point at time of impact (world space)
20    pub point: Vec3Fix,
21    /// Contact normal at time of impact
22    pub normal: Vec3Fix,
23}
24
25/// CCD configuration
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub struct CcdConfig {
28    /// Maximum iterations for conservative advancement
29    pub max_iterations: usize,
30    /// Convergence threshold (stop when remaining gap < this)
31    pub tolerance: Fix128,
32    /// Minimum velocity magnitude to trigger CCD
33    pub velocity_threshold: Fix128,
34}
35
36impl Default for CcdConfig {
37    fn default() -> Self {
38        Self {
39            max_iterations: 32,
40            tolerance: Fix128::from_ratio(1, 1000),
41            velocity_threshold: Fix128::from_int(5),
42        }
43    }
44}
45
46/// Check if a body needs CCD based on its velocity
47#[inline]
48#[must_use]
49pub fn needs_ccd(velocity: Vec3Fix, radius: Fix128, dt: Fix128, config: &CcdConfig) -> bool {
50    let speed = velocity.length();
51    // CCD needed if displacement > half the body's radius
52    speed * dt > radius.half() && speed > config.velocity_threshold
53}
54
55/// Exact TOI for two moving spheres
56///
57/// Sphere A moves from `center_a` by `vel_a * t`
58/// Sphere B moves from `center_b` by `vel_b * t`
59/// Returns t in [0, 1] or None if no collision in this timestep.
60#[must_use]
61pub fn sphere_sphere_toi(
62    center_a: Vec3Fix,
63    radius_a: Fix128,
64    vel_a: Vec3Fix,
65    center_b: Vec3Fix,
66    radius_b: Fix128,
67    vel_b: Vec3Fix,
68) -> Option<TOI> {
69    let rel_pos = center_b - center_a;
70    let rel_vel = vel_b - vel_a;
71    let combined_r = radius_a + radius_b;
72
73    // Quadratic: |rel_pos + rel_vel * t|^2 = combined_r^2
74    let a = rel_vel.dot(rel_vel);
75    let b = rel_pos.dot(rel_vel).double();
76    let c = rel_pos.dot(rel_pos) - combined_r * combined_r;
77
78    // Already overlapping
79    if c <= Fix128::ZERO {
80        let normal = rel_pos.normalize();
81        return Some(TOI {
82            t: Fix128::ZERO,
83            point: center_a + normal * radius_a,
84            normal,
85        });
86    }
87
88    // No relative motion
89    if a.is_zero() {
90        return None;
91    }
92
93    let discriminant = b * b - Fix128::from_int(4) * a * c;
94    if discriminant < Fix128::ZERO {
95        return None;
96    }
97
98    let sqrt_d = discriminant.sqrt();
99    let two_a = a.double();
100    let t = (-b - sqrt_d) / two_a;
101
102    if t >= Fix128::ZERO && t <= Fix128::ONE {
103        let pos_a = center_a + vel_a * t;
104        let pos_b = center_b + vel_b * t;
105        let normal = (pos_b - pos_a).normalize();
106        let point = pos_a + normal * radius_a;
107        Some(TOI { t, point, normal })
108    } else {
109        None
110    }
111}
112
113/// Exact TOI for a moving sphere against a static plane
114///
115/// Plane: dot(normal, p) = offset. Two-sided: a sphere on the back side
116/// (`dist < −r`) moving towards the plane hits it at
117/// `t = (−r − dist) / v·n`, and the returned normal points towards the
118/// sphere's side (`−n` from behind). Before 1.2.0 the back side used the
119/// front-side formula and reported the *exit* time (when the far surface
120/// crossed the plane) with the front normal.
121#[must_use]
122pub fn sphere_plane_toi(
123    center: Vec3Fix,
124    radius: Fix128,
125    velocity: Vec3Fix,
126    plane_normal: Vec3Fix,
127    plane_offset: Fix128,
128) -> Option<TOI> {
129    let dist = center.dot(plane_normal) - plane_offset;
130    let vel_toward = velocity.dot(plane_normal);
131
132    // Moving away from plane
133    if vel_toward >= Fix128::ZERO && dist > radius {
134        return None;
135    }
136
137    // +1 in front of the plane, −1 behind: the contact normal and the
138    // touching surface point are on the sphere's side
139    let side = if dist.is_negative() {
140        -Fix128::ONE
141    } else {
142        Fix128::ONE
143    };
144
145    // Already penetrating
146    if dist.abs() <= radius {
147        let point = center - plane_normal * dist;
148        return Some(TOI {
149            t: Fix128::ZERO,
150            point,
151            normal: plane_normal * side,
152        });
153    }
154
155    if vel_toward.is_zero() {
156        return None;
157    }
158
159    // t when the near surface of the sphere touches the plane
160    let t = (side * radius - dist) / vel_toward;
161
162    if t >= Fix128::ZERO && t <= Fix128::ONE {
163        let point = center + velocity * t - plane_normal * (side * radius);
164        Some(TOI {
165            t,
166            point,
167            normal: plane_normal * side,
168        })
169    } else {
170        None
171    }
172}
173
174/// Conservative advancement for convex shapes
175///
176/// Iteratively advances along trajectory, using distance function
177/// to determine safe step sizes.
178pub fn conservative_advancement<F>(
179    start_pos: Vec3Fix,
180    displacement: Vec3Fix,
181    radius: Fix128,
182    distance_fn: F,
183    config: &CcdConfig,
184) -> Option<TOI>
185where
186    F: Fn(Vec3Fix) -> (Fix128, Vec3Fix), // (signed_distance, normal)
187{
188    let mut t = Fix128::ZERO;
189
190    for _ in 0..config.max_iterations {
191        let pos = start_pos + displacement * t;
192        let (dist, normal) = distance_fn(pos);
193
194        // Account for sphere radius
195        let gap = dist - radius;
196
197        if gap <= config.tolerance {
198            let point = pos - normal * dist;
199            return Some(TOI { t, point, normal });
200        }
201
202        // Safe advance: we can move at most `gap` along the trajectory
203        let speed = displacement.length();
204        if speed.is_zero() {
205            return None;
206        }
207
208        let dt = gap / speed;
209        t = t + dt;
210
211        if t > Fix128::ONE {
212            return None; // No collision this timestep
213        }
214    }
215
216    None
217}
218
219/// Swept AABB test (moving AABB vs static AABB)
220///
221/// Returns t in [0, 1] for first overlap, or None.
222#[must_use]
223pub fn swept_aabb(moving: &AABB, velocity: Vec3Fix, target: &AABB) -> Option<Fix128> {
224    let mut t_enter = Fix128::from_int(-1000000);
225    let mut t_exit = Fix128::from_int(1000000);
226
227    // X axis
228    if let Some((te, tx)) = slab_test(
229        moving.min.x,
230        moving.max.x,
231        target.min.x,
232        target.max.x,
233        velocity.x,
234    ) {
235        t_enter = if te > t_enter { te } else { t_enter };
236        t_exit = if tx < t_exit { tx } else { t_exit };
237        if t_enter > t_exit {
238            return None;
239        }
240    } else {
241        return None;
242    }
243
244    // Y axis
245    if let Some((te, tx)) = slab_test(
246        moving.min.y,
247        moving.max.y,
248        target.min.y,
249        target.max.y,
250        velocity.y,
251    ) {
252        t_enter = if te > t_enter { te } else { t_enter };
253        t_exit = if tx < t_exit { tx } else { t_exit };
254        if t_enter > t_exit {
255            return None;
256        }
257    } else {
258        return None;
259    }
260
261    // Z axis
262    if let Some((te, tx)) = slab_test(
263        moving.min.z,
264        moving.max.z,
265        target.min.z,
266        target.max.z,
267        velocity.z,
268    ) {
269        t_enter = if te > t_enter { te } else { t_enter };
270        t_exit = if tx < t_exit { tx } else { t_exit };
271        if t_enter > t_exit {
272            return None;
273        }
274    } else {
275        return None;
276    }
277
278    if t_enter >= Fix128::ZERO && t_enter <= Fix128::ONE {
279        Some(t_enter)
280    } else if t_enter < Fix128::ZERO && t_exit >= Fix128::ZERO {
281        Some(Fix128::ZERO) // Already overlapping
282    } else {
283        None
284    }
285}
286
287/// Slab test for one axis of swept AABB
288fn slab_test(
289    a_min: Fix128,
290    a_max: Fix128,
291    b_min: Fix128,
292    b_max: Fix128,
293    vel: Fix128,
294) -> Option<(Fix128, Fix128)> {
295    if vel.is_zero() {
296        // Static on this axis - check overlap
297        if a_max < b_min || a_min > b_max {
298            return None;
299        }
300        return Some((Fix128::from_int(-1000000), Fix128::from_int(1000000)));
301    }
302
303    let inv_vel = Fix128::ONE / vel;
304    let mut t0 = (b_min - a_max) * inv_vel;
305    let mut t1 = (b_max - a_min) * inv_vel;
306
307    if t0 > t1 {
308        core::mem::swap(&mut t0, &mut t1);
309    }
310
311    Some((t0, t1))
312}
313
314/// Speculative contact for CCD integration with the solver
315///
316/// Instead of rewinding time to TOI, creates a contact constraint
317/// at the predicted collision point with a negative depth (gap).
318/// Adaptive TOI-aware sub-step count for a pair of moving bodies
319/// (Phase F 11.1 CCD integration).
320///
321/// Combines the sub-stepping policy from
322/// `crate::solver_tgs::adaptive_substeps_for_ccd` (crate-internal since
323/// v0.14.0-preview.8) with the speculative contact TOI so that
324/// fast-moving pairs receive extra sub-steps proportional to their own
325/// speed. This prevents tunneling through thin walls.
326///
327/// # Behavior
328///
329/// 1. `speculative_contact` decides whether the pair is on a collision
330///    course within `dt` at all; if not, a single sub-step (`1`) is
331///    returned — no reason to pay for extra sub-stepping on a pair that
332///    is not closing.
333/// 2. Otherwise the two bodies' own velocities (L∞ norm, see
334///    `solver_tgs::HasVelocity`) are handed to
335///    `solver_tgs::adaptive_substeps_for_ccd`, with the per-sub-step
336///    travel cap set to half the *smaller* collider's radius — a body
337///    must not advance more than that in a single sub-step or it can
338///    skip clean through the other body's cross-section — clamped into
339///    `[1, max_substeps]`.
340///
341/// # Determinism
342/// No floating-point comparison, closed-form
343/// clamp, deterministic sub-step count. `speculative_contact` and
344/// `solver_tgs::adaptive_substeps_for_ccd` are both pure Fix128
345/// functions of their inputs with index-ordered iteration.
346///
347/// # `no_std`
348/// `solver_tgs` (and therefore step 2 above) is `std`-gated, the same as
349/// the rest of the `solver_tgs` family (it uses `std::collections::HashMap`).
350/// In a build without the `std` feature this function keeps the
351/// pre-wiring behavior for step 2 — `max_substeps.max(1)` once a
352/// collision course is confirmed — the same documented, defined fallback
353/// shape as [`crate::solver::SolverBackend::Tgs`]'s own `no_std` note (a
354/// coarser-but-correct policy, not silently ignored or state corruption).
355#[must_use]
356// 1.0.0 で公開済の signature (crates.io)、引数 struct 化は semver major = 2.0 で実施
357#[allow(clippy::too_many_arguments)]
358pub fn adaptive_toi_substeps(
359    pos_a: Vec3Fix,
360    vel_a: Vec3Fix,
361    radius_a: Fix128,
362    pos_b: Vec3Fix,
363    vel_b: Vec3Fix,
364    radius_b: Fix128,
365    dt: Fix128,
366    max_substeps: u32,
367) -> u32 {
368    // 1. Not on a collision course within `dt` at all → no extra
369    //    sub-stepping is warranted. `std`-independent.
370    if speculative_contact(pos_a, vel_a, radius_a, pos_b, vel_b, radius_b, dt).is_none() {
371        return 1;
372    }
373    // 2. On a collision course: see each cfg-gated variant's own doc.
374    on_collision_course_substeps(vel_a, radius_a, vel_b, radius_b, dt, max_substeps)
375}
376
377/// Step 2 of [`adaptive_toi_substeps`] (`std` build): delegate the actual
378/// count to `solver_tgs`'s CCD-aware adaptive formula instead of blindly
379/// returning `max_substeps`. `HasVelocity` only needs each body's raw
380/// velocity vector (L∞ norm), not its full rigid-body state.
381#[cfg(feature = "std")]
382fn on_collision_course_substeps(
383    vel_a: Vec3Fix,
384    radius_a: Fix128,
385    vel_b: Vec3Fix,
386    radius_b: Fix128,
387    dt: Fix128,
388    max_substeps: u32,
389) -> u32 {
390    struct CcdVelocityBody(Vec3Fix);
391    impl crate::solver_tgs::HasVelocity for CcdVelocityBody {
392        fn velocity_l_inf(&self) -> Fix128 {
393            let ax = self.0.x.abs();
394            let ay = self.0.y.abs();
395            let az = self.0.z.abs();
396            let m = if ax > ay { ax } else { ay };
397            if az > m {
398                az
399            } else {
400                m
401            }
402        }
403    }
404
405    let bodies = [CcdVelocityBody(vel_a), CcdVelocityBody(vel_b)];
406    let smallest_radius = if radius_a < radius_b {
407        radius_a
408    } else {
409        radius_b
410    };
411    // Half the smaller collider's radius: the hard per-sub-step travel
412    // cap a fast body must respect so it cannot pass clean through the
413    // other body within one sub-step.
414    let ccd_safety_factor = Fix128::from_ratio(1, 2);
415    let cfg = crate::solver_tgs::AdaptiveSubStepConfig {
416        max_substeps: max_substeps.max(1),
417        min_substeps: 1,
418        ..crate::solver_tgs::AdaptiveSubStepConfig::default()
419    };
420    crate::solver_tgs::adaptive_substeps_for_ccd(
421        &bodies,
422        dt,
423        smallest_radius,
424        ccd_safety_factor,
425        &cfg,
426    )
427}
428
429/// Step 2 of [`adaptive_toi_substeps`] (`no_std` build): `solver_tgs` is
430/// unavailable, so this keeps the pre-wiring constant-on-collision
431/// behavior — see [`adaptive_toi_substeps`]'s `# no_std` doc.
432#[cfg(not(feature = "std"))]
433fn on_collision_course_substeps(
434    _vel_a: Vec3Fix,
435    _radius_a: Fix128,
436    _vel_b: Vec3Fix,
437    _radius_b: Fix128,
438    _dt: Fix128,
439    max_substeps: u32,
440) -> u32 {
441    max_substeps.max(1)
442}
443
444/// The solver then prevents penetration by maintaining the gap.
445///
446/// The returned [`crate::collider::Contact::normal`] follows the crate-wide
447/// contact convention: it points from B to A (`(pos_a - pos_b) / dist`), so
448/// translating A by `depth * normal` separates the pair.
449#[must_use]
450pub fn speculative_contact(
451    pos_a: Vec3Fix,
452    vel_a: Vec3Fix,
453    radius_a: Fix128,
454    pos_b: Vec3Fix,
455    vel_b: Vec3Fix,
456    radius_b: Fix128,
457    dt: Fix128,
458) -> Option<crate::collider::Contact> {
459    let rel_pos = pos_b - pos_a;
460    let dist = rel_pos.length();
461    let combined_r = radius_a + radius_b;
462
463    if dist.is_zero() {
464        return None;
465    }
466
467    // Unit direction A -> B; the reported contact normal is its negation (B -> A).
468    let dir_ab = rel_pos / dist;
469    let normal = -dir_ab;
470    let gap = dist - combined_r;
471
472    // Already overlapping — regular contact
473    if gap <= Fix128::ZERO {
474        return Some(crate::collider::Contact {
475            depth: -gap,
476            normal,
477            point_a: pos_a + dir_ab * radius_a,
478            point_b: pos_b - dir_ab * radius_b,
479        });
480    }
481
482    // Check if closing velocity will breach the gap within dt
483    let rel_vel = vel_b - vel_a;
484    let closing_speed = -rel_vel.dot(dir_ab);
485
486    if closing_speed <= Fix128::ZERO {
487        return None; // Moving apart
488    }
489
490    let predicted_gap = gap - closing_speed * dt;
491
492    if predicted_gap < Fix128::ZERO {
493        // Speculative contact: depth = predicted penetration
494        Some(crate::collider::Contact {
495            depth: -predicted_gap,
496            normal,
497            point_a: pos_a + dir_ab * radius_a,
498            point_b: pos_b - dir_ab * radius_b,
499        })
500    } else {
501        None
502    }
503}
504
505/// TOI for a moving capsule against a static plane
506///
507/// Capsule defined by two endpoints and radius, moving with given velocity.
508/// Plane in Hessian normal form: dot(normal, p) = offset.
509#[must_use]
510pub fn capsule_plane_toi(
511    cap_a: Vec3Fix,
512    cap_b: Vec3Fix,
513    radius: Fix128,
514    velocity: Vec3Fix,
515    plane_normal: Vec3Fix,
516    plane_offset: Fix128,
517) -> Option<TOI> {
518    // Closest capsule endpoint to the plane determines first contact
519    let dist_a = cap_a.dot(plane_normal) - plane_offset;
520    let dist_b = cap_b.dot(plane_normal) - plane_offset;
521    // Use the endpoint closer to the plane
522    let (center, _dist) = if dist_a < dist_b {
523        (cap_a, dist_a)
524    } else {
525        (cap_b, dist_b)
526    };
527    sphere_plane_toi(center, radius, velocity, plane_normal, plane_offset)
528}
529
530/// TOI for a moving sphere against a static capsule
531///
532/// Reduces to sphere-sphere TOI against the closest point on capsule segment.
533#[must_use]
534pub fn sphere_capsule_toi(
535    sphere_center: Vec3Fix,
536    sphere_radius: Fix128,
537    sphere_vel: Vec3Fix,
538    cap_a: Vec3Fix,
539    cap_b: Vec3Fix,
540    cap_radius: Fix128,
541) -> Option<TOI> {
542    // Find closest point on capsule segment to sphere center
543    let ab = cap_b - cap_a;
544    let ab_len_sq = ab.dot(ab);
545    let closest = if ab_len_sq.is_zero() {
546        cap_a
547    } else {
548        let t = (sphere_center - cap_a).dot(ab) / ab_len_sq;
549        let t_clamped = if t < Fix128::ZERO {
550            Fix128::ZERO
551        } else if t > Fix128::ONE {
552            Fix128::ONE
553        } else {
554            t
555        };
556        cap_a + ab * t_clamped
557    };
558    sphere_sphere_toi(
559        sphere_center,
560        sphere_radius,
561        sphere_vel,
562        closest,
563        cap_radius,
564        Vec3Fix::ZERO, // Static capsule
565    )
566}
567
568/// TOI for a moving AABB against a static plane
569///
570/// Finds the AABB vertex closest to the plane and computes point-plane TOI.
571#[must_use]
572pub fn aabb_plane_toi(
573    aabb: &AABB,
574    velocity: Vec3Fix,
575    plane_normal: Vec3Fix,
576    plane_offset: Fix128,
577) -> Option<TOI> {
578    // Find the AABB vertex most in the direction of -plane_normal (closest to plane)
579    let support_x = if plane_normal.x < Fix128::ZERO {
580        aabb.max.x
581    } else {
582        aabb.min.x
583    };
584    let support_y = if plane_normal.y < Fix128::ZERO {
585        aabb.max.y
586    } else {
587        aabb.min.y
588    };
589    let support_z = if plane_normal.z < Fix128::ZERO {
590        aabb.max.z
591    } else {
592        aabb.min.z
593    };
594    let support = Vec3Fix::new(support_x, support_y, support_z);
595
596    let dist = support.dot(plane_normal) - plane_offset;
597    let vel_toward = velocity.dot(plane_normal);
598
599    // Moving away from plane
600    if vel_toward >= Fix128::ZERO && dist > Fix128::ZERO {
601        return None;
602    }
603
604    // Already penetrating
605    if dist <= Fix128::ZERO {
606        return Some(TOI {
607            t: Fix128::ZERO,
608            point: support,
609            normal: plane_normal,
610        });
611    }
612
613    if vel_toward.is_zero() {
614        return None;
615    }
616
617    // Time when support vertex touches plane
618    let t = -dist / vel_toward;
619
620    if t >= Fix128::ZERO && t <= Fix128::ONE {
621        let point = support + velocity * t;
622        Some(TOI {
623            t,
624            point,
625            normal: plane_normal,
626        })
627    } else {
628        None
629    }
630}
631
632// ============================================================================
633// World time of impact
634// ============================================================================
635
636impl crate::solver::PhysicsWorld {
637    /// The first time in a step at which body `body`, translating with
638    /// `velocity` for `dt`, touches any other geometry the world collides with
639    /// (body shapes, compound children, static colliders and, with `std`, SDF
640    /// colliders), as a [`TOI`].
641    ///
642    /// - **Shape.** The body is swept as the sphere of its collision radius about
643    ///   its position ([`crate::solver::PhysicsWorld::cast_sphere`]). For a body
644    ///   with only a collision radius that is its exact geometry. For a body with
645    ///   a [`crate::shape::Shape`] or a compound collider the radius is the
646    ///   bounding radius about its centre of mass, so the reported time is never
647    ///   later than the true shape's first contact (it can be earlier). A body
648    ///   with no collision radius collides with nothing: `None`.
649    /// - **Time.** [`TOI::t`] is the fraction of the step, `0` at the start and
650    ///   `1` at `velocity · dt`: the distance travelled to the contact divided by
651    ///   `|velocity · dt|`.
652    /// - **Point and normal.** The contact point on the other geometry and its
653    ///   surface normal there, toward the moving body: the convention of
654    ///   [`sphere_plane_toi`], and of [`sphere_sphere_toi`] with the obstacle as
655    ///   sphere A and the moving body as sphere B (moving by `velocity · dt`). A
656    ///   body that already overlaps something reports `t = 0`, its own centre as
657    ///   the point and `−velocity` (normalized) as the normal
658    ///   (see [`crate::world_shape_query`]).
659    /// - **What is seen.** `filter` decides, except that the body itself (and any
660    ///   SDF collider attached to it) is always excluded: the filter's
661    ///   [`RayFilter::exclude_body`](crate::shape_raycast::RayFilter::exclude_body)
662    ///   is replaced by `body`. Everything else is taken at its current pose and
663    ///   treated as still; pass a relative velocity for a moving target.
664    ///
665    /// An index out of range, `dt ≤ 0` or a zero displacement gives `None`.
666    #[must_use]
667    pub fn time_of_impact(
668        &self,
669        body: usize,
670        velocity: Vec3Fix,
671        dt: Fix128,
672        filter: &crate::shape_raycast::RayFilter,
673    ) -> Option<TOI> {
674        if dt <= Fix128::ZERO {
675            return None;
676        }
677        let (radius, _) = self.ray_geometry(body)?;
678        let start = self.bodies.get(body)?.position;
679        let displacement = velocity * dt;
680        let travel = displacement.length();
681        if travel.is_zero() {
682            return None;
683        }
684        let filter = filter.excluding_body(body);
685        let hit = self.cast_sphere(start, radius, displacement, travel, &filter)?;
686        let t = if hit.t >= travel {
687            Fix128::ONE
688        } else {
689            hit.t / travel
690        };
691        Some(TOI {
692            t,
693            point: hit.point,
694            normal: hit.normal,
695        })
696    }
697}
698
699#[cfg(test)]
700mod tests {
701    use super::*;
702
703    #[test]
704    fn test_sphere_sphere_toi() {
705        // Two spheres moving toward each other
706        let toi = sphere_sphere_toi(
707            Vec3Fix::from_int(-5, 0, 0),
708            Fix128::ONE,
709            Vec3Fix::from_int(10, 0, 0),
710            Vec3Fix::from_int(5, 0, 0),
711            Fix128::ONE,
712            Vec3Fix::from_int(-10, 0, 0),
713        );
714
715        let toi = toi.expect("Should find collision");
716        // Distance = 10, combined_r = 2, relative_speed = 20
717        // t = (10 - 2) / 20 = 0.4
718        let expected = Fix128::from_ratio(4, 10);
719        let error = (toi.t - expected).abs();
720        assert!(
721            error < Fix128::from_ratio(1, 10),
722            "TOI should be ~0.4, got {:?}",
723            toi.t
724        );
725    }
726
727    #[test]
728    fn test_sphere_sphere_miss() {
729        // Spheres moving apart
730        let toi = sphere_sphere_toi(
731            Vec3Fix::from_int(-5, 0, 0),
732            Fix128::ONE,
733            Vec3Fix::from_int(-10, 0, 0),
734            Vec3Fix::from_int(5, 0, 0),
735            Fix128::ONE,
736            Vec3Fix::from_int(10, 0, 0),
737        );
738        assert!(toi.is_none());
739    }
740
741    #[test]
742    fn test_sphere_plane_toi() {
743        // Sphere falling toward ground plane at y=0
744        let toi = sphere_plane_toi(
745            Vec3Fix::from_int(0, 10, 0),
746            Fix128::ONE,
747            Vec3Fix::from_int(0, -20, 0),
748            Vec3Fix::UNIT_Y,
749            Fix128::ZERO,
750        );
751
752        let toi = toi.expect("Should find collision");
753        // dist = 10, radius = 1, vel_toward = -20
754        // t = (1 - 10) / (-20) = 0.45
755        let expected = Fix128::from_ratio(45, 100);
756        let error = (toi.t - expected).abs();
757        assert!(error < Fix128::from_ratio(1, 10), "TOI should be ~0.45");
758    }
759
760    #[test]
761    fn test_needs_ccd() {
762        let config = CcdConfig::default();
763        let fast = Vec3Fix::from_int(100, 0, 0);
764        let slow = Vec3Fix::from_int(1, 0, 0);
765        let dt = Fix128::from_ratio(1, 60);
766        let radius = Fix128::ONE;
767
768        assert!(needs_ccd(fast, radius, dt, &config), "Fast body needs CCD");
769        assert!(
770            !needs_ccd(slow, radius, dt, &config),
771            "Slow body doesn't need CCD"
772        );
773    }
774
775    #[test]
776    fn test_swept_aabb() {
777        let moving = AABB::new(Vec3Fix::from_int(-1, -1, -1), Vec3Fix::from_int(1, 1, 1));
778        let target = AABB::new(Vec3Fix::from_int(5, -1, -1), Vec3Fix::from_int(7, 1, 1));
779        let velocity = Vec3Fix::from_int(10, 0, 0);
780
781        let t = swept_aabb(&moving, velocity, &target);
782        assert!(t.is_some(), "Should detect swept collision");
783        let t = t.unwrap();
784        // Moving AABB right edge at x=1, target left edge at x=5
785        // distance = 4, speed = 10, t = 0.4
786        let expected = Fix128::from_ratio(4, 10);
787        let error = (t - expected).abs();
788        assert!(error < Fix128::from_ratio(1, 10), "TOI should be ~0.4");
789    }
790
791    #[test]
792    fn test_conservative_advancement() {
793        let config = CcdConfig::default();
794        let start = Vec3Fix::from_int(-10, 0, 0);
795        let displacement = Vec3Fix::from_int(20, 0, 0);
796        let radius = Fix128::ONE;
797
798        // Distance function: sphere at origin with radius 2
799        let toi = conservative_advancement(
800            start,
801            displacement,
802            radius,
803            |pos| {
804                let dist = pos.length() - Fix128::from_int(2);
805                let normal = pos.normalize();
806                (dist, normal)
807            },
808            &config,
809        );
810
811        assert!(
812            toi.is_some(),
813            "Should find collision via conservative advancement"
814        );
815    }
816
817    #[test]
818    fn test_speculative_contact_closing() {
819        let dt = Fix128::from_ratio(1, 60);
820        // gap=4, closing_speed=300, predicted_gap = 4 - 300/60 = -1 < 0
821        let contact = speculative_contact(
822            Vec3Fix::from_int(-3, 0, 0),
823            Vec3Fix::from_int(300, 0, 0),
824            Fix128::ONE,
825            Vec3Fix::from_int(3, 0, 0),
826            Vec3Fix::ZERO,
827            Fix128::ONE,
828            dt,
829        );
830        assert!(
831            contact.is_some(),
832            "Fast-closing spheres should generate speculative contact"
833        );
834    }
835
836    #[test]
837    fn test_speculative_contact_separating() {
838        let dt = Fix128::from_ratio(1, 60);
839        let contact = speculative_contact(
840            Vec3Fix::from_int(-3, 0, 0),
841            Vec3Fix::from_int(-10, 0, 0), // moving away
842            Fix128::ONE,
843            Vec3Fix::from_int(3, 0, 0),
844            Vec3Fix::ZERO,
845            Fix128::ONE,
846            dt,
847        );
848        assert!(
849            contact.is_none(),
850            "Separating spheres should not generate contact"
851        );
852    }
853
854    #[test]
855    fn test_capsule_plane_toi() {
856        let toi = capsule_plane_toi(
857            Vec3Fix::from_int(0, 10, -1),
858            Vec3Fix::from_int(0, 10, 1),
859            Fix128::ONE,
860            Vec3Fix::from_int(0, -20, 0),
861            Vec3Fix::UNIT_Y,
862            Fix128::ZERO,
863        );
864        let toi = toi.expect("Should find capsule-plane collision");
865        assert!(toi.t > Fix128::ZERO && toi.t < Fix128::ONE);
866    }
867
868    #[test]
869    fn test_sphere_capsule_toi() {
870        let toi = sphere_capsule_toi(
871            Vec3Fix::from_int(-10, 0, 0),
872            Fix128::ONE,
873            Vec3Fix::from_int(20, 0, 0),
874            Vec3Fix::from_int(5, -2, 0),
875            Vec3Fix::from_int(5, 2, 0),
876            Fix128::ONE,
877        );
878        let toi = toi.expect("Should find sphere-capsule collision");
879        assert!(toi.t > Fix128::ZERO && toi.t < Fix128::ONE);
880    }
881
882    #[test]
883    fn test_aabb_plane_toi() {
884        let aabb = AABB::new(Vec3Fix::from_int(-1, 8, -1), Vec3Fix::from_int(1, 10, 1));
885        let toi = aabb_plane_toi(
886            &aabb,
887            Vec3Fix::from_int(0, -20, 0),
888            Vec3Fix::UNIT_Y,
889            Fix128::ZERO,
890        );
891        let toi = toi.expect("Should find AABB-plane collision");
892        assert!(toi.t > Fix128::ZERO && toi.t < Fix128::ONE);
893    }
894
895    #[test]
896    fn test_aabb_plane_miss() {
897        let aabb = AABB::new(Vec3Fix::from_int(-1, 8, -1), Vec3Fix::from_int(1, 10, 1));
898        let toi = aabb_plane_toi(
899            &aabb,
900            Vec3Fix::from_int(0, 20, 0), // Moving away
901            Vec3Fix::UNIT_Y,
902            Fix128::ZERO,
903        );
904        assert!(toi.is_none());
905    }
906
907    // ---- mutation-score tests (2026-09-15) ----------------------------
908
909    fn fi(n: i64) -> Fix128 {
910        Fix128::from_int(n)
911    }
912
913    fn r(n: i64, d: i64) -> Fix128 {
914        Fix128::from_ratio(n, d)
915    }
916
917    fn v3i(x: i64, y: i64, z: i64) -> Vec3Fix {
918        Vec3Fix::from_int(x, y, z)
919    }
920
921    #[test]
922    fn needs_ccd_requires_both_displacement_and_speed_thresholds() {
923        let cfg = CcdConfig::default(); // velocity_threshold 5
924                                        // speed 8, dt 1/4 → 変位 2 > r/2 (r=2 → 1) かつ 8 > 5 → true
925        assert!(needs_ccd(v3i(8, 0, 0), fi(2), r(1, 4), &cfg));
926        // 変位不足: speed 8, dt 1/16 → 0.5 == r/2 → `>` false
927        assert!(!needs_ccd(v3i(8, 0, 0), fi(2), r(1, 16), &cfg));
928        // 速度不足: speed 4 (< 5) だが変位 4 > 1
929        assert!(!needs_ccd(v3i(4, 0, 0), fi(2), fi(1), &cfg));
930        // 速度ちょうど 5 は `>` false
931        assert!(!needs_ccd(v3i(5, 0, 0), fi(2), fi(1), &cfg));
932        assert!(needs_ccd(v3i(0, 0, 6), fi(2), fi(1), &cfg));
933    }
934
935    #[test]
936    fn sphere_sphere_toi_closed_form() {
937        // A at 0 r=1 速度 (8,0,0)、B at (10,0,0) r=1 静止 → 接触は距離 2 → t = (10-2)/8 = 1
938        let toi = sphere_sphere_toi(
939            Vec3Fix::ZERO,
940            fi(1),
941            v3i(8, 0, 0),
942            v3i(10, 0, 0),
943            fi(1),
944            Vec3Fix::ZERO,
945        )
946        .expect("hit at t=1");
947        assert_eq!(toi.t, Fix128::ONE);
948        assert_eq!(toi.normal, v3i(1, 0, 0));
949        assert_eq!(toi.point, v3i(9, 0, 0)); // pos_a(8) + n * r_a
950                                             // 相対速度: B も逆向き (-8) → t = 8/16 = 1/2、point = 4 + 1
951        let t2 = sphere_sphere_toi(
952            Vec3Fix::ZERO,
953            fi(1),
954            v3i(8, 0, 0),
955            v3i(10, 0, 0),
956            fi(1),
957            v3i(-8, 0, 0),
958        )
959        .expect("t=1/2");
960        assert_eq!(t2.t, r(1, 2));
961        assert_eq!(t2.point, v3i(5, 0, 0));
962        // 半径が効く: r_b = 3 → 距離 4 で接触 → t = 6/8 = 3/4
963        let t3 = sphere_sphere_toi(
964            Vec3Fix::ZERO,
965            fi(1),
966            v3i(8, 0, 0),
967            v3i(10, 0, 0),
968            fi(3),
969            Vec3Fix::ZERO,
970        )
971        .expect("t=3/4");
972        assert_eq!(t3.t, r(3, 4));
973        // 届かない (t > 1)、離れていく、横にずれて外れる、静止
974        assert!(sphere_sphere_toi(
975            Vec3Fix::ZERO,
976            fi(1),
977            v3i(4, 0, 0),
978            v3i(10, 0, 0),
979            fi(1),
980            Vec3Fix::ZERO
981        )
982        .is_none());
983        assert!(sphere_sphere_toi(
984            Vec3Fix::ZERO,
985            fi(1),
986            v3i(-8, 0, 0),
987            v3i(10, 0, 0),
988            fi(1),
989            Vec3Fix::ZERO
990        )
991        .is_none());
992        assert!(sphere_sphere_toi(
993            Vec3Fix::ZERO,
994            fi(1),
995            v3i(8, 0, 0),
996            v3i(10, 5, 0),
997            fi(1),
998            Vec3Fix::ZERO
999        )
1000        .is_none());
1001        assert!(sphere_sphere_toi(
1002            Vec3Fix::ZERO,
1003            fi(1),
1004            Vec3Fix::ZERO,
1005            v3i(10, 0, 0),
1006            fi(1),
1007            Vec3Fix::ZERO
1008        )
1009        .is_none());
1010        // 既に重なっている → t = 0、normal は a→b
1011        let ov = sphere_sphere_toi(
1012            Vec3Fix::ZERO,
1013            fi(1),
1014            Vec3Fix::ZERO,
1015            v3i(1, 0, 0),
1016            fi(1),
1017            Vec3Fix::ZERO,
1018        )
1019        .expect("overlap");
1020        assert_eq!(ov.t, Fix128::ZERO);
1021        assert_eq!(ov.normal, v3i(1, 0, 0));
1022        assert_eq!(ov.point, v3i(1, 0, 0));
1023        // 接線 (disc == 0): B at (10, 2, 0) r=1、A r=1 → 距離 2 の接線経路 → t = 10/8 > 1 → None、速度 16 → t = 10/16
1024        let tan = sphere_sphere_toi(
1025            Vec3Fix::ZERO,
1026            fi(1),
1027            v3i(16, 0, 0),
1028            v3i(10, 2, 0),
1029            fi(1),
1030            Vec3Fix::ZERO,
1031        )
1032        .expect("tangent");
1033        assert_eq!(tan.t, r(5, 8));
1034    }
1035
1036    #[test]
1037    fn sphere_plane_toi_closed_form() {
1038        // 平面 y = 0 (normal +y、offset 0)、球 center (0,5,0) r=1 速度 (0,-8,0) → t = (1-5)/(-8) = 1/2
1039        let toi = sphere_plane_toi(
1040            v3i(0, 5, 0),
1041            fi(1),
1042            v3i(0, -8, 0),
1043            v3i(0, 1, 0),
1044            Fix128::ZERO,
1045        )
1046        .expect("hit");
1047        assert_eq!(toi.t, r(1, 2));
1048        assert_eq!(toi.point, v3i(0, 0, 0)); // center + v t - n r = (0,1,0) - (0,1,0)
1049        assert_eq!(toi.normal, v3i(0, 1, 0));
1050        // offset が効く: 平面 y = 2 → dist 3 → t = (1-3)/(-8) = 1/4、point = (0, 2, 0)
1051        let off =
1052            sphere_plane_toi(v3i(0, 5, 0), fi(1), v3i(0, -8, 0), v3i(0, 1, 0), fi(2)).expect("hit");
1053        assert_eq!(off.t, r(1, 4));
1054        assert_eq!(off.point, v3i(0, 2, 0));
1055        // 離れていく / 平行 / 届かない
1056        assert!(sphere_plane_toi(
1057            v3i(0, 5, 0),
1058            fi(1),
1059            v3i(0, 8, 0),
1060            v3i(0, 1, 0),
1061            Fix128::ZERO
1062        )
1063        .is_none());
1064        assert!(sphere_plane_toi(
1065            v3i(0, 5, 0),
1066            fi(1),
1067            v3i(8, 0, 0),
1068            v3i(0, 1, 0),
1069            Fix128::ZERO
1070        )
1071        .is_none());
1072        assert!(sphere_plane_toi(
1073            v3i(0, 5, 0),
1074            fi(1),
1075            v3i(0, -2, 0),
1076            v3i(0, 1, 0),
1077            Fix128::ZERO
1078        )
1079        .is_none());
1080        // 既に貫通 (|dist| <= r): dist 1 == r → t 0、point は投影点
1081        let pen = sphere_plane_toi(
1082            v3i(3, 1, 0),
1083            fi(1),
1084            v3i(0, 8, 0),
1085            v3i(0, 1, 0),
1086            Fix128::ZERO,
1087        )
1088        .expect("touching");
1089        assert_eq!(pen.t, Fix128::ZERO);
1090        assert_eq!(pen.point, v3i(3, 0, 0));
1091        // 裏側から: center (0,-5,0)、速度 (0,8,0) → dist -5、近い面 (y = -4) が
1092        // 平面に触れる t = (-1 + 5)/8 = 1/2 (1.2.0 以前は遠い面の 3/4 = 通過時刻を返していた)
1093        // normal は球側 (-y)、接触点は平面上
1094        let back = sphere_plane_toi(
1095            v3i(0, -5, 0),
1096            fi(1),
1097            v3i(0, 8, 0),
1098            v3i(0, 1, 0),
1099            Fix128::ZERO,
1100        )
1101        .expect("from behind");
1102        assert_eq!(back.t, r(1, 2));
1103        assert_eq!(back.normal, v3i(0, -1, 0));
1104        assert_eq!(back.point, Vec3Fix::ZERO);
1105        // 裏側で離れる向き → None
1106        assert!(sphere_plane_toi(
1107            v3i(0, -5, 0),
1108            fi(1),
1109            v3i(0, -8, 0),
1110            v3i(0, 1, 0),
1111            Fix128::ZERO
1112        )
1113        .is_none());
1114    }
1115
1116    #[test]
1117    fn conservative_advancement_converges_on_a_plane() {
1118        let cfg = CcdConfig {
1119            max_iterations: 32,
1120            tolerance: r(1, 1000),
1121            velocity_threshold: fi(5),
1122        };
1123        // 距離関数: 平面 x = 10 (signed distance = 10 - x、normal -x 向き)
1124        let plane = |p: Vec3Fix| (fi(10) - p.x, v3i(-1, 0, 0));
1125        // start 0、変位 (16,0,0)、r=1 → 接触は x = 9 → t = 9/16 (gap は幾何級数で 0 に収束)
1126        let toi = conservative_advancement(Vec3Fix::ZERO, v3i(16, 0, 0), fi(1), plane, &cfg)
1127            .expect("hit");
1128        assert!((toi.t - r(9, 16)).abs() < r(1, 1000), "t {:?}", toi.t);
1129        assert_eq!(toi.normal, v3i(-1, 0, 0));
1130        // 届かない (変位 4 → x=4、gap 5)、速度 0
1131        assert!(
1132            conservative_advancement(Vec3Fix::ZERO, v3i(4, 0, 0), fi(1), plane, &cfg).is_none()
1133        );
1134        assert!(
1135            conservative_advancement(Vec3Fix::ZERO, Vec3Fix::ZERO, fi(1), plane, &cfg).is_none()
1136        );
1137        // 既に tolerance 内 (start x = 9) → t = 0
1138        let now = conservative_advancement(v3i(9, 0, 0), v3i(16, 0, 0), fi(1), plane, &cfg)
1139            .expect("immediate");
1140        assert_eq!(now.t, Fix128::ZERO);
1141        // max_iterations 1 では収束せず None
1142        let one = CcdConfig {
1143            max_iterations: 1,
1144            ..cfg
1145        };
1146        assert!(
1147            conservative_advancement(Vec3Fix::ZERO, v3i(16, 0, 0), fi(1), plane, &one).is_none()
1148        );
1149    }
1150
1151    #[test]
1152    fn swept_aabb_entry_time_per_axis_and_overlap_cases() {
1153        let m = AABB::new(v3i(-1, -1, -1), v3i(1, 1, 1));
1154        let target = AABB::new(v3i(5, -1, -1), v3i(7, 1, 1));
1155        // x で 4 離れ、速度 8 → t = 1/2
1156        assert_eq!(swept_aabb(&m, v3i(8, 0, 0), &target), Some(r(1, 2)));
1157        // 速度 2 → t = 2 > 1 → None
1158        assert_eq!(swept_aabb(&m, v3i(2, 0, 0), &target), None);
1159        // 逆向き → None、y でずれて通過しない → None
1160        assert_eq!(swept_aabb(&m, v3i(-8, 0, 0), &target), None);
1161        assert_eq!(
1162            swept_aabb(&m, v3i(8, 0, 0), &AABB::new(v3i(5, 3, -1), v3i(7, 5, 1))),
1163            None
1164        );
1165        // 斜め: y でも 4 離れた target、速度 (8, 8, 0) → x,y とも t 1/2 で進入 → 1/2
1166        assert_eq!(
1167            swept_aabb(&m, v3i(8, 8, 0), &AABB::new(v3i(5, 5, -1), v3i(7, 7, 1))),
1168            Some(r(1, 2))
1169        );
1170        // 各軸の enter は max 側: x は 1/2、y は 1/4 で進入 → 1/2
1171        assert_eq!(
1172            swept_aabb(&m, v3i(8, 8, 0), &AABB::new(v3i(5, 3, -1), v3i(7, 5, 1))),
1173            Some(r(1, 2))
1174        );
1175        // y 軸と z 軸単独
1176        assert_eq!(
1177            swept_aabb(&m, v3i(0, 8, 0), &AABB::new(v3i(-1, 5, -1), v3i(1, 7, 1))),
1178            Some(r(1, 2))
1179        );
1180        assert_eq!(
1181            swept_aabb(
1182                &m,
1183                v3i(0, 0, -8),
1184                &AABB::new(v3i(-1, -1, -7), v3i(1, 1, -5))
1185            ),
1186            Some(r(1, 2))
1187        );
1188        // 既に重なっている → 0、静止で重ならない → None、静止で重なる → 0
1189        assert_eq!(
1190            swept_aabb(&m, v3i(8, 0, 0), &AABB::new(Vec3Fix::ZERO, v3i(2, 2, 2))),
1191            Some(Fix128::ZERO)
1192        );
1193        assert_eq!(swept_aabb(&m, Vec3Fix::ZERO, &target), None);
1194        assert_eq!(
1195            swept_aabb(&m, Vec3Fix::ZERO, &AABB::new(Vec3Fix::ZERO, v3i(2, 2, 2))),
1196            Some(Fix128::ZERO)
1197        );
1198        // 速度ちょうど 4 → t = 1 (`<=` 境界)
1199        assert_eq!(swept_aabb(&m, v3i(4, 0, 0), &target), Some(Fix128::ONE));
1200    }
1201
1202    #[test]
1203    fn slab_test_orders_times_and_handles_static_axis() {
1204        // a [-1,1] → b [5,7]、vel 8: t0 = (5-1)/8 = 1/2、t1 = (7+1)/8 = 1
1205        assert_eq!(
1206            slab_test(fi(-1), fi(1), fi(5), fi(7), fi(8)),
1207            Some((r(1, 2), fi(1)))
1208        );
1209        // 負の速度では swap される: a [5,7] → b [-1,1]、vel -8: t0 = (-1-7)/-8 = 1、t1 = (1-5)/-8 = 1/2 → (1/2, 1)
1210        assert_eq!(
1211            slab_test(fi(5), fi(7), fi(-1), fi(1), fi(-8)),
1212            Some((r(1, 2), fi(1)))
1213        );
1214        // 静止: 重なりなし → None、重なり / 接触 → 無限区間
1215        assert_eq!(slab_test(fi(-1), fi(1), fi(5), fi(7), Fix128::ZERO), None);
1216        assert_eq!(
1217            slab_test(fi(-1), fi(1), fi(0), fi(7), Fix128::ZERO),
1218            Some((fi(-1000000), fi(1000000)))
1219        );
1220        assert_eq!(
1221            slab_test(fi(-1), fi(1), fi(1), fi(7), Fix128::ZERO),
1222            Some((fi(-1000000), fi(1000000)))
1223        );
1224        assert_eq!(
1225            slab_test(fi(-1), fi(1), fi(-7), fi(-1), Fix128::ZERO),
1226            Some((fi(-1000000), fi(1000000)))
1227        );
1228    }
1229
1230    #[test]
1231    fn speculative_contact_predicts_gap_breach() {
1232        // A at 0 r=1、B at (10,0,0) r=1、gap 8、closing 8*dt: dt 1/2 → predicted 4 > 0 → None
1233        assert!(speculative_contact(
1234            Vec3Fix::ZERO,
1235            v3i(8, 0, 0),
1236            fi(1),
1237            v3i(10, 0, 0),
1238            Vec3Fix::ZERO,
1239            fi(1),
1240            r(1, 2)
1241        )
1242        .is_none());
1243        // dt 2 → predicted -8 → depth 8
1244        let c = speculative_contact(
1245            Vec3Fix::ZERO,
1246            v3i(8, 0, 0),
1247            fi(1),
1248            v3i(10, 0, 0),
1249            Vec3Fix::ZERO,
1250            fi(1),
1251            fi(2),
1252        )
1253        .expect("breach");
1254        assert_eq!(c.depth, fi(8));
1255        // normal points from B to A (crate-wide Contact convention)
1256        assert_eq!(c.normal, v3i(-1, 0, 0));
1257        assert_eq!(c.point_a, v3i(1, 0, 0));
1258        assert_eq!(c.point_b, v3i(9, 0, 0));
1259        // predicted == 0 ちょうど (dt 1) は `<` false → None
1260        assert!(speculative_contact(
1261            Vec3Fix::ZERO,
1262            v3i(8, 0, 0),
1263            fi(1),
1264            v3i(10, 0, 0),
1265            Vec3Fix::ZERO,
1266            fi(1),
1267            fi(1)
1268        )
1269        .is_none());
1270        // 離れる向き → None、既に重なり → depth = -gap
1271        assert!(speculative_contact(
1272            Vec3Fix::ZERO,
1273            v3i(-8, 0, 0),
1274            fi(1),
1275            v3i(10, 0, 0),
1276            Vec3Fix::ZERO,
1277            fi(1),
1278            fi(9)
1279        )
1280        .is_none());
1281        let ov = speculative_contact(
1282            Vec3Fix::ZERO,
1283            Vec3Fix::ZERO,
1284            fi(1),
1285            v3i(1, 0, 0),
1286            Vec3Fix::ZERO,
1287            fi(1),
1288            fi(1),
1289        )
1290        .expect("overlap");
1291        assert_eq!(ov.depth, fi(1));
1292        assert_eq!(ov.point_b, Vec3Fix::ZERO);
1293        // 同一点 → None、B の速度も効く (B が -8 → closing 16、dt 1 → predicted -8)
1294        assert!(speculative_contact(
1295            Vec3Fix::ZERO,
1296            v3i(8, 0, 0),
1297            fi(1),
1298            Vec3Fix::ZERO,
1299            Vec3Fix::ZERO,
1300            fi(1),
1301            fi(1)
1302        )
1303        .is_none());
1304        let both = speculative_contact(
1305            Vec3Fix::ZERO,
1306            v3i(8, 0, 0),
1307            fi(1),
1308            v3i(10, 0, 0),
1309            v3i(-8, 0, 0),
1310            fi(1),
1311            fi(1),
1312        )
1313        .expect("both");
1314        assert_eq!(both.depth, fi(8));
1315        // adaptive_toi_substeps: 衝突コースなら max、そうでなければ 1、max 0 は 1 に clamp
1316        assert_eq!(
1317            adaptive_toi_substeps(
1318                Vec3Fix::ZERO,
1319                v3i(8, 0, 0),
1320                fi(1),
1321                v3i(10, 0, 0),
1322                Vec3Fix::ZERO,
1323                fi(1),
1324                fi(2),
1325                6
1326            ),
1327            6
1328        );
1329        assert_eq!(
1330            adaptive_toi_substeps(
1331                Vec3Fix::ZERO,
1332                v3i(8, 0, 0),
1333                fi(1),
1334                v3i(10, 0, 0),
1335                Vec3Fix::ZERO,
1336                fi(1),
1337                r(1, 2),
1338                6
1339            ),
1340            1
1341        );
1342        assert_eq!(
1343            adaptive_toi_substeps(
1344                Vec3Fix::ZERO,
1345                v3i(8, 0, 0),
1346                fi(1),
1347                v3i(10, 0, 0),
1348                Vec3Fix::ZERO,
1349                fi(1),
1350                fi(2),
1351                0
1352            ),
1353            1
1354        );
1355    }
1356
1357    #[test]
1358    fn capsule_and_aabb_plane_toi_use_closest_feature() {
1359        let n = v3i(0, 1, 0);
1360        // capsule 端点 a (0,5,0) / b (0,9,0) r=1、速度 (0,-8,0) → 近い方 a → t = (1-5)/-8 = 1/2
1361        let cap = capsule_plane_toi(
1362            v3i(0, 5, 0),
1363            v3i(0, 9, 0),
1364            fi(1),
1365            v3i(0, -8, 0),
1366            n,
1367            Fix128::ZERO,
1368        )
1369        .expect("hit");
1370        assert_eq!(cap.t, r(1, 2));
1371        // 端点を入れ替えても同じ (b が近い場合の分岐)
1372        let cap2 = capsule_plane_toi(
1373            v3i(0, 9, 0),
1374            v3i(0, 5, 0),
1375            fi(1),
1376            v3i(0, -8, 0),
1377            n,
1378            Fix128::ZERO,
1379        )
1380        .expect("hit");
1381        assert_eq!(cap2.t, r(1, 2));
1382        // sphere_capsule: 球 (0,0,0) r=1 速度 (8,0,0)、capsule 線分 (10,-5,0)-(10,5,0) r=1 → 最近点 (10,0,0) → t = 1
1383        let sc = sphere_capsule_toi(
1384            Vec3Fix::ZERO,
1385            fi(1),
1386            v3i(8, 0, 0),
1387            v3i(10, -5, 0),
1388            v3i(10, 5, 0),
1389            fi(1),
1390        )
1391        .expect("hit");
1392        assert_eq!(sc.t, Fix128::ONE);
1393        // clamp: 球が線分の延長上 (y = 9) → 最近点は端 (10,5,0) → 距離 √(100+16)... 横方向 4 のずれで外れる (速度 8 では届かない)
1394        assert!(sphere_capsule_toi(
1395            v3i(0, 9, 0),
1396            fi(1),
1397            v3i(8, 0, 0),
1398            v3i(10, -5, 0),
1399            v3i(10, 5, 0),
1400            fi(1)
1401        )
1402        .is_none());
1403        // 退化 capsule (a == b) は球扱い
1404        let deg = sphere_capsule_toi(
1405            Vec3Fix::ZERO,
1406            fi(1),
1407            v3i(8, 0, 0),
1408            v3i(10, 0, 0),
1409            v3i(10, 0, 0),
1410            fi(1),
1411        )
1412        .expect("sphere");
1413        assert_eq!(deg.t, Fix128::ONE);
1414        // aabb_plane: 箱 [1,3]³ 速度 (0,-8,0)、平面 y=0 → 最下点 y=1 → t = 1/8、point は support 頂点 + 移動
1415        let ab = aabb_plane_toi(
1416            &AABB::new(v3i(1, 1, 1), v3i(3, 3, 3)),
1417            v3i(0, -8, 0),
1418            n,
1419            Fix128::ZERO,
1420        )
1421        .expect("hit");
1422        assert_eq!(ab.t, r(1, 8));
1423        assert_eq!(ab.normal, n);
1424        // 法線が -y なら support は max 側 (y=3)、平面 y = 5 (offset -5 for normal -y): dist = -3 + 5 = 2、速度 (0,8,0) → vel_toward -8 → t = 2/8 = 1/4
1425        let ab2 = aabb_plane_toi(
1426            &AABB::new(v3i(1, 1, 1), v3i(3, 3, 3)),
1427            v3i(0, 8, 0),
1428            v3i(0, -1, 0),
1429            fi(-5),
1430        )
1431        .expect("hit");
1432        assert_eq!(ab2.t, r(1, 4));
1433        // 離れる / 既に貫通 (t 0) / 平行
1434        assert!(aabb_plane_toi(
1435            &AABB::new(v3i(1, 1, 1), v3i(3, 3, 3)),
1436            v3i(0, 8, 0),
1437            n,
1438            Fix128::ZERO
1439        )
1440        .is_none());
1441        let pen = aabb_plane_toi(
1442            &AABB::new(v3i(1, -1, 1), v3i(3, 3, 3)),
1443            v3i(0, 8, 0),
1444            n,
1445            Fix128::ZERO,
1446        )
1447        .expect("penetrating");
1448        assert_eq!(pen.t, Fix128::ZERO);
1449        assert_eq!(pen.point, v3i(1, -1, 1));
1450        assert!(aabb_plane_toi(
1451            &AABB::new(v3i(1, 1, 1), v3i(3, 3, 3)),
1452            v3i(8, 0, 0),
1453            n,
1454            Fix128::ZERO
1455        )
1456        .is_none());
1457    }
1458
1459    // ---- mutation-score tests, round 2 (2026-09-15) ---------------------
1460    //
1461    // Each test below names the surviving cargo-mutants mutant it kills
1462    // (line numbers refer to the non-test code above). All inputs are chosen
1463    // so that the arithmetic is exact in I64F64 (powers of two, perfect
1464    // squares), so equality assertions are used unless noted otherwise.
1465
1466    /// Kills L52 `>` -> `>=` (displacement test in `needs_ccd`).
1467    ///
1468    /// speed 8, dt 1/16 -> displacement 0.5; radius 1 -> half 0.5.
1469    /// The contract is strict (`displacement > radius/2`), so exactly on the
1470    /// boundary CCD is NOT needed even though speed 8 > threshold 5.
1471    #[test]
1472    fn needs_ccd_displacement_boundary_is_strict() {
1473        let cfg = CcdConfig::default();
1474        assert!(!needs_ccd(v3i(8, 0, 0), fi(1), r(1, 16), &cfg));
1475        // One step past the boundary (displacement 1.0 > 0.5) -> needed.
1476        assert!(needs_ccd(v3i(8, 0, 0), fi(1), r(1, 8), &cfg));
1477    }
1478
1479    /// Kills L83 `*` -> `/` (contact point of the already-overlapping branch
1480    /// in `sphere_sphere_toi`).
1481    ///
1482    /// A at (3,0,0) r=2, B at (4,0,0) r=1: c = 1 - 9 < 0 -> overlap,
1483    /// normal = +x, point = center_a + normal * 2 = (5,0,0)
1484    /// (normal / 2 would give (3.5,0,0)).
1485    #[test]
1486    fn sphere_sphere_toi_overlap_point_scales_with_radius_a() {
1487        let toi = sphere_sphere_toi(
1488            v3i(3, 0, 0),
1489            fi(2),
1490            Vec3Fix::ZERO,
1491            v3i(4, 0, 0),
1492            fi(1),
1493            Vec3Fix::ZERO,
1494        )
1495        .expect("overlapping");
1496        assert_eq!(toi.t, Fix128::ZERO);
1497        assert_eq!(toi.normal, v3i(1, 0, 0));
1498        assert_eq!(toi.point, v3i(5, 0, 0));
1499    }
1500
1501    /// Kills L104 `+` -> `-` (pos_b), L105 `-` -> `+` (normal direction) and
1502    /// L106 `*` -> `/` (point) in `sphere_sphere_toi`.
1503    ///
1504    /// Oblique, non-collinear impact so that every mutant changes the result:
1505    /// A: center (0,1,0), r 3/2, vel (8,0,0); B: center (8,7,0), r 1/2,
1506    /// vel (0,-8,0).
1507    /// rel_pos = (8,6,0), rel_vel = (-8,-8,0), combined_r = 2
1508    /// a = 128, b = 2*(-64-48) = -224, c = 100 - 4 = 96
1509    /// disc = 224^2 - 4*128*96 = 50176 - 49152 = 1024, sqrt = 32
1510    /// t = (224 - 32) / 256 = 3/4 (exact)
1511    /// pos_a = (6,1,0), pos_b = (8,1,0) -> normal = (1,0,0)
1512    /// point = pos_a + normal * 3/2 = (15/2, 1, 0)
1513    /// - L104 mutant: pos_b = (8,13,0) -> normal = normalize(2,12,0) != +x
1514    /// - L105 mutant: normal = normalize(14,2,0) != +x
1515    /// - L106 mutant: point = (6 + 2/3, 1, 0)
1516    #[test]
1517    fn sphere_sphere_toi_oblique_impact_closed_form() {
1518        let toi = sphere_sphere_toi(
1519            v3i(0, 1, 0),
1520            r(3, 2),
1521            v3i(8, 0, 0),
1522            v3i(8, 7, 0),
1523            r(1, 2),
1524            v3i(0, -8, 0),
1525        )
1526        .expect("hit at t=3/4");
1527        assert_eq!(toi.t, r(3, 4));
1528        assert_eq!(toi.normal, v3i(1, 0, 0));
1529        assert_eq!(toi.point, Vec3Fix::new(r(15, 2), fi(1), Fix128::ZERO));
1530    }
1531
1532    /// Kills L134 `*` -> `/` (penetrating-branch point) and L150 `*` -> `/`
1533    /// (surface point, `plane_normal * radius`) in `sphere_plane_toi`.
1534    ///
1535    /// Penetrating: center (3, 5/2, 0), r 1, plane y = 2 -> dist 1/2 <= r,
1536    /// point = center - n * dist = (3, 2, 0)  (n / dist gives (3, 1/2, 0)).
1537    /// Surface: center (0,5,0), r 2, vel (0,-8,0), plane y = 0:
1538    /// t = (2 - 5) / (-8) = 3/8, point = (0,5,0) + (0,-3,0) - (0,2,0) = 0
1539    /// (n / r gives (0, 3/2, 0)).
1540    #[test]
1541    fn sphere_plane_toi_points_scale_with_dist_and_radius() {
1542        let n = v3i(0, 1, 0);
1543        let pen = sphere_plane_toi(
1544            Vec3Fix::new(fi(3), r(5, 2), Fix128::ZERO),
1545            fi(1),
1546            v3i(0, 8, 0),
1547            n,
1548            fi(2),
1549        )
1550        .expect("penetrating");
1551        assert_eq!(pen.t, Fix128::ZERO);
1552        assert_eq!(pen.point, v3i(3, 2, 0));
1553
1554        let hit = sphere_plane_toi(v3i(0, 5, 0), fi(2), v3i(0, -8, 0), n, Fix128::ZERO)
1555            .expect("surface hit");
1556        assert_eq!(hit.t, r(3, 8));
1557        assert_eq!(hit.point, Vec3Fix::ZERO);
1558    }
1559
1560    /// Kills L185 `-` -> `+`, L185 `*` -> `/` (contact point) and
1561    /// L198 `>` -> `>=` (t == 1 must still be checked) in
1562    /// `conservative_advancement`.
1563    ///
1564    /// Distance function: plane x = 10, signed distance 10 - x, normal -x.
1565    /// (1) start (8,0,0), r 2: dist 2, gap 0 -> immediate hit,
1566    ///     point = pos - normal * dist = (8,0,0) - (-2,0,0) = (10,0,0)
1567    ///     (`+` gives (6,0,0); normal / dist gives (8.5,0,0)).
1568    /// (2) start 0, displacement (9,0,0), r 1: iteration 0 has dist 10,
1569    ///     gap 9, speed 9 -> dt = 1 -> t = 1 exactly. `t > 1` is false so
1570    ///     iteration 1 evaluates pos (9,0,0): dist 1, gap 0 -> Some(t = 1).
1571    ///     With `>=` the function returns None at t == 1.
1572    #[test]
1573    fn conservative_advancement_contact_point_and_t_equal_one() {
1574        let cfg = CcdConfig::default();
1575        let plane = |p: Vec3Fix| (fi(10) - p.x, v3i(-1, 0, 0));
1576
1577        let now = conservative_advancement(v3i(8, 0, 0), v3i(16, 0, 0), fi(2), plane, &cfg)
1578            .expect("immediate");
1579        assert_eq!(now.t, Fix128::ZERO);
1580        assert_eq!(now.point, v3i(10, 0, 0));
1581
1582        let edge = conservative_advancement(Vec3Fix::ZERO, v3i(9, 0, 0), fi(1), plane, &cfg)
1583            .expect("touch exactly at t = 1");
1584        assert_eq!(edge.t, Fix128::ONE);
1585        assert_eq!(edge.point, v3i(10, 0, 0));
1586    }
1587
1588    /// Kills the `t_enter > t_exit` mutants in `swept_aabb`:
1589    /// L224 `>` -> `==` / `>=` (x), L241 `>` -> `==` / `>=` (y),
1590    /// L258 `>` -> `==` / `>=` (z).
1591    ///
1592    /// Closed intervals: a corner-to-corner touch (`t_enter == t_exit`) is a
1593    /// hit, not a miss.
1594    /// - x axis: after the first slab `t_enter <= t_exit` always holds
1595    ///   (`slab_test` sorts), so equality needs zero-thickness boxes:
1596    ///   a = [0,0], b = [4,4], v 8 -> t0 = t1 = 1/2 -> Some(1/2).
1597    /// - y axis: m [-1,1]^3, v (8,8,0), target x [5,7] y [9,11]:
1598    ///   x (1/2, 1), y (1, 3/2) -> t_enter = t_exit = 1 -> Some(1).
1599    /// - z axis: same with v (8,0,8), target x [5,7] z [9,11] -> Some(1).
1600    #[test]
1601    fn swept_aabb_corner_touch_counts_as_hit() {
1602        let thin_m = AABB::new(v3i(0, -1, -1), v3i(0, 1, 1));
1603        let thin_t = AABB::new(v3i(4, -1, -1), v3i(4, 1, 1));
1604        assert_eq!(swept_aabb(&thin_m, v3i(8, 0, 0), &thin_t), Some(r(1, 2)));
1605
1606        let m = AABB::new(v3i(-1, -1, -1), v3i(1, 1, 1));
1607        assert_eq!(
1608            swept_aabb(&m, v3i(8, 8, 0), &AABB::new(v3i(5, 9, -1), v3i(7, 11, 1))),
1609            Some(Fix128::ONE)
1610        );
1611        assert_eq!(
1612            swept_aabb(&m, v3i(8, 0, 8), &AABB::new(v3i(5, -1, 9), v3i(7, 1, 11))),
1613            Some(Fix128::ONE)
1614        );
1615    }
1616
1617    /// Kills L240 `<` -> `==` (y exit) and L257 `<` -> `==` (z exit) in
1618    /// `swept_aabb`: the exit time must be lowered by a later axis.
1619    ///
1620    /// m [-1,1]^3, v (8,8,0), target x [5,7] y [1,2]:
1621    /// x (1/2, 1), y (0, 3/8) -> t_enter 1/2 > t_exit 3/8 -> None
1622    /// (the box leaves the y slab before entering the x slab).
1623    /// With the mutant t_exit stays 1 and the call returns Some(1/2).
1624    #[test]
1625    fn swept_aabb_exit_before_enter_on_later_axis_is_miss() {
1626        let m = AABB::new(v3i(-1, -1, -1), v3i(1, 1, 1));
1627        assert_eq!(
1628            swept_aabb(&m, v3i(8, 8, 0), &AABB::new(v3i(5, 1, -1), v3i(7, 2, 1))),
1629            None
1630        );
1631        assert_eq!(
1632            swept_aabb(&m, v3i(8, 0, 8), &AABB::new(v3i(5, -1, 1), v3i(7, 1, 2))),
1633            None
1634        );
1635    }
1636
1637    /// Kills L383 `+` -> `-`, L383 `*` -> `/`, L384 `*` -> `/` (overlap
1638    /// branch) and L403 / L404 `*` -> `/` (speculative branch) in
1639    /// `speculative_contact`.
1640    ///
1641    /// Overlap: A (2,0,0) r 2, B (3,0,0) r 1/2: dist 1, gap -3/2,
1642    /// point_a = (2,0,0) + (1,0,0)*2 = (4,0,0), point_b = (3,0,0) - 1/2 = (5/2,0,0).
1643    /// Speculative: A 0 r 2 vel (8,0,0), B (10,0,0) r 1/2, dt 1:
1644    /// gap 15/2, closing 8 -> predicted -1/2 -> depth 1/2,
1645    /// point_a = (2,0,0), point_b = (19/2,0,0).
1646    #[test]
1647    fn speculative_contact_points_scale_with_radii() {
1648        let ov = speculative_contact(
1649            v3i(2, 0, 0),
1650            Vec3Fix::ZERO,
1651            fi(2),
1652            v3i(3, 0, 0),
1653            Vec3Fix::ZERO,
1654            r(1, 2),
1655            fi(1),
1656        )
1657        .expect("overlap");
1658        assert_eq!(ov.depth, r(3, 2));
1659        assert_eq!(ov.point_a, v3i(4, 0, 0));
1660        assert_eq!(
1661            ov.point_b,
1662            Vec3Fix::new(r(5, 2), Fix128::ZERO, Fix128::ZERO)
1663        );
1664
1665        let sp = speculative_contact(
1666            Vec3Fix::ZERO,
1667            v3i(8, 0, 0),
1668            fi(2),
1669            v3i(10, 0, 0),
1670            Vec3Fix::ZERO,
1671            r(1, 2),
1672            fi(1),
1673        )
1674        .expect("speculative");
1675        assert_eq!(sp.depth, r(1, 2));
1676        assert_eq!(sp.point_a, v3i(2, 0, 0));
1677        assert_eq!(
1678            sp.point_b,
1679            Vec3Fix::new(r(19, 2), Fix128::ZERO, Fix128::ZERO)
1680        );
1681    }
1682
1683    /// Kills L425 `-` -> `+` (dist_a), L426 `-` -> `+` (dist_b) and
1684    /// L428 `<` -> `<=` (tie-break) in `capsule_plane_toi`.
1685    ///
1686    /// Plane y = 2 (offset 2), r 1, vel (0,-8,0).
1687    /// - a y=5, b y=7: dist_a 3 < dist_b 5 -> endpoint a -> t = (1-3)/(-8) = 1/4.
1688    ///   L425 mutant: dist_a = 7 -> picks b -> t = 1/2.
1689    /// - a y=7, b y=5: dist_b 3 -> endpoint b -> t = 1/4.
1690    ///   L426 mutant: dist_b = 7 -> picks a -> t = 1/2.
1691    /// - Tie (capsule parallel to the plane): a (0,9,-1), b (0,9,1), r 1,
1692    ///   vel (0,-16,0), plane y = 0: `dist_a < dist_b` is false, so endpoint b
1693    ///   is used -> t = 1/2, point = (0,9,1) + (0,-8,0) - (0,1,0) = (0,0,1).
1694    ///   `<=` would pick a and report (0,0,-1).
1695    #[test]
1696    fn capsule_plane_toi_offset_and_tie_break() {
1697        let n = v3i(0, 1, 0);
1698        let a_near = capsule_plane_toi(v3i(0, 5, 0), v3i(0, 7, 0), fi(1), v3i(0, -8, 0), n, fi(2))
1699            .expect("hit");
1700        assert_eq!(a_near.t, r(1, 4));
1701        assert_eq!(a_near.point, v3i(0, 2, 0));
1702
1703        let b_near = capsule_plane_toi(v3i(0, 7, 0), v3i(0, 5, 0), fi(1), v3i(0, -8, 0), n, fi(2))
1704            .expect("hit");
1705        assert_eq!(b_near.t, r(1, 4));
1706        assert_eq!(b_near.point, v3i(0, 2, 0));
1707
1708        let tie = capsule_plane_toi(
1709            v3i(0, 9, -1),
1710            v3i(0, 9, 1),
1711            fi(1),
1712            v3i(0, -16, 0),
1713            n,
1714            Fix128::ZERO,
1715        )
1716        .expect("hit");
1717        assert_eq!(tie.t, r(1, 2));
1718        assert_eq!(tie.point, v3i(0, 0, 1));
1719    }
1720
1721    /// Kills L455 `<` -> `==` (lower clamp of the segment parameter) in
1722    /// `sphere_capsule_toi`.
1723    ///
1724    /// Sphere (0,-9,0) r 1 vel (8,0,0); capsule (10,-5,0)-(10,5,0) r 1.
1725    /// t = ((-10,-4,0)·(0,10,0)) / 100 = -2/5 -> clamped to 0 -> closest
1726    /// point (10,-5,0); the sphere passes 4 below it (combined r 2) -> None.
1727    /// Without the clamp the closest point would be (10,-9,0) and the sphere
1728    /// would hit it at t = 1.
1729    #[test]
1730    fn sphere_capsule_toi_clamps_below_segment_start() {
1731        assert!(sphere_capsule_toi(
1732            v3i(0, -9, 0),
1733            fi(1),
1734            v3i(8, 0, 0),
1735            v3i(10, -5, 0),
1736            v3i(10, 5, 0),
1737            fi(1)
1738        )
1739        .is_none());
1740        // Control: level with the segment start it does hit at t = 1.
1741        let hit = sphere_capsule_toi(
1742            v3i(0, -5, 0),
1743            fi(1),
1744            v3i(8, 0, 0),
1745            v3i(10, -5, 0),
1746            v3i(10, 5, 0),
1747            fi(1),
1748        )
1749        .expect("hit");
1750        assert_eq!(hit.t, Fix128::ONE);
1751    }
1752
1753    /// Kills L485 `<` -> `>` (support x), L490 `<` -> `<=` (support y) and
1754    /// L495 `<` -> `>` (support z) in `aabb_plane_toi`.
1755    ///
1756    /// AABB [1,3]^3.
1757    /// - Plane x = 10 with normal -x (offset -10), vel (8,0,0): support
1758    ///   (max.x, min.y, min.z) = (3,1,1), dist = -3 + 10 = 7, vel_toward -8,
1759    ///   t = 7/8, point = (3,1,1) + (7,0,0) = (10,1,1).
1760    ///   L485 mutant picks min.x -> dist 9 -> t 9/8 -> None.
1761    ///   L490 mutant picks max.y (normal.y == 0) -> point (10,3,1).
1762    /// - Plane x = -5 with normal +x (offset -5), vel (-8,0,0): support
1763    ///   min.x = 1, dist 6, t = 3/4 (mutant: max.x 3 -> dist 8 -> t 1).
1764    /// - Plane z = 10 with normal -z (offset -10), vel (0,0,8): support
1765    ///   max.z = 3, t = 7/8, point (1,1,10) (L495 mutant -> None).
1766    #[test]
1767    fn aabb_plane_toi_support_vertex_follows_normal_sign() {
1768        let bx = AABB::new(v3i(1, 1, 1), v3i(3, 3, 3));
1769
1770        let neg_x = aabb_plane_toi(&bx, v3i(8, 0, 0), v3i(-1, 0, 0), fi(-10)).expect("hit");
1771        assert_eq!(neg_x.t, r(7, 8));
1772        assert_eq!(neg_x.point, v3i(10, 1, 1));
1773
1774        let pos_x = aabb_plane_toi(&bx, v3i(-8, 0, 0), v3i(1, 0, 0), fi(-5)).expect("hit");
1775        assert_eq!(pos_x.t, r(3, 4));
1776        assert_eq!(pos_x.point, v3i(-5, 1, 1));
1777
1778        let neg_z = aabb_plane_toi(&bx, v3i(0, 0, 8), v3i(0, 0, -1), fi(-10)).expect("hit");
1779        assert_eq!(neg_z.t, r(7, 8));
1780        assert_eq!(neg_z.point, v3i(1, 1, 10));
1781    }
1782
1783    /// Kills L506 `>` -> `==` / `>=` (resting contact while separating),
1784    /// L526 `&&` -> `||` (t > 1 must be a miss) and L527 `+` -> `-`,
1785    /// `*` -> `/` (contact point) in `aabb_plane_toi`.
1786    ///
1787    /// - AABB resting on y = 0 (min.y = 0) moving away (0,8,0): dist == 0 is
1788    ///   not "moving away from the plane", it is the penetrating branch ->
1789    ///   Some(t = 0, point = support (1,0,1)).
1790    /// - AABB [1,3]^3, vel (0,-1/2,0): t = 1 / (1/2) = 2 > 1 -> None.
1791    /// - AABB [1,3]^3, vel (0,-8,0): t = 1/8,
1792    ///   point = (1,1,1) + (0,-8,0) * 1/8 = (1,0,1)
1793    ///   (`-` gives (1,2,1); velocity / t gives (1,-63,1)).
1794    #[test]
1795    fn aabb_plane_toi_resting_contact_and_contact_point() {
1796        let n = v3i(0, 1, 0);
1797        let resting = aabb_plane_toi(
1798            &AABB::new(v3i(1, 0, 1), v3i(3, 2, 3)),
1799            v3i(0, 8, 0),
1800            n,
1801            Fix128::ZERO,
1802        )
1803        .expect("resting contact");
1804        assert_eq!(resting.t, Fix128::ZERO);
1805        assert_eq!(resting.point, v3i(1, 0, 1));
1806
1807        let bx = AABB::new(v3i(1, 1, 1), v3i(3, 3, 3));
1808        assert!(aabb_plane_toi(
1809            &bx,
1810            Vec3Fix::new(Fix128::ZERO, r(-1, 2), Fix128::ZERO),
1811            n,
1812            Fix128::ZERO
1813        )
1814        .is_none());
1815
1816        let hit = aabb_plane_toi(&bx, v3i(0, -8, 0), n, Fix128::ZERO).expect("hit");
1817        assert_eq!(hit.t, r(1, 8));
1818        assert_eq!(hit.point, v3i(1, 0, 1));
1819    }
1820
1821    // ---- coverage: closed-form boundaries (2026-10-07) -----------------
1822
1823    /// `sphere_plane_toi` with the sphere wholly behind the plane
1824    /// (dist = -5 < -r = -1) and no velocity: it never reaches the plane,
1825    /// so there is no impact in this step.
1826    #[test]
1827    fn sphere_plane_toi_behind_the_plane_without_velocity_is_a_miss() {
1828        assert!(
1829            sphere_plane_toi(v3i(0, -5, 0), fi(1), Vec3Fix::ZERO, v3i(0, 1, 0), fi(0)).is_none()
1830        );
1831        // control: the same sphere moving up at 8 crosses y = -1 (near
1832        // surface at y = -4) after t = (-1 - (-5)) / 8 = 1/2
1833        let hit = sphere_plane_toi(v3i(0, -5, 0), fi(1), v3i(0, 8, 0), v3i(0, 1, 0), fi(0))
1834            .expect("rising sphere reaches the plane");
1835        assert_eq!(hit.t, r(1, 2));
1836    }
1837
1838    /// `swept_aabb` slab exits.
1839    ///
1840    /// - x: moving [2e6, 2e6 + 1] with vx = 1 against target [0, 1]: the
1841    ///   x slab is t in [(0 - (2e6 + 1)) / 1, (1 - 2e6) / 1], which ends
1842    ///   before the initial window (-1e6) begins -> None.
1843    /// - z: vz = 0 and z ranges [0, 1] vs [5, 6] never overlap -> None,
1844    ///   although x and y alone would hit.
1845    /// - control: the same boxes with target z [0, 1] enter at
1846    ///   t = (2 - 1) / 4 = 1/4 for vx = 4.
1847    #[test]
1848    fn swept_aabb_x_window_and_static_z_separation_are_misses() {
1849        let far = AABB::new(v3i(2_000_000, 0, 0), v3i(2_000_001, 1, 1));
1850        let unit = AABB::new(v3i(0, 0, 0), v3i(1, 1, 1));
1851        assert!(swept_aabb(&far, v3i(1, 0, 0), &unit).is_none());
1852
1853        let mover = AABB::new(v3i(0, 0, 0), v3i(1, 1, 1));
1854        let off_z = AABB::new(v3i(2, 0, 5), v3i(3, 1, 6));
1855        assert!(swept_aabb(&mover, v3i(4, 0, 0), &off_z).is_none());
1856        let in_z = AABB::new(v3i(2, 0, 0), v3i(3, 1, 1));
1857        assert_eq!(swept_aabb(&mover, v3i(4, 0, 0), &in_z), Some(r(1, 4)));
1858    }
1859
1860    /// `adaptive_toi_substeps` on a collision course: the count is the
1861    /// smallest n with n * step >= v_max * dt, where v_max is the L-inf
1862    /// norm of the faster velocity and step = min(r_min / 2, 1/10).
1863    ///
1864    /// A (r = 1/8) at the origin moving (1, 2, 8), B (r = 1) at (0, 0, 2)
1865    /// at rest, dt = 1/8: v_max = 8 (the z component), travel = 1,
1866    /// step = (1/8) / 2 = 1/16, n = 16. Swapping the roles (B moving, the
1867    /// small radius second) gives the same 16.
1868    ///
1869    /// `solver_tgs::adaptive_substeps_for_ccd` (the formula this oracle
1870    /// pins) is itself `std`-gated; see [`adaptive_toi_substeps`]'s
1871    /// `# no_std` doc for the documented `max_substeps` fallback this test
1872    /// does not apply to.
1873    #[cfg(feature = "std")]
1874    #[test]
1875    fn adaptive_toi_substeps_uses_the_z_dominated_speed_and_the_smaller_radius() {
1876        let n = adaptive_toi_substeps(
1877            Vec3Fix::ZERO,
1878            v3i(1, 2, 8),
1879            r(1, 8),
1880            v3i(0, 0, 2),
1881            Vec3Fix::ZERO,
1882            fi(1),
1883            r(1, 8),
1884            100,
1885        );
1886        assert_eq!(n, 16);
1887        let swapped = adaptive_toi_substeps(
1888            v3i(0, 0, 2),
1889            Vec3Fix::ZERO,
1890            fi(1),
1891            Vec3Fix::ZERO,
1892            v3i(1, 2, 8),
1893            r(1, 8),
1894            r(1, 8),
1895            100,
1896        );
1897        assert_eq!(swapped, 16);
1898    }
1899
1900    fn sweep_world() -> (crate::solver::PhysicsWorld, usize) {
1901        use crate::solver::{PhysicsConfig, PhysicsWorld, RigidBody};
1902        let mut w = PhysicsWorld::new(PhysicsConfig::default());
1903        let a = w.add_body_with_radius(RigidBody::new(Vec3Fix::ZERO, fi(1)), r(1, 2));
1904        (w, a)
1905    }
1906
1907    /// `PhysicsWorld::time_of_impact` against a static sphere.
1908    ///
1909    /// Mover r = 1/2 at the origin, obstacle r = 1/4 at (3, 0, 0),
1910    /// velocity (3, 0, 0), dt = 3/2: travel 9/2, gap 3 - 1/2 - 1/4 = 9/4,
1911    /// t = (9/4) / (9/2) = 1/2, contact on the obstacle at x = 11/4.
1912    #[test]
1913    fn world_time_of_impact_is_gap_over_travel() {
1914        use crate::solver::RigidBody;
1915        let (mut w, a) = sweep_world();
1916        w.add_body_with_radius(RigidBody::new_static(v3i(3, 0, 0)), r(1, 4));
1917        let f = crate::shape_raycast::RayFilter::default();
1918        let hit = w.time_of_impact(a, v3i(3, 0, 0), r(3, 2), &f).expect("hit");
1919        let tol = r(1, 1_000_000);
1920        assert!((hit.t - r(1, 2)).abs() < tol, "t = {:?}", hit.t);
1921        assert!(
1922            (hit.point.x - r(11, 4)).abs() < tol,
1923            "x = {:?}",
1924            hit.point.x
1925        );
1926        assert!(hit.point.y.abs() < tol && hit.point.z.abs() < tol);
1927
1928        // moving away: nothing ahead within the travel
1929        assert_eq!(w.time_of_impact(a, v3i(-3, 0, 0), r(3, 2), &f), None);
1930        // documented `None` inputs: dt = 0, dt < 0, zero displacement,
1931        // body index out of range
1932        assert_eq!(w.time_of_impact(a, v3i(3, 0, 0), fi(0), &f), None);
1933        assert_eq!(w.time_of_impact(a, v3i(3, 0, 0), fi(-1), &f), None);
1934        assert_eq!(w.time_of_impact(a, Vec3Fix::ZERO, fi(1), &f), None);
1935        assert_eq!(w.time_of_impact(99, v3i(3, 0, 0), fi(1), &f), None);
1936    }
1937
1938    /// A body without a collision radius collides with nothing.
1939    #[test]
1940    fn world_time_of_impact_of_a_body_without_radius_is_none() {
1941        use crate::solver::{PhysicsConfig, PhysicsWorld, RigidBody};
1942        let mut w = PhysicsWorld::new(PhysicsConfig::default());
1943        let bare = w.add_body(RigidBody::new(Vec3Fix::ZERO, fi(1)));
1944        w.add_body_with_radius(RigidBody::new_static(v3i(2, 0, 0)), fi(1));
1945        let f = crate::shape_raycast::RayFilter::default();
1946        assert_eq!(w.time_of_impact(bare, v3i(4, 0, 0), fi(1), &f), None);
1947    }
1948
1949    /// Contact exactly at the end of the step: gap 3 - 1/2 - 1/2 = 2 and
1950    /// travel |(2, 0, 0)| * 1 = 2, so t = 1 (never past 1).
1951    #[test]
1952    fn world_time_of_impact_touching_at_the_end_of_the_step_is_one() {
1953        use crate::solver::RigidBody;
1954        let (mut w, a) = sweep_world();
1955        w.add_body_with_radius(RigidBody::new_static(v3i(3, 0, 0)), r(1, 2));
1956        let f = crate::shape_raycast::RayFilter::default();
1957        let hit = w.time_of_impact(a, v3i(2, 0, 0), fi(1), &f).expect("hit");
1958        assert!(hit.t <= fi(1));
1959        assert!((fi(1) - hit.t) < r(1, 1_000_000), "t = {:?}", hit.t);
1960        // half the step is not enough: gap 2 > travel 1
1961        assert_eq!(w.time_of_impact(a, v3i(2, 0, 0), r(1, 2), &f), None);
1962    }
1963
1964    /// Already overlapping (centres 1 apart < 1/2 + 3/4): t = 0 and the
1965    /// point is the mover's own centre (the documented overlap report), in
1966    /// either direction of motion.
1967    #[test]
1968    fn world_time_of_impact_when_already_overlapping_is_zero_at_the_centre() {
1969        use crate::solver::RigidBody;
1970        let (mut w, a) = sweep_world();
1971        w.add_body_with_radius(RigidBody::new_static(v3i(1, 0, 0)), r(3, 4));
1972        let f = crate::shape_raycast::RayFilter::default();
1973        for vel in [v3i(1, 0, 0), v3i(-1, 0, 0)] {
1974            let hit = w.time_of_impact(a, vel, fi(1), &f).expect("overlap");
1975            assert_eq!(hit.t, Fix128::ZERO);
1976            assert_eq!(hit.point, Vec3Fix::ZERO);
1977        }
1978    }
1979}