1fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
14 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
15}
16
17fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
18 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
19}
20
21fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
22 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
23}
24
25fn scale3(a: [f64; 3], s: f64) -> [f64; 3] {
26 [a[0] * s, a[1] * s, a[2] * s]
27}
28
29fn len3(a: [f64; 3]) -> f64 {
30 dot3(a, a).sqrt()
31}
32
33fn normalize3(a: [f64; 3]) -> [f64; 3] {
34 let l = len3(a).max(1e-15);
35 scale3(a, 1.0 / l)
36}
37
38fn lerp3(a: [f64; 3], b: [f64; 3], t: f64) -> [f64; 3] {
39 add3(scale3(a, 1.0 - t), scale3(b, t))
40}
41
42fn dist3(a: [f64; 3], b: [f64; 3]) -> f64 {
43 len3(sub3(a, b))
44}
45
46#[derive(Debug, Clone, Copy)]
52pub struct ShapeCastConfig {
53 pub max_toc: f64,
55 pub target_distance: f64,
57 pub toi_tolerance: f64,
59}
60
61impl Default for ShapeCastConfig {
62 fn default() -> Self {
63 Self {
64 max_toc: 1.0,
65 target_distance: 0.0,
66 toi_tolerance: 1e-6,
67 }
68 }
69}
70
71#[derive(Debug, Clone)]
77pub struct ShapeCastResult {
78 pub toi: f64,
80 pub normal_at_toi: [f64; 3],
82 pub witness_a: [f64; 3],
84 pub witness_b: [f64; 3],
86 pub is_penetrating: bool,
88 pub hit: bool,
90}
91
92impl ShapeCastResult {
93 pub fn hit(
95 toi: f64,
96 normal_at_toi: [f64; 3],
97 witness_a: [f64; 3],
98 witness_b: [f64; 3],
99 ) -> Self {
100 Self {
101 toi,
102 normal_at_toi,
103 witness_a,
104 witness_b,
105 is_penetrating: false,
106 hit: true,
107 }
108 }
109
110 pub fn penetrating(witness_a: [f64; 3], witness_b: [f64; 3], normal: [f64; 3]) -> Self {
112 Self {
113 toi: 0.0,
114 normal_at_toi: normal,
115 witness_a,
116 witness_b,
117 is_penetrating: true,
118 hit: true,
119 }
120 }
121
122 pub fn no_hit(max_toc: f64) -> Self {
124 Self {
125 toi: max_toc,
126 normal_at_toi: [0.0, 1.0, 0.0],
127 witness_a: [0.0; 3],
128 witness_b: [0.0; 3],
129 is_penetrating: false,
130 hit: false,
131 }
132 }
133}
134
135#[derive(Debug, Clone, Copy)]
141pub struct LinearMotion {
142 pub pos: [f64; 3],
144 pub vel: [f64; 3],
146}
147
148impl LinearMotion {
149 pub fn new(pos: [f64; 3], vel: [f64; 3]) -> Self {
151 Self { pos, vel }
152 }
153
154 pub fn position_at(&self, t: f64) -> [f64; 3] {
156 add3(self.pos, scale3(self.vel, t))
157 }
158
159 pub fn relative_velocity(a: &Self, b: &Self) -> [f64; 3] {
161 sub3(a.vel, b.vel)
162 }
163
164 pub fn compute_toi_linear_sphere(
168 motion_a: &LinearMotion,
169 radius_a: f64,
170 motion_b: &LinearMotion,
171 radius_b: f64,
172 max_t: f64,
173 ) -> Option<f64> {
174 let rel_pos = sub3(motion_a.pos, motion_b.pos);
175 let rel_vel = sub3(motion_a.vel, motion_b.vel);
176 let r = radius_a + radius_b;
177
178 let a = dot3(rel_vel, rel_vel);
179 let b = 2.0 * dot3(rel_pos, rel_vel);
180 let c = dot3(rel_pos, rel_pos) - r * r;
181
182 if c <= 0.0 {
183 return Some(0.0);
185 }
186 if a < 1e-15 {
187 return None;
188 }
189 let disc = b * b - 4.0 * a * c;
190 if disc < 0.0 {
191 return None;
192 }
193 let t = (-b - disc.sqrt()) / (2.0 * a);
194 if t >= 0.0 && t <= max_t {
195 Some(t)
196 } else {
197 None
198 }
199 }
200}
201
202#[derive(Debug, Clone, Copy)]
208pub struct AngularMotion {
209 pub pos: [f64; 3],
211 pub vel: [f64; 3],
213 pub angular_vel: [f64; 3],
215 pub orientation: [f64; 4],
217}
218
219impl AngularMotion {
220 pub fn new(pos: [f64; 3], vel: [f64; 3]) -> Self {
222 Self {
223 pos,
224 vel,
225 angular_vel: [0.0; 3],
226 orientation: [0.0, 0.0, 0.0, 1.0],
227 }
228 }
229
230 pub fn with_rotation(pos: [f64; 3], vel: [f64; 3], angular_vel: [f64; 3]) -> Self {
232 Self {
233 pos,
234 vel,
235 angular_vel,
236 orientation: [0.0, 0.0, 0.0, 1.0],
237 }
238 }
239
240 pub fn position_at(&self, t: f64) -> [f64; 3] {
242 add3(self.pos, scale3(self.vel, t))
243 }
244
245 pub fn motion_bound(&self, t: f64, point_offset: f64) -> f64 {
247 let linear = len3(self.vel) * t;
248 let angular = len3(self.angular_vel) * t * point_offset;
249 linear + angular
250 }
251}
252
253pub struct SphereCast;
259
260impl SphereCast {
261 pub fn vs_sphere(
263 motion_a: &LinearMotion,
264 radius_a: f64,
265 center_b: [f64; 3],
266 radius_b: f64,
267 cfg: &ShapeCastConfig,
268 ) -> ShapeCastResult {
269 let static_motion = LinearMotion::new(center_b, [0.0; 3]);
270 match LinearMotion::compute_toi_linear_sphere(
271 motion_a,
272 radius_a,
273 &static_motion,
274 radius_b,
275 cfg.max_toc,
276 ) {
277 Some(toi) if toi <= 0.0 => {
278 let n = normalize3(sub3(motion_a.pos, center_b));
279 ShapeCastResult::penetrating(motion_a.pos, center_b, n)
280 }
281 Some(toi) => {
282 let pos_a = motion_a.position_at(toi);
283 let n = normalize3(sub3(pos_a, center_b));
284 let wa = add3(pos_a, scale3(n, -radius_a));
285 let wb = add3(center_b, scale3(n, radius_b));
286 ShapeCastResult::hit(toi, n, wa, wb)
287 }
288 None => ShapeCastResult::no_hit(cfg.max_toc),
289 }
290 }
291
292 pub fn vs_box(
294 motion_a: &LinearMotion,
295 radius_a: f64,
296 box_center: [f64; 3],
297 half_extents: [f64; 3],
298 cfg: &ShapeCastConfig,
299 ) -> ShapeCastResult {
300 let lo = sub3(box_center, half_extents);
302 let hi = add3(box_center, half_extents);
303
304 let mut t = 0.0;
305 let max_iters = 64;
306 for _ in 0..max_iters {
307 let pos = motion_a.position_at(t);
308 let q = [
310 pos[0].clamp(lo[0], hi[0]),
311 pos[1].clamp(lo[1], hi[1]),
312 pos[2].clamp(lo[2], hi[2]),
313 ];
314 let d = dist3(pos, q) - radius_a;
315 if d <= cfg.target_distance + cfg.toi_tolerance {
316 let n = if len3(sub3(pos, q)) > 1e-12 {
317 normalize3(sub3(pos, q))
318 } else {
319 [0.0, 1.0, 0.0]
320 };
321 let wa = add3(pos, scale3(n, -radius_a));
322 return ShapeCastResult::hit(t, n, wa, q);
323 }
324 let speed = len3(motion_a.vel).max(1e-12);
326 t += d.max(cfg.toi_tolerance) / speed;
327 if t > cfg.max_toc {
328 break;
329 }
330 }
331 ShapeCastResult::no_hit(cfg.max_toc)
332 }
333
334 pub fn vs_triangle(
336 motion_a: &LinearMotion,
337 radius_a: f64,
338 v0: [f64; 3],
339 v1: [f64; 3],
340 v2: [f64; 3],
341 cfg: &ShapeCastConfig,
342 ) -> ShapeCastResult {
343 let mut t = 0.0;
344 let max_iters = 64;
345 for _ in 0..max_iters {
346 let pos = motion_a.position_at(t);
347 let ab = sub3(v1, v0);
348 let ac = sub3(v2, v0);
349 let ap = sub3(pos, v0);
350 let d1 = dot3(ab, ap);
351 let d2 = dot3(ac, ap);
352 let d3 = dot3(ab, sub3(pos, v1));
353 let d4 = dot3(ac, sub3(pos, v1));
354 let (u, v) = if d1 <= 0.0 && d2 <= 0.0 {
355 (1.0, 0.0)
356 } else if d3 >= 0.0 && d4 <= d3 {
357 (0.0, 1.0)
358 } else {
359 let a_coef = dot3(ab, ab);
360 let b_coef = dot3(ab, ac);
361 let c_coef = dot3(ac, ac);
362 let det = a_coef * c_coef - b_coef * b_coef;
363 if det.abs() < 1e-12 {
364 (1.0, 0.0)
365 } else {
366 let inv_det = 1.0 / det;
367 let v_ = (c_coef * d1 - b_coef * d2) * inv_det;
368 let w_ = (a_coef * d2 - b_coef * d1) * inv_det;
369 let v_ = v_.clamp(0.0, 1.0);
370 let w_ = w_.clamp(0.0, 1.0 - v_);
371 (1.0 - v_ - w_, v_)
372 }
373 };
374 let _q = add3(
375 add3(scale3(v0, u + v), scale3(v1, 1.0 - u - v)),
376 scale3(v2, 0.0),
377 );
378 let _ = (u, v);
379 let (q, _) = crate_point_tri(pos, v0, v1, v2);
381 let d = dist3(pos, q) - radius_a;
382 if d <= cfg.target_distance + cfg.toi_tolerance {
383 let n = if len3(sub3(pos, q)) > 1e-12 {
384 normalize3(sub3(pos, q))
385 } else {
386 [0.0, 0.0, 1.0]
387 };
388 let wa = add3(pos, scale3(n, -radius_a));
389 return ShapeCastResult::hit(t, n, wa, q);
390 }
391 let speed = len3(motion_a.vel).max(1e-12);
392 t += d.max(cfg.toi_tolerance) / speed;
393 if t > cfg.max_toc {
394 break;
395 }
396 }
397 ShapeCastResult::no_hit(cfg.max_toc)
398 }
399}
400
401fn crate_point_tri(p: [f64; 3], v0: [f64; 3], v1: [f64; 3], v2: [f64; 3]) -> ([f64; 3], [f64; 3]) {
403 let ab = sub3(v1, v0);
404 let ac = sub3(v2, v0);
405 let ap = sub3(p, v0);
406
407 let d1 = dot3(ab, ap);
408 let d2 = dot3(ac, ap);
409 if d1 <= 0.0 && d2 <= 0.0 {
410 return (v0, [1.0, 0.0, 0.0]);
411 }
412 let bp = sub3(p, v1);
413 let d3 = dot3(ab, bp);
414 let d4 = dot3(ac, bp);
415 if d3 >= 0.0 && d4 <= d3 {
416 return (v1, [0.0, 1.0, 0.0]);
417 }
418 let cp = sub3(p, v2);
419 let d5 = dot3(ab, cp);
420 let d6 = dot3(ac, cp);
421 if d6 >= 0.0 && d5 <= d6 {
422 return (v2, [0.0, 0.0, 1.0]);
423 }
424 let vc = d1 * d4 - d3 * d2;
425 if vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0 {
426 let v = d1 / (d1 - d3);
427 let q = add3(v0, scale3(ab, v));
428 return (q, [1.0 - v, v, 0.0]);
429 }
430 let vb = d5 * d2 - d1 * d6;
431 if vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0 {
432 let w = d2 / (d2 - d6);
433 let q = add3(v0, scale3(ac, w));
434 return (q, [1.0 - w, 0.0, w]);
435 }
436 let va = d3 * d6 - d5 * d4;
437 if va <= 0.0 && (d4 - d3) >= 0.0 && (d5 - d6) >= 0.0 {
438 let w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
439 let q = lerp3(v1, v2, w);
440 return (q, [0.0, 1.0 - w, w]);
441 }
442 let denom = 1.0 / (va + vb + vc);
443 let v = vb * denom;
444 let w = vc * denom;
445 let u = 1.0 - v - w;
446 let q = add3(add3(scale3(v0, u), scale3(v1, v)), scale3(v2, w));
447 (q, [u, v, w])
448}
449
450pub struct CapsuleCast;
456
457impl CapsuleCast {
458 pub fn vs_capsule(
460 motion: &LinearMotion,
461 local_a: [f64; 3],
462 local_b: [f64; 3],
463 radius_a: f64,
464 static_a: [f64; 3],
465 static_b: [f64; 3],
466 radius_b: f64,
467 cfg: &ShapeCastConfig,
468 ) -> ShapeCastResult {
469 let r = radius_a + radius_b;
470 let mut t = 0.0;
471 let max_iters = 64;
472 for _ in 0..max_iters {
473 let offset = motion.position_at(t);
474 let cap_a = add3(local_a, offset);
475 let cap_b = add3(local_b, offset);
476 let (seg_dist, wa, wb) = seg_seg_dist(cap_a, cap_b, static_a, static_b);
478 let d = seg_dist - r;
479 if d <= cfg.target_distance {
480 let n = if seg_dist > 1e-12 {
481 normalize3(sub3(wa, wb))
482 } else {
483 [0.0, 1.0, 0.0]
484 };
485 return ShapeCastResult::hit(t, n, wa, wb);
486 }
487 let speed = len3(motion.vel).max(1e-12);
488 t += d / speed;
489 if t > cfg.max_toc {
490 break;
491 }
492 }
493 ShapeCastResult::no_hit(cfg.max_toc)
494 }
495
496 pub fn vs_box(
498 motion: &LinearMotion,
499 local_a: [f64; 3],
500 local_b: [f64; 3],
501 radius: f64,
502 box_center: [f64; 3],
503 half_extents: [f64; 3],
504 cfg: &ShapeCastConfig,
505 ) -> ShapeCastResult {
506 let lo = sub3(box_center, half_extents);
507 let hi = add3(box_center, half_extents);
508 let mut t = 0.0;
509 let max_iters = 64;
510 for _ in 0..max_iters {
511 let offset = motion.position_at(t);
512 let cap_a = add3(local_a, offset);
513 let cap_b = add3(local_b, offset);
514 let mut min_dist = f64::INFINITY;
516 let mut best_q = cap_a;
517 for &p in &[cap_a, cap_b] {
518 let q = [
519 p[0].clamp(lo[0], hi[0]),
520 p[1].clamp(lo[1], hi[1]),
521 p[2].clamp(lo[2], hi[2]),
522 ];
523 let d = dist3(p, q);
524 if d < min_dist {
525 min_dist = d;
526 best_q = q;
527 }
528 }
529 let _ = best_q;
530 let d = min_dist - radius;
531 if d <= cfg.target_distance {
532 return ShapeCastResult::hit(t, [0.0, 1.0, 0.0], cap_a, box_center);
533 }
534 let speed = len3(motion.vel).max(1e-12);
535 t += d / speed;
536 if t > cfg.max_toc {
537 break;
538 }
539 }
540 ShapeCastResult::no_hit(cfg.max_toc)
541 }
542}
543
544fn seg_seg_dist(
546 p1: [f64; 3],
547 p2: [f64; 3],
548 p3: [f64; 3],
549 p4: [f64; 3],
550) -> (f64, [f64; 3], [f64; 3]) {
551 let d1 = sub3(p2, p1);
552 let d2 = sub3(p4, p3);
553 let r = sub3(p1, p3);
554 let a = dot3(d1, d1);
555 let e = dot3(d2, d2);
556 let f = dot3(d2, r);
557 let (s, t);
558 if a < 1e-15 && e < 1e-15 {
559 return (dist3(p1, p3), p1, p3);
560 }
561 if a < 1e-15 {
562 s = 0.0;
563 t = (f / e).clamp(0.0, 1.0);
564 } else {
565 let c = dot3(d1, r);
566 if e < 1e-15 {
567 t = 0.0;
568 s = (-c / a).clamp(0.0, 1.0);
569 } else {
570 let b = dot3(d1, d2);
571 let denom = a * e - b * b;
572 s = if denom > 1e-15 {
573 ((b * f - c * e) / denom).clamp(0.0, 1.0)
574 } else {
575 0.0
576 };
577 t = ((b * s + f) / e).clamp(0.0, 1.0);
578 }
579 }
580 let wa = lerp3(p1, p2, s);
581 let wb = lerp3(p3, p4, t);
582 (dist3(wa, wb), wa, wb)
583}
584
585pub struct BoxCast;
591
592impl BoxCast {
593 pub fn vs_box(
595 motion: &LinearMotion,
596 half_a: [f64; 3],
597 center_b: [f64; 3],
598 half_b: [f64; 3],
599 cfg: &ShapeCastConfig,
600 ) -> ShapeCastResult {
601 let mut t = 0.0;
602 let max_iters = 64;
603 for _ in 0..max_iters {
604 let pos_a = motion.position_at(t);
605 let lo_a = sub3(pos_a, half_a);
606 let hi_a = add3(pos_a, half_a);
607 let lo_b = sub3(center_b, half_b);
608 let hi_b = add3(center_b, half_b);
609
610 let sep = [
612 (lo_a[0] - hi_b[0]).max(lo_b[0] - hi_a[0]).max(0.0),
613 (lo_a[1] - hi_b[1]).max(lo_b[1] - hi_a[1]).max(0.0),
614 (lo_a[2] - hi_b[2]).max(lo_b[2] - hi_a[2]).max(0.0),
615 ];
616 let d = len3(sep);
617 if d <= cfg.target_distance {
618 let n = normalize3(sub3(pos_a, center_b));
619 return ShapeCastResult::hit(t, n, pos_a, center_b);
620 }
621 let speed = len3(motion.vel).max(1e-12);
622 t += d / speed;
623 if t > cfg.max_toc {
624 break;
625 }
626 }
627 ShapeCastResult::no_hit(cfg.max_toc)
628 }
629
630 pub fn vs_triangle(
632 motion: &LinearMotion,
633 half_extents: [f64; 3],
634 v0: [f64; 3],
635 v1: [f64; 3],
636 v2: [f64; 3],
637 cfg: &ShapeCastConfig,
638 ) -> ShapeCastResult {
639 let mut t = 0.0;
640 let max_iters = 64;
641 for _ in 0..max_iters {
642 let pos = motion.position_at(t);
643 let lo = sub3(pos, half_extents);
644 let hi = add3(pos, half_extents);
645 let mut min_dist = f64::INFINITY;
647 for &v in &[v0, v1, v2] {
648 let q = [
649 v[0].clamp(lo[0], hi[0]),
650 v[1].clamp(lo[1], hi[1]),
651 v[2].clamp(lo[2], hi[2]),
652 ];
653 let d = dist3(v, q);
654 if d < min_dist {
655 min_dist = d;
656 }
657 }
658 if min_dist <= cfg.target_distance {
659 let tri_center = scale3(add3(add3(v0, v1), v2), 1.0 / 3.0);
660 let n = normalize3(sub3(pos, tri_center));
661 return ShapeCastResult::hit(t, n, pos, tri_center);
662 }
663 let speed = len3(motion.vel).max(1e-12);
664 t += min_dist / speed;
665 if t > cfg.max_toc {
666 break;
667 }
668 }
669 ShapeCastResult::no_hit(cfg.max_toc)
670 }
671}
672
673pub struct ConvexCast;
681
682impl ConvexCast {
683 fn gjk_distance(pts_a: &[[f64; 3]], pts_b: &[[f64; 3]]) -> (f64, [f64; 3], [f64; 3]) {
685 let mut min_dist = f64::INFINITY;
687 let mut best_a = pts_a[0];
688 let mut best_b = pts_b[0];
689 for &a in pts_a {
690 for &b in pts_b {
691 let d = dist3(a, b);
692 if d < min_dist {
693 min_dist = d;
694 best_a = a;
695 best_b = b;
696 }
697 }
698 }
699 (min_dist, best_a, best_b)
700 }
701
702 fn translate_shape(pts: &[[f64; 3]], offset: [f64; 3]) -> Vec<[f64; 3]> {
704 pts.iter().map(|&p| add3(p, offset)).collect()
705 }
706
707 pub fn cast(
709 motion: &LinearMotion,
710 shape_a: &[[f64; 3]],
711 shape_b: &[[f64; 3]],
712 cfg: &ShapeCastConfig,
713 ) -> ShapeCastResult {
714 let mut t = 0.0;
715 let max_iters = 32;
716 for _ in 0..max_iters {
717 let offset = motion.position_at(t);
718 let moved_a = Self::translate_shape(shape_a, offset);
719 let (d, wa, wb) = Self::gjk_distance(&moved_a, shape_b);
720 if d <= cfg.target_distance {
721 let n = if d > 1e-12 {
722 normalize3(sub3(wa, wb))
723 } else {
724 [0.0, 1.0, 0.0]
725 };
726 return ShapeCastResult::hit(t, n, wa, wb);
727 }
728 let speed = len3(motion.vel).max(1e-12);
729 t += d / speed;
730 if t > cfg.max_toc {
731 break;
732 }
733 }
734 ShapeCastResult::no_hit(cfg.max_toc)
735 }
736}
737
738pub struct SubstepCast {
744 pub min_substep: f64,
746 pub max_substeps: usize,
748}
749
750impl SubstepCast {
751 pub fn new(min_substep: f64, max_substeps: usize) -> Self {
753 Self {
754 min_substep,
755 max_substeps,
756 }
757 }
758
759 pub fn cast<F>(&self, t_start: f64, t_end: f64, cast_fn: F) -> Option<ShapeCastResult>
763 where
764 F: Fn(f64, f64) -> ShapeCastResult,
765 {
766 let dt = t_end - t_start;
767 let n_steps = ((dt / self.min_substep).ceil() as usize)
768 .min(self.max_substeps)
769 .max(1);
770 let step = dt / n_steps as f64;
771 for i in 0..n_steps {
772 let ta = t_start + i as f64 * step;
773 let tb = ta + step;
774 let result = cast_fn(ta, tb);
775 if result.hit {
776 return Some(result);
777 }
778 }
779 None
780 }
781}
782
783pub struct NonLinearCast {
792 pub dt: f64,
794 pub tol: f64,
796 pub max_bisect: usize,
798}
799
800impl NonLinearCast {
801 pub fn new(dt: f64, tol: f64, max_bisect: usize) -> Self {
803 Self {
804 dt,
805 tol,
806 max_bisect,
807 }
808 }
809
810 pub fn cast<F>(&self, t_max: f64, dist_fn: F) -> Option<f64>
814 where
815 F: Fn(f64) -> f64,
816 {
817 let mut t = 0.0;
818 let d0 = dist_fn(0.0);
819 if d0 <= self.tol {
820 return Some(0.0);
821 }
822
823 while t < t_max {
824 t += self.dt;
825 let t_cur = t.min(t_max);
826 let d = dist_fn(t_cur);
827 if d <= self.tol {
828 let mut lo = (t - self.dt).max(0.0);
830 let mut hi = t_cur;
831 for _ in 0..self.max_bisect {
832 let mid = 0.5 * (lo + hi);
833 let d_mid = dist_fn(mid);
834 if d_mid <= self.tol {
835 hi = mid;
836 } else {
837 lo = mid;
838 }
839 if hi - lo < self.tol * 0.1 {
840 break;
841 }
842 }
843 return Some(hi);
844 }
845 }
846 None
847 }
848}
849
850pub struct MeshBvhCast {
856 pub vertices: Vec<[f64; 3]>,
858 pub indices: Vec<usize>,
860}
861
862impl MeshBvhCast {
863 pub fn new(vertices: Vec<[f64; 3]>, indices: Vec<usize>) -> Self {
865 assert!(indices.len().is_multiple_of(3));
866 Self { vertices, indices }
867 }
868
869 pub fn sphere_cast(
873 &self,
874 motion: &LinearMotion,
875 radius: f64,
876 cfg: &ShapeCastConfig,
877 ) -> ShapeCastResult {
878 let n_tris = self.indices.len() / 3;
879 let mut best = ShapeCastResult::no_hit(cfg.max_toc);
880
881 for tri in 0..n_tris {
882 let v0 = self.vertices[self.indices[3 * tri]];
883 let v1 = self.vertices[self.indices[3 * tri + 1]];
884 let v2 = self.vertices[self.indices[3 * tri + 2]];
885 let result = SphereCast::vs_triangle(motion, radius, v0, v1, v2, cfg);
886 if result.hit && result.toi < best.toi {
887 best = result;
888 }
889 }
890 best
891 }
892
893 pub fn n_triangles(&self) -> usize {
895 self.indices.len() / 3
896 }
897}
898
899#[cfg(test)]
904mod tests {
905 use super::*;
906
907 fn default_cfg() -> ShapeCastConfig {
908 ShapeCastConfig::default()
909 }
910
911 #[test]
912 fn test_shape_cast_config_default() {
913 let cfg = ShapeCastConfig::default();
914 assert!((cfg.max_toc - 1.0).abs() < 1e-12);
915 assert_eq!(cfg.target_distance, 0.0);
916 }
917
918 #[test]
919 fn test_linear_motion_position_at() {
920 let m = LinearMotion::new([0.0, 0.0, 0.0], [1.0, 0.0, 0.0]);
921 let p = m.position_at(2.0);
922 assert!((p[0] - 2.0).abs() < 1e-12);
923 }
924
925 #[test]
926 fn test_linear_motion_toi_sphere_hit() {
927 let a = LinearMotion::new([0.0, 0.0, -5.0], [0.0, 0.0, 1.0]);
928 let b = LinearMotion::new([0.0, 0.0, 0.0], [0.0, 0.0, 0.0]);
929 let toi = LinearMotion::compute_toi_linear_sphere(&a, 0.5, &b, 0.5, 10.0);
930 assert!(toi.is_some());
931 assert!((toi.unwrap() - 4.0).abs() < 0.1);
932 }
933
934 #[test]
935 fn test_linear_motion_toi_sphere_miss() {
936 let a = LinearMotion::new([10.0, 0.0, -5.0], [0.0, 0.0, 1.0]);
937 let b = LinearMotion::new([0.0, 0.0, 0.0], [0.0, 0.0, 0.0]);
938 let toi = LinearMotion::compute_toi_linear_sphere(&a, 0.5, &b, 0.5, 10.0);
939 assert!(toi.is_none());
940 }
941
942 #[test]
943 fn test_linear_motion_toi_sphere_initial_overlap() {
944 let a = LinearMotion::new([0.0, 0.0, 0.0], [0.0, 0.0, 1.0]);
945 let b = LinearMotion::new([0.0, 0.0, 0.0], [0.0, 0.0, 0.0]);
946 let toi = LinearMotion::compute_toi_linear_sphere(&a, 1.0, &b, 1.0, 10.0);
947 assert!(toi.is_some());
948 assert!(toi.unwrap() <= 0.0);
949 }
950
951 #[test]
952 fn test_sphere_cast_vs_sphere_hit() {
953 let motion = LinearMotion::new([0.0, 0.0, -1.5], [0.0, 0.0, 1.0]);
956 let cfg = default_cfg();
957 let result = SphereCast::vs_sphere(&motion, 0.5, [0.0, 0.0, 0.0], 0.5, &cfg);
958 assert!(result.hit);
959 assert!(result.toi > 0.0 && result.toi <= 1.0);
960 }
961
962 #[test]
963 fn test_sphere_cast_vs_sphere_miss() {
964 let motion = LinearMotion::new([10.0, 0.0, -5.0], [0.0, 0.0, -1.0]);
965 let cfg = default_cfg();
966 let result = SphereCast::vs_sphere(&motion, 0.5, [0.0, 0.0, 0.0], 0.5, &cfg);
967 assert!(!result.hit);
968 }
969
970 #[test]
971 fn test_sphere_cast_vs_sphere_penetrating() {
972 let motion = LinearMotion::new([0.0, 0.0, 0.0], [0.0, 0.0, 1.0]);
973 let cfg = default_cfg();
974 let result = SphereCast::vs_sphere(&motion, 1.0, [0.0, 0.0, 0.0], 1.0, &cfg);
975 assert!(result.hit);
976 assert!(result.is_penetrating);
977 }
978
979 #[test]
980 fn test_sphere_cast_vs_box_hit() {
981 let motion = LinearMotion::new([0.0, 0.0, -5.0], [0.0, 0.0, 1.0]);
982 let cfg = ShapeCastConfig {
983 max_toc: 10.0,
984 ..default_cfg()
985 };
986 let result = SphereCast::vs_box(&motion, 0.5, [0.0, 0.0, 0.0], [1.0, 1.0, 1.0], &cfg);
987 assert!(result.hit);
988 }
989
990 #[test]
991 fn test_capsule_cast_vs_capsule() {
992 let motion = LinearMotion::new([0.0, 0.0, -5.0], [0.0, 0.0, 1.0]);
993 let cfg = ShapeCastConfig {
994 max_toc: 10.0,
995 ..default_cfg()
996 };
997 let result = CapsuleCast::vs_capsule(
998 &motion,
999 [-0.5, 0.0, 0.0],
1000 [0.5, 0.0, 0.0],
1001 0.25,
1002 [-0.5, 0.0, 0.0],
1003 [0.5, 0.0, 0.0],
1004 0.25,
1005 &cfg,
1006 );
1007 assert!(result.hit);
1008 }
1009
1010 #[test]
1011 fn test_box_cast_vs_box_hit() {
1012 let motion = LinearMotion::new([0.0, 0.0, -5.0], [0.0, 0.0, 1.0]);
1013 let cfg = ShapeCastConfig {
1014 max_toc: 10.0,
1015 ..default_cfg()
1016 };
1017 let result = BoxCast::vs_box(
1018 &motion,
1019 [0.5, 0.5, 0.5],
1020 [0.0, 0.0, 0.0],
1021 [0.5, 0.5, 0.5],
1022 &cfg,
1023 );
1024 assert!(result.hit);
1025 }
1026
1027 #[test]
1028 fn test_box_cast_vs_box_miss() {
1029 let motion = LinearMotion::new([10.0, 10.0, -5.0], [0.0, 0.0, -1.0]);
1030 let cfg = default_cfg();
1031 let result = BoxCast::vs_box(
1032 &motion,
1033 [0.1, 0.1, 0.1],
1034 [0.0, 0.0, 0.0],
1035 [0.1, 0.1, 0.1],
1036 &cfg,
1037 );
1038 assert!(!result.hit);
1039 }
1040
1041 #[test]
1042 fn test_convex_cast_hit() {
1043 let motion = LinearMotion::new([0.0, 0.0, -5.0], [0.0, 0.0, 1.0]);
1044 let cfg = ShapeCastConfig {
1045 max_toc: 10.0,
1046 ..default_cfg()
1047 };
1048 let shape_a = vec![[-0.5, 0.0, 0.0], [0.5, 0.0, 0.0]];
1049 let shape_b = vec![[-0.5, 0.0, 0.0], [0.5, 0.0, 0.0]];
1050 let result = ConvexCast::cast(&motion, &shape_a, &shape_b, &cfg);
1051 assert!(result.hit);
1052 }
1053
1054 #[test]
1055 fn test_angular_motion_position_at() {
1056 let m = AngularMotion::new([0.0, 0.0, 0.0], [1.0, 0.0, 0.0]);
1057 let p = m.position_at(1.0);
1058 assert!((p[0] - 1.0).abs() < 1e-12);
1059 }
1060
1061 #[test]
1062 fn test_angular_motion_bound() {
1063 let m = AngularMotion::with_rotation([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]);
1064 let bound = m.motion_bound(1.0, 0.5);
1065 assert!(bound > 1.0); }
1067
1068 #[test]
1069 fn test_substep_cast_finds_hit() {
1070 let caster = SubstepCast::new(0.1, 100);
1071 let result = caster.cast(0.0, 1.0, |ta, _tb| {
1072 if ta > 0.5 {
1073 ShapeCastResult::hit(ta, [0.0, 1.0, 0.0], [0.0; 3], [0.0; 3])
1074 } else {
1075 ShapeCastResult::no_hit(1.0)
1076 }
1077 });
1078 assert!(result.is_some());
1079 }
1080
1081 #[test]
1082 fn test_substep_cast_no_hit() {
1083 let caster = SubstepCast::new(0.1, 10);
1084 let result = caster.cast(0.0, 1.0, |_ta, _tb| ShapeCastResult::no_hit(1.0));
1085 assert!(result.is_none());
1086 }
1087
1088 #[test]
1089 fn test_nonlinear_cast_hit() {
1090 let caster = NonLinearCast::new(0.01, 0.05, 20);
1091 let toi = caster.cast(2.0, |t| 1.0 - t);
1093 assert!(toi.is_some());
1094 let t = toi.unwrap();
1095 assert!((t - 1.0).abs() < 0.1, "toi={}", t);
1096 }
1097
1098 #[test]
1099 fn test_nonlinear_cast_no_hit() {
1100 let caster = NonLinearCast::new(0.1, 0.05, 10);
1101 let toi = caster.cast(1.0, |_t| 2.0);
1103 assert!(toi.is_none());
1104 }
1105
1106 #[test]
1107 fn test_mesh_bvh_cast_sphere() {
1108 let verts = vec![[-1.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1109 let idx = vec![0usize, 1, 2];
1110 let mesh = MeshBvhCast::new(verts, idx);
1111 let motion = LinearMotion::new([0.0, 0.5, -3.0], [0.0, 0.0, 1.0]);
1112 let cfg = ShapeCastConfig {
1113 max_toc: 10.0,
1114 ..default_cfg()
1115 };
1116 let result = mesh.sphere_cast(&motion, 0.2, &cfg);
1117 assert!(result.hit);
1118 }
1119
1120 #[test]
1121 fn test_shape_cast_result_no_hit() {
1122 let r = ShapeCastResult::no_hit(1.0);
1123 assert!(!r.hit);
1124 assert!(!r.is_penetrating);
1125 }
1126
1127 #[test]
1128 fn test_shape_cast_result_penetrating() {
1129 let r = ShapeCastResult::penetrating([0.0; 3], [0.0; 3], [0.0, 1.0, 0.0]);
1130 assert!(r.hit);
1131 assert!(r.is_penetrating);
1132 assert_eq!(r.toi, 0.0);
1133 }
1134
1135 #[test]
1136 fn test_relative_velocity() {
1137 let a = LinearMotion::new([0.0; 3], [1.0, 0.0, 0.0]);
1138 let b = LinearMotion::new([0.0; 3], [0.5, 0.0, 0.0]);
1139 let rel = LinearMotion::relative_velocity(&a, &b);
1140 assert!((rel[0] - 0.5).abs() < 1e-12);
1141 }
1142
1143 #[test]
1144 fn test_sphere_vs_triangle_no_hit() {
1145 let motion = LinearMotion::new([10.0, 10.0, -5.0], [0.0, 0.0, -1.0]);
1146 let cfg = default_cfg();
1147 let result = SphereCast::vs_triangle(
1148 &motion,
1149 0.1,
1150 [-0.1, -0.1, 0.0],
1151 [0.1, -0.1, 0.0],
1152 [0.0, 0.1, 0.0],
1153 &cfg,
1154 );
1155 assert!(!result.hit);
1156 }
1157}