1pub type Vec3 = [f64; 3];
19
20#[inline]
22pub fn dot(a: Vec3, b: Vec3) -> f64 {
23 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
24}
25
26#[inline]
28pub fn cross(a: Vec3, b: Vec3) -> Vec3 {
29 [
30 a[1] * b[2] - a[2] * b[1],
31 a[2] * b[0] - a[0] * b[2],
32 a[0] * b[1] - a[1] * b[0],
33 ]
34}
35
36#[inline]
38pub fn sub(a: Vec3, b: Vec3) -> Vec3 {
39 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
40}
41
42#[inline]
44pub fn add(a: Vec3, b: Vec3) -> Vec3 {
45 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
46}
47
48#[inline]
50pub fn scale(v: Vec3, s: f64) -> Vec3 {
51 [v[0] * s, v[1] * s, v[2] * s]
52}
53
54#[inline]
56pub fn norm(v: Vec3) -> f64 {
57 dot(v, v).sqrt()
58}
59
60#[inline]
62pub fn normalize(v: Vec3) -> Vec3 {
63 let n = norm(v);
64 if n < 1e-15 {
65 [0.0; 3]
66 } else {
67 scale(v, 1.0 / n)
68 }
69}
70
71#[inline]
73pub fn neg(v: Vec3) -> Vec3 {
74 [-v[0], -v[1], -v[2]]
75}
76
77#[derive(Debug, Clone)]
81pub struct HalfEdge {
82 pub origin: usize,
84 pub twin: usize,
86 pub next: usize,
88 pub face: usize,
90}
91
92#[derive(Debug, Clone)]
94pub struct HullFace {
95 pub edge: usize,
97 pub normal: Vec3,
99}
100
101#[derive(Debug, Clone)]
106pub struct ConvexHull3d {
107 pub vertices: Vec<Vec3>,
109 pub half_edges: Vec<HalfEdge>,
111 pub faces: Vec<HullFace>,
113 pub triangles: Vec<[usize; 3]>,
115}
116
117impl ConvexHull3d {
118 pub fn build(points: &[Vec3]) -> Self {
122 assert!(points.len() >= 4, "need at least 4 non-coplanar points");
123 let tris = incremental_hull(points);
125 let vertices = points.to_vec();
126 let (half_edges, faces) = build_half_edge_structure(&tris, &vertices);
128 Self {
129 vertices,
130 half_edges,
131 faces,
132 triangles: tris,
133 }
134 }
135
136 pub fn all_points_inside_or_on(&self, points: &[Vec3], tol: f64) -> bool {
138 points.iter().all(|&p| {
139 self.faces.iter().enumerate().all(|(fi, f)| {
140 let tri = &self.triangles[fi];
141 let v0 = self.vertices[tri[0]];
142 let signed_dist = dot(f.normal, sub(p, v0));
143 signed_dist <= tol
144 })
145 })
146 }
147
148 pub fn volume(&self) -> f64 {
150 let mut vol = 0.0_f64;
151 for tri in &self.triangles {
152 let a = self.vertices[tri[0]];
153 let b = self.vertices[tri[1]];
154 let c = self.vertices[tri[2]];
155 vol += dot(a, cross(b, c));
157 }
158 (vol / 6.0).abs()
159 }
160
161 pub fn centroid(&self) -> Vec3 {
163 let n = self.vertices.len() as f64;
164 let s = self.vertices.iter().fold([0.0; 3], |acc, &v| add(acc, v));
165 scale(s, 1.0 / n)
166 }
167
168 pub fn support(&self, direction: Vec3) -> f64 {
170 self.vertices
171 .iter()
172 .map(|&v| dot(v, direction))
173 .fold(f64::NEG_INFINITY, f64::max)
174 }
175
176 pub fn support_point(&self, direction: Vec3) -> Vec3 {
178 *self
179 .vertices
180 .iter()
181 .max_by(|&&a, &&b| {
182 dot(a, direction)
183 .partial_cmp(&dot(b, direction))
184 .unwrap_or(std::cmp::Ordering::Equal)
185 })
186 .expect("operation should succeed")
187 }
188}
189
190fn incremental_hull(pts: &[Vec3]) -> Vec<[usize; 3]> {
192 let mut tris: Vec<[usize; 3]> = vec![[0, 1, 2], [0, 3, 1], [0, 2, 3], [1, 3, 2]];
194 let centroid = {
196 let s = pts[0..4].iter().fold([0.0; 3], |acc, &v| add(acc, v));
197 scale(s, 0.25)
198 };
199 for tri in &mut tris {
200 orient_outward(pts, tri, centroid);
201 }
202
203 for i in 4..pts.len() {
205 let p = pts[i];
206 let visible: Vec<bool> = tris
208 .iter()
209 .map(|tri| {
210 let n = face_normal(pts, tri);
211 let v0 = pts[tri[0]];
212 dot(n, sub(p, v0)) > 1e-12
213 })
214 .collect();
215
216 let mut horizon: Vec<(usize, usize)> = Vec::new();
218 for (fi, &vis) in visible.iter().enumerate() {
219 if vis {
220 let tri = tris[fi];
221 for k in 0..3 {
222 let a = tri[k];
223 let b = tri[(k + 1) % 3];
224 let twin_visible = tris.iter().enumerate().any(|(fj, &t)| {
226 fj != fi && t.contains(&b) && t.contains(&a) && visible[fj]
227 });
228 if !twin_visible {
229 horizon.push((a, b));
230 }
231 }
232 }
233 }
234 let mut new_tris: Vec<[usize; 3]> = tris
236 .iter()
237 .zip(visible.iter())
238 .filter(|&(_, v)| !v)
239 .map(|(&t, _)| t)
240 .collect();
241 for (a, b) in horizon {
243 let mut tri = [a, b, i];
244 orient_outward(pts, &mut tri, centroid);
245 new_tris.push(tri);
246 }
247 if !new_tris.is_empty() {
248 tris = new_tris;
249 }
250 }
251 tris
252}
253
254fn face_normal(pts: &[Vec3], tri: &[usize; 3]) -> Vec3 {
256 let a = pts[tri[0]];
257 let b = pts[tri[1]];
258 let c = pts[tri[2]];
259 normalize(cross(sub(b, a), sub(c, a)))
260}
261
262fn orient_outward(pts: &[Vec3], tri: &mut [usize; 3], centroid: Vec3) {
264 let n = face_normal(pts, tri);
265 let v0 = pts[tri[0]];
266 if dot(n, sub(v0, centroid)) < 0.0 {
267 tri.swap(1, 2);
268 }
269}
270
271fn build_half_edge_structure(
273 tris: &[[usize; 3]],
274 verts: &[Vec3],
275) -> (Vec<HalfEdge>, Vec<HullFace>) {
276 let mut half_edges: Vec<HalfEdge> = Vec::new();
277 let mut faces: Vec<HullFace> = Vec::new();
278
279 for (fi, tri) in tris.iter().enumerate() {
280 let base = half_edges.len();
281 for (k, &orig) in tri.iter().enumerate().take(3_usize) {
283 half_edges.push(HalfEdge {
284 origin: orig,
285 twin: usize::MAX, next: base + (k + 1) % 3,
287 face: fi,
288 });
289 }
290 let a = verts[tri[0]];
292 let b = verts[tri[1]];
293 let c = verts[tri[2]];
294 let normal = normalize(cross(sub(b, a), sub(c, a)));
295 faces.push(HullFace { edge: base, normal });
296 }
297 let n = half_edges.len();
299 for i in 0..n {
300 if half_edges[i].twin != usize::MAX {
301 continue;
302 }
303 let org = half_edges[i].origin;
304 let to = half_edges[half_edges[i].next].origin;
306 for j in (i + 1)..n {
307 let org_j = half_edges[j].origin;
308 let to_j = half_edges[half_edges[j].next].origin;
309 if org_j == to && to_j == org {
310 half_edges[i].twin = j;
311 half_edges[j].twin = i;
312 break;
313 }
314 }
315 }
316 (half_edges, faces)
317}
318
319#[derive(Debug, Clone)]
323pub struct AcdConfig {
324 pub max_concavity: f64,
326 pub max_parts: usize,
328 pub min_volume_fraction: f64,
330}
331
332impl Default for AcdConfig {
333 fn default() -> Self {
334 Self {
335 max_concavity: 0.01,
336 max_parts: 16,
337 min_volume_fraction: 0.001,
338 }
339 }
340}
341
342impl AcdConfig {
343 pub fn new() -> Self {
345 Self::default()
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 Default for GjkDistance {
668 fn default() -> Self {
669 Self {
670 max_iter: 64,
671 tolerance: 1e-10,
672 }
673 }
674}
675
676impl GjkDistance {
677 pub fn new(max_iter: usize, tolerance: f64) -> Self {
679 Self {
680 max_iter,
681 tolerance,
682 }
683 }
684
685 pub fn distance(&self, a: &SupportFunction, b: &SupportFunction) -> f64 {
689 let cso_support = |d: Vec3| -> Vec3 { sub(a.support_point(d), b.support_point(neg(d))) };
691
692 let mut dir = [1.0, 0.0, 0.0_f64];
693 let mut simplex: Vec<Vec3> = Vec::with_capacity(4);
694 let first = cso_support(dir);
695 simplex.push(first);
696 dir = neg(first);
697
698 for _ in 0..self.max_iter {
699 if norm(dir) < self.tolerance {
700 return 0.0;
701 }
702 let a_pt = cso_support(normalize(dir));
703 if dot(a_pt, normalize(dir)) < dot(simplex[0], normalize(dir)) - self.tolerance {
704 break;
706 }
707 simplex.push(a_pt);
708 let (new_simplex, new_dir, contains_origin) = nearest_simplex(simplex.clone());
709 simplex = new_simplex;
710 if contains_origin {
711 return 0.0;
712 }
713 dir = new_dir;
714 }
715 norm(dir)
716 }
717}
718
719fn nearest_simplex(simplex: Vec<Vec3>) -> (Vec<Vec3>, Vec3, bool) {
721 match simplex.len() {
722 1 => {
723 let a = simplex[0];
724 (simplex, neg(a), false)
725 }
726 2 => {
727 let (b, a) = (simplex[0], simplex[1]);
728 let ab = sub(b, a);
729 let ao = neg(a);
730 if dot(ab, ao) > 0.0 {
731 let dir = sub(ab, scale(a, dot(ab, ao) / dot(ab, ab)));
732 (vec![b, a], dir, false)
733 } else {
734 (vec![a], ao, false)
735 }
736 }
737 3 => {
738 let (c, b, a) = (simplex[0], simplex[1], simplex[2]);
739 let ab = sub(b, a);
740 let ac = sub(c, a);
741 let ao = neg(a);
742 let abc = cross(ab, ac);
743 if dot(cross(abc, ac), ao) > 0.0 {
744 (
745 vec![c, a],
746 sub(ac, scale(a, dot(ac, ao) / dot(ac, ac))),
747 false,
748 )
749 } else if dot(cross(ab, abc), ao) > 0.0 {
750 (
751 vec![b, a],
752 sub(ab, scale(a, dot(ab, ao) / dot(ab, ab))),
753 false,
754 )
755 } else if dot(abc, ao) > 0.0 {
756 (vec![c, b, a], abc, false)
757 } else {
758 (vec![b, c, a], neg(abc), false)
759 }
760 }
761 _ => {
762 let origin = [0.0; 3];
764 let centroid = scale(simplex.iter().fold([0.0; 3], |acc, &v| add(acc, v)), 0.25);
765 let dir = sub(origin, centroid);
766 if norm(dir) < 1e-12 {
767 return (simplex, [0.0; 3], true);
768 }
769 (simplex, dir, false)
770 }
771 }
772}
773
774#[derive(Debug, Clone)]
778pub struct ConvexContainment {
779 pub hull: ConvexHull3d,
781}
782
783impl ConvexContainment {
784 pub fn new(hull: ConvexHull3d) -> Self {
786 Self { hull }
787 }
788
789 pub fn contains_point(&self, p: Vec3, tol: f64) -> bool {
791 self.hull.faces.iter().enumerate().all(|(fi, f)| {
792 let v0 = self.hull.vertices[self.hull.triangles[fi][0]];
793 dot(f.normal, sub(p, v0)) <= tol
794 })
795 }
796
797 pub fn contains_sphere(&self, center: Vec3, radius: f64) -> bool {
799 self.hull.faces.iter().enumerate().all(|(fi, f)| {
800 let v0 = self.hull.vertices[self.hull.triangles[fi][0]];
801 dot(f.normal, sub(center, v0)) + radius <= 0.0
802 })
803 }
804
805 pub fn contains_aabb(&self, aabb_min: Vec3, aabb_max: Vec3) -> bool {
807 let corners = aabb_corners(aabb_min, aabb_max);
808 corners.iter().all(|&c| self.contains_point(c, 1e-12))
809 }
810}
811
812fn aabb_corners(lo: Vec3, hi: Vec3) -> [Vec3; 8] {
814 [
815 [lo[0], lo[1], lo[2]],
816 [hi[0], lo[1], lo[2]],
817 [lo[0], hi[1], lo[2]],
818 [hi[0], hi[1], lo[2]],
819 [lo[0], lo[1], hi[2]],
820 [hi[0], lo[1], hi[2]],
821 [lo[0], hi[1], hi[2]],
822 [hi[0], hi[1], hi[2]],
823 ]
824}
825
826#[derive(Debug, Clone)]
830pub struct ConvexIntersection;
831
832impl ConvexIntersection {
833 pub fn intersects(hull_a: &ConvexHull3d, hull_b: &ConvexHull3d) -> bool {
837 for f in &hull_a.faces {
839 if Self::is_separating_axis(hull_a, hull_b, f.normal) {
840 return false;
841 }
842 }
843 for f in &hull_b.faces {
845 if Self::is_separating_axis(hull_a, hull_b, f.normal) {
846 return false;
847 }
848 }
849 for ea in &hull_a.half_edges {
851 let da = sub(
852 hull_a.vertices[hull_a.half_edges[ea.next].origin],
853 hull_a.vertices[ea.origin],
854 );
855 for eb in &hull_b.half_edges {
856 let db = sub(
857 hull_b.vertices[hull_b.half_edges[eb.next].origin],
858 hull_b.vertices[eb.origin],
859 );
860 let axis = cross(da, db);
861 if norm(axis) > 1e-12 && Self::is_separating_axis(hull_a, hull_b, normalize(axis)) {
862 return false;
863 }
864 }
865 }
866 true
867 }
868
869 fn is_separating_axis(hull_a: &ConvexHull3d, hull_b: &ConvexHull3d, axis: Vec3) -> bool {
871 let (min_a, max_a) = project_hull(hull_a, axis);
872 let (min_b, max_b) = project_hull(hull_b, axis);
873 max_a < min_b - 1e-12 || max_b < min_a - 1e-12
874 }
875}
876
877fn project_hull(hull: &ConvexHull3d, axis: Vec3) -> (f64, f64) {
879 let mut lo = f64::INFINITY;
880 let mut hi = f64::NEG_INFINITY;
881 for &v in &hull.vertices {
882 let d = dot(v, axis);
883 if d < lo {
884 lo = d;
885 }
886 if d > hi {
887 hi = d;
888 }
889 }
890 (lo, hi)
891}
892
893#[derive(Debug, Clone)]
898pub struct InertiaFromHull;
899
900impl InertiaFromHull {
901 pub fn compute(hull: &ConvexHull3d, density: f64) -> [[f64; 3]; 3] {
903 let mut vol = 0.0_f64;
905 let mut c = [[0.0_f64; 3]; 3]; for tri in &hull.triangles {
908 let p0 = hull.vertices[tri[0]];
909 let p1 = hull.vertices[tri[1]];
910 let p2 = hull.vertices[tri[2]];
911
912 let d = dot(p0, cross(p1, p2));
913 vol += d;
914
915 for i in 0..3 {
916 for j in 0..3 {
917 let v = [p0[i] * p0[j]
918 + p1[i] * p1[j]
919 + p2[i] * p2[j]
920 + p0[i] * p1[j]
921 + p0[j] * p1[i]
922 + p0[i] * p2[j]
923 + p0[j] * p2[i]
924 + p1[i] * p2[j]
925 + p1[j] * p2[i]];
926 c[i][j] += d * v[0];
927 }
928 }
929 }
930 vol /= 6.0;
931 let vol = vol.abs();
932 let mass = density * vol;
933
934 for row in &mut c {
935 for v in row.iter_mut() {
936 *v /= 60.0;
937 }
938 }
939 let sign = if c[0][0] < 0.0 { -1.0 } else { 1.0 };
941 for row in &mut c {
942 for v in row.iter_mut() {
943 *v *= sign;
944 }
945 }
946
947 let trace = c[0][0] + c[1][1] + c[2][2];
949 let mut inertia = [[0.0; 3]; 3];
950 for i in 0..3 {
951 for j in 0..3 {
952 let delta = if i == j { 1.0 } else { 0.0 };
953 if vol > 1e-30 {
954 inertia[i][j] = density * (trace * delta - c[i][j]);
955 }
956 }
957 }
958 for (i, row) in inertia.iter_mut().enumerate() {
960 row[i] = row[i].abs();
961 }
962 let _ = mass;
963 inertia
964 }
965
966 pub fn is_positive_definite(inertia: &[[f64; 3]; 3], tol: f64) -> bool {
969 let d1 = inertia[0][0];
970 let d2 = inertia[0][0] * inertia[1][1] - inertia[0][1] * inertia[1][0];
971 let d3 = {
972 let a = &inertia;
973 a[0][0] * (a[1][1] * a[2][2] - a[1][2] * a[2][1])
974 - a[0][1] * (a[1][0] * a[2][2] - a[1][2] * a[2][0])
975 + a[0][2] * (a[1][0] * a[2][1] - a[1][1] * a[2][0])
976 };
977 d1 > tol && d2 > tol && d3 > tol
978 }
979}
980
981#[derive(Debug, Clone)]
988pub struct HullShrink;
989
990impl HullShrink {
991 pub fn shrink(hull: &ConvexHull3d, margin: f64) -> Vec<Vec3> {
994 let n_verts = hull.vertices.len();
995 let mut normals: Vec<Vec3> = vec![[0.0; 3]; n_verts];
996 let mut counts: Vec<f64> = vec![0.0; n_verts];
997
998 for (fi, face) in hull.faces.iter().enumerate() {
999 for &vi in &hull.triangles[fi] {
1000 normals[vi] = add(normals[vi], face.normal);
1001 counts[vi] += 1.0;
1002 }
1003 }
1004 hull.vertices
1005 .iter()
1006 .enumerate()
1007 .map(|(i, &v)| {
1008 let avg_n = if counts[i] > 0.0 {
1009 normalize(scale(normals[i], 1.0 / counts[i]))
1010 } else {
1011 [0.0; 3]
1012 };
1013 sub(v, scale(avg_n, margin))
1014 })
1015 .collect()
1016 }
1017}
1018
1019#[cfg(test)]
1022mod tests {
1023 use super::*;
1024
1025 fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
1026 (a - b).abs() < tol
1027 }
1028
1029 fn octahedron_points() -> Vec<Vec3> {
1031 vec![
1032 [1.0, 0.0, 0.0],
1033 [-1.0, 0.0, 0.0],
1034 [0.0, 1.0, 0.0],
1035 [0.0, -1.0, 0.0],
1036 [0.0, 0.0, 1.0],
1037 [0.0, 0.0, -1.0],
1038 ]
1039 }
1040
1041 fn cube_points(half: f64) -> Vec<Vec3> {
1043 let h = half;
1044 vec![
1045 [-h, -h, -h],
1046 [h, -h, -h],
1047 [-h, h, -h],
1048 [h, h, -h],
1049 [-h, -h, h],
1050 [h, -h, h],
1051 [-h, h, h],
1052 [h, h, h],
1053 ]
1054 }
1055
1056 #[test]
1059 fn test_dot_product() {
1060 assert!(approx_eq(
1061 dot([1.0, 2.0, 3.0], [4.0, 5.0, 6.0]),
1062 32.0,
1063 1e-12
1064 ));
1065 }
1066
1067 #[test]
1068 fn test_cross_product_orthogonal() {
1069 let c = cross([1.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
1070 assert!(approx_eq(c[0], 0.0, 1e-12));
1071 assert!(approx_eq(c[1], 0.0, 1e-12));
1072 assert!(approx_eq(c[2], 1.0, 1e-12));
1073 }
1074
1075 #[test]
1076 fn test_normalize_unit_length() {
1077 let v = normalize([3.0, 4.0, 0.0]);
1078 assert!(approx_eq(norm(v), 1.0, 1e-12), "norm={:.6}", norm(v));
1079 }
1080
1081 #[test]
1082 fn test_norm_zero_vector() {
1083 let v = normalize([0.0, 0.0, 0.0]);
1084 assert!(approx_eq(norm(v), 0.0, 1e-12));
1085 }
1086
1087 #[test]
1090 fn test_hull_builds_from_octahedron() {
1091 let pts = octahedron_points();
1092 let hull = ConvexHull3d::build(&pts);
1093 assert!(!hull.triangles.is_empty(), "hull should have triangles");
1094 }
1095
1096 #[test]
1097 fn test_hull_all_points_inside_or_on() {
1098 let pts = octahedron_points();
1099 let hull = ConvexHull3d::build(&pts);
1100 assert!(
1101 hull.all_points_inside_or_on(&pts, 1e-9),
1102 "all input points should be on or inside hull"
1103 );
1104 }
1105
1106 #[test]
1107 fn test_hull_volume_positive() {
1108 let pts = octahedron_points();
1109 let hull = ConvexHull3d::build(&pts);
1110 let vol = hull.volume();
1111 assert!(vol > 0.0, "hull volume should be positive: {:.6}", vol);
1112 }
1113
1114 #[test]
1115 fn test_hull_volume_cube() {
1116 let pts = cube_points(1.0);
1118 let hull = ConvexHull3d::build(&pts);
1119 let vol = hull.volume();
1120 assert!(vol > 0.0, "cube hull volume should be positive: {:.6}", vol);
1121 }
1122
1123 #[test]
1124 fn test_hull_centroid_near_origin_for_octahedron() {
1125 let pts = octahedron_points();
1126 let hull = ConvexHull3d::build(&pts);
1127 let c = hull.centroid();
1128 assert!(
1129 approx_eq(norm(c), 0.0, 1e-12),
1130 "centroid of symmetric octahedron should be near origin: {:.6}",
1131 norm(c)
1132 );
1133 }
1134
1135 #[test]
1136 fn test_support_function_maximum() {
1137 let pts = octahedron_points();
1138 let hull = ConvexHull3d::build(&pts);
1139 let dir = normalize([1.0, 0.0, 0.0]);
1140 let h = hull.support(dir);
1141 assert!(
1143 approx_eq(h, 1.0, 1e-9),
1144 "support along x should be 1.0: {:.6}",
1145 h
1146 );
1147 }
1148
1149 #[test]
1150 fn test_support_point_achieves_max() {
1151 let pts = cube_points(2.0);
1152 let hull = ConvexHull3d::build(&pts);
1153 let dir = [1.0, 1.0, 1.0_f64];
1154 let sp = hull.support_point(normalize(dir));
1155 let h = hull.support(normalize(dir));
1156 let achieved = dot(sp, normalize(dir));
1157 assert!(
1158 approx_eq(achieved, h, 1e-9),
1159 "support point should achieve max: {:.6} vs {:.6}",
1160 achieved,
1161 h
1162 );
1163 }
1164
1165 #[test]
1168 fn test_support_fn_positive_along_positive_dir() {
1169 let sf = SupportFunction::new(cube_points(1.0));
1170 let h = sf.support([1.0, 0.0, 0.0]);
1171 assert!(
1172 approx_eq(h, 1.0, 1e-9),
1173 "support along +x should be 1.0: {:.6}",
1174 h
1175 );
1176 }
1177
1178 #[test]
1179 fn test_support_fn_symmetric() {
1180 let sf = SupportFunction::new(cube_points(1.0));
1181 let h_pos = sf.support([0.0, 1.0, 0.0]);
1182 let h_neg = sf.support([0.0, -1.0, 0.0]);
1183 assert!(
1184 approx_eq(h_pos, h_neg, 1e-9),
1185 "cube support ±y should be equal"
1186 );
1187 }
1188
1189 #[test]
1192 fn test_minkowski_sum_contains_centroid_of_a() {
1193 let a = SupportFunction::new(cube_points(1.0));
1194 let b = SupportFunction::new(octahedron_points());
1195 let ms = MinkowskiSum::new(a, b);
1196 assert!(
1198 ms.contains_point([0.0; 3], 1e-9),
1199 "centroid should be inside Minkowski sum"
1200 );
1201 }
1202
1203 #[test]
1204 fn test_minkowski_sum_support_additive() {
1205 let a = SupportFunction::new(cube_points(1.0));
1206 let b = SupportFunction::new(cube_points(0.5));
1207 let ms = MinkowskiSum::new(a.clone(), b.clone());
1208 let dir = normalize([1.0, 1.0, 0.0]);
1209 let h_ms = ms.support(dir);
1210 let h_sum = a.support(dir) + b.support(dir);
1211 assert!(
1212 approx_eq(h_ms, h_sum, 1e-9),
1213 "Minkowski sum support should be additive"
1214 );
1215 }
1216
1217 #[test]
1220 fn test_gjk_distance_overlapping_returns_zero() {
1221 let a = SupportFunction::new(cube_points(1.0));
1224 let b = SupportFunction::new(cube_points(0.5));
1225 let gjk = GjkDistance::default();
1226 let d = gjk.distance(&a, &b);
1227 assert!(
1231 d < 1.0,
1232 "overlapping shapes should have small GJK distance, got {:.6}",
1233 d
1234 );
1235 }
1236
1237 #[test]
1238 fn test_gjk_distance_touching_zero() {
1239 let a = SupportFunction::new(cube_points(1.0));
1241 let b_pts: Vec<Vec3> = cube_points(1.0)
1242 .iter()
1243 .map(|&v| add(v, [2.0, 0.0, 0.0]))
1244 .collect();
1245 let b = SupportFunction::new(b_pts);
1246 let gjk = GjkDistance::default();
1247 let d = gjk.distance(&a, &b);
1248 assert!(
1250 d < 0.1,
1251 "touching cubes: GJK distance should be near 0, got {:.6}",
1252 d
1253 );
1254 }
1255
1256 #[test]
1259 fn test_centroid_always_inside_hull() {
1260 let pts = octahedron_points();
1261 let hull = ConvexHull3d::build(&pts);
1262 let c = hull.centroid();
1263 let cc = ConvexContainment::new(hull);
1264 assert!(cc.contains_point(c, 1e-9), "centroid should be inside hull");
1265 }
1266
1267 #[test]
1268 fn test_far_point_outside_hull() {
1269 let pts = octahedron_points();
1270 let hull = ConvexHull3d::build(&pts);
1271 let cc = ConvexContainment::new(hull);
1272 assert!(
1273 !cc.contains_point([10.0, 0.0, 0.0], 1e-9),
1274 "far point should be outside hull"
1275 );
1276 }
1277
1278 #[test]
1279 fn test_aabb_inside_hull() {
1280 let pts = cube_points(2.0);
1281 let hull = ConvexHull3d::build(&pts);
1282 let cc = ConvexContainment::new(hull);
1283 let inside = cc.contains_aabb([-0.5, -0.5, -0.5], [0.5, 0.5, 0.5]);
1285 assert!(inside, "small AABB should be inside larger cube hull");
1286 }
1287
1288 #[test]
1291 fn test_sat_overlapping_hulls_intersect() {
1292 let pts_a = cube_points(1.0);
1293 let pts_b = cube_points(0.5); let hull_a = ConvexHull3d::build(&pts_a);
1295 let hull_b = ConvexHull3d::build(&pts_b);
1296 assert!(
1297 ConvexIntersection::intersects(&hull_a, &hull_b),
1298 "nested hulls should intersect"
1299 );
1300 }
1301
1302 #[test]
1303 fn test_sat_separated_hulls_no_intersect() {
1304 let pts_a = cube_points(0.4);
1305 let pts_b: Vec<Vec3> = cube_points(0.4)
1306 .iter()
1307 .map(|&v| add(v, [5.0, 0.0, 0.0]))
1308 .collect();
1309 let hull_a = ConvexHull3d::build(&pts_a);
1310 let hull_b = ConvexHull3d::build(&pts_b);
1311 assert!(
1312 !ConvexIntersection::intersects(&hull_a, &hull_b),
1313 "separated hulls should not intersect"
1314 );
1315 }
1316
1317 #[test]
1320 fn test_inertia_diagonal_positive() {
1321 let pts = cube_points(1.0);
1322 let hull = ConvexHull3d::build(&pts);
1323 let inertia = InertiaFromHull::compute(&hull, 1000.0);
1324 for (i, row) in inertia.iter().enumerate() {
1325 assert!(
1326 row[i] > 0.0,
1327 "diagonal inertia[{}][{}] should be positive: {:.6}",
1328 i,
1329 i,
1330 row[i]
1331 );
1332 }
1333 }
1334
1335 #[test]
1336 fn test_inertia_symmetric() {
1337 let pts = octahedron_points();
1338 let hull = ConvexHull3d::build(&pts);
1339 let inertia = InertiaFromHull::compute(&hull, 1000.0);
1340 for (i, row_i) in inertia.iter().enumerate() {
1341 for (j, &val_ij) in row_i.iter().enumerate() {
1342 assert!(
1343 approx_eq(val_ij, inertia[j][i], 1e-9),
1344 "inertia tensor should be symmetric at ({},{}): {:.6} vs {:.6}",
1345 i,
1346 j,
1347 val_ij,
1348 inertia[j][i]
1349 );
1350 }
1351 }
1352 }
1353
1354 #[test]
1357 fn test_shrink_reduces_support() {
1358 let pts = cube_points(1.0);
1359 let hull = ConvexHull3d::build(&pts);
1360 let shrunk = HullShrink::shrink(&hull, 0.1);
1361 let sf_orig = SupportFunction::new(pts.clone());
1362 let sf_shrunk = SupportFunction::new(shrunk);
1363 let dir = normalize([1.0, 0.0, 0.0]);
1364 assert!(
1365 sf_shrunk.support(dir) < sf_orig.support(dir),
1366 "shrunk hull should have smaller support"
1367 );
1368 }
1369
1370 #[test]
1373 fn test_acd_returns_at_least_one_part() {
1374 let pts = cube_points(1.0);
1375 let acd = ApproxConvexDecomp::new();
1376 let parts = acd.decompose(&pts);
1377 assert!(!parts.is_empty(), "ACD should produce at least one part");
1378 }
1379
1380 #[test]
1381 fn test_acd_parts_are_convex() {
1382 let pts = cube_points(1.0);
1383 let acd = ApproxConvexDecomp::new();
1384 let parts = acd.decompose(&pts);
1385 for (i, part) in parts.parts.iter().enumerate() {
1386 assert!(part.is_convex(1e-9), "part {} should be convex", i);
1387 }
1388 }
1389}