use crate::boolean_exact::unsupported;
use axiolid_contracts::{GeomError, GeomResult};
use axiolid_core::{Point2, Point3, Vec3};
use axiolid_guarantees::Sign;
use axiolid_mesh::TriMesh;
use axiolid_predicates::{orient2d, orient3d};
use std::collections::BTreeMap;
#[derive(Debug, Clone, PartialEq)]
pub struct Polyhedron {
faces: Vec<Vec<Point3>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BooleanOp {
Union,
Intersection,
Difference,
}
impl Polyhedron {
pub fn new(faces: Vec<Vec<Point3>>) -> GeomResult<Self> {
if faces.len() < 4 {
return Err(GeomError::InvalidInput(
"a closed solid needs at least 4 faces".to_owned(),
));
}
for face in &faces {
if face.len() < 3 {
return Err(GeomError::InvalidInput(
"a face needs at least 3 vertices".to_owned(),
));
}
if face.iter().any(|p| !p.is_finite()) {
return Err(GeomError::InvalidInput(
"face vertices must be finite".to_owned(),
));
}
for &v in &face[3..] {
if orient3d(face[0], face[1], face[2], v).sign() != Some(Sign::Zero) {
return Err(GeomError::InvalidInput(
"face is not planar; no single plane to classify against".to_owned(),
));
}
}
}
Ok(Self { faces })
}
#[must_use]
pub fn faces(&self) -> &[Vec<Point3>] {
&self.faces
}
}
fn side_of_face(face: &[Point3], point: Point3) -> Option<Sign> {
orient3d(face[0], face[1], face[2], point).sign()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Containment {
Inside,
OnBoundary,
Outside,
}
fn contains(solid: &Polyhedron, point: Point3, direction: Vec3) -> Option<Containment> {
let reach = solid_reach(solid, point);
let direction = direction * reach;
let mut crossings = 0usize;
for face in solid.faces() {
match ray_crosses_face(face, point, direction)? {
RayHit::Miss => {}
RayHit::Crosses => crossings += 1,
RayHit::OnFace => return Some(Containment::OnBoundary),
}
}
Some(if crossings % 2 == 1 {
Containment::Inside
} else {
Containment::Outside
})
}
fn solid_reach(solid: &Polyhedron, point: Point3) -> f64 {
let mut furthest: f64 = 1.0;
for face in solid.faces() {
for &v in face {
furthest = furthest.max((v - point).length());
}
}
furthest * 2.0
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RayHit {
Miss,
Crosses,
OnFace,
}
fn ray_crosses_face(face: &[Point3], origin: Point3, direction: Vec3) -> Option<RayHit> {
let far = origin + direction;
let near_side = side_of_face(face, origin)?;
let far_side = side_of_face(face, far)?;
if near_side == Sign::Zero {
return if point_in_ring(face, origin)? {
Some(RayHit::OnFace)
} else {
Some(RayHit::Miss)
};
}
if near_side == far_side || far_side == Sign::Zero {
return Some(RayHit::Miss);
}
ray_enters_ring(face, origin, far)
}
fn ray_enters_ring(face: &[Point3], origin: Point3, far: Point3) -> Option<RayHit> {
let mut sign: Option<Sign> = None;
for i in 0..face.len() {
let a = face[i];
let b = face[(i + 1) % face.len()];
match orient3d(origin, far, a, b).sign()? {
Sign::Zero => return None,
s => match sign {
None => sign = Some(s),
Some(previous) if previous == s => {}
Some(_) => return Some(RayHit::Miss),
},
}
}
Some(RayHit::Crosses)
}
fn point_in_ring(face: &[Point3], point: Point3) -> Option<bool> {
let normal = face_normal(face);
let (nx, ny, nz) = (normal.x.abs(), normal.y.abs(), normal.z.abs());
let flatten = |p: Point3| {
if nx >= ny && nx >= nz {
Point2::new(p.y, p.z)
} else if ny >= nz {
Point2::new(p.z, p.x)
} else {
Point2::new(p.x, p.y)
}
};
let ring: Vec<Point2> = face.iter().map(|&v| flatten(v)).collect();
let q = flatten(point);
for i in 0..ring.len() {
let a = ring[i];
let b = ring[(i + 1) % ring.len()];
if orient2d(a, b, q).sign()? == Sign::Zero
&& q.x >= a.x.min(b.x)
&& q.x <= a.x.max(b.x)
&& q.y >= a.y.min(b.y)
&& q.y <= a.y.max(b.y)
{
return Some(true);
}
}
let mut inside = false;
for i in 0..ring.len() {
let a = ring[i];
let b = ring[(i + 1) % ring.len()];
if (a.y > q.y) != (b.y > q.y) {
let sign = orient2d(a, b, q).sign()?;
let upward = b.y > a.y;
let right = if upward {
sign == Sign::Negative
} else {
sign == Sign::Positive
};
if right {
inside = !inside;
}
}
}
Some(inside)
}
fn coplanar_normals_agree(fragment: &[Point3], other: &Polyhedron) -> GeomResult<bool> {
let centroid = centroid_of(fragment);
let ours = face_normal(fragment);
for face in other.faces() {
let on_plane = side_of_face(face, centroid)
.ok_or_else(|| unsupported("coplanar classification undecidable"))?;
if on_plane != Sign::Zero {
continue;
}
if point_in_ring(face, centroid)
.ok_or_else(|| unsupported("coplanar containment undecidable"))?
{
return Ok(ours.dot(face_normal(face)) > 0.0);
}
}
Ok(true)
}
fn face_normal(face: &[Point3]) -> Vec3 {
(face[1] - face[0]).cross(face[2] - face[0])
}
type SplitParts = (Option<Vec<Point3>>, Option<Vec<Point3>>);
fn split_polygon(polygon: &[Point3], plane: &[Point3]) -> Option<SplitParts> {
let mut signs = Vec::with_capacity(polygon.len());
for &v in polygon {
signs.push(side_of_face(plane, v)?);
}
let has_negative = signs.contains(&Sign::Negative);
let has_positive = signs.contains(&Sign::Positive);
if !has_positive {
return Some((Some(polygon.to_vec()), None));
}
if !has_negative {
return Some((None, Some(polygon.to_vec())));
}
let mut negative = Vec::new();
let mut positive = Vec::new();
for i in 0..polygon.len() {
let j = (i + 1) % polygon.len();
let (vi, vj) = (polygon[i], polygon[j]);
let (si, sj) = (signs[i], signs[j]);
match si {
Sign::Negative => negative.push(vi),
Sign::Positive => positive.push(vi),
Sign::Zero => {
negative.push(vi);
positive.push(vi);
}
_ => {}
}
let crosses = matches!(
(si, sj),
(Sign::Negative, Sign::Positive) | (Sign::Positive, Sign::Negative)
);
if crosses {
let cut = plane_crossing(plane, vi, vj)?;
negative.push(cut);
positive.push(cut);
}
}
Some((
(negative.len() >= 3).then_some(negative),
(positive.len() >= 3).then_some(positive),
))
}
fn plane_crossing(plane: &[Point3], a: Point3, b: Point3) -> Option<Point3> {
let normal = face_normal(plane);
let denominator = normal.dot(b - a);
if denominator == 0.0 {
return None;
}
let t = normal.dot(plane[0] - a) / denominator;
if !t.is_finite() {
return None;
}
Some(a + (b - a) * t)
}
pub fn boolean_polyhedra_exact(
subject: &Polyhedron,
tool: &Polyhedron,
op: BooleanOp,
) -> GeomResult<Polyhedron> {
let subject_parts = split_all(subject.faces(), tool.faces())?;
let tool_parts = split_all(tool.faces(), subject.faces())?;
let mut faces = Vec::new();
for fragment in subject_parts {
let keep = match classify_fragment(&fragment, tool)? {
Containment::Inside => matches!(op, BooleanOp::Intersection),
Containment::Outside => matches!(op, BooleanOp::Union | BooleanOp::Difference),
Containment::OnBoundary => {
if coplanar_normals_agree(&fragment, tool)? {
!matches!(op, BooleanOp::Difference)
} else {
matches!(op, BooleanOp::Difference)
}
}
};
if keep {
faces.push(fragment);
}
}
for fragment in tool_parts {
let containment = classify_fragment(&fragment, subject)?;
let keep = match op {
BooleanOp::Union => containment == Containment::Outside,
BooleanOp::Intersection | BooleanOp::Difference => containment == Containment::Inside,
};
if keep {
faces.push(if op == BooleanOp::Difference {
fragment.into_iter().rev().collect()
} else {
fragment
});
}
}
if faces.len() < 4 {
return Err(unsupported("boolean produced no closed solid"));
}
Polyhedron::new(faces)
}
fn split_all(faces: &[Vec<Point3>], planes: &[Vec<Point3>]) -> GeomResult<Vec<Vec<Point3>>> {
let mut current: Vec<Vec<Point3>> = faces.to_vec();
for plane in planes {
let mut next = Vec::with_capacity(current.len());
for polygon in current {
let (negative, positive) = split_polygon(&polygon, plane).ok_or_else(|| {
unsupported("face not splittable exactly against an operand plane")
})?;
next.extend(negative);
next.extend(positive);
}
current = next;
}
Ok(current)
}
fn classify_fragment(fragment: &[Point3], other: &Polyhedron) -> GeomResult<Containment> {
let centroid = centroid_of(fragment);
for direction in probe_directions() {
if let Some(containment) = contains(other, centroid, direction) {
return Ok(containment);
}
}
Err(unsupported(
"every probe direction met a vertex or edge exactly",
))
}
fn centroid_of(polygon: &[Point3]) -> Point3 {
let mut sum = Vec3::new(0.0, 0.0, 0.0);
for &v in polygon {
sum += v - Point3::new(0.0, 0.0, 0.0);
}
Point3::new(0.0, 0.0, 0.0) + sum / polygon.len() as f64
}
fn probe_directions() -> [Vec3; 4] {
[
Vec3::new(0.577_215_664_9, 0.313_724_518_3, 0.144_729_885_8),
Vec3::new(0.211_324_865_4, 0.788_675_134_6, 0.366_025_403_8),
Vec3::new(0.867_513_459_5, 0.132_486_540_5, 0.539_189_129_1),
Vec3::new(0.404_508_497_2, 0.595_491_502_8, 0.951_056_516_3),
]
}
#[must_use]
pub fn triangulate(solid: &Polyhedron) -> TriMesh {
let mut positions: Vec<Point3> = Vec::new();
let mut indices = Vec::new();
let mut lookup: BTreeMap<[u64; 3], u32> = BTreeMap::new();
for face in solid.faces() {
let ring: Vec<u32> = face
.iter()
.map(|&p| {
let key = [p.x.to_bits(), p.y.to_bits(), p.z.to_bits()];
let next = u32::try_from(positions.len()).unwrap_or(u32::MAX);
*lookup.entry(key).or_insert_with(|| {
positions.push(p);
next
})
})
.collect();
for i in 1..ring.len() - 1 {
indices.extend([ring[0], ring[i], ring[i + 1]]);
}
}
TriMesh::new(positions, indices)
}