use std::collections::HashMap;
use crate::error::{Error, Result};
use crate::math::{BoundingBox, Vec3};
use super::geom::REL_TOL;
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Polyhedron {
vertices: Vec<Vec3>,
faces: Vec<Vec<u32>>,
tris: Vec<[u32; 3]>,
bbox: BoundingBox,
closed: bool,
}
impl Polyhedron {
pub fn new(vertices: Vec<Vec3>, faces: Vec<Vec<u32>>) -> Result<Self> {
let nv = vertices.len();
let mut tris = Vec::new();
for (i, f) in faces.iter().enumerate() {
if f.len() < 3 {
return Err(Error::Invalid(format!(
"face {i} has fewer than 3 vertices"
)));
}
if let Some(&bad) = f.iter().find(|&&k| k as usize >= nv) {
return Err(Error::Invalid(format!(
"face {i} references vertex {bad} of {nv}"
)));
}
for k in 1..f.len() - 1 {
tris.push([f[0], f[k], f[k + 1]]);
}
}
if let Some(v) = vertices.iter().find(|v| !v.is_finite()) {
return Err(Error::Invalid(format!("non-finite vertex {v:?}")));
}
let bbox = BoundingBox::from_points(vertices.iter().copied());
let closed = is_closed(&faces);
Ok(Polyhedron {
vertices,
faces,
tris,
bbox,
closed,
})
}
pub fn from_triangles(triangles: &[[Vec3; 3]]) -> Result<Self> {
let mut index: HashMap<[u64; 3], u32> = HashMap::new();
let mut vertices = Vec::new();
let mut faces = Vec::with_capacity(triangles.len());
for t in triangles {
let mut f = Vec::with_capacity(3);
for v in t {
let key = v.0.map(|c| (c + 0.0).to_bits());
let id = *index.entry(key).or_insert_with(|| {
vertices.push(*v);
(vertices.len() - 1) as u32
});
f.push(id);
}
if f[0] != f[1] && f[1] != f[2] && f[0] != f[2] {
faces.push(f);
}
}
Polyhedron::new(vertices, faces)
}
pub fn vertices(&self) -> &[Vec3] {
&self.vertices
}
pub fn faces(&self) -> &[Vec<u32>] {
&self.faces
}
pub fn triangles(&self) -> &[[u32; 3]] {
&self.tris
}
pub fn is_closed(&self) -> bool {
self.closed
}
pub fn bounding_box(&self) -> BoundingBox {
self.bbox
}
fn tri(&self, t: &[u32; 3]) -> [Vec3; 3] {
t.map(|i| self.vertices[i as usize])
}
pub fn winding_number(&self, p: Vec3) -> f64 {
let mut total = 0.0;
for t in &self.tris {
let [a, b, c] = self.tri(t).map(|v| v - p);
let (la, lb, lc) = (a.norm(), b.norm(), c.norm());
let det = a.dot(b.cross(c));
let den = la * lb * lc + a.dot(b) * lc + a.dot(c) * lb + b.dot(c) * la;
total += 2.0 * det.atan2(den);
}
total / (4.0 * core::f64::consts::PI)
}
pub fn on_surface(&self, p: Vec3) -> bool {
let scale = self
.bbox
.size()
.norm()
.max(self.bbox.max.norm())
.max(self.bbox.min.norm());
let eps = REL_TOL * scale.max(1e-300);
self.tris.iter().any(|t| {
let [a, b, c] = self.tri(t);
(closest_on_triangle(p, a, b, c) - p).norm() <= eps
})
}
pub fn contains(&self, p: Vec3) -> bool {
if !self
.bbox
.expanded(REL_TOL * self.bbox.size().norm())
.contains(p)
{
return false;
}
if self.on_surface(p) {
return true;
}
self.closed && self.winding_number(p).abs() > 0.5
}
}
fn is_closed(faces: &[Vec<u32>]) -> bool {
if faces.is_empty() {
return false;
}
let mut edges: HashMap<(u32, u32), i32> = HashMap::new();
for f in faces {
for k in 0..f.len() {
let (a, b) = (f[k], f[(k + 1) % f.len()]);
*edges.entry((a.min(b), a.max(b))).or_default() += 1;
}
}
edges.values().all(|&n| n == 2)
}
fn closest_on_triangle(p: Vec3, a: Vec3, b: Vec3, c: Vec3) -> Vec3 {
let ab = b - a;
let ac = c - a;
let ap = p - a;
let d1 = ab.dot(ap);
let d2 = ac.dot(ap);
if d1 <= 0.0 && d2 <= 0.0 {
return a;
}
let bp = p - b;
let d3 = ab.dot(bp);
let d4 = ac.dot(bp);
if d3 >= 0.0 && d4 <= d3 {
return b;
}
let vc = d1 * d4 - d3 * d2;
if vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0 {
return a + ab * (d1 / (d1 - d3));
}
let cp = p - c;
let d5 = ab.dot(cp);
let d6 = ac.dot(cp);
if d6 >= 0.0 && d5 <= d6 {
return c;
}
let vb = d5 * d2 - d1 * d6;
if vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0 {
return a + ac * (d2 / (d2 - d6));
}
let va = d3 * d6 - d5 * d4;
if va <= 0.0 && (d4 - d3) >= 0.0 && (d5 - d6) >= 0.0 {
return b + (c - b) * ((d4 - d3) / ((d4 - d3) + (d5 - d6)));
}
let denom = va + vb + vc;
if denom == 0.0 {
return a;
}
a + ab * (vb / denom) + ac * (vc / denom)
}
#[cfg(test)]
pub(crate) fn unit_cube() -> Polyhedron {
let v = (0..8)
.map(|i| Vec3::new((i & 1) as f64, ((i >> 1) & 1) as f64, ((i >> 2) & 1) as f64))
.collect();
let faces = vec![
vec![0, 2, 3, 1],
vec![4, 5, 7, 6],
vec![0, 1, 5, 4],
vec![2, 6, 7, 3],
vec![0, 4, 6, 2],
vec![1, 3, 7, 5],
];
Polyhedron::new(v, faces).unwrap()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cube() {
let c = unit_cube();
assert!(c.is_closed());
assert_eq!(c.triangles().len(), 12);
assert!((c.winding_number(Vec3::new(0.5, 0.5, 0.5)).abs() - 1.0).abs() < 1e-9);
assert!(c.contains(Vec3::new(0.2, 0.7, 0.9)));
assert!(c.contains(Vec3::new(1.0, 0.5, 0.5)));
assert!(c.contains(Vec3::new(1.0, 1.0, 1.0)));
assert!(!c.contains(Vec3::new(1.2, 0.5, 0.5)));
assert!(!c.contains(Vec3::new(-0.1, 0.5, 0.5)));
}
#[test]
fn open_surface() {
let p = Polyhedron::new(
vec![Vec3::ZERO, Vec3::unit(0), Vec3::unit(1)],
vec![vec![0, 1, 2]],
)
.unwrap();
assert!(!p.is_closed());
assert!(p.contains(Vec3::new(0.2, 0.2, 0.0)));
assert!(!p.contains(Vec3::new(0.2, 0.2, 0.1)));
assert!(Polyhedron::new(vec![Vec3::ZERO], vec![vec![0, 1, 2]]).is_err());
}
#[test]
fn soup() {
let c = unit_cube();
let tris: Vec<[Vec3; 3]> = c.triangles().iter().map(|t| c.tri(t)).collect();
let s = Polyhedron::from_triangles(&tris).unwrap();
assert_eq!(s.vertices().len(), 8);
assert!(s.is_closed());
}
}