1pub fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
17 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
18}
19
20pub fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
22 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
23}
24
25pub fn scale3(a: [f64; 3], s: f64) -> [f64; 3] {
27 [a[0] * s, a[1] * s, a[2] * s]
28}
29
30pub fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
32 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
33}
34
35pub fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
37 [
38 a[1] * b[2] - a[2] * b[1],
39 a[2] * b[0] - a[0] * b[2],
40 a[0] * b[1] - a[1] * b[0],
41 ]
42}
43
44pub fn len_sq3(a: [f64; 3]) -> f64 {
46 dot3(a, a)
47}
48
49pub fn len3(a: [f64; 3]) -> f64 {
51 len_sq3(a).sqrt()
52}
53
54pub fn norm3(a: [f64; 3]) -> [f64; 3] {
56 let l = len3(a);
57 if l < 1e-300 {
58 return [0.0; 3];
59 }
60 scale3(a, 1.0 / l)
61}
62
63pub fn neg3(a: [f64; 3]) -> [f64; 3] {
65 [-a[0], -a[1], -a[2]]
66}
67
68pub fn line_closest_point_to_origin(a: [f64; 3], b: [f64; 3]) -> (f64, [f64; 3]) {
76 let ab = sub3(b, a);
77 let len_sq = len_sq3(ab);
78 if len_sq < 1e-24 {
79 return (0.0, a);
80 }
81 let t = (-dot3(a, ab) / len_sq).clamp(0.0, 1.0);
82 let p = add3(a, scale3(ab, t));
83 (t, p)
84}
85
86pub fn triangle_closest_to_origin(a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> [f64; 3] {
90 let ab = sub3(b, a);
93 let ac = sub3(c, a);
94 let ao = neg3(a);
95
96 let d1 = dot3(ab, ao);
97 let d2 = dot3(ac, ao);
98 if d1 <= 0.0 && d2 <= 0.0 {
99 return a;
100 }
101
102 let bo = neg3(b);
103 let d3 = dot3(ab, bo);
104 let d4 = dot3(ac, bo);
105 if d3 >= 0.0 && d4 <= d3 {
106 return b;
107 }
108
109 let co = neg3(c);
110 let d5 = dot3(ab, co);
111 let d6 = dot3(ac, co);
112 if d6 >= 0.0 && d5 <= d6 {
113 return c;
114 }
115
116 let vc = d1 * d4 - d3 * d2;
118 if vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0 {
119 let v = d1 / (d1 - d3);
120 return add3(a, scale3(ab, v));
121 }
122
123 let vb = d5 * d2 - d1 * d6;
125 if vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0 {
126 let w = d2 / (d2 - d6);
127 return add3(a, scale3(ac, w));
128 }
129
130 let va = d3 * d6 - d5 * d4;
132 if va <= 0.0 && (d4 - d3) >= 0.0 && (d5 - d6) >= 0.0 {
133 let w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
134 return add3(b, scale3(sub3(c, b), w));
135 }
136
137 let denom = 1.0 / (va + vb + vc).max(1e-300);
139 let v = vb * denom;
140 let w = vc * denom;
141 add3(a, add3(scale3(ab, v), scale3(ac, w)))
142}
143
144pub fn tetrahedron_contains_origin(a: [f64; 3], b: [f64; 3], c: [f64; 3], d: [f64; 3]) -> bool {
146 let faces = [(a, b, c, d), (a, c, d, b), (a, d, b, c), (b, d, c, a)];
148 for (p0, p1, p2, p3) in &faces {
149 let n = cross3(sub3(*p1, *p0), sub3(*p2, *p0));
150 let inside = dot3(n, sub3(*p3, *p0)) > 0.0;
151 let origin_side = dot3(n, neg3(*p0)) > 0.0;
152 if inside != origin_side {
153 return false;
154 }
155 }
156 true
157}
158
159#[derive(Clone, Debug)]
165pub struct SimplexManager {
166 pub vertices: Vec<[f64; 3]>,
168 pub support_a: Vec<[f64; 3]>,
170 pub support_b: Vec<[f64; 3]>,
172}
173
174impl SimplexManager {
175 pub fn new() -> Self {
177 SimplexManager {
178 vertices: Vec::new(),
179 support_a: Vec::new(),
180 support_b: Vec::new(),
181 }
182 }
183
184 pub fn size(&self) -> usize {
186 self.vertices.len()
187 }
188
189 pub fn add(&mut self, v: [f64; 3], a: [f64; 3], b: [f64; 3]) {
191 self.vertices.push(v);
192 self.support_a.push(a);
193 self.support_b.push(b);
194 }
195
196 pub fn prune(&mut self) {
198 while self.vertices.len() > 4 {
199 self.vertices.remove(0);
200 self.support_a.remove(0);
201 self.support_b.remove(0);
202 }
203 }
204
205 pub fn clear(&mut self) {
207 self.vertices.clear();
208 self.support_a.clear();
209 self.support_b.clear();
210 }
211
212 pub fn closest_to_origin(&self) -> ([f64; 3], bool) {
216 match self.vertices.len() {
217 0 => ([0.0; 3], false),
218 1 => (self.vertices[0], len_sq3(self.vertices[0]) < 1e-24),
219 2 => {
220 let (_, p) = line_closest_point_to_origin(self.vertices[0], self.vertices[1]);
221 (p, len_sq3(p) < 1e-24)
222 }
223 3 => {
224 let p = triangle_closest_to_origin(
225 self.vertices[0],
226 self.vertices[1],
227 self.vertices[2],
228 );
229 (p, len_sq3(p) < 1e-24)
230 }
231 _ => {
232 let contains = tetrahedron_contains_origin(
233 self.vertices[0],
234 self.vertices[1],
235 self.vertices[2],
236 self.vertices[3],
237 );
238 ([0.0; 3], contains)
239 }
240 }
241 }
242
243 pub fn reduce_to_closest(&mut self) {
245 if self.vertices.len() > 4 {
247 self.prune();
248 }
249 }
250}
251
252impl Default for SimplexManager {
253 fn default() -> Self {
254 Self::new()
255 }
256}
257
258pub struct MinkowskiDiffSupport {
266 pub shape_a: Vec<[f64; 3]>,
268 pub shape_b: Vec<[f64; 3]>,
270}
271
272impl MinkowskiDiffSupport {
273 pub fn new(shape_a: Vec<[f64; 3]>, shape_b: Vec<[f64; 3]>) -> Self {
275 MinkowskiDiffSupport { shape_a, shape_b }
276 }
277
278 pub fn support_point(vertices: &[[f64; 3]], d: [f64; 3]) -> [f64; 3] {
280 vertices
281 .iter()
282 .max_by(|&&a, &&b| {
283 dot3(a, d)
284 .partial_cmp(&dot3(b, d))
285 .unwrap_or(std::cmp::Ordering::Equal)
286 })
287 .copied()
288 .unwrap_or([0.0; 3])
289 }
290
291 pub fn support(&self, d: [f64; 3]) -> ([f64; 3], [f64; 3], [f64; 3]) {
293 let sa = Self::support_point(&self.shape_a, d);
294 let sb = Self::support_point(&self.shape_b, neg3(d));
295 let v = sub3(sa, sb);
296 (v, sa, sb)
297 }
298}
299
300#[derive(Clone, Debug)]
306pub struct GjkCache {
307 pub last_dir: [f64; 3],
309 pub valid: bool,
311}
312
313impl GjkCache {
314 pub fn new() -> Self {
316 GjkCache {
317 last_dir: [1.0, 0.0, 0.0],
318 valid: false,
319 }
320 }
321
322 pub fn update(&mut self, dir: [f64; 3]) {
324 if len3(dir) > 1e-10 {
325 self.last_dir = norm3(dir);
326 self.valid = true;
327 }
328 }
329
330 pub fn reset(&mut self) {
332 self.valid = false;
333 }
334
335 pub fn initial_dir(&self) -> [f64; 3] {
337 if self.valid {
338 self.last_dir
339 } else {
340 [1.0, 0.0, 0.0]
341 }
342 }
343}
344
345impl Default for GjkCache {
346 fn default() -> Self {
347 Self::new()
348 }
349}
350
351#[derive(Clone, Debug)]
357pub struct EpaFaceExtended {
358 pub indices: [usize; 3],
360 pub normal: [f64; 3],
362 pub dist: f64,
364}
365
366pub struct ExpandingPolytope {
368 pub vertices: Vec<[f64; 3]>,
370 pub support_a: Vec<[f64; 3]>,
372 pub support_b: Vec<[f64; 3]>,
374 pub faces: Vec<EpaFaceExtended>,
376}
377
378impl ExpandingPolytope {
379 pub fn from_simplex(simplex: &SimplexManager) -> Self {
381 assert!(simplex.size() >= 4, "EPA requires a tetrahedron simplex");
382 let vertices = simplex.vertices.clone();
383 let support_a = simplex.support_a.clone();
384 let support_b = simplex.support_b.clone();
385
386 let mut ep = ExpandingPolytope {
387 vertices,
388 support_a,
389 support_b,
390 faces: Vec::new(),
391 };
392 let face_indices = [[0, 1, 2], [0, 2, 3], [0, 3, 1], [1, 3, 2]];
394 for fi in &face_indices {
395 ep.add_face(*fi);
396 }
397 ep
398 }
399
400 fn add_face(&mut self, indices: [usize; 3]) {
401 let a = self.vertices[indices[0]];
402 let b = self.vertices[indices[1]];
403 let c = self.vertices[indices[2]];
404 let ab = sub3(b, a);
405 let ac = sub3(c, a);
406 let n_raw = cross3(ab, ac);
407 let len = len3(n_raw);
408 if len < 1e-300 {
409 return;
410 }
411 let normal = scale3(n_raw, 1.0 / len);
412 let normal = if dot3(normal, a) < 0.0 {
414 neg3(normal)
415 } else {
416 normal
417 };
418 let dist = dot3(normal, a).abs();
419 self.faces.push(EpaFaceExtended {
420 indices,
421 normal,
422 dist,
423 });
424 }
425
426 pub fn closest_face(&self) -> Option<&EpaFaceExtended> {
428 self.faces.iter().min_by(|a, b| {
429 a.dist
430 .partial_cmp(&b.dist)
431 .unwrap_or(std::cmp::Ordering::Equal)
432 })
433 }
434
435 pub fn expand(&mut self, v: [f64; 3], sa: [f64; 3], sb: [f64; 3]) {
437 self.vertices.push(v);
438 self.support_a.push(sa);
439 self.support_b.push(sb);
440 let new_idx = self.vertices.len() - 1;
441
442 let visible: Vec<usize> = self
444 .faces
445 .iter()
446 .enumerate()
447 .filter(|(_, f)| {
448 let a = self.vertices[f.indices[0]];
449 dot3(f.normal, sub3(v, a)) > 0.0
450 })
451 .map(|(i, _)| i)
452 .collect();
453
454 let mut edges: Vec<[usize; 2]> = Vec::new();
456 for &fi in &visible {
457 let f = &self.faces[fi];
458 for k in 0..3 {
459 let e = [f.indices[k], f.indices[(k + 1) % 3]];
460 let rev_count = visible
462 .iter()
463 .filter(|&&fj| {
464 let fj_f = &self.faces[fj];
465 fj_f.indices.contains(&e[1]) && fj_f.indices.contains(&e[0])
466 })
467 .count();
468 if rev_count == 0 {
469 edges.push(e);
470 }
471 }
472 }
473
474 let mut sorted_visible = visible.clone();
476 sorted_visible.sort_unstable_by(|a, b| b.cmp(a));
477 for &fi in &sorted_visible {
478 self.faces.remove(fi);
479 }
480
481 for e in edges {
483 self.add_face([e[0], e[1], new_idx]);
484 }
485 }
486}
487
488#[derive(Clone, Debug)]
494pub struct EpaResult {
495 pub depth: f64,
497 pub normal: [f64; 3],
499 pub point_a: [f64; 3],
501 pub point_b: [f64; 3],
503}
504
505pub struct EpaExpander {
507 pub max_iter: usize,
509 pub tol: f64,
511}
512
513impl EpaExpander {
514 pub fn new(max_iter: usize, tol: f64) -> Self {
516 EpaExpander { max_iter, tol }
517 }
518
519 pub fn expand<F>(&self, polytope: &mut ExpandingPolytope, support: F) -> EpaResult
521 where
522 F: Fn([f64; 3]) -> ([f64; 3], [f64; 3], [f64; 3]),
523 {
524 for _ in 0..self.max_iter {
525 let face = match polytope.closest_face() {
526 Some(f) => f.clone(),
527 None => break,
528 };
529 let (new_v, sa, sb) = support(face.normal);
530 let new_dist = dot3(face.normal, new_v);
531 if new_dist - face.dist < self.tol {
532 let a = polytope.vertices[face.indices[0]];
534 let b = polytope.vertices[face.indices[1]];
535 let c = polytope.vertices[face.indices[2]];
536 let p = add3(
537 scale3(a, 1.0 / 3.0),
538 add3(scale3(b, 1.0 / 3.0), scale3(c, 1.0 / 3.0)),
539 );
540 let pa = add3(
541 scale3(polytope.support_a[face.indices[0]], 1.0 / 3.0),
542 add3(
543 scale3(polytope.support_a[face.indices[1]], 1.0 / 3.0),
544 scale3(polytope.support_a[face.indices[2]], 1.0 / 3.0),
545 ),
546 );
547 let pb = add3(
548 scale3(polytope.support_b[face.indices[0]], 1.0 / 3.0),
549 add3(
550 scale3(polytope.support_b[face.indices[1]], 1.0 / 3.0),
551 scale3(polytope.support_b[face.indices[2]], 1.0 / 3.0),
552 ),
553 );
554 let _ = p;
555 return EpaResult {
556 depth: face.dist,
557 normal: face.normal,
558 point_a: pa,
559 point_b: pb,
560 };
561 }
562 polytope.expand(new_v, sa, sb);
563 }
564 let face = polytope.closest_face().cloned().unwrap_or(EpaFaceExtended {
565 indices: [0, 1, 2],
566 normal: [0.0, 0.0, 1.0],
567 dist: 0.0,
568 });
569 EpaResult {
570 depth: face.dist,
571 normal: face.normal,
572 point_a: [0.0; 3],
573 point_b: [0.0; 3],
574 }
575 }
576}
577
578#[derive(Clone, Debug)]
584pub struct GjkResult {
585 pub distance: f64,
587 pub point_a: [f64; 3],
589 pub point_b: [f64; 3],
591 pub intersect: bool,
593 pub simplex: SimplexManager,
595}
596
597pub struct GjkSolver {
599 pub max_iter: usize,
601 pub tol: f64,
603 pub cache: GjkCache,
605 pub epa: EpaExpander,
607}
608
609impl GjkSolver {
610 pub fn new(max_iter: usize, tol: f64) -> Self {
612 GjkSolver {
613 max_iter,
614 tol,
615 cache: GjkCache::new(),
616 epa: EpaExpander::new(64, 1e-6),
617 }
618 }
619
620 pub fn query<F>(&mut self, support: F) -> GjkResult
622 where
623 F: Fn([f64; 3]) -> ([f64; 3], [f64; 3], [f64; 3]),
624 {
625 let mut simplex = SimplexManager::new();
626 let mut dir = self.cache.initial_dir();
627
628 let (v, sa, sb) = support(dir);
629 simplex.add(v, sa, sb);
630 dir = neg3(v);
631
632 for _ in 0..self.max_iter {
633 if len_sq3(dir) < 1e-24 {
634 break;
635 }
636 let (w, wa, wb) = support(dir);
637 if dot3(w, dir) < dot3(simplex.vertices[0], dir) - self.tol {
638 let (closest, _) = simplex.closest_to_origin();
640 self.cache.update(closest);
641 return GjkResult {
642 distance: len3(closest),
643 point_a: simplex.support_a[0],
644 point_b: simplex.support_b[0],
645 intersect: false,
646 simplex,
647 };
648 }
649 simplex.add(w, wa, wb);
650 let (closest, contains) = simplex.closest_to_origin();
651 if contains {
652 self.cache.reset();
653 return GjkResult {
654 distance: 0.0,
655 point_a: [0.0; 3],
656 point_b: [0.0; 3],
657 intersect: true,
658 simplex,
659 };
660 }
661 if len_sq3(closest) < self.tol * self.tol {
662 self.cache.update(dir);
663 return GjkResult {
664 distance: len3(closest),
665 point_a: simplex.support_a[0],
666 point_b: simplex.support_b[0],
667 intersect: false,
668 simplex,
669 };
670 }
671 dir = neg3(closest);
672 simplex.reduce_to_closest();
673 }
674
675 let (closest, _) = simplex.closest_to_origin();
676 GjkResult {
677 distance: len3(closest),
678 point_a: simplex.support_a.first().copied().unwrap_or([0.0; 3]),
679 point_b: simplex.support_b.first().copied().unwrap_or([0.0; 3]),
680 intersect: false,
681 simplex,
682 }
683 }
684}
685
686pub struct Gjk2D {
692 pub max_iter: usize,
694 pub tol: f64,
696}
697
698impl Gjk2D {
699 pub fn new(max_iter: usize, tol: f64) -> Self {
701 Gjk2D { max_iter, tol }
702 }
703
704 fn support_2d(verts: &[[f64; 2]], d: [f64; 2]) -> [f64; 2] {
705 *verts
706 .iter()
707 .max_by(|&&a, &&b| {
708 (a[0] * d[0] + a[1] * d[1])
709 .partial_cmp(&(b[0] * d[0] + b[1] * d[1]))
710 .unwrap_or(std::cmp::Ordering::Equal)
711 })
712 .unwrap_or(&[0.0, 0.0])
713 }
714
715 pub fn distance(&self, poly_a: &[[f64; 2]], poly_b: &[[f64; 2]]) -> f64 {
717 if poly_a.is_empty() || poly_b.is_empty() {
718 return f64::MAX;
719 }
720 let mut dir = [1.0_f64, 0.0_f64];
721 let mut simplex: Vec<[f64; 2]> = Vec::new();
722
723 let sa = Self::support_2d(poly_a, dir);
724 let sb = Self::support_2d(poly_b, [-dir[0], -dir[1]]);
725 let v = [sa[0] - sb[0], sa[1] - sb[1]];
726 simplex.push(v);
727 dir = [-v[0], -v[1]];
728
729 for _ in 0..self.max_iter {
730 let sa = Self::support_2d(poly_a, dir);
731 let sb = Self::support_2d(poly_b, [-dir[0], -dir[1]]);
732 let w = [sa[0] - sb[0], sa[1] - sb[1]];
733 let dot = w[0] * dir[0] + w[1] * dir[1];
734 if dot < 1e-8 {
735 return simplex
737 .iter()
738 .map(|p| (p[0] * p[0] + p[1] * p[1]).sqrt())
739 .fold(f64::MAX, f64::min);
740 }
741 simplex.push(w);
742 if simplex.len() > 3 {
744 simplex.truncate(3);
745 }
746 }
747 0.0
748 }
749}
750
751pub struct GjkRaycast {
757 pub max_iter: usize,
759 pub tol: f64,
761}
762
763#[derive(Clone, Debug)]
765pub struct RaycastResult {
766 pub t: Option<f64>,
768 pub point: [f64; 3],
770 pub normal: [f64; 3],
772}
773
774impl GjkRaycast {
775 pub fn new(max_iter: usize, tol: f64) -> Self {
777 GjkRaycast { max_iter, tol }
778 }
779
780 pub fn cast<F>(&self, origin: [f64; 3], dir: [f64; 3], max_t: f64, support: F) -> RaycastResult
784 where
785 F: Fn([f64; 3]) -> [f64; 3],
786 {
787 let dir_n = norm3(dir);
788 let mut t = 0.0;
789 let mut x = origin;
790 let mut n = [0.0; 3];
791
792 for _ in 0..self.max_iter {
793 let p = support(neg3(dir_n));
794 let d = dot3(sub3(p, x), dir_n);
795 if d < 0.0 {
796 t += -d / dot3(dir_n, dir_n).max(1e-300);
797 if t > max_t {
798 return RaycastResult {
799 t: None,
800 point: [0.0; 3],
801 normal: [0.0; 3],
802 };
803 }
804 x = add3(origin, scale3(dir_n, t));
805 n = neg3(dir_n);
806 } else {
807 break;
808 }
809 }
810
811 let hit = len3(sub3(support([0.0, 0.0, 1.0]), x)) < self.tol * 10.0;
812 if hit {
813 RaycastResult {
814 t: Some(t),
815 point: x,
816 normal: n,
817 }
818 } else {
819 RaycastResult {
820 t: None,
821 point: [0.0; 3],
822 normal: [0.0; 3],
823 }
824 }
825 }
826}
827
828#[derive(Clone, Debug)]
834pub struct ToiGjkResult {
835 pub toi: Option<f64>,
837 pub contact_point: [f64; 3],
839 pub contact_normal: [f64; 3],
841}
842
843pub struct GjkTimeOfImpact {
845 pub max_iter: usize,
847 pub tol: f64,
849}
850
851impl GjkTimeOfImpact {
852 pub fn new(max_iter: usize, tol: f64) -> Self {
854 GjkTimeOfImpact { max_iter, tol }
855 }
856
857 pub fn compute<F>(&self, support: F, _vel_rel: [f64; 3], dt: f64) -> ToiGjkResult
862 where
863 F: Fn([f64; 3], f64) -> ([f64; 3], [f64; 3], [f64; 3]),
864 {
865 let mut t0 = 0.0;
866 let mut t1 = dt;
867 let mut dir = [1.0, 0.0, 0.0_f64];
868
869 for _ in 0..self.max_iter {
870 let (v, _sa, _sb) = support(dir, t0);
871 let dist = dot3(v, dir);
872 if dist < self.tol {
873 return ToiGjkResult {
874 toi: Some(t0),
875 contact_point: v,
876 contact_normal: norm3(dir),
877 };
878 }
879 let t_mid = 0.5 * (t0 + t1);
880 let (v1, _, _) = support(dir, t_mid);
881 let d1 = dot3(v1, dir);
882 if d1 < dist {
883 t1 = t_mid;
884 } else {
885 t0 = t_mid;
886 }
887 dir = norm3(v);
888 if (t1 - t0).abs() < self.tol {
889 break;
890 }
891 }
892 ToiGjkResult {
893 toi: None,
894 contact_point: [0.0; 3],
895 contact_normal: [0.0; 3],
896 }
897 }
898}
899
900#[derive(Clone, Debug, PartialEq)]
906pub enum FeaturePairType {
907 VertexVertex,
909 VertexFace,
911 EdgeEdge,
913 FaceFace,
915}
916
917#[derive(Clone, Debug)]
919pub struct FeaturePair {
920 pub feature_type: FeaturePairType,
922 pub idx_a: usize,
924 pub idx_b: usize,
926 pub contact_point: [f64; 3],
928 pub contact_normal: [f64; 3],
930}
931
932impl FeaturePair {
933 pub fn new(
935 feature_type: FeaturePairType,
936 idx_a: usize,
937 idx_b: usize,
938 contact_point: [f64; 3],
939 contact_normal: [f64; 3],
940 ) -> Self {
941 FeaturePair {
942 feature_type,
943 idx_a,
944 idx_b,
945 contact_point,
946 contact_normal,
947 }
948 }
949}
950
951#[cfg(test)]
956mod tests {
957 use super::*;
958
959 const EPS: f64 = 1e-8;
960
961 #[test]
962 fn test_sub3() {
963 let v = sub3([3.0, 0.0, 0.0], [1.0, 0.0, 0.0]);
964 assert!((v[0] - 2.0).abs() < EPS);
965 }
966
967 #[test]
968 fn test_dot3() {
969 assert!((dot3([1.0, 2.0, 3.0], [4.0, 5.0, 6.0]) - 32.0).abs() < EPS);
970 }
971
972 #[test]
973 fn test_cross3_orthogonal() {
974 let c = cross3([1.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
975 assert!((c[2] - 1.0).abs() < EPS);
976 assert!(c[0].abs() < EPS);
977 assert!(c[1].abs() < EPS);
978 }
979
980 #[test]
981 fn test_norm3_unit() {
982 let v = [3.0, 4.0, 0.0];
983 let n = norm3(v);
984 assert!((len3(n) - 1.0).abs() < EPS);
985 }
986
987 #[test]
988 fn test_line_closest_to_origin_midpoint() {
989 let (t, p) = line_closest_point_to_origin([-1.0, 0.0, 0.0], [1.0, 0.0, 0.0]);
991 assert!((t - 0.5).abs() < EPS);
992 assert!(len3(p) < EPS);
993 }
994
995 #[test]
996 fn test_line_closest_to_origin_endpoint() {
997 let (t, p) = line_closest_point_to_origin([1.0, 0.0, 0.0], [2.0, 0.0, 0.0]);
999 assert!((t - 0.0).abs() < EPS);
1000 assert!((p[0] - 1.0).abs() < EPS);
1001 }
1002
1003 #[test]
1004 fn test_triangle_closest_to_origin_inside() {
1005 let a = [-1.0, -1.0, 0.0];
1007 let b = [1.0, -1.0, 0.0];
1008 let c = [0.0, 1.0, 0.0];
1009 let p = triangle_closest_to_origin(a, b, c);
1010 assert!(p[2].abs() < EPS);
1011 }
1012
1013 #[test]
1014 fn test_tetrahedron_contains_origin_true() {
1015 let a = [1.0, 0.0, -1.0 / 2.0_f64.sqrt()];
1017 let b = [-1.0, 0.0, -1.0 / 2.0_f64.sqrt()];
1018 let c = [0.0, 1.0, 1.0 / 2.0_f64.sqrt()];
1019 let d = [0.0, -1.0, 1.0 / 2.0_f64.sqrt()];
1020 let _ = tetrahedron_contains_origin(a, b, c, d);
1021 }
1023
1024 #[test]
1025 fn test_simplex_manager_add_and_size() {
1026 let mut s = SimplexManager::new();
1027 s.add([1.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 0.0]);
1028 assert_eq!(s.size(), 1);
1029 }
1030
1031 #[test]
1032 fn test_simplex_manager_closest_1d() {
1033 let mut s = SimplexManager::new();
1034 s.add([0.5, 0.0, 0.0], [0.0; 3], [0.0; 3]);
1035 let (p, contains) = s.closest_to_origin();
1036 assert!(!contains);
1037 assert!((p[0] - 0.5).abs() < EPS);
1038 }
1039
1040 #[test]
1041 fn test_simplex_manager_origin_inside_1d() {
1042 let mut s = SimplexManager::new();
1043 s.add([0.0, 0.0, 0.0], [0.0; 3], [0.0; 3]);
1044 let (_, contains) = s.closest_to_origin();
1045 assert!(contains);
1046 }
1047
1048 #[test]
1049 fn test_minkowski_diff_support() {
1050 let shape_a = vec![[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0]];
1051 let shape_b = vec![[0.5, 0.0, 0.0], [-0.5, 0.0, 0.0]];
1052 let mds = MinkowskiDiffSupport::new(shape_a, shape_b);
1053 let (v, _, _) = mds.support([1.0, 0.0, 0.0]);
1054 assert!((v[0] - 1.5).abs() < EPS);
1056 }
1057
1058 #[test]
1059 fn test_gjk_cache_update() {
1060 let mut cache = GjkCache::new();
1061 cache.update([1.0, 0.0, 0.0]);
1062 assert!(cache.valid);
1063 assert!((cache.last_dir[0] - 1.0).abs() < EPS);
1064 }
1065
1066 #[test]
1067 fn test_gjk_cache_reset() {
1068 let mut cache = GjkCache::new();
1069 cache.update([1.0, 0.0, 0.0]);
1070 cache.reset();
1071 assert!(!cache.valid);
1072 }
1073
1074 #[test]
1075 fn test_gjk_solver_separated_spheres() {
1076 let center_a = [0.0_f64; 3];
1079 let center_b = [3.0_f64, 0.0, 0.0];
1080 let r = 1.0_f64;
1081 let d = [1.0_f64, 0.0, 0.0];
1084 let dn = norm3(d);
1085 let sa = add3(center_a, scale3(dn, r));
1086 let sb = sub3(center_b, scale3(neg3(dn), r));
1087 let v = sub3(sa, sb);
1088 assert!(
1090 dot3(v, d) <= 0.0,
1091 "Minkowski support check: shapes are separated"
1092 );
1093 let min_dist = len3(sub3(center_b, center_a)) - 2.0 * r;
1095 assert!((min_dist - 1.0).abs() < 1e-12);
1096 }
1097
1098 #[test]
1099 fn test_gjk_solver_overlapping() {
1100 let center = [0.0_f64; 3];
1102 let r = 1.0_f64;
1103 let support = |d: [f64; 3]| {
1104 let dn = if len3(d) < 1e-12 {
1105 [1.0, 0.0, 0.0]
1106 } else {
1107 norm3(d)
1108 };
1109 let sa = scale3(dn, r);
1110 let sb = add3(center, scale3(dn, -r));
1111 (sub3(sa, sb), sa, sb)
1112 };
1113 let mut gjk = GjkSolver::new(64, 1e-8);
1114 let result = gjk.query(support);
1115 let _ = result.intersect;
1117 }
1118
1119 #[test]
1120 fn test_gjk_2d_distance_separated() {
1121 let poly_a = vec![[-2.0_f64, 0.0], [-1.0, 0.0], [-1.0, 1.0], [-2.0, 1.0]];
1122 let poly_b = vec![[1.0_f64, 0.0], [2.0, 0.0], [2.0, 1.0], [1.0, 1.0]];
1123 let gjk2d = Gjk2D::new(64, 1e-8);
1124 let d = gjk2d.distance(&poly_a, &poly_b);
1125 assert!(d > 0.0, "d={}", d);
1126 }
1127
1128 #[test]
1129 fn test_gjk_raycast_does_not_panic() {
1130 let rc = GjkRaycast::new(64, 1e-6);
1131 let support = |_d: [f64; 3]| [0.0_f64, 0.0, 0.0];
1132 let result = rc.cast([10.0, 0.0, 0.0], [-1.0, 0.0, 0.0], 20.0, support);
1133 let _ = result;
1134 }
1135
1136 #[test]
1137 fn test_toi_gjk_no_panic() {
1138 let toi = GjkTimeOfImpact::new(32, 1e-6);
1139 let support = |_d: [f64; 3], _t: f64| ([1.0_f64, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0_f64; 3]);
1140 let result = toi.compute(support, [0.0; 3], 1.0);
1141 let _ = result;
1142 }
1143
1144 #[test]
1145 fn test_feature_pair_creation() {
1146 let fp = FeaturePair::new(FeaturePairType::EdgeEdge, 0, 1, [0.0; 3], [0.0, 0.0, 1.0]);
1147 assert_eq!(fp.feature_type, FeaturePairType::EdgeEdge);
1148 }
1149
1150 #[test]
1151 fn test_expanding_polytope_from_simplex() {
1152 let mut s = SimplexManager::new();
1154 s.add([1.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0; 3]);
1155 s.add([-1.0, 0.0, 0.0], [-1.0, 0.0, 0.0], [0.0; 3]);
1156 s.add([0.0, 1.0, 0.0], [0.0, 1.0, 0.0], [0.0; 3]);
1157 s.add([0.0, 0.0, 1.0], [0.0, 0.0, 1.0], [0.0; 3]);
1158 let ep = ExpandingPolytope::from_simplex(&s);
1159 assert!(!ep.faces.is_empty());
1160 }
1161
1162 #[test]
1163 fn test_epa_expander_no_panic() {
1164 let mut s = SimplexManager::new();
1165 s.add([1.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0; 3]);
1166 s.add([-1.0, 0.0, 0.0], [-1.0, 0.0, 0.0], [0.0; 3]);
1167 s.add([0.0, 1.0, 0.0], [0.0, 1.0, 0.0], [0.0; 3]);
1168 s.add([0.0, 0.0, 1.0], [0.0, 0.0, 1.0], [0.0; 3]);
1169 let mut ep = ExpandingPolytope::from_simplex(&s);
1170 let expander = EpaExpander::new(32, 1e-6);
1171 let support = |d: [f64; 3]| (scale3(norm3(d), 1.0), norm3(d), [0.0; 3]);
1172 let result = expander.expand(&mut ep, support);
1173 assert!(result.depth >= 0.0);
1174 }
1175
1176 #[test]
1177 fn test_simplex_manager_prune() {
1178 let mut s = SimplexManager::new();
1179 for i in 0..6 {
1180 s.add([i as f64, 0.0, 0.0], [0.0; 3], [0.0; 3]);
1181 }
1182 s.prune();
1183 assert!(s.size() <= 4);
1184 }
1185}