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
10#![allow(dead_code)]
11
12use oxiphysics_core::math::Vec3;
13
14/// 3D Convex Hull computed via QuickHull algorithm (nalgebra Vec3 variant).
15pub struct ConvexHull3DVec {
16    /// Hull vertices (deduplicated subset of input points).
17    pub vertices: Vec<Vec3>,
18    /// Triangular faces stored as triples of vertex indices (outward-facing winding).
19    pub faces: Vec<[usize; 3]>,
20}
21
22impl ConvexHull3DVec {
23    /// Compute convex hull from a point cloud using the QuickHull algorithm.
24    ///
25    /// Returns `None` if the input is degenerate (fewer than 4 non-coplanar points).
26    pub fn build(points: &[Vec3]) -> Option<Self> {
27        if points.len() < 4 {
28            return None;
29        }
30
31        // --- Seed tetrahedron ------------------------------------------------
32        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        // Build the 4 outward-facing faces of the initial tetrahedron.
44        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        // --- Iterative QuickHull expansion -----------------------------------
58        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        // Collect unique vertex indices and remap.
118        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    /// Support function: returns the farthest point in direction `d`.
136    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    /// Volume of the convex hull via signed tetrahedral decomposition from the centroid.
149    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    /// Surface area of the convex hull (sum of triangle areas).
165    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    /// Returns `true` if point `p` is inside (or on the boundary of) the hull.
177    ///
178    /// A point is inside iff it is on the non-positive side of every face plane.
179    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    /// Outward-pointing face normal for face `face_idx`.
191    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    /// Number of hull vertices.
197    pub fn n_vertices(&self) -> usize {
198        self.vertices.len()
199    }
200
201    /// Number of triangular faces.
202    pub fn n_faces(&self) -> usize {
203        self.faces.len()
204    }
205
206    /// Get the list of unique edges as pairs of vertex indices.
207    ///
208    /// Each edge appears once, with the smaller index first.
209    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    /// Number of unique edges.
223    pub fn n_edges(&self) -> usize {
224        self.edges().len()
225    }
226
227    /// Enumerate all vertices with their indices.
228    pub fn vertex_iter(&self) -> impl Iterator<Item = (usize, &Vec3)> {
229        self.vertices.iter().enumerate()
230    }
231
232    /// Enumerate all faces with their indices.
233    pub fn face_iter(&self) -> impl Iterator<Item = (usize, &[usize; 3])> {
234        self.faces.iter().enumerate()
235    }
236
237    /// Area of a specific face.
238    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    /// Compute the AABB of the hull.
247    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    /// Compute the diameter of the hull (maximum distance between any two vertices).
267    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    /// Euler characteristic: V - E + F = 2 for convex polyhedra.
281    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    /// Signed distance from point `p` to the hull surface.
286    ///
287    /// Negative if inside, positive if outside.
288    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    /// Check if two hulls overlap (conservative test using separating axis theorem
302    /// on face normals only).
303    pub fn overlaps_sat_faces(&self, other: &ConvexHull3DVec) -> bool {
304        // Check all face normals of self
305        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; // separating axis found
311            }
312        }
313        // Check all face normals of other
314        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    // ------------------------------------------------------------------
326    // Private helpers
327    // ------------------------------------------------------------------
328
329    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
342/// Project all vertices onto an axis and return (min, max).
343fn 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
358/// Merge two convex hulls into one by computing the hull of the union of vertices.
359pub 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
365/// Compute convex hull and return both hull and the indices (into original
366/// point cloud) of vertices on the hull.
367pub fn build_with_indices(points: &[Vec3]) -> Option<(ConvexHull3DVec, Vec<usize>)> {
368    let hull = ConvexHull3DVec::build(points)?;
369    // Find which original point corresponds to each hull vertex
370    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
381/// Check if a point set is convex (i.e., all points lie on the hull).
382pub 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, // degenerate
389    }
390}
391
392// ---------------------------------------------------------------------------
393// Internal QuickHull helpers
394// ---------------------------------------------------------------------------
395
396/// Signed distance from point `p` to the plane `(normal, offset)`.
397fn point_plane_distance(p: Vec3, normal: Vec3, offset: f64) -> f64 {
398    normal.dot(&p) - offset
399}
400
401/// Compute the plane (normal, offset) for triangle (a, b, c).
402fn 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
408/// Orient face so that `interior` is strictly behind the face plane.
409fn 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
424/// Find the horizon edges of the hull given a set of visible face indices.
425fn 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
452/// Find the pair of points that are farthest apart along the x-axis (for seeding).
453fn 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
481/// Find the point farthest from the line through `points[i0]` and `points[i1]`.
482fn 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
502/// Find the farthest point from the plane, excluding the given indices.
503fn 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/// QuickHull recursive step (kept for API compatibility).
522#[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
600// ---------------------------------------------------------------------------
601// Incremental convex hull
602// ---------------------------------------------------------------------------
603
604/// Incremental convex hull that can accept new points one at a time.
605///
606/// Internally maintains the same triangulated hull structure as `ConvexHull3DVec`.
607pub struct IncrementalConvexHull {
608    /// All points ever inserted.
609    pub all_points: Vec<Vec3>,
610    /// Current hull faces.
611    pub faces: Vec<[usize; 3]>,
612    /// Current hull vertex indices (into `all_points`).
613    pub hull_verts: Vec<usize>,
614}
615
616impl IncrementalConvexHull {
617    /// Build an incremental hull from a slice of initial points.
618    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    /// Insert a new point, updating the hull if necessary.
631    pub fn insert(&mut self, p: Vec3) {
632        self.all_points.push(p);
633        // Rebuild from scratch (simple but correct)
634        if let Some(ch) = ConvexHull3DVec::build(&self.all_points) {
635            // Remap hull_verts to current all_points indices
636            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            // Map from ch.vertices index back to all_points index
644            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    /// Number of vertices on the current hull.
665    pub fn n_vertices(&self) -> usize {
666        self.hull_verts.len()
667    }
668
669    /// Number of triangular faces.
670    pub fn n_faces(&self) -> usize {
671        self.faces.len()
672    }
673
674    /// Number of unique edges.
675    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    /// Euler characteristic V - E + F = 2.
688    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    /// Volume via signed tetrahedral decomposition.
693    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
712// ---------------------------------------------------------------------------
713// Conflict graph
714// ---------------------------------------------------------------------------
715
716/// A conflict graph recording which faces can "see" which points outside the hull.
717///
718/// Used in the full incremental QuickHull algorithm to speed up point insertion.
719pub struct ConflictGraph {
720    /// Number of hull faces.
721    pub face_count: usize,
722    /// For each face, list of point indices visible from that face.
723    pub face_to_points: Vec<Vec<usize>>,
724    /// For each point, list of face indices it can see.
725    pub point_to_faces: Vec<Vec<usize>>,
726}
727
728impl ConflictGraph {
729    /// Build a conflict graph for `hull` against a set of `points`.
730    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    /// Returns the faces visible from point `pi`.
761    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    /// Returns the points visible from face `fi`.
770    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
783// ---------------------------------------------------------------------------
784// Hull inertia tensor
785// ---------------------------------------------------------------------------
786
787/// Compute the inertia tensor of a convex hull treated as a uniform solid.
788///
789/// Uses the signed-volume tetrahedral decomposition method, integrating
790/// `I = ∫ρ(r²I - r⊗r)dV` over the hull volume.
791///
792/// # Arguments
793/// * `hull`   – convex hull structure.
794/// * `mass`   – total mass of the solid.
795///
796/// # Returns
797/// The 3×3 inertia tensor as `[[f64;3\];3]`.
798pub 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        // Use covariance integrals for the tetrahedron (origin, a, b, cc)
822        // Diagonal: I_xx ≈ (a²_y+a²_z + ...) * tet_vol / 10  (approximation)
823        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
842// ---------------------------------------------------------------------------
843// Closest surface point
844// ---------------------------------------------------------------------------
845
846impl ConvexHull3DVec {
847    /// Find the closest point on the hull surface to query point `q`.
848    ///
849    /// Iterates over all triangular faces and returns the point that minimizes
850    /// distance to `q`.
851    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
870/// Closest point on triangle (a, b, c) to query point p.
871fn 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// ---------------------------------------------------------------------------
914// Tests
915// ---------------------------------------------------------------------------
916
917#[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    // --- New tests ---
1062
1063    #[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        // Unit cube surface area = 6 faces * 1.0 = 6.0
1069        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        // A cube has 12 edges; triangulated cube has 18 edges (12 original + 6 diagonals)
1082        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        // Diagonal of unit cube = sqrt(3)
1114        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        // Use two cube-like hulls that share some space to ensure
1155        // the merged point set is non-degenerate in all axes.
1156        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        // Merged hull should contain all vertices of both inputs
1162        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        // All indices should be in [0, 3]
1173        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)); // interior point
1188        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        // Volume of unit tetrahedron = 1/6
1256        assert!(
1257            (vol - 1.0 / 6.0).abs() < 0.02,
1258            "tetrahedron volume = {}",
1259            vol
1260        );
1261    }
1262
1263    // ── Incremental hull tests ──────────────────────────────────────────────
1264
1265    #[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    // ── Inertia tensor tests ──────────────────────────────────────────────────
1290
1291    #[test]
1292    fn test_hull_inertia_tensor_diagonal_symmetry() {
1293        // Symmetric hull: cube → off-diagonal inertia should be ~0
1294        let pts = unit_cube_points();
1295        let hull = ConvexHull3DVec::build(&pts).expect("build");
1296        let inertia = hull_inertia_tensor(&hull, 1.0);
1297        // Off-diagonals should be small for a symmetric body
1298        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    // ── Conflict graph tests ─────────────────────────────────────────────────
1314
1315    #[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        // For the unit cube (all on hull), no extra point should conflict any face
1321        assert_eq!(cg.face_count, hull.n_faces());
1322    }
1323
1324    // ── Closest point on hull tests ───────────────────────────────────────────
1325
1326    #[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); // inside cube
1331        let cp = hull.closest_surface_point(q);
1332        // Closest surface point should be at distance 0.5 from center
1333        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); // outside cube
1342        let cp = hull.closest_surface_point(q);
1343        // Closest point should be on the x=1 face
1344        assert!((cp.x - 1.0).abs() < 0.1, "cp.x={}", cp.x);
1345    }
1346}