Skip to main content

oxiphysics_geometry/
quickhull.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! 3D Convex Hull via QuickHull algorithm.
5//!
6//! Provides convex hull construction, vertex/face/edge enumeration,
7//! volume and surface area computation, point-in-hull test, hull merging,
8//! and support queries.
9
10use oxiphysics_core::math::Vec3;
11
12/// 3D Convex Hull computed via QuickHull algorithm (nalgebra Vec3 variant).
13pub struct ConvexHull3DVec {
14    /// Hull vertices (deduplicated subset of input points).
15    pub vertices: Vec<Vec3>,
16    /// Triangular faces stored as triples of vertex indices (outward-facing winding).
17    pub faces: Vec<[usize; 3]>,
18}
19
20impl ConvexHull3DVec {
21    /// Compute convex hull from a point cloud using the QuickHull algorithm.
22    ///
23    /// Returns `None` if the input is degenerate (fewer than 4 non-coplanar points).
24    pub fn build(points: &[Vec3]) -> Option<Self> {
25        if points.len() < 4 {
26            return None;
27        }
28
29        // --- Seed tetrahedron ------------------------------------------------
30        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        // Build the 4 outward-facing faces of the initial tetrahedron.
42        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        // --- Iterative QuickHull expansion -----------------------------------
56        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        // Collect unique vertex indices and remap.
116        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    /// Support function: returns the farthest point in direction `d`.
134    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    /// Volume of the convex hull via signed tetrahedral decomposition from the centroid.
147    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    /// Surface area of the convex hull (sum of triangle areas).
163    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    /// Returns `true` if point `p` is inside (or on the boundary of) the hull.
175    ///
176    /// A point is inside iff it is on the non-positive side of every face plane.
177    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    /// Outward-pointing face normal for face `face_idx`.
189    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    /// Number of hull vertices.
195    pub fn n_vertices(&self) -> usize {
196        self.vertices.len()
197    }
198
199    /// Number of triangular faces.
200    pub fn n_faces(&self) -> usize {
201        self.faces.len()
202    }
203
204    /// Get the list of unique edges as pairs of vertex indices.
205    ///
206    /// Each edge appears once, with the smaller index first.
207    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    /// Number of unique edges.
221    pub fn n_edges(&self) -> usize {
222        self.edges().len()
223    }
224
225    /// Enumerate all vertices with their indices.
226    pub fn vertex_iter(&self) -> impl Iterator<Item = (usize, &Vec3)> {
227        self.vertices.iter().enumerate()
228    }
229
230    /// Enumerate all faces with their indices.
231    pub fn face_iter(&self) -> impl Iterator<Item = (usize, &[usize; 3])> {
232        self.faces.iter().enumerate()
233    }
234
235    /// Area of a specific face.
236    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    /// Compute the AABB of the hull.
245    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    /// Compute the diameter of the hull (maximum distance between any two vertices).
265    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    /// Euler characteristic: V - E + F = 2 for convex polyhedra.
279    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    /// Signed distance from point `p` to the hull surface.
284    ///
285    /// Negative if inside, positive if outside.
286    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    /// Check if two hulls overlap (conservative test using separating axis theorem
300    /// on face normals only).
301    pub fn overlaps_sat_faces(&self, other: &ConvexHull3DVec) -> bool {
302        // Check all face normals of self
303        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; // separating axis found
309            }
310        }
311        // Check all face normals of other
312        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    // ------------------------------------------------------------------
324    // Private helpers
325    // ------------------------------------------------------------------
326
327    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
340/// Project all vertices onto an axis and return (min, max).
341fn 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
356/// Merge two convex hulls into one by computing the hull of the union of vertices.
357pub 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
363/// Compute convex hull and return both hull and the indices (into original
364/// point cloud) of vertices on the hull.
365pub fn build_with_indices(points: &[Vec3]) -> Option<(ConvexHull3DVec, Vec<usize>)> {
366    let hull = ConvexHull3DVec::build(points)?;
367    // Find which original point corresponds to each hull vertex
368    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
379/// Check if a point set is convex (i.e., all points lie on the hull).
380pub 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, // degenerate
387    }
388}
389
390// ---------------------------------------------------------------------------
391// Internal QuickHull helpers
392// ---------------------------------------------------------------------------
393
394/// Signed distance from point `p` to the plane `(normal, offset)`.
395fn point_plane_distance(p: Vec3, normal: Vec3, offset: f64) -> f64 {
396    normal.dot(&p) - offset
397}
398
399/// Compute the plane (normal, offset) for triangle (a, b, c).
400fn 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
406/// Orient face so that `interior` is strictly behind the face plane.
407fn 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
422/// Find the horizon edges of the hull given a set of visible face indices.
423fn 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
450/// Find the pair of points that are farthest apart along the x-axis (for seeding).
451fn 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
479/// Find the point farthest from the line through `points[i0]` and `points[i1]`.
480fn 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
500/// Find the farthest point from the plane, excluding the given indices.
501fn 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
519// ---------------------------------------------------------------------------
520// Incremental convex hull
521// ---------------------------------------------------------------------------
522
523/// Incremental convex hull that can accept new points one at a time.
524///
525/// Internally maintains the same triangulated hull structure as `ConvexHull3DVec`.
526pub struct IncrementalConvexHull {
527    /// All points ever inserted.
528    pub all_points: Vec<Vec3>,
529    /// Current hull faces.
530    pub faces: Vec<[usize; 3]>,
531    /// Current hull vertex indices (into `all_points`).
532    pub hull_verts: Vec<usize>,
533}
534
535impl IncrementalConvexHull {
536    /// Build an incremental hull from a slice of initial points.
537    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    /// Insert a new point, updating the hull if necessary.
550    pub fn insert(&mut self, p: Vec3) {
551        self.all_points.push(p);
552        // Rebuild from scratch (simple but correct)
553        if let Some(ch) = ConvexHull3DVec::build(&self.all_points) {
554            // Remap hull_verts to current all_points indices
555            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            // Map from ch.vertices index back to all_points index
563            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    /// Number of vertices on the current hull.
584    pub fn n_vertices(&self) -> usize {
585        self.hull_verts.len()
586    }
587
588    /// Number of triangular faces.
589    pub fn n_faces(&self) -> usize {
590        self.faces.len()
591    }
592
593    /// Number of unique edges.
594    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    /// Euler characteristic V - E + F = 2.
607    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    /// Volume via signed tetrahedral decomposition.
612    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
631// ---------------------------------------------------------------------------
632// Conflict graph
633// ---------------------------------------------------------------------------
634
635/// A conflict graph recording which faces can "see" which points outside the hull.
636///
637/// Used in the full incremental QuickHull algorithm to speed up point insertion.
638pub struct ConflictGraph {
639    /// Number of hull faces.
640    pub face_count: usize,
641    /// For each face, list of point indices visible from that face.
642    pub face_to_points: Vec<Vec<usize>>,
643    /// For each point, list of face indices it can see.
644    pub point_to_faces: Vec<Vec<usize>>,
645}
646
647impl ConflictGraph {
648    /// Build a conflict graph for `hull` against a set of `points`.
649    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    /// Returns the faces visible from point `pi`.
680    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    /// Returns the points visible from face `fi`.
689    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
702// ---------------------------------------------------------------------------
703// Hull inertia tensor
704// ---------------------------------------------------------------------------
705
706/// Compute the inertia tensor of a convex hull treated as a uniform solid.
707///
708/// Uses the signed-volume tetrahedral decomposition method, integrating
709/// `I = ∫ρ(r²I - r⊗r)dV` over the hull volume.
710///
711/// # Arguments
712/// * `hull`   – convex hull structure.
713/// * `mass`   – total mass of the solid.
714///
715/// # Returns
716/// The 3×3 inertia tensor as `[[f64;3\];3]`.
717pub 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        // Use covariance integrals for the tetrahedron (origin, a, b, cc)
741        // Diagonal: I_xx ≈ (a²_y+a²_z + ...) * tet_vol / 10  (approximation)
742        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
761// ---------------------------------------------------------------------------
762// Closest surface point
763// ---------------------------------------------------------------------------
764
765impl ConvexHull3DVec {
766    /// Find the closest point on the hull surface to query point `q`.
767    ///
768    /// Iterates over all triangular faces and returns the point that minimizes
769    /// distance to `q`.
770    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
789/// Closest point on triangle (a, b, c) to query point p.
790fn 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// ---------------------------------------------------------------------------
833// Tests
834// ---------------------------------------------------------------------------
835
836#[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    // --- New tests ---
981
982    #[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        // Unit cube surface area = 6 faces * 1.0 = 6.0
988        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        // A cube has 12 edges; triangulated cube has 18 edges (12 original + 6 diagonals)
1001        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        // Diagonal of unit cube = sqrt(3)
1033        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        // Use two cube-like hulls that share some space to ensure
1074        // the merged point set is non-degenerate in all axes.
1075        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        // Merged hull should contain all vertices of both inputs
1081        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        // All indices should be in [0, 3]
1092        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)); // interior point
1107        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        // Volume of unit tetrahedron = 1/6
1175        assert!(
1176            (vol - 1.0 / 6.0).abs() < 0.02,
1177            "tetrahedron volume = {}",
1178            vol
1179        );
1180    }
1181
1182    // ── Incremental hull tests ──────────────────────────────────────────────
1183
1184    #[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    // ── Inertia tensor tests ──────────────────────────────────────────────────
1209
1210    #[test]
1211    fn test_hull_inertia_tensor_diagonal_symmetry() {
1212        // Symmetric hull: cube → off-diagonal inertia should be ~0
1213        let pts = unit_cube_points();
1214        let hull = ConvexHull3DVec::build(&pts).expect("build");
1215        let inertia = hull_inertia_tensor(&hull, 1.0);
1216        // Off-diagonals should be small for a symmetric body
1217        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    // ── Conflict graph tests ─────────────────────────────────────────────────
1233
1234    #[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        // For the unit cube (all on hull), no extra point should conflict any face
1240        assert_eq!(cg.face_count, hull.n_faces());
1241    }
1242
1243    // ── Closest point on hull tests ───────────────────────────────────────────
1244
1245    #[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); // inside cube
1250        let cp = hull.closest_surface_point(q);
1251        // Closest surface point should be at distance 0.5 from center
1252        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); // outside cube
1261        let cp = hull.closest_surface_point(q);
1262        // Closest point should be on the x=1 face
1263        assert!((cp.x - 1.0).abs() < 0.1, "cp.x={}", cp.x);
1264    }
1265}