1use oxiphysics_core::math::Vec3;
11
12pub struct ConvexHull3DVec {
14 pub vertices: Vec<Vec3>,
16 pub faces: Vec<[usize; 3]>,
18}
19
20impl ConvexHull3DVec {
21 pub fn build(points: &[Vec3]) -> Option<Self> {
25 if points.len() < 4 {
26 return None;
27 }
28
29 let (i0, i1) = extreme_pair(points)?;
31 let i2 = farthest_from_line(points, i0, i1)?;
32 let (normal_seed, offset_seed) = plane_from_triangle(points[i0], points[i1], points[i2]);
33 if normal_seed.norm() < 1e-12 {
34 return None;
35 }
36 let i3 = farthest_point_excluding(points, normal_seed, offset_seed, &[i0, i1, i2])?;
37 if point_plane_distance(points[i3], normal_seed, offset_seed).abs() < 1e-12 {
38 return None;
39 }
40
41 let tet = [i0, i1, i2, i3];
43 let mut hull: Vec<[usize; 3]> = Vec::new();
44 let combos: [([usize; 3], usize); 4] = [
45 ([i0, i1, i2], i3),
46 ([i0, i1, i3], i2),
47 ([i0, i2, i3], i1),
48 ([i1, i2, i3], i0),
49 ];
50 for (face, interior) in &combos {
51 let oriented = orient_face(*face, *interior, points)?;
52 hull.push(oriented);
53 }
54
55 let exterior: Vec<usize> = (0..points.len()).filter(|i| !tet.contains(i)).collect();
57
58 for &p_idx in &exterior {
59 let p = points[p_idx];
60
61 let visible: Vec<usize> = hull
62 .iter()
63 .enumerate()
64 .filter(|(_, face)| {
65 let (n, off) =
66 plane_from_triangle(points[face[0]], points[face[1]], points[face[2]]);
67 point_plane_distance(p, n, off) > 1e-10
68 })
69 .map(|(i, _)| i)
70 .collect();
71
72 if visible.is_empty() {
73 continue;
74 }
75
76 let horizon = horizon_edges(&hull, &visible);
77
78 let mut new_hull: Vec<[usize; 3]> = hull
79 .iter()
80 .enumerate()
81 .filter(|(i, _)| !visible.contains(i))
82 .map(|(_, f)| *f)
83 .collect();
84
85 for (e0, e1) in horizon {
86 let new_face = [e0, e1, p_idx];
87 let (nn, noff) = plane_from_triangle(points[e0], points[e1], points[p_idx]);
88 if nn.norm() < 1e-12 {
89 continue;
90 }
91 let interior_ref = new_hull.first().and_then(|f| {
92 f.iter()
93 .find(|&&v| v != e0 && v != e1 && v != p_idx)
94 .copied()
95 });
96 let oriented = if let Some(ref_v) = interior_ref {
97 if point_plane_distance(points[ref_v], nn, noff) > 0.0 {
98 [e1, e0, p_idx]
99 } else {
100 new_face
101 }
102 } else {
103 new_face
104 };
105 new_hull.push(oriented);
106 }
107
108 hull = new_hull;
109 }
110
111 if hull.is_empty() {
112 return None;
113 }
114
115 let mut used: Vec<usize> = hull.iter().flat_map(|f| f.iter().copied()).collect();
117 used.sort_unstable();
118 used.dedup();
119
120 let vertices: Vec<Vec3> = used.iter().map(|&i| points[i]).collect();
121 let remap = |old: usize| -> usize {
122 used.binary_search(&old)
123 .expect("element must be present in sorted list")
124 };
125 let faces: Vec<[usize; 3]> = hull
126 .iter()
127 .map(|f| [remap(f[0]), remap(f[1]), remap(f[2])])
128 .collect();
129
130 Some(Self { vertices, faces })
131 }
132
133 pub fn support(&self, d: Vec3) -> Vec3 {
135 self.vertices
136 .iter()
137 .copied()
138 .max_by(|a, b| {
139 a.dot(&d)
140 .partial_cmp(&b.dot(&d))
141 .unwrap_or(std::cmp::Ordering::Equal)
142 })
143 .unwrap_or_else(Vec3::zeros)
144 }
145
146 pub fn volume(&self) -> f64 {
148 if self.faces.is_empty() || self.vertices.is_empty() {
149 return 0.0;
150 }
151 let c = self.centroid();
152 let mut vol = 0.0f64;
153 for face in &self.faces {
154 let a = self.vertices[face[0]] - c;
155 let b = self.vertices[face[1]] - c;
156 let cc = self.vertices[face[2]] - c;
157 vol += a.dot(&b.cross(&cc));
158 }
159 (vol / 6.0).abs()
160 }
161
162 pub fn surface_area(&self) -> f64 {
164 let mut area = 0.0f64;
165 for face in &self.faces {
166 let a = self.vertices[face[0]];
167 let b = self.vertices[face[1]];
168 let c = self.vertices[face[2]];
169 area += (b - a).cross(&(c - a)).norm() * 0.5;
170 }
171 area
172 }
173
174 pub fn contains_point(&self, p: Vec3) -> bool {
178 for face in &self.faces {
179 let n = self.face_normal_raw(face);
180 let off = n.dot(&self.vertices[face[0]]);
181 if point_plane_distance(p, n, off) > 1e-9 {
182 return false;
183 }
184 }
185 true
186 }
187
188 pub fn face_normal(&self, face_idx: usize) -> Vec3 {
190 let face = &self.faces[face_idx];
191 self.face_normal_raw(face)
192 }
193
194 pub fn n_vertices(&self) -> usize {
196 self.vertices.len()
197 }
198
199 pub fn n_faces(&self) -> usize {
201 self.faces.len()
202 }
203
204 pub fn edges(&self) -> Vec<(usize, usize)> {
208 let mut edge_set = std::collections::BTreeSet::new();
209 for face in &self.faces {
210 for i in 0..3 {
211 let a = face[i];
212 let b = face[(i + 1) % 3];
213 let (lo, hi) = if a < b { (a, b) } else { (b, a) };
214 edge_set.insert((lo, hi));
215 }
216 }
217 edge_set.into_iter().collect()
218 }
219
220 pub fn n_edges(&self) -> usize {
222 self.edges().len()
223 }
224
225 pub fn vertex_iter(&self) -> impl Iterator<Item = (usize, &Vec3)> {
227 self.vertices.iter().enumerate()
228 }
229
230 pub fn face_iter(&self) -> impl Iterator<Item = (usize, &[usize; 3])> {
232 self.faces.iter().enumerate()
233 }
234
235 pub fn face_area(&self, face_idx: usize) -> f64 {
237 let face = &self.faces[face_idx];
238 let a = self.vertices[face[0]];
239 let b = self.vertices[face[1]];
240 let c = self.vertices[face[2]];
241 (b - a).cross(&(c - a)).norm() * 0.5
242 }
243
244 pub fn aabb(&self) -> (Vec3, Vec3) {
246 if self.vertices.is_empty() {
247 return (Vec3::zeros(), Vec3::zeros());
248 }
249 let mut mn = self.vertices[0];
250 let mut mx = self.vertices[0];
251 for v in &self.vertices[1..] {
252 for k in 0..3 {
253 if v[k] < mn[k] {
254 mn[k] = v[k];
255 }
256 if v[k] > mx[k] {
257 mx[k] = v[k];
258 }
259 }
260 }
261 (mn, mx)
262 }
263
264 pub fn diameter(&self) -> f64 {
266 let mut max_dist = 0.0f64;
267 for i in 0..self.vertices.len() {
268 for j in (i + 1)..self.vertices.len() {
269 let d = (self.vertices[i] - self.vertices[j]).norm();
270 if d > max_dist {
271 max_dist = d;
272 }
273 }
274 }
275 max_dist
276 }
277
278 pub fn euler_characteristic(&self) -> i64 {
280 self.n_vertices() as i64 - self.n_edges() as i64 + self.n_faces() as i64
281 }
282
283 pub fn signed_distance(&self, p: Vec3) -> f64 {
287 let mut max_signed = f64::NEG_INFINITY;
288 for face in &self.faces {
289 let n = self.face_normal_raw(face);
290 let off = n.dot(&self.vertices[face[0]]);
291 let d = point_plane_distance(p, n, off);
292 if d > max_signed {
293 max_signed = d;
294 }
295 }
296 max_signed
297 }
298
299 pub fn overlaps_sat_faces(&self, other: &ConvexHull3DVec) -> bool {
302 for fi in 0..self.n_faces() {
304 let n = self.face_normal(fi);
305 let (a_min, a_max) = project_hull_on_axis(&self.vertices, n);
306 let (b_min, b_max) = project_hull_on_axis(&other.vertices, n);
307 if a_max < b_min || b_max < a_min {
308 return false; }
310 }
311 for fi in 0..other.n_faces() {
313 let n = other.face_normal(fi);
314 let (a_min, a_max) = project_hull_on_axis(&self.vertices, n);
315 let (b_min, b_max) = project_hull_on_axis(&other.vertices, n);
316 if a_max < b_min || b_max < a_min {
317 return false;
318 }
319 }
320 true
321 }
322
323 fn centroid(&self) -> Vec3 {
328 let sum: Vec3 = self.vertices.iter().copied().sum();
329 sum / self.vertices.len() as f64
330 }
331
332 fn face_normal_raw(&self, face: &[usize; 3]) -> Vec3 {
333 let a = self.vertices[face[0]];
334 let b = self.vertices[face[1]];
335 let c = self.vertices[face[2]];
336 (b - a).cross(&(c - a)).normalize()
337 }
338}
339
340fn project_hull_on_axis(verts: &[Vec3], axis: Vec3) -> (f64, f64) {
342 let mut mn = f64::INFINITY;
343 let mut mx = f64::NEG_INFINITY;
344 for v in verts {
345 let d = v.dot(&axis);
346 if d < mn {
347 mn = d;
348 }
349 if d > mx {
350 mx = d;
351 }
352 }
353 (mn, mx)
354}
355
356pub fn merge_hulls(a: &ConvexHull3DVec, b: &ConvexHull3DVec) -> Option<ConvexHull3DVec> {
358 let mut all_points: Vec<Vec3> = a.vertices.clone();
359 all_points.extend_from_slice(&b.vertices);
360 ConvexHull3DVec::build(&all_points)
361}
362
363pub fn build_with_indices(points: &[Vec3]) -> Option<(ConvexHull3DVec, Vec<usize>)> {
366 let hull = ConvexHull3DVec::build(points)?;
367 let mut indices = Vec::with_capacity(hull.vertices.len());
369 for hv in &hull.vertices {
370 let idx = points
371 .iter()
372 .position(|p| (*p - *hv).norm() < 1e-12)
373 .unwrap_or(0);
374 indices.push(idx);
375 }
376 Some((hull, indices))
377}
378
379pub fn is_point_set_convex(points: &[Vec3]) -> bool {
381 if points.len() < 4 {
382 return true;
383 }
384 match ConvexHull3DVec::build(points) {
385 Some(hull) => hull.n_vertices() == points.len(),
386 None => true, }
388}
389
390fn point_plane_distance(p: Vec3, normal: Vec3, offset: f64) -> f64 {
396 normal.dot(&p) - offset
397}
398
399fn plane_from_triangle(a: Vec3, b: Vec3, c: Vec3) -> (Vec3, f64) {
401 let n = (b - a).cross(&(c - a));
402 let offset = n.dot(&a);
403 (n, offset)
404}
405
406fn orient_face(face: [usize; 3], interior: usize, points: &[Vec3]) -> Option<[usize; 3]> {
408 let (n, off) = plane_from_triangle(points[face[0]], points[face[1]], points[face[2]]);
409 if n.norm() < 1e-12 {
410 return None;
411 }
412 if interior == usize::MAX {
413 return Some(face);
414 }
415 if point_plane_distance(points[interior], n, off) > 0.0 {
416 Some([face[0], face[2], face[1]])
417 } else {
418 Some(face)
419 }
420}
421
422fn horizon_edges(hull: &[[usize; 3]], visible: &[usize]) -> Vec<(usize, usize)> {
424 let mut edges: Vec<(usize, usize)> = Vec::new();
425
426 for &fi in visible {
427 let face = &hull[fi];
428 let face_edges = [(face[0], face[1]), (face[1], face[2]), (face[2], face[0])];
429 for (e0, e1) in face_edges {
430 let adjacent_visible = visible.iter().any(|&other_fi| {
431 if other_fi == fi {
432 return false;
433 }
434 let other = &hull[other_fi];
435 let other_edges = [
436 (other[0], other[1]),
437 (other[1], other[2]),
438 (other[2], other[0]),
439 ];
440 other_edges.contains(&(e1, e0))
441 });
442 if !adjacent_visible {
443 edges.push((e0, e1));
444 }
445 }
446 }
447 edges
448}
449
450fn extreme_pair(points: &[Vec3]) -> Option<(usize, usize)> {
452 if points.len() < 2 {
453 return None;
454 }
455 let i0 = points
456 .iter()
457 .enumerate()
458 .min_by(|(_, a), (_, b)| a.x.partial_cmp(&b.x).unwrap_or(std::cmp::Ordering::Equal))
459 .map(|(i, _)| i)?;
460 let i1 = points
461 .iter()
462 .enumerate()
463 .max_by(|(_, a), (_, b)| a.x.partial_cmp(&b.x).unwrap_or(std::cmp::Ordering::Equal))
464 .map(|(i, _)| i)?;
465 if i0 == i1 {
466 let i1y = points
467 .iter()
468 .enumerate()
469 .max_by(|(_, a), (_, b)| a.y.partial_cmp(&b.y).unwrap_or(std::cmp::Ordering::Equal))
470 .map(|(i, _)| i)?;
471 if i0 == i1y {
472 return None;
473 }
474 return Some((i0, i1y));
475 }
476 Some((i0, i1))
477}
478
479fn farthest_from_line(points: &[Vec3], i0: usize, i1: usize) -> Option<usize> {
481 let a = points[i0];
482 let b = points[i1];
483 let ab = b - a;
484 let ab_len_sq = ab.dot(&ab);
485 if ab_len_sq < 1e-24 {
486 return None;
487 }
488 points
489 .iter()
490 .enumerate()
491 .filter(|&(i, _)| i != i0 && i != i1)
492 .max_by(|(_, p), (_, q)| {
493 let dp = ((*p - a).cross(&ab)).norm_squared() / ab_len_sq;
494 let dq = ((*q - a).cross(&ab)).norm_squared() / ab_len_sq;
495 dp.partial_cmp(&dq).unwrap_or(std::cmp::Ordering::Equal)
496 })
497 .map(|(i, _)| i)
498}
499
500fn farthest_point_excluding(
502 points: &[Vec3],
503 normal: Vec3,
504 offset: f64,
505 exclude: &[usize],
506) -> Option<usize> {
507 points
508 .iter()
509 .enumerate()
510 .filter(|(i, _)| !exclude.contains(i))
511 .max_by(|(_, a), (_, b)| {
512 point_plane_distance(**a, normal, offset)
513 .partial_cmp(&point_plane_distance(**b, normal, offset))
514 .expect("operation should succeed")
515 })
516 .map(|(i, _)| i)
517}
518
519pub struct IncrementalConvexHull {
527 pub all_points: Vec<Vec3>,
529 pub faces: Vec<[usize; 3]>,
531 pub hull_verts: Vec<usize>,
533}
534
535impl IncrementalConvexHull {
536 pub fn build(points: &[Vec3]) -> Self {
538 let mut hull = Self {
539 all_points: Vec::new(),
540 faces: Vec::new(),
541 hull_verts: Vec::new(),
542 };
543 for &p in points {
544 hull.insert(p);
545 }
546 hull
547 }
548
549 pub fn insert(&mut self, p: Vec3) {
551 self.all_points.push(p);
552 if let Some(ch) = ConvexHull3DVec::build(&self.all_points) {
554 self.hull_verts = (0..self.all_points.len())
556 .filter(|&i| {
557 ch.vertices
558 .iter()
559 .any(|&v| (v - self.all_points[i]).norm() < 1e-10)
560 })
561 .collect();
562 let vert_to_orig: Vec<usize> = ch
564 .vertices
565 .iter()
566 .map(|hv| {
567 self.all_points
568 .iter()
569 .enumerate()
570 .find(|&(_, ap)| (*ap - *hv).norm() < 1e-10)
571 .map(|(i, _)| i)
572 .unwrap_or(0)
573 })
574 .collect();
575 self.faces = ch
576 .faces
577 .iter()
578 .map(|f| [vert_to_orig[f[0]], vert_to_orig[f[1]], vert_to_orig[f[2]]])
579 .collect();
580 }
581 }
582
583 pub fn n_vertices(&self) -> usize {
585 self.hull_verts.len()
586 }
587
588 pub fn n_faces(&self) -> usize {
590 self.faces.len()
591 }
592
593 pub fn n_edges(&self) -> usize {
595 let mut edges = std::collections::BTreeSet::new();
596 for f in &self.faces {
597 for k in 0..3 {
598 let a = f[k];
599 let b = f[(k + 1) % 3];
600 edges.insert((a.min(b), a.max(b)));
601 }
602 }
603 edges.len()
604 }
605
606 pub fn euler_characteristic(&self) -> i64 {
608 self.n_vertices() as i64 - self.n_edges() as i64 + self.n_faces() as i64
609 }
610
611 pub fn volume(&self) -> f64 {
613 if self.faces.is_empty() {
614 return 0.0;
615 }
616 let c = {
617 let sum: Vec3 = self.hull_verts.iter().map(|&i| self.all_points[i]).sum();
618 sum / self.hull_verts.len() as f64
619 };
620 let mut vol = 0.0f64;
621 for f in &self.faces {
622 let a = self.all_points[f[0]] - c;
623 let b = self.all_points[f[1]] - c;
624 let cc = self.all_points[f[2]] - c;
625 vol += a.dot(&b.cross(&cc));
626 }
627 (vol / 6.0).abs()
628 }
629}
630
631pub struct ConflictGraph {
639 pub face_count: usize,
641 pub face_to_points: Vec<Vec<usize>>,
643 pub point_to_faces: Vec<Vec<usize>>,
645}
646
647impl ConflictGraph {
648 pub fn new(hull: &ConvexHull3DVec, points: &[Vec3]) -> Self {
650 let n_faces = hull.n_faces();
651 let n_points = points.len();
652 let mut face_to_points = vec![Vec::new(); n_faces];
653 let mut point_to_faces = vec![Vec::new(); n_points];
654
655 for (fi, face) in hull.faces.iter().enumerate() {
656 let (n, off) = {
657 let a = hull.vertices[face[0]];
658 let b = hull.vertices[face[1]];
659 let c = hull.vertices[face[2]];
660 let nn = (b - a).cross(&(c - a));
661 let off = nn.dot(&a);
662 (nn, off)
663 };
664 for (pi, &p) in points.iter().enumerate() {
665 if nn_point_plane_distance(p, n, off) > 1e-10 {
666 face_to_points[fi].push(pi);
667 point_to_faces[pi].push(fi);
668 }
669 }
670 }
671
672 Self {
673 face_count: n_faces,
674 face_to_points,
675 point_to_faces,
676 }
677 }
678
679 pub fn faces_for_point(&self, pi: usize) -> &[usize] {
681 if pi < self.point_to_faces.len() {
682 &self.point_to_faces[pi]
683 } else {
684 &[]
685 }
686 }
687
688 pub fn points_for_face(&self, fi: usize) -> &[usize] {
690 if fi < self.face_to_points.len() {
691 &self.face_to_points[fi]
692 } else {
693 &[]
694 }
695 }
696}
697
698fn nn_point_plane_distance(p: Vec3, n: Vec3, offset: f64) -> f64 {
699 n.dot(&p) - offset
700}
701
702pub fn hull_inertia_tensor(hull: &ConvexHull3DVec, mass: f64) -> [[f64; 3]; 3] {
718 let vol = hull.volume();
719 if vol < 1e-12 {
720 return [[0.0; 3]; 3];
721 }
722 let density = mass / vol;
723 let c = {
724 let sum: Vec3 = hull.vertices.iter().copied().sum();
725 sum / hull.vertices.len() as f64
726 };
727 let mut i_xx = 0.0f64;
728 let mut i_yy = 0.0f64;
729 let mut i_zz = 0.0f64;
730 let mut i_xy = 0.0f64;
731 let mut i_xz = 0.0f64;
732 let mut i_yz = 0.0f64;
733
734 for face in &hull.faces {
735 let a = hull.vertices[face[0]] - c;
736 let b = hull.vertices[face[1]] - c;
737 let cc = hull.vertices[face[2]] - c;
738 let tet_vol = a.dot(&b.cross(&cc)) / 6.0;
739
740 let coeff = tet_vol / 10.0;
743 for p in &[a, b, cc] {
744 i_xx += coeff * (p.y * p.y + p.z * p.z);
745 i_yy += coeff * (p.x * p.x + p.z * p.z);
746 i_zz += coeff * (p.x * p.x + p.y * p.y);
747 i_xy -= coeff * p.x * p.y;
748 i_xz -= coeff * p.x * p.z;
749 i_yz -= coeff * p.y * p.z;
750 }
751 }
752
753 let s = density;
754 [
755 [i_xx * s, i_xy * s, i_xz * s],
756 [i_xy * s, i_yy * s, i_yz * s],
757 [i_xz * s, i_yz * s, i_zz * s],
758 ]
759}
760
761impl ConvexHull3DVec {
766 pub fn closest_surface_point(&self, q: Vec3) -> Vec3 {
771 let mut best_dist = f64::INFINITY;
772 let mut best_pt = q;
773
774 for face in &self.faces {
775 let a = self.vertices[face[0]];
776 let b = self.vertices[face[1]];
777 let c = self.vertices[face[2]];
778 let cp = closest_point_on_tri(q, a, b, c);
779 let d = (cp - q).norm();
780 if d < best_dist {
781 best_dist = d;
782 best_pt = cp;
783 }
784 }
785 best_pt
786 }
787}
788
789fn closest_point_on_tri(p: Vec3, a: Vec3, b: Vec3, c: Vec3) -> Vec3 {
791 let ab = b - a;
792 let ac = c - a;
793 let ap = p - a;
794 let d1 = ab.dot(&ap);
795 let d2 = ac.dot(&ap);
796 if d1 <= 0.0 && d2 <= 0.0 {
797 return a;
798 }
799 let bp = p - b;
800 let d3 = ab.dot(&bp);
801 let d4 = ac.dot(&bp);
802 if d3 >= 0.0 && d4 <= d3 {
803 return b;
804 }
805 let vc = d1 * d4 - d3 * d2;
806 if vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0 {
807 let v = d1 / (d1 - d3);
808 return a + ab * v;
809 }
810 let cp = p - c;
811 let d5 = ab.dot(&cp);
812 let d6 = ac.dot(&cp);
813 if d6 >= 0.0 && d5 <= d6 {
814 return c;
815 }
816 let vb = d5 * d2 - d1 * d6;
817 if vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0 {
818 let w = d2 / (d2 - d6);
819 return a + ac * w;
820 }
821 let va = d3 * d6 - d5 * d4;
822 if va <= 0.0 && (d4 - d3) >= 0.0 && (d5 - d6) >= 0.0 {
823 let w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
824 return b + (c - b) * w;
825 }
826 let denom = 1.0 / (va + vb + vc);
827 let v = vb * denom;
828 let w = vc * denom;
829 a + ab * v + ac * w
830}
831
832#[cfg(test)]
837mod tests {
838 use super::*;
839
840 fn tetrahedron_points() -> Vec<Vec3> {
841 vec![
842 Vec3::new(0.0, 0.0, 0.0),
843 Vec3::new(1.0, 0.0, 0.0),
844 Vec3::new(0.0, 1.0, 0.0),
845 Vec3::new(0.0, 0.0, 1.0),
846 ]
847 }
848
849 fn unit_cube_points() -> Vec<Vec3> {
850 vec![
851 Vec3::new(0.0, 0.0, 0.0),
852 Vec3::new(1.0, 0.0, 0.0),
853 Vec3::new(0.0, 1.0, 0.0),
854 Vec3::new(1.0, 1.0, 0.0),
855 Vec3::new(0.0, 0.0, 1.0),
856 Vec3::new(1.0, 0.0, 1.0),
857 Vec3::new(0.0, 1.0, 1.0),
858 Vec3::new(1.0, 1.0, 1.0),
859 ]
860 }
861
862 #[test]
863 fn test_hull_tetrahedron() {
864 let pts = tetrahedron_points();
865 let hull = ConvexHull3DVec::build(&pts).expect("should build");
866 assert_eq!(
867 hull.n_faces(),
868 4,
869 "tetrahedron should have 4 faces, got {}",
870 hull.n_faces()
871 );
872 }
873
874 #[test]
875 fn test_hull_cube() {
876 let pts = unit_cube_points();
877 let hull = ConvexHull3DVec::build(&pts).expect("should build cube hull");
878 assert_eq!(
879 hull.n_faces(),
880 12,
881 "unit cube should have 12 triangular faces, got {}",
882 hull.n_faces()
883 );
884 }
885
886 #[test]
887 fn test_hull_sphere_points() {
888 let n = 50usize;
889 let pts: Vec<Vec3> = (0..n)
890 .map(|i| {
891 let theta = std::f64::consts::PI * (i as f64) / (n as f64 / 2.0);
892 let phi = 2.0 * std::f64::consts::PI * (i as f64) / (n as f64);
893 Vec3::new(
894 theta.sin() * phi.cos(),
895 theta.sin() * phi.sin(),
896 theta.cos(),
897 )
898 })
899 .collect();
900
901 let hull = ConvexHull3DVec::build(&pts).expect("should build sphere hull");
902 for v in &hull.vertices {
903 let r = v.norm();
904 assert!(
905 (r - 1.0).abs() < 1e-9,
906 "hull vertex not on unit sphere: norm={}",
907 r
908 );
909 }
910 assert!(hull.n_faces() >= 4, "too few faces: {}", hull.n_faces());
911 }
912
913 #[test]
914 fn test_hull_support_x() {
915 let pts = unit_cube_points();
916 let hull = ConvexHull3DVec::build(&pts).expect("build");
917 let sp = hull.support(Vec3::new(1.0, 0.0, 0.0));
918 assert!(
919 (sp.x - 1.0).abs() < 1e-9,
920 "support in +X should have x=1, got x={}",
921 sp.x
922 );
923 }
924
925 #[test]
926 fn test_hull_contains_interior() {
927 let pts = unit_cube_points();
928 let hull = ConvexHull3DVec::build(&pts).expect("build");
929 let centroid: Vec3 = hull.vertices.iter().copied().sum::<Vec3>() / hull.n_vertices() as f64;
930 assert!(
931 hull.contains_point(centroid),
932 "centroid should be inside the hull"
933 );
934 }
935
936 #[test]
937 fn test_hull_exterior_point() {
938 let pts = unit_cube_points();
939 let hull = ConvexHull3DVec::build(&pts).expect("build");
940 let outside = Vec3::new(5.0, 5.0, 5.0);
941 assert!(
942 !hull.contains_point(outside),
943 "point far outside should not be contained"
944 );
945 }
946
947 #[test]
948 fn test_hull_volume_cube() {
949 let pts = unit_cube_points();
950 let hull = ConvexHull3DVec::build(&pts).expect("build");
951 let vol = hull.volume();
952 assert!(
953 (vol - 1.0).abs() < 0.05,
954 "unit cube volume should be ~1.0, got {}",
955 vol
956 );
957 }
958
959 #[test]
960 fn test_hull_degenerate_coplanar() {
961 let pts = vec![
962 Vec3::new(0.0, 0.0, 0.0),
963 Vec3::new(1.0, 0.0, 0.0),
964 Vec3::new(0.0, 1.0, 0.0),
965 Vec3::new(1.0, 1.0, 0.0),
966 ];
967 match ConvexHull3DVec::build(&pts) {
968 None => {}
969 Some(hull) => {
970 let vol = hull.volume();
971 assert!(
972 vol < 1e-6,
973 "coplanar hull should have ~0 volume, got {}",
974 vol
975 );
976 }
977 }
978 }
979
980 #[test]
983 fn test_hull_surface_area_cube() {
984 let pts = unit_cube_points();
985 let hull = ConvexHull3DVec::build(&pts).expect("build");
986 let sa = hull.surface_area();
987 assert!(
989 (sa - 6.0).abs() < 0.1,
990 "unit cube SA should be ~6.0, got {}",
991 sa
992 );
993 }
994
995 #[test]
996 fn test_hull_edges_cube() {
997 let pts = unit_cube_points();
998 let hull = ConvexHull3DVec::build(&pts).expect("build");
999 let edges = hull.edges();
1000 assert!(
1002 edges.len() >= 12,
1003 "cube should have >= 12 edges, got {}",
1004 edges.len()
1005 );
1006 }
1007
1008 #[test]
1009 fn test_hull_euler_characteristic() {
1010 let pts = unit_cube_points();
1011 let hull = ConvexHull3DVec::build(&pts).expect("build");
1012 let chi = hull.euler_characteristic();
1013 assert_eq!(
1014 chi, 2,
1015 "Euler characteristic should be 2 for convex polyhedron, got {}",
1016 chi
1017 );
1018 }
1019
1020 #[test]
1021 fn test_hull_euler_characteristic_tetrahedron() {
1022 let pts = tetrahedron_points();
1023 let hull = ConvexHull3DVec::build(&pts).expect("build");
1024 assert_eq!(hull.euler_characteristic(), 2);
1025 }
1026
1027 #[test]
1028 fn test_hull_diameter_cube() {
1029 let pts = unit_cube_points();
1030 let hull = ConvexHull3DVec::build(&pts).expect("build");
1031 let d = hull.diameter();
1032 assert!((d - 3.0_f64.sqrt()).abs() < 1e-9, "diameter={}", d);
1034 }
1035
1036 #[test]
1037 fn test_hull_aabb_cube() {
1038 let pts = unit_cube_points();
1039 let hull = ConvexHull3DVec::build(&pts).expect("build");
1040 let (mn, mx) = hull.aabb();
1041 for k in 0..3 {
1042 assert!(mn[k] >= -1e-9, "min[{}] = {}", k, mn[k]);
1043 assert!((mx[k] - 1.0).abs() < 1e-9, "max[{}] = {}", k, mx[k]);
1044 }
1045 }
1046
1047 #[test]
1048 fn test_hull_signed_distance_inside() {
1049 let pts = unit_cube_points();
1050 let hull = ConvexHull3DVec::build(&pts).expect("build");
1051 let sd = hull.signed_distance(Vec3::new(0.5, 0.5, 0.5));
1052 assert!(
1053 sd < 1e-9,
1054 "centroid should have non-positive signed distance, got {}",
1055 sd
1056 );
1057 }
1058
1059 #[test]
1060 fn test_hull_signed_distance_outside() {
1061 let pts = unit_cube_points();
1062 let hull = ConvexHull3DVec::build(&pts).expect("build");
1063 let sd = hull.signed_distance(Vec3::new(5.0, 5.0, 5.0));
1064 assert!(
1065 sd > 0.0,
1066 "point outside should have positive signed distance, got {}",
1067 sd
1068 );
1069 }
1070
1071 #[test]
1072 fn test_merge_hulls() {
1073 let pts_a = unit_cube_points();
1076 let pts_b: Vec<Vec3> = pts_a.iter().map(|p| p + Vec3::new(0.5, 0.0, 0.0)).collect();
1077 let hull_a = ConvexHull3DVec::build(&pts_a).expect("build A");
1078 let hull_b = ConvexHull3DVec::build(&pts_b).expect("build B");
1079 let merged = merge_hulls(&hull_a, &hull_b).expect("merge");
1080 assert!(merged.n_vertices() >= hull_a.n_vertices());
1082 assert!(merged.volume() >= hull_a.volume() - 1e-6);
1083 }
1084
1085 #[test]
1086 fn test_build_with_indices() {
1087 let pts = tetrahedron_points();
1088 let (hull, indices) = build_with_indices(&pts).expect("build");
1089 assert_eq!(hull.n_vertices(), 4);
1090 assert_eq!(indices.len(), 4);
1091 for &idx in &indices {
1093 assert!(idx < 4, "index {} out of range", idx);
1094 }
1095 }
1096
1097 #[test]
1098 fn test_is_point_set_convex_cube() {
1099 let pts = unit_cube_points();
1100 assert!(is_point_set_convex(&pts));
1101 }
1102
1103 #[test]
1104 fn test_is_point_set_convex_with_interior() {
1105 let mut pts = unit_cube_points();
1106 pts.push(Vec3::new(0.5, 0.5, 0.5)); assert!(!is_point_set_convex(&pts));
1108 }
1109
1110 #[test]
1111 fn test_face_area_tetrahedron() {
1112 let pts = tetrahedron_points();
1113 let hull = ConvexHull3DVec::build(&pts).expect("build");
1114 for fi in 0..hull.n_faces() {
1115 let area = hull.face_area(fi);
1116 assert!(area > 0.0, "face {} has zero area", fi);
1117 }
1118 }
1119
1120 #[test]
1121 fn test_vertex_iter() {
1122 let pts = tetrahedron_points();
1123 let hull = ConvexHull3DVec::build(&pts).expect("build");
1124 let verts: Vec<_> = hull.vertex_iter().collect();
1125 assert_eq!(verts.len(), 4);
1126 }
1127
1128 #[test]
1129 fn test_face_iter() {
1130 let pts = tetrahedron_points();
1131 let hull = ConvexHull3DVec::build(&pts).expect("build");
1132 let faces: Vec<_> = hull.face_iter().collect();
1133 assert_eq!(faces.len(), 4);
1134 }
1135
1136 #[test]
1137 fn test_overlaps_sat_faces_overlapping() {
1138 let pts_a = unit_cube_points();
1139 let pts_b: Vec<Vec3> = pts_a.iter().map(|p| p + Vec3::new(0.5, 0.0, 0.0)).collect();
1140 let hull_a = ConvexHull3DVec::build(&pts_a).expect("build A");
1141 let hull_b = ConvexHull3DVec::build(&pts_b).expect("build B");
1142 assert!(hull_a.overlaps_sat_faces(&hull_b));
1143 }
1144
1145 #[test]
1146 fn test_overlaps_sat_faces_separated() {
1147 let pts_a = unit_cube_points();
1148 let pts_b: Vec<Vec3> = pts_a
1149 .iter()
1150 .map(|p| p + Vec3::new(10.0, 0.0, 0.0))
1151 .collect();
1152 let hull_a = ConvexHull3DVec::build(&pts_a).expect("build A");
1153 let hull_b = ConvexHull3DVec::build(&pts_b).expect("build B");
1154 assert!(!hull_a.overlaps_sat_faces(&hull_b));
1155 }
1156
1157 #[test]
1158 fn test_n_edges_tetrahedron() {
1159 let pts = tetrahedron_points();
1160 let hull = ConvexHull3DVec::build(&pts).expect("build");
1161 assert_eq!(
1162 hull.n_edges(),
1163 6,
1164 "tetrahedron has 6 edges, got {}",
1165 hull.n_edges()
1166 );
1167 }
1168
1169 #[test]
1170 fn test_hull_volume_tetrahedron() {
1171 let pts = tetrahedron_points();
1172 let hull = ConvexHull3DVec::build(&pts).expect("build");
1173 let vol = hull.volume();
1174 assert!(
1176 (vol - 1.0 / 6.0).abs() < 0.02,
1177 "tetrahedron volume = {}",
1178 vol
1179 );
1180 }
1181
1182 #[test]
1185 fn test_incremental_hull_cube() {
1186 let pts = unit_cube_points();
1187 let hull = IncrementalConvexHull::build(&pts);
1188 assert!(hull.n_vertices() == 8, "cube has 8 vertices");
1189 assert!(hull.n_faces() >= 12, "cube has >= 12 triangular faces");
1190 }
1191
1192 #[test]
1193 fn test_incremental_hull_volume() {
1194 let pts = unit_cube_points();
1195 let hull = IncrementalConvexHull::build(&pts);
1196 let vol = hull.volume();
1197 assert!((vol - 1.0).abs() < 0.1, "volume={}", vol);
1198 }
1199
1200 #[test]
1201 fn test_incremental_hull_euler() {
1202 let pts = unit_cube_points();
1203 let hull = IncrementalConvexHull::build(&pts);
1204 let chi = hull.euler_characteristic();
1205 assert_eq!(chi, 2, "Euler characteristic={}", chi);
1206 }
1207
1208 #[test]
1211 fn test_hull_inertia_tensor_diagonal_symmetry() {
1212 let pts = unit_cube_points();
1214 let hull = ConvexHull3DVec::build(&pts).expect("build");
1215 let inertia = hull_inertia_tensor(&hull, 1.0);
1216 assert!(inertia[0][1].abs() < 0.1, "I_xy={}", inertia[0][1]);
1218 assert!(inertia[0][2].abs() < 0.1, "I_xz={}", inertia[0][2]);
1219 assert!(inertia[1][2].abs() < 0.1, "I_yz={}", inertia[1][2]);
1220 }
1221
1222 #[test]
1223 fn test_hull_inertia_tensor_positive_diagonal() {
1224 let pts = unit_cube_points();
1225 let hull = ConvexHull3DVec::build(&pts).expect("build");
1226 let inertia = hull_inertia_tensor(&hull, 1.0);
1227 assert!(inertia[0][0] > 0.0, "I_xx={}", inertia[0][0]);
1228 assert!(inertia[1][1] > 0.0, "I_yy={}", inertia[1][1]);
1229 assert!(inertia[2][2] > 0.0, "I_zz={}", inertia[2][2]);
1230 }
1231
1232 #[test]
1235 fn test_conflict_graph_build() {
1236 let pts = unit_cube_points();
1237 let hull = ConvexHull3DVec::build(&pts).expect("build");
1238 let cg = ConflictGraph::new(&hull, &pts);
1239 assert_eq!(cg.face_count, hull.n_faces());
1241 }
1242
1243 #[test]
1246 fn test_closest_point_on_hull_inside() {
1247 let pts = unit_cube_points();
1248 let hull = ConvexHull3DVec::build(&pts).expect("build");
1249 let q = Vec3::new(0.5, 0.5, 0.5); let cp = hull.closest_surface_point(q);
1251 let dist = (cp - q).norm();
1253 assert!(dist < 0.6, "dist to surface from center={}", dist);
1254 }
1255
1256 #[test]
1257 fn test_closest_point_on_hull_outside() {
1258 let pts = unit_cube_points();
1259 let hull = ConvexHull3DVec::build(&pts).expect("build");
1260 let q = Vec3::new(2.0, 0.5, 0.5); let cp = hull.closest_surface_point(q);
1262 assert!((cp.x - 1.0).abs() < 0.1, "cp.x={}", cp.x);
1264 }
1265}