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