1use crate::collider::AABB;
12use crate::math::{Fix128, Vec3Fix};
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub struct TOI {
17 pub t: Fix128,
19 pub point: Vec3Fix,
21 pub normal: Vec3Fix,
23}
24
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub struct CcdConfig {
28 pub max_iterations: usize,
30 pub tolerance: Fix128,
32 pub velocity_threshold: Fix128,
34}
35
36impl Default for CcdConfig {
37 fn default() -> Self {
38 Self {
39 max_iterations: 32,
40 tolerance: Fix128::from_ratio(1, 1000),
41 velocity_threshold: Fix128::from_int(5),
42 }
43 }
44}
45
46#[inline]
48#[must_use]
49pub fn needs_ccd(velocity: Vec3Fix, radius: Fix128, dt: Fix128, config: &CcdConfig) -> bool {
50 let speed = velocity.length();
51 speed * dt > radius.half() && speed > config.velocity_threshold
53}
54
55#[must_use]
61pub fn sphere_sphere_toi(
62 center_a: Vec3Fix,
63 radius_a: Fix128,
64 vel_a: Vec3Fix,
65 center_b: Vec3Fix,
66 radius_b: Fix128,
67 vel_b: Vec3Fix,
68) -> Option<TOI> {
69 let rel_pos = center_b - center_a;
70 let rel_vel = vel_b - vel_a;
71 let combined_r = radius_a + radius_b;
72
73 let a = rel_vel.dot(rel_vel);
75 let b = rel_pos.dot(rel_vel).double();
76 let c = rel_pos.dot(rel_pos) - combined_r * combined_r;
77
78 if c <= Fix128::ZERO {
80 let normal = rel_pos.normalize();
81 return Some(TOI {
82 t: Fix128::ZERO,
83 point: center_a + normal * radius_a,
84 normal,
85 });
86 }
87
88 if a.is_zero() {
90 return None;
91 }
92
93 let discriminant = b * b - Fix128::from_int(4) * a * c;
94 if discriminant < Fix128::ZERO {
95 return None;
96 }
97
98 let sqrt_d = discriminant.sqrt();
99 let two_a = a.double();
100 let t = (-b - sqrt_d) / two_a;
101
102 if t >= Fix128::ZERO && t <= Fix128::ONE {
103 let pos_a = center_a + vel_a * t;
104 let pos_b = center_b + vel_b * t;
105 let normal = (pos_b - pos_a).normalize();
106 let point = pos_a + normal * radius_a;
107 Some(TOI { t, point, normal })
108 } else {
109 None
110 }
111}
112
113#[must_use]
122pub fn sphere_plane_toi(
123 center: Vec3Fix,
124 radius: Fix128,
125 velocity: Vec3Fix,
126 plane_normal: Vec3Fix,
127 plane_offset: Fix128,
128) -> Option<TOI> {
129 let dist = center.dot(plane_normal) - plane_offset;
130 let vel_toward = velocity.dot(plane_normal);
131
132 if vel_toward >= Fix128::ZERO && dist > radius {
134 return None;
135 }
136
137 let side = if dist.is_negative() {
140 -Fix128::ONE
141 } else {
142 Fix128::ONE
143 };
144
145 if dist.abs() <= radius {
147 let point = center - plane_normal * dist;
148 return Some(TOI {
149 t: Fix128::ZERO,
150 point,
151 normal: plane_normal * side,
152 });
153 }
154
155 if vel_toward.is_zero() {
156 return None;
157 }
158
159 let t = (side * radius - dist) / vel_toward;
161
162 if t >= Fix128::ZERO && t <= Fix128::ONE {
163 let point = center + velocity * t - plane_normal * (side * radius);
164 Some(TOI {
165 t,
166 point,
167 normal: plane_normal * side,
168 })
169 } else {
170 None
171 }
172}
173
174pub fn conservative_advancement<F>(
179 start_pos: Vec3Fix,
180 displacement: Vec3Fix,
181 radius: Fix128,
182 distance_fn: F,
183 config: &CcdConfig,
184) -> Option<TOI>
185where
186 F: Fn(Vec3Fix) -> (Fix128, Vec3Fix), {
188 let mut t = Fix128::ZERO;
189
190 for _ in 0..config.max_iterations {
191 let pos = start_pos + displacement * t;
192 let (dist, normal) = distance_fn(pos);
193
194 let gap = dist - radius;
196
197 if gap <= config.tolerance {
198 let point = pos - normal * dist;
199 return Some(TOI { t, point, normal });
200 }
201
202 let speed = displacement.length();
204 if speed.is_zero() {
205 return None;
206 }
207
208 let dt = gap / speed;
209 t = t + dt;
210
211 if t > Fix128::ONE {
212 return None; }
214 }
215
216 None
217}
218
219#[must_use]
223pub fn swept_aabb(moving: &AABB, velocity: Vec3Fix, target: &AABB) -> Option<Fix128> {
224 let mut t_enter = Fix128::from_int(-1000000);
225 let mut t_exit = Fix128::from_int(1000000);
226
227 if let Some((te, tx)) = slab_test(
229 moving.min.x,
230 moving.max.x,
231 target.min.x,
232 target.max.x,
233 velocity.x,
234 ) {
235 t_enter = if te > t_enter { te } else { t_enter };
236 t_exit = if tx < t_exit { tx } else { t_exit };
237 if t_enter > t_exit {
238 return None;
239 }
240 } else {
241 return None;
242 }
243
244 if let Some((te, tx)) = slab_test(
246 moving.min.y,
247 moving.max.y,
248 target.min.y,
249 target.max.y,
250 velocity.y,
251 ) {
252 t_enter = if te > t_enter { te } else { t_enter };
253 t_exit = if tx < t_exit { tx } else { t_exit };
254 if t_enter > t_exit {
255 return None;
256 }
257 } else {
258 return None;
259 }
260
261 if let Some((te, tx)) = slab_test(
263 moving.min.z,
264 moving.max.z,
265 target.min.z,
266 target.max.z,
267 velocity.z,
268 ) {
269 t_enter = if te > t_enter { te } else { t_enter };
270 t_exit = if tx < t_exit { tx } else { t_exit };
271 if t_enter > t_exit {
272 return None;
273 }
274 } else {
275 return None;
276 }
277
278 if t_enter >= Fix128::ZERO && t_enter <= Fix128::ONE {
279 Some(t_enter)
280 } else if t_enter < Fix128::ZERO && t_exit >= Fix128::ZERO {
281 Some(Fix128::ZERO) } else {
283 None
284 }
285}
286
287fn slab_test(
289 a_min: Fix128,
290 a_max: Fix128,
291 b_min: Fix128,
292 b_max: Fix128,
293 vel: Fix128,
294) -> Option<(Fix128, Fix128)> {
295 if vel.is_zero() {
296 if a_max < b_min || a_min > b_max {
298 return None;
299 }
300 return Some((Fix128::from_int(-1000000), Fix128::from_int(1000000)));
301 }
302
303 let inv_vel = Fix128::ONE / vel;
304 let mut t0 = (b_min - a_max) * inv_vel;
305 let mut t1 = (b_max - a_min) * inv_vel;
306
307 if t0 > t1 {
308 core::mem::swap(&mut t0, &mut t1);
309 }
310
311 Some((t0, t1))
312}
313
314#[must_use]
356#[allow(clippy::too_many_arguments)]
358pub fn adaptive_toi_substeps(
359 pos_a: Vec3Fix,
360 vel_a: Vec3Fix,
361 radius_a: Fix128,
362 pos_b: Vec3Fix,
363 vel_b: Vec3Fix,
364 radius_b: Fix128,
365 dt: Fix128,
366 max_substeps: u32,
367) -> u32 {
368 if speculative_contact(pos_a, vel_a, radius_a, pos_b, vel_b, radius_b, dt).is_none() {
371 return 1;
372 }
373 on_collision_course_substeps(vel_a, radius_a, vel_b, radius_b, dt, max_substeps)
375}
376
377#[cfg(feature = "std")]
382fn on_collision_course_substeps(
383 vel_a: Vec3Fix,
384 radius_a: Fix128,
385 vel_b: Vec3Fix,
386 radius_b: Fix128,
387 dt: Fix128,
388 max_substeps: u32,
389) -> u32 {
390 struct CcdVelocityBody(Vec3Fix);
391 impl crate::solver_tgs::HasVelocity for CcdVelocityBody {
392 fn velocity_l_inf(&self) -> Fix128 {
393 let ax = self.0.x.abs();
394 let ay = self.0.y.abs();
395 let az = self.0.z.abs();
396 let m = if ax > ay { ax } else { ay };
397 if az > m {
398 az
399 } else {
400 m
401 }
402 }
403 }
404
405 let bodies = [CcdVelocityBody(vel_a), CcdVelocityBody(vel_b)];
406 let smallest_radius = if radius_a < radius_b {
407 radius_a
408 } else {
409 radius_b
410 };
411 let ccd_safety_factor = Fix128::from_ratio(1, 2);
415 let cfg = crate::solver_tgs::AdaptiveSubStepConfig {
416 max_substeps: max_substeps.max(1),
417 min_substeps: 1,
418 ..crate::solver_tgs::AdaptiveSubStepConfig::default()
419 };
420 crate::solver_tgs::adaptive_substeps_for_ccd(
421 &bodies,
422 dt,
423 smallest_radius,
424 ccd_safety_factor,
425 &cfg,
426 )
427}
428
429#[cfg(not(feature = "std"))]
433fn on_collision_course_substeps(
434 _vel_a: Vec3Fix,
435 _radius_a: Fix128,
436 _vel_b: Vec3Fix,
437 _radius_b: Fix128,
438 _dt: Fix128,
439 max_substeps: u32,
440) -> u32 {
441 max_substeps.max(1)
442}
443
444#[must_use]
450pub fn speculative_contact(
451 pos_a: Vec3Fix,
452 vel_a: Vec3Fix,
453 radius_a: Fix128,
454 pos_b: Vec3Fix,
455 vel_b: Vec3Fix,
456 radius_b: Fix128,
457 dt: Fix128,
458) -> Option<crate::collider::Contact> {
459 let rel_pos = pos_b - pos_a;
460 let dist = rel_pos.length();
461 let combined_r = radius_a + radius_b;
462
463 if dist.is_zero() {
464 return None;
465 }
466
467 let dir_ab = rel_pos / dist;
469 let normal = -dir_ab;
470 let gap = dist - combined_r;
471
472 if gap <= Fix128::ZERO {
474 return Some(crate::collider::Contact {
475 depth: -gap,
476 normal,
477 point_a: pos_a + dir_ab * radius_a,
478 point_b: pos_b - dir_ab * radius_b,
479 });
480 }
481
482 let rel_vel = vel_b - vel_a;
484 let closing_speed = -rel_vel.dot(dir_ab);
485
486 if closing_speed <= Fix128::ZERO {
487 return None; }
489
490 let predicted_gap = gap - closing_speed * dt;
491
492 if predicted_gap < Fix128::ZERO {
493 Some(crate::collider::Contact {
495 depth: -predicted_gap,
496 normal,
497 point_a: pos_a + dir_ab * radius_a,
498 point_b: pos_b - dir_ab * radius_b,
499 })
500 } else {
501 None
502 }
503}
504
505#[must_use]
510pub fn capsule_plane_toi(
511 cap_a: Vec3Fix,
512 cap_b: Vec3Fix,
513 radius: Fix128,
514 velocity: Vec3Fix,
515 plane_normal: Vec3Fix,
516 plane_offset: Fix128,
517) -> Option<TOI> {
518 let dist_a = cap_a.dot(plane_normal) - plane_offset;
520 let dist_b = cap_b.dot(plane_normal) - plane_offset;
521 let (center, _dist) = if dist_a < dist_b {
523 (cap_a, dist_a)
524 } else {
525 (cap_b, dist_b)
526 };
527 sphere_plane_toi(center, radius, velocity, plane_normal, plane_offset)
528}
529
530#[must_use]
534pub fn sphere_capsule_toi(
535 sphere_center: Vec3Fix,
536 sphere_radius: Fix128,
537 sphere_vel: Vec3Fix,
538 cap_a: Vec3Fix,
539 cap_b: Vec3Fix,
540 cap_radius: Fix128,
541) -> Option<TOI> {
542 let ab = cap_b - cap_a;
544 let ab_len_sq = ab.dot(ab);
545 let closest = if ab_len_sq.is_zero() {
546 cap_a
547 } else {
548 let t = (sphere_center - cap_a).dot(ab) / ab_len_sq;
549 let t_clamped = if t < Fix128::ZERO {
550 Fix128::ZERO
551 } else if t > Fix128::ONE {
552 Fix128::ONE
553 } else {
554 t
555 };
556 cap_a + ab * t_clamped
557 };
558 sphere_sphere_toi(
559 sphere_center,
560 sphere_radius,
561 sphere_vel,
562 closest,
563 cap_radius,
564 Vec3Fix::ZERO, )
566}
567
568#[must_use]
572pub fn aabb_plane_toi(
573 aabb: &AABB,
574 velocity: Vec3Fix,
575 plane_normal: Vec3Fix,
576 plane_offset: Fix128,
577) -> Option<TOI> {
578 let support_x = if plane_normal.x < Fix128::ZERO {
580 aabb.max.x
581 } else {
582 aabb.min.x
583 };
584 let support_y = if plane_normal.y < Fix128::ZERO {
585 aabb.max.y
586 } else {
587 aabb.min.y
588 };
589 let support_z = if plane_normal.z < Fix128::ZERO {
590 aabb.max.z
591 } else {
592 aabb.min.z
593 };
594 let support = Vec3Fix::new(support_x, support_y, support_z);
595
596 let dist = support.dot(plane_normal) - plane_offset;
597 let vel_toward = velocity.dot(plane_normal);
598
599 if vel_toward >= Fix128::ZERO && dist > Fix128::ZERO {
601 return None;
602 }
603
604 if dist <= Fix128::ZERO {
606 return Some(TOI {
607 t: Fix128::ZERO,
608 point: support,
609 normal: plane_normal,
610 });
611 }
612
613 if vel_toward.is_zero() {
614 return None;
615 }
616
617 let t = -dist / vel_toward;
619
620 if t >= Fix128::ZERO && t <= Fix128::ONE {
621 let point = support + velocity * t;
622 Some(TOI {
623 t,
624 point,
625 normal: plane_normal,
626 })
627 } else {
628 None
629 }
630}
631
632impl crate::solver::PhysicsWorld {
637 #[must_use]
667 pub fn time_of_impact(
668 &self,
669 body: usize,
670 velocity: Vec3Fix,
671 dt: Fix128,
672 filter: &crate::shape_raycast::RayFilter,
673 ) -> Option<TOI> {
674 if dt <= Fix128::ZERO {
675 return None;
676 }
677 let (radius, _) = self.ray_geometry(body)?;
678 let start = self.bodies.get(body)?.position;
679 let displacement = velocity * dt;
680 let travel = displacement.length();
681 if travel.is_zero() {
682 return None;
683 }
684 let filter = filter.excluding_body(body);
685 let hit = self.cast_sphere(start, radius, displacement, travel, &filter)?;
686 let t = if hit.t >= travel {
687 Fix128::ONE
688 } else {
689 hit.t / travel
690 };
691 Some(TOI {
692 t,
693 point: hit.point,
694 normal: hit.normal,
695 })
696 }
697}
698
699#[cfg(test)]
700mod tests {
701 use super::*;
702
703 #[test]
704 fn test_sphere_sphere_toi() {
705 let toi = sphere_sphere_toi(
707 Vec3Fix::from_int(-5, 0, 0),
708 Fix128::ONE,
709 Vec3Fix::from_int(10, 0, 0),
710 Vec3Fix::from_int(5, 0, 0),
711 Fix128::ONE,
712 Vec3Fix::from_int(-10, 0, 0),
713 );
714
715 let toi = toi.expect("Should find collision");
716 let expected = Fix128::from_ratio(4, 10);
719 let error = (toi.t - expected).abs();
720 assert!(
721 error < Fix128::from_ratio(1, 10),
722 "TOI should be ~0.4, got {:?}",
723 toi.t
724 );
725 }
726
727 #[test]
728 fn test_sphere_sphere_miss() {
729 let toi = sphere_sphere_toi(
731 Vec3Fix::from_int(-5, 0, 0),
732 Fix128::ONE,
733 Vec3Fix::from_int(-10, 0, 0),
734 Vec3Fix::from_int(5, 0, 0),
735 Fix128::ONE,
736 Vec3Fix::from_int(10, 0, 0),
737 );
738 assert!(toi.is_none());
739 }
740
741 #[test]
742 fn test_sphere_plane_toi() {
743 let toi = sphere_plane_toi(
745 Vec3Fix::from_int(0, 10, 0),
746 Fix128::ONE,
747 Vec3Fix::from_int(0, -20, 0),
748 Vec3Fix::UNIT_Y,
749 Fix128::ZERO,
750 );
751
752 let toi = toi.expect("Should find collision");
753 let expected = Fix128::from_ratio(45, 100);
756 let error = (toi.t - expected).abs();
757 assert!(error < Fix128::from_ratio(1, 10), "TOI should be ~0.45");
758 }
759
760 #[test]
761 fn test_needs_ccd() {
762 let config = CcdConfig::default();
763 let fast = Vec3Fix::from_int(100, 0, 0);
764 let slow = Vec3Fix::from_int(1, 0, 0);
765 let dt = Fix128::from_ratio(1, 60);
766 let radius = Fix128::ONE;
767
768 assert!(needs_ccd(fast, radius, dt, &config), "Fast body needs CCD");
769 assert!(
770 !needs_ccd(slow, radius, dt, &config),
771 "Slow body doesn't need CCD"
772 );
773 }
774
775 #[test]
776 fn test_swept_aabb() {
777 let moving = AABB::new(Vec3Fix::from_int(-1, -1, -1), Vec3Fix::from_int(1, 1, 1));
778 let target = AABB::new(Vec3Fix::from_int(5, -1, -1), Vec3Fix::from_int(7, 1, 1));
779 let velocity = Vec3Fix::from_int(10, 0, 0);
780
781 let t = swept_aabb(&moving, velocity, &target);
782 assert!(t.is_some(), "Should detect swept collision");
783 let t = t.unwrap();
784 let expected = Fix128::from_ratio(4, 10);
787 let error = (t - expected).abs();
788 assert!(error < Fix128::from_ratio(1, 10), "TOI should be ~0.4");
789 }
790
791 #[test]
792 fn test_conservative_advancement() {
793 let config = CcdConfig::default();
794 let start = Vec3Fix::from_int(-10, 0, 0);
795 let displacement = Vec3Fix::from_int(20, 0, 0);
796 let radius = Fix128::ONE;
797
798 let toi = conservative_advancement(
800 start,
801 displacement,
802 radius,
803 |pos| {
804 let dist = pos.length() - Fix128::from_int(2);
805 let normal = pos.normalize();
806 (dist, normal)
807 },
808 &config,
809 );
810
811 assert!(
812 toi.is_some(),
813 "Should find collision via conservative advancement"
814 );
815 }
816
817 #[test]
818 fn test_speculative_contact_closing() {
819 let dt = Fix128::from_ratio(1, 60);
820 let contact = speculative_contact(
822 Vec3Fix::from_int(-3, 0, 0),
823 Vec3Fix::from_int(300, 0, 0),
824 Fix128::ONE,
825 Vec3Fix::from_int(3, 0, 0),
826 Vec3Fix::ZERO,
827 Fix128::ONE,
828 dt,
829 );
830 assert!(
831 contact.is_some(),
832 "Fast-closing spheres should generate speculative contact"
833 );
834 }
835
836 #[test]
837 fn test_speculative_contact_separating() {
838 let dt = Fix128::from_ratio(1, 60);
839 let contact = speculative_contact(
840 Vec3Fix::from_int(-3, 0, 0),
841 Vec3Fix::from_int(-10, 0, 0), Fix128::ONE,
843 Vec3Fix::from_int(3, 0, 0),
844 Vec3Fix::ZERO,
845 Fix128::ONE,
846 dt,
847 );
848 assert!(
849 contact.is_none(),
850 "Separating spheres should not generate contact"
851 );
852 }
853
854 #[test]
855 fn test_capsule_plane_toi() {
856 let toi = capsule_plane_toi(
857 Vec3Fix::from_int(0, 10, -1),
858 Vec3Fix::from_int(0, 10, 1),
859 Fix128::ONE,
860 Vec3Fix::from_int(0, -20, 0),
861 Vec3Fix::UNIT_Y,
862 Fix128::ZERO,
863 );
864 let toi = toi.expect("Should find capsule-plane collision");
865 assert!(toi.t > Fix128::ZERO && toi.t < Fix128::ONE);
866 }
867
868 #[test]
869 fn test_sphere_capsule_toi() {
870 let toi = sphere_capsule_toi(
871 Vec3Fix::from_int(-10, 0, 0),
872 Fix128::ONE,
873 Vec3Fix::from_int(20, 0, 0),
874 Vec3Fix::from_int(5, -2, 0),
875 Vec3Fix::from_int(5, 2, 0),
876 Fix128::ONE,
877 );
878 let toi = toi.expect("Should find sphere-capsule collision");
879 assert!(toi.t > Fix128::ZERO && toi.t < Fix128::ONE);
880 }
881
882 #[test]
883 fn test_aabb_plane_toi() {
884 let aabb = AABB::new(Vec3Fix::from_int(-1, 8, -1), Vec3Fix::from_int(1, 10, 1));
885 let toi = aabb_plane_toi(
886 &aabb,
887 Vec3Fix::from_int(0, -20, 0),
888 Vec3Fix::UNIT_Y,
889 Fix128::ZERO,
890 );
891 let toi = toi.expect("Should find AABB-plane collision");
892 assert!(toi.t > Fix128::ZERO && toi.t < Fix128::ONE);
893 }
894
895 #[test]
896 fn test_aabb_plane_miss() {
897 let aabb = AABB::new(Vec3Fix::from_int(-1, 8, -1), Vec3Fix::from_int(1, 10, 1));
898 let toi = aabb_plane_toi(
899 &aabb,
900 Vec3Fix::from_int(0, 20, 0), Vec3Fix::UNIT_Y,
902 Fix128::ZERO,
903 );
904 assert!(toi.is_none());
905 }
906
907 fn fi(n: i64) -> Fix128 {
910 Fix128::from_int(n)
911 }
912
913 fn r(n: i64, d: i64) -> Fix128 {
914 Fix128::from_ratio(n, d)
915 }
916
917 fn v3i(x: i64, y: i64, z: i64) -> Vec3Fix {
918 Vec3Fix::from_int(x, y, z)
919 }
920
921 #[test]
922 fn needs_ccd_requires_both_displacement_and_speed_thresholds() {
923 let cfg = CcdConfig::default(); assert!(needs_ccd(v3i(8, 0, 0), fi(2), r(1, 4), &cfg));
926 assert!(!needs_ccd(v3i(8, 0, 0), fi(2), r(1, 16), &cfg));
928 assert!(!needs_ccd(v3i(4, 0, 0), fi(2), fi(1), &cfg));
930 assert!(!needs_ccd(v3i(5, 0, 0), fi(2), fi(1), &cfg));
932 assert!(needs_ccd(v3i(0, 0, 6), fi(2), fi(1), &cfg));
933 }
934
935 #[test]
936 fn sphere_sphere_toi_closed_form() {
937 let toi = sphere_sphere_toi(
939 Vec3Fix::ZERO,
940 fi(1),
941 v3i(8, 0, 0),
942 v3i(10, 0, 0),
943 fi(1),
944 Vec3Fix::ZERO,
945 )
946 .expect("hit at t=1");
947 assert_eq!(toi.t, Fix128::ONE);
948 assert_eq!(toi.normal, v3i(1, 0, 0));
949 assert_eq!(toi.point, v3i(9, 0, 0)); let t2 = sphere_sphere_toi(
952 Vec3Fix::ZERO,
953 fi(1),
954 v3i(8, 0, 0),
955 v3i(10, 0, 0),
956 fi(1),
957 v3i(-8, 0, 0),
958 )
959 .expect("t=1/2");
960 assert_eq!(t2.t, r(1, 2));
961 assert_eq!(t2.point, v3i(5, 0, 0));
962 let t3 = sphere_sphere_toi(
964 Vec3Fix::ZERO,
965 fi(1),
966 v3i(8, 0, 0),
967 v3i(10, 0, 0),
968 fi(3),
969 Vec3Fix::ZERO,
970 )
971 .expect("t=3/4");
972 assert_eq!(t3.t, r(3, 4));
973 assert!(sphere_sphere_toi(
975 Vec3Fix::ZERO,
976 fi(1),
977 v3i(4, 0, 0),
978 v3i(10, 0, 0),
979 fi(1),
980 Vec3Fix::ZERO
981 )
982 .is_none());
983 assert!(sphere_sphere_toi(
984 Vec3Fix::ZERO,
985 fi(1),
986 v3i(-8, 0, 0),
987 v3i(10, 0, 0),
988 fi(1),
989 Vec3Fix::ZERO
990 )
991 .is_none());
992 assert!(sphere_sphere_toi(
993 Vec3Fix::ZERO,
994 fi(1),
995 v3i(8, 0, 0),
996 v3i(10, 5, 0),
997 fi(1),
998 Vec3Fix::ZERO
999 )
1000 .is_none());
1001 assert!(sphere_sphere_toi(
1002 Vec3Fix::ZERO,
1003 fi(1),
1004 Vec3Fix::ZERO,
1005 v3i(10, 0, 0),
1006 fi(1),
1007 Vec3Fix::ZERO
1008 )
1009 .is_none());
1010 let ov = sphere_sphere_toi(
1012 Vec3Fix::ZERO,
1013 fi(1),
1014 Vec3Fix::ZERO,
1015 v3i(1, 0, 0),
1016 fi(1),
1017 Vec3Fix::ZERO,
1018 )
1019 .expect("overlap");
1020 assert_eq!(ov.t, Fix128::ZERO);
1021 assert_eq!(ov.normal, v3i(1, 0, 0));
1022 assert_eq!(ov.point, v3i(1, 0, 0));
1023 let tan = sphere_sphere_toi(
1025 Vec3Fix::ZERO,
1026 fi(1),
1027 v3i(16, 0, 0),
1028 v3i(10, 2, 0),
1029 fi(1),
1030 Vec3Fix::ZERO,
1031 )
1032 .expect("tangent");
1033 assert_eq!(tan.t, r(5, 8));
1034 }
1035
1036 #[test]
1037 fn sphere_plane_toi_closed_form() {
1038 let toi = sphere_plane_toi(
1040 v3i(0, 5, 0),
1041 fi(1),
1042 v3i(0, -8, 0),
1043 v3i(0, 1, 0),
1044 Fix128::ZERO,
1045 )
1046 .expect("hit");
1047 assert_eq!(toi.t, r(1, 2));
1048 assert_eq!(toi.point, v3i(0, 0, 0)); assert_eq!(toi.normal, v3i(0, 1, 0));
1050 let off =
1052 sphere_plane_toi(v3i(0, 5, 0), fi(1), v3i(0, -8, 0), v3i(0, 1, 0), fi(2)).expect("hit");
1053 assert_eq!(off.t, r(1, 4));
1054 assert_eq!(off.point, v3i(0, 2, 0));
1055 assert!(sphere_plane_toi(
1057 v3i(0, 5, 0),
1058 fi(1),
1059 v3i(0, 8, 0),
1060 v3i(0, 1, 0),
1061 Fix128::ZERO
1062 )
1063 .is_none());
1064 assert!(sphere_plane_toi(
1065 v3i(0, 5, 0),
1066 fi(1),
1067 v3i(8, 0, 0),
1068 v3i(0, 1, 0),
1069 Fix128::ZERO
1070 )
1071 .is_none());
1072 assert!(sphere_plane_toi(
1073 v3i(0, 5, 0),
1074 fi(1),
1075 v3i(0, -2, 0),
1076 v3i(0, 1, 0),
1077 Fix128::ZERO
1078 )
1079 .is_none());
1080 let pen = sphere_plane_toi(
1082 v3i(3, 1, 0),
1083 fi(1),
1084 v3i(0, 8, 0),
1085 v3i(0, 1, 0),
1086 Fix128::ZERO,
1087 )
1088 .expect("touching");
1089 assert_eq!(pen.t, Fix128::ZERO);
1090 assert_eq!(pen.point, v3i(3, 0, 0));
1091 let back = sphere_plane_toi(
1095 v3i(0, -5, 0),
1096 fi(1),
1097 v3i(0, 8, 0),
1098 v3i(0, 1, 0),
1099 Fix128::ZERO,
1100 )
1101 .expect("from behind");
1102 assert_eq!(back.t, r(1, 2));
1103 assert_eq!(back.normal, v3i(0, -1, 0));
1104 assert_eq!(back.point, Vec3Fix::ZERO);
1105 assert!(sphere_plane_toi(
1107 v3i(0, -5, 0),
1108 fi(1),
1109 v3i(0, -8, 0),
1110 v3i(0, 1, 0),
1111 Fix128::ZERO
1112 )
1113 .is_none());
1114 }
1115
1116 #[test]
1117 fn conservative_advancement_converges_on_a_plane() {
1118 let cfg = CcdConfig {
1119 max_iterations: 32,
1120 tolerance: r(1, 1000),
1121 velocity_threshold: fi(5),
1122 };
1123 let plane = |p: Vec3Fix| (fi(10) - p.x, v3i(-1, 0, 0));
1125 let toi = conservative_advancement(Vec3Fix::ZERO, v3i(16, 0, 0), fi(1), plane, &cfg)
1127 .expect("hit");
1128 assert!((toi.t - r(9, 16)).abs() < r(1, 1000), "t {:?}", toi.t);
1129 assert_eq!(toi.normal, v3i(-1, 0, 0));
1130 assert!(
1132 conservative_advancement(Vec3Fix::ZERO, v3i(4, 0, 0), fi(1), plane, &cfg).is_none()
1133 );
1134 assert!(
1135 conservative_advancement(Vec3Fix::ZERO, Vec3Fix::ZERO, fi(1), plane, &cfg).is_none()
1136 );
1137 let now = conservative_advancement(v3i(9, 0, 0), v3i(16, 0, 0), fi(1), plane, &cfg)
1139 .expect("immediate");
1140 assert_eq!(now.t, Fix128::ZERO);
1141 let one = CcdConfig {
1143 max_iterations: 1,
1144 ..cfg
1145 };
1146 assert!(
1147 conservative_advancement(Vec3Fix::ZERO, v3i(16, 0, 0), fi(1), plane, &one).is_none()
1148 );
1149 }
1150
1151 #[test]
1152 fn swept_aabb_entry_time_per_axis_and_overlap_cases() {
1153 let m = AABB::new(v3i(-1, -1, -1), v3i(1, 1, 1));
1154 let target = AABB::new(v3i(5, -1, -1), v3i(7, 1, 1));
1155 assert_eq!(swept_aabb(&m, v3i(8, 0, 0), &target), Some(r(1, 2)));
1157 assert_eq!(swept_aabb(&m, v3i(2, 0, 0), &target), None);
1159 assert_eq!(swept_aabb(&m, v3i(-8, 0, 0), &target), None);
1161 assert_eq!(
1162 swept_aabb(&m, v3i(8, 0, 0), &AABB::new(v3i(5, 3, -1), v3i(7, 5, 1))),
1163 None
1164 );
1165 assert_eq!(
1167 swept_aabb(&m, v3i(8, 8, 0), &AABB::new(v3i(5, 5, -1), v3i(7, 7, 1))),
1168 Some(r(1, 2))
1169 );
1170 assert_eq!(
1172 swept_aabb(&m, v3i(8, 8, 0), &AABB::new(v3i(5, 3, -1), v3i(7, 5, 1))),
1173 Some(r(1, 2))
1174 );
1175 assert_eq!(
1177 swept_aabb(&m, v3i(0, 8, 0), &AABB::new(v3i(-1, 5, -1), v3i(1, 7, 1))),
1178 Some(r(1, 2))
1179 );
1180 assert_eq!(
1181 swept_aabb(
1182 &m,
1183 v3i(0, 0, -8),
1184 &AABB::new(v3i(-1, -1, -7), v3i(1, 1, -5))
1185 ),
1186 Some(r(1, 2))
1187 );
1188 assert_eq!(
1190 swept_aabb(&m, v3i(8, 0, 0), &AABB::new(Vec3Fix::ZERO, v3i(2, 2, 2))),
1191 Some(Fix128::ZERO)
1192 );
1193 assert_eq!(swept_aabb(&m, Vec3Fix::ZERO, &target), None);
1194 assert_eq!(
1195 swept_aabb(&m, Vec3Fix::ZERO, &AABB::new(Vec3Fix::ZERO, v3i(2, 2, 2))),
1196 Some(Fix128::ZERO)
1197 );
1198 assert_eq!(swept_aabb(&m, v3i(4, 0, 0), &target), Some(Fix128::ONE));
1200 }
1201
1202 #[test]
1203 fn slab_test_orders_times_and_handles_static_axis() {
1204 assert_eq!(
1206 slab_test(fi(-1), fi(1), fi(5), fi(7), fi(8)),
1207 Some((r(1, 2), fi(1)))
1208 );
1209 assert_eq!(
1211 slab_test(fi(5), fi(7), fi(-1), fi(1), fi(-8)),
1212 Some((r(1, 2), fi(1)))
1213 );
1214 assert_eq!(slab_test(fi(-1), fi(1), fi(5), fi(7), Fix128::ZERO), None);
1216 assert_eq!(
1217 slab_test(fi(-1), fi(1), fi(0), fi(7), Fix128::ZERO),
1218 Some((fi(-1000000), fi(1000000)))
1219 );
1220 assert_eq!(
1221 slab_test(fi(-1), fi(1), fi(1), fi(7), Fix128::ZERO),
1222 Some((fi(-1000000), fi(1000000)))
1223 );
1224 assert_eq!(
1225 slab_test(fi(-1), fi(1), fi(-7), fi(-1), Fix128::ZERO),
1226 Some((fi(-1000000), fi(1000000)))
1227 );
1228 }
1229
1230 #[test]
1231 fn speculative_contact_predicts_gap_breach() {
1232 assert!(speculative_contact(
1234 Vec3Fix::ZERO,
1235 v3i(8, 0, 0),
1236 fi(1),
1237 v3i(10, 0, 0),
1238 Vec3Fix::ZERO,
1239 fi(1),
1240 r(1, 2)
1241 )
1242 .is_none());
1243 let c = speculative_contact(
1245 Vec3Fix::ZERO,
1246 v3i(8, 0, 0),
1247 fi(1),
1248 v3i(10, 0, 0),
1249 Vec3Fix::ZERO,
1250 fi(1),
1251 fi(2),
1252 )
1253 .expect("breach");
1254 assert_eq!(c.depth, fi(8));
1255 assert_eq!(c.normal, v3i(-1, 0, 0));
1257 assert_eq!(c.point_a, v3i(1, 0, 0));
1258 assert_eq!(c.point_b, v3i(9, 0, 0));
1259 assert!(speculative_contact(
1261 Vec3Fix::ZERO,
1262 v3i(8, 0, 0),
1263 fi(1),
1264 v3i(10, 0, 0),
1265 Vec3Fix::ZERO,
1266 fi(1),
1267 fi(1)
1268 )
1269 .is_none());
1270 assert!(speculative_contact(
1272 Vec3Fix::ZERO,
1273 v3i(-8, 0, 0),
1274 fi(1),
1275 v3i(10, 0, 0),
1276 Vec3Fix::ZERO,
1277 fi(1),
1278 fi(9)
1279 )
1280 .is_none());
1281 let ov = speculative_contact(
1282 Vec3Fix::ZERO,
1283 Vec3Fix::ZERO,
1284 fi(1),
1285 v3i(1, 0, 0),
1286 Vec3Fix::ZERO,
1287 fi(1),
1288 fi(1),
1289 )
1290 .expect("overlap");
1291 assert_eq!(ov.depth, fi(1));
1292 assert_eq!(ov.point_b, Vec3Fix::ZERO);
1293 assert!(speculative_contact(
1295 Vec3Fix::ZERO,
1296 v3i(8, 0, 0),
1297 fi(1),
1298 Vec3Fix::ZERO,
1299 Vec3Fix::ZERO,
1300 fi(1),
1301 fi(1)
1302 )
1303 .is_none());
1304 let both = speculative_contact(
1305 Vec3Fix::ZERO,
1306 v3i(8, 0, 0),
1307 fi(1),
1308 v3i(10, 0, 0),
1309 v3i(-8, 0, 0),
1310 fi(1),
1311 fi(1),
1312 )
1313 .expect("both");
1314 assert_eq!(both.depth, fi(8));
1315 assert_eq!(
1317 adaptive_toi_substeps(
1318 Vec3Fix::ZERO,
1319 v3i(8, 0, 0),
1320 fi(1),
1321 v3i(10, 0, 0),
1322 Vec3Fix::ZERO,
1323 fi(1),
1324 fi(2),
1325 6
1326 ),
1327 6
1328 );
1329 assert_eq!(
1330 adaptive_toi_substeps(
1331 Vec3Fix::ZERO,
1332 v3i(8, 0, 0),
1333 fi(1),
1334 v3i(10, 0, 0),
1335 Vec3Fix::ZERO,
1336 fi(1),
1337 r(1, 2),
1338 6
1339 ),
1340 1
1341 );
1342 assert_eq!(
1343 adaptive_toi_substeps(
1344 Vec3Fix::ZERO,
1345 v3i(8, 0, 0),
1346 fi(1),
1347 v3i(10, 0, 0),
1348 Vec3Fix::ZERO,
1349 fi(1),
1350 fi(2),
1351 0
1352 ),
1353 1
1354 );
1355 }
1356
1357 #[test]
1358 fn capsule_and_aabb_plane_toi_use_closest_feature() {
1359 let n = v3i(0, 1, 0);
1360 let cap = capsule_plane_toi(
1362 v3i(0, 5, 0),
1363 v3i(0, 9, 0),
1364 fi(1),
1365 v3i(0, -8, 0),
1366 n,
1367 Fix128::ZERO,
1368 )
1369 .expect("hit");
1370 assert_eq!(cap.t, r(1, 2));
1371 let cap2 = capsule_plane_toi(
1373 v3i(0, 9, 0),
1374 v3i(0, 5, 0),
1375 fi(1),
1376 v3i(0, -8, 0),
1377 n,
1378 Fix128::ZERO,
1379 )
1380 .expect("hit");
1381 assert_eq!(cap2.t, r(1, 2));
1382 let sc = sphere_capsule_toi(
1384 Vec3Fix::ZERO,
1385 fi(1),
1386 v3i(8, 0, 0),
1387 v3i(10, -5, 0),
1388 v3i(10, 5, 0),
1389 fi(1),
1390 )
1391 .expect("hit");
1392 assert_eq!(sc.t, Fix128::ONE);
1393 assert!(sphere_capsule_toi(
1395 v3i(0, 9, 0),
1396 fi(1),
1397 v3i(8, 0, 0),
1398 v3i(10, -5, 0),
1399 v3i(10, 5, 0),
1400 fi(1)
1401 )
1402 .is_none());
1403 let deg = sphere_capsule_toi(
1405 Vec3Fix::ZERO,
1406 fi(1),
1407 v3i(8, 0, 0),
1408 v3i(10, 0, 0),
1409 v3i(10, 0, 0),
1410 fi(1),
1411 )
1412 .expect("sphere");
1413 assert_eq!(deg.t, Fix128::ONE);
1414 let ab = aabb_plane_toi(
1416 &AABB::new(v3i(1, 1, 1), v3i(3, 3, 3)),
1417 v3i(0, -8, 0),
1418 n,
1419 Fix128::ZERO,
1420 )
1421 .expect("hit");
1422 assert_eq!(ab.t, r(1, 8));
1423 assert_eq!(ab.normal, n);
1424 let ab2 = aabb_plane_toi(
1426 &AABB::new(v3i(1, 1, 1), v3i(3, 3, 3)),
1427 v3i(0, 8, 0),
1428 v3i(0, -1, 0),
1429 fi(-5),
1430 )
1431 .expect("hit");
1432 assert_eq!(ab2.t, r(1, 4));
1433 assert!(aabb_plane_toi(
1435 &AABB::new(v3i(1, 1, 1), v3i(3, 3, 3)),
1436 v3i(0, 8, 0),
1437 n,
1438 Fix128::ZERO
1439 )
1440 .is_none());
1441 let pen = aabb_plane_toi(
1442 &AABB::new(v3i(1, -1, 1), v3i(3, 3, 3)),
1443 v3i(0, 8, 0),
1444 n,
1445 Fix128::ZERO,
1446 )
1447 .expect("penetrating");
1448 assert_eq!(pen.t, Fix128::ZERO);
1449 assert_eq!(pen.point, v3i(1, -1, 1));
1450 assert!(aabb_plane_toi(
1451 &AABB::new(v3i(1, 1, 1), v3i(3, 3, 3)),
1452 v3i(8, 0, 0),
1453 n,
1454 Fix128::ZERO
1455 )
1456 .is_none());
1457 }
1458
1459 #[test]
1472 fn needs_ccd_displacement_boundary_is_strict() {
1473 let cfg = CcdConfig::default();
1474 assert!(!needs_ccd(v3i(8, 0, 0), fi(1), r(1, 16), &cfg));
1475 assert!(needs_ccd(v3i(8, 0, 0), fi(1), r(1, 8), &cfg));
1477 }
1478
1479 #[test]
1486 fn sphere_sphere_toi_overlap_point_scales_with_radius_a() {
1487 let toi = sphere_sphere_toi(
1488 v3i(3, 0, 0),
1489 fi(2),
1490 Vec3Fix::ZERO,
1491 v3i(4, 0, 0),
1492 fi(1),
1493 Vec3Fix::ZERO,
1494 )
1495 .expect("overlapping");
1496 assert_eq!(toi.t, Fix128::ZERO);
1497 assert_eq!(toi.normal, v3i(1, 0, 0));
1498 assert_eq!(toi.point, v3i(5, 0, 0));
1499 }
1500
1501 #[test]
1517 fn sphere_sphere_toi_oblique_impact_closed_form() {
1518 let toi = sphere_sphere_toi(
1519 v3i(0, 1, 0),
1520 r(3, 2),
1521 v3i(8, 0, 0),
1522 v3i(8, 7, 0),
1523 r(1, 2),
1524 v3i(0, -8, 0),
1525 )
1526 .expect("hit at t=3/4");
1527 assert_eq!(toi.t, r(3, 4));
1528 assert_eq!(toi.normal, v3i(1, 0, 0));
1529 assert_eq!(toi.point, Vec3Fix::new(r(15, 2), fi(1), Fix128::ZERO));
1530 }
1531
1532 #[test]
1541 fn sphere_plane_toi_points_scale_with_dist_and_radius() {
1542 let n = v3i(0, 1, 0);
1543 let pen = sphere_plane_toi(
1544 Vec3Fix::new(fi(3), r(5, 2), Fix128::ZERO),
1545 fi(1),
1546 v3i(0, 8, 0),
1547 n,
1548 fi(2),
1549 )
1550 .expect("penetrating");
1551 assert_eq!(pen.t, Fix128::ZERO);
1552 assert_eq!(pen.point, v3i(3, 2, 0));
1553
1554 let hit = sphere_plane_toi(v3i(0, 5, 0), fi(2), v3i(0, -8, 0), n, Fix128::ZERO)
1555 .expect("surface hit");
1556 assert_eq!(hit.t, r(3, 8));
1557 assert_eq!(hit.point, Vec3Fix::ZERO);
1558 }
1559
1560 #[test]
1573 fn conservative_advancement_contact_point_and_t_equal_one() {
1574 let cfg = CcdConfig::default();
1575 let plane = |p: Vec3Fix| (fi(10) - p.x, v3i(-1, 0, 0));
1576
1577 let now = conservative_advancement(v3i(8, 0, 0), v3i(16, 0, 0), fi(2), plane, &cfg)
1578 .expect("immediate");
1579 assert_eq!(now.t, Fix128::ZERO);
1580 assert_eq!(now.point, v3i(10, 0, 0));
1581
1582 let edge = conservative_advancement(Vec3Fix::ZERO, v3i(9, 0, 0), fi(1), plane, &cfg)
1583 .expect("touch exactly at t = 1");
1584 assert_eq!(edge.t, Fix128::ONE);
1585 assert_eq!(edge.point, v3i(10, 0, 0));
1586 }
1587
1588 #[test]
1601 fn swept_aabb_corner_touch_counts_as_hit() {
1602 let thin_m = AABB::new(v3i(0, -1, -1), v3i(0, 1, 1));
1603 let thin_t = AABB::new(v3i(4, -1, -1), v3i(4, 1, 1));
1604 assert_eq!(swept_aabb(&thin_m, v3i(8, 0, 0), &thin_t), Some(r(1, 2)));
1605
1606 let m = AABB::new(v3i(-1, -1, -1), v3i(1, 1, 1));
1607 assert_eq!(
1608 swept_aabb(&m, v3i(8, 8, 0), &AABB::new(v3i(5, 9, -1), v3i(7, 11, 1))),
1609 Some(Fix128::ONE)
1610 );
1611 assert_eq!(
1612 swept_aabb(&m, v3i(8, 0, 8), &AABB::new(v3i(5, -1, 9), v3i(7, 1, 11))),
1613 Some(Fix128::ONE)
1614 );
1615 }
1616
1617 #[test]
1625 fn swept_aabb_exit_before_enter_on_later_axis_is_miss() {
1626 let m = AABB::new(v3i(-1, -1, -1), v3i(1, 1, 1));
1627 assert_eq!(
1628 swept_aabb(&m, v3i(8, 8, 0), &AABB::new(v3i(5, 1, -1), v3i(7, 2, 1))),
1629 None
1630 );
1631 assert_eq!(
1632 swept_aabb(&m, v3i(8, 0, 8), &AABB::new(v3i(5, -1, 1), v3i(7, 1, 2))),
1633 None
1634 );
1635 }
1636
1637 #[test]
1647 fn speculative_contact_points_scale_with_radii() {
1648 let ov = speculative_contact(
1649 v3i(2, 0, 0),
1650 Vec3Fix::ZERO,
1651 fi(2),
1652 v3i(3, 0, 0),
1653 Vec3Fix::ZERO,
1654 r(1, 2),
1655 fi(1),
1656 )
1657 .expect("overlap");
1658 assert_eq!(ov.depth, r(3, 2));
1659 assert_eq!(ov.point_a, v3i(4, 0, 0));
1660 assert_eq!(
1661 ov.point_b,
1662 Vec3Fix::new(r(5, 2), Fix128::ZERO, Fix128::ZERO)
1663 );
1664
1665 let sp = speculative_contact(
1666 Vec3Fix::ZERO,
1667 v3i(8, 0, 0),
1668 fi(2),
1669 v3i(10, 0, 0),
1670 Vec3Fix::ZERO,
1671 r(1, 2),
1672 fi(1),
1673 )
1674 .expect("speculative");
1675 assert_eq!(sp.depth, r(1, 2));
1676 assert_eq!(sp.point_a, v3i(2, 0, 0));
1677 assert_eq!(
1678 sp.point_b,
1679 Vec3Fix::new(r(19, 2), Fix128::ZERO, Fix128::ZERO)
1680 );
1681 }
1682
1683 #[test]
1696 fn capsule_plane_toi_offset_and_tie_break() {
1697 let n = v3i(0, 1, 0);
1698 let a_near = capsule_plane_toi(v3i(0, 5, 0), v3i(0, 7, 0), fi(1), v3i(0, -8, 0), n, fi(2))
1699 .expect("hit");
1700 assert_eq!(a_near.t, r(1, 4));
1701 assert_eq!(a_near.point, v3i(0, 2, 0));
1702
1703 let b_near = capsule_plane_toi(v3i(0, 7, 0), v3i(0, 5, 0), fi(1), v3i(0, -8, 0), n, fi(2))
1704 .expect("hit");
1705 assert_eq!(b_near.t, r(1, 4));
1706 assert_eq!(b_near.point, v3i(0, 2, 0));
1707
1708 let tie = capsule_plane_toi(
1709 v3i(0, 9, -1),
1710 v3i(0, 9, 1),
1711 fi(1),
1712 v3i(0, -16, 0),
1713 n,
1714 Fix128::ZERO,
1715 )
1716 .expect("hit");
1717 assert_eq!(tie.t, r(1, 2));
1718 assert_eq!(tie.point, v3i(0, 0, 1));
1719 }
1720
1721 #[test]
1730 fn sphere_capsule_toi_clamps_below_segment_start() {
1731 assert!(sphere_capsule_toi(
1732 v3i(0, -9, 0),
1733 fi(1),
1734 v3i(8, 0, 0),
1735 v3i(10, -5, 0),
1736 v3i(10, 5, 0),
1737 fi(1)
1738 )
1739 .is_none());
1740 let hit = sphere_capsule_toi(
1742 v3i(0, -5, 0),
1743 fi(1),
1744 v3i(8, 0, 0),
1745 v3i(10, -5, 0),
1746 v3i(10, 5, 0),
1747 fi(1),
1748 )
1749 .expect("hit");
1750 assert_eq!(hit.t, Fix128::ONE);
1751 }
1752
1753 #[test]
1767 fn aabb_plane_toi_support_vertex_follows_normal_sign() {
1768 let bx = AABB::new(v3i(1, 1, 1), v3i(3, 3, 3));
1769
1770 let neg_x = aabb_plane_toi(&bx, v3i(8, 0, 0), v3i(-1, 0, 0), fi(-10)).expect("hit");
1771 assert_eq!(neg_x.t, r(7, 8));
1772 assert_eq!(neg_x.point, v3i(10, 1, 1));
1773
1774 let pos_x = aabb_plane_toi(&bx, v3i(-8, 0, 0), v3i(1, 0, 0), fi(-5)).expect("hit");
1775 assert_eq!(pos_x.t, r(3, 4));
1776 assert_eq!(pos_x.point, v3i(-5, 1, 1));
1777
1778 let neg_z = aabb_plane_toi(&bx, v3i(0, 0, 8), v3i(0, 0, -1), fi(-10)).expect("hit");
1779 assert_eq!(neg_z.t, r(7, 8));
1780 assert_eq!(neg_z.point, v3i(1, 1, 10));
1781 }
1782
1783 #[test]
1795 fn aabb_plane_toi_resting_contact_and_contact_point() {
1796 let n = v3i(0, 1, 0);
1797 let resting = aabb_plane_toi(
1798 &AABB::new(v3i(1, 0, 1), v3i(3, 2, 3)),
1799 v3i(0, 8, 0),
1800 n,
1801 Fix128::ZERO,
1802 )
1803 .expect("resting contact");
1804 assert_eq!(resting.t, Fix128::ZERO);
1805 assert_eq!(resting.point, v3i(1, 0, 1));
1806
1807 let bx = AABB::new(v3i(1, 1, 1), v3i(3, 3, 3));
1808 assert!(aabb_plane_toi(
1809 &bx,
1810 Vec3Fix::new(Fix128::ZERO, r(-1, 2), Fix128::ZERO),
1811 n,
1812 Fix128::ZERO
1813 )
1814 .is_none());
1815
1816 let hit = aabb_plane_toi(&bx, v3i(0, -8, 0), n, Fix128::ZERO).expect("hit");
1817 assert_eq!(hit.t, r(1, 8));
1818 assert_eq!(hit.point, v3i(1, 0, 1));
1819 }
1820
1821 #[test]
1827 fn sphere_plane_toi_behind_the_plane_without_velocity_is_a_miss() {
1828 assert!(
1829 sphere_plane_toi(v3i(0, -5, 0), fi(1), Vec3Fix::ZERO, v3i(0, 1, 0), fi(0)).is_none()
1830 );
1831 let hit = sphere_plane_toi(v3i(0, -5, 0), fi(1), v3i(0, 8, 0), v3i(0, 1, 0), fi(0))
1834 .expect("rising sphere reaches the plane");
1835 assert_eq!(hit.t, r(1, 2));
1836 }
1837
1838 #[test]
1848 fn swept_aabb_x_window_and_static_z_separation_are_misses() {
1849 let far = AABB::new(v3i(2_000_000, 0, 0), v3i(2_000_001, 1, 1));
1850 let unit = AABB::new(v3i(0, 0, 0), v3i(1, 1, 1));
1851 assert!(swept_aabb(&far, v3i(1, 0, 0), &unit).is_none());
1852
1853 let mover = AABB::new(v3i(0, 0, 0), v3i(1, 1, 1));
1854 let off_z = AABB::new(v3i(2, 0, 5), v3i(3, 1, 6));
1855 assert!(swept_aabb(&mover, v3i(4, 0, 0), &off_z).is_none());
1856 let in_z = AABB::new(v3i(2, 0, 0), v3i(3, 1, 1));
1857 assert_eq!(swept_aabb(&mover, v3i(4, 0, 0), &in_z), Some(r(1, 4)));
1858 }
1859
1860 #[cfg(feature = "std")]
1874 #[test]
1875 fn adaptive_toi_substeps_uses_the_z_dominated_speed_and_the_smaller_radius() {
1876 let n = adaptive_toi_substeps(
1877 Vec3Fix::ZERO,
1878 v3i(1, 2, 8),
1879 r(1, 8),
1880 v3i(0, 0, 2),
1881 Vec3Fix::ZERO,
1882 fi(1),
1883 r(1, 8),
1884 100,
1885 );
1886 assert_eq!(n, 16);
1887 let swapped = adaptive_toi_substeps(
1888 v3i(0, 0, 2),
1889 Vec3Fix::ZERO,
1890 fi(1),
1891 Vec3Fix::ZERO,
1892 v3i(1, 2, 8),
1893 r(1, 8),
1894 r(1, 8),
1895 100,
1896 );
1897 assert_eq!(swapped, 16);
1898 }
1899
1900 fn sweep_world() -> (crate::solver::PhysicsWorld, usize) {
1901 use crate::solver::{PhysicsConfig, PhysicsWorld, RigidBody};
1902 let mut w = PhysicsWorld::new(PhysicsConfig::default());
1903 let a = w.add_body_with_radius(RigidBody::new(Vec3Fix::ZERO, fi(1)), r(1, 2));
1904 (w, a)
1905 }
1906
1907 #[test]
1913 fn world_time_of_impact_is_gap_over_travel() {
1914 use crate::solver::RigidBody;
1915 let (mut w, a) = sweep_world();
1916 w.add_body_with_radius(RigidBody::new_static(v3i(3, 0, 0)), r(1, 4));
1917 let f = crate::shape_raycast::RayFilter::default();
1918 let hit = w.time_of_impact(a, v3i(3, 0, 0), r(3, 2), &f).expect("hit");
1919 let tol = r(1, 1_000_000);
1920 assert!((hit.t - r(1, 2)).abs() < tol, "t = {:?}", hit.t);
1921 assert!(
1922 (hit.point.x - r(11, 4)).abs() < tol,
1923 "x = {:?}",
1924 hit.point.x
1925 );
1926 assert!(hit.point.y.abs() < tol && hit.point.z.abs() < tol);
1927
1928 assert_eq!(w.time_of_impact(a, v3i(-3, 0, 0), r(3, 2), &f), None);
1930 assert_eq!(w.time_of_impact(a, v3i(3, 0, 0), fi(0), &f), None);
1933 assert_eq!(w.time_of_impact(a, v3i(3, 0, 0), fi(-1), &f), None);
1934 assert_eq!(w.time_of_impact(a, Vec3Fix::ZERO, fi(1), &f), None);
1935 assert_eq!(w.time_of_impact(99, v3i(3, 0, 0), fi(1), &f), None);
1936 }
1937
1938 #[test]
1940 fn world_time_of_impact_of_a_body_without_radius_is_none() {
1941 use crate::solver::{PhysicsConfig, PhysicsWorld, RigidBody};
1942 let mut w = PhysicsWorld::new(PhysicsConfig::default());
1943 let bare = w.add_body(RigidBody::new(Vec3Fix::ZERO, fi(1)));
1944 w.add_body_with_radius(RigidBody::new_static(v3i(2, 0, 0)), fi(1));
1945 let f = crate::shape_raycast::RayFilter::default();
1946 assert_eq!(w.time_of_impact(bare, v3i(4, 0, 0), fi(1), &f), None);
1947 }
1948
1949 #[test]
1952 fn world_time_of_impact_touching_at_the_end_of_the_step_is_one() {
1953 use crate::solver::RigidBody;
1954 let (mut w, a) = sweep_world();
1955 w.add_body_with_radius(RigidBody::new_static(v3i(3, 0, 0)), r(1, 2));
1956 let f = crate::shape_raycast::RayFilter::default();
1957 let hit = w.time_of_impact(a, v3i(2, 0, 0), fi(1), &f).expect("hit");
1958 assert!(hit.t <= fi(1));
1959 assert!((fi(1) - hit.t) < r(1, 1_000_000), "t = {:?}", hit.t);
1960 assert_eq!(w.time_of_impact(a, v3i(2, 0, 0), r(1, 2), &f), None);
1962 }
1963
1964 #[test]
1968 fn world_time_of_impact_when_already_overlapping_is_zero_at_the_centre() {
1969 use crate::solver::RigidBody;
1970 let (mut w, a) = sweep_world();
1971 w.add_body_with_radius(RigidBody::new_static(v3i(1, 0, 0)), r(3, 4));
1972 let f = crate::shape_raycast::RayFilter::default();
1973 for vel in [v3i(1, 0, 0), v3i(-1, 0, 0)] {
1974 let hit = w.time_of_impact(a, vel, fi(1), &f).expect("overlap");
1975 assert_eq!(hit.t, Fix128::ZERO);
1976 assert_eq!(hit.point, Vec3Fix::ZERO);
1977 }
1978 }
1979}