use axiolid_contracts::{GeomError, GeomResult};
use axiolid_core::Point3;
use axiolid_guarantees::Sign;
use axiolid_mesh::TriMesh;
use axiolid_predicates::orient3d;
use std::collections::BTreeMap;
pub fn convex_hull(points: &[Point3]) -> GeomResult<TriMesh> {
for (index, point) in points.iter().enumerate() {
if !point.is_finite() {
return Err(GeomError::InvalidInput(format!(
"hull point {index} is not finite"
)));
}
}
if points.len() < 4 {
return Err(GeomError::InvalidInput(format!(
"a hull needs at least 4 points, got {}",
points.len()
)));
}
let seed = initial_tetrahedron(points)?;
let mut faces = seed_faces(&seed, points);
for (index, &point) in points.iter().enumerate() {
if seed.contains(&index) {
continue;
}
add_point(&mut faces, points, point);
}
Ok(assemble(&faces, points))
}
fn initial_tetrahedron(points: &[Point3]) -> GeomResult<[usize; 4]> {
let a = 0;
let b = (1..points.len())
.find(|&i| points[i] != points[a])
.ok_or_else(|| GeomError::Degenerate("all hull points are collinear".to_owned()))?;
let c = (0..points.len())
.find(|&i| i != a && i != b && !collinear(points[a], points[b], points[i]))
.ok_or_else(|| GeomError::Degenerate("all hull points are collinear".to_owned()))?;
let d = (0..points.len())
.find(|&i| {
i != a
&& i != b
&& i != c
&& orient3d(points[a], points[b], points[c], points[i]).sign() != Some(Sign::Zero)
})
.ok_or_else(|| GeomError::Degenerate("all hull points are coplanar".to_owned()))?;
Ok([a, b, c, d])
}
fn collinear(a: Point3, b: Point3, c: Point3) -> bool {
(b - a).cross(c - a).length_squared() == 0.0
}
fn seed_faces(seed: &[usize; 4], points: &[Point3]) -> Vec<[usize; 3]> {
let [a, b, c, d] = *seed;
let (a, b, c) =
if orient3d(points[a], points[b], points[c], points[d]).sign() == Some(Sign::Positive) {
(a, c, b)
} else {
(a, b, c)
};
vec![[a, b, c], [a, d, c], [a, b, d], [b, c, d]]
.into_iter()
.map(|face| orient_outward(face, points, d, a, b, c))
.collect()
}
fn orient_outward(
face: [usize; 3],
points: &[Point3],
d: usize,
a: usize,
b: usize,
c: usize,
) -> [usize; 3] {
let centroid = (points[a] + points[b] + points[c] + points[d]) / 4.0;
if orient3d(points[face[0]], points[face[1]], points[face[2]], centroid).sign()
== Some(Sign::Negative)
{
[face[0], face[2], face[1]]
} else {
face
}
}
fn add_point(faces: &mut Vec<[usize; 3]>, points: &[Point3], point: Point3) {
let mut visible = Vec::new();
let mut kept = Vec::new();
for &face in faces.iter() {
if sees(points, face, point) {
visible.push(face);
} else {
kept.push(face);
}
}
if visible.is_empty() {
return;
}
let mut usage: BTreeMap<(usize, usize), i32> = BTreeMap::new();
for face in &visible {
for (u, v) in edges_of(*face) {
*usage.entry((u.min(v), u.max(v))).or_insert(0) += 1;
}
}
let index = points.iter().position(|p| *p == point).unwrap_or(0);
for face in &visible {
for (u, v) in edges_of(*face) {
if usage[&(u.min(v), u.max(v))] == 1 {
kept.push([u, v, index]);
}
}
}
*faces = kept;
}
fn sees(points: &[Point3], face: [usize; 3], point: Point3) -> bool {
orient3d(points[face[0]], points[face[1]], points[face[2]], point).sign()
== Some(Sign::Negative)
}
fn edges_of(face: [usize; 3]) -> [(usize, usize); 3] {
[(face[0], face[1]), (face[1], face[2]), (face[2], face[0])]
}
fn assemble(faces: &[[usize; 3]], points: &[Point3]) -> TriMesh {
let mut remap: BTreeMap<usize, u32> = BTreeMap::new();
let mut positions = Vec::new();
let mut indices = Vec::new();
for face in faces {
for &corner in face {
let next = u32::try_from(positions.len()).unwrap_or(u32::MAX);
let slot = *remap.entry(corner).or_insert_with(|| {
positions.push(points[corner]);
next
});
indices.push(slot);
}
}
TriMesh::new(positions, indices)
}