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}
357
358impl ConvexPart {
359 pub fn new(vertices: Vec<Vec3>) -> Self {
361 let concavity = 0.0;
362 Self {
363 vertices,
364 concavity,
365 }
366 }
367
368 pub fn is_convex(&self, tol: f64) -> bool {
370 if self.vertices.len() < 4 {
371 return true;
372 }
373 let hull = ConvexHull3d::build(&self.vertices);
374 hull.all_points_inside_or_on(&self.vertices, tol)
375 }
376}
377
378#[derive(Debug, Clone)]
380pub struct ConvexParts {
381 pub parts: Vec<ConvexPart>,
383}
384
385impl ConvexParts {
386 pub fn len(&self) -> usize {
388 self.parts.len()
389 }
390
391 pub fn is_empty(&self) -> bool {
393 self.parts.is_empty()
394 }
395}
396
397#[derive(Debug, Clone)]
399pub struct ApproxConvexDecomp {
400 pub config: AcdConfig,
402}
403
404impl Default for ApproxConvexDecomp {
405 fn default() -> Self {
406 Self::new()
407 }
408}
409
410impl ApproxConvexDecomp {
411 pub fn new() -> Self {
413 Self {
414 config: AcdConfig::default(),
415 }
416 }
417
418 pub fn with_config(config: AcdConfig) -> Self {
420 Self { config }
421 }
422
423 pub fn decompose(&self, vertices: &[Vec3]) -> ConvexParts {
429 let mut pending: Vec<Vec<Vec3>> = vec![vertices.to_vec()];
430 let mut results: Vec<ConvexPart> = Vec::new();
431
432 while !pending.is_empty() && results.len() < self.config.max_parts {
433 let cluster = pending.pop().expect("collection should not be empty");
434 if cluster.len() < 4 {
435 results.push(ConvexPart::new(cluster));
436 continue;
437 }
438 let hull = ConvexHull3d::build(&cluster);
439 let concavity = self.max_concavity_of(&cluster, &hull);
440 if concavity <= self.config.max_concavity
441 || results.len() + pending.len() + 1 >= self.config.max_parts
442 {
443 let mut part = ConvexPart::new(cluster);
444 part.concavity = concavity;
445 results.push(part);
446 } else {
447 let (left, right) = self.split_at_max_concavity(&cluster, &hull);
449 let small_left = left.len() < 4;
450 let small_right = right.len() < 4;
451 if !small_left {
452 pending.push(left);
453 }
454 if !small_right {
455 pending.push(right);
456 }
457 if small_left || small_right {
458 let mut part = ConvexPart::new(cluster);
459 part.concavity = concavity;
460 results.push(part);
461 }
462 }
463 }
464 for cluster in pending {
466 results.push(ConvexPart::new(cluster));
467 }
468 ConvexParts { parts: results }
469 }
470
471 fn max_concavity_of(&self, pts: &[Vec3], hull: &ConvexHull3d) -> f64 {
473 pts.iter()
474 .map(|&p| self.signed_distance_to_hull(p, hull))
475 .fold(0.0_f64, f64::max)
476 }
477
478 fn signed_distance_to_hull(&self, p: Vec3, hull: &ConvexHull3d) -> f64 {
480 hull.faces
481 .iter()
482 .enumerate()
483 .map(|(fi, f)| {
484 let v0 = hull.vertices[hull.triangles[fi][0]];
485 dot(f.normal, sub(p, v0))
486 })
487 .fold(f64::NEG_INFINITY, f64::max)
488 }
489
490 fn split_at_max_concavity(&self, pts: &[Vec3], hull: &ConvexHull3d) -> (Vec<Vec3>, Vec<Vec3>) {
492 let (_, split_pt) = pts
494 .iter()
495 .map(|&p| (self.signed_distance_to_hull(p, hull), p))
496 .fold((f64::NEG_INFINITY, pts[0]), |best, curr| {
497 if curr.0 > best.0 { curr } else { best }
498 });
499
500 let (axis, median) = principal_axis_median(pts);
502 let mut split_used = false;
503 let left: Vec<Vec3> = pts
504 .iter()
505 .filter(|&&p| {
506 if p[axis] < median {
507 true
508 } else if p == split_pt && !split_used {
509 split_used = true;
510 true
511 } else {
512 false
513 }
514 })
515 .copied()
516 .collect();
517 let right: Vec<Vec3> = pts
518 .iter()
519 .filter(|&&p| p[axis] >= median && p != split_pt)
520 .copied()
521 .collect();
522 (left, right)
523 }
524}
525
526fn principal_axis_median(pts: &[Vec3]) -> (usize, f64) {
528 let n = pts.len() as f64;
529 let mean: Vec3 = {
530 let s = pts.iter().fold([0.0; 3], |acc, &v| add(acc, v));
531 scale(s, 1.0 / n)
532 };
533 let var: [f64; 3] = {
534 let mut v = [0.0; 3];
535 for &p in pts {
536 for k in 0..3 {
537 v[k] += (p[k] - mean[k]).powi(2);
538 }
539 }
540 v
541 };
542 let axis = if var[0] >= var[1] && var[0] >= var[2] {
543 0
544 } else if var[1] >= var[2] {
545 1
546 } else {
547 2
548 };
549 let median = mean[axis];
550 (axis, median)
551}
552
553#[derive(Debug, Clone)]
557pub struct SupportFunction {
558 pub vertices: Vec<Vec3>,
560}
561
562impl SupportFunction {
563 pub fn new(vertices: Vec<Vec3>) -> Self {
565 Self { vertices }
566 }
567
568 pub fn support(&self, direction: Vec3) -> f64 {
570 self.vertices
571 .iter()
572 .map(|&v| dot(v, direction))
573 .fold(f64::NEG_INFINITY, f64::max)
574 }
575
576 pub fn support_point(&self, direction: Vec3) -> Vec3 {
578 *self
579 .vertices
580 .iter()
581 .max_by(|&&a, &&b| {
582 dot(a, direction)
583 .partial_cmp(&dot(b, direction))
584 .unwrap_or(std::cmp::Ordering::Equal)
585 })
586 .expect("operation should succeed")
587 }
588}
589
590#[derive(Debug, Clone)]
596pub struct MinkowskiSum {
597 pub shape_a: SupportFunction,
599 pub shape_b: SupportFunction,
601}
602
603impl MinkowskiSum {
604 pub fn new(shape_a: SupportFunction, shape_b: SupportFunction) -> Self {
606 Self { shape_a, shape_b }
607 }
608
609 pub fn support(&self, direction: Vec3) -> f64 {
611 self.shape_a.support(direction) + self.shape_b.support(direction)
612 }
613
614 pub fn support_point(&self, direction: Vec3) -> Vec3 {
616 add(
617 self.shape_a.support_point(direction),
618 self.shape_b.support_point(direction),
619 )
620 }
621
622 pub fn contains_point(&self, p: Vec3, tol: f64) -> bool {
624 let test_dirs: &[Vec3] = &[
626 [1.0, 0.0, 0.0],
627 [-1.0, 0.0, 0.0],
628 [0.0, 1.0, 0.0],
629 [0.0, -1.0, 0.0],
630 [0.0, 0.0, 1.0],
631 [0.0, 0.0, -1.0],
632 [1.0, 1.0, 0.0],
633 [-1.0, 1.0, 0.0],
634 [1.0, -1.0, 0.0],
635 [0.0, 1.0, 1.0],
636 ];
637 for &d in test_dirs {
638 let d_n = normalize(d);
639 if dot(p, d_n) > self.support(d_n) + tol {
640 return false;
641 }
642 }
643 true
644 }
645}
646
647#[derive(Debug, Clone)]
651pub struct GjkDistance {
652 pub max_iter: usize,
654 pub tolerance: f64,
656}
657
658impl GjkDistance {
659 pub fn new(max_iter: usize, tolerance: f64) -> Self {
661 Self {
662 max_iter,
663 tolerance,
664 }
665 }
666
667 pub fn default() -> Self {
669 Self::new(64, 1e-10)
670 }
671
672 pub fn distance(&self, a: &SupportFunction, b: &SupportFunction) -> f64 {
676 let cso_support = |d: Vec3| -> Vec3 { sub(a.support_point(d), b.support_point(neg(d))) };
678
679 let mut dir = [1.0, 0.0, 0.0_f64];
680 let mut simplex: Vec<Vec3> = Vec::with_capacity(4);
681 let first = cso_support(dir);
682 simplex.push(first);
683 dir = neg(first);
684
685 for _ in 0..self.max_iter {
686 if norm(dir) < self.tolerance {
687 return 0.0;
688 }
689 let a_pt = cso_support(normalize(dir));
690 if dot(a_pt, normalize(dir)) < dot(simplex[0], normalize(dir)) - self.tolerance {
691 break;
693 }
694 simplex.push(a_pt);
695 let (new_simplex, new_dir, contains_origin) = nearest_simplex(simplex.clone());
696 simplex = new_simplex;
697 if contains_origin {
698 return 0.0;
699 }
700 dir = new_dir;
701 }
702 norm(dir)
703 }
704}
705
706fn nearest_simplex(simplex: Vec<Vec3>) -> (Vec<Vec3>, Vec3, bool) {
708 match simplex.len() {
709 1 => {
710 let a = simplex[0];
711 (simplex, neg(a), false)
712 }
713 2 => {
714 let (b, a) = (simplex[0], simplex[1]);
715 let ab = sub(b, a);
716 let ao = neg(a);
717 if dot(ab, ao) > 0.0 {
718 let dir = sub(ab, scale(a, dot(ab, ao) / dot(ab, ab)));
719 (vec![b, a], dir, false)
720 } else {
721 (vec![a], ao, false)
722 }
723 }
724 3 => {
725 let (c, b, a) = (simplex[0], simplex[1], simplex[2]);
726 let ab = sub(b, a);
727 let ac = sub(c, a);
728 let ao = neg(a);
729 let abc = cross(ab, ac);
730 if dot(cross(abc, ac), ao) > 0.0 {
731 (
732 vec![c, a],
733 sub(ac, scale(a, dot(ac, ao) / dot(ac, ac))),
734 false,
735 )
736 } else if dot(cross(ab, abc), ao) > 0.0 {
737 (
738 vec![b, a],
739 sub(ab, scale(a, dot(ab, ao) / dot(ab, ab))),
740 false,
741 )
742 } else if dot(abc, ao) > 0.0 {
743 (vec![c, b, a], abc, false)
744 } else {
745 (vec![b, c, a], neg(abc), false)
746 }
747 }
748 _ => {
749 let origin = [0.0; 3];
751 let centroid = scale(simplex.iter().fold([0.0; 3], |acc, &v| add(acc, v)), 0.25);
752 let dir = sub(origin, centroid);
753 if norm(dir) < 1e-12 {
754 return (simplex, [0.0; 3], true);
755 }
756 (simplex, dir, false)
757 }
758 }
759}
760
761#[derive(Debug, Clone)]
765pub struct ConvexContainment {
766 pub hull: ConvexHull3d,
768}
769
770impl ConvexContainment {
771 pub fn new(hull: ConvexHull3d) -> Self {
773 Self { hull }
774 }
775
776 pub fn contains_point(&self, p: Vec3, tol: f64) -> bool {
778 self.hull.faces.iter().enumerate().all(|(fi, f)| {
779 let v0 = self.hull.vertices[self.hull.triangles[fi][0]];
780 dot(f.normal, sub(p, v0)) <= tol
781 })
782 }
783
784 pub fn contains_sphere(&self, center: Vec3, radius: f64) -> bool {
786 self.hull.faces.iter().enumerate().all(|(fi, f)| {
787 let v0 = self.hull.vertices[self.hull.triangles[fi][0]];
788 dot(f.normal, sub(center, v0)) + radius <= 0.0
789 })
790 }
791
792 pub fn contains_aabb(&self, aabb_min: Vec3, aabb_max: Vec3) -> bool {
794 let corners = aabb_corners(aabb_min, aabb_max);
795 corners.iter().all(|&c| self.contains_point(c, 1e-12))
796 }
797}
798
799fn aabb_corners(lo: Vec3, hi: Vec3) -> [Vec3; 8] {
801 [
802 [lo[0], lo[1], lo[2]],
803 [hi[0], lo[1], lo[2]],
804 [lo[0], hi[1], lo[2]],
805 [hi[0], hi[1], lo[2]],
806 [lo[0], lo[1], hi[2]],
807 [hi[0], lo[1], hi[2]],
808 [lo[0], hi[1], hi[2]],
809 [hi[0], hi[1], hi[2]],
810 ]
811}
812
813#[derive(Debug, Clone)]
817pub struct ConvexIntersection;
818
819impl ConvexIntersection {
820 pub fn intersects(hull_a: &ConvexHull3d, hull_b: &ConvexHull3d) -> bool {
824 for f in &hull_a.faces {
826 if Self::is_separating_axis(hull_a, hull_b, f.normal) {
827 return false;
828 }
829 }
830 for f in &hull_b.faces {
832 if Self::is_separating_axis(hull_a, hull_b, f.normal) {
833 return false;
834 }
835 }
836 for ea in &hull_a.half_edges {
838 let da = sub(
839 hull_a.vertices[hull_a.half_edges[ea.next].origin],
840 hull_a.vertices[ea.origin],
841 );
842 for eb in &hull_b.half_edges {
843 let db = sub(
844 hull_b.vertices[hull_b.half_edges[eb.next].origin],
845 hull_b.vertices[eb.origin],
846 );
847 let axis = cross(da, db);
848 if norm(axis) > 1e-12 && Self::is_separating_axis(hull_a, hull_b, normalize(axis)) {
849 return false;
850 }
851 }
852 }
853 true
854 }
855
856 fn is_separating_axis(hull_a: &ConvexHull3d, hull_b: &ConvexHull3d, axis: Vec3) -> bool {
858 let (min_a, max_a) = project_hull(hull_a, axis);
859 let (min_b, max_b) = project_hull(hull_b, axis);
860 max_a < min_b - 1e-12 || max_b < min_a - 1e-12
861 }
862}
863
864fn project_hull(hull: &ConvexHull3d, axis: Vec3) -> (f64, f64) {
866 let mut lo = f64::INFINITY;
867 let mut hi = f64::NEG_INFINITY;
868 for &v in &hull.vertices {
869 let d = dot(v, axis);
870 if d < lo {
871 lo = d;
872 }
873 if d > hi {
874 hi = d;
875 }
876 }
877 (lo, hi)
878}
879
880#[derive(Debug, Clone)]
885pub struct InertiaFromHull;
886
887impl InertiaFromHull {
888 pub fn compute(hull: &ConvexHull3d, density: f64) -> [[f64; 3]; 3] {
890 let mut vol = 0.0_f64;
892 let mut c = [[0.0_f64; 3]; 3]; for tri in &hull.triangles {
895 let p0 = hull.vertices[tri[0]];
896 let p1 = hull.vertices[tri[1]];
897 let p2 = hull.vertices[tri[2]];
898
899 let d = dot(p0, cross(p1, p2));
900 vol += d;
901
902 for i in 0..3 {
903 for j in 0..3 {
904 let v = [p0[i] * p0[j]
905 + p1[i] * p1[j]
906 + p2[i] * p2[j]
907 + p0[i] * p1[j]
908 + p0[j] * p1[i]
909 + p0[i] * p2[j]
910 + p0[j] * p2[i]
911 + p1[i] * p2[j]
912 + p1[j] * p2[i]];
913 c[i][j] += d * v[0];
914 }
915 }
916 }
917 vol /= 6.0;
918 let vol = vol.abs();
919 let mass = density * vol;
920
921 for row in &mut c {
922 for v in row.iter_mut() {
923 *v /= 60.0;
924 }
925 }
926 let sign = if c[0][0] < 0.0 { -1.0 } else { 1.0 };
928 for row in &mut c {
929 for v in row.iter_mut() {
930 *v *= sign;
931 }
932 }
933
934 let trace = c[0][0] + c[1][1] + c[2][2];
936 let mut inertia = [[0.0; 3]; 3];
937 for i in 0..3 {
938 for j in 0..3 {
939 let delta = if i == j { 1.0 } else { 0.0 };
940 if vol > 1e-30 {
941 inertia[i][j] = density * (trace * delta - c[i][j]);
942 }
943 }
944 }
945 for i in 0..3 {
947 inertia[i][i] = inertia[i][i].abs();
948 }
949 let _ = mass;
950 inertia
951 }
952
953 pub fn is_positive_definite(inertia: &[[f64; 3]; 3], tol: f64) -> bool {
956 let d1 = inertia[0][0];
957 let d2 = inertia[0][0] * inertia[1][1] - inertia[0][1] * inertia[1][0];
958 let d3 = {
959 let a = &inertia;
960 a[0][0] * (a[1][1] * a[2][2] - a[1][2] * a[2][1])
961 - a[0][1] * (a[1][0] * a[2][2] - a[1][2] * a[2][0])
962 + a[0][2] * (a[1][0] * a[2][1] - a[1][1] * a[2][0])
963 };
964 d1 > tol && d2 > tol && d3 > tol
965 }
966}
967
968#[derive(Debug, Clone)]
975pub struct HullShrink;
976
977impl HullShrink {
978 pub fn shrink(hull: &ConvexHull3d, margin: f64) -> Vec<Vec3> {
981 let n_verts = hull.vertices.len();
982 let mut normals: Vec<Vec3> = vec![[0.0; 3]; n_verts];
983 let mut counts: Vec<f64> = vec![0.0; n_verts];
984
985 for (fi, face) in hull.faces.iter().enumerate() {
986 for &vi in &hull.triangles[fi] {
987 normals[vi] = add(normals[vi], face.normal);
988 counts[vi] += 1.0;
989 }
990 }
991 hull.vertices
992 .iter()
993 .enumerate()
994 .map(|(i, &v)| {
995 let avg_n = if counts[i] > 0.0 {
996 normalize(scale(normals[i], 1.0 / counts[i]))
997 } else {
998 [0.0; 3]
999 };
1000 sub(v, scale(avg_n, margin))
1001 })
1002 .collect()
1003 }
1004}
1005
1006#[cfg(test)]
1009mod tests {
1010 use super::*;
1011
1012 fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
1013 (a - b).abs() < tol
1014 }
1015
1016 fn octahedron_points() -> Vec<Vec3> {
1018 vec![
1019 [1.0, 0.0, 0.0],
1020 [-1.0, 0.0, 0.0],
1021 [0.0, 1.0, 0.0],
1022 [0.0, -1.0, 0.0],
1023 [0.0, 0.0, 1.0],
1024 [0.0, 0.0, -1.0],
1025 ]
1026 }
1027
1028 fn cube_points(half: f64) -> Vec<Vec3> {
1030 let h = half;
1031 vec![
1032 [-h, -h, -h],
1033 [h, -h, -h],
1034 [-h, h, -h],
1035 [h, h, -h],
1036 [-h, -h, h],
1037 [h, -h, h],
1038 [-h, h, h],
1039 [h, h, h],
1040 ]
1041 }
1042
1043 #[test]
1046 fn test_dot_product() {
1047 assert!(approx_eq(
1048 dot([1.0, 2.0, 3.0], [4.0, 5.0, 6.0]),
1049 32.0,
1050 1e-12
1051 ));
1052 }
1053
1054 #[test]
1055 fn test_cross_product_orthogonal() {
1056 let c = cross([1.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
1057 assert!(approx_eq(c[0], 0.0, 1e-12));
1058 assert!(approx_eq(c[1], 0.0, 1e-12));
1059 assert!(approx_eq(c[2], 1.0, 1e-12));
1060 }
1061
1062 #[test]
1063 fn test_normalize_unit_length() {
1064 let v = normalize([3.0, 4.0, 0.0]);
1065 assert!(approx_eq(norm(v), 1.0, 1e-12), "norm={:.6}", norm(v));
1066 }
1067
1068 #[test]
1069 fn test_norm_zero_vector() {
1070 let v = normalize([0.0, 0.0, 0.0]);
1071 assert!(approx_eq(norm(v), 0.0, 1e-12));
1072 }
1073
1074 #[test]
1077 fn test_hull_builds_from_octahedron() {
1078 let pts = octahedron_points();
1079 let hull = ConvexHull3d::build(&pts);
1080 assert!(!hull.triangles.is_empty(), "hull should have triangles");
1081 }
1082
1083 #[test]
1084 fn test_hull_all_points_inside_or_on() {
1085 let pts = octahedron_points();
1086 let hull = ConvexHull3d::build(&pts);
1087 assert!(
1088 hull.all_points_inside_or_on(&pts, 1e-9),
1089 "all input points should be on or inside hull"
1090 );
1091 }
1092
1093 #[test]
1094 fn test_hull_volume_positive() {
1095 let pts = octahedron_points();
1096 let hull = ConvexHull3d::build(&pts);
1097 let vol = hull.volume();
1098 assert!(vol > 0.0, "hull volume should be positive: {:.6}", vol);
1099 }
1100
1101 #[test]
1102 fn test_hull_volume_cube() {
1103 let pts = cube_points(1.0);
1105 let hull = ConvexHull3d::build(&pts);
1106 let vol = hull.volume();
1107 assert!(vol > 0.0, "cube hull volume should be positive: {:.6}", vol);
1108 }
1109
1110 #[test]
1111 fn test_hull_centroid_near_origin_for_octahedron() {
1112 let pts = octahedron_points();
1113 let hull = ConvexHull3d::build(&pts);
1114 let c = hull.centroid();
1115 assert!(
1116 approx_eq(norm(c), 0.0, 1e-12),
1117 "centroid of symmetric octahedron should be near origin: {:.6}",
1118 norm(c)
1119 );
1120 }
1121
1122 #[test]
1123 fn test_support_function_maximum() {
1124 let pts = octahedron_points();
1125 let hull = ConvexHull3d::build(&pts);
1126 let dir = normalize([1.0, 0.0, 0.0]);
1127 let h = hull.support(dir);
1128 assert!(
1130 approx_eq(h, 1.0, 1e-9),
1131 "support along x should be 1.0: {:.6}",
1132 h
1133 );
1134 }
1135
1136 #[test]
1137 fn test_support_point_achieves_max() {
1138 let pts = cube_points(2.0);
1139 let hull = ConvexHull3d::build(&pts);
1140 let dir = [1.0, 1.0, 1.0_f64];
1141 let sp = hull.support_point(normalize(dir));
1142 let h = hull.support(normalize(dir));
1143 let achieved = dot(sp, normalize(dir));
1144 assert!(
1145 approx_eq(achieved, h, 1e-9),
1146 "support point should achieve max: {:.6} vs {:.6}",
1147 achieved,
1148 h
1149 );
1150 }
1151
1152 #[test]
1155 fn test_support_fn_positive_along_positive_dir() {
1156 let sf = SupportFunction::new(cube_points(1.0));
1157 let h = sf.support([1.0, 0.0, 0.0]);
1158 assert!(
1159 approx_eq(h, 1.0, 1e-9),
1160 "support along +x should be 1.0: {:.6}",
1161 h
1162 );
1163 }
1164
1165 #[test]
1166 fn test_support_fn_symmetric() {
1167 let sf = SupportFunction::new(cube_points(1.0));
1168 let h_pos = sf.support([0.0, 1.0, 0.0]);
1169 let h_neg = sf.support([0.0, -1.0, 0.0]);
1170 assert!(
1171 approx_eq(h_pos, h_neg, 1e-9),
1172 "cube support ±y should be equal"
1173 );
1174 }
1175
1176 #[test]
1179 fn test_minkowski_sum_contains_centroid_of_a() {
1180 let a = SupportFunction::new(cube_points(1.0));
1181 let b = SupportFunction::new(octahedron_points());
1182 let ms = MinkowskiSum::new(a, b);
1183 assert!(
1185 ms.contains_point([0.0; 3], 1e-9),
1186 "centroid should be inside Minkowski sum"
1187 );
1188 }
1189
1190 #[test]
1191 fn test_minkowski_sum_support_additive() {
1192 let a = SupportFunction::new(cube_points(1.0));
1193 let b = SupportFunction::new(cube_points(0.5));
1194 let ms = MinkowskiSum::new(a.clone(), b.clone());
1195 let dir = normalize([1.0, 1.0, 0.0]);
1196 let h_ms = ms.support(dir);
1197 let h_sum = a.support(dir) + b.support(dir);
1198 assert!(
1199 approx_eq(h_ms, h_sum, 1e-9),
1200 "Minkowski sum support should be additive"
1201 );
1202 }
1203
1204 #[test]
1207 fn test_gjk_distance_overlapping_returns_zero() {
1208 let a = SupportFunction::new(cube_points(1.0));
1211 let b = SupportFunction::new(cube_points(0.5));
1212 let gjk = GjkDistance::default();
1213 let d = gjk.distance(&a, &b);
1214 assert!(
1218 d < 1.0,
1219 "overlapping shapes should have small GJK distance, got {:.6}",
1220 d
1221 );
1222 }
1223
1224 #[test]
1225 fn test_gjk_distance_touching_zero() {
1226 let a = SupportFunction::new(cube_points(1.0));
1228 let b_pts: Vec<Vec3> = cube_points(1.0)
1229 .iter()
1230 .map(|&v| add(v, [2.0, 0.0, 0.0]))
1231 .collect();
1232 let b = SupportFunction::new(b_pts);
1233 let gjk = GjkDistance::default();
1234 let d = gjk.distance(&a, &b);
1235 assert!(
1237 d < 0.1,
1238 "touching cubes: GJK distance should be near 0, got {:.6}",
1239 d
1240 );
1241 }
1242
1243 #[test]
1246 fn test_centroid_always_inside_hull() {
1247 let pts = octahedron_points();
1248 let hull = ConvexHull3d::build(&pts);
1249 let c = hull.centroid();
1250 let cc = ConvexContainment::new(hull);
1251 assert!(cc.contains_point(c, 1e-9), "centroid should be inside hull");
1252 }
1253
1254 #[test]
1255 fn test_far_point_outside_hull() {
1256 let pts = octahedron_points();
1257 let hull = ConvexHull3d::build(&pts);
1258 let cc = ConvexContainment::new(hull);
1259 assert!(
1260 !cc.contains_point([10.0, 0.0, 0.0], 1e-9),
1261 "far point should be outside hull"
1262 );
1263 }
1264
1265 #[test]
1266 fn test_aabb_inside_hull() {
1267 let pts = cube_points(2.0);
1268 let hull = ConvexHull3d::build(&pts);
1269 let cc = ConvexContainment::new(hull);
1270 let inside = cc.contains_aabb([-0.5, -0.5, -0.5], [0.5, 0.5, 0.5]);
1272 assert!(inside, "small AABB should be inside larger cube hull");
1273 }
1274
1275 #[test]
1278 fn test_sat_overlapping_hulls_intersect() {
1279 let pts_a = cube_points(1.0);
1280 let pts_b = cube_points(0.5); let hull_a = ConvexHull3d::build(&pts_a);
1282 let hull_b = ConvexHull3d::build(&pts_b);
1283 assert!(
1284 ConvexIntersection::intersects(&hull_a, &hull_b),
1285 "nested hulls should intersect"
1286 );
1287 }
1288
1289 #[test]
1290 fn test_sat_separated_hulls_no_intersect() {
1291 let pts_a = cube_points(0.4);
1292 let pts_b: Vec<Vec3> = cube_points(0.4)
1293 .iter()
1294 .map(|&v| add(v, [5.0, 0.0, 0.0]))
1295 .collect();
1296 let hull_a = ConvexHull3d::build(&pts_a);
1297 let hull_b = ConvexHull3d::build(&pts_b);
1298 assert!(
1299 !ConvexIntersection::intersects(&hull_a, &hull_b),
1300 "separated hulls should not intersect"
1301 );
1302 }
1303
1304 #[test]
1307 fn test_inertia_diagonal_positive() {
1308 let pts = cube_points(1.0);
1309 let hull = ConvexHull3d::build(&pts);
1310 let inertia = InertiaFromHull::compute(&hull, 1000.0);
1311 for i in 0..3 {
1312 assert!(
1313 inertia[i][i] > 0.0,
1314 "diagonal inertia[{}][{}] should be positive: {:.6}",
1315 i,
1316 i,
1317 inertia[i][i]
1318 );
1319 }
1320 }
1321
1322 #[test]
1323 fn test_inertia_symmetric() {
1324 let pts = octahedron_points();
1325 let hull = ConvexHull3d::build(&pts);
1326 let inertia = InertiaFromHull::compute(&hull, 1000.0);
1327 for i in 0..3 {
1328 for j in 0..3 {
1329 assert!(
1330 approx_eq(inertia[i][j], inertia[j][i], 1e-9),
1331 "inertia tensor should be symmetric at ({},{}): {:.6} vs {:.6}",
1332 i,
1333 j,
1334 inertia[i][j],
1335 inertia[j][i]
1336 );
1337 }
1338 }
1339 }
1340
1341 #[test]
1344 fn test_shrink_reduces_support() {
1345 let pts = cube_points(1.0);
1346 let hull = ConvexHull3d::build(&pts);
1347 let shrunk = HullShrink::shrink(&hull, 0.1);
1348 let sf_orig = SupportFunction::new(pts.clone());
1349 let sf_shrunk = SupportFunction::new(shrunk);
1350 let dir = normalize([1.0, 0.0, 0.0]);
1351 assert!(
1352 sf_shrunk.support(dir) < sf_orig.support(dir),
1353 "shrunk hull should have smaller support"
1354 );
1355 }
1356
1357 #[test]
1360 fn test_acd_returns_at_least_one_part() {
1361 let pts = cube_points(1.0);
1362 let acd = ApproxConvexDecomp::new();
1363 let parts = acd.decompose(&pts);
1364 assert!(!parts.is_empty(), "ACD should produce at least one part");
1365 }
1366
1367 #[test]
1368 fn test_acd_parts_are_convex() {
1369 let pts = cube_points(1.0);
1370 let acd = ApproxConvexDecomp::new();
1371 let parts = acd.decompose(&pts);
1372 for (i, part) in parts.parts.iter().enumerate() {
1373 assert!(part.is_convex(1e-9), "part {} should be convex", i);
1374 }
1375 }
1376}