1#![allow(clippy::needless_range_loop, clippy::should_implement_trait)]
2#![allow(dead_code)]
17
18#[allow(unused_imports)]
19use std::f64::consts::PI;
20
21pub type Vec3 = [f64; 3];
25
26#[inline]
28pub fn dot(a: Vec3, b: Vec3) -> f64 {
29 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
30}
31
32#[inline]
34pub fn cross(a: Vec3, b: Vec3) -> Vec3 {
35 [
36 a[1] * b[2] - a[2] * b[1],
37 a[2] * b[0] - a[0] * b[2],
38 a[0] * b[1] - a[1] * b[0],
39 ]
40}
41
42#[inline]
44pub fn sub(a: Vec3, b: Vec3) -> Vec3 {
45 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
46}
47
48#[inline]
50pub fn add(a: Vec3, b: Vec3) -> Vec3 {
51 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
52}
53
54#[inline]
56pub fn scale(v: Vec3, s: f64) -> Vec3 {
57 [v[0] * s, v[1] * s, v[2] * s]
58}
59
60#[inline]
62pub fn norm(v: Vec3) -> f64 {
63 dot(v, v).sqrt()
64}
65
66#[inline]
68pub fn normalize(v: Vec3) -> Vec3 {
69 let n = norm(v);
70 if n < 1e-15 {
71 [0.0; 3]
72 } else {
73 scale(v, 1.0 / n)
74 }
75}
76
77#[inline]
79pub fn neg(v: Vec3) -> Vec3 {
80 [-v[0], -v[1], -v[2]]
81}
82
83#[derive(Debug, Clone)]
87pub struct HalfEdge {
88 pub origin: usize,
90 pub twin: usize,
92 pub next: usize,
94 pub face: usize,
96}
97
98#[derive(Debug, Clone)]
100pub struct HullFace {
101 pub edge: usize,
103 pub normal: Vec3,
105}
106
107#[derive(Debug, Clone)]
112pub struct ConvexHull3d {
113 pub vertices: Vec<Vec3>,
115 pub half_edges: Vec<HalfEdge>,
117 pub faces: Vec<HullFace>,
119 pub triangles: Vec<[usize; 3]>,
121}
122
123impl ConvexHull3d {
124 pub fn build(points: &[Vec3]) -> Self {
128 assert!(points.len() >= 4, "need at least 4 non-coplanar points");
129 let tris = incremental_hull(points);
131 let vertices = points.to_vec();
132 let (half_edges, faces) = build_half_edge_structure(&tris, &vertices);
134 Self {
135 vertices,
136 half_edges,
137 faces,
138 triangles: tris,
139 }
140 }
141
142 pub fn all_points_inside_or_on(&self, points: &[Vec3], tol: f64) -> bool {
144 points.iter().all(|&p| {
145 self.faces.iter().enumerate().all(|(fi, f)| {
146 let tri = &self.triangles[fi];
147 let v0 = self.vertices[tri[0]];
148 let signed_dist = dot(f.normal, sub(p, v0));
149 signed_dist <= tol
150 })
151 })
152 }
153
154 pub fn volume(&self) -> f64 {
156 let mut vol = 0.0_f64;
157 for tri in &self.triangles {
158 let a = self.vertices[tri[0]];
159 let b = self.vertices[tri[1]];
160 let c = self.vertices[tri[2]];
161 vol += dot(a, cross(b, c));
163 }
164 (vol / 6.0).abs()
165 }
166
167 pub fn centroid(&self) -> Vec3 {
169 let n = self.vertices.len() as f64;
170 let s = self.vertices.iter().fold([0.0; 3], |acc, &v| add(acc, v));
171 scale(s, 1.0 / n)
172 }
173
174 pub fn support(&self, direction: Vec3) -> f64 {
176 self.vertices
177 .iter()
178 .map(|&v| dot(v, direction))
179 .fold(f64::NEG_INFINITY, f64::max)
180 }
181
182 pub fn support_point(&self, direction: Vec3) -> Vec3 {
184 *self
185 .vertices
186 .iter()
187 .max_by(|&&a, &&b| {
188 dot(a, direction)
189 .partial_cmp(&dot(b, direction))
190 .unwrap_or(std::cmp::Ordering::Equal)
191 })
192 .expect("operation should succeed")
193 }
194}
195
196fn incremental_hull(pts: &[Vec3]) -> Vec<[usize; 3]> {
198 let mut tris: Vec<[usize; 3]> = vec![[0, 1, 2], [0, 3, 1], [0, 2, 3], [1, 3, 2]];
200 let centroid = {
202 let s = pts[0..4].iter().fold([0.0; 3], |acc, &v| add(acc, v));
203 scale(s, 0.25)
204 };
205 for tri in &mut tris {
206 orient_outward(pts, tri, centroid);
207 }
208
209 for i in 4..pts.len() {
211 let p = pts[i];
212 let visible: Vec<bool> = tris
214 .iter()
215 .map(|tri| {
216 let n = face_normal(pts, tri);
217 let v0 = pts[tri[0]];
218 dot(n, sub(p, v0)) > 1e-12
219 })
220 .collect();
221
222 let mut horizon: Vec<(usize, usize)> = Vec::new();
224 for (fi, &vis) in visible.iter().enumerate() {
225 if vis {
226 let tri = tris[fi];
227 for k in 0..3 {
228 let a = tri[k];
229 let b = tri[(k + 1) % 3];
230 let twin_visible = tris.iter().enumerate().any(|(fj, &t)| {
232 fj != fi && t.contains(&b) && t.contains(&a) && visible[fj]
233 });
234 if !twin_visible {
235 horizon.push((a, b));
236 }
237 }
238 }
239 }
240 let mut new_tris: Vec<[usize; 3]> = tris
242 .iter()
243 .zip(visible.iter())
244 .filter(|&(_, v)| !v)
245 .map(|(&t, _)| t)
246 .collect();
247 for (a, b) in horizon {
249 let mut tri = [a, b, i];
250 orient_outward(pts, &mut tri, centroid);
251 new_tris.push(tri);
252 }
253 if !new_tris.is_empty() {
254 tris = new_tris;
255 }
256 }
257 tris
258}
259
260fn face_normal(pts: &[Vec3], tri: &[usize; 3]) -> Vec3 {
262 let a = pts[tri[0]];
263 let b = pts[tri[1]];
264 let c = pts[tri[2]];
265 normalize(cross(sub(b, a), sub(c, a)))
266}
267
268fn orient_outward(pts: &[Vec3], tri: &mut [usize; 3], centroid: Vec3) {
270 let n = face_normal(pts, tri);
271 let v0 = pts[tri[0]];
272 if dot(n, sub(v0, centroid)) < 0.0 {
273 tri.swap(1, 2);
274 }
275}
276
277fn build_half_edge_structure(
279 tris: &[[usize; 3]],
280 verts: &[Vec3],
281) -> (Vec<HalfEdge>, Vec<HullFace>) {
282 let mut half_edges: Vec<HalfEdge> = Vec::new();
283 let mut faces: Vec<HullFace> = Vec::new();
284
285 for (fi, tri) in tris.iter().enumerate() {
286 let base = half_edges.len();
287 for k in 0..3_usize {
289 half_edges.push(HalfEdge {
290 origin: tri[k],
291 twin: usize::MAX, next: base + (k + 1) % 3,
293 face: fi,
294 });
295 }
296 let a = verts[tri[0]];
298 let b = verts[tri[1]];
299 let c = verts[tri[2]];
300 let normal = normalize(cross(sub(b, a), sub(c, a)));
301 faces.push(HullFace { edge: base, normal });
302 }
303 let n = half_edges.len();
305 for i in 0..n {
306 if half_edges[i].twin != usize::MAX {
307 continue;
308 }
309 let org = half_edges[i].origin;
310 let to = half_edges[half_edges[i].next].origin;
312 for j in (i + 1)..n {
313 let org_j = half_edges[j].origin;
314 let to_j = half_edges[half_edges[j].next].origin;
315 if org_j == to && to_j == org {
316 half_edges[i].twin = j;
317 half_edges[j].twin = i;
318 break;
319 }
320 }
321 }
322 (half_edges, faces)
323}
324
325#[derive(Debug, Clone)]
329pub struct AcdConfig {
330 pub max_concavity: f64,
332 pub max_parts: usize,
334 pub min_volume_fraction: f64,
336}
337
338impl AcdConfig {
339 pub fn default() -> Self {
341 Self {
342 max_concavity: 0.01,
343 max_parts: 16,
344 min_volume_fraction: 0.001,
345 }
346 }
347}
348
349#[derive(Debug, Clone)]
351pub struct ConvexPart {
352 pub vertices: Vec<Vec3>,
354 pub concavity: f64,
356 pub indices: Vec<[u32; 3]>,
358 pub volume: f64,
360 pub centroid: Vec3,
362}
363
364impl ConvexPart {
365 pub fn new(vertices: Vec<Vec3>) -> Self {
367 let concavity = 0.0;
368 Self {
369 vertices,
370 concavity,
371 indices: Vec::new(),
372 volume: 0.0,
373 centroid: [0.0; 3],
374 }
375 }
376
377 pub fn is_convex(&self, tol: f64) -> bool {
379 if self.vertices.len() < 4 {
380 return true;
381 }
382 let hull = ConvexHull3d::build(&self.vertices);
383 hull.all_points_inside_or_on(&self.vertices, tol)
384 }
385}
386
387#[derive(Debug, Clone)]
389pub struct ConvexParts {
390 pub parts: Vec<ConvexPart>,
392}
393
394impl ConvexParts {
395 pub fn len(&self) -> usize {
397 self.parts.len()
398 }
399
400 pub fn is_empty(&self) -> bool {
402 self.parts.is_empty()
403 }
404}
405
406#[derive(Debug, Clone)]
408pub struct ApproxConvexDecomp {
409 pub config: AcdConfig,
411}
412
413impl Default for ApproxConvexDecomp {
414 fn default() -> Self {
415 Self::new()
416 }
417}
418
419impl ApproxConvexDecomp {
420 pub fn new() -> Self {
422 Self {
423 config: AcdConfig::default(),
424 }
425 }
426
427 pub fn with_config(config: AcdConfig) -> Self {
429 Self { config }
430 }
431
432 pub fn decompose(&self, vertices: &[Vec3]) -> ConvexParts {
438 let mut pending: Vec<Vec<Vec3>> = vec![vertices.to_vec()];
439 let mut results: Vec<ConvexPart> = Vec::new();
440
441 while !pending.is_empty() && results.len() < self.config.max_parts {
442 let cluster = pending.pop().expect("collection should not be empty");
443 if cluster.len() < 4 {
444 results.push(ConvexPart::new(cluster));
445 continue;
446 }
447 let hull = ConvexHull3d::build(&cluster);
448 let concavity = self.max_concavity_of(&cluster, &hull);
449 if concavity <= self.config.max_concavity
450 || results.len() + pending.len() + 1 >= self.config.max_parts
451 {
452 let mut part = ConvexPart::new(cluster);
453 part.concavity = concavity;
454 results.push(part);
455 } else {
456 let (left, right) = self.split_at_max_concavity(&cluster, &hull);
458 let small_left = left.len() < 4;
459 let small_right = right.len() < 4;
460 if !small_left {
461 pending.push(left);
462 }
463 if !small_right {
464 pending.push(right);
465 }
466 if small_left || small_right {
467 let mut part = ConvexPart::new(cluster);
468 part.concavity = concavity;
469 results.push(part);
470 }
471 }
472 }
473 for cluster in pending {
475 results.push(ConvexPart::new(cluster));
476 }
477 ConvexParts { parts: results }
478 }
479
480 fn max_concavity_of(&self, pts: &[Vec3], hull: &ConvexHull3d) -> f64 {
482 pts.iter()
483 .map(|&p| self.signed_distance_to_hull(p, hull))
484 .fold(0.0_f64, f64::max)
485 }
486
487 fn signed_distance_to_hull(&self, p: Vec3, hull: &ConvexHull3d) -> f64 {
489 hull.faces
490 .iter()
491 .enumerate()
492 .map(|(fi, f)| {
493 let v0 = hull.vertices[hull.triangles[fi][0]];
494 dot(f.normal, sub(p, v0))
495 })
496 .fold(f64::NEG_INFINITY, f64::max)
497 }
498
499 fn split_at_max_concavity(&self, pts: &[Vec3], hull: &ConvexHull3d) -> (Vec<Vec3>, Vec<Vec3>) {
501 let (_, split_pt) = pts
503 .iter()
504 .map(|&p| (self.signed_distance_to_hull(p, hull), p))
505 .fold((f64::NEG_INFINITY, pts[0]), |best, curr| {
506 if curr.0 > best.0 { curr } else { best }
507 });
508
509 let (axis, median) = principal_axis_median(pts);
511 let mut split_used = false;
512 let left: Vec<Vec3> = pts
513 .iter()
514 .filter(|&&p| {
515 if p[axis] < median {
516 true
517 } else if p == split_pt && !split_used {
518 split_used = true;
519 true
520 } else {
521 false
522 }
523 })
524 .copied()
525 .collect();
526 let right: Vec<Vec3> = pts
527 .iter()
528 .filter(|&&p| p[axis] >= median && p != split_pt)
529 .copied()
530 .collect();
531 (left, right)
532 }
533}
534
535fn principal_axis_median(pts: &[Vec3]) -> (usize, f64) {
537 let n = pts.len() as f64;
538 let mean: Vec3 = {
539 let s = pts.iter().fold([0.0; 3], |acc, &v| add(acc, v));
540 scale(s, 1.0 / n)
541 };
542 let var: [f64; 3] = {
543 let mut v = [0.0; 3];
544 for &p in pts {
545 for k in 0..3 {
546 v[k] += (p[k] - mean[k]).powi(2);
547 }
548 }
549 v
550 };
551 let axis = if var[0] >= var[1] && var[0] >= var[2] {
552 0
553 } else if var[1] >= var[2] {
554 1
555 } else {
556 2
557 };
558 let median = mean[axis];
559 (axis, median)
560}
561
562#[derive(Debug, Clone)]
566pub struct SupportFunction {
567 pub vertices: Vec<Vec3>,
569}
570
571impl SupportFunction {
572 pub fn new(vertices: Vec<Vec3>) -> Self {
574 Self { vertices }
575 }
576
577 pub fn support(&self, direction: Vec3) -> f64 {
579 self.vertices
580 .iter()
581 .map(|&v| dot(v, direction))
582 .fold(f64::NEG_INFINITY, f64::max)
583 }
584
585 pub fn support_point(&self, direction: Vec3) -> Vec3 {
587 *self
588 .vertices
589 .iter()
590 .max_by(|&&a, &&b| {
591 dot(a, direction)
592 .partial_cmp(&dot(b, direction))
593 .unwrap_or(std::cmp::Ordering::Equal)
594 })
595 .expect("operation should succeed")
596 }
597}
598
599#[derive(Debug, Clone)]
605pub struct MinkowskiSum {
606 pub shape_a: SupportFunction,
608 pub shape_b: SupportFunction,
610}
611
612impl MinkowskiSum {
613 pub fn new(shape_a: SupportFunction, shape_b: SupportFunction) -> Self {
615 Self { shape_a, shape_b }
616 }
617
618 pub fn support(&self, direction: Vec3) -> f64 {
620 self.shape_a.support(direction) + self.shape_b.support(direction)
621 }
622
623 pub fn support_point(&self, direction: Vec3) -> Vec3 {
625 add(
626 self.shape_a.support_point(direction),
627 self.shape_b.support_point(direction),
628 )
629 }
630
631 pub fn contains_point(&self, p: Vec3, tol: f64) -> bool {
633 let test_dirs: &[Vec3] = &[
635 [1.0, 0.0, 0.0],
636 [-1.0, 0.0, 0.0],
637 [0.0, 1.0, 0.0],
638 [0.0, -1.0, 0.0],
639 [0.0, 0.0, 1.0],
640 [0.0, 0.0, -1.0],
641 [1.0, 1.0, 0.0],
642 [-1.0, 1.0, 0.0],
643 [1.0, -1.0, 0.0],
644 [0.0, 1.0, 1.0],
645 ];
646 for &d in test_dirs {
647 let d_n = normalize(d);
648 if dot(p, d_n) > self.support(d_n) + tol {
649 return false;
650 }
651 }
652 true
653 }
654}
655
656#[derive(Debug, Clone)]
660pub struct GjkDistance {
661 pub max_iter: usize,
663 pub tolerance: f64,
665}
666
667impl GjkDistance {
668 pub fn new(max_iter: usize, tolerance: f64) -> Self {
670 Self {
671 max_iter,
672 tolerance,
673 }
674 }
675
676 pub fn default() -> Self {
678 Self::new(64, 1e-10)
679 }
680
681 pub fn distance(&self, a: &SupportFunction, b: &SupportFunction) -> f64 {
685 let cso_support = |d: Vec3| -> Vec3 { sub(a.support_point(d), b.support_point(neg(d))) };
687
688 let mut dir = [1.0, 0.0, 0.0_f64];
689 let mut simplex: Vec<Vec3> = Vec::with_capacity(4);
690 let first = cso_support(dir);
691 simplex.push(first);
692 dir = neg(first);
693
694 for _ in 0..self.max_iter {
695 if norm(dir) < self.tolerance {
696 return 0.0;
697 }
698 let a_pt = cso_support(normalize(dir));
699 if dot(a_pt, normalize(dir)) < dot(simplex[0], normalize(dir)) - self.tolerance {
700 break;
702 }
703 simplex.push(a_pt);
704 let (new_simplex, new_dir, contains_origin) = nearest_simplex(simplex.clone());
705 simplex = new_simplex;
706 if contains_origin {
707 return 0.0;
708 }
709 dir = new_dir;
710 }
711 norm(dir)
712 }
713}
714
715fn nearest_simplex(simplex: Vec<Vec3>) -> (Vec<Vec3>, Vec3, bool) {
717 match simplex.len() {
718 1 => {
719 let a = simplex[0];
720 (simplex, neg(a), false)
721 }
722 2 => {
723 let (b, a) = (simplex[0], simplex[1]);
724 let ab = sub(b, a);
725 let ao = neg(a);
726 if dot(ab, ao) > 0.0 {
727 let dir = sub(ab, scale(a, dot(ab, ao) / dot(ab, ab)));
728 (vec![b, a], dir, false)
729 } else {
730 (vec![a], ao, false)
731 }
732 }
733 3 => {
734 let (c, b, a) = (simplex[0], simplex[1], simplex[2]);
735 let ab = sub(b, a);
736 let ac = sub(c, a);
737 let ao = neg(a);
738 let abc = cross(ab, ac);
739 if dot(cross(abc, ac), ao) > 0.0 {
740 (
741 vec![c, a],
742 sub(ac, scale(a, dot(ac, ao) / dot(ac, ac))),
743 false,
744 )
745 } else if dot(cross(ab, abc), ao) > 0.0 {
746 (
747 vec![b, a],
748 sub(ab, scale(a, dot(ab, ao) / dot(ab, ab))),
749 false,
750 )
751 } else if dot(abc, ao) > 0.0 {
752 (vec![c, b, a], abc, false)
753 } else {
754 (vec![b, c, a], neg(abc), false)
755 }
756 }
757 _ => {
758 let origin = [0.0; 3];
760 let centroid = scale(simplex.iter().fold([0.0; 3], |acc, &v| add(acc, v)), 0.25);
761 let dir = sub(origin, centroid);
762 if norm(dir) < 1e-12 {
763 return (simplex, [0.0; 3], true);
764 }
765 (simplex, dir, false)
766 }
767 }
768}
769
770#[derive(Debug, Clone)]
774pub struct ConvexContainment {
775 pub hull: ConvexHull3d,
777}
778
779impl ConvexContainment {
780 pub fn new(hull: ConvexHull3d) -> Self {
782 Self { hull }
783 }
784
785 pub fn contains_point(&self, p: Vec3, tol: f64) -> bool {
787 self.hull.faces.iter().enumerate().all(|(fi, f)| {
788 let v0 = self.hull.vertices[self.hull.triangles[fi][0]];
789 dot(f.normal, sub(p, v0)) <= tol
790 })
791 }
792
793 pub fn contains_sphere(&self, center: Vec3, radius: f64) -> bool {
795 self.hull.faces.iter().enumerate().all(|(fi, f)| {
796 let v0 = self.hull.vertices[self.hull.triangles[fi][0]];
797 dot(f.normal, sub(center, v0)) + radius <= 0.0
798 })
799 }
800
801 pub fn contains_aabb(&self, aabb_min: Vec3, aabb_max: Vec3) -> bool {
803 let corners = aabb_corners(aabb_min, aabb_max);
804 corners.iter().all(|&c| self.contains_point(c, 1e-12))
805 }
806}
807
808fn aabb_corners(lo: Vec3, hi: Vec3) -> [Vec3; 8] {
810 [
811 [lo[0], lo[1], lo[2]],
812 [hi[0], lo[1], lo[2]],
813 [lo[0], hi[1], lo[2]],
814 [hi[0], hi[1], lo[2]],
815 [lo[0], lo[1], hi[2]],
816 [hi[0], lo[1], hi[2]],
817 [lo[0], hi[1], hi[2]],
818 [hi[0], hi[1], hi[2]],
819 ]
820}
821
822#[derive(Debug, Clone)]
826pub struct ConvexIntersection;
827
828impl ConvexIntersection {
829 pub fn intersects(hull_a: &ConvexHull3d, hull_b: &ConvexHull3d) -> bool {
833 for f in &hull_a.faces {
835 if Self::is_separating_axis(hull_a, hull_b, f.normal) {
836 return false;
837 }
838 }
839 for f in &hull_b.faces {
841 if Self::is_separating_axis(hull_a, hull_b, f.normal) {
842 return false;
843 }
844 }
845 for ea in &hull_a.half_edges {
847 let da = sub(
848 hull_a.vertices[hull_a.half_edges[ea.next].origin],
849 hull_a.vertices[ea.origin],
850 );
851 for eb in &hull_b.half_edges {
852 let db = sub(
853 hull_b.vertices[hull_b.half_edges[eb.next].origin],
854 hull_b.vertices[eb.origin],
855 );
856 let axis = cross(da, db);
857 if norm(axis) > 1e-12 && Self::is_separating_axis(hull_a, hull_b, normalize(axis)) {
858 return false;
859 }
860 }
861 }
862 true
863 }
864
865 fn is_separating_axis(hull_a: &ConvexHull3d, hull_b: &ConvexHull3d, axis: Vec3) -> bool {
867 let (min_a, max_a) = project_hull(hull_a, axis);
868 let (min_b, max_b) = project_hull(hull_b, axis);
869 max_a < min_b - 1e-12 || max_b < min_a - 1e-12
870 }
871}
872
873fn project_hull(hull: &ConvexHull3d, axis: Vec3) -> (f64, f64) {
875 let mut lo = f64::INFINITY;
876 let mut hi = f64::NEG_INFINITY;
877 for &v in &hull.vertices {
878 let d = dot(v, axis);
879 if d < lo {
880 lo = d;
881 }
882 if d > hi {
883 hi = d;
884 }
885 }
886 (lo, hi)
887}
888
889#[derive(Debug, Clone)]
894pub struct InertiaFromHull;
895
896impl InertiaFromHull {
897 pub fn compute(hull: &ConvexHull3d, density: f64) -> [[f64; 3]; 3] {
899 let mut vol = 0.0_f64;
901 let mut c = [[0.0_f64; 3]; 3]; for tri in &hull.triangles {
904 let p0 = hull.vertices[tri[0]];
905 let p1 = hull.vertices[tri[1]];
906 let p2 = hull.vertices[tri[2]];
907
908 let d = dot(p0, cross(p1, p2));
909 vol += d;
910
911 for i in 0..3 {
912 for j in 0..3 {
913 let v = [p0[i] * p0[j]
914 + p1[i] * p1[j]
915 + p2[i] * p2[j]
916 + p0[i] * p1[j]
917 + p0[j] * p1[i]
918 + p0[i] * p2[j]
919 + p0[j] * p2[i]
920 + p1[i] * p2[j]
921 + p1[j] * p2[i]];
922 c[i][j] += d * v[0];
923 }
924 }
925 }
926 vol /= 6.0;
927 let vol = vol.abs();
928 let mass = density * vol;
929
930 for row in &mut c {
931 for v in row.iter_mut() {
932 *v /= 60.0;
933 }
934 }
935 let sign = if c[0][0] < 0.0 { -1.0 } else { 1.0 };
937 for row in &mut c {
938 for v in row.iter_mut() {
939 *v *= sign;
940 }
941 }
942
943 let trace = c[0][0] + c[1][1] + c[2][2];
945 let mut inertia = [[0.0; 3]; 3];
946 for i in 0..3 {
947 for j in 0..3 {
948 let delta = if i == j { 1.0 } else { 0.0 };
949 if vol > 1e-30 {
950 inertia[i][j] = density * (trace * delta - c[i][j]);
951 }
952 }
953 }
954 for i in 0..3 {
956 inertia[i][i] = inertia[i][i].abs();
957 }
958 let _ = mass;
959 inertia
960 }
961
962 pub fn is_positive_definite(inertia: &[[f64; 3]; 3], tol: f64) -> bool {
965 let d1 = inertia[0][0];
966 let d2 = inertia[0][0] * inertia[1][1] - inertia[0][1] * inertia[1][0];
967 let d3 = {
968 let a = &inertia;
969 a[0][0] * (a[1][1] * a[2][2] - a[1][2] * a[2][1])
970 - a[0][1] * (a[1][0] * a[2][2] - a[1][2] * a[2][0])
971 + a[0][2] * (a[1][0] * a[2][1] - a[1][1] * a[2][0])
972 };
973 d1 > tol && d2 > tol && d3 > tol
974 }
975}
976
977#[derive(Debug, Clone)]
984pub struct HullShrink;
985
986impl HullShrink {
987 pub fn shrink(hull: &ConvexHull3d, margin: f64) -> Vec<Vec3> {
990 let n_verts = hull.vertices.len();
991 let mut normals: Vec<Vec3> = vec![[0.0; 3]; n_verts];
992 let mut counts: Vec<f64> = vec![0.0; n_verts];
993
994 for (fi, face) in hull.faces.iter().enumerate() {
995 for &vi in &hull.triangles[fi] {
996 normals[vi] = add(normals[vi], face.normal);
997 counts[vi] += 1.0;
998 }
999 }
1000 hull.vertices
1001 .iter()
1002 .enumerate()
1003 .map(|(i, &v)| {
1004 let avg_n = if counts[i] > 0.0 {
1005 normalize(scale(normals[i], 1.0 / counts[i]))
1006 } else {
1007 [0.0; 3]
1008 };
1009 sub(v, scale(avg_n, margin))
1010 })
1011 .collect()
1012 }
1013}
1014
1015#[cfg(test)]
1018mod tests {
1019 use super::*;
1020
1021 fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
1022 (a - b).abs() < tol
1023 }
1024
1025 fn octahedron_points() -> Vec<Vec3> {
1027 vec![
1028 [1.0, 0.0, 0.0],
1029 [-1.0, 0.0, 0.0],
1030 [0.0, 1.0, 0.0],
1031 [0.0, -1.0, 0.0],
1032 [0.0, 0.0, 1.0],
1033 [0.0, 0.0, -1.0],
1034 ]
1035 }
1036
1037 fn cube_points(half: f64) -> Vec<Vec3> {
1039 let h = half;
1040 vec![
1041 [-h, -h, -h],
1042 [h, -h, -h],
1043 [-h, h, -h],
1044 [h, h, -h],
1045 [-h, -h, h],
1046 [h, -h, h],
1047 [-h, h, h],
1048 [h, h, h],
1049 ]
1050 }
1051
1052 #[test]
1055 fn test_dot_product() {
1056 assert!(approx_eq(
1057 dot([1.0, 2.0, 3.0], [4.0, 5.0, 6.0]),
1058 32.0,
1059 1e-12
1060 ));
1061 }
1062
1063 #[test]
1064 fn test_cross_product_orthogonal() {
1065 let c = cross([1.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
1066 assert!(approx_eq(c[0], 0.0, 1e-12));
1067 assert!(approx_eq(c[1], 0.0, 1e-12));
1068 assert!(approx_eq(c[2], 1.0, 1e-12));
1069 }
1070
1071 #[test]
1072 fn test_normalize_unit_length() {
1073 let v = normalize([3.0, 4.0, 0.0]);
1074 assert!(approx_eq(norm(v), 1.0, 1e-12), "norm={:.6}", norm(v));
1075 }
1076
1077 #[test]
1078 fn test_norm_zero_vector() {
1079 let v = normalize([0.0, 0.0, 0.0]);
1080 assert!(approx_eq(norm(v), 0.0, 1e-12));
1081 }
1082
1083 #[test]
1086 fn test_hull_builds_from_octahedron() {
1087 let pts = octahedron_points();
1088 let hull = ConvexHull3d::build(&pts);
1089 assert!(!hull.triangles.is_empty(), "hull should have triangles");
1090 }
1091
1092 #[test]
1093 fn test_hull_all_points_inside_or_on() {
1094 let pts = octahedron_points();
1095 let hull = ConvexHull3d::build(&pts);
1096 assert!(
1097 hull.all_points_inside_or_on(&pts, 1e-9),
1098 "all input points should be on or inside hull"
1099 );
1100 }
1101
1102 #[test]
1103 fn test_hull_volume_positive() {
1104 let pts = octahedron_points();
1105 let hull = ConvexHull3d::build(&pts);
1106 let vol = hull.volume();
1107 assert!(vol > 0.0, "hull volume should be positive: {:.6}", vol);
1108 }
1109
1110 #[test]
1111 fn test_hull_volume_cube() {
1112 let pts = cube_points(1.0);
1114 let hull = ConvexHull3d::build(&pts);
1115 let vol = hull.volume();
1116 assert!(vol > 0.0, "cube hull volume should be positive: {:.6}", vol);
1117 }
1118
1119 #[test]
1120 fn test_hull_centroid_near_origin_for_octahedron() {
1121 let pts = octahedron_points();
1122 let hull = ConvexHull3d::build(&pts);
1123 let c = hull.centroid();
1124 assert!(
1125 approx_eq(norm(c), 0.0, 1e-12),
1126 "centroid of symmetric octahedron should be near origin: {:.6}",
1127 norm(c)
1128 );
1129 }
1130
1131 #[test]
1132 fn test_support_function_maximum() {
1133 let pts = octahedron_points();
1134 let hull = ConvexHull3d::build(&pts);
1135 let dir = normalize([1.0, 0.0, 0.0]);
1136 let h = hull.support(dir);
1137 assert!(
1139 approx_eq(h, 1.0, 1e-9),
1140 "support along x should be 1.0: {:.6}",
1141 h
1142 );
1143 }
1144
1145 #[test]
1146 fn test_support_point_achieves_max() {
1147 let pts = cube_points(2.0);
1148 let hull = ConvexHull3d::build(&pts);
1149 let dir = [1.0, 1.0, 1.0_f64];
1150 let sp = hull.support_point(normalize(dir));
1151 let h = hull.support(normalize(dir));
1152 let achieved = dot(sp, normalize(dir));
1153 assert!(
1154 approx_eq(achieved, h, 1e-9),
1155 "support point should achieve max: {:.6} vs {:.6}",
1156 achieved,
1157 h
1158 );
1159 }
1160
1161 #[test]
1164 fn test_support_fn_positive_along_positive_dir() {
1165 let sf = SupportFunction::new(cube_points(1.0));
1166 let h = sf.support([1.0, 0.0, 0.0]);
1167 assert!(
1168 approx_eq(h, 1.0, 1e-9),
1169 "support along +x should be 1.0: {:.6}",
1170 h
1171 );
1172 }
1173
1174 #[test]
1175 fn test_support_fn_symmetric() {
1176 let sf = SupportFunction::new(cube_points(1.0));
1177 let h_pos = sf.support([0.0, 1.0, 0.0]);
1178 let h_neg = sf.support([0.0, -1.0, 0.0]);
1179 assert!(
1180 approx_eq(h_pos, h_neg, 1e-9),
1181 "cube support ±y should be equal"
1182 );
1183 }
1184
1185 #[test]
1188 fn test_minkowski_sum_contains_centroid_of_a() {
1189 let a = SupportFunction::new(cube_points(1.0));
1190 let b = SupportFunction::new(octahedron_points());
1191 let ms = MinkowskiSum::new(a, b);
1192 assert!(
1194 ms.contains_point([0.0; 3], 1e-9),
1195 "centroid should be inside Minkowski sum"
1196 );
1197 }
1198
1199 #[test]
1200 fn test_minkowski_sum_support_additive() {
1201 let a = SupportFunction::new(cube_points(1.0));
1202 let b = SupportFunction::new(cube_points(0.5));
1203 let ms = MinkowskiSum::new(a.clone(), b.clone());
1204 let dir = normalize([1.0, 1.0, 0.0]);
1205 let h_ms = ms.support(dir);
1206 let h_sum = a.support(dir) + b.support(dir);
1207 assert!(
1208 approx_eq(h_ms, h_sum, 1e-9),
1209 "Minkowski sum support should be additive"
1210 );
1211 }
1212
1213 #[test]
1216 fn test_gjk_distance_overlapping_returns_zero() {
1217 let a = SupportFunction::new(cube_points(1.0));
1220 let b = SupportFunction::new(cube_points(0.5));
1221 let gjk = GjkDistance::default();
1222 let d = gjk.distance(&a, &b);
1223 assert!(
1227 d < 1.0,
1228 "overlapping shapes should have small GJK distance, got {:.6}",
1229 d
1230 );
1231 }
1232
1233 #[test]
1234 fn test_gjk_distance_touching_zero() {
1235 let a = SupportFunction::new(cube_points(1.0));
1237 let b_pts: Vec<Vec3> = cube_points(1.0)
1238 .iter()
1239 .map(|&v| add(v, [2.0, 0.0, 0.0]))
1240 .collect();
1241 let b = SupportFunction::new(b_pts);
1242 let gjk = GjkDistance::default();
1243 let d = gjk.distance(&a, &b);
1244 assert!(
1246 d < 0.1,
1247 "touching cubes: GJK distance should be near 0, got {:.6}",
1248 d
1249 );
1250 }
1251
1252 #[test]
1255 fn test_centroid_always_inside_hull() {
1256 let pts = octahedron_points();
1257 let hull = ConvexHull3d::build(&pts);
1258 let c = hull.centroid();
1259 let cc = ConvexContainment::new(hull);
1260 assert!(cc.contains_point(c, 1e-9), "centroid should be inside hull");
1261 }
1262
1263 #[test]
1264 fn test_far_point_outside_hull() {
1265 let pts = octahedron_points();
1266 let hull = ConvexHull3d::build(&pts);
1267 let cc = ConvexContainment::new(hull);
1268 assert!(
1269 !cc.contains_point([10.0, 0.0, 0.0], 1e-9),
1270 "far point should be outside hull"
1271 );
1272 }
1273
1274 #[test]
1275 fn test_aabb_inside_hull() {
1276 let pts = cube_points(2.0);
1277 let hull = ConvexHull3d::build(&pts);
1278 let cc = ConvexContainment::new(hull);
1279 let inside = cc.contains_aabb([-0.5, -0.5, -0.5], [0.5, 0.5, 0.5]);
1281 assert!(inside, "small AABB should be inside larger cube hull");
1282 }
1283
1284 #[test]
1287 fn test_sat_overlapping_hulls_intersect() {
1288 let pts_a = cube_points(1.0);
1289 let pts_b = cube_points(0.5); let hull_a = ConvexHull3d::build(&pts_a);
1291 let hull_b = ConvexHull3d::build(&pts_b);
1292 assert!(
1293 ConvexIntersection::intersects(&hull_a, &hull_b),
1294 "nested hulls should intersect"
1295 );
1296 }
1297
1298 #[test]
1299 fn test_sat_separated_hulls_no_intersect() {
1300 let pts_a = cube_points(0.4);
1301 let pts_b: Vec<Vec3> = cube_points(0.4)
1302 .iter()
1303 .map(|&v| add(v, [5.0, 0.0, 0.0]))
1304 .collect();
1305 let hull_a = ConvexHull3d::build(&pts_a);
1306 let hull_b = ConvexHull3d::build(&pts_b);
1307 assert!(
1308 !ConvexIntersection::intersects(&hull_a, &hull_b),
1309 "separated hulls should not intersect"
1310 );
1311 }
1312
1313 #[test]
1316 fn test_inertia_diagonal_positive() {
1317 let pts = cube_points(1.0);
1318 let hull = ConvexHull3d::build(&pts);
1319 let inertia = InertiaFromHull::compute(&hull, 1000.0);
1320 for i in 0..3 {
1321 assert!(
1322 inertia[i][i] > 0.0,
1323 "diagonal inertia[{}][{}] should be positive: {:.6}",
1324 i,
1325 i,
1326 inertia[i][i]
1327 );
1328 }
1329 }
1330
1331 #[test]
1332 fn test_inertia_symmetric() {
1333 let pts = octahedron_points();
1334 let hull = ConvexHull3d::build(&pts);
1335 let inertia = InertiaFromHull::compute(&hull, 1000.0);
1336 for i in 0..3 {
1337 for j in 0..3 {
1338 assert!(
1339 approx_eq(inertia[i][j], inertia[j][i], 1e-9),
1340 "inertia tensor should be symmetric at ({},{}): {:.6} vs {:.6}",
1341 i,
1342 j,
1343 inertia[i][j],
1344 inertia[j][i]
1345 );
1346 }
1347 }
1348 }
1349
1350 #[test]
1353 fn test_shrink_reduces_support() {
1354 let pts = cube_points(1.0);
1355 let hull = ConvexHull3d::build(&pts);
1356 let shrunk = HullShrink::shrink(&hull, 0.1);
1357 let sf_orig = SupportFunction::new(pts.clone());
1358 let sf_shrunk = SupportFunction::new(shrunk);
1359 let dir = normalize([1.0, 0.0, 0.0]);
1360 assert!(
1361 sf_shrunk.support(dir) < sf_orig.support(dir),
1362 "shrunk hull should have smaller support"
1363 );
1364 }
1365
1366 #[test]
1369 fn test_acd_returns_at_least_one_part() {
1370 let pts = cube_points(1.0);
1371 let acd = ApproxConvexDecomp::new();
1372 let parts = acd.decompose(&pts);
1373 assert!(!parts.is_empty(), "ACD should produce at least one part");
1374 }
1375
1376 #[test]
1377 fn test_acd_parts_are_convex() {
1378 let pts = cube_points(1.0);
1379 let acd = ApproxConvexDecomp::new();
1380 let parts = acd.decompose(&pts);
1381 for (i, part) in parts.parts.iter().enumerate() {
1382 assert!(part.is_convex(1e-9), "part {} should be convex", i);
1383 }
1384 }
1385}