use molgfx_math::Vec3;
#[cfg(test)]
#[path = "polyhedra_tests.rs"]
mod tests;
pub const MAX_SHELL: usize = 24;
const MERGE_DISTANCE: f32 = 1.0e-3;
const PLANE_TOLERANCE: f32 = 1.0e-4;
#[must_use]
pub fn coordination_hull(shell: &[Vec3]) -> Option<(Vec<Vec3>, Vec<u32>)> {
if shell.len() < 4 || shell.len() > MAX_SHELL {
return None;
}
let points = merge_duplicates(shell);
if points.len() < 4 {
return None;
}
if !spans_volume(&points) {
return None;
}
let (sum, count) = points
.iter()
.fold((Vec3::ZERO, 0.0f32), |(sum, count), point| {
(sum + *point, count + 1.0)
});
let centre = sum / count.max(1.0);
let mut indices = Vec::new();
for first in 0..points.len() {
for second in (first + 1)..points.len() {
for third in (second + 1)..points.len() {
let Some(face) = hull_face(&points, [first, second, third], centre) else {
continue;
};
indices.extend_from_slice(&face);
}
}
}
if indices.is_empty() {
return None;
}
Some((points, indices))
}
fn hull_face(points: &[Vec3], triple: [usize; 3], centre: Vec3) -> Option<[u32; 3]> {
let [first, second, third] = triple;
let (a, b, c) = (
*points.get(first)?,
*points.get(second)?,
*points.get(third)?,
);
let normal = (b - a).cross(c - a).try_normalize()?;
let offset = normal.dot(a);
let mut above = false;
let mut below = false;
for (index, point) in points.iter().enumerate() {
if index == first || index == second || index == third {
continue;
}
let distance = normal.dot(*point) - offset;
if distance > PLANE_TOLERANCE {
above = true;
} else if distance < -PLANE_TOLERANCE {
below = true;
}
if above && below {
return None;
}
}
let outward = if normal.dot(a - centre) >= 0.0 {
[first, second, third]
} else {
[first, third, second]
};
Some([
u32::try_from(outward[0]).ok()?,
u32::try_from(outward[1]).ok()?,
u32::try_from(outward[2]).ok()?,
])
}
fn spans_volume(points: &[Vec3]) -> bool {
let Some(&origin) = points.first() else {
return false;
};
let mut first_axis = None;
let mut plane_normal = None;
for point in points.iter().skip(1) {
let edge = *point - origin;
let Some(edge) = edge.try_normalize() else {
continue;
};
match (first_axis, plane_normal) {
(None, _) => first_axis = Some(edge),
(Some(axis), None) => {
if let Some(normal) = axis.cross(edge).try_normalize() {
plane_normal = Some(normal);
}
}
(Some(_), Some(normal)) => {
if normal.dot(edge).abs() > PLANE_TOLERANCE {
return true;
}
}
}
}
false
}
fn merge_duplicates(shell: &[Vec3]) -> Vec<Vec3> {
let mut points: Vec<Vec3> = Vec::with_capacity(shell.len());
for candidate in shell {
if !candidate.is_finite() {
continue;
}
if points
.iter()
.any(|kept| kept.distance_squared(*candidate) <= MERGE_DISTANCE * MERGE_DISTANCE)
{
continue;
}
points.push(*candidate);
}
points
}