1use 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
140const TRACE_TOLERANCE: Fix128 = Fix128 {
142 hi: 0,
143 lo: 0x0000_0001_0000_0000,
144};
145
146const NEAR_TANGENT: Fix128 = Fix128 {
150 hi: 0,
151 lo: 0x0400_0000_0000_0000,
152};
153
154const TRACE_MAX_STEPS: usize = 1024;
157
158#[cfg(feature = "std")]
160const SDF_MAX_STEPS: usize = 4096;
161
162const GJK_RELATIVE: Fix128 = Fix128 {
164 hi: 0,
165 lo: 0x0000_0000_0100_0000,
166};
167
168const GJK_INTERSECT: Fix128 = Fix128 { hi: 0, lo: 1 };
174
175const GJK_MAX_ITERATIONS: usize = 64;
176
177const CURVE_COARSE: Fix128 = Fix128 {
180 hi: 0,
181 lo: 0x0001_0000_0000_0000,
182};
183
184const GOLDEN_STEPS: usize = 64;
186
187const CURVE_MAX_NODES: usize = 256;
189
190const CELL_MAX_NODES: usize = 512;
192
193const TREE_MAX_NODES: usize = 1024;
195
196#[cfg(feature = "std")]
198const SDF_BOX_DEPTH: u32 = 10;
199
200#[derive(Clone, Copy, Debug, PartialEq, Eq)]
206#[non_exhaustive]
207pub struct WorldShapeHit {
208 pub t: Fix128,
211 pub point: Vec3Fix,
214 pub normal: Vec3Fix,
217 pub target: RayTarget,
219 pub body: Option<usize>,
221}
222
223#[derive(Clone, Copy, Debug)]
225pub(crate) struct Penetration {
226 pub(crate) target: RayTarget,
228 pub(crate) depth: Fix128,
230 pub(crate) normal: Vec3Fix,
233}
234
235#[derive(Clone, Copy, Debug)]
241enum Dist {
242 Inside,
244 Outside {
247 dist: Fix128,
248 point: Vec3Fix,
249 normal: Vec3Fix,
250 },
251 AtLeast(Fix128),
253}
254
255impl Dist {
256 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 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 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 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
312fn 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
343const FINE_LENGTH_SQUARED: Fix128 = Fix128 {
347 hi: 0,
348 lo: 0x0000_0001_0000_0000,
349};
350
351const FINE_LENGTH: Fix128 = Fix128 {
353 hi: 0,
354 lo: 0x0001_0000_0000_0000,
355};
356
357fn scaled_up(v: Vec3Fix) -> (Vec3Fix, u32) {
360 let raw = |f: Fix128| ((f.hi as i128) << 64) | (f.lo as i128);
361 let (x, y, z) = (raw(v.x), raw(v.y), raw(v.z));
362 let m = x.unsigned_abs().max(y.unsigned_abs()).max(z.unsigned_abs());
363 if m == 0 {
364 return (Vec3Fix::ZERO, 0);
365 }
366 let msb = 127 - m.leading_zeros();
368 if msb >= 64 {
369 return (v, 0);
370 }
371 let k = 64 - msb;
372 let up = |r: i128| {
373 let s = r << k;
374 Fix128::from_raw((s >> 64) as i64, s as u64)
375 };
376 (Vec3Fix::new(up(x), up(y), up(z)), k)
377}
378
379fn fine_length(v: Vec3Fix) -> Option<Fix128> {
383 if v.length_squared() >= FINE_LENGTH_SQUARED {
384 let len = v.length();
385 return (!len.is_zero()).then_some(len);
386 }
387 let (s, k) = scaled_up(v);
388 let len = s.length();
389 if len.is_zero() {
390 return None;
391 }
392 let back = (((len.hi as i128) << 64) | (len.lo as i128)) >> k;
393 Some(Fix128::from_raw((back >> 64) as i64, back as u64))
394}
395
396fn clamp(x: Fix128, lo: Fix128, hi: Fix128) -> Fix128 {
397 max_fix(lo, min_fix(x, hi))
398}
399
400fn closest_on_segment(a: Vec3Fix, b: Vec3Fix, p: Vec3Fix) -> Vec3Fix {
402 let ab = b - a;
403 let len2 = ab.length_squared();
404 if len2.is_zero() {
405 return a;
406 }
407 a + ab * clamp((p - a).dot(ab) / len2, Fix128::ZERO, Fix128::ONE)
408}
409
410fn closest_on_aabb(aabb: &AABB, p: Vec3Fix) -> Vec3Fix {
412 Vec3Fix::new(
413 clamp(p.x, aabb.min.x, aabb.max.x),
414 clamp(p.y, aabb.min.y, aabb.max.y),
415 clamp(p.z, aabb.min.z, aabb.max.z),
416 )
417}
418
419struct PointSupport(Vec3Fix);
421
422impl Support for PointSupport {
423 fn support(&self, _direction: Vec3Fix) -> Vec3Fix {
424 self.0
425 }
426}
427
428struct SegmentSupport(Vec3Fix, Vec3Fix);
430
431impl Support for SegmentSupport {
432 fn support(&self, direction: Vec3Fix) -> Vec3Fix {
433 if self.1.dot(direction) > self.0.dot(direction) {
434 self.1
435 } else {
436 self.0
437 }
438 }
439}
440
441struct TriangleSupport<'a>(&'a Triangle);
443
444impl Support for TriangleSupport<'_> {
445 fn support(&self, direction: Vec3Fix) -> Vec3Fix {
446 let t = self.0;
447 let (d0, d1, d2) = (
448 t.v0.dot(direction),
449 t.v1.dot(direction),
450 t.v2.dot(direction),
451 );
452 if d1 > d0 && d1 >= d2 {
453 t.v1
454 } else if d2 > d0 && d2 > d1 {
455 t.v2
456 } else {
457 t.v0
458 }
459 }
460}
461
462struct ChildSupport<'a> {
464 child: &'a CompoundChild,
465 position: Vec3Fix,
466 rotation: QuatFix,
467}
468
469struct RoundSupport<'a>(&'a PosedShape);
481
482impl Support for RoundSupport<'_> {
483 fn support(&self, direction: Vec3Fix) -> Vec3Fix {
484 self.0.support(scaled_up(direction).0)
485 }
486}
487
488fn is_round(posed: &PosedShape) -> bool {
490 matches!(posed.shape, Shape::Ellipsoid { .. })
491}
492
493impl Support for ChildSupport<'_> {
494 fn support(&self, direction: Vec3Fix) -> Vec3Fix {
495 self.child
496 .support_world(direction, self.position, self.rotation)
497 }
498}
499
500#[derive(Clone, Copy)]
503struct Vertex {
504 w: Vec3Fix,
505 a: Vec3Fix,
506 b: Vec3Fix,
507}
508
509fn vertex<A: Support, B: Support>(a: &A, b: &B, direction: Vec3Fix) -> Vertex {
510 let pa = a.support(direction);
511 let pb = b.support(-direction);
512 Vertex {
513 w: pa - pb,
514 a: pa,
515 b: pb,
516 }
517}
518
519type Nearest = (Vec<Vertex>, Vec<Fix128>, bool);
522
523fn closest_on_simplex(s: &[Vertex]) -> Option<Nearest> {
532 let k = simplex_scale(s);
533 if k == 0 {
534 return closest_on_scaled_simplex(s, 0);
535 }
536 let up: Vec<Vertex> = s
537 .iter()
538 .map(|v| Vertex {
539 w: shift_vec(v.w, k as i32),
540 ..*v
541 })
542 .collect();
543 let (sub, lambda, face) = closest_on_scaled_simplex(&up, k)?;
544 let sub = sub
545 .into_iter()
546 .map(|v| Vertex {
547 w: shift_vec(v.w, -(k as i32)),
548 ..v
549 })
550 .collect();
551 Some((sub, lambda, face))
552}
553
554fn closest_on_scaled_simplex(s: &[Vertex], k: u32) -> Option<Nearest> {
556 match s.len() {
557 1 => Some((s.to_vec(), vec![Fix128::ONE], false)),
558 2 => {
559 let (sub, lambda) = closest_on_edge(s[0], s[1]);
560 Some((sub, lambda, false))
561 }
562 3 => Some(closest_on_triangle(s[0], s[1], s[2])),
563 _ => closest_on_tetrahedron(s[0], s[1], s[2], s[3], k),
564 }
565}
566
567const SMALL_SIMPLEX_BITS: u32 = 56;
570
571fn simplex_scale(s: &[Vertex]) -> u32 {
575 let raw = |f: Fix128| ((f.hi as i128) << 64) | (f.lo as i128);
576 let mag = |v: Vec3Fix| {
577 raw(v.x)
578 .unsigned_abs()
579 .max(raw(v.y).unsigned_abs())
580 .max(raw(v.z).unsigned_abs())
581 };
582 let Some(first) = s.first() else {
583 return 0;
584 };
585 let edge = s.iter().map(|v| mag(v.w - first.w)).max().unwrap_or(0);
586 if edge == 0 {
587 return 0;
588 }
589 let msb = 127 - edge.leading_zeros();
591 if msb >= SMALL_SIMPLEX_BITS {
592 return 0;
593 }
594 let far = s.iter().map(|v| mag(v.w)).max().unwrap_or(0);
595 let far_msb = 127 - far.leading_zeros();
596 (64 - msb).min(88u32.saturating_sub(far_msb))
598}
599
600fn shift_vec(v: Vec3Fix, k: i32) -> Vec3Fix {
602 let raw = |f: Fix128| ((f.hi as i128) << 64) | (f.lo as i128);
603 let sh = |f: Fix128| {
604 let r = raw(f);
605 let s = if k >= 0 { r << k } else { r >> (-k) };
606 Fix128::from_raw((s >> 64) as i64, s as u64)
607 };
608 Vec3Fix::new(sh(v.x), sh(v.y), sh(v.z))
609}
610
611fn closest_on_edge(a: Vertex, b: Vertex) -> (Vec<Vertex>, Vec<Fix128>) {
612 let ab = b.w - a.w;
613 let len2 = ab.length_squared();
614 let t = if len2.is_zero() {
615 Fix128::ZERO
616 } else {
617 -a.w.dot(ab) / len2
618 };
619 if t <= Fix128::ZERO {
620 (vec![a], vec![Fix128::ONE])
621 } else if t >= Fix128::ONE {
622 (vec![b], vec![Fix128::ONE])
623 } else {
624 (vec![a, b], vec![Fix128::ONE - t, t])
625 }
626}
627
628const NO_SIZE: i32 = i32::MIN / 4;
630
631fn log2_size(v: Vec3Fix) -> i32 {
633 let raw = |f: Fix128| ((f.hi as i128) << 64) | (f.lo as i128);
634 let m = raw(v.x)
635 .unsigned_abs()
636 .max(raw(v.y).unsigned_abs())
637 .max(raw(v.z).unsigned_abs());
638 if m == 0 {
639 return NO_SIZE;
640 }
641 63 - m.leading_zeros() as i32
643}
644
645fn barycentric_is_precise(a: Vertex, b: Vertex, c: Vertex, point: Vec3Fix) -> bool {
663 let ab = b.w - a.w;
664 let ac = c.w - a.w;
665 let n = log2_size(ab.cross(ac));
666 let near = log2_size(point);
667 if n == NO_SIZE || near == NO_SIZE {
668 return false;
669 }
670 let l = log2_size(ab).max(log2_size(ac)).max(log2_size(c.w - b.w));
671 let p = log2_size(a.w).max(log2_size(b.w)).max(log2_size(c.w));
672 -62 + 2 * l + p - 2 * n - near <= -32
674}
675
676fn closest_on_triangle(a: Vertex, b: Vertex, c: Vertex) -> Nearest {
681 let (sub, lambda) = closest_on_triangle_barycentric(a, b, c);
682 if barycentric_is_precise(a, b, c, weighted(&(sub.clone(), lambda.clone()))) {
683 return (sub, lambda, false);
684 }
685 let (sub, lambda) = closest_on_triangle_by_area(a, b, c);
686 let face = sub.len() == 3;
687 (sub, lambda, face)
688}
689
690fn closest_on_triangle_by_area(a: Vertex, b: Vertex, c: Vertex) -> (Vec<Vertex>, Vec<Fix128>) {
707 let ab = b.w - a.w;
708 let ac = c.w - a.w;
709 let bc = c.w - b.w;
710 let n = scaled_up(ab.cross(ac)).0;
711 let area_a = n.dot(bc.cross(-b.w));
713 let area_b = n.dot((-ac).cross(-c.w));
714 let area_c = n.dot(ab.cross(-a.w));
715 let sum = area_a + area_b + area_c;
716 if n.length_squared().is_zero() || sum <= Fix128::ZERO {
717 let mut best = closest_on_edge(a, b);
719 for cand in [closest_on_edge(b, c), closest_on_edge(a, c)] {
720 if shorter(weighted(&cand), weighted(&best)) {
721 best = cand;
722 }
723 }
724 return best;
725 }
726 if !area_a.is_negative() && !area_b.is_negative() && !area_c.is_negative() {
727 let (wb, wc) = (area_b / sum, area_c / sum);
728 return (vec![a, b, c], vec![Fix128::ONE - wb - wc, wb, wc]);
729 }
730 let mut best: Option<(Vec<Vertex>, Vec<Fix128>)> = None;
731 for (area, p, q) in [(area_c, a, b), (area_a, b, c), (area_b, a, c)] {
732 if !area.is_negative() {
733 continue;
734 }
735 let cand = closest_on_edge(p, q);
736 if best
737 .as_ref()
738 .is_none_or(|b| shorter(weighted(&cand), weighted(b)))
739 {
740 best = Some(cand);
741 }
742 }
743 best.unwrap_or_else(|| (vec![a], vec![Fix128::ONE]))
745}
746
747fn closest_on_triangle_barycentric(a: Vertex, b: Vertex, c: Vertex) -> (Vec<Vertex>, Vec<Fix128>) {
749 let ab = b.w - a.w;
750 let ac = c.w - a.w;
751 let ap = -a.w;
752 let d1 = ab.dot(ap);
753 let d2 = ac.dot(ap);
754 if d1 <= Fix128::ZERO && d2 <= Fix128::ZERO {
755 return (vec![a], vec![Fix128::ONE]);
756 }
757 let bp = -b.w;
758 let d3 = ab.dot(bp);
759 let d4 = ac.dot(bp);
760 if d3 >= Fix128::ZERO && d4 <= d3 {
761 return (vec![b], vec![Fix128::ONE]);
762 }
763 let vc = d1 * d4 - d3 * d2;
764 if vc <= Fix128::ZERO && d1 >= Fix128::ZERO && d3 <= Fix128::ZERO {
765 let v = d1 / (d1 - d3);
766 return (vec![a, b], vec![Fix128::ONE - v, v]);
767 }
768 let cp = -c.w;
769 let d5 = ab.dot(cp);
770 let d6 = ac.dot(cp);
771 if d6 >= Fix128::ZERO && d5 <= d6 {
772 return (vec![c], vec![Fix128::ONE]);
773 }
774 let vb = d5 * d2 - d1 * d6;
775 if vb <= Fix128::ZERO && d2 >= Fix128::ZERO && d6 <= Fix128::ZERO {
776 let w = d2 / (d2 - d6);
777 return (vec![a, c], vec![Fix128::ONE - w, w]);
778 }
779 let va = d3 * d6 - d5 * d4;
780 if va <= Fix128::ZERO && d4 - d3 >= Fix128::ZERO && d5 - d6 >= Fix128::ZERO {
781 let w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
782 return (vec![b, c], vec![Fix128::ONE - w, w]);
783 }
784 let denom = va + vb + vc;
785 if denom <= Fix128::ZERO {
786 let mut best = closest_on_edge(a, b);
788 for cand in [closest_on_edge(b, c), closest_on_edge(a, c)] {
789 if weighted(&cand).length_squared() < weighted(&best).length_squared() {
790 best = cand;
791 }
792 }
793 return best;
794 }
795 let v = vb / denom;
796 let w = vc / denom;
797 (vec![a, b, c], vec![Fix128::ONE - v - w, v, w])
798}
799
800fn closest_on_tetrahedron(a: Vertex, b: Vertex, c: Vertex, d: Vertex, k: u32) -> Option<Nearest> {
814 let faces = [(a, b, c, d), (a, c, d, b), (a, d, b, c), (b, d, c, a)];
815 let flat_height = if k >= 32 {
816 Fix128::from_raw(1i64 << (k - 32), 0)
817 } else {
818 Fix128::from_raw(0, 1u64 << (32 + k))
819 };
820 let flat = faces.iter().any(|&(p, q, r, opposite)| {
821 let n = (q.w - p.w).cross(r.w - p.w);
822 n.dot(opposite.w - p.w).abs() <= n.length() * flat_height
823 });
824 let mut best: Option<Nearest> = None;
825 let mut inside = true;
826 for (p, q, r, opposite) in faces {
827 let n = (q.w - p.w).cross(r.w - p.w);
828 let origin_side = n.dot(-p.w);
829 let other_side = n.dot(opposite.w - p.w);
830 let outside = other_side.is_zero()
833 || (!origin_side.is_zero() && origin_side.is_negative() != other_side.is_negative());
834 if outside {
835 inside = false;
836 }
837 if outside || flat {
838 let cand = closest_on_triangle(p, q, r);
839 if best
840 .as_ref()
841 .is_none_or(|b| shorter(nearest_point(&cand), nearest_point(b)))
842 {
843 best = Some(cand);
844 }
845 }
846 }
847 if inside && !flat {
848 None
849 } else {
850 best
851 }
852}
853
854fn shorter(x: Vec3Fix, y: Vec3Fix) -> bool {
861 let (xx, yy) = (x.length_squared(), y.length_squared());
862 if xx >= FINE_LENGTH_SQUARED && yy >= FINE_LENGTH_SQUARED {
863 return xx < yy;
864 }
865 let len = |v: Vec3Fix| fine_length(v).unwrap_or(Fix128::ZERO);
866 len(x) < len(y)
867}
868
869fn nearest_point(n: &Nearest) -> Vec3Fix {
871 n.0.iter()
872 .zip(&n.1)
873 .fold(Vec3Fix::ZERO, |acc, (v, &l)| acc + v.w * l)
874}
875
876fn nearest_on_affine_hull(s: &[Vertex]) -> Vec3Fix {
882 match *s {
883 [a] => a.w,
884 [a, b] => {
885 let e = scaled_up(b.w - a.w).0;
886 let ee = e.length_squared();
887 if ee.is_zero() {
888 return a.w;
889 }
890 a.w - e * (a.w.dot(e) / ee)
891 }
892 [a, b, c, ..] => {
893 let n = scaled_up((b.w - a.w).cross(c.w - a.w)).0;
894 let nn = n.length_squared();
895 if nn.is_zero() {
896 return a.w;
897 }
898 n * (a.w.dot(n) / nn)
899 }
900 [] => Vec3Fix::ZERO,
901 }
902}
903
904fn weighted(s: &(Vec<Vertex>, Vec<Fix128>)) -> Vec3Fix {
905 s.0.iter()
906 .zip(&s.1)
907 .fold(Vec3Fix::ZERO, |acc, (v, &l)| acc + v.w * l)
908}
909
910fn gjk_distance<A: Support, B: Support>(a: &A, b: &B) -> Option<(Fix128, Vec3Fix, Vec3Fix)> {
973 gjk_bounds(a, b).map(|g| (g.dist, g.pa, g.pb))
974}
975
976struct GjkBounds {
978 dist: Fix128,
980 pa: Vec3Fix,
982 pb: Vec3Fix,
983 lower: Fix128,
985 face: bool,
988}
989
990fn gjk_bounds<A: Support, B: Support>(a: &A, b: &B) -> Option<GjkBounds> {
992 let mut simplex = vec![vertex(a, b, Vec3Fix::UNIT_X)];
993 let mut weights = vec![Fix128::ONE];
994 let mut v = simplex[0].w;
995 let mut on_face = false;
998 let mut lower = Fix128::ZERO;
1000 for _ in 0..GJK_MAX_ITERATIONS {
1001 let vv = v.length_squared();
1002 if vv <= GJK_INTERSECT {
1003 return None;
1004 }
1005 let fine = vv < FINE_LENGTH_SQUARED;
1009 let u = if fine { scaled_up(v).0 } else { v };
1010 let w = vertex(a, b, -u);
1011 if let Some(len) = fine_length(u) {
1012 let support = u.dot(w.w) / len;
1013 if support > lower {
1014 lower = support;
1015 }
1016 }
1017 let converged = if fine {
1018 u.dot(v - w.w) <= u.dot(v) * GJK_RELATIVE
1019 } else {
1020 vv - v.dot(w.w) <= vv * GJK_RELATIVE
1021 };
1022 if converged {
1023 break;
1024 }
1025 if simplex.iter().any(|s| s.w == w.w) {
1026 break;
1027 }
1028 let mut grown = simplex.clone();
1029 grown.push(w);
1030 let (sub, lambda, face) = closest_on_simplex(&grown)?;
1031 let next = if fine || face {
1032 nearest_on_affine_hull(&sub)
1033 } else {
1034 weighted(&(sub.clone(), lambda.clone()))
1035 };
1036 let no_progress = if fine {
1037 let len = |x: Vec3Fix| fine_length(x).unwrap_or(Fix128::ZERO);
1039 len(next) >= len(v)
1040 } else {
1041 next.length_squared() >= vv
1042 };
1043 if no_progress {
1044 break;
1046 }
1047 on_face = face;
1048 simplex = sub;
1049 weights = lambda;
1050 v = next;
1051 }
1052 let pa = simplex
1053 .iter()
1054 .zip(&weights)
1055 .fold(Vec3Fix::ZERO, |acc, (s, &l)| acc + s.a * l);
1056 let pb = simplex
1057 .iter()
1058 .zip(&weights)
1059 .fold(Vec3Fix::ZERO, |acc, (s, &l)| acc + s.b * l);
1060 let len = fine_length(v)?;
1061 if on_face || v.length_squared() < FINE_LENGTH_SQUARED {
1062 return Some(GjkBounds {
1066 dist: len,
1067 pa: pb + v,
1068 pb,
1069 lower: min_fix(lower, len),
1070 face: on_face,
1071 });
1072 }
1073 Some(GjkBounds {
1074 dist: len,
1075 pa,
1076 pb,
1077 lower: min_fix(lower, len),
1078 face: false,
1079 })
1080}
1081
1082fn convex_dist<S: Support>(a: Vec3Fix, b: Vec3Fix, solid: &S) -> Dist {
1084 convex_bounds(a, b, solid, Fix128::ZERO).0
1085}
1086
1087fn convex_bounds<S: Support>(a: Vec3Fix, b: Vec3Fix, solid: &S, slack: Fix128) -> (Dist, Bound) {
1091 let found = if a == b {
1092 gjk_bounds(&PointSupport(a), solid)
1093 } else {
1094 gjk_bounds(&SegmentSupport(a, b), solid)
1095 };
1096 match found {
1097 None => (
1098 Dist::Inside,
1099 Bound {
1100 lower: Fix128::ZERO,
1101 step_lower: false,
1102 },
1103 ),
1104 Some(g) => (
1105 Dist::Outside {
1106 dist: g.dist,
1107 point: g.pb,
1108 normal: gjk_normal(g.pa - g.pb, g.dist),
1109 },
1110 Bound {
1111 lower: max_fix(g.lower - slack, Fix128::ZERO),
1112 step_lower: g.face,
1113 },
1114 ),
1115 }
1116}
1117
1118#[derive(Clone, Copy)]
1120struct Bound {
1121 lower: Fix128,
1123 step_lower: bool,
1126}
1127
1128impl Bound {
1129 fn exact(d: &Dist) -> Self {
1131 let lower = match *d {
1132 Dist::Outside { dist, .. } => dist,
1133 _ => Fix128::ZERO,
1134 };
1135 Self {
1136 lower,
1137 step_lower: false,
1138 }
1139 }
1140}
1141
1142fn gjk_normal(diff: Vec3Fix, dist: Fix128) -> Vec3Fix {
1146 if diff.length_squared() >= FINE_LENGTH_SQUARED {
1147 return diff / dist;
1148 }
1149 let s = scaled_up(diff).0;
1150 s / s.length()
1151}
1152
1153fn segment_segment(
1156 p0: Vec3Fix,
1157 p1: Vec3Fix,
1158 q0: Vec3Fix,
1159 q1: Vec3Fix,
1160) -> Option<(Fix128, Vec3Fix, Vec3Fix)> {
1161 if p0 == p1 && q0 == q1 {
1162 let (_, d) = (p0 - q0).normalize_with_length();
1163 return (!d.is_zero()).then_some((d, p0, q0));
1164 }
1165 if p0 == p1 {
1166 let q = closest_on_segment(q0, q1, p0);
1167 let d = (p0 - q).length();
1168 return (!d.is_zero()).then_some((d, p0, q));
1169 }
1170 if q0 == q1 {
1171 let p = closest_on_segment(p0, p1, q0);
1172 let d = (p - q0).length();
1173 return (!d.is_zero()).then_some((d, p, q0));
1174 }
1175 gjk_distance(&SegmentSupport(p0, p1), &SegmentSupport(q0, q1))
1176}
1177
1178fn sphere_dist(a: Vec3Fix, b: Vec3Fix, center: Vec3Fix, radius: Fix128) -> Dist {
1180 let s = closest_on_segment(a, b, center);
1181 let (n, d) = (s - center).normalize_with_length();
1182 if d < radius || d.is_zero() {
1183 return Dist::Inside;
1184 }
1185 Dist::Outside {
1186 dist: d - radius,
1187 point: center + n * radius,
1188 normal: n,
1189 }
1190}
1191
1192fn capsule_dist(a: Vec3Fix, b: Vec3Fix, capsule: &Capsule) -> Dist {
1194 match segment_segment(a, b, capsule.a, capsule.b) {
1195 None => Dist::Inside,
1196 Some((d, pa, pb)) => Dist::Outside {
1197 dist: d,
1198 point: pb,
1199 normal: gjk_normal(pa - pb, d),
1200 }
1201 .shrunk(capsule.radius),
1202 }
1203}
1204
1205fn point_box_local(p: Vec3Fix, h: Vec3Fix) -> Dist {
1208 if p.x.abs() < h.x && p.y.abs() < h.y && p.z.abs() < h.z {
1209 return Dist::Inside;
1210 }
1211 let q = Vec3Fix::new(
1212 clamp(p.x, -h.x, h.x),
1213 clamp(p.y, -h.y, h.y),
1214 clamp(p.z, -h.z, h.z),
1215 );
1216 if q == p {
1217 let axis = |v: Fix128, e: Fix128, unit: Vec3Fix| {
1219 (v.abs() == e).then(|| if v.is_negative() { -unit } else { unit })
1220 };
1221 let z = Vec3Fix::new(Fix128::ZERO, Fix128::ZERO, Fix128::ONE);
1222 let normal = axis(p.x, h.x, Vec3Fix::UNIT_X)
1223 .or_else(|| axis(p.y, h.y, Vec3Fix::UNIT_Y))
1224 .or_else(|| axis(p.z, h.z, z))
1225 .unwrap_or(Vec3Fix::UNIT_Y);
1226 return Dist::Outside {
1227 dist: Fix128::ZERO,
1228 point: p,
1229 normal,
1230 };
1231 }
1232 Dist::toward(p, q)
1233}
1234
1235fn point_cylinder_local(p: Vec3Fix, radius: Fix128, hh: Fix128) -> Dist {
1238 let (dir, rho) = Vec3Fix::new(p.x, Fix128::ZERO, p.z).normalize_with_length();
1239 let qy = clamp(p.y, -hh, hh);
1240 if rho < radius && p.y.abs() < hh {
1241 return Dist::Inside;
1242 }
1243 let qr = min_fix(rho, radius);
1244 let q = if rho.is_zero() {
1245 Vec3Fix::new(Fix128::ZERO, qy, Fix128::ZERO)
1246 } else {
1247 Vec3Fix::new(dir.x * qr, qy, dir.z * qr)
1248 };
1249 if (q - p).length_squared().is_zero() {
1250 let normal = if p.y.abs() == hh {
1252 if p.y.is_negative() {
1253 -Vec3Fix::UNIT_Y
1254 } else {
1255 Vec3Fix::UNIT_Y
1256 }
1257 } else {
1258 dir
1259 };
1260 return Dist::Outside {
1261 dist: Fix128::ZERO,
1262 point: p,
1263 normal,
1264 };
1265 }
1266 Dist::toward(p, q)
1267}
1268
1269fn toward_fine(core: Vec3Fix, point: Vec3Fix) -> Dist {
1272 let diff = core - point;
1273 match fine_length(diff) {
1274 None => Dist::Inside,
1275 Some(dist) => Dist::Outside {
1276 dist,
1277 point,
1278 normal: gjk_normal(diff, dist),
1279 },
1280 }
1281}
1282
1283fn toward_axis(p: Vec3Fix, rho: Fix128, len: Fix128) -> (Fix128, Fix128) {
1286 if len >= rho || rho.is_zero() {
1287 return (p.x, p.z);
1288 }
1289 let k = len / rho;
1290 (p.x * k, p.z * k)
1291}
1292
1293fn point_cylinder_exact(p: Vec3Fix, radius: Fix128, hh: Fix128) -> Dist {
1299 let rho = fine_length(Vec3Fix::new(p.x, Fix128::ZERO, p.z)).unwrap_or(Fix128::ZERO);
1300 if rho < radius && p.y.abs() < hh {
1301 return Dist::Inside;
1302 }
1303 let (x, z) = toward_axis(p, rho, min_fix(rho, radius));
1304 toward_fine(p, Vec3Fix::new(x, clamp(p.y, -hh, hh), z))
1305}
1306
1307fn point_cone_exact(p: Vec3Fix, radius: Fix128, hh: Fix128) -> Dist {
1313 let rho = fine_length(Vec3Fix::new(p.x, Fix128::ZERO, p.z)).unwrap_or(Fix128::ZERO);
1314 let height = hh.double();
1315 if p.y >= -hh && rho * height <= radius * (hh - p.y) {
1316 return Dist::Inside;
1317 }
1318 let at = |q_rho: Fix128, q_y: Fix128| {
1319 let (x, z) = toward_axis(p, rho, q_rho);
1320 Vec3Fix::new(x, q_y, z)
1321 };
1322 let base = at(min_fix(rho, radius), -hh);
1324 let len2 = radius * radius + height * height;
1326 let s = clamp(
1327 (rho * radius - (p.y - hh) * height) / len2,
1328 Fix128::ZERO,
1329 Fix128::ONE,
1330 );
1331 let side = at(radius * s, hh - height * s);
1332 if shorter(p - side, p - base) {
1333 toward_fine(p, side)
1334 } else {
1335 toward_fine(p, base)
1336 }
1337}
1338
1339fn round_support_slack(posed: &PosedShape) -> Fix128 {
1346 let radius = match posed.shape {
1347 Shape::Cylinder { radius, .. } | Shape::Cone { radius, .. } => radius.abs(),
1348 _ => Fix128::ZERO,
1349 };
1350 let scaled = |r: Fix128, k: i32| {
1351 let raw = ((r.hi as i128) << 64) | (r.lo as i128);
1352 let s = raw >> k;
1353 Fix128::from_raw((s >> 64) as i64, s as u64)
1354 };
1355 scaled(radius, 41) + Fix128::from_raw(0, 1 << 8)
1356}
1357
1358fn exact_round_dist(posed: &PosedShape, a: Vec3Fix, b: Vec3Fix) -> Option<Dist> {
1362 let (center, rotation) = posed_frame(posed);
1363 let inv = rotation.conjugate();
1364 let point: fn(Vec3Fix, Fix128, Fix128) -> Dist = match posed.shape {
1365 Shape::Cylinder { .. } => point_cylinder_exact,
1366 Shape::Cone { .. } => point_cone_exact,
1367 _ => return None,
1368 };
1369 let (radius, hh) = match posed.shape {
1370 Shape::Cylinder {
1371 radius,
1372 half_height,
1373 }
1374 | Shape::Cone {
1375 radius,
1376 half_height,
1377 } => (radius, half_height),
1378 _ => return None,
1379 };
1380 let n2 = rotation.length_squared();
1386 let n4 = n2 * n2;
1387 let f = |q: Vec3Fix| match point(inv.rotate_vec(q - center) / n4, radius, hh) {
1388 Dist::Outside {
1389 dist,
1390 point,
1391 normal,
1392 } => Dist::Outside {
1393 dist: dist * n2,
1394 point: center + rotation.rotate_vec(point),
1395 normal: rotation.rotate_vec(normal) / n2,
1396 },
1397 other => other,
1398 };
1399 Some(segment_min(a, b, f))
1400}
1401
1402fn point_torus_local(p: Vec3Fix, major: Fix128, minor: Fix128) -> Dist {
1405 if torus_distance(p, major, minor).is_negative() {
1406 return Dist::Inside;
1407 }
1408 let (ring_dir, rho) = Vec3Fix::new(p.x, Fix128::ZERO, p.z).normalize_with_length();
1409 let ring_dir = if rho.is_zero() {
1410 Vec3Fix::UNIT_X
1411 } else {
1412 ring_dir
1413 };
1414 let ring = ring_dir * major;
1415 match Dist::toward(p, ring) {
1416 Dist::Outside { dist, normal, .. } => Dist::Outside {
1417 dist,
1418 point: ring,
1419 normal,
1420 }
1421 .shrunk(minor),
1422 other => other,
1423 }
1424}
1425
1426fn dist_to_world(d: Dist, center: Vec3Fix, rotation: QuatFix) -> Dist {
1428 match d {
1429 Dist::Outside {
1430 dist,
1431 point,
1432 normal,
1433 } => Dist::Outside {
1434 dist,
1435 point: center + rotation.rotate_vec(point),
1436 normal: rotation.rotate_vec(normal),
1437 },
1438 other => other,
1439 }
1440}
1441
1442fn curve_min(lipschitz: Fix128, f: impl Fn(Fix128) -> Dist) -> Dist {
1446 const SEEDS: i64 = 16;
1447 let mut best: Option<(Fix128, Fix128, Dist)> = None;
1449 let mut stack: Vec<(Fix128, Fix128, Fix128)> = Vec::new();
1451 let seed_half = Fix128::from_ratio(1, 2 * SEEDS);
1452 let mut nodes = 0usize;
1453 let mut todo: Vec<(Fix128, Fix128)> = (0..SEEDS)
1454 .map(|k| (Fix128::from_ratio(2 * k + 1, 2 * SEEDS), seed_half))
1455 .collect();
1456 loop {
1457 for (m, hw) in todo.drain(..) {
1458 let d = f(m);
1459 if matches!(d, Dist::Inside) {
1460 return Dist::Inside;
1461 }
1462 if best.as_ref().is_none_or(|(_, _, b)| d.value() < b.value()) {
1463 best = Some((m, hw, d));
1464 }
1465 stack.push((m, hw, d.value() - lipschitz * hw));
1467 }
1468 let best_value = best.as_ref().map_or(Fix128::ZERO, |(_, _, b)| b.value());
1469 stack.retain(|&(_, hw, bound)| {
1472 bound < best_value - CURVE_COARSE && lipschitz * hw > CURVE_COARSE
1473 });
1474 let Some(k) = (0..stack.len()).min_by_key(|&k| stack[k].2) else {
1476 break;
1477 };
1478 nodes += 1;
1479 if nodes > CURVE_MAX_NODES {
1480 break;
1481 }
1482 let (mid, hw, _) = stack.remove(k);
1483 let quarter = hw.half();
1484 todo.push((mid - quarter, quarter));
1485 todo.push((mid + quarter, quarter));
1486 }
1487 let Some((s_best, hw_best, d_best)) = best else {
1488 return Dist::AtLeast(Fix128::ZERO);
1489 };
1490 let mut lo = max_fix(Fix128::ZERO, s_best - hw_best.double());
1492 let mut hi = min_fix(Fix128::ONE, s_best + hw_best.double());
1493 let ratio = Fix128::from_ratio(618_033_988_749_895, 1_000_000_000_000_000);
1494 let mut x1 = hi - (hi - lo) * ratio;
1495 let mut x2 = lo + (hi - lo) * ratio;
1496 let mut f1 = f(x1);
1497 let mut f2 = f(x2);
1498 let mut refined = d_best;
1499 for _ in 0..GOLDEN_STEPS {
1500 for d in [f1, f2] {
1501 if matches!(d, Dist::Inside) {
1502 return Dist::Inside;
1503 }
1504 if d.value() < refined.value() {
1505 refined = d;
1506 }
1507 }
1508 if f1.value() <= f2.value() {
1509 hi = x2;
1510 x2 = x1;
1511 f2 = f1;
1512 x1 = hi - (hi - lo) * ratio;
1513 f1 = f(x1);
1514 } else {
1515 lo = x1;
1516 x1 = x2;
1517 f1 = f2;
1518 x2 = lo + (hi - lo) * ratio;
1519 f2 = f(x2);
1520 }
1521 }
1522 refined
1523}
1524
1525fn segment_min(a: Vec3Fix, b: Vec3Fix, f: impl Fn(Vec3Fix) -> Dist) -> Dist {
1528 if a == b {
1529 return f(a);
1530 }
1531 let ab = b - a;
1532 curve_min(ab.length(), |s| f(a + ab * s))
1533}
1534
1535fn field_has_surface(field: &HeightField) -> bool {
1541 field.width >= 2 && field.depth >= 2 && field.spacing > Fix128::ZERO
1542}
1543
1544fn cell_range(
1547 x0: Fix128,
1548 x1: Fix128,
1549 origin: Fix128,
1550 spacing: Fix128,
1551 n: u32,
1552) -> Option<(u32, u32)> {
1553 let last = i64::from(n) - 2;
1554 let lo = ((x0 - origin) / spacing).floor().hi;
1555 let hi = ((x1 - origin) / spacing).floor().hi;
1556 if hi < 0 || lo > last {
1557 return None;
1558 }
1559 Some((lo.clamp(0, last) as u32, hi.clamp(0, last) as u32))
1560}
1561
1562fn cell_origin(field: &HeightField, gx: u32, gz: u32) -> (Fix128, Fix128) {
1564 (
1565 field.origin.x + field.spacing * Fix128::from_int(i64::from(gx)),
1566 field.origin.z + field.spacing * Fix128::from_int(i64::from(gz)),
1567 )
1568}
1569
1570fn cell_heights(field: &HeightField, gx: u32, gz: u32) -> [Fix128; 4] {
1572 [
1573 field.get_height(gx, gz),
1574 field.get_height(gx + 1, gz),
1575 field.get_height(gx, gz + 1),
1576 field.get_height(gx + 1, gz + 1),
1577 ]
1578}
1579
1580struct QuadSupport([Vec3Fix; 4]);
1583
1584impl Support for QuadSupport {
1585 fn support(&self, direction: Vec3Fix) -> Vec3Fix {
1586 let mut best = self.0[0];
1587 let mut best_dot = best.dot(direction);
1588 for &p in &self.0[1..] {
1589 let d = p.dot(direction);
1590 if d > best_dot {
1591 best = p;
1592 best_dot = d;
1593 }
1594 }
1595 best
1596 }
1597}
1598
1599#[derive(Clone, Copy)]
1602struct Cell {
1603 x0: Fix128,
1604 z0: Fix128,
1605 s: Fix128,
1606 h: [Fix128; 4],
1607}
1608
1609impl Cell {
1610 fn new(field: &HeightField, gx: u32, gz: u32) -> Self {
1611 let (x0, z0) = cell_origin(field, gx, gz);
1612 Self {
1613 x0,
1614 z0,
1615 s: field.spacing,
1616 h: cell_heights(field, gx, gz),
1617 }
1618 }
1619
1620 fn height(&self, u: Fix128, v: Fix128) -> Fix128 {
1622 let [h00, h10, h01, h11] = self.h;
1623 let one = Fix128::ONE;
1624 h00 * (one - u) * (one - v) + h10 * u * (one - v) + h01 * (one - u) * v + h11 * u * v
1625 }
1626
1627 fn point(&self, u: Fix128, v: Fix128) -> Vec3Fix {
1628 Vec3Fix::new(
1629 self.x0 + self.s * u,
1630 self.height(u, v),
1631 self.z0 + self.s * v,
1632 )
1633 }
1634
1635 fn twist(&self) -> Fix128 {
1637 let [h00, h10, h01, h11] = self.h;
1638 h00 - h10 - h01 + h11
1639 }
1640
1641 fn normal(&self, u: Fix128, v: Fix128) -> Vec3Fix {
1643 let [h00, h10, h01, h11] = self.h;
1644 let one = Fix128::ONE;
1645 let hu = (h10 - h00) * (one - v) + (h11 - h01) * v;
1646 let hv = (h01 - h00) * (one - u) + (h11 - h10) * u;
1647 Vec3Fix::new(-hu, self.s, -hv).normalize()
1648 }
1649
1650 fn uv(&self, p: Vec3Fix, part: &Patch) -> (Fix128, Fix128) {
1652 (
1653 clamp((p.x - self.x0) / self.s, part.u0, part.u1),
1654 clamp((p.z - self.z0) / self.s, part.v0, part.v1),
1655 )
1656 }
1657}
1658
1659#[derive(Clone, Copy)]
1663struct Patch {
1664 u0: Fix128,
1665 u1: Fix128,
1666 v0: Fix128,
1667 v1: Fix128,
1668}
1669
1670impl Patch {
1671 const WHOLE: Self = Self {
1672 u0: Fix128::ZERO,
1673 u1: Fix128::ONE,
1674 v0: Fix128::ZERO,
1675 v1: Fix128::ONE,
1676 };
1677
1678 fn hull(&self, cell: &Cell) -> QuadSupport {
1679 QuadSupport([
1680 cell.point(self.u0, self.v0),
1681 cell.point(self.u1, self.v0),
1682 cell.point(self.u0, self.v1),
1683 cell.point(self.u1, self.v1),
1684 ])
1685 }
1686
1687 fn thickness(&self, cell: &Cell) -> Fix128 {
1692 (cell.twist().abs() * (self.u1 - self.u0) * (self.v1 - self.v0))
1693 .half()
1694 .half()
1695 }
1696
1697 fn split(&self) -> [Self; 4] {
1698 let um = (self.u0 + self.u1).half();
1699 let vm = (self.v0 + self.v1).half();
1700 [
1701 Self {
1702 u1: um,
1703 v1: vm,
1704 ..*self
1705 },
1706 Self {
1707 u0: um,
1708 v1: vm,
1709 ..*self
1710 },
1711 Self {
1712 u1: um,
1713 v0: vm,
1714 ..*self
1715 },
1716 Self {
1717 u0: um,
1718 v0: vm,
1719 ..*self
1720 },
1721 ]
1722 }
1723
1724 fn surface_under(&self, cell: &Cell, p: Vec3Fix) -> Vec3Fix {
1726 let (u, v) = cell.uv(p, self);
1727 cell.point(u, v)
1728 }
1729}
1730
1731fn cell_dist(cell: &Cell, a: Vec3Fix, b: Vec3Fix) -> (Fix128, Vec3Fix) {
1739 let to_core = |q: Vec3Fix| (closest_on_segment(a, b, q) - q).length();
1740 let eval = |part: &Patch| {
1741 let hull = part.hull(cell);
1742 let found = if a == b {
1743 gjk_distance(&PointSupport(a), &hull)
1744 } else {
1745 gjk_distance(&SegmentSupport(a, b), &hull)
1746 };
1747 let (lower, near) = match found {
1748 Some((d, _, pb)) => (d, pb),
1749 None => (
1750 Fix128::ZERO,
1751 cell.point((part.u0 + part.u1).half(), (part.v0 + part.v1).half()),
1752 ),
1753 };
1754 let q = part.surface_under(cell, near);
1755 (lower, q, to_core(q))
1756 };
1757 let (lower, q, upper) = eval(&Patch::WHOLE);
1758 let mut best = (upper, q);
1759 let mut open = vec![(lower, Patch::WHOLE)];
1760 let mut nodes = 0usize;
1761 while let Some(k) = (0..open.len()).min_by_key(|&k| open[k].0) {
1762 let (lower, part) = open.remove(k);
1763 if lower + TRACE_TOLERANCE >= best.0 {
1766 break;
1767 }
1768 nodes += 1;
1769 if nodes > CELL_MAX_NODES {
1770 return (lower, best.1);
1771 }
1772 for child in part.split() {
1773 let (lower, q, upper) = eval(&child);
1774 if upper < best.0 {
1775 best = (upper, q);
1776 }
1777 if lower + TRACE_TOLERANCE < best.0 && child.thickness(cell) > TRACE_TOLERANCE {
1780 open.push((lower, child));
1781 }
1782 }
1783 }
1784 best
1785}
1786
1787fn heightfield_dist(field: &HeightField, a: Vec3Fix, b: Vec3Fix, cap: Fix128) -> Dist {
1793 if !field_has_surface(field) {
1794 return Dist::AtLeast(cap);
1795 }
1796 let core = core_box(a, b, Fix128::ZERO);
1797 let far = aabb_gap(&core, &field.aabb());
1798 if far >= cap {
1799 return Dist::AtLeast(far);
1800 }
1801 let reach = core_box(a, b, cap);
1802 let s = field.spacing;
1803 let (Some((gx0, gx1)), Some((gz0, gz1))) = (
1804 cell_range(reach.min.x, reach.max.x, field.origin.x, s, field.width),
1805 cell_range(reach.min.z, reach.max.z, field.origin.z, s, field.depth),
1806 ) else {
1807 return Dist::AtLeast(cap);
1808 };
1809 let mut cells: Vec<(Fix128, u32, u32)> = Vec::new();
1812 for gz in gz0..=gz1 {
1813 for gx in gx0..=gx1 {
1814 let lower = aabb_gap(&core, &cell_box(field, gx, gz));
1815 if lower < cap {
1816 cells.push((lower, gz, gx));
1817 }
1818 }
1819 }
1820 cells.sort_unstable();
1821 let mut best: Option<(Fix128, Vec3Fix, Cell)> = None;
1822 for (lower, gz, gx) in cells {
1823 if best.is_some_and(|(d, _, _)| lower >= d) {
1824 break;
1825 }
1826 let cell = Cell::new(field, gx, gz);
1827 let (d, q) = cell_dist(&cell, a, b);
1828 if best.is_none_or(|(bd, _, _)| d < bd) {
1829 best = Some((d, q, cell));
1830 }
1831 }
1832 let Some((d, q, cell)) = best.filter(|&(d, _, _)| d < cap) else {
1833 return Dist::AtLeast(cap);
1834 };
1835 let core_point = closest_on_segment(a, b, q);
1836 if d <= TRACE_TOLERANCE {
1837 if a != b {
1838 return Dist::Inside;
1839 }
1840 let (u, v) = cell.uv(q, &Patch::WHOLE);
1841 let n = cell.normal(u, v);
1842 let side = if (a - q).dot(n).is_negative() { -n } else { n };
1843 return Dist::Outside {
1844 dist: d,
1845 point: q,
1846 normal: side,
1847 };
1848 }
1849 let normal = match (core_point - q).try_normalize() {
1850 Some(n) => n,
1851 None => return Dist::Inside,
1852 };
1853 Dist::Outside {
1854 dist: d,
1855 point: q,
1856 normal,
1857 }
1858}
1859
1860fn cell_box(field: &HeightField, gx: u32, gz: u32) -> AABB {
1863 let h = cell_heights(field, gx, gz);
1864 let (x0, z0) = cell_origin(field, gx, gz);
1865 let s = field.spacing;
1866 let lo = min_fix(min_fix(h[0], h[1]), min_fix(h[2], h[3]));
1867 let hi = max_fix(max_fix(h[0], h[1]), max_fix(h[2], h[3]));
1868 AABB::new(Vec3Fix::new(x0, lo, z0), Vec3Fix::new(x0 + s, hi, z0 + s))
1869}
1870
1871fn cell_toi(
1876 cell: &Cell,
1877 a: Vec3Fix,
1878 b: Vec3Fix,
1879 r: Fix128,
1880 d: Vec3Fix,
1881 max_t: Fix128,
1882) -> Option<Contact> {
1883 let toi = |part: &Patch| toi_convex(a, b, r, Fix128::ZERO, d, max_t, &part.hull(cell));
1884 if cell.twist().is_zero() {
1885 return toi(&Patch::WHOLE);
1886 }
1887 let found = toi_tree(
1888 &[Patch::WHOLE],
1889 toi,
1890 |part| part.thickness(cell) <= TRACE_TOLERANCE,
1891 |part| part.split().to_vec(),
1892 )?;
1893 let off = d * found.t;
1894 Some(refine_on_cell(cell, a + off, b + off, found))
1895}
1896
1897fn refine_on_cell(cell: &Cell, a: Vec3Fix, b: Vec3Fix, found: Contact) -> Contact {
1903 let [h00, h10, h01, h11] = cell.h;
1904 let k = cell.twist();
1905 let s = cell.s;
1906 let one = Fix128::ONE;
1907 let (mut u, mut v) = cell.uv(found.point, &Patch::WHOLE);
1908 for _ in 0..8 {
1909 let q = cell.point(u, v);
1910 let e = q - closest_on_segment(a, b, q);
1911 let hu = (h10 - h00) * (one - v) + (h11 - h01) * v;
1912 let hv = (h01 - h00) * (one - u) + (h11 - h10) * u;
1913 let pu = Vec3Fix::new(s, hu, Fix128::ZERO);
1914 let pv = Vec3Fix::new(Fix128::ZERO, hv, s);
1915 let (gu, gv) = (e.dot(pu), e.dot(pv));
1916 let (huu, hvv) = (pu.dot(pu), pv.dot(pv));
1917 let huv = pu.dot(pv) + e.y * k;
1918 let det = huu * hvv - huv * huv;
1919 if det <= Fix128::ZERO {
1920 return found;
1921 }
1922 u = clamp(u - (hvv * gu - huv * gv) / det, Fix128::ZERO, one);
1923 v = clamp(v - (huu * gv - huv * gu) / det, Fix128::ZERO, one);
1924 }
1925 let q = cell.point(u, v);
1926 let core = closest_on_segment(a, b, q);
1927 let before = (closest_on_segment(a, b, found.point) - found.point).length();
1928 match (core - q).normalize_with_length() {
1929 (n, dist)
1930 if !dist.is_zero()
1931 && dist <= before + TRACE_TOLERANCE + TRACE_TOLERANCE
1932 && n.dot(found.normal) > Fix128::ZERO =>
1933 {
1934 Contact {
1935 t: found.t,
1936 point: q,
1937 normal: n,
1938 }
1939 }
1940 _ => found,
1941 }
1942}
1943
1944fn sweep_heightfield(
1949 field: &HeightField,
1950 a: Vec3Fix,
1951 b: Vec3Fix,
1952 r: Fix128,
1953 d: Vec3Fix,
1954 max_t: Fix128,
1955) -> Option<Contact> {
1956 if !field_has_surface(field) {
1957 return None;
1958 }
1959 let start = core_box(a, b, r);
1960 let swept = swept_box(a, b, r, d, max_t);
1961 let s = field.spacing;
1962 let (Some((gx0, gx1)), Some((gz0, gz1))) = (
1963 cell_range(swept.min.x, swept.max.x, field.origin.x, s, field.width),
1964 cell_range(swept.min.z, swept.max.z, field.origin.z, s, field.depth),
1965 ) else {
1966 return None;
1967 };
1968 let mut cells: Vec<(Fix128, u32, u32)> = Vec::new();
1969 for gz in gz0..=gz1 {
1970 for gx in gx0..=gx1 {
1971 if let Some(entry) = box_entry(&start, d, max_t, &cell_box(field, gx, gz)) {
1972 cells.push((entry, gz, gx));
1973 }
1974 }
1975 }
1976 cells.sort_unstable();
1977 let mut best: Option<Contact> = None;
1978 for (entry, gz, gx) in cells {
1979 if best.is_some_and(|c| c.t < entry) {
1980 break;
1981 }
1982 if let Some(c) = cell_toi(&Cell::new(field, gx, gz), a, b, r, d, max_t) {
1983 if best.is_none_or(|bc| c.t < bc.t) {
1984 best = Some(c);
1985 }
1986 }
1987 }
1988 best
1989}
1990
1991fn ratio_within(num: Fix128, den: Fix128, big: Fix128) -> Fix128 {
1993 if num.abs() >= den.abs() * big {
1994 if num.is_negative() == den.is_negative() {
1995 big
1996 } else {
1997 -big
1998 }
1999 } else {
2000 num / den
2001 }
2002}
2003
2004fn box_entry(moving: &AABB, d: Vec3Fix, max_t: Fix128, target: &AABB) -> Option<Fix128> {
2007 let big = max_t + Fix128::ONE;
2008 let mut lo = Fix128::ZERO;
2009 let mut hi = max_t;
2010 for (m0, m1, t0, t1, di) in [
2011 (moving.min.x, moving.max.x, target.min.x, target.max.x, d.x),
2012 (moving.min.y, moving.max.y, target.min.y, target.max.y, d.y),
2013 (moving.min.z, moving.max.z, target.min.z, target.max.z, d.z),
2014 ] {
2015 if di.abs() < PARALLEL_EPSILON {
2016 let slack = max_t * di.abs();
2018 if m1 + slack < t0 || m0 - slack > t1 {
2019 return None;
2020 }
2021 continue;
2022 }
2023 let (enter, leave) = if di.is_negative() {
2024 (
2025 ratio_within(t1 - m0, di, big),
2026 ratio_within(t0 - m1, di, big),
2027 )
2028 } else {
2029 (
2030 ratio_within(t0 - m1, di, big),
2031 ratio_within(t1 - m0, di, big),
2032 )
2033 };
2034 lo = max_fix(lo, enter);
2035 hi = min_fix(hi, leave);
2036 }
2037 (lo <= hi).then_some(lo)
2038}
2039
2040fn swept_box(a: Vec3Fix, b: Vec3Fix, r: Fix128, d: Vec3Fix, max_t: Fix128) -> AABB {
2042 let start = core_box(a, b, r);
2043 let end = core_box(a + d * max_t, b + d * max_t, r);
2044 AABB::new(
2045 Vec3Fix::new(
2046 min_fix(start.min.x, end.min.x),
2047 min_fix(start.min.y, end.min.y),
2048 min_fix(start.min.z, end.min.z),
2049 ),
2050 Vec3Fix::new(
2051 max_fix(start.max.x, end.max.x),
2052 max_fix(start.max.y, end.max.y),
2053 max_fix(start.max.z, end.max.z),
2054 ),
2055 )
2056}
2057
2058fn sweep_mesh(
2064 mesh: &TriMesh,
2065 a: Vec3Fix,
2066 b: Vec3Fix,
2067 r: Fix128,
2068 d: Vec3Fix,
2069 max_t: Fix128,
2070) -> Option<Contact> {
2071 let start = core_box(a, b, r);
2072 let mut order: Vec<(Fix128, u32)> = mesh_candidates(mesh, &swept_box(a, b, r, d, max_t))
2073 .into_iter()
2074 .filter_map(|i| {
2075 let tri = &mesh.triangles[i as usize];
2076 let tri_box = AABB::new(
2077 Vec3Fix::new(
2078 min_fix(min_fix(tri.v0.x, tri.v1.x), tri.v2.x),
2079 min_fix(min_fix(tri.v0.y, tri.v1.y), tri.v2.y),
2080 min_fix(min_fix(tri.v0.z, tri.v1.z), tri.v2.z),
2081 ),
2082 Vec3Fix::new(
2083 max_fix(max_fix(tri.v0.x, tri.v1.x), tri.v2.x),
2084 max_fix(max_fix(tri.v0.y, tri.v1.y), tri.v2.y),
2085 max_fix(max_fix(tri.v0.z, tri.v1.z), tri.v2.z),
2086 ),
2087 );
2088 box_entry(&start, d, max_t, &tri_box).map(|entry| (entry, i))
2089 })
2090 .collect();
2091 order.sort_unstable();
2092 let mut best: Option<Contact> = None;
2093 for (entry, i) in order {
2094 if best.is_some_and(|c| c.t < entry) {
2095 break;
2096 }
2097 let tri = &mesh.triangles[i as usize];
2098 if let Some(c) = toi_convex(a, b, r, Fix128::ZERO, d, max_t, &TriangleSupport(tri)) {
2099 if best.is_none_or(|bc| c.t < bc.t) {
2100 best = Some(c);
2101 }
2102 }
2103 }
2104 best
2105}
2106
2107#[derive(Clone, Copy)]
2110struct Ring {
2111 center: Vec3Fix,
2112 rotation: QuatFix,
2113 major: Fix128,
2114 minor: Fix128,
2115}
2116
2117#[derive(Clone, Copy)]
2120struct RingArc {
2121 e0: Vec3Fix,
2122 e1: Vec3Fix,
2123}
2124
2125fn turn(a: Vec3Fix, b: Vec3Fix) -> Fix128 {
2128 a.x * b.z - a.z * b.x
2129}
2130
2131impl RingArc {
2132 fn thickness(&self, major: Fix128) -> Fix128 {
2135 let half_chord = ((self.e1 - self.e0).length() * major).half();
2136 major - (major * major - half_chord * half_chord).sqrt()
2137 }
2138
2139 fn split(&self) -> [Self; 2] {
2140 let mid = (self.e0 + self.e1).normalize();
2141 [
2142 Self {
2143 e0: self.e0,
2144 e1: mid,
2145 },
2146 Self {
2147 e0: mid,
2148 e1: self.e1,
2149 },
2150 ]
2151 }
2152}
2153
2154struct ArcSupport {
2157 ring: Ring,
2158 arc: RingArc,
2159}
2160
2161impl Support for ArcSupport {
2162 fn support(&self, direction: Vec3Fix) -> Vec3Fix {
2163 let local = self.ring.rotation.conjugate().rotate_vec(direction);
2164 let flat = Vec3Fix::new(local.x, Fix128::ZERO, local.z);
2165 let (e0, e1) = (self.arc.e0, self.arc.e1);
2166 let e = match flat.try_normalize() {
2167 Some(u) if !turn(e0, u).is_negative() && !turn(u, e1).is_negative() => u,
2168 _ if e1.dot(flat) > e0.dot(flat) => e1,
2169 _ => e0,
2170 };
2171 self.ring.center + self.ring.rotation.rotate_vec(e * self.ring.major)
2172 }
2173}
2174
2175impl Ring {
2176 fn sweep(
2181 &self,
2182 a: Vec3Fix,
2183 b: Vec3Fix,
2184 r: Fix128,
2185 d: Vec3Fix,
2186 max_t: Fix128,
2187 ) -> Option<Contact> {
2188 let x = Vec3Fix::UNIT_X;
2189 let z = Vec3Fix::new(Fix128::ZERO, Fix128::ZERO, Fix128::ONE);
2190 let quarters = [
2191 RingArc { e0: x, e1: z },
2192 RingArc { e0: z, e1: -x },
2193 RingArc { e0: -x, e1: -z },
2194 RingArc { e0: -z, e1: x },
2195 ];
2196 toi_tree(
2197 &quarters,
2198 |arc| {
2199 let hull = ArcSupport {
2200 ring: *self,
2201 arc: *arc,
2202 };
2203 toi_convex(a, b, r, self.minor, d, max_t, &hull)
2204 },
2205 |arc| arc.thickness(self.major) <= TRACE_TOLERANCE,
2206 |arc| arc.split().to_vec(),
2207 )
2208 .map(|c| self.refine(a + d * c.t, b + d * c.t, c))
2209 }
2210
2211 fn refine(&self, a: Vec3Fix, b: Vec3Fix, found: Contact) -> Contact {
2215 let core = closest_on_segment(a, b, found.point);
2216 let inv = self.rotation.conjugate();
2217 let local = inv.rotate_vec(core - self.center);
2218 let Some(dir) = Vec3Fix::new(local.x, Fix128::ZERO, local.z).try_normalize() else {
2219 return found;
2220 };
2221 let ring = self.center + self.rotation.rotate_vec(dir * self.major);
2222 match (core - ring).try_normalize() {
2223 Some(n) if n.dot(found.normal) > Fix128::ZERO => Contact {
2224 t: found.t,
2225 point: ring + n * self.minor,
2226 normal: n,
2227 },
2228 _ => found,
2229 }
2230 }
2231}
2232
2233fn toi_convex<S: Support>(
2261 a: Vec3Fix,
2262 b: Vec3Fix,
2263 r: Fix128,
2264 inflate: Fix128,
2265 d: Vec3Fix,
2266 max_t: Fix128,
2267 solid: &S,
2268) -> Option<Contact> {
2269 toi_convex_refined(a, b, r, inflate, d, max_t, solid, Fix128::ZERO, |_, _| None)
2270}
2271
2272#[allow(clippy::too_many_arguments)]
2288fn toi_convex_refined<S: Support>(
2289 a: Vec3Fix,
2290 b: Vec3Fix,
2291 r: Fix128,
2292 inflate: Fix128,
2293 d: Vec3Fix,
2294 max_t: Fix128,
2295 solid: &S,
2296 slack: Fix128,
2297 exact: impl Fn(Vec3Fix, Vec3Fix) -> Option<Dist>,
2298) -> Option<Contact> {
2299 let reach = r + inflate;
2300 toi_by(a, b, r, inflate, d, max_t, |a, b, along| {
2301 if along {
2302 if let Some(e) = exact(a, b) {
2305 let exact_bound = Bound::exact(&e);
2306 return (e, exact_bound);
2307 }
2308 }
2309 let (found, bound) = convex_bounds(a, b, solid, slack);
2310 if let Dist::Outside { dist, .. } = found {
2311 if straddles(bound.lower - reach, dist - reach) {
2312 if let Some(e) = exact(a, b) {
2313 let exact_bound = Bound::exact(&e);
2314 return (e, exact_bound);
2315 }
2316 }
2317 }
2318 (found, bound)
2319 })
2320}
2321
2322fn straddles(lower: Fix128, upper: Fix128) -> bool {
2325 let crosses = |x: Fix128| lower <= x && x < upper;
2326 crosses(TRACE_TOLERANCE) || crosses(-TRACE_TOLERANCE)
2327}
2328
2329fn toi_by(
2340 a: Vec3Fix,
2341 b: Vec3Fix,
2342 r: Fix128,
2343 inflate: Fix128,
2344 d: Vec3Fix,
2345 max_t: Fix128,
2346 core_dist: impl Fn(Vec3Fix, Vec3Fix, bool) -> (Dist, Bound),
2347) -> Option<Contact> {
2348 let reach = r + inflate;
2349 let mut t = Fix128::ZERO;
2350 let mut clear: Option<Contact> = None;
2353 let mut clear_gap = (Fix128::ZERO, Fix128::ZERO, false);
2354 let mut deep: Option<Fix128> = None;
2356 for _ in 0..TRACE_MAX_STEPS {
2357 let off = d * t;
2358 let (found, bound) = core_dist(a + off, b + off, false);
2359 let state = match found {
2360 Dist::Outside {
2361 dist,
2362 point,
2363 normal,
2364 } if dist - reach >= -TRACE_TOLERANCE => {
2365 let gap = if bound.step_lower {
2370 max_fix(bound.lower - reach, TRACE_TOLERANCE)
2371 } else {
2372 dist - reach
2373 };
2374 let touching = dist - reach <= TRACE_TOLERANCE;
2375 let slope = d.dot(normal);
2376 let contact = Contact {
2377 t,
2378 point: point + normal * inflate,
2379 normal,
2380 };
2381 if touching {
2382 let gap_at = |t: Fix128| {
2383 let off = d * t;
2384 match core_dist(a + off, b + off, true).0 {
2385 Dist::Outside { dist, .. } => Some(dist - reach),
2386 _ => None,
2387 }
2388 };
2389 return touch_is_hit(contact, d, max_t, gap_at).then_some(contact);
2390 }
2391 if !slope.is_negative() {
2392 if slope < NEAR_TANGENT && gap <= GRAZE_GAP {
2393 let probe = |t: Fix128| {
2395 let off = d * t;
2396 core_dist(a + off, b + off, true).0
2397 };
2398 return graze_contact(probe, reach, inflate, t, max_t);
2399 }
2400 return None;
2401 }
2402 Some((gap, -slope, contact, dist < FINE_LENGTH))
2403 }
2404 _ => None,
2405 };
2406 match state {
2407 Some((gap, speed, contact, fine)) => {
2408 clear = Some(contact);
2409 clear_gap = (gap, speed, fine);
2410 let limit = deep.unwrap_or(max_t);
2411 if gap >= (limit - t) * speed {
2413 match deep {
2414 None if gap > (limit - t) * speed => return None,
2415 None => t = limit,
2416 Some(hi) => t = (t + hi).half(),
2417 }
2418 } else {
2419 t = t + gap / speed;
2420 }
2421 }
2422 None => {
2423 if t.is_zero() {
2424 return Some(Contact {
2425 t,
2426 point: half_vec(a + b),
2427 normal: -d,
2428 });
2429 }
2430 deep = Some(t);
2431 let lo = clear.map_or(Fix128::ZERO, |c| c.t);
2432 if t - lo <= TRACE_TOLERANCE {
2433 let (gap, speed, fine) = clear_gap;
2442 if !fine {
2443 return clear;
2444 }
2445 return clear.map(|c| {
2446 let slack = ratio_within(TRACE_TOLERANCE, speed, max_t);
2447 let step = ratio_within(gap, speed, t - lo + slack);
2448 Contact {
2449 t: min_fix(lo + max_fix(step, Fix128::ZERO), max_t),
2450 ..c
2451 }
2452 });
2453 }
2454 t = (lo + t).half();
2455 }
2456 }
2457 }
2458 clear
2459}
2460
2461const GRAZE_GAP: Fix128 = Fix128 {
2469 hi: 0,
2470 lo: 0x0000_0100_0000_0000,
2471};
2472
2473fn graze_contact(
2480 dist: impl Fn(Fix128) -> Dist,
2481 reach: Fix128,
2482 inflate: Fix128,
2483 t0: Fix128,
2484 max_t: Fix128,
2485) -> Option<Contact> {
2486 let gap_at = |t: Fix128| match dist(t) {
2487 Dist::Outside { dist, .. } => Some(dist - reach),
2488 _ => None,
2489 };
2490 let deep = |g: Option<Fix128>| g.is_none_or(|g| g < -TRACE_TOLERANCE);
2491 let mut inside = deep(gap_at(max_t)).then_some(max_t);
2492 if inside.is_none() {
2493 let ratio = Fix128::from_ratio(618_033_988_749_895, 1_000_000_000_000_000);
2494 let (mut lo, mut hi) = (t0, max_t);
2495 for _ in 0..GOLDEN_STEPS {
2496 let x1 = hi - (hi - lo) * ratio;
2497 let x2 = lo + (hi - lo) * ratio;
2498 let (g1, g2) = (gap_at(x1), gap_at(x2));
2499 if deep(g1) {
2500 inside = Some(x1);
2501 break;
2502 }
2503 if deep(g2) {
2504 inside = Some(x2);
2505 break;
2506 }
2507 if g1 <= g2 {
2508 hi = x2;
2509 } else {
2510 lo = x1;
2511 }
2512 }
2513 }
2514 let (mut lo, mut hi) = (t0, inside?);
2515 for _ in 0..GOLDEN_STEPS {
2516 let mid = (lo + hi).half();
2517 match gap_at(mid) {
2518 Some(g) if g > TRACE_TOLERANCE => lo = mid,
2519 Some(g) if g >= -TRACE_TOLERANCE => {
2520 lo = mid;
2521 break;
2522 }
2523 _ => hi = mid,
2524 }
2525 }
2526 match dist(lo) {
2527 Dist::Outside { point, normal, .. } => Some(Contact {
2528 t: lo,
2529 point: point + normal * inflate,
2530 normal,
2531 }),
2532 _ => None,
2533 }
2534}
2535
2536fn touch_is_hit(
2546 contact: Contact,
2547 d: Vec3Fix,
2548 max_t: Fix128,
2549 gap_at: impl Fn(Fix128) -> Option<Fix128>,
2550) -> bool {
2551 let slope = d.dot(contact.normal);
2552 if slope < -NEAR_TANGENT {
2553 return true;
2554 }
2555 if !slope.is_negative() {
2556 return false;
2557 }
2558 match gap_at(contact.t) {
2559 None => true,
2560 Some(gap) => dips_below(gap_at, contact.t, gap, -slope, max_t),
2561 }
2562}
2563
2564const DIPS_GROWTH: Fix128 = Fix128 { hi: 0, lo: 1 << 8 };
2574
2575fn dips_below(
2581 gap_at: impl Fn(Fix128) -> Option<Fix128>,
2582 t0: Fix128,
2583 f0: Fix128,
2584 speed: Fix128,
2585 max_t: Fix128,
2586) -> bool {
2587 let deep = |g: Option<Fix128>| g.is_none_or(|g| g < -TRACE_TOLERANCE);
2588 if deep(gap_at(max_t)) {
2592 return true;
2593 }
2594 let big = max_t + Fix128::ONE;
2595 let mut step = max_fix(
2596 ratio_within(max_fix(f0, Fix128::ZERO), speed, big),
2597 TRACE_TOLERANCE,
2598 );
2599 let (mut before, mut last, mut last_gap) = (t0, t0, f0);
2600 for _ in 0..GOLDEN_STEPS {
2601 let t1 = min_fix(t0 + step, max_t);
2602 let g1 = gap_at(t1);
2603 if deep(g1) {
2604 return true;
2605 }
2606 let g1 = g1.unwrap_or(Fix128::ZERO);
2607 if g1 > last_gap + DIPS_GROWTH || t1 >= max_t {
2608 let (mut lo, mut hi) = (before, t1);
2610 let ratio = Fix128::from_ratio(618_033_988_749_895, 1_000_000_000_000_000);
2611 for _ in 0..GOLDEN_STEPS {
2612 let x1 = hi - (hi - lo) * ratio;
2613 let x2 = lo + (hi - lo) * ratio;
2614 let (g1, g2) = (gap_at(x1), gap_at(x2));
2615 if deep(g1) || deep(g2) {
2616 return true;
2617 }
2618 let (g1, g2) = (g1.unwrap_or(Fix128::ZERO), g2.unwrap_or(Fix128::ZERO));
2619 if g1 <= g2 {
2620 hi = x2;
2621 } else {
2622 lo = x1;
2623 }
2624 }
2625 return false;
2626 }
2627 before = last;
2628 last = t1;
2629 last_gap = g1;
2630 step = step.double();
2631 }
2632 false
2633}
2634
2635fn toi_tree<N: Copy>(
2644 roots: &[N],
2645 toi: impl Fn(&N) -> Option<Contact>,
2646 leaf: impl Fn(&N) -> bool,
2647 split: impl Fn(&N) -> Vec<N>,
2648) -> Option<Contact> {
2649 let mut open: Vec<(Contact, N)> = roots
2650 .iter()
2651 .filter_map(|n| toi(n).map(|c| (c, *n)))
2652 .collect();
2653 let mut nodes = 0usize;
2654 loop {
2655 let k = (0..open.len()).min_by_key(|&k| open[k].0.t)?;
2656 let (contact, node) = open.remove(k);
2657 if leaf(&node) {
2658 return Some(contact);
2659 }
2660 nodes += 1;
2661 if nodes > TREE_MAX_NODES {
2662 return Some(contact);
2663 }
2664 for child in split(&node) {
2665 if let Some(c) = toi(&child) {
2666 open.push((c, child));
2667 }
2668 }
2669 }
2670}
2671
2672fn heightfield_meets_aabb(field: &HeightField, aabb: &AABB) -> bool {
2676 if !field_has_surface(field) {
2677 return false;
2678 }
2679 let s = field.spacing;
2680 let x_end = field.origin.x + s * Fix128::from_int(i64::from(field.width) - 1);
2681 let z_end = field.origin.z + s * Fix128::from_int(i64::from(field.depth) - 1);
2682 let fx0 = max_fix(aabb.min.x, field.origin.x);
2683 let fx1 = min_fix(aabb.max.x, x_end);
2684 let fz0 = max_fix(aabb.min.z, field.origin.z);
2685 let fz1 = min_fix(aabb.max.z, z_end);
2686 if fx0 > fx1 || fz0 > fz1 {
2687 return false;
2688 }
2689 let (Some((gx0, gx1)), Some((gz0, gz1))) = (
2690 cell_range(fx0, fx1, field.origin.x, s, field.width),
2691 cell_range(fz0, fz1, field.origin.z, s, field.depth),
2692 ) else {
2693 return false;
2694 };
2695 for gz in gz0..=gz1 {
2696 for gx in gx0..=gx1 {
2697 let x0 = field.origin.x + s * Fix128::from_int(i64::from(gx));
2698 let z0 = field.origin.z + s * Fix128::from_int(i64::from(gz));
2699 let ux0 = max_fix(fx0, x0);
2700 let ux1 = min_fix(fx1, x0 + s);
2701 let uz0 = max_fix(fz0, z0);
2702 let uz1 = min_fix(fz1, z0 + s);
2703 if ux0 > ux1 || uz0 > uz1 {
2704 continue;
2705 }
2706 let [h00, h10, h01, h11] = cell_heights(field, gx, gz);
2707 let mut lo: Option<Fix128> = None;
2708 let mut hi: Option<Fix128> = None;
2709 for x in [ux0, ux1] {
2710 for z in [uz0, uz1] {
2711 let u = (x - x0) / s;
2712 let v = (z - z0) / s;
2713 let one = Fix128::ONE;
2714 let h = h00 * (one - u) * (one - v)
2715 + h10 * u * (one - v)
2716 + h01 * (one - u) * v
2717 + h11 * u * v;
2718 lo = Some(lo.map_or(h, |l| min_fix(l, h)));
2719 hi = Some(hi.map_or(h, |m| max_fix(m, h)));
2720 }
2721 }
2722 if let (Some(lo), Some(hi)) = (lo, hi) {
2723 if lo < aabb.max.y && hi > aabb.min.y {
2724 return true;
2725 }
2726 }
2727 }
2728 }
2729 false
2730}
2731
2732fn mesh_candidates(mesh: &TriMesh, query: &AABB) -> Vec<u32> {
2738 mesh.bvh.query(query)
2739}
2740
2741fn mesh_dist(mesh: &TriMesh, a: Vec3Fix, b: Vec3Fix, cap: Fix128) -> Dist {
2744 if mesh.triangles.is_empty() {
2745 return Dist::AtLeast(cap);
2746 }
2747 let core = core_box(a, b, Fix128::ZERO);
2748 let far = aabb_gap(&core, &mesh.bounds);
2749 if far >= cap {
2750 return Dist::AtLeast(far);
2751 }
2752 let mut best: Option<Dist> = None;
2753 for i in mesh_candidates(mesh, &core_box(a, b, cap)) {
2754 let tri = &mesh.triangles[i as usize];
2755 let d = if a == b {
2756 match Dist::toward(a, tri.closest_point(a)) {
2757 Dist::Inside => Dist::Outside {
2759 dist: Fix128::ZERO,
2760 point: a,
2761 normal: tri.normal().try_normalize().unwrap_or(Vec3Fix::UNIT_Y),
2762 },
2763 other => other,
2764 }
2765 } else {
2766 convex_dist(a, b, &TriangleSupport(tri))
2767 };
2768 if matches!(d, Dist::Inside) {
2769 return Dist::Inside;
2770 }
2771 if best.as_ref().is_none_or(|x| d.value() < x.value()) {
2772 best = Some(d);
2773 }
2774 }
2775 match best {
2776 Some(d) if d.value() < cap => d,
2777 _ => Dist::AtLeast(cap),
2778 }
2779}
2780
2781fn sweep_triangle(
2784 o: Vec3Fix,
2785 d: Vec3Fix,
2786 tri: &Triangle,
2787 r: Fix128,
2788 max_t: Fix128,
2789) -> Option<LocalHit> {
2790 let ray = crate::raycast::Ray {
2791 origin: o,
2792 direction: d,
2793 };
2794 let mut best = None;
2795 if let Some(n) = tri.normal().try_normalize() {
2796 for off in [n * r, -(n * r)] {
2797 let moved = Triangle::new(tri.v0 + off, tri.v1 + off, tri.v2 + off);
2798 best = nearer(
2799 best,
2800 ray_triangle(&ray, &moved, max_t).map(|h| LocalHit {
2801 t: h.t,
2802 normal: h.normal,
2803 }),
2804 );
2805 }
2806 }
2807 for (e0, e1) in [(tri.v0, tri.v1), (tri.v1, tri.v2), (tri.v2, tri.v0)] {
2808 best = nearer(
2809 best,
2810 ray_solid_capsule(o, d, &Capsule::new(e0, e1, r), max_t),
2811 );
2812 }
2813 best
2814}
2815
2816enum Piece<'a> {
2822 Sphere {
2823 center: Vec3Fix,
2824 radius: Fix128,
2825 },
2826 Capsule(Capsule),
2827 Box {
2828 center: Vec3Fix,
2829 half: Vec3Fix,
2830 rotation: QuatFix,
2831 },
2832 Posed(PosedShape),
2833 Hull(ChildSupport<'a>),
2834 Plane(&'a PlaneCollider),
2835 Height(&'a HeightField),
2836 Mesh(&'a TriMesh),
2837 #[cfg(feature = "std")]
2838 Sdf(&'a crate::sdf_collider::SdfCollider),
2839}
2840
2841fn body_pieces(world: &PhysicsWorld, i: usize) -> Vec<Piece<'_>> {
2843 let Some((radius, collider)) = world.ray_geometry(i) else {
2844 return Vec::new();
2845 };
2846 let body = &world.bodies[i];
2847 match collider {
2848 None => vec![Piece::Sphere {
2849 center: body.position,
2850 radius,
2851 }],
2852 Some(BodyCollider::Shape(shape)) => {
2853 let posed = PosedShape {
2854 shape: *shape,
2855 position: body.position,
2856 rotation: body.rotation,
2857 };
2858 vec![match *shape {
2859 Shape::Box { half_extents } => {
2860 let rotation = body.rotation.unit_rotation();
2861 Piece::Box {
2862 center: body.position - rotation.rotate_vec(shape.center_of_mass_offset()),
2863 half: half_extents,
2864 rotation,
2865 }
2866 }
2867 _ => Piece::Posed(posed),
2868 }]
2869 }
2870 Some(BodyCollider::Compound(compound)) => compound
2871 .children
2872 .iter()
2873 .map(|child| child_piece(child, body.position, body.rotation))
2874 .collect(),
2875 }
2876}
2877
2878fn child_piece(child: &CompoundChild, body_pos: Vec3Fix, body_rot: QuatFix) -> Piece<'_> {
2880 let body_rot = body_rot.unit_rotation();
2881 let child_rot = body_rot.mul(child.local_rotation);
2882 let child_pos = body_pos + body_rot.rotate_vec(child.local_position);
2883 match &child.shape {
2884 ShapeRef::Sphere(s) => Piece::Sphere {
2885 center: child_pos + child_rot.rotate_vec(s.center),
2886 radius: s.radius,
2887 },
2888 ShapeRef::Capsule(c) => Piece::Capsule(Capsule::new(
2889 child_pos + child_rot.rotate_vec(c.a),
2890 child_pos + child_rot.rotate_vec(c.b),
2891 c.radius,
2892 )),
2893 ShapeRef::Box(b) => Piece::Box {
2894 center: child_pos + child_rot.rotate_vec(b.center),
2895 half: b.half_extents,
2896 rotation: child_rot.mul(b.rotation),
2897 },
2898 ShapeRef::ConvexHull(_) => Piece::Hull(ChildSupport {
2899 child,
2900 position: body_pos,
2901 rotation: body_rot,
2902 }),
2903 }
2904}
2905
2906fn posed_frame(posed: &PosedShape) -> (Vec3Fix, QuatFix) {
2908 let rotation = posed.rotation.unit_rotation();
2909 (
2910 posed.position - rotation.rotate_vec(posed.shape.center_of_mass_offset()),
2911 rotation,
2912 )
2913}
2914
2915struct Settings {
2917 #[cfg(feature = "std")]
2918 sdf: SdfCcdConfig,
2919}
2920
2921impl Piece<'_> {
2922 fn dist(&self, a: Vec3Fix, b: Vec3Fix, cap: Fix128) -> Dist {
2925 match self {
2926 Self::Sphere { center, radius } => sphere_dist(a, b, *center, *radius),
2927 Self::Capsule(c) => capsule_dist(a, b, c),
2928 Self::Box {
2929 center,
2930 half,
2931 rotation,
2932 } => {
2933 if a == b {
2934 let inv = rotation.conjugate();
2935 let p = inv.rotate_vec(a - *center);
2936 dist_to_world(point_box_local(p, *half), *center, *rotation)
2937 } else {
2938 let obb = crate::box_collider::OrientedBox::new(*center, *half, *rotation);
2939 convex_dist(a, b, &obb)
2940 }
2941 }
2942 Self::Posed(posed) => {
2943 let (center, rotation) = posed_frame(posed);
2944 let inv = rotation.conjugate();
2945 match posed.shape {
2946 Shape::Cylinder {
2947 radius,
2948 half_height,
2949 } if a == b => dist_to_world(
2950 point_cylinder_local(inv.rotate_vec(a - center), radius, half_height),
2951 center,
2952 rotation,
2953 ),
2954 Shape::Torus {
2955 major_radius,
2956 minor_radius,
2957 } => {
2958 let f = |p: Vec3Fix| {
2959 dist_to_world(
2960 point_torus_local(
2961 inv.rotate_vec(p - center),
2962 major_radius,
2963 minor_radius,
2964 ),
2965 center,
2966 rotation,
2967 )
2968 };
2969 segment_min(a, b, f)
2970 }
2971 _ if is_round(posed) => convex_dist(a, b, &RoundSupport(posed)),
2972 _ => convex_dist(a, b, posed),
2973 }
2974 }
2975 Self::Hull(child) => convex_dist(a, b, child),
2976 Self::Plane(plane) => plane_dist(plane, a, b),
2977 Self::Height(field) => heightfield_dist(field, a, b, cap),
2978 Self::Mesh(mesh) => mesh_dist(mesh, a, b, cap),
2979 #[cfg(feature = "std")]
2980 Self::Sdf(sdf) => segment_min(a, b, |p| point_sdf(sdf, p)),
2981 }
2982 }
2983
2984 fn sweep(
2992 &self,
2993 a: Vec3Fix,
2994 b: Vec3Fix,
2995 r: Fix128,
2996 d: Vec3Fix,
2997 max_t: Fix128,
2998 settings: &Settings,
2999 ) -> Option<Contact> {
3000 #[cfg(not(feature = "std"))]
3001 let _ = settings;
3002 let point_core = a == b;
3003 match self {
3004 Self::Sphere { center, radius } => {
3005 let c = if point_core {
3006 let h = ray_solid_sphere(a, d, *center, *radius + r, max_t)?;
3007 Contact::from_ray(a, d, r, h)
3008 } else {
3009 let grown = Capsule::new(a, b, r + *radius);
3012 let h = ray_solid_capsule(*center, -d, &grown, max_t)?;
3013 let normal = -h.normal;
3014 Contact {
3015 t: h.t,
3016 point: *center + normal * *radius,
3017 normal,
3018 }
3019 };
3020 self.entering(c, a, b, r, d, max_t)
3021 }
3022 Self::Capsule(c) => {
3023 if point_core {
3024 let grown = Capsule::new(c.a, c.b, c.radius + r);
3025 let h = ray_solid_capsule(a, d, &grown, max_t)?;
3026 self.entering(Contact::from_ray(a, d, r, h), a, b, r, d, max_t)
3027 } else {
3028 toi_convex(a, b, r, c.radius, d, max_t, &SegmentSupport(c.a, c.b))
3029 }
3030 }
3031 Self::Box {
3032 center,
3033 half,
3034 rotation,
3035 } => {
3036 if point_core {
3037 let (o, dl) = to_local(a, d, *center, *rotation);
3038 let h = to_world(sweep_local_box(o, dl, *half, r, max_t), *rotation)?;
3039 self.entering(Contact::from_ray(a, d, r, h), a, b, r, d, max_t)
3040 } else {
3041 let obb = crate::box_collider::OrientedBox::new(*center, *half, *rotation);
3042 toi_convex(a, b, r, Fix128::ZERO, d, max_t, &obb)
3043 }
3044 }
3045 Self::Posed(posed) => {
3046 let (center, rotation) = posed_frame(posed);
3047 match posed.shape {
3048 Shape::Cylinder {
3049 radius,
3050 half_height,
3051 } if point_core => {
3052 let (o, dl) = to_local(a, d, center, rotation);
3053 let found = to_world(
3054 sweep_local_cylinder(o, dl, radius, half_height, r, max_t),
3055 rotation,
3056 );
3057 let cap = r + r + Fix128::ONE;
3058 let checked = found.and_then(|h| {
3064 if h.t.is_zero() {
3065 return Some(Contact::from_ray(a, d, r, h));
3066 }
3067 let c = a + d * h.t;
3068 match self.dist(c, c, cap) {
3069 Dist::Outside {
3070 dist,
3071 point,
3072 normal,
3073 } if dist - r >= -TRACE_TOLERANCE => Some(Contact {
3074 t: h.t,
3075 point,
3076 normal,
3077 }),
3078 _ => None,
3079 }
3080 });
3081 match checked {
3082 Some(c) => self.entering(c, a, b, r, d, max_t),
3083 None => toi_by(a, b, r, Fix128::ZERO, d, max_t, |p, q, _| {
3084 let found = self.dist(p, q, cap);
3085 let bound = Bound::exact(&found);
3086 (found, bound)
3087 }),
3088 }
3089 }
3090 Shape::Torus {
3091 major_radius,
3092 minor_radius,
3093 } => {
3094 let ring = Ring {
3095 center,
3096 rotation,
3097 major: major_radius,
3098 minor: minor_radius,
3099 };
3100 if point_core {
3101 let (o, dl) = to_local(a, d, center, rotation);
3102 let h = ray_local_torus(o, dl, major_radius, minor_radius + r, max_t);
3103 let c = Contact::from_ray(a, d, r, to_world(h, rotation)?);
3104 self.entering(c, a, b, r, d, max_t)
3107 .or_else(|| ring.sweep(a, b, r, d, max_t))
3108 } else {
3109 ring.sweep(a, b, r, d, max_t)
3110 }
3111 }
3112 _ if is_round(posed) => {
3113 toi_convex(a, b, r, Fix128::ZERO, d, max_t, &RoundSupport(posed))
3114 }
3115 Shape::Cylinder { .. } | Shape::Cone { .. } => toi_convex_refined(
3116 a,
3117 b,
3118 r,
3119 Fix128::ZERO,
3120 d,
3121 max_t,
3122 posed,
3123 round_support_slack(posed),
3124 |p, q| exact_round_dist(posed, p, q),
3125 ),
3126 _ => toi_convex(a, b, r, Fix128::ZERO, d, max_t, posed),
3127 }
3128 }
3129 Self::Hull(child) => toi_convex(a, b, r, Fix128::ZERO, d, max_t, child),
3130 Self::Plane(plane) => sweep_plane(plane, a, b, r, d, max_t),
3131 Self::Height(field) => sweep_heightfield(field, a, b, r, d, max_t),
3132 Self::Mesh(mesh) => {
3133 if point_core {
3134 let swept = core_box(a, a + d * max_t, r);
3135 let mut best: Option<Contact> = None;
3136 for i in mesh_candidates(mesh, &swept) {
3137 let tri = &mesh.triangles[i as usize];
3138 let Some(h) = sweep_triangle(a, d, tri, r, max_t) else {
3139 continue;
3140 };
3141 let gap_at = |t: Fix128| {
3142 let p = a + d * t;
3143 match Dist::toward(p, tri.closest_point(p)) {
3144 Dist::Outside { dist, .. } => Some(dist - r),
3145 _ => None,
3146 }
3147 };
3148 let mut c = Contact::from_ray(a, d, r, h);
3149 if c.t <= TRACE_TOLERANCE {
3150 if let Dist::Outside { point, normal, .. } =
3153 Dist::toward(a, tri.closest_point(a))
3154 {
3155 c = Contact {
3156 t: Fix128::ZERO,
3157 point,
3158 normal,
3159 };
3160 }
3161 }
3162 if !touch_is_hit(c, d, max_t, gap_at) {
3163 continue;
3164 }
3165 if best.is_none_or(|bc| c.t < bc.t) {
3166 best = Some(c);
3167 }
3168 }
3169 best
3170 } else {
3171 sweep_mesh(mesh, a, b, r, d, max_t)
3172 }
3173 }
3174 #[cfg(feature = "std")]
3175 Self::Sdf(_) => {
3176 let tol = Fix128::from_f32(settings.sdf.tolerance);
3177 self.trace(a, b, r, d, max_t, tol)
3178 }
3179 }
3180 .filter(|c| c.t >= Fix128::ZERO && c.t <= max_t)
3181 }
3182
3183 fn entering(
3190 &self,
3191 c: Contact,
3192 a: Vec3Fix,
3193 b: Vec3Fix,
3194 r: Fix128,
3195 d: Vec3Fix,
3196 max_t: Fix128,
3197 ) -> Option<Contact> {
3198 let cap = r + r + Fix128::ONE;
3199 let gap_at = |t: Fix128| {
3200 let off = d * t;
3201 match self.dist(a + off, b + off, cap) {
3202 Dist::Outside { dist, .. } => Some(dist - r),
3203 Dist::AtLeast(bound) => Some(bound - r),
3204 Dist::Inside => None,
3205 }
3206 };
3207 let c = if c.t > TRACE_TOLERANCE {
3208 c
3209 } else {
3210 match self.dist(a, b, cap) {
3211 Dist::Outside { point, normal, .. } => Contact {
3212 t: Fix128::ZERO,
3213 point,
3214 normal,
3215 },
3216 _ => return Some(c),
3217 }
3218 };
3219 touch_is_hit(c, d, max_t, gap_at).then_some(c)
3220 }
3221
3222 #[cfg(feature = "std")]
3227 fn trace(
3228 &self,
3229 a: Vec3Fix,
3230 b: Vec3Fix,
3231 r: Fix128,
3232 d: Vec3Fix,
3233 max_t: Fix128,
3234 tol: Fix128,
3235 ) -> Option<Contact> {
3236 let mut t = Fix128::ZERO;
3237 let cap = r + r + Fix128::ONE;
3238 let mut last: Option<Contact> = None;
3239 for _ in 0..SDF_MAX_STEPS {
3240 let off = d * t;
3241 let gap = match self.dist(a + off, b + off, cap) {
3242 Dist::Inside => {
3243 return Some(Contact {
3244 t,
3245 point: half_vec(a + b) + off,
3246 normal: -d,
3247 })
3248 }
3249 Dist::AtLeast(bound) => bound - r,
3250 Dist::Outside {
3251 dist,
3252 point,
3253 normal,
3254 } => {
3255 let gap = dist - r;
3256 let contact = Contact { t, point, normal };
3257 last = Some(contact);
3258 if gap <= tol {
3259 if d.dot(normal).is_negative() {
3262 return Some(contact);
3263 }
3264 tol
3265 } else {
3266 gap
3267 }
3268 }
3269 };
3270 if gap <= Fix128::ZERO {
3271 return None;
3272 }
3273 t = t + gap;
3274 if t > max_t {
3275 return None;
3276 }
3277 }
3278 last
3279 }
3280
3281 fn meets_aabb(&self, aabb: &AABB) -> bool {
3283 match self {
3284 Self::Sphere { center, radius } => {
3285 (closest_on_aabb(aabb, *center) - *center).length_squared() < *radius * *radius
3286 }
3287 Self::Capsule(c) => match gjk_distance(&SegmentSupport(c.a, c.b), aabb) {
3288 None => true,
3289 Some((dist, _, _)) => dist < c.radius,
3290 },
3291 Self::Box {
3292 center,
3293 half,
3294 rotation,
3295 } => obb_meets_aabb(*center, *half, *rotation, aabb),
3296 Self::Posed(posed) => match posed.shape {
3297 Shape::Torus {
3298 major_radius,
3299 minor_radius,
3300 } => torus_meets_aabb(posed, major_radius, minor_radius, aabb),
3301 _ => gjk_distance(posed, aabb).is_none(),
3302 },
3303 Self::Hull(child) => gjk_distance(child, aabb).is_none(),
3304 Self::Plane(plane) => {
3305 let c = half_vec(aabb.min + aabb.max);
3306 let h = half_vec(aabb.max - aabb.min);
3307 let n = plane.normal;
3308 let reach = n.x.abs() * h.x + n.y.abs() * h.y + n.z.abs() * h.z;
3309 (n.dot(c) - plane.offset).abs() < reach
3310 }
3311 Self::Height(field) => heightfield_meets_aabb(field, aabb),
3312 Self::Mesh(mesh) => mesh_candidates(mesh, aabb).into_iter().any(|i| {
3313 gjk_distance(&TriangleSupport(&mesh.triangles[i as usize]), aabb).is_none()
3314 }),
3315 #[cfg(feature = "std")]
3316 Self::Sdf(sdf) => sdf_meets_aabb(sdf, aabb),
3317 }
3318 }
3319}
3320
3321#[derive(Clone, Copy, Debug)]
3323struct Contact {
3324 t: Fix128,
3325 point: Vec3Fix,
3326 normal: Vec3Fix,
3327}
3328
3329impl Contact {
3330 fn from_ray(o: Vec3Fix, d: Vec3Fix, r: Fix128, h: LocalHit) -> Self {
3333 Self {
3334 t: h.t,
3335 point: o + d * h.t - h.normal * r,
3336 normal: h.normal,
3337 }
3338 }
3339}
3340
3341fn sweep_local_box(
3344 o: Vec3Fix,
3345 d: Vec3Fix,
3346 h: Vec3Fix,
3347 r: Fix128,
3348 max_t: Fix128,
3349) -> Option<LocalHit> {
3350 let mut best = None;
3351 for grow in [
3352 Vec3Fix::new(r, Fix128::ZERO, Fix128::ZERO),
3353 Vec3Fix::new(Fix128::ZERO, r, Fix128::ZERO),
3354 Vec3Fix::new(Fix128::ZERO, Fix128::ZERO, r),
3355 ] {
3356 best = nearer(best, ray_planes(o, d, &box_planes(h + grow), max_t));
3357 }
3358 let signs = [Fix128::ONE, Fix128::NEG_ONE];
3359 for &s1 in &signs {
3360 for &s2 in &signs {
3361 for (a, b) in [
3362 (
3363 Vec3Fix::new(-h.x, s1 * h.y, s2 * h.z),
3364 Vec3Fix::new(h.x, s1 * h.y, s2 * h.z),
3365 ),
3366 (
3367 Vec3Fix::new(s1 * h.x, -h.y, s2 * h.z),
3368 Vec3Fix::new(s1 * h.x, h.y, s2 * h.z),
3369 ),
3370 (
3371 Vec3Fix::new(s1 * h.x, s2 * h.y, -h.z),
3372 Vec3Fix::new(s1 * h.x, s2 * h.y, h.z),
3373 ),
3374 ] {
3375 best = nearer(best, ray_solid_capsule(o, d, &Capsule::new(a, b, r), max_t));
3376 }
3377 }
3378 }
3379 best
3380}
3381
3382fn sweep_local_cylinder(
3395 o: Vec3Fix,
3396 d: Vec3Fix,
3397 radius: Fix128,
3398 hh: Fix128,
3399 r: Fix128,
3400 max_t: Fix128,
3401) -> Option<LocalHit> {
3402 let mut best = ray_local_cylinder(o, d, radius + r, hh, max_t);
3403 best = nearer(best, ray_local_cylinder(o, d, radius, hh + r, max_t));
3404 for y in [hh, -hh] {
3405 let shift = Vec3Fix::new(Fix128::ZERO, y, Fix128::ZERO);
3406 best = nearer(best, ray_local_torus(o - shift, d, radius, r, max_t));
3407 }
3408 best
3409}
3410
3411fn obb_meets_aabb(center: Vec3Fix, half: Vec3Fix, rotation: QuatFix, aabb: &AABB) -> bool {
3417 let z = Vec3Fix::new(Fix128::ZERO, Fix128::ZERO, Fix128::ONE);
3418 let a_axes = [
3419 rotation.rotate_vec(Vec3Fix::UNIT_X),
3420 rotation.rotate_vec(Vec3Fix::UNIT_Y),
3421 rotation.rotate_vec(z),
3422 ];
3423 let a_half = [half.x, half.y, half.z];
3424 let b_axes = [Vec3Fix::UNIT_X, Vec3Fix::UNIT_Y, z];
3425 let b_center = half_vec(aabb.min + aabb.max);
3426 let b_ext = half_vec(aabb.max - aabb.min);
3427 let b_half = [b_ext.x, b_ext.y, b_ext.z];
3428 let between = b_center - center;
3429 let separates = |l: Vec3Fix| {
3430 let reach_a: Fix128 = (0..3).fold(Fix128::ZERO, |acc, i| {
3431 acc + a_half[i] * a_axes[i].dot(l).abs()
3432 });
3433 let reach_b: Fix128 = (0..3).fold(Fix128::ZERO, |acc, i| {
3434 acc + b_half[i] * b_axes[i].dot(l).abs()
3435 });
3436 between.dot(l).abs() >= reach_a + reach_b
3437 };
3438 let mut axes: Vec<Vec3Fix> = Vec::with_capacity(15);
3439 axes.extend_from_slice(&a_axes);
3440 axes.extend_from_slice(&b_axes);
3441 for ea in a_axes {
3442 for eb in b_axes {
3443 let l = ea.cross(eb);
3444 if !l.length_squared().is_zero() {
3445 axes.push(l);
3446 }
3447 }
3448 }
3449 !axes.into_iter().any(separates)
3450}
3451
3452fn plane_dist(plane: &PlaneCollider, a: Vec3Fix, b: Vec3Fix) -> Dist {
3454 let n = plane.normal;
3455 let sa = n.dot(a) - plane.offset;
3456 let sb = n.dot(b) - plane.offset;
3457 if !sa.is_zero() && !sb.is_zero() && sa.is_negative() != sb.is_negative() {
3458 return Dist::Inside;
3460 }
3461 let (p, s) = if sb.abs() < sa.abs() {
3462 (b, sb)
3463 } else {
3464 (a, sa)
3465 };
3466 let other = if sb.abs() < sa.abs() { sa } else { sb };
3468 let side = if s.is_negative() || (s.is_zero() && other.is_negative()) {
3469 -n
3470 } else {
3471 n
3472 };
3473 Dist::Outside {
3474 dist: s.abs(),
3475 point: p - n * s,
3476 normal: side,
3477 }
3478}
3479
3480fn sweep_plane(
3483 plane: &PlaneCollider,
3484 a: Vec3Fix,
3485 b: Vec3Fix,
3486 r: Fix128,
3487 d: Vec3Fix,
3488 max_t: Fix128,
3489) -> Option<Contact> {
3490 let n = plane.normal;
3491 let sa = n.dot(a) - plane.offset;
3492 let sb = n.dot(b) - plane.offset;
3493 if sa.is_zero() || sb.is_zero() || sa.is_negative() != sb.is_negative() {
3494 return None;
3495 }
3496 let (p, s) = if sb.abs() < sa.abs() {
3498 (b, sb)
3499 } else {
3500 (a, sa)
3501 };
3502 let normal = if s.is_negative() { -n } else { n };
3503 let speed = -normal.dot(d);
3504 if speed <= Fix128::ZERO {
3505 return None;
3506 }
3507 let gap = s.abs() - r;
3508 if speed < PARALLEL_EPSILON && gap - speed * max_t >= -TRACE_TOLERANCE {
3512 return None;
3513 }
3514 let t = max_fix(ratio_within(gap, speed, max_t + Fix128::ONE), Fix128::ZERO);
3517 if t > max_t {
3518 return None;
3519 }
3520 Some(Contact {
3521 t,
3522 point: p + d * t - normal * r,
3523 normal,
3524 })
3525}
3526
3527fn torus_meets_aabb(posed: &PosedShape, major: Fix128, minor: Fix128, aabb: &AABB) -> bool {
3529 let (center, rotation) = posed_frame(posed);
3530 if (closest_on_aabb(aabb, center) - center).length() >= major + minor {
3532 return false;
3533 }
3534 let ring = |s: Fix128| {
3535 let (sin, cos) = (Fix128::TWO_PI * s).sin_cos();
3536 center + rotation.rotate_vec(Vec3Fix::new(major * cos, Fix128::ZERO, major * sin))
3537 };
3538 let d = curve_min(Fix128::TWO_PI * major, |s| {
3539 let p = ring(s);
3540 let q = closest_on_aabb(aabb, p);
3541 if q == p {
3542 Dist::Inside
3543 } else {
3544 Dist::toward(p, q)
3545 }
3546 });
3547 d.within(minor)
3548}
3549
3550#[cfg(feature = "std")]
3551fn point_sdf(sdf: &crate::sdf_collider::SdfCollider, p: Vec3Fix) -> Dist {
3552 let (lx, ly, lz) = sdf.world_to_local(p);
3553 let (d, (nx, ny, nz)) = sdf.field.distance_and_normal(lx, ly, lz);
3554 let d = d * sdf.scale_f32;
3555 if !d.is_finite() || d < 0.0 {
3556 return Dist::Inside;
3557 }
3558 let dist = Fix128::from_f32(d);
3559 let normal = sdf.local_normal_to_world(nx, ny, nz);
3560 Dist::Outside {
3561 dist,
3562 point: p - normal * dist,
3563 normal,
3564 }
3565}
3566
3567#[cfg(feature = "std")]
3572fn sdf_meets_aabb(sdf: &crate::sdf_collider::SdfCollider, aabb: &AABB) -> bool {
3573 let mut stack = vec![(aabb.min, aabb.max, 0u32)];
3574 while let Some((lo, hi, depth)) = stack.pop() {
3575 let c = half_vec(lo + hi);
3576 let half_diag = half_vec(hi - lo).length();
3577 let d = match point_sdf(sdf, c) {
3578 Dist::Inside => return true,
3579 Dist::Outside { dist, .. } => dist,
3580 Dist::AtLeast(d) => d,
3581 };
3582 if d >= half_diag {
3583 continue;
3584 }
3585 if depth >= SDF_BOX_DEPTH {
3586 return true;
3587 }
3588 for k in 0..8u8 {
3589 let pick = |bit: u8, l: Fix128, m: Fix128, h: Fix128| {
3590 if k & bit == 0 {
3591 (l, m)
3592 } else {
3593 (m, h)
3594 }
3595 };
3596 let (x0, x1) = pick(1, lo.x, c.x, hi.x);
3597 let (y0, y1) = pick(2, lo.y, c.y, hi.y);
3598 let (z0, z1) = pick(4, lo.z, c.z, hi.z);
3599 stack.push((
3600 Vec3Fix::new(x0, y0, z0),
3601 Vec3Fix::new(x1, y1, z1),
3602 depth + 1,
3603 ));
3604 }
3605 }
3606 false
3607}
3608
3609fn core_box(a: Vec3Fix, b: Vec3Fix, r: Fix128) -> AABB {
3611 let lo = Vec3Fix::new(min_fix(a.x, b.x), min_fix(a.y, b.y), min_fix(a.z, b.z));
3612 let hi = Vec3Fix::new(max_fix(a.x, b.x), max_fix(a.y, b.y), max_fix(a.z, b.z));
3613 let g = Vec3Fix::new(r, r, r);
3614 AABB::new(lo - g, hi + g)
3615}
3616
3617fn aabb_gap(a: &AABB, b: &AABB) -> Fix128 {
3619 let gap = |lo1: Fix128, hi1: Fix128, lo2: Fix128, hi2: Fix128| {
3620 max_fix(Fix128::ZERO, max_fix(lo2 - hi1, lo1 - hi2))
3621 };
3622 Vec3Fix::new(
3623 gap(a.min.x, a.max.x, b.min.x, b.max.x),
3624 gap(a.min.y, a.max.y, b.min.y, b.max.y),
3625 gap(a.min.z, a.max.z, b.min.z, b.max.z),
3626 )
3627 .length()
3628}
3629
3630fn tame_direction(direction: Vec3Fix) -> Vec3Fix {
3634 let m = max_fix(
3635 max_fix(direction.x.abs(), direction.y.abs()),
3636 direction.z.abs(),
3637 );
3638 if m > Fix128::from_int(1 << 20) {
3639 direction / m
3640 } else {
3641 direction
3642 }
3643}
3644
3645fn box_is_valid(aabb: &AABB) -> bool {
3646 aabb.min.x <= aabb.max.x && aabb.min.y <= aabb.max.y && aabb.min.z <= aabb.max.z
3647}
3648
3649fn for_each_target<'w>(
3656 world: &'w PhysicsWorld,
3657 filter: &RayFilter,
3658 query: &AABB,
3659 mut visit: impl FnMut(RayTarget, Option<usize>, &[Piece<'w>]),
3660) {
3661 let (bvh, _) = world.query_body_bvh(filter);
3662 let mut bodies: Vec<u32> = bvh.query(query);
3663 bodies.sort_unstable();
3664 bodies.dedup();
3665 for i in bodies {
3666 let i = i as usize;
3667 let pieces = body_pieces(world, i);
3668 visit(RayTarget::Body(i), Some(i), &pieces);
3669 }
3670 if filter.include_static {
3671 for (j, collider) in world.static_colliders_slice().iter().enumerate() {
3672 let piece = match collider {
3673 StaticCollider::Plane(p) => Piece::Plane(p),
3674 StaticCollider::HeightField(f) => Piece::Height(f),
3675 StaticCollider::TriMesh(m) => Piece::Mesh(m),
3676 };
3677 visit(
3678 RayTarget::StaticCollider(j),
3679 None,
3680 core::slice::from_ref(&piece),
3681 );
3682 }
3683 }
3684 #[cfg(feature = "std")]
3685 if filter.include_sdf {
3686 for (k, sdf) in world.sdf_colliders.iter().enumerate() {
3687 let body = (sdf.body_index < world.bodies.len()).then_some(sdf.body_index);
3688 if body.is_some_and(|b| !filter.sees_body(world, b)) {
3689 continue;
3690 }
3691 let piece = Piece::Sdf(sdf);
3692 visit(RayTarget::Sdf(k), body, core::slice::from_ref(&piece));
3693 }
3694 }
3695}
3696
3697impl PhysicsWorld {
3698 #[must_use]
3704 pub fn cast_sphere(
3705 &self,
3706 center: Vec3Fix,
3707 radius: Fix128,
3708 direction: Vec3Fix,
3709 max_t: Fix128,
3710 filter: &RayFilter,
3711 ) -> Option<WorldShapeHit> {
3712 if radius.is_negative() {
3713 return None;
3714 }
3715 let direction = tame_direction(direction);
3716 if radius.is_zero() {
3717 return self
3718 .cast_ray(center, direction, max_t, filter)
3719 .map(|h| WorldShapeHit {
3720 t: h.t,
3721 point: h.point,
3722 normal: h.normal,
3723 target: h.target,
3724 body: h.body,
3725 });
3726 }
3727 self.cast_core(center, center, radius, direction, max_t, filter)
3728 }
3729
3730 #[must_use]
3735 pub fn cast_capsule(
3736 &self,
3737 a: Vec3Fix,
3738 b: Vec3Fix,
3739 radius: Fix128,
3740 direction: Vec3Fix,
3741 max_t: Fix128,
3742 filter: &RayFilter,
3743 ) -> Option<WorldShapeHit> {
3744 if a == b {
3745 return self.cast_sphere(a, radius, direction, max_t, filter);
3746 }
3747 if radius.is_negative() {
3748 return None;
3749 }
3750 self.cast_core(a, b, radius, tame_direction(direction), max_t, filter)
3751 }
3752
3753 #[must_use]
3757 pub fn overlap_sphere(
3758 &self,
3759 center: Vec3Fix,
3760 radius: Fix128,
3761 filter: &RayFilter,
3762 ) -> Vec<RayTarget> {
3763 let mut out = Vec::new();
3764 if radius.is_negative() {
3765 return out;
3766 }
3767 let query = core_box(center, center, radius);
3768 let cap = radius + radius + Fix128::ONE;
3769 for_each_target(self, filter, &query, |target, _, pieces| {
3770 if pieces
3771 .iter()
3772 .any(|p| p.dist(center, center, cap).within(radius))
3773 {
3774 out.push(target);
3775 }
3776 });
3777 out.sort_unstable();
3778 out
3779 }
3780
3781 #[must_use]
3785 pub fn overlap_aabb(&self, aabb: &AABB, filter: &RayFilter) -> Vec<RayTarget> {
3786 let mut out = Vec::new();
3787 if !box_is_valid(aabb) {
3788 return out;
3789 }
3790 for_each_target(self, filter, aabb, |target, _, pieces| {
3791 if pieces.iter().any(|p| p.meets_aabb(aabb)) {
3792 out.push(target);
3793 }
3794 });
3795 out.sort_unstable();
3796 out
3797 }
3798
3799 pub(crate) fn capsule_penetrations(
3805 &self,
3806 a: Vec3Fix,
3807 b: Vec3Fix,
3808 r: Fix128,
3809 filter: &RayFilter,
3810 ) -> Option<Vec<Penetration>> {
3811 let query = core_box(a, b, r);
3812 let cap = r + r + Fix128::ONE;
3813 let mut out = Vec::new();
3814 let mut core_inside = false;
3815 for_each_target(self, filter, &query, |target, _, pieces| {
3816 for piece in pieces {
3817 match piece.dist(a, b, cap) {
3818 Dist::Inside => core_inside = true,
3819 Dist::Outside { dist, normal, .. } if dist + TRACE_TOLERANCE < r => {
3820 out.push(Penetration {
3821 target,
3822 depth: r - dist,
3823 normal,
3824 })
3825 }
3826 _ => {}
3827 }
3828 }
3829 });
3830 if core_inside {
3831 return None;
3832 }
3833 out.sort_by_key(|x| x.target);
3834 Some(out)
3835 }
3836
3837 fn cast_core(
3844 &self,
3845 a: Vec3Fix,
3846 b: Vec3Fix,
3847 r: Fix128,
3848 direction: Vec3Fix,
3849 max_t: Fix128,
3850 filter: &RayFilter,
3851 ) -> Option<WorldShapeHit> {
3852 if max_t <= Fix128::ZERO {
3853 return None;
3854 }
3855 let d = direction.try_normalize()?;
3856 let settings = Settings {
3857 #[cfg(feature = "std")]
3858 sdf: filter.sdf,
3859 };
3860 let swept = {
3861 let start = core_box(a, b, r);
3862 let end = core_box(a + d * max_t, b + d * max_t, r);
3863 AABB::new(
3864 Vec3Fix::new(
3865 min_fix(start.min.x, end.min.x),
3866 min_fix(start.min.y, end.min.y),
3867 min_fix(start.min.z, end.min.z),
3868 ),
3869 Vec3Fix::new(
3870 max_fix(start.max.x, end.max.x),
3871 max_fix(start.max.y, end.max.y),
3872 max_fix(start.max.z, end.max.z),
3873 ),
3874 )
3875 };
3876 let start_cap = r + r + Fix128::ONE;
3877 let centre = half_vec(a + b);
3878 let mut best: Option<WorldShapeHit> = None;
3879 for_each_target(self, filter, &swept, |target, body, pieces| {
3880 let mut nearest: Option<Contact> = None;
3881 for piece in pieces {
3882 let c = if starts_overlapping(&piece.dist(a, b, start_cap), r) {
3883 Some(Contact {
3884 t: Fix128::ZERO,
3885 point: centre,
3886 normal: -d,
3887 })
3888 } else {
3889 piece.sweep(a, b, r, d, max_t, &settings)
3890 };
3891 if let Some(c) = c {
3892 if nearest.is_none_or(|n| c.t < n.t) {
3893 nearest = Some(c);
3894 }
3895 }
3896 }
3897 if let Some(c) = nearest {
3898 let hit = WorldShapeHit {
3899 t: c.t,
3900 point: c.point,
3901 normal: c.normal,
3902 target,
3903 body,
3904 };
3905 if best.is_none_or(|b| (hit.t, hit.target) < (b.t, b.target)) {
3906 best = Some(hit);
3907 }
3908 }
3909 });
3910 best
3911 }
3912}
3913
3914#[cfg(test)]
3915mod tests {
3916 use super::*;
3928 use crate::box_collider::OrientedBox;
3929 use crate::collider::{ConvexHull, Sphere};
3930 use crate::compound::CompoundShape;
3931 use crate::solver::{PhysicsConfig, RigidBody};
3932
3933 const EXACT: f64 = 1e-12;
3934 const ITER: f64 = 1e-8;
3935
3936 fn fx(v: f64) -> Fix128 {
3937 Fix128::from_f64(v)
3938 }
3939
3940 fn v3(x: f64, y: f64, z: f64) -> Vec3Fix {
3941 Vec3Fix::new(fx(x), fx(y), fx(z))
3942 }
3943
3944 fn p3(p: [f64; 3]) -> Vec3Fix {
3945 v3(p[0], p[1], p[2])
3946 }
3947
3948 fn world() -> PhysicsWorld {
3949 PhysicsWorld::new(PhysicsConfig::default())
3950 }
3951
3952 fn all() -> RayFilter {
3953 RayFilter::default()
3954 }
3955
3956 fn sphere_cast(
3957 w: &PhysicsWorld,
3958 c: [f64; 3],
3959 r: f64,
3960 d: [f64; 3],
3961 max: f64,
3962 ) -> Option<WorldShapeHit> {
3963 w.cast_sphere(p3(c), fx(r), p3(d), fx(max), &all())
3964 }
3965
3966 fn capsule_cast(
3967 w: &PhysicsWorld,
3968 a: [f64; 3],
3969 b: [f64; 3],
3970 r: f64,
3971 d: [f64; 3],
3972 max: f64,
3973 ) -> Option<WorldShapeHit> {
3974 w.cast_capsule(p3(a), p3(b), fx(r), p3(d), fx(max), &all())
3975 }
3976
3977 fn overlap_s(w: &PhysicsWorld, c: [f64; 3], r: f64) -> Vec<RayTarget> {
3978 w.overlap_sphere(p3(c), fx(r), &all())
3979 }
3980
3981 fn overlap_b(w: &PhysicsWorld, lo: [f64; 3], hi: [f64; 3]) -> Vec<RayTarget> {
3982 w.overlap_aabb(&AABB::new(p3(lo), p3(hi)), &all())
3983 }
3984
3985 fn unit(v: [f64; 3]) -> [f64; 3] {
3986 let l = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
3987 [v[0] / l, v[1] / l, v[2] / l]
3988 }
3989
3990 #[track_caller]
3991 fn assert_vec(got: Vec3Fix, want: [f64; 3], tol: f64, what: &str) {
3992 let g = [got.x.to_f64(), got.y.to_f64(), got.z.to_f64()];
3993 for k in 0..3 {
3994 assert!(
3995 (g[k] - want[k]).abs() < tol,
3996 "{what} {g:?} but the closed form is {want:?}"
3997 );
3998 }
3999 }
4000
4001 #[track_caller]
4003 fn assert_hit(
4004 hit: Option<WorldShapeHit>,
4005 target: RayTarget,
4006 t: f64,
4007 normal: [f64; 3],
4008 point: [f64; 3],
4009 tol: f64,
4010 ) {
4011 let h = hit.unwrap_or_else(|| panic!("expected a hit at t = {t}, got none"));
4012 assert_eq!(h.target, target, "target");
4013 assert!(
4014 (h.t.to_f64() - t).abs() < tol,
4015 "t = {} but the closed form is {t}",
4016 h.t.to_f64()
4017 );
4018 assert_vec(h.normal, unit(normal), tol, "normal");
4019 assert_vec(h.point, point, tol, "point");
4020 }
4021
4022 #[track_caller]
4026 fn assert_face_hit(
4027 hit: Option<WorldShapeHit>,
4028 target: RayTarget,
4029 t: f64,
4030 y: f64,
4031 x0: f64,
4032 x1: f64,
4033 ) {
4034 let h = hit.unwrap_or_else(|| panic!("expected a hit at t = {t}, got none"));
4035 assert_eq!(h.target, target, "target");
4036 assert!((h.t.to_f64() - t).abs() < ITER, "t = {}", h.t.to_f64());
4037 assert_vec(h.normal, [0.0, 1.0, 0.0], ITER, "normal");
4038 assert!(
4039 (h.point.y.to_f64() - y).abs() < ITER,
4040 "y = {}",
4041 h.point.y.to_f64()
4042 );
4043 let x = h.point.x.to_f64();
4044 assert!(x > x0 - ITER && x < x1 + ITER, "x = {x}");
4045 }
4046
4047 fn shaped(w: &mut PhysicsWorld, shape: Shape, pos: Vec3Fix) -> usize {
4048 w.add_shaped_body(&shape, Fix128::ONE, pos)
4049 .expect("valid shape")
4050 }
4051
4052 fn unit_box(w: &mut PhysicsWorld) -> usize {
4053 shaped(
4054 w,
4055 Shape::Box {
4056 half_extents: v3(1.0, 1.0, 1.0),
4057 },
4058 Vec3Fix::ZERO,
4059 )
4060 }
4061
4062 fn sphere_body(w: &mut PhysicsWorld, pos: Vec3Fix, r: f64) -> usize {
4063 w.add_body_with_radius(RigidBody::new_static(pos), fx(r))
4064 }
4065
4066 fn plane(w: &mut PhysicsWorld, normal: Vec3Fix, offset: f64) -> usize {
4067 w.add_static_collider(StaticCollider::Plane(PlaneCollider::new(
4068 normal,
4069 fx(offset),
4070 )))
4071 }
4072
4073 fn floor_mesh(w: &mut PhysicsWorld) -> usize {
4075 let verts = [
4076 v3(0.0, 0.0, 0.0),
4077 v3(4.0, 0.0, 0.0),
4078 v3(4.0, 0.0, 4.0),
4079 v3(0.0, 0.0, 4.0),
4080 ];
4081 w.add_static_collider(StaticCollider::TriMesh(TriMesh::from_indexed(
4082 &verts,
4083 &[0, 1, 2, 0, 2, 3],
4084 )))
4085 }
4086
4087 fn saddle_field(w: &mut PhysicsWorld) -> usize {
4090 let heights = vec![Fix128::ZERO, Fix128::ZERO, Fix128::ZERO, Fix128::ONE];
4091 w.add_static_collider(StaticCollider::HeightField(HeightField::new(
4092 heights,
4093 2,
4094 2,
4095 Fix128::ONE,
4096 Vec3Fix::ZERO,
4097 )))
4098 }
4099
4100 #[test]
4103 fn sphere_cast_against_a_sphere_body() {
4104 let mut w = world();
4105 let b = sphere_body(&mut w, Vec3Fix::ZERO, 1.0);
4106 assert_hit(
4109 sphere_cast(&w, [-10.0, 0.0, 0.0], 0.5, [2.0, 0.0, 0.0], 100.0),
4110 RayTarget::Body(b),
4111 8.5,
4112 [-1.0, 0.0, 0.0],
4113 [-1.0, 0.0, 0.0],
4114 EXACT,
4115 );
4116 let s = 1.89f64.sqrt();
4119 let n = [-s / 1.5, 0.6 / 1.5, 0.0];
4120 assert_hit(
4121 sphere_cast(&w, [-10.0, 0.6, 0.0], 0.5, [1.0, 0.0, 0.0], 100.0),
4122 RayTarget::Body(b),
4123 10.0 - s,
4124 n,
4125 n,
4126 EXACT,
4127 );
4128 assert_eq!(
4130 sphere_cast(&w, [-10.0, 1.6, 0.0], 0.5, [1.0, 0.0, 0.0], 100.0),
4131 None
4132 );
4133 assert_eq!(
4135 sphere_cast(&w, [-10.0, 0.0, 0.0], 0.5, [1.0, 0.0, 0.0], 5.0),
4136 None
4137 );
4138 let h = sphere_cast(&w, [-10.0, 0.0, 0.0], 0.5, [1.0, 0.0, 0.0], 100.0).expect("hit");
4139 assert_eq!(h.body, Some(b));
4140 }
4141
4142 #[test]
4143 fn degenerate_casts_give_no_hit() {
4144 let mut w = world();
4145 sphere_body(&mut w, Vec3Fix::ZERO, 1.0);
4146 let c = [-10.0, 0.0, 0.0];
4147 assert_eq!(sphere_cast(&w, c, -0.5, [1.0, 0.0, 0.0], 100.0), None);
4149 assert_eq!(sphere_cast(&w, c, 0.5, [0.0, 0.0, 0.0], 100.0), None);
4150 assert_eq!(sphere_cast(&w, c, 0.5, [1.0, 0.0, 0.0], 0.0), None);
4151 assert_eq!(sphere_cast(&w, c, 0.5, [1.0, 0.0, 0.0], -1.0), None);
4152 assert_eq!(
4153 capsule_cast(&w, c, [-10.0, 1.0, 0.0], -0.5, [1.0, 0.0, 0.0], 100.0),
4154 None
4155 );
4156 assert_eq!(
4157 capsule_cast(&w, c, [-10.0, 1.0, 0.0], 0.5, [1.0, 0.0, 0.0], 0.0),
4158 None
4159 );
4160 }
4161
4162 #[test]
4163 fn radius_zero_is_a_ray_and_a_huge_direction_is_normalized() {
4164 let mut w = world();
4165 let b = sphere_body(&mut w, Vec3Fix::ZERO, 1.0);
4166 assert_hit(
4168 sphere_cast(&w, [-10.0, 0.0, 0.0], 0.0, [1.0, 0.0, 0.0], 100.0),
4169 RayTarget::Body(b),
4170 9.0,
4171 [-1.0, 0.0, 0.0],
4172 [-1.0, 0.0, 0.0],
4173 EXACT,
4174 );
4175 assert_hit(
4177 sphere_cast(&w, [-10.0, 0.0, 0.0], 0.5, [1e7, 0.0, 0.0], 100.0),
4178 RayTarget::Body(b),
4179 8.5,
4180 [-1.0, 0.0, 0.0],
4181 [-1.0, 0.0, 0.0],
4182 EXACT,
4183 );
4184 }
4185
4186 #[test]
4187 fn a_cast_that_starts_overlapping_hits_at_zero() {
4188 let mut w = world();
4189 let b = sphere_body(&mut w, Vec3Fix::ZERO, 1.0);
4190 assert_hit(
4193 sphere_cast(&w, [0.5, 0.0, 0.0], 0.5, [0.0, 1.0, 0.0], 100.0),
4194 RayTarget::Body(b),
4195 0.0,
4196 [0.0, -1.0, 0.0],
4197 [0.5, 0.0, 0.0],
4198 EXACT,
4199 );
4200 assert_hit(
4202 capsule_cast(
4203 &w,
4204 [0.0, 0.0, 0.0],
4205 [0.0, 4.0, 0.0],
4206 0.5,
4207 [1.0, 0.0, 0.0],
4208 100.0,
4209 ),
4210 RayTarget::Body(b),
4211 0.0,
4212 [-1.0, 0.0, 0.0],
4213 [0.0, 2.0, 0.0],
4214 EXACT,
4215 );
4216 }
4217
4218 #[test]
4219 fn a_cast_that_starts_touching_hits_only_when_it_moves_in() {
4220 let mut w = world();
4221 let b = sphere_body(&mut w, Vec3Fix::ZERO, 1.0);
4222 let c = [-1.5, 0.0, 0.0];
4224 assert_hit(
4226 sphere_cast(&w, c, 0.5, [1.0, 0.0, 0.0], 100.0),
4227 RayTarget::Body(b),
4228 0.0,
4229 [-1.0, 0.0, 0.0],
4230 [-1.0, 0.0, 0.0],
4231 EXACT,
4232 );
4233 assert_eq!(sphere_cast(&w, c, 0.5, [-1.0, 0.0, 0.0], 100.0), None);
4235 assert_eq!(sphere_cast(&w, c, 0.5, [0.0, 1.0, 0.0], 100.0), None);
4236
4237 let mut w = world();
4239 let p = plane(&mut w, Vec3Fix::UNIT_Y, 0.0);
4240 let c = [1.0, 0.5, 2.0];
4241 assert_hit(
4242 sphere_cast(&w, c, 0.5, [0.0, -1.0, 0.0], 100.0),
4243 RayTarget::StaticCollider(p),
4244 0.0,
4245 [0.0, 1.0, 0.0],
4246 [1.0, 0.0, 2.0],
4247 EXACT,
4248 );
4249 assert_eq!(sphere_cast(&w, c, 0.5, [1.0, 0.0, 0.0], 100.0), None);
4250 assert_eq!(sphere_cast(&w, c, 0.5, [0.0, 1.0, 0.0], 100.0), None);
4251
4252 let mut w = world();
4254 let bx = unit_box(&mut w);
4255 let c = [0.2, 1.5, 0.3];
4256 assert_hit(
4257 sphere_cast(&w, c, 0.5, [0.0, -1.0, 0.0], 100.0),
4258 RayTarget::Body(bx),
4259 0.0,
4260 [0.0, 1.0, 0.0],
4261 [0.2, 1.0, 0.3],
4262 EXACT,
4263 );
4264 assert_eq!(sphere_cast(&w, c, 0.5, [1.0, 0.0, 0.0], 100.0), None);
4265 }
4266
4267 #[test]
4268 fn sphere_cast_against_box_face_edge_and_turned_box() {
4269 let mut w = world();
4270 let b = unit_box(&mut w);
4271 assert_hit(
4273 sphere_cast(&w, [-10.0, 0.2, 0.3], 0.5, [1.0, 0.0, 0.0], 100.0),
4274 RayTarget::Body(b),
4275 8.5,
4276 [-1.0, 0.0, 0.0],
4277 [-1.0, 0.2, 0.3],
4278 EXACT,
4279 );
4280 assert_hit(
4283 sphere_cast(&w, [-10.0, 1.3, 0.0], 0.5, [1.0, 0.0, 0.0], 100.0),
4284 RayTarget::Body(b),
4285 8.6,
4286 [-0.8, 0.6, 0.0],
4287 [-1.0, 1.0, 0.0],
4288 EXACT,
4289 );
4290 let mut w = world();
4293 let b = unit_box(&mut w);
4294 w.bodies[b].rotation = QuatFix::from_axis_angle(Vec3Fix::UNIT_Y, Fix128::HALF_PI.half());
4295 let s2 = 2f64.sqrt();
4296 assert_hit(
4297 sphere_cast(&w, [-10.0, 0.0, 0.0], 0.5, [1.0, 0.0, 0.0], 100.0),
4298 RayTarget::Body(b),
4299 9.5 - s2,
4300 [-1.0, 0.0, 0.0],
4301 [-s2, 0.0, 0.0],
4302 ITER,
4303 );
4304 }
4305
4306 #[test]
4307 fn sphere_cast_against_cylinder_side_cap_and_rim() {
4308 let mut w = world();
4309 let b = shaped(
4310 &mut w,
4311 Shape::Cylinder {
4312 radius: Fix128::ONE,
4313 half_height: Fix128::ONE,
4314 },
4315 Vec3Fix::ZERO,
4316 );
4317 assert_hit(
4319 sphere_cast(&w, [-10.0, 0.0, 0.0], 0.5, [1.0, 0.0, 0.0], 100.0),
4320 RayTarget::Body(b),
4321 8.5,
4322 [-1.0, 0.0, 0.0],
4323 [-1.0, 0.0, 0.0],
4324 EXACT,
4325 );
4326 assert_hit(
4328 sphere_cast(&w, [0.2, 10.0, 0.0], 0.5, [0.0, -1.0, 0.0], 100.0),
4329 RayTarget::Body(b),
4330 8.5,
4331 [0.0, 1.0, 0.0],
4332 [0.2, 1.0, 0.0],
4333 EXACT,
4334 );
4335 assert_hit(
4338 sphere_cast(&w, [1.3, 10.0, 0.0], 0.5, [0.0, -1.0, 0.0], 100.0),
4339 RayTarget::Body(b),
4340 8.6,
4341 [0.6, 0.8, 0.0],
4342 [1.0, 1.0, 0.0],
4343 EXACT,
4344 );
4345 assert_face_hit(
4347 capsule_cast(
4348 &w,
4349 [-0.5, 5.0, 0.0],
4350 [0.5, 5.0, 0.0],
4351 0.5,
4352 [0.0, -1.0, 0.0],
4353 100.0,
4354 ),
4355 RayTarget::Body(b),
4356 3.5,
4357 1.0,
4358 -0.5,
4359 0.5,
4360 );
4361 assert_eq!(
4363 overlap_s(&w, [0.0, 1.2, 0.0], 0.3),
4364 vec![RayTarget::Body(b)]
4365 );
4366 assert!(overlap_s(&w, [0.0, 1.2, 0.0], 0.1).is_empty());
4367 assert_eq!(
4369 overlap_s(&w, [0.0, 0.0, 0.0], 0.0),
4370 vec![RayTarget::Body(b)]
4371 );
4372 assert!(overlap_s(&w, [0.0, 1.0, 0.0], 0.0).is_empty());
4373 }
4374
4375 #[test]
4376 fn sphere_and_capsule_casts_against_an_ellipsoid() {
4377 let mut w = world();
4378 let b = shaped(
4379 &mut w,
4380 Shape::Ellipsoid {
4381 radii: v3(1.0, 2.0, 1.0),
4382 },
4383 Vec3Fix::ZERO,
4384 );
4385 assert_hit(
4387 sphere_cast(&w, [0.0, 10.0, 0.0], 0.5, [0.0, -1.0, 0.0], 100.0),
4388 RayTarget::Body(b),
4389 7.5,
4390 [0.0, 1.0, 0.0],
4391 [0.0, 2.0, 0.0],
4392 ITER,
4393 );
4394 assert_hit(
4396 capsule_cast(
4397 &w,
4398 [-10.0, -0.5, 0.0],
4399 [-10.0, 0.5, 0.0],
4400 0.5,
4401 [1.0, 0.0, 0.0],
4402 100.0,
4403 ),
4404 RayTarget::Body(b),
4405 8.5,
4406 [-1.0, 0.0, 0.0],
4407 [-1.0, 0.0, 0.0],
4408 ITER,
4409 );
4410 assert_eq!(
4412 sphere_cast(&w, [1.6, 10.0, 0.0], 0.5, [0.0, -1.0, 0.0], 100.0),
4413 None
4414 );
4415 assert_eq!(
4417 overlap_s(&w, [1.2, 0.0, 0.0], 0.3),
4418 vec![RayTarget::Body(b)]
4419 );
4420 assert!(overlap_s(&w, [1.2, 0.0, 0.0], 0.1).is_empty());
4421 assert_eq!(
4422 overlap_b(&w, [0.9, -0.1, -0.1], [1.5, 0.1, 0.1]),
4423 vec![RayTarget::Body(b)]
4424 );
4425 assert!(overlap_b(&w, [1.1, -0.1, -0.1], [1.5, 0.1, 0.1]).is_empty());
4426 }
4427
4428 #[test]
4429 fn casts_and_overlaps_against_a_torus() {
4430 let mut w = world();
4431 let b = shaped(
4433 &mut w,
4434 Shape::Torus {
4435 major_radius: fx(2.0),
4436 minor_radius: fx(0.5),
4437 },
4438 Vec3Fix::ZERO,
4439 );
4440 assert_hit(
4442 sphere_cast(&w, [2.0, 10.0, 0.0], 0.25, [0.0, -1.0, 0.0], 100.0),
4443 RayTarget::Body(b),
4444 9.25,
4445 [0.0, 1.0, 0.0],
4446 [2.0, 0.5, 0.0],
4447 EXACT,
4448 );
4449 assert_eq!(
4451 sphere_cast(&w, [0.0, 10.0, 0.0], 0.25, [0.0, -1.0, 0.0], 100.0),
4452 None
4453 );
4454 assert_hit(
4457 capsule_cast(
4458 &w,
4459 [1.5, 5.0, 0.0],
4460 [2.5, 5.0, 0.0],
4461 0.25,
4462 [0.0, -1.0, 0.0],
4463 100.0,
4464 ),
4465 RayTarget::Body(b),
4466 4.25,
4467 [0.0, 1.0, 0.0],
4468 [2.0, 0.5, 0.0],
4469 ITER,
4470 );
4471 assert!(overlap_s(&w, [0.0, 0.0, 0.0], 1.4).is_empty());
4473 assert_eq!(
4474 overlap_s(&w, [0.0, 0.0, 0.0], 1.6),
4475 vec![RayTarget::Body(b)]
4476 );
4477 assert_eq!(
4478 overlap_s(&w, [2.0, 0.0, 0.0], 0.1),
4479 vec![RayTarget::Body(b)]
4480 );
4481 assert!(overlap_b(&w, [-1.0, -1.0, -1.0], [1.0, 1.0, 1.0]).is_empty());
4485 assert_eq!(
4486 overlap_b(&w, [-1.2, -1.0, -1.2], [1.2, 1.0, 1.2]),
4487 vec![RayTarget::Body(b)]
4488 );
4489 assert!(overlap_b(&w, [5.0, 5.0, 5.0], [6.0, 6.0, 6.0]).is_empty());
4491 }
4492
4493 #[test]
4494 fn casts_and_overlaps_against_compound_children() {
4495 let mut w = world();
4496 let mut c = CompoundShape::new();
4500 for s in [3.0, -3.0] {
4501 c.add_box(
4502 OrientedBox::new(Vec3Fix::ZERO, v3(0.5, 0.5, 0.5), QuatFix::IDENTITY),
4503 v3(s, 0.0, 0.0),
4504 QuatFix::IDENTITY,
4505 );
4506 c.add_sphere(
4507 Sphere::new(Vec3Fix::ZERO, fx(0.5)),
4508 v3(0.0, 0.0, s),
4509 QuatFix::IDENTITY,
4510 );
4511 let mut cube = Vec::new();
4512 for x in [-0.5, 0.5] {
4513 for y in [-0.5, 0.5] {
4514 for z in [-0.5, 0.5] {
4515 cube.push(v3(x, y, z));
4516 }
4517 }
4518 }
4519 c.add_convex_hull(
4520 ConvexHull::new(cube),
4521 v3(0.0, s + s / 3.0, 0.0),
4522 QuatFix::IDENTITY,
4523 );
4524 }
4525 c.add_capsule(
4526 Capsule::new(v3(-1.5, 0.0, 0.0), v3(1.5, 0.0, 0.0), fx(0.5)),
4527 Vec3Fix::ZERO,
4528 QuatFix::IDENTITY,
4529 );
4530 let b = w
4531 .add_compound_body(&c, Fix128::ONE, Vec3Fix::ZERO)
4532 .expect("valid compound");
4533 let body = RayTarget::Body(b);
4534 assert_hit(
4536 sphere_cast(&w, [3.0, 10.0, 0.0], 0.25, [0.0, -1.0, 0.0], 100.0),
4537 body,
4538 9.25,
4539 [0.0, 1.0, 0.0],
4540 [3.0, 0.5, 0.0],
4541 EXACT,
4542 );
4543 assert_hit(
4545 sphere_cast(&w, [0.0, 10.0, 3.0], 0.25, [0.0, -1.0, 0.0], 100.0),
4546 body,
4547 9.25,
4548 [0.0, 1.0, 0.0],
4549 [0.0, 0.5, 3.0],
4550 EXACT,
4551 );
4552 assert_hit(
4555 sphere_cast(&w, [1.0, 0.0, 10.0], 0.25, [0.0, 0.0, -1.0], 100.0),
4556 body,
4557 9.25,
4558 [0.0, 0.0, 1.0],
4559 [1.0, 0.0, 0.5],
4560 EXACT,
4561 );
4562 assert_hit(
4565 sphere_cast(&w, [10.0, 4.0, 0.0], 0.25, [-1.0, 0.0, 0.0], 100.0),
4566 body,
4567 9.25,
4568 [1.0, 0.0, 0.0],
4569 [0.5, 4.0, 0.0],
4570 ITER,
4571 );
4572 assert_face_hit(
4574 capsule_cast(
4575 &w,
4576 [2.8, 5.0, 0.0],
4577 [3.2, 5.0, 0.0],
4578 0.25,
4579 [0.0, -1.0, 0.0],
4580 100.0,
4581 ),
4582 body,
4583 4.25,
4584 0.5,
4585 2.8,
4586 3.2,
4587 );
4588 assert_hit(
4591 capsule_cast(
4592 &w,
4593 [0.0, 2.0, -1.0],
4594 [0.0, 2.0, 1.0],
4595 0.25,
4596 [0.0, -1.0, 0.0],
4597 100.0,
4598 ),
4599 body,
4600 1.25,
4601 [0.0, 1.0, 0.0],
4602 [0.0, 0.5, 0.0],
4603 ITER,
4604 );
4605 assert_hit(
4607 capsule_cast(
4608 &w,
4609 [-1.0, 5.0, -3.0],
4610 [1.0, 5.0, -3.0],
4611 0.25,
4612 [0.0, -1.0, 0.0],
4613 100.0,
4614 ),
4615 body,
4616 4.25,
4617 [0.0, 1.0, 0.0],
4618 [0.0, 0.5, -3.0],
4619 EXACT,
4620 );
4621 assert!(overlap_s(&w, [2.1, 0.0, 0.0], 0.05).is_empty());
4625 assert_eq!(overlap_s(&w, [2.1, 0.0, 0.0], 0.45), vec![body]);
4626 assert_eq!(overlap_b(&w, [3.4, 0.4, -0.1], [3.6, 0.6, 0.1]), vec![body]);
4628 assert_eq!(overlap_b(&w, [-0.1, 0.4, 2.9], [0.1, 0.6, 3.1]), vec![body]);
4629 assert_eq!(overlap_b(&w, [1.0, 0.4, -0.1], [1.2, 0.6, 0.1]), vec![body]);
4630 assert_eq!(overlap_b(&w, [0.4, 4.4, -0.1], [0.6, 4.6, 0.1]), vec![body]);
4631 assert!(overlap_b(&w, [1.0, 2.0, 1.0], [2.0, 3.0, 2.0]).is_empty());
4632 }
4633
4634 #[test]
4635 fn casts_and_overlaps_against_a_plane() {
4636 let mut w = world();
4637 let p = plane(&mut w, Vec3Fix::UNIT_Y, 0.0);
4638 let sp = RayTarget::StaticCollider(p);
4639 assert_hit(
4641 sphere_cast(&w, [0.0, 5.0, 0.0], 0.5, [1.0, -1.0, 0.0], 100.0),
4642 sp,
4643 4.5 * 2f64.sqrt(),
4644 [0.0, 1.0, 0.0],
4645 [4.5, 0.0, 0.0],
4646 EXACT,
4647 );
4648 assert_hit(
4650 sphere_cast(&w, [0.0, -5.0, 0.0], 0.5, [0.0, 1.0, 0.0], 100.0),
4651 sp,
4652 4.5,
4653 [0.0, -1.0, 0.0],
4654 [0.0, 0.0, 0.0],
4655 EXACT,
4656 );
4657 assert_eq!(
4659 sphere_cast(&w, [0.0, 1.0, 0.0], 0.5, [1.0, 0.0, 0.0], 100.0),
4660 None
4661 );
4662 assert_hit(
4664 capsule_cast(
4665 &w,
4666 [-1.0, 5.0, 0.0],
4667 [1.0, 6.0, 0.0],
4668 0.5,
4669 [0.0, -1.0, 0.0],
4670 100.0,
4671 ),
4672 sp,
4673 4.5,
4674 [0.0, 1.0, 0.0],
4675 [-1.0, 0.0, 0.0],
4676 EXACT,
4677 );
4678 assert_hit(
4680 capsule_cast(
4681 &w,
4682 [0.0, -1.0, 0.0],
4683 [0.0, 1.0, 0.0],
4684 0.5,
4685 [1.0, 0.0, 0.0],
4686 100.0,
4687 ),
4688 sp,
4689 0.0,
4690 [-1.0, 0.0, 0.0],
4691 [0.0, 0.0, 0.0],
4692 EXACT,
4693 );
4694 assert_eq!(overlap_s(&w, [0.0, 0.4, 0.0], 0.5), vec![sp]);
4696 assert!(overlap_s(&w, [0.0, 0.6, 0.0], 0.5).is_empty());
4697 assert_eq!(overlap_b(&w, [0.0, -0.1, 0.0], [1.0, 0.1, 1.0]), vec![sp]);
4699 assert!(overlap_b(&w, [0.0, 0.0, 0.0], [1.0, 1.0, 1.0]).is_empty());
4700 }
4701
4702 #[test]
4703 fn casts_along_a_mesh_floor_cross_its_edges() {
4704 let mut w = world();
4705 let m = floor_mesh(&mut w);
4706 let sm = RayTarget::StaticCollider(m);
4707 assert_hit(
4709 sphere_cast(&w, [1.0, 3.0, 3.0], 0.5, [0.0, -1.0, 0.0], 100.0),
4710 sm,
4711 2.5,
4712 [0.0, 1.0, 0.0],
4713 [1.0, 0.0, 3.0],
4714 EXACT,
4715 );
4716 assert_hit(
4719 sphere_cast(&w, [-10.0, 0.3, 1.0], 0.5, [1.0, 0.0, 0.0], 100.0),
4720 sm,
4721 9.6,
4722 [-0.8, 0.6, 0.0],
4723 [0.0, 0.0, 1.0],
4724 EXACT,
4725 );
4726 assert_eq!(
4729 sphere_cast(&w, [1.0, 0.5, 3.0], 0.5, [1.0, 0.0, -1.0], 2.0),
4730 None
4731 );
4732 assert_eq!(
4733 capsule_cast(
4734 &w,
4735 [1.0, 0.5, 2.0],
4736 [1.0, 1.5, 2.0],
4737 0.5,
4738 [1.0, 0.0, 0.0],
4739 2.0
4740 ),
4741 None
4742 );
4743 assert_hit(
4745 sphere_cast(&w, [1.0, 0.5, 3.0], 0.5, [0.0, -1.0, 0.0], 100.0),
4746 sm,
4747 0.0,
4748 [0.0, 1.0, 0.0],
4749 [1.0, 0.0, 3.0],
4750 EXACT,
4751 );
4752 assert_hit(
4754 capsule_cast(
4755 &w,
4756 [2.0, 3.0, 1.0],
4757 [3.0, 4.0, 1.0],
4758 0.5,
4759 [0.0, -1.0, 0.0],
4760 100.0,
4761 ),
4762 sm,
4763 2.5,
4764 [0.0, 1.0, 0.0],
4765 [2.0, 0.0, 1.0],
4766 ITER,
4767 );
4768 assert_eq!(overlap_s(&w, [2.0, 0.3, 2.0], 0.5), vec![sm]);
4770 assert!(overlap_s(&w, [-0.6, 0.0, 2.0], 0.5).is_empty());
4771 assert_eq!(overlap_b(&w, [1.0, -0.1, 1.0], [2.0, 0.1, 2.0]), vec![sm]);
4772 assert!(overlap_b(&w, [1.0, 0.1, 1.0], [2.0, 1.0, 2.0]).is_empty());
4773 }
4774
4775 #[test]
4776 fn casts_and_overlaps_against_height_fields() {
4777 let mut w = world();
4779 let h = w.add_static_collider(StaticCollider::HeightField(HeightField::flat(
4780 5,
4781 5,
4782 Fix128::ONE,
4783 Vec3Fix::ZERO,
4784 fx(0.25),
4785 )));
4786 let sh = RayTarget::StaticCollider(h);
4787 assert_hit(
4788 sphere_cast(&w, [2.0, 5.0, 2.0], 0.5, [0.0, -1.0, 0.0], 100.0),
4789 sh,
4790 4.25,
4791 [0.0, 1.0, 0.0],
4792 [2.0, 0.25, 2.0],
4793 ITER,
4794 );
4795 assert_face_hit(
4797 capsule_cast(
4798 &w,
4799 [1.5, 5.0, 2.0],
4800 [2.5, 5.0, 2.0],
4801 0.5,
4802 [0.0, -1.0, 0.0],
4803 100.0,
4804 ),
4805 sh,
4806 4.25,
4807 0.25,
4808 1.5,
4809 2.5,
4810 );
4811 assert_eq!(
4813 sphere_cast(&w, [0.5, 0.75, 0.5], 0.5, [1.0, 0.0, 0.0], 3.0),
4814 None
4815 );
4816 assert_eq!(
4818 sphere_cast(&w, [10.0, 5.0, 10.0], 0.5, [0.0, -1.0, 0.0], 100.0),
4819 None
4820 );
4821 assert_eq!(overlap_s(&w, [2.0, 0.5, 2.0], 0.3), vec![sh]);
4822 assert!(overlap_s(&w, [2.0, 0.6, 2.0], 0.3).is_empty());
4823 assert_eq!(overlap_b(&w, [1.0, 0.2, 1.0], [3.0, 0.3, 3.0]), vec![sh]);
4824 assert!(overlap_b(&w, [1.0, 0.3, 1.0], [3.0, 1.0, 3.0]).is_empty());
4825 assert!(overlap_b(&w, [5.0, 0.0, 5.0], [6.0, 1.0, 6.0]).is_empty());
4826 }
4827
4828 #[test]
4829 fn casts_and_overlaps_against_a_twisted_height_field_cell() {
4830 let mut w = world();
4831 let h = saddle_field(&mut w);
4832 let sh = RayTarget::StaticCollider(h);
4833 let k = 1.5f64.sqrt();
4838 let n = [-0.5 / k, 1.0 / k, -0.5 / k];
4839 let r = 0.1;
4840 let cx = 0.5 + r * n[0];
4841 assert_hit(
4843 sphere_cast(&w, [cx, 5.0, cx], r, [0.0, -1.0, 0.0], 100.0),
4844 sh,
4845 5.0 - (0.25 + r * n[1]),
4846 n,
4847 [0.5, 0.25, 0.5],
4848 ITER,
4849 );
4850 let at = |s: f64| [0.5 + s * n[0], 0.25 + s * n[1], 0.5 + s * n[2]];
4853 assert_eq!(overlap_s(&w, at(0.09), 0.1), vec![sh]);
4854 assert!(overlap_s(&w, at(0.11), 0.1).is_empty());
4855 assert_eq!(overlap_b(&w, [0.9, 0.9, 0.9], [1.0, 2.0, 1.0]), vec![sh]);
4858 assert!(overlap_b(&w, [0.0, 0.9, 0.0], [0.1, 2.0, 0.1]).is_empty());
4859 }
4860
4861 #[cfg(feature = "std")]
4862 #[test]
4863 fn casts_and_overlaps_against_an_sdf() {
4864 use crate::sdf_collider::{ClosureSdf, SdfCollider};
4865 let mut w = world();
4866 w.sdf_colliders.push(SdfCollider::new_static(
4868 Box::new(ClosureSdf::new(
4869 |x, y, z| (x * x + y * y + z * z).sqrt() - 1.0,
4870 |x, y, z| {
4871 let l = (x * x + y * y + z * z).sqrt();
4872 (x / l, y / l, z / l)
4873 },
4874 )),
4875 Vec3Fix::ZERO,
4876 QuatFix::IDENTITY,
4877 ));
4878 let tol = f64::from(all().sdf.tolerance);
4879 let h = sphere_cast(&w, [-5.0, 0.0, 0.0], 0.5, [1.0, 0.0, 0.0], 100.0).expect("hit");
4881 assert_eq!(h.target, RayTarget::Sdf(0));
4882 assert!(h.t.to_f64() > 3.5 - tol - 1e-5 && h.t.to_f64() < 3.5 + 1e-5);
4883 assert_vec(h.normal, [-1.0, 0.0, 0.0], 1e-3, "normal");
4884 let h = capsule_cast(
4886 &w,
4887 [-5.0, -0.5, 0.0],
4888 [-5.0, 0.5, 0.0],
4889 0.5,
4890 [1.0, 0.0, 0.0],
4891 100.0,
4892 )
4893 .expect("hit");
4894 assert!(h.t.to_f64() > 3.5 - tol - 1e-4 && h.t.to_f64() < 3.5 + 1e-4);
4895 assert_eq!(
4897 sphere_cast(&w, [-5.0, 2.0, 0.0], 0.5, [1.0, 0.0, 0.0], 100.0),
4898 None
4899 );
4900 assert_eq!(overlap_s(&w, [1.2, 0.0, 0.0], 0.3), vec![RayTarget::Sdf(0)]);
4902 assert!(overlap_s(&w, [1.2, 0.0, 0.0], 0.1).is_empty());
4903 assert_eq!(
4904 overlap_b(&w, [0.8, -0.1, -0.1], [1.2, 0.1, 0.1]),
4905 vec![RayTarget::Sdf(0)]
4906 );
4907 assert!(overlap_b(&w, [2.0, 2.0, 2.0], [3.0, 3.0, 3.0]).is_empty());
4908 assert!(w
4910 .overlap_sphere(v3(1.2, 0.0, 0.0), fx(0.3), &all().with_sdf(false))
4911 .is_empty());
4912 }
4913
4914 #[test]
4915 fn capsule_cast_against_a_sphere_body_and_a_box() {
4916 let mut w = world();
4917 let b = sphere_body(&mut w, Vec3Fix::ZERO, 1.0);
4918 let s = 1.25f64.sqrt();
4921 let n = [1.0 / 1.5, s / 1.5, 0.0];
4922 assert_hit(
4923 capsule_cast(
4924 &w,
4925 [1.0, 5.0, 0.0],
4926 [3.0, 5.0, 0.0],
4927 0.5,
4928 [0.0, -1.0, 0.0],
4929 100.0,
4930 ),
4931 RayTarget::Body(b),
4932 5.0 - s,
4933 n,
4934 n,
4935 EXACT,
4936 );
4937 assert_hit(
4939 capsule_cast(
4940 &w,
4941 [-10.0, 0.0, 0.0],
4942 [-10.0, 0.0, 0.0],
4943 0.5,
4944 [1.0, 0.0, 0.0],
4945 100.0,
4946 ),
4947 RayTarget::Body(b),
4948 8.5,
4949 [-1.0, 0.0, 0.0],
4950 [-1.0, 0.0, 0.0],
4951 EXACT,
4952 );
4953 let mut w = world();
4954 let b = unit_box(&mut w);
4955 assert_face_hit(
4957 capsule_cast(
4958 &w,
4959 [-0.5, 5.0, 0.0],
4960 [0.5, 5.0, 0.0],
4961 0.5,
4962 [0.0, -1.0, 0.0],
4963 100.0,
4964 ),
4965 RayTarget::Body(b),
4966 3.5,
4967 1.0,
4968 -0.5,
4969 0.5,
4970 );
4971 assert_eq!(
4973 capsule_cast(
4974 &w,
4975 [-0.5, 5.0, 0.0],
4976 [0.5, 5.0, 0.0],
4977 0.5,
4978 [0.0, 1.0, 0.0],
4979 100.0
4980 ),
4981 None
4982 );
4983 }
4984
4985 #[test]
4986 fn overlaps_of_bodies_and_the_box_edge_gap() {
4987 let mut w = world();
4988 let s = sphere_body(&mut w, Vec3Fix::ZERO, 1.0);
4989 assert_eq!(
4991 overlap_s(&w, [1.4, 0.0, 0.0], 0.5),
4992 vec![RayTarget::Body(s)]
4993 );
4994 assert!(overlap_s(&w, [1.4, 0.0, 0.0], 0.3).is_empty());
4995 assert!(overlap_s(&w, [0.0, 0.0, 0.0], -1.0).is_empty());
4997 assert_eq!(
5000 overlap_b(&w, [0.5, 0.5, 0.5], [2.0, 2.0, 2.0]),
5001 vec![RayTarget::Body(s)]
5002 );
5003 assert!(overlap_b(&w, [0.6, 0.6, 0.6], [2.0, 2.0, 2.0]).is_empty());
5005 assert!(overlap_b(&w, [1.0, -1.0, -1.0], [-1.0, 1.0, 1.0]).is_empty());
5007
5008 let mut w = world();
5009 let b = unit_box(&mut w);
5010 assert_eq!(
5013 overlap_s(&w, [1.3, 1.3, 0.0], 0.5),
5014 vec![RayTarget::Body(b)]
5015 );
5016 assert!(overlap_s(&w, [1.3, 1.3, 0.0], 0.4).is_empty());
5017 w.bodies[b].rotation = QuatFix::from_axis_angle(Vec3Fix::UNIT_Y, Fix128::HALF_PI.half());
5020 assert_eq!(
5021 overlap_b(&w, [1.3, -0.5, -0.1], [2.0, 0.5, 0.1]),
5022 vec![RayTarget::Body(b)]
5023 );
5024 assert!(overlap_b(&w, [1.5, -0.5, -0.1], [2.0, 0.5, 0.1]).is_empty());
5025 }
5026
5027 #[test]
5028 fn filters_and_ties() {
5029 let mut w = world();
5030 let b = sphere_body(&mut w, Vec3Fix::ZERO, 1.0);
5031 let p = plane(&mut w, Vec3Fix::UNIT_Y, -5.0);
5032 let c = v3(0.0, 10.0, 0.0);
5036 let down = -Vec3Fix::UNIT_Y;
5037 let h = w
5038 .cast_sphere(c, fx(0.5), down, fx(100.0), &all())
5039 .expect("hit");
5040 assert_eq!(h.target, RayTarget::Body(b));
5041 let h = w
5042 .cast_sphere(c, fx(0.5), down, fx(100.0), &all().excluding_body(b))
5043 .expect("hit");
5044 assert_eq!(h.target, RayTarget::StaticCollider(p));
5045 assert!((h.t.to_f64() - 14.5).abs() < EXACT);
5046 assert_eq!(
5047 w.cast_sphere(
5048 c,
5049 fx(0.5),
5050 down,
5051 fx(100.0),
5052 &all().excluding_body(b).with_static(false)
5053 ),
5054 None
5055 );
5056 let mut w = world();
5059 let b0 = sphere_body(&mut w, Vec3Fix::ZERO, 1.0);
5060 let b1 = sphere_body(&mut w, Vec3Fix::ZERO, 1.0);
5061 let h = sphere_cast(&w, [-10.0, 0.0, 0.0], 0.5, [1.0, 0.0, 0.0], 100.0).expect("hit");
5062 assert_eq!(h.target, RayTarget::Body(b0));
5063 assert_eq!(
5064 overlap_s(&w, [0.0, 0.0, 0.0], 0.1),
5065 vec![RayTarget::Body(b0), RayTarget::Body(b1)]
5066 );
5067 }
5068
5069 #[test]
5070 fn capsule_penetrations_closed_forms() {
5071 let mut w = world();
5072 let p = plane(&mut w, Vec3Fix::UNIT_Y, 0.0);
5073 let pens = w
5076 .capsule_penetrations(v3(0.0, 0.2, 0.0), v3(0.0, 1.4, 0.0), fx(0.3), &all())
5077 .expect("the segment is outside");
5078 assert_eq!(pens.len(), 1);
5079 assert_eq!(pens[0].target, RayTarget::StaticCollider(p));
5080 assert!((pens[0].depth.to_f64() - 0.1).abs() < EXACT);
5081 assert_vec(pens[0].normal, [0.0, 1.0, 0.0], EXACT, "normal");
5082 assert!(w
5084 .capsule_penetrations(v3(0.0, 0.4, 0.0), v3(0.0, 1.4, 0.0), fx(0.3), &all())
5085 .expect("outside")
5086 .is_empty());
5087 assert!(w
5089 .capsule_penetrations(v3(0.0, -0.2, 0.0), v3(0.0, 1.4, 0.0), fx(0.3), &all())
5090 .is_none());
5091 }
5092
5093 #[test]
5094 fn helper_closed_forms() {
5095 let a = AABB::new(v3(0.0, 0.0, 0.0), v3(1.0, 1.0, 1.0));
5097 let b = AABB::new(v3(2.0, 0.5, 3.0), v3(3.0, 2.0, 4.0));
5098 assert!((aabb_gap(&a, &b).to_f64() - 5f64.sqrt()).abs() < EXACT);
5099 assert_eq!(aabb_gap(&a, &a), Fix128::ZERO);
5100 let target = AABB::new(v3(3.0, 0.0, 0.0), v3(4.0, 1.0, 1.0));
5103 let entry = box_entry(&a, Vec3Fix::UNIT_X, fx(10.0), &target).expect("meets");
5104 assert!((entry.to_f64() - 2.0).abs() < EXACT);
5105 assert_eq!(box_entry(&a, Vec3Fix::UNIT_X, Fix128::ONE, &target), None);
5106 let above = AABB::new(v3(3.0, 5.0, 0.0), v3(4.0, 6.0, 1.0));
5107 assert_eq!(box_entry(&a, Vec3Fix::UNIT_X, fx(10.0), &above), None);
5108 let behind = AABB::new(v3(-4.0, 0.0, 0.0), v3(-3.0, 1.0, 1.0));
5110 let entry = box_entry(&a, -Vec3Fix::UNIT_X, fx(10.0), &behind).expect("meets");
5111 assert!((entry.to_f64() - 3.0).abs() < EXACT);
5112 let tiny = Fix128 { hi: 0, lo: 1 };
5114 assert!((ratio_within(Fix128::ONE, fx(4.0), fx(5.0)).to_f64() - 0.25).abs() < EXACT);
5115 assert_eq!(ratio_within(Fix128::ONE, tiny, fx(5.0)), fx(5.0));
5116 assert_eq!(ratio_within(Fix128::NEG_ONE, tiny, fx(5.0)), fx(-5.0));
5117 assert_eq!(
5119 tame_direction(Vec3Fix::new(
5120 Fix128::from_int(1 << 21),
5121 Fix128::ZERO,
5122 Fix128::ZERO
5123 )),
5124 Vec3Fix::UNIT_X
5125 );
5126 assert_eq!(tame_direction(v3(3.0, 4.0, 0.0)), v3(3.0, 4.0, 0.0));
5127 assert!(box_is_valid(&a));
5128 assert!(!box_is_valid(&AABB::new(
5129 v3(0.0, 1.0, 0.0),
5130 v3(1.0, 0.0, 1.0)
5131 )));
5132 let (s0, s1) = (v3(0.0, 0.0, 0.0), v3(2.0, 0.0, 0.0));
5134 assert_eq!(
5135 closest_on_segment(s0, s1, v3(1.0, 5.0, 0.0)),
5136 v3(1.0, 0.0, 0.0)
5137 );
5138 assert_eq!(closest_on_segment(s0, s1, v3(-3.0, 1.0, 0.0)), s0);
5139 assert_eq!(closest_on_segment(s0, s0, v3(5.0, 5.0, 5.0)), s0);
5140 assert!(segment_segment(
5142 v3(-1.0, 0.0, 0.0),
5143 v3(1.0, 0.0, 0.0),
5144 v3(0.0, -1.0, 0.0),
5145 v3(0.0, 1.0, 0.0)
5146 )
5147 .is_none());
5148 let (d, _, _) = segment_segment(
5149 v3(-1.0, 0.0, 0.0),
5150 v3(1.0, 0.0, 0.0),
5151 v3(0.0, -1.0, 1.0),
5152 v3(0.0, 1.0, 1.0),
5153 )
5154 .expect("apart");
5155 assert!((d.to_f64() - 1.0).abs() < ITER);
5156 let (d, _, _) =
5157 segment_segment(s0, s0, v3(3.0, 4.0, 0.0), v3(3.0, 4.0, 0.0)).expect("apart");
5158 assert!((d.to_f64() - 5.0).abs() < EXACT);
5159 let (d, _, _) =
5160 segment_segment(v3(1.0, 3.0, 0.0), v3(1.0, 3.0, 0.0), s0, s1).expect("apart");
5161 assert!((d.to_f64() - 3.0).abs() < EXACT);
5162 let (d, _, _) =
5163 segment_segment(s0, s1, v3(1.0, 0.0, 2.0), v3(1.0, 0.0, 2.0)).expect("apart");
5164 assert!((d.to_f64() - 2.0).abs() < EXACT);
5165 let h = v3(1.0, 1.0, 1.0);
5168 match point_box_local(v3(1.0, 0.5, 0.5), h) {
5169 Dist::Outside { dist, normal, .. } => {
5170 assert_eq!(dist, Fix128::ZERO);
5171 assert_eq!(normal, Vec3Fix::UNIT_X);
5172 }
5173 other => panic!("expected touching, got {other:?}"),
5174 }
5175 assert!((point_box_local(v3(4.0, 5.0, 0.0), h).value().to_f64() - 5.0).abs() < EXACT);
5176 assert!(matches!(point_box_local(Vec3Fix::ZERO, h), Dist::Inside));
5177 match point_cylinder_local(v3(0.0, -1.0, 0.0), Fix128::ONE, Fix128::ONE) {
5179 Dist::Outside { dist, normal, .. } => {
5180 assert_eq!(dist, Fix128::ZERO);
5181 assert_eq!(normal, -Vec3Fix::UNIT_Y);
5182 }
5183 other => panic!("expected touching, got {other:?}"),
5184 }
5185 assert!(
5186 (point_cylinder_local(v3(3.0, 0.0, 0.0), Fix128::ONE, Fix128::ONE)
5187 .value()
5188 .to_f64()
5189 - 2.0)
5190 .abs()
5191 < EXACT
5192 );
5193 assert!(matches!(
5195 Dist::toward(v3(2.0, 0.0, 0.0), Vec3Fix::ZERO).shrunk(fx(3.0)),
5196 Dist::Inside
5197 ));
5198 assert_eq!(
5199 Dist::AtLeast(fx(2.0)).shrunk(Fix128::ONE).value(),
5200 Fix128::ONE
5201 );
5202 assert!(!Dist::AtLeast(Fix128::ZERO).within(fx(10.0)));
5203 assert!(Dist::Inside.within(Fix128::ZERO));
5204 assert_eq!(Dist::Inside.value(), Fix128::NEG_ONE);
5205 assert!(matches!(
5206 Dist::toward(Vec3Fix::ZERO, Vec3Fix::ZERO),
5207 Dist::Inside
5208 ));
5209 }
5210
5211 #[test]
5218 #[allow(clippy::disallowed_methods)] fn the_lower_bound_is_not_above_the_distance() {
5220 struct Rng(u64);
5221 impl Rng {
5222 fn u(&mut self) -> f64 {
5223 self.0 = self
5224 .0
5225 .wrapping_mul(6_364_136_223_846_793_005)
5226 .wrapping_add(1_442_695_040_888_963_407);
5227 ((self.0 >> 11) as f64) / ((1u64 << 53) as f64)
5228 }
5229 fn r(&mut self, a: f64, b: f64) -> f64 {
5230 a + (b - a) * self.u()
5231 }
5232 }
5233 fn seg2(p: (f64, f64), a: (f64, f64), b: (f64, f64)) -> f64 {
5234 let (ex, ey) = (b.0 - a.0, b.1 - a.1);
5235 let t = (((p.0 - a.0) * ex + (p.1 - a.1) * ey) / (ex * ex + ey * ey)).clamp(0.0, 1.0);
5236 ((p.0 - a.0 - t * ex).powi(2) + (p.1 - a.1 - t * ey).powi(2)).sqrt()
5237 }
5238 let dist_local = |cone: bool, r: f64, h: f64, l: [f64; 3]| {
5240 let rho = (l[0] * l[0] + l[2] * l[2]).sqrt();
5241 if cone {
5242 if l[1] >= -h && rho * 2.0 * h <= r * (h - l[1]) {
5243 return 0.0;
5244 }
5245 let p = (rho, l[1]);
5246 seg2(p, (0.0, h), (r, -h)).min(seg2(p, (r, -h), (0.0, -h)))
5247 } else {
5248 let (dx, dy) = ((rho - r).max(0.0), (l[1].abs() - h).max(0.0));
5249 (dx * dx + dy * dy).sqrt()
5250 }
5251 };
5252 let rot_inv = |q: [f64; 4], v: [f64; 3]| {
5253 let n = q.iter().map(|x| x * x).sum::<f64>();
5254 let (w, x, y, z) = (q[0], -q[1], -q[2], -q[3]);
5255 let t = [
5256 2.0 * (y * v[2] - z * v[1]),
5257 2.0 * (z * v[0] - x * v[2]),
5258 2.0 * (x * v[1] - y * v[0]),
5259 ];
5260 let o = [
5261 v[0] + w * t[0] + (y * t[2] - z * t[1]),
5262 v[1] + w * t[1] + (z * t[0] - x * t[2]),
5263 v[2] + w * t[2] + (x * t[1] - y * t[0]),
5264 ];
5265 [o[0] / (n * n), o[1] / (n * n), o[2] / (n * n)]
5266 };
5267 let rot = |q: [f64; 4], v: [f64; 3]| {
5268 let (w, x, y, z) = (q[0], q[1], q[2], q[3]);
5269 let t = [
5270 2.0 * (y * v[2] - z * v[1]),
5271 2.0 * (z * v[0] - x * v[2]),
5272 2.0 * (x * v[1] - y * v[0]),
5273 ];
5274 [
5275 v[0] + w * t[0] + (y * t[2] - z * t[1]),
5276 v[1] + w * t[1] + (z * t[0] - x * t[2]),
5277 v[2] + w * t[2] + (x * t[1] - y * t[0]),
5278 ]
5279 };
5280 let mut rng = Rng(0x1046_b0d5);
5281 let (mut checked, mut stalled) = (0usize, 0usize);
5282 for cone in [false, true] {
5283 for turned in [false, true] {
5284 let (r, h) = if cone { (1.0, 0.8) } else { (0.9, 0.7) };
5285 let shape = if cone {
5286 Shape::Cone {
5287 radius: fx(r),
5288 half_height: fx(h),
5289 }
5290 } else {
5291 Shape::Cylinder {
5292 radius: fx(r),
5293 half_height: fx(h),
5294 }
5295 };
5296 let rotation = if turned {
5297 let k = (0.36f64 + 0.49 + 0.04).sqrt();
5298 QuatFix::from_axis_angle(v3(0.6 / k, 0.7 / k, -0.2 / k), fx(0.9))
5299 } else {
5300 QuatFix::IDENTITY
5301 };
5302 let posed = PosedShape {
5303 shape,
5304 position: v3(0.25, -0.5, 0.75),
5305 rotation,
5306 };
5307 let (center, _) = posed_frame(&posed);
5308 let c = [center.x.to_f64(), center.y.to_f64(), center.z.to_f64()];
5309 let q = [
5310 rotation.w.to_f64(),
5311 rotation.x.to_f64(),
5312 rotation.y.to_f64(),
5313 rotation.z.to_f64(),
5314 ];
5315 let local = |p: [f64; 3]| rot_inv(q, [p[0] - c[0], p[1] - c[1], p[2] - c[2]]);
5316 let world = |l: [f64; 3]| {
5317 let w = rot(q, l);
5318 [w[0] + c[0], w[1] + c[1], w[2] + c[2]]
5319 };
5320 let slack = round_support_slack(&posed);
5321 for _ in 0..300 {
5322 let phi = rng.r(0.0, std::f64::consts::TAU);
5326 let pick = rng.u();
5327 let (rho, y) = if pick < 0.3 {
5328 (
5329 if cone { r * rng.u() } else { r },
5330 if cone { 0.0 } else { rng.r(-h, h) },
5331 )
5332 } else if pick < 0.6 {
5333 (rng.r(0.0, r), -h)
5334 } else if pick < 0.9 {
5335 (r, -h)
5336 } else if cone {
5337 (0.0, h)
5338 } else {
5339 (r, h)
5340 };
5341 let y = if cone && pick < 0.3 {
5342 h - 2.0 * h * rho / r
5343 } else {
5344 y
5345 };
5346 let off = [rng.r(-1.0, 1.0), rng.r(-1.0, 1.0), rng.r(-1.0, 1.0)];
5347 let e = 2f64.powi(-(20 + (rng.u() * 14.0) as i32));
5348 let p = [
5349 rho * phi.cos() + e * off[0],
5350 y + e * off[1],
5351 rho * phi.sin() + e * off[2],
5352 ];
5353 let (pa, pb) = if rng.u() < 0.5 {
5354 (p, p)
5355 } else if rng.u() < 0.5 {
5356 (p, [p[0], p[1] + 0.3 * y.signum(), p[2]])
5357 } else {
5358 let s = [rng.r(-0.2, 0.2), rng.r(-0.2, 0.2), rng.r(-0.2, 0.2)];
5359 (p, [p[0] + s[0], p[1] + s[1], p[2] + s[2]])
5360 };
5361 let (wa, wb) = (world(pa), world(pb));
5362 let (a, b) = (v3(wa[0], wa[1], wa[2]), v3(wb[0], wb[1], wb[2]));
5363 let (fa, fb) = (
5364 local([a.x.to_f64(), a.y.to_f64(), a.z.to_f64()]),
5365 local([b.x.to_f64(), b.y.to_f64(), b.z.to_f64()]),
5366 );
5367 let at = |s: f64| {
5370 dist_local(
5371 cone,
5372 r,
5373 h,
5374 [
5375 fa[0] + s * (fb[0] - fa[0]),
5376 fa[1] + s * (fb[1] - fa[1]),
5377 fa[2] + s * (fb[2] - fa[2]),
5378 ],
5379 )
5380 };
5381 let (mut lo, mut hi) = (0.0f64, 1.0f64);
5382 for _ in 0..200 {
5383 let (m1, m2) = (lo + (hi - lo) / 3.0, hi - (hi - lo) / 3.0);
5384 if at(m1) <= at(m2) {
5385 hi = m2
5386 } else {
5387 lo = m1
5388 }
5389 }
5390 let truth = at(0.5 * (lo + hi)).min(at(0.0)).min(at(1.0));
5391 let (found, bound) = convex_bounds(a, b, &posed, slack);
5392 if let Dist::Outside { dist, .. } = found {
5393 checked += 1;
5394 let (lower, dist) = (bound.lower.to_f64(), dist.to_f64());
5395 if dist - lower > 2f64.powi(-32) {
5396 stalled += 1;
5397 }
5398 assert!(
5400 lower <= truth + 2f64.powi(-50),
5401 "lower {lower:e} above the distance {truth:e} by {:e} (cone {cone}, turned {turned})",
5402 lower - truth
5403 );
5404 }
5405 }
5406 }
5407 }
5408 assert!(checked > 500, "{checked} outside");
5410 assert!(
5411 stalled > 0,
5412 "no sample with the lower bound below the distance"
5413 );
5414 }
5415
5416 #[test]
5422 fn a_tetrahedron_just_thicker_than_flat_holds_the_origin() {
5423 let vertex = |w: Vec3Fix| Vertex {
5424 w,
5425 a: w,
5426 b: Vec3Fix::ZERO,
5427 };
5428 for e in [17, 20, 24, 27] {
5429 let h = 1.0 / ((1u64 << e) as f64);
5430 let pts = [
5432 v3(-1.0, -1.0, -0.5 * h),
5433 v3(1.0, -1.0, -0.5 * h),
5434 v3(0.0, 1.0, -0.5 * h),
5435 v3(0.0, 0.0, 0.5 * h),
5436 ];
5437 let least = [(0, 1, 2, 3), (0, 2, 3, 1), (0, 3, 1, 2), (1, 3, 2, 0)]
5439 .iter()
5440 .map(|&(p, q, r, o)| {
5441 let n = (pts[q] - pts[p]).cross(pts[r] - pts[p]);
5442 (n.dot(pts[o] - pts[p]).abs() / n.length()).to_f64()
5443 })
5444 .fold(f64::MAX, f64::min);
5445 assert!(
5446 least > 2.0 / 4_294_967_296.0 && least < 1.0 / 32768.0,
5447 "least height {least:e} for h = 2^-{e}"
5448 );
5449 let [a, b, c, d] = pts.map(vertex);
5450 let got = closest_on_tetrahedron(a, b, c, d, 0);
5451 assert!(
5452 got.is_none(),
5453 "h = 2^-{e} (least height {least:e}): the origin is inside, got the face {:?}",
5454 got.map(|n| nearest_point(&n).length().to_f64())
5455 );
5456 }
5457 }
5458
5459 #[allow(clippy::disallowed_methods)] mod round_reference {
5468 use super::*;
5469
5470 pub(super) const TOL: f64 = 1.0 / 4_294_967_296.0;
5472
5473 #[derive(Clone, Copy, Debug)]
5477 pub(super) struct Solid {
5478 pub(super) cone: bool,
5479 pub(super) r: f64,
5480 pub(super) hh: f64,
5481 }
5482
5483 fn seg2(p: (f64, f64), a: (f64, f64), b: (f64, f64)) -> f64 {
5484 let (ex, ey) = (b.0 - a.0, b.1 - a.1);
5485 let u = (((p.0 - a.0) * ex + (p.1 - a.1) * ey) / (ex * ex + ey * ey)).clamp(0.0, 1.0);
5486 ((p.0 - a.0 - u * ex).powi(2) + (p.1 - a.1 - u * ey).powi(2)).sqrt()
5487 }
5488
5489 impl Solid {
5490 pub(super) fn shape(self) -> Shape {
5491 let (radius, half_height) = (fx(self.r), fx(self.hh));
5492 if self.cone {
5493 Shape::Cone {
5494 radius,
5495 half_height,
5496 }
5497 } else {
5498 Shape::Cylinder {
5499 radius,
5500 half_height,
5501 }
5502 }
5503 }
5504
5505 pub(super) fn signed(self, l: [f64; 3]) -> f64 {
5508 let (r, h) = (self.r, self.hh);
5509 let rho = (l[0] * l[0] + l[2] * l[2]).sqrt();
5510 if self.cone {
5511 let p = (rho, l[1]);
5512 let (a, b, c) = ((0.0, -h), (0.0, h), (r, -h));
5513 let to_side = seg2(p, b, c);
5514 let to_base = seg2(p, c, a);
5515 if p.1 >= -h && p.0 * 2.0 * h <= r * (h - p.1) {
5516 -to_side.min(to_base)
5517 } else {
5518 seg2(p, a, b).min(to_side).min(to_base)
5519 }
5520 } else {
5521 let (dx, dy) = (rho - r, l[1].abs() - h);
5522 let out = (dx.max(0.0).powi(2) + dy.max(0.0).powi(2)).sqrt();
5523 out + dx.max(dy).min(0.0)
5524 }
5525 }
5526 }
5527
5528 pub(super) fn rotate(q: [f64; 4], v: [f64; 3], inverse: bool) -> [f64; 3] {
5530 let s = if inverse { -1.0 } else { 1.0 };
5531 let (w, x, y, z) = (q[0], s * q[1], s * q[2], s * q[3]);
5532 let t = [
5533 2.0 * (y * v[2] - z * v[1]),
5534 2.0 * (z * v[0] - x * v[2]),
5535 2.0 * (x * v[1] - y * v[0]),
5536 ];
5537 [
5538 v[0] + w * t[0] + (y * t[2] - z * t[1]),
5539 v[1] + w * t[1] + (z * t[0] - x * t[2]),
5540 v[2] + w * t[2] + (x * t[1] - y * t[0]),
5541 ]
5542 }
5543
5544 pub(super) fn f3(v: Vec3Fix) -> [f64; 3] {
5545 [v.x.to_f64(), v.y.to_f64(), v.z.to_f64()]
5546 }
5547
5548 pub(super) struct Scene {
5550 pub(super) solid: Solid,
5551 pub(super) world: PhysicsWorld,
5552 pub(super) posed: PosedShape,
5553 position: [f64; 3],
5554 q: [f64; 4],
5555 }
5556
5557 impl Scene {
5558 pub(super) fn new(solid: Solid, position: [f64; 3], turn: QuatFix) -> Self {
5559 let mut world = world();
5560 let i = world.add_body(RigidBody::new_static(p3(position)));
5561 assert!(world.set_body_shape(i, &solid.shape()));
5562 world.bodies[i].rotation = turn;
5563 let r = world.bodies[i].rotation;
5564 Self {
5565 solid,
5566 world,
5567 posed: PosedShape {
5568 shape: solid.shape(),
5569 position: p3(position),
5570 rotation: r,
5571 },
5572 position: f3(p3(position)),
5573 q: [r.w.to_f64(), r.x.to_f64(), r.y.to_f64(), r.z.to_f64()],
5574 }
5575 }
5576
5577 pub(super) fn turned(
5579 solid: Solid,
5580 position: [f64; 3],
5581 axis: [f64; 3],
5582 angle: f64,
5583 ) -> Self {
5584 let turn = QuatFix::from_axis_angle(p3(unit(axis)), fx(angle));
5585 Self::new(solid, position, turn)
5586 }
5587
5588 pub(super) fn point(&self, p: [f64; 3]) -> f64 {
5590 let o = self.position;
5591 let mut l = rotate(self.q, [p[0] - o[0], p[1] - o[1], p[2] - o[2]], true);
5592 if self.solid.cone {
5593 l[1] -= 0.5 * self.solid.hh;
5594 }
5595 self.solid.signed(l)
5596 }
5597
5598 pub(super) fn segment(&self, a: [f64; 3], b: [f64; 3]) -> f64 {
5600 let at = |s: f64| {
5601 self.point([
5602 a[0] + s * (b[0] - a[0]),
5603 a[1] + s * (b[1] - a[1]),
5604 a[2] + s * (b[2] - a[2]),
5605 ])
5606 };
5607 if a == b {
5608 return at(0.0);
5609 }
5610 let (mut lo, mut hi) = (0.0f64, 1.0f64);
5611 for _ in 0..100 {
5612 let m1 = lo + (hi - lo) / 3.0;
5613 let m2 = hi - (hi - lo) / 3.0;
5614 if at(m1) <= at(m2) {
5615 hi = m2;
5616 } else {
5617 lo = m1;
5618 }
5619 }
5620 at(0.5 * (lo + hi)).min(at(0.0)).min(at(1.0))
5621 }
5622 }
5623
5624 pub(super) struct Cast {
5628 fixed: [Vec3Fix; 3],
5629 a: [f64; 3],
5630 b: [f64; 3],
5631 d: [f64; 3],
5632 r: f64,
5633 max_t: f64,
5634 }
5635
5636 impl Cast {
5637 pub(super) fn new(a: [f64; 3], b: [f64; 3], d: [f64; 3], r: f64, max_t: f64) -> Self {
5638 let fixed = [p3(a), p3(b), p3(d)];
5639 Self {
5640 a: f3(fixed[0]),
5641 b: f3(fixed[1]),
5642 d: unit(f3(fixed[2])),
5643 fixed,
5644 r,
5645 max_t,
5646 }
5647 }
5648
5649 pub(super) fn gap(&self, s: &Scene, t: f64) -> f64 {
5651 let mv = |p: [f64; 3]| {
5652 [
5653 p[0] + t * self.d[0],
5654 p[1] + t * self.d[1],
5655 p[2] + t * self.d[2],
5656 ]
5657 };
5658 s.segment(mv(self.a), mv(self.b)) - self.r
5659 }
5660
5661 pub(super) fn least(&self, s: &Scene) -> f64 {
5663 let (mut lo, mut hi) = (0.0f64, self.max_t);
5664 for _ in 0..120 {
5665 let m1 = lo + (hi - lo) / 3.0;
5666 let m2 = hi - (hi - lo) / 3.0;
5667 if self.gap(s, m1) <= self.gap(s, m2) {
5668 hi = m2;
5669 } else {
5670 lo = m1;
5671 }
5672 }
5673 self.gap(s, 0.5 * (lo + hi))
5674 }
5675
5676 pub(super) fn run(&self, s: &Scene) -> Option<f64> {
5678 let f = RayFilter::default();
5679 let (r, max_t) = (fx(self.r), fx(self.max_t));
5680 let h = if self.fixed[0] == self.fixed[1] {
5681 s.world
5682 .cast_sphere(self.fixed[0], r, self.fixed[2], max_t, &f)
5683 } else {
5684 s.world
5685 .cast_capsule(self.fixed[0], self.fixed[1], r, self.fixed[2], max_t, &f)
5686 };
5687 h.map(|h| h.t.to_f64())
5688 }
5689 }
5690 }
5691
5692 #[test]
5699 #[allow(clippy::disallowed_methods)] fn point_distances_to_a_cylinder_and_a_cone_are_their_meridian_closed_forms() {
5701 use round_reference::Solid;
5702 let solids = [
5703 Solid {
5704 cone: false,
5705 r: 0.9,
5706 hh: 0.7,
5707 },
5708 Solid {
5709 cone: true,
5710 r: 1.0,
5711 hh: 0.8,
5712 },
5713 ];
5714 let meridian = [
5716 (1.5, 0.1),
5717 (0.3, 1.4),
5718 (0.3, -1.6),
5719 (1.3, 1.2),
5720 (1.4, -1.1),
5721 (0.0, 2.0),
5722 (0.0, -2.0),
5723 (0.2, 0.0),
5724 (0.0, 0.0),
5725 (0.7, 0.5),
5726 (2.0, 2.0),
5727 (0.05, 1.3),
5728 (1.0, -0.79),
5729 ];
5730 let turns = [(1.0, 0.0), (0.6, 0.8), (-0.28, 0.96), (0.0, -1.0)];
5731 let (mut outside, mut inside) = (0, 0);
5732 for solid in solids {
5733 let (radius, hh) = (fx(solid.r), fx(solid.hh));
5734 for &(rho, y) in &meridian {
5735 for &(c, s) in &turns {
5736 let p = [rho * c, y, rho * s];
5737 let want = solid.signed(p);
5738 let got = if solid.cone {
5739 point_cone_exact(p3(p), radius, hh)
5740 } else {
5741 point_cylinder_exact(p3(p), radius, hh)
5742 };
5743 match got {
5744 Dist::Outside {
5745 dist,
5746 point,
5747 normal,
5748 } => {
5749 outside += 1;
5750 assert!(want > 0.0, "{solid:?} {p:?}: outside, closed form {want}");
5751 assert!(
5752 (dist.to_f64() - want).abs() < EXACT,
5753 "{solid:?} {p:?}: distance {} but the closed form is {want}",
5754 dist.to_f64()
5755 );
5756 let q = round_reference::f3(point);
5757 assert!(
5758 solid.signed(q).abs() < EXACT,
5759 "{solid:?} {p:?}: nearest point {q:?} is {} off the surface",
5760 solid.signed(q)
5761 );
5762 let to = [
5763 (p[0] - q[0]) / want,
5764 (p[1] - q[1]) / want,
5765 (p[2] - q[2]) / want,
5766 ];
5767 assert_vec(normal, to, 1e-10, "normal");
5768 }
5769 Dist::Inside => {
5770 inside += 1;
5771 assert!(want <= 0.0, "{solid:?} {p:?}: inside, closed form {want}");
5772 }
5773 Dist::AtLeast(_) => panic!("{solid:?} {p:?}: a bound, not a distance"),
5774 }
5775 }
5776 }
5777 let on = if solid.cone {
5779 point_cone_exact(v3(0.5, -0.8, 0.0), radius, hh)
5780 } else {
5781 point_cylinder_exact(v3(0.3, 0.7, 0.0), radius, hh)
5782 };
5783 assert!(matches!(on, Dist::Inside), "{solid:?}: on the surface");
5784 }
5785 assert!(
5787 outside >= 70 && inside >= 10,
5788 "{outside} outside, {inside} inside"
5789 );
5790 }
5791
5792 #[test]
5797 fn exact_round_distances_are_the_least_along_the_segment() {
5798 use round_reference::{Scene, Solid};
5799 let cylinder = Solid {
5800 cone: false,
5801 r: 0.9,
5802 hh: 0.7,
5803 };
5804 let cone = Solid {
5805 cone: true,
5806 r: 1.0,
5807 hh: 0.8,
5808 };
5809 let position = [0.25, -0.5, 0.75];
5810 let scenes = [
5811 Scene::new(cylinder, position, QuatFix::IDENTITY),
5812 Scene::turned(cylinder, position, [0.6, 0.7, -0.2], 0.9),
5813 Scene::new(cone, position, QuatFix::IDENTITY),
5814 Scene::turned(cone, position, [0.6, 0.7, -0.2], 0.9),
5815 ];
5816 let cores = [
5817 ([2.5, -0.4, 0.8], [2.5, -0.4, 0.8]),
5818 ([0.1, 2.0, 0.9], [0.6, 2.2, 0.4]),
5819 ([-1.5, -2.5, 1.0], [-1.0, -2.0, 2.0]),
5820 ([1.6, 1.0, -0.5], [1.4, 1.6, -0.9]),
5821 ([-2.0, 0.0, 0.75], [2.5, 0.0, 3.0]),
5822 ([0.25, 3.0, 0.75], [0.25, 3.0, 0.75]),
5823 ];
5824 let mut checked = 0;
5825 for s in &scenes {
5826 for &(a, b) in &cores {
5827 let (pa, pb) = (p3(a), p3(b));
5828 let (a, b) = (round_reference::f3(pa), round_reference::f3(pb));
5829 let want = s.segment(a, b);
5830 let got = exact_round_dist(&s.posed, pa, pb).expect("a cylinder or a cone");
5831 match got {
5832 Dist::Outside { dist, point, .. } => {
5833 checked += 1;
5834 assert!(
5835 (dist.to_f64() - want).abs() < 1e-10,
5836 "{:?} {a:?}–{b:?}: distance {} but the reference is {want}",
5837 s.solid,
5838 dist.to_f64()
5839 );
5840 let q = round_reference::f3(point);
5841 assert!(
5842 s.point(q).abs() < 1e-10,
5843 "{:?}: nearest point {q:?} is {} off the surface",
5844 s.solid,
5845 s.point(q)
5846 );
5847 }
5848 _ => assert!(
5849 want <= 0.0,
5850 "{:?} {a:?}–{b:?}: inside, reference {want}",
5851 s.solid
5852 ),
5853 }
5854 }
5855 }
5856 assert!(checked >= 20, "only {checked} distances checked");
5857 let ball = PosedShape {
5858 shape: Shape::Box {
5859 half_extents: v3(1.0, 1.0, 1.0),
5860 },
5861 position: Vec3Fix::ZERO,
5862 rotation: QuatFix::IDENTITY,
5863 };
5864 assert!(exact_round_dist(&ball, v3(3.0, 0.0, 0.0), v3(3.0, 0.0, 0.0)).is_none());
5865 }
5866
5867 #[test]
5875 fn casts_at_the_edge_of_the_tolerance_stop_at_the_curved_surface() {
5876 use round_reference::{Cast, Scene, Solid, TOL};
5877 let cyl = |r, hh| Solid { cone: false, r, hh };
5878 let cone = |r, hh| Solid { cone: true, r, hh };
5879 let p = [0.25, -0.5, 0.75];
5880 let tilt = [0.6, 0.7, -0.2];
5881 let cases: Vec<(&str, Scene, Cast)> = vec![
5882 (
5883 "segment over a rim",
5884 Scene::new(cyl(0.9, 0.7), p, QuatFix::IDENTITY),
5885 Cast::new(
5886 [-1.6640986290561979, 0.19999999975304572, 1.8206196513237867],
5887 [-1.834506720909574, 0.41213203410901, 1.6942824745669378],
5888 [0.5955591626715423, 0.0, -0.8033114487905494],
5889 0.0,
5890 4.0,
5891 ),
5892 ),
5893 (
5894 "segment over a rim, stepping by the lower bound",
5895 Scene::new(cyl(0.9, 0.7), p, QuatFix::IDENTITY),
5896 Cast::new(
5897 [1.8673886946205231, 0.19999999967072768, -0.7312338808982968],
5898 [2.0622378155658776, 0.41213203402669196, -0.6473660082245325],
5899 [-0.3953569432753914, 0.0, 0.9185275648579845],
5900 0.0,
5901 4.0,
5902 ),
5903 ),
5904 (
5905 "segment over a turned rim",
5906 Scene::turned(cyl(0.9, 0.7), p, tilt, 0.9),
5907 Cast::new(
5908 [-0.21852465418609512, -0.6507157640535539, 2.998948511156243],
5909 [
5910 -0.34405370476815655,
5911 -0.41720885309588485,
5912 3.139365675331331,
5913 ],
5914 [-0.06648810702703647, 0.487707135982694, -0.8704717578046512],
5915 0.0,
5916 4.0,
5917 ),
5918 ),
5919 (
5920 "sphere over a cone's rim",
5921 Scene::new(cone(1.0, 0.8), p, QuatFix::IDENTITY),
5922 Cast::new(
5923 [-1.7015908184354425, -0.9027621356993638, 1.8439950540065662],
5924 [-1.7015908184354425, -0.9027621356993638, 1.8439950540065662],
5925 [0.5606127132316385, 0.0, -0.8280781278134695],
5926 1.0 / 256.0,
5927 4.0,
5928 ),
5929 ),
5930 (
5931 "sphere onto a turned cone",
5932 Scene::turned(cone(1.0, 0.8), p, tilt, 0.9),
5933 Cast::new(
5934 [0.634936017399154, 0.008205912075554433, -1.4320403873864982],
5935 [0.634936017399154, 0.008205912075554433, -1.4320403873864982],
5936 [0.19818663470094447, -0.5210934105185997, 0.8301708952619384],
5937 1.0 / 256.0,
5938 4.0,
5939 ),
5940 ),
5941 (
5942 "sphere along a turned cone's base",
5943 Scene::turned(cone(1.0, 0.8), p, tilt, 0.9),
5944 Cast::new(
5945 [1.4096423077353353, -1.9430159060992016, 1.6571642739508496],
5946 [1.4096423077353353, -1.9430159060992016, 1.6571642739508496],
5947 [-0.7361296676461404, 0.528908761536265, -0.422337109879486],
5948 1.0 / 65536.0,
5949 4.0,
5950 ),
5951 ),
5952 (
5953 "segment into a cylinder's side below its cap",
5954 Scene::new(cyl(0.9, 0.7), p, QuatFix::IDENTITY),
5955 Cast::new(
5956 [-1.695761194159812, 0.19999999973806548, 0.2143570449433858],
5957 [-1.695761194159812, 0.4999999997380655, 0.2143570449433858],
5958 [0.9909759596810406, 0.0, 0.1340397229713666],
5959 0.0,
5960 4.0,
5961 ),
5962 ),
5963 (
5964 "segment into a turned cylinder's side below its cap",
5965 Scene::turned(cyl(0.9, 0.7), p, tilt, 0.9),
5966 Cast::new(
5967 [
5968 -0.7916938670894744,
5969 1.1912886520345445,
5970 -0.03569496775777914,
5971 ],
5972 [-0.6883045505593128, 1.4402697713519372, 0.09590689944358055],
5973 [0.7223971058724328, -0.5328868033551468, 0.4406518764706059],
5974 0.0,
5975 4.0,
5976 ),
5977 ),
5978 (
5979 "sphere grazing into a turned cone",
5980 Scene::turned(cone(1.0, 0.8), [0.0; 3], tilt, 0.9),
5981 Cast::new(
5982 [3.49676953785352, -1.9311419641957024, -0.20807093324643489],
5983 [3.49676953785352, -1.9311419641957024, -0.20807093324643489],
5984 [
5985 -0.8417749934677569,
5986 0.49646776600120957,
5987 -0.21197787076514807,
5988 ],
5989 1.0 / 65536.0,
5990 10.0,
5991 ),
5992 ),
5993 (
5994 "sphere at 9·10⁻¹⁰ rad to a turned cone's base",
5995 Scene::turned(
5996 cone(0.6428203322684567, 0.7475816177966408),
5997 [
5998 -1.6526587531452606,
5999 -1.2186799371102097,
6000 0.18938048544987396,
6001 ],
6002 [
6003 -0.5803070933934578,
6004 0.7790505106677301,
6005 -0.23732673508311564,
6006 ],
6007 1.8984897774453202,
6008 ),
6009 Cast::new(
6010 [
6011 -0.8965821042720563,
6012 -3.3486672559565704,
6013 -0.9839086836373099,
6014 ],
6015 [
6016 -0.8965821042720563,
6017 -3.3486672559565704,
6018 -0.9839086836373099,
6019 ],
6020 [-0.1482674077997217, 0.8136896687128683, 0.5620728589901773],
6021 1.0 / 65536.0,
6022 10.0,
6023 ),
6024 ),
6025 (
6026 "capsule sliding onto a turned cylinder's cap at 9·10⁻¹⁰ rad",
6027 Scene::turned(
6028 cyl(0.9012093363780878, 0.5179403340707722),
6029 [
6030 -0.4023466290173019,
6031 -0.46594330930383876,
6032 -1.2957395342946256,
6033 ],
6034 [
6035 -0.6714485362126652,
6036 -0.09612558018852724,
6037 -0.7347902667097514,
6038 ],
6039 0.5010374581070209,
6040 ),
6041 Cast::new(
6042 [1.8608221708009296, -2.0500185708660865, -1.4757067685941365],
6043 [1.3062351220451092, -1.5734718199873896, -0.7798910803267063],
6044 [
6045 -0.9249401521738037,
6046 0.2939600468880599,
6047 -0.24098382877309632,
6048 ],
6049 1.0 / 65536.0,
6050 10.0,
6051 ),
6052 ),
6053 ];
6054 for (what, scene, cast) in &cases {
6055 let least = cast.least(scene);
6056 assert!(
6057 least < -TOL,
6058 "{what}: the path goes only {} in",
6059 least / TOL
6060 );
6061 let t = cast
6062 .run(scene)
6063 .unwrap_or_else(|| panic!("{what}: no hit, the path goes {} in", least / TOL));
6064 let g = cast.gap(scene, t) / TOL;
6065 assert!(
6066 (-1.0..=2.0).contains(&g),
6067 "{what}: a hit at t = {t} with gap {g}·2⁻³², expected within [−1, +2]"
6068 );
6069 }
6070 }
6071
6072 #[test]
6077 fn paths_tangent_to_a_cylinder_hit_only_when_they_go_in() {
6078 use round_reference::{Cast, Scene, Solid, TOL};
6079 let solid = Solid {
6080 cone: false,
6081 r: 0.9,
6082 hh: 0.7,
6083 };
6084 let scene = Scene::new(solid, [0.0; 3], QuatFix::IDENTITY);
6085 let off = 1.0 / 134_217_728.0; for (g, hit) in [(off, false), (64.0 * off, false), (-off, true)] {
6087 for r in [0.0, 1.0 / 65536.0] {
6088 let z = 0.9 + r + g;
6089 let cast = if r == 0.0 {
6090 Cast::new([-3.0, 0.1, z], [-3.0, 0.3, z], [1.0, 0.0, 0.0], 0.0, 6.0)
6092 } else {
6093 Cast::new([-3.0, 0.2, z], [-3.0, 0.2, z], [1.0, 0.0, 0.0], r, 6.0)
6094 };
6095 let least = cast.least(&scene);
6097 assert!(
6098 (least - g).abs() < 1e-3 * TOL,
6099 "g = {g:e}, r = {r}: least gap {least:e}"
6100 );
6101 match (cast.run(&scene), hit) {
6102 (None, false) => {}
6103 (Some(t), true) => {
6104 let gt = cast.gap(&scene, t) / TOL;
6105 assert!(
6106 (-1.0..=2.0).contains(>),
6107 "g = {g:e}, r = {r}: a hit at t = {t} with gap {gt}·2⁻³²"
6108 );
6109 }
6110 (got, _) => panic!("g = {g:e}, r = {r}: {got:?}, expected a hit: {hit}"),
6111 }
6112 }
6113 }
6114 }
6115
6116 #[test]
6121 fn the_near_tangent_searches_find_the_roots_of_a_convex_gap() {
6122 type Gap<'a> = &'a dyn Fn(f64) -> Option<f64>;
6123 fn as_dist(g: Gap<'_>) -> impl Fn(Fix128) -> Dist + '_ {
6125 move |t: Fix128| match g(t.to_f64()) {
6126 Some(v) => Dist::Outside {
6127 dist: fx(v),
6128 point: Vec3Fix::ZERO,
6129 normal: Vec3Fix::UNIT_Y,
6130 },
6131 None => Dist::Inside,
6132 }
6133 }
6134 let tol = 1.0 / 4_294_967_296.0;
6135 let quadratic = |c: f64| move |t: f64| Some((t - 1.0) * (t - 1.0) - c);
6136 let deep_inside = |t: f64| {
6137 let g = (t - 1.0) * (t - 1.0) - 0.25;
6138 (g >= -0.125).then_some(g)
6139 };
6140 let falling = |t: f64| Some(0.5 - 0.25 * t);
6141 let (t0, max_t) = (Fix128::ZERO, fx(3.0));
6142 let (q4, q20, never) = (
6144 quadratic(0.25),
6145 quadratic(1.0 / 1_048_576.0),
6146 quadratic(-1e-3),
6147 );
6148 let cases: [(&str, Gap<'_>, Option<f64>); 5] = [
6149 ("into by 1/4", &q4, Some(0.5)),
6150 ("into by 2⁻²⁰", &q20, Some(1.0 - 1.0 / 1024.0)),
6151 ("inside below −1/8", &deep_inside, Some(0.5)),
6152 ("in only at the far end", &falling, Some(2.0)),
6153 ("never in", &never, None),
6154 ];
6155 for (name, g, root) in cases {
6156 let got = graze_contact(as_dist(g), Fix128::ZERO, Fix128::ZERO, t0, max_t);
6157 match (got, root) {
6158 (None, None) => {}
6159 (Some(c), Some(root)) => {
6160 let t = c.t.to_f64();
6161 let gap = g(t).expect("the contact is outside");
6163 assert!(
6164 (-tol..=tol).contains(&gap) || (gap > 0.0 && (t - root).abs() < 1e-9),
6165 "{name}: contact at t = {t} with gap {gap:e}, the root is {root}"
6166 );
6167 assert!(
6168 (t - root).abs() < 1e-6,
6169 "{name}: t = {t}, the root is {root}"
6170 );
6171 assert_vec(c.normal, [0.0, 1.0, 0.0], EXACT, "normal");
6172 }
6173 (got, _) => panic!(
6174 "{name}: {:?}, the root is {root:?}",
6175 got.map(|c| c.t.to_f64())
6176 ),
6177 }
6178 let gap_at = |t: Fix128| g(t.to_f64()).map(fx);
6180 let f0 = g(0.0).expect("clear at the start");
6181 let speed = match name {
6182 "in only at the far end" => 0.25,
6183 _ => 2.0,
6184 };
6185 assert_eq!(
6186 dips_below(gap_at, t0, fx(f0), fx(speed), max_t),
6187 root.is_some(),
6188 "{name}: dips below"
6189 );
6190 }
6191 let shallow = |t: f64| Some((t - 1.0) * (t - 1.0) - 0.5 * tol);
6193 let gap_at = |t: Fix128| shallow(t.to_f64()).map(fx);
6194 assert!(
6195 !dips_below(gap_at, t0, fx(1.0), fx(2.0), max_t),
6196 "half a tolerance in"
6197 );
6198 }
6199}