#![allow(dead_code)]
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HullFace {
pub a: usize,
pub b: usize,
pub c: usize,
}
impl HullFace {
pub fn new(a: usize, b: usize, c: usize) -> Self {
Self { a, b, c }
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct ConvexHull3D {
pub vertices: Vec<[f32; 3]>,
pub faces: Vec<HullFace>,
}
fn sub3(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
fn cross3(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
fn dot3(a: [f32; 3], b: [f32; 3]) -> f32 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn norm3(v: [f32; 3]) -> f32 {
dot3(v, v).sqrt()
}
fn face_normal(pts: &[[f32; 3]], f: &HullFace) -> [f32; 3] {
let ab = sub3(pts[f.b], pts[f.a]);
let ac = sub3(pts[f.c], pts[f.a]);
cross3(ab, ac)
}
#[allow(dead_code)]
pub fn convex_hull_3d(points: &[[f32; 3]]) -> Option<ConvexHull3D> {
if points.len() < 4 {
return None;
}
let (i0, i1, i2, i3) = find_tetrahedron(points)?;
let pts = points.to_vec();
let mut faces = initial_tet_faces(i0, i1, i2, i3, &pts);
for (i, p) in pts.iter().enumerate() {
if i == i0 || i == i1 || i == i2 || i == i3 {
continue;
}
let visible: Vec<HullFace> = faces
.iter()
.filter(|f| {
let n = face_normal(&pts, f);
let d = sub3(*p, pts[f.a]);
dot3(n, d) > 1e-8
})
.copied()
.collect();
if visible.is_empty() {
continue;
}
let horizon = horizon_edges(&visible);
faces.retain(|f| !visible.contains(f));
for (ea, eb) in horizon {
let nf = HullFace::new(ea, eb, i);
let n = face_normal(&pts, &nf);
let center = centroid_of_faces(&faces, &pts);
let d = sub3(pts[nf.a], center);
if dot3(n, d) < 0.0 {
faces.push(HullFace::new(eb, ea, i));
} else {
faces.push(nf);
}
}
}
Some(ConvexHull3D {
vertices: pts,
faces,
})
}
fn find_tetrahedron(pts: &[[f32; 3]]) -> Option<(usize, usize, usize, usize)> {
let n = pts.len();
let i0 = 0;
let i1 = (0..n).max_by(|&a, &b| {
let da = norm3(sub3(pts[a], pts[i0]));
let db = norm3(sub3(pts[b], pts[i0]));
da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
})?;
let ab = sub3(pts[i1], pts[i0]);
let i2 = (0..n).max_by(|&a, &b| {
let da = norm3(cross3(sub3(pts[a], pts[i0]), ab));
let db = norm3(cross3(sub3(pts[b], pts[i0]), ab));
da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
})?;
if norm3(cross3(sub3(pts[i2], pts[i0]), ab)) < 1e-8 {
return None; }
let ab2 = sub3(pts[i1], pts[i0]);
let ac2 = sub3(pts[i2], pts[i0]);
let normal = cross3(ab2, ac2);
let i3 = (0..n).max_by(|&a, &b| {
let da = dot3(sub3(pts[a], pts[i0]), normal).abs();
let db = dot3(sub3(pts[b], pts[i0]), normal).abs();
da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
})?;
if dot3(sub3(pts[i3], pts[i0]), normal).abs() < 1e-8 {
return None; }
Some((i0, i1, i2, i3))
}
fn initial_tet_faces(
i0: usize,
i1: usize,
i2: usize,
i3: usize,
pts: &[[f32; 3]],
) -> Vec<HullFace> {
let mut faces = vec![
HullFace::new(i0, i1, i2),
HullFace::new(i0, i1, i3),
HullFace::new(i0, i2, i3),
HullFace::new(i1, i2, i3),
];
let centroid = [
(pts[i0][0] + pts[i1][0] + pts[i2][0] + pts[i3][0]) / 4.0,
(pts[i0][1] + pts[i1][1] + pts[i2][1] + pts[i3][1]) / 4.0,
(pts[i0][2] + pts[i1][2] + pts[i2][2] + pts[i3][2]) / 4.0,
];
for f in &mut faces {
let n = face_normal(pts, f);
let d = sub3(pts[f.a], centroid);
if dot3(n, d) < 0.0 {
std::mem::swap(&mut f.a, &mut f.b);
}
}
faces
}
fn horizon_edges(visible: &[HullFace]) -> Vec<(usize, usize)> {
let mut edge_count: std::collections::HashMap<(usize, usize), usize> =
std::collections::HashMap::new();
for f in visible {
let edges = [(f.a, f.b), (f.b, f.c), (f.c, f.a)];
for (ea, eb) in edges {
let key = if ea < eb { (ea, eb) } else { (eb, ea) };
*edge_count.entry(key).or_insert(0) += 1;
}
}
edge_count
.into_iter()
.filter(|(_, c)| *c == 1)
.map(|(k, _)| k)
.collect()
}
fn centroid_of_faces(faces: &[HullFace], pts: &[[f32; 3]]) -> [f32; 3] {
if faces.is_empty() {
return [0.0; 3];
}
let mut sum = [0.0f32; 3];
for f in faces {
for j in 0..3 {
sum[j] += pts[f.a][j] + pts[f.b][j] + pts[f.c][j];
}
}
let n = (faces.len() * 3) as f32;
[sum[0] / n, sum[1] / n, sum[2] / n]
}
#[allow(dead_code)]
pub fn hull_volume(hull: &ConvexHull3D) -> f32 {
let pts = &hull.vertices;
let mut vol = 0.0f32;
for f in &hull.faces {
let a = pts[f.a];
let b = pts[f.b];
let c = pts[f.c];
vol += a[0] * (b[1] * c[2] - b[2] * c[1])
+ a[1] * (b[2] * c[0] - b[0] * c[2])
+ a[2] * (b[0] * c[1] - b[1] * c[0]);
}
(vol / 6.0).abs()
}
#[allow(dead_code)]
pub fn hull_face_count(hull: &ConvexHull3D) -> usize {
hull.faces.len()
}
#[cfg(test)]
mod tests {
use super::*;
fn cube_points() -> Vec<[f32; 3]> {
let mut pts = Vec::new();
for &x in &[0.0f32, 1.0] {
for &y in &[0.0f32, 1.0] {
for &z in &[0.0f32, 1.0] {
pts.push([x, y, z]);
}
}
}
pts
}
#[test]
fn hull_from_cube() {
let pts = cube_points();
let hull = convex_hull_3d(&pts).expect("should produce hull");
assert!(!hull.faces.is_empty());
}
#[test]
fn hull_face_count_cube() {
let pts = cube_points();
let hull = convex_hull_3d(&pts).expect("should succeed");
assert!(
hull_face_count(&hull) >= 12,
"got {}",
hull_face_count(&hull)
);
}
#[test]
fn hull_fewer_than_four_returns_none() {
let pts = vec![[0.0f32, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
assert!(convex_hull_3d(&pts).is_none());
}
#[test]
fn hull_volume_positive() {
let pts = cube_points();
let hull = convex_hull_3d(&pts).expect("should succeed");
let vol = hull_volume(&hull);
assert!(vol > 0.0, "vol={vol}");
}
#[test]
fn hull_volume_cube_approx_one() {
let pts = cube_points();
let hull = convex_hull_3d(&pts).expect("should succeed");
let vol = hull_volume(&hull);
assert!(vol > 0.5, "vol too small: {vol}");
}
#[test]
fn coplanar_returns_none() {
let pts = vec![
[0.0f32, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[1.0, 1.0, 0.0],
];
assert!(convex_hull_3d(&pts).is_none());
}
#[test]
fn tetrahedron_four_faces() {
let pts = vec![
[0.0f32, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
];
let hull = convex_hull_3d(&pts).expect("should succeed");
assert_eq!(hull.faces.len(), 4);
}
#[test]
fn hull_face_indices_in_range() {
let pts = cube_points();
let hull = convex_hull_3d(&pts).expect("should succeed");
for f in &hull.faces {
assert!(f.a < hull.vertices.len());
assert!(f.b < hull.vertices.len());
assert!(f.c < hull.vertices.len());
}
}
#[test]
fn sub3_correct() {
let a = [3.0f32, 5.0, 7.0];
let b = [1.0, 2.0, 3.0];
let r = sub3(a, b);
assert!((r[0] - 2.0).abs() < 1e-5);
}
#[test]
fn cross3_orthogonal() {
let a = [1.0f32, 0.0, 0.0];
let b = [0.0, 1.0, 0.0];
let c = cross3(a, b);
assert!((c[2] - 1.0).abs() < 1e-5);
}
}