use crate::boolean_exact::unsupported;
use crate::polyhedron_exact::{self as exact, ExactPoint, RingPosition};
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,
}
#[derive(Debug, Clone)]
enum Probe {
Float(Point3),
Exact(ExactPoint),
}
impl Probe {
fn of(fragment: &[Point3]) -> GeomResult<Self> {
let centroid = centroid_of(fragment);
if side_of_face(fragment, centroid) == Some(Sign::Zero)
&& ring_position(fragment, centroid) == Some(RingPosition::Inside)
{
return Ok(Self::Float(centroid));
}
exact::interior_point(fragment)
.map(Self::Exact)
.ok_or_else(|| {
unsupported(
"a split fragment encloses no area (collapsed ring); cannot classify it",
)
})
}
fn approx(&self) -> Point3 {
match self {
Self::Float(p) => *p,
Self::Exact(p) => exact::approx(p),
}
}
fn side(&self, face: &[Point3]) -> Option<Sign> {
match self {
Self::Float(p) => side_of_face(face, *p),
Self::Exact(p) => exact::side(face, p),
}
}
fn side_of(&self, face: &[Point3], point: Point3) -> Option<Sign> {
match self {
Self::Float(_) => side_of_face(face, point),
Self::Exact(_) => exact::side(face, &exact::exact(point)?),
}
}
fn turn(&self, far: Point3, a: Point3, b: Point3) -> Option<Sign> {
match self {
Self::Float(p) => orient3d(*p, far, a, b).sign(),
Self::Exact(p) => exact::turn(p, far, a, b),
}
}
fn ring(&self, face: &[Point3]) -> Option<RingPosition> {
match self {
Self::Float(p) => ring_position(face, *p),
Self::Exact(p) => exact::ring_position(face, p),
}
}
}
fn contains(solid: &Polyhedron, probe: &Probe, direction: Vec3) -> Option<Containment> {
let point = probe.approx();
let reach = solid_reach(solid, point);
let far = point + direction * reach;
let mut crossings = 0usize;
for face in solid.faces() {
match ray_crosses_face(face, probe, far)? {
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], probe: &Probe, far: Point3) -> Option<RayHit> {
let near_side = probe.side(face)?;
let far_side = probe.side_of(face, far)?;
if near_side == Sign::Zero {
return if probe.ring(face)? == RingPosition::Outside {
Some(RayHit::Miss)
} else {
Some(RayHit::OnFace)
};
}
if near_side == far_side || far_side == Sign::Zero {
return Some(RayHit::Miss);
}
ray_enters_ring(face, probe, far)
}
fn ray_enters_ring(face: &[Point3], probe: &Probe, 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 probe.turn(far, a, b)? {
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 ring_position(face: &[Point3], point: Point3) -> Option<RingPosition> {
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(RingPosition::OnEdge);
}
}
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(if inside {
RingPosition::Inside
} else {
RingPosition::Outside
})
}
fn coplanar_normals_agree(
fragment: &[Point3],
probe: &Probe,
other: &Polyhedron,
) -> GeomResult<bool> {
for face in other.faces() {
let on_plane = probe
.side(face)
.ok_or_else(|| unsupported("coplanar classification undecidable"))?;
if on_plane != Sign::Zero {
continue;
}
let position = probe
.ring(face)
.ok_or_else(|| unsupported("coplanar containment undecidable"))?;
if position != RingPosition::Outside {
return exact::normals_agree(fragment, face)
.ok_or_else(|| unsupported("coplanar normals undecidable"));
}
}
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 t = normal.dot(plane[0] - a) / normal.dot(b - a);
exact::plane_crossing(plane, a, b, 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 probe = Probe::of(&fragment)?;
let keep = match classify_fragment(&probe, tool)? {
Containment::Inside => matches!(op, BooleanOp::Intersection),
Containment::Outside => matches!(op, BooleanOp::Union | BooleanOp::Difference),
Containment::OnBoundary => {
if coplanar_normals_agree(&fragment, &probe, tool)? {
!matches!(op, BooleanOp::Difference)
} else {
matches!(op, BooleanOp::Difference)
}
}
};
if keep {
faces.push(fragment);
}
}
for fragment in tool_parts {
let containment = classify_fragment(&Probe::of(&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(probe: &Probe, other: &Polyhedron) -> GeomResult<Containment> {
for direction in probe_directions() {
if let Some(containment) = contains(other, probe, 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);
}
let mean = Point3::new(0.0, 0.0, 0.0) + sum / polygon.len() as f64;
let first = polygon[0];
let shared = |axis: fn(Point3) -> f64, averaged: f64| {
if polygon.iter().all(|&v| axis(v) == axis(first)) {
axis(first)
} else {
averaged
}
};
Point3::new(
shared(|p| p.x, mean.x),
shared(|p| p.y, mean.y),
shared(|p| p.z, mean.z),
)
}
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 + 0.0).to_bits(),
(p.y + 0.0).to_bits(),
(p.z + 0.0).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)
}