use crate::diagnostics::{BoolFailure, BoolFailureReason, BoolOp};
use crate::error::Result;
use crate::mesh::Mesh;
use crate::triangulation::{calculate_polygon_normal, project_to_2d, triangulate_polygon};
use nalgebra::{Point3, Vector3};
use rustc_hash::FxHashMap;
use smallvec::SmallVec;
use std::cell::RefCell;
pub type TriangleVec = SmallVec<[Triangle; 4]>;
#[derive(Debug, Clone, Copy)]
pub struct Plane {
pub point: Point3<f64>,
pub normal: Vector3<f64>,
}
impl Plane {
pub fn new(point: Point3<f64>, normal: Vector3<f64>) -> Self {
Self {
point,
normal: normal.normalize(),
}
}
pub fn signed_distance(&self, point: &Point3<f64>) -> f64 {
(point - self.point).dot(&self.normal)
}
pub fn is_front(&self, point: &Point3<f64>) -> bool {
self.signed_distance(point) >= 0.0
}
}
#[derive(Debug, Clone)]
pub enum ClipResult {
AllFront(Triangle),
AllBehind,
Split(TriangleVec),
}
#[derive(Debug, Clone)]
pub struct Triangle {
pub v0: Point3<f64>,
pub v1: Point3<f64>,
pub v2: Point3<f64>,
}
impl Triangle {
#[inline]
pub fn new(v0: Point3<f64>, v1: Point3<f64>, v2: Point3<f64>) -> Self {
Self { v0, v1, v2 }
}
#[inline]
pub fn normal(&self) -> Vector3<f64> {
let edge1 = self.v1 - self.v0;
let edge2 = self.v2 - self.v0;
edge1.cross(&edge2).normalize()
}
#[inline]
pub fn cross_product(&self) -> Vector3<f64> {
let edge1 = self.v1 - self.v0;
let edge2 = self.v2 - self.v0;
edge1.cross(&edge2)
}
#[inline]
pub fn area(&self) -> f64 {
self.cross_product().norm() * 0.5
}
#[inline]
pub fn is_degenerate(&self, epsilon: f64) -> bool {
self.cross_product().try_normalize(epsilon).is_none()
}
}
const MAX_CSG_POLYGONS_PER_MESH: usize = 128;
const MAX_CSG_POLYGONS: usize = MAX_CSG_POLYGONS_PER_MESH * 2;
pub struct ClippingProcessor {
pub epsilon: f64,
failures: RefCell<Vec<BoolFailure>>,
}
#[cfg_attr(feature = "manifold-csg", allow(dead_code))]
struct BucketTri {
v: [crate::bsp_csg::Vertex; 3],
keys: [(i64, i64, i64); 3],
normal: [f64; 3],
}
#[cfg_attr(feature = "manifold-csg", allow(dead_code))]
fn merge_coplanar_bucket(
tris: &[BucketTri],
indices: &[usize],
) -> Vec<crate::bsp_csg::Polygon> {
use crate::bsp_csg::{Polygon, Vertex};
use rustc_hash::{FxHashMap, FxHashSet};
if indices.is_empty() {
return Vec::new();
}
if indices.len() == 1 {
let t = &tris[indices[0]];
return vec![Polygon::new(t.v.to_vec())];
}
let fallback_per_triangle = |bucket: &[usize]| -> Vec<Polygon> {
bucket
.iter()
.map(|&i| Polygon::new(tris[i].v.to_vec()))
.collect()
};
type Key = (i64, i64, i64);
let mut vertex_data: FxHashMap<Key, Vertex> = FxHashMap::default();
let mut edge_count: FxHashMap<(Key, Key), u32> = FxHashMap::default();
for &i in indices {
let t = &tris[i];
for k in 0..3 {
vertex_data
.entry(t.keys[k])
.or_insert_with(|| t.v[k].clone());
}
for (a, b) in [(0, 1), (1, 2), (2, 0)] {
*edge_count.entry((t.keys[a], t.keys[b])).or_insert(0) += 1;
}
}
let mut next_edge: FxHashMap<Key, Key> = FxHashMap::default();
for ((u, v), _) in edge_count
.iter()
.filter(|((u, v), _)| !edge_count.contains_key(&(*v, *u)))
{
if next_edge.insert(*u, *v).is_some() {
return fallback_per_triangle(indices);
}
}
if next_edge.is_empty() {
return fallback_per_triangle(indices);
}
let plane_normal = tris[indices[0]].normal;
let starts: Vec<Key> = next_edge.keys().copied().collect();
let mut visited: FxHashSet<Key> = FxHashSet::default();
let mut rings: Vec<Vec<Vertex>> = Vec::new();
for start in starts {
if visited.contains(&start) {
continue;
}
let mut ring: Vec<Vertex> = Vec::new();
let mut current = start;
let mut steps = 0;
loop {
if !visited.insert(current) {
break;
}
if let Some(v) = vertex_data.get(¤t) {
ring.push(v.clone());
} else {
return fallback_per_triangle(indices);
}
let next = match next_edge.get(¤t) {
Some(&n) => n,
None => break,
};
if next == start {
break;
}
current = next;
steps += 1;
if steps > vertex_data.len() {
return fallback_per_triangle(indices);
}
}
if ring.len() >= 3 {
if !ring_is_convex(&ring, plane_normal) {
return fallback_per_triangle(indices);
}
rings.push(ring);
}
}
if rings.is_empty() {
return fallback_per_triangle(indices);
}
rings
.into_iter()
.map(|r| simplify_collinear(r))
.filter(|r| r.len() >= 3)
.map(Polygon::new)
.collect()
}
#[cfg_attr(feature = "manifold-csg", allow(dead_code))]
fn simplify_collinear(
ring: Vec<crate::bsp_csg::Vertex>,
) -> Vec<crate::bsp_csg::Vertex> {
let n = ring.len();
if n < 4 {
return ring;
}
let mut keep: Vec<bool> = vec![true; n];
let mut changed = true;
while changed {
changed = false;
for i in 0..n {
if !keep[i] {
continue;
}
let prev = (1..n)
.map(|k| (i + n - k) % n)
.find(|&k| keep[k]);
let next = (1..n).map(|k| (i + k) % n).find(|&k| keep[k]);
let (prev, next) = match (prev, next) {
(Some(p), Some(n)) if p != i && n != i && p != n => (p, n),
_ => continue,
};
let a = ring[prev].pos;
let b = ring[i].pos;
let c = ring[next].pos;
let e1 = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
let e2 = [c[0] - b[0], c[1] - b[1], c[2] - b[2]];
let cross = [
e1[1] * e2[2] - e1[2] * e2[1],
e1[2] * e2[0] - e1[0] * e2[2],
e1[0] * e2[1] - e1[1] * e2[0],
];
let cross_mag =
(cross[0].powi(2) + cross[1].powi(2) + cross[2].powi(2)).sqrt();
let e1_len = (e1[0].powi(2) + e1[1].powi(2) + e1[2].powi(2)).sqrt();
let e2_len = (e2[0].powi(2) + e2[1].powi(2) + e2[2].powi(2)).sqrt();
let denom = e1_len * e2_len;
if denom < 1.0e-18 || cross_mag / denom < 1.0e-6 {
keep[i] = false;
changed = true;
}
}
}
let kept: Vec<_> = ring
.into_iter()
.zip(keep.iter())
.filter_map(|(v, k)| if *k { Some(v) } else { None })
.collect();
if kept.len() >= 3 {
kept
} else {
Vec::new()
}
}
#[cfg_attr(feature = "manifold-csg", allow(dead_code))]
fn emit_triangle(mesh: &mut Mesh, v: &[Point3<f64>; 3], normal: &Vector3<f64>) {
let base = mesh.vertex_count() as u32;
mesh.add_vertex(v[0], *normal);
mesh.add_vertex(v[1], *normal);
mesh.add_vertex(v[2], *normal);
mesh.add_triangle(base, base + 1, base + 2);
}
#[cfg_attr(feature = "manifold-csg", allow(dead_code))]
fn simplify_2d_collinear(ring: &[nalgebra::Point2<f64>]) -> Vec<nalgebra::Point2<f64>> {
let n = ring.len();
if n < 4 {
return ring.to_vec();
}
let mut keep = vec![true; n];
let mut changed = true;
while changed {
changed = false;
for i in 0..n {
if !keep[i] {
continue;
}
let prev = (1..n).map(|k| (i + n - k) % n).find(|&k| keep[k]);
let next = (1..n).map(|k| (i + k) % n).find(|&k| keep[k]);
let (prev, next) = match (prev, next) {
(Some(p), Some(n)) if p != i && n != i && p != n => (p, n),
_ => continue,
};
let a = ring[prev];
let b = ring[i];
let c = ring[next];
let e1x = b.x - a.x;
let e1y = b.y - a.y;
let e2x = c.x - b.x;
let e2y = c.y - b.y;
let cross = e1x * e2y - e1y * e2x;
let len1 = (e1x * e1x + e1y * e1y).sqrt();
let len2 = (e2x * e2x + e2y * e2y).sqrt();
let denom = len1 * len2;
if denom < 1.0e-18 || (cross.abs() / denom) < 1.0e-4 {
keep[i] = false;
changed = true;
}
}
}
ring.iter()
.zip(keep.iter())
.filter_map(|(p, k)| if *k { Some(*p) } else { None })
.collect()
}
#[cfg_attr(feature = "manifold-csg", allow(dead_code))]
fn ring_is_convex(ring: &[crate::bsp_csg::Vertex], normal: [f64; 3]) -> bool {
let n = ring.len();
if n < 3 {
return false;
}
let mut sign: i32 = 0;
for i in 0..n {
let a = ring[i].pos;
let b = ring[(i + 1) % n].pos;
let c = ring[(i + 2) % n].pos;
let e1 = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
let e2 = [c[0] - b[0], c[1] - b[1], c[2] - b[2]];
let cross = [
e1[1] * e2[2] - e1[2] * e2[1],
e1[2] * e2[0] - e1[0] * e2[2],
e1[0] * e2[1] - e1[1] * e2[0],
];
let dot = cross[0] * normal[0] + cross[1] * normal[1] + cross[2] * normal[2];
let s = if dot > 1.0e-12 {
1
} else if dot < -1.0e-12 {
-1
} else {
0
};
if s != 0 {
if sign == 0 {
sign = s;
} else if sign != s {
return false;
}
}
}
true
}
fn aabb_to_mesh(min: Point3<f64>, max: Point3<f64>) -> Mesh {
let mut mesh = Mesh::with_capacity(8, 36);
let v0 = Point3::new(min.x, min.y, min.z); let v1 = Point3::new(max.x, min.y, min.z); let v2 = Point3::new(max.x, max.y, min.z); let v3 = Point3::new(min.x, max.y, min.z); let v4 = Point3::new(min.x, min.y, max.z); let v5 = Point3::new(max.x, min.y, max.z); let v6 = Point3::new(max.x, max.y, max.z); let v7 = Point3::new(min.x, max.y, max.z);
add_triangle_to_mesh(&mut mesh, &Triangle::new(v0, v2, v1));
add_triangle_to_mesh(&mut mesh, &Triangle::new(v0, v3, v2));
add_triangle_to_mesh(&mut mesh, &Triangle::new(v4, v5, v6));
add_triangle_to_mesh(&mut mesh, &Triangle::new(v4, v6, v7));
add_triangle_to_mesh(&mut mesh, &Triangle::new(v0, v4, v7));
add_triangle_to_mesh(&mut mesh, &Triangle::new(v0, v7, v3));
add_triangle_to_mesh(&mut mesh, &Triangle::new(v1, v2, v6));
add_triangle_to_mesh(&mut mesh, &Triangle::new(v1, v6, v5));
add_triangle_to_mesh(&mut mesh, &Triangle::new(v0, v1, v5));
add_triangle_to_mesh(&mut mesh, &Triangle::new(v0, v5, v4));
add_triangle_to_mesh(&mut mesh, &Triangle::new(v3, v7, v6));
add_triangle_to_mesh(&mut mesh, &Triangle::new(v3, v6, v2));
mesh
}
impl ClippingProcessor {
#[cfg_attr(feature = "manifold-csg", allow(dead_code))]
#[inline]
fn can_run_csg_operation(polygons_a: usize, polygons_b: usize) -> bool {
if polygons_a < 4 || polygons_b < 4 {
return false;
}
if polygons_a > MAX_CSG_POLYGONS_PER_MESH || polygons_b > MAX_CSG_POLYGONS_PER_MESH {
return false;
}
polygons_a + polygons_b <= MAX_CSG_POLYGONS
}
pub fn new() -> Self {
Self {
epsilon: 1e-6,
failures: RefCell::new(Vec::new()),
}
}
pub fn take_failures(&self) -> Vec<BoolFailure> {
std::mem::take(&mut *self.failures.borrow_mut())
}
pub fn failure_count(&self) -> usize {
self.failures.borrow().len()
}
pub(crate) fn record_failure(&self, op: BoolOp, reason: BoolFailureReason) {
self.failures.borrow_mut().push(BoolFailure::new(op, reason));
}
pub fn clip_triangle(&self, triangle: &Triangle, plane: &Plane) -> ClipResult {
let d0 = plane.signed_distance(&triangle.v0);
let d1 = plane.signed_distance(&triangle.v1);
let d2 = plane.signed_distance(&triangle.v2);
let mut front_count = 0;
if d0 >= -self.epsilon {
front_count += 1;
}
if d1 >= -self.epsilon {
front_count += 1;
}
if d2 >= -self.epsilon {
front_count += 1;
}
match front_count {
0 => ClipResult::AllBehind,
3 => ClipResult::AllFront(triangle.clone()),
1 => {
let (front, back1, back2) = if d0 >= -self.epsilon {
(triangle.v0, triangle.v1, triangle.v2)
} else if d1 >= -self.epsilon {
(triangle.v1, triangle.v2, triangle.v0)
} else {
(triangle.v2, triangle.v0, triangle.v1)
};
let d_front = if d0 >= -self.epsilon {
d0
} else if d1 >= -self.epsilon {
d1
} else {
d2
};
let d_back1 = if d0 >= -self.epsilon {
d1
} else if d1 >= -self.epsilon {
d2
} else {
d0
};
let d_back2 = if d0 >= -self.epsilon {
d2
} else if d1 >= -self.epsilon {
d0
} else {
d1
};
let t1 = d_front / (d_front - d_back1);
let t2 = d_front / (d_front - d_back2);
let p1 = front + (back1 - front) * t1;
let p2 = front + (back2 - front) * t2;
ClipResult::Split(smallvec::smallvec![Triangle::new(front, p1, p2)])
}
2 => {
let (front1, front2, back) = if d0 < -self.epsilon {
(triangle.v1, triangle.v2, triangle.v0)
} else if d1 < -self.epsilon {
(triangle.v2, triangle.v0, triangle.v1)
} else {
(triangle.v0, triangle.v1, triangle.v2)
};
let d_back = if d0 < -self.epsilon {
d0
} else if d1 < -self.epsilon {
d1
} else {
d2
};
let d_front1 = if d0 < -self.epsilon {
d1
} else if d1 < -self.epsilon {
d2
} else {
d0
};
let d_front2 = if d0 < -self.epsilon {
d2
} else if d1 < -self.epsilon {
d0
} else {
d1
};
let t1 = d_front1 / (d_front1 - d_back);
let t2 = d_front2 / (d_front2 - d_back);
let p1 = front1 + (back - front1) * t1;
let p2 = front2 + (back - front2) * t2;
ClipResult::Split(smallvec::smallvec![
Triangle::new(front1, front2, p1),
Triangle::new(front2, p2, p1),
])
}
_ => unreachable!(),
}
}
pub fn subtract_box(&self, mesh: &Mesh, min: Point3<f64>, max: Point3<f64>) -> Result<Mesh> {
if mesh.is_empty() {
return Ok(Mesh::new());
}
let box_mesh = aabb_to_mesh(min, max);
self.subtract_mesh(mesh, &box_mesh)
}
#[cfg_attr(feature = "manifold-csg", allow(dead_code))]
fn mesh_to_polygons(mesh: &Mesh) -> Vec<crate::bsp_csg::Polygon> {
use crate::bsp_csg::{Polygon, Vertex};
use rustc_hash::FxHashMap;
if mesh.is_empty() || mesh.indices.len() < 3 {
return Vec::new();
}
let vertex_count = mesh.positions.len() / 3;
const QUANT: f64 = 1.0e6;
let quantize = |p: [f64; 3]| -> (i64, i64, i64) {
(
(p[0] * QUANT).round() as i64,
(p[1] * QUANT).round() as i64,
(p[2] * QUANT).round() as i64,
)
};
let mut tris: Vec<BucketTri> = Vec::with_capacity(mesh.indices.len() / 3);
for chunk in mesh.indices.chunks_exact(3) {
let i0 = chunk[0] as usize;
let i1 = chunk[1] as usize;
let i2 = chunk[2] as usize;
if i0 >= vertex_count || i1 >= vertex_count || i2 >= vertex_count {
continue;
}
let p0 = i0 * 3;
let p1 = i1 * 3;
let p2 = i2 * 3;
let v0 = [
mesh.positions[p0] as f64,
mesh.positions[p0 + 1] as f64,
mesh.positions[p0 + 2] as f64,
];
let v1 = [
mesh.positions[p1] as f64,
mesh.positions[p1 + 1] as f64,
mesh.positions[p1 + 2] as f64,
];
let v2 = [
mesh.positions[p2] as f64,
mesh.positions[p2 + 1] as f64,
mesh.positions[p2 + 2] as f64,
];
if !v0.iter().chain(&v1).chain(&v2).all(|x| x.is_finite()) {
continue;
}
let e1 = [v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]];
let e2 = [v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2]];
let cross = [
e1[1] * e2[2] - e1[2] * e2[1],
e1[2] * e2[0] - e1[0] * e2[2],
e1[0] * e2[1] - e1[1] * e2[0],
];
let len =
(cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2]).sqrt();
if len < 1e-10 {
continue;
}
let n = [cross[0] / len, cross[1] / len, cross[2] / len];
tris.push(BucketTri {
v: [
Vertex::new(v0, n),
Vertex::new(v1, n),
Vertex::new(v2, n),
],
keys: [quantize(v0), quantize(v1), quantize(v2)],
normal: n,
});
}
let plane_key = |n: [f64; 3], anchor: [f64; 3]| -> (i64, i64, i64, i64) {
let off = n[0] * anchor[0] + n[1] * anchor[1] + n[2] * anchor[2];
let (a, b, c) = quantize(n);
(a, b, c, (off * QUANT).round() as i64)
};
let mut buckets: FxHashMap<(i64, i64, i64, i64), Vec<usize>> = FxHashMap::default();
for (i, t) in tris.iter().enumerate() {
let key = plane_key(t.normal, t.v[0].pos);
buckets.entry(key).or_default().push(i);
}
let mut polygons: Vec<Polygon> = Vec::with_capacity(tris.len());
for indices in buckets.values() {
let merged = merge_coplanar_bucket(&tris, indices);
polygons.extend(merged);
}
polygons
}
#[cfg_attr(feature = "manifold-csg", allow(dead_code))]
fn polygons_to_mesh(polygons: &[crate::bsp_csg::Polygon]) -> Result<Mesh> {
let mut mesh = Mesh::new();
for polygon in polygons {
let vertices = &polygon.vertices;
if vertices.len() < 3 {
continue;
}
let points_3d: Vec<Point3<f64>> = vertices
.iter()
.map(|v| Point3::new(v.pos[0], v.pos[1], v.pos[2]))
.collect();
let raw_normal =
Vector3::new(vertices[0].normal[0], vertices[0].normal[1], vertices[0].normal[2]);
let csg_normal = match raw_normal.try_normalize(1e-10) {
Some(n) if n.x.is_finite() && n.y.is_finite() && n.z.is_finite() => n,
_ => {
let computed = calculate_polygon_normal(&points_3d);
match computed.try_normalize(1e-10) {
Some(n) => n,
None => continue,
}
}
};
if points_3d.len() == 3 {
let base_idx = mesh.vertex_count();
for v in vertices {
mesh.add_vertex(
Point3::new(v.pos[0], v.pos[1], v.pos[2]),
Vector3::new(v.normal[0], v.normal[1], v.normal[2]),
);
}
mesh.add_triangle(
base_idx as u32,
(base_idx + 1) as u32,
(base_idx + 2) as u32,
);
continue;
}
let (points_2d, _, _, _) = project_to_2d(&points_3d, &csg_normal);
let indices = match triangulate_polygon(&points_2d) {
Ok(idx) => idx,
Err(_) => continue,
};
let base_idx = mesh.vertex_count();
for v in vertices {
mesh.add_vertex(
Point3::new(v.pos[0], v.pos[1], v.pos[2]),
Vector3::new(v.normal[0], v.normal[1], v.normal[2]),
);
}
for tri in indices.chunks(3) {
if tri.len() == 3 {
mesh.add_triangle(
(base_idx + tri[0]) as u32,
(base_idx + tri[1]) as u32,
(base_idx + tri[2]) as u32,
);
}
}
}
Ok(mesh)
}
fn bounds_overlap(host_mesh: &Mesh, opening_mesh: &Mesh) -> bool {
let (host_min, host_max) = host_mesh.bounds();
let (open_min, open_max) = opening_mesh.bounds();
let overlap_x = open_min.x < host_max.x && open_max.x > host_min.x;
let overlap_y = open_min.y < host_max.y && open_max.y > host_min.y;
let overlap_z = open_min.z < host_max.z && open_max.z > host_min.z;
overlap_x && overlap_y && overlap_z
}
pub fn subtract_mesh(&self, host_mesh: &Mesh, opening_mesh: &Mesh) -> Result<Mesh> {
if host_mesh.is_empty() {
return Ok(Mesh::new());
}
if opening_mesh.is_empty() {
self.record_failure(BoolOp::Difference, BoolFailureReason::EmptyOperand);
return Ok(host_mesh.clone());
}
if !Self::bounds_overlap(host_mesh, opening_mesh) {
self.record_failure(BoolOp::Difference, BoolFailureReason::NoBoundsOverlap);
return Ok(host_mesh.clone());
}
#[cfg(feature = "manifold-csg")]
{
match crate::manifold_kernel::difference(host_mesh, opening_mesh) {
Ok(result) => {
if !self.validate_mesh(&result) {
self.record_failure(
BoolOp::Difference,
BoolFailureReason::KernelOutputInvalid,
);
return Ok(host_mesh.clone());
}
if Self::manifold_result_looks_degenerate(host_mesh, &result) {
let host_tris = host_mesh.indices.len() / 3;
let result_tris = result.indices.len() / 3;
eprintln!(
"[manifold-csg] difference result looks degenerate \
(host {} tris -> result {} tris); retrying via BSP fallback",
host_tris, result_tris,
);
if let Some(bsp_result) = self.try_bsp_difference(host_mesh, opening_mesh) {
if !Self::manifold_result_looks_degenerate(host_mesh, &bsp_result) {
self.record_failure(
BoolOp::Difference,
BoolFailureReason::ManifoldOutputDegenerate {
host_tris,
result_tris,
},
);
return Ok(bsp_result);
}
}
self.record_failure(
BoolOp::Difference,
BoolFailureReason::ManifoldOutputDegenerate {
host_tris,
result_tris,
},
);
}
return Ok(result);
}
Err(reason) => {
self.record_failure(BoolOp::Difference, reason);
return Ok(host_mesh.clone());
}
}
}
#[cfg(not(feature = "manifold-csg"))]
{
let host_polys = Self::mesh_to_polygons(host_mesh);
let opening_polys = Self::mesh_to_polygons(opening_mesh);
if host_polys.is_empty() || opening_polys.is_empty() {
self.record_failure(BoolOp::Difference, BoolFailureReason::DegenerateOperand);
return Ok(host_mesh.clone());
}
if !Self::can_run_csg_operation(host_polys.len(), opening_polys.len()) {
self.record_failure(
BoolOp::Difference,
BoolFailureReason::OperandTooLarge {
polys_a: host_polys.len(),
polys_b: opening_polys.len(),
},
);
return Ok(host_mesh.clone());
}
let result_polys = crate::bsp_csg::difference(host_polys, opening_polys);
match Self::polygons_to_mesh(&result_polys) {
Ok(result) => Ok(Self::consolidate_coplanar(result)),
Err(e) => {
self.record_failure(
BoolOp::Difference,
BoolFailureReason::KernelError(e.to_string()),
);
Ok(host_mesh.clone())
}
}
}
}
#[cfg_attr(feature = "manifold-csg", allow(dead_code))]
fn consolidate_coplanar(mesh: Mesh) -> Mesh {
use crate::triangulation::{
project_to_2d_with_basis, triangulate_polygon_with_holes,
};
use i_overlay::core::fill_rule::FillRule;
use i_overlay::core::overlay_rule::OverlayRule;
use i_overlay::float::single::SingleFloatOverlay;
if mesh.indices.len() < 6 {
return mesh;
}
const POS_QUANT: f64 = 1.0e6;
const NORMAL_QUANT: f64 = 1.0e3;
let qpos = |p: f64| (p * POS_QUANT).round() as i64;
let qnorm = |n: f64| (n * NORMAL_QUANT).round() as i64;
struct PlaneTri {
v: [Point3<f64>; 3],
normal: Vector3<f64>,
}
let positions = &mesh.positions;
let vertex_count = positions.len() / 3;
let mut buckets: FxHashMap<(i64, i64, i64, i64), Vec<PlaneTri>> =
FxHashMap::default();
for chunk in mesh.indices.chunks_exact(3) {
let (i0, i1, i2) = (chunk[0] as usize, chunk[1] as usize, chunk[2] as usize);
if i0 >= vertex_count || i1 >= vertex_count || i2 >= vertex_count {
continue;
}
let v0 = Point3::new(
positions[i0 * 3] as f64,
positions[i0 * 3 + 1] as f64,
positions[i0 * 3 + 2] as f64,
);
let v1 = Point3::new(
positions[i1 * 3] as f64,
positions[i1 * 3 + 1] as f64,
positions[i1 * 3 + 2] as f64,
);
let v2 = Point3::new(
positions[i2 * 3] as f64,
positions[i2 * 3 + 1] as f64,
positions[i2 * 3 + 2] as f64,
);
let edge1 = v1 - v0;
let edge2 = v2 - v0;
let cross = edge1.cross(&edge2);
let len = cross.norm();
if len < 1.0e-10 {
continue;
}
let normal = cross / len;
let offset = normal.dot(&v0.coords);
let key = (
qnorm(normal.x),
qnorm(normal.y),
qnorm(normal.z),
qpos(offset),
);
buckets.entry(key).or_default().push(PlaneTri {
v: [v0, v1, v2],
normal,
});
}
let mut output = Mesh::new();
for tris in buckets.values() {
if tris.is_empty() {
continue;
}
let normal = tris[0].normal;
let origin = tris[0].v[0];
let abs = (normal.x.abs(), normal.y.abs(), normal.z.abs());
let reference = if abs.0 <= abs.1 && abs.0 <= abs.2 {
Vector3::new(1.0, 0.0, 0.0)
} else if abs.1 <= abs.2 {
Vector3::new(0.0, 1.0, 0.0)
} else {
Vector3::new(0.0, 0.0, 1.0)
};
let u_axis = normal.cross(&reference).normalize();
let v_axis = normal.cross(&u_axis).normalize();
if tris.len() == 1 {
emit_triangle(&mut output, &tris[0].v, &normal);
continue;
}
let mut subject: Vec<Vec<[f64; 2]>> = Vec::with_capacity(1);
let mut clip: Vec<Vec<[f64; 2]>> = Vec::with_capacity(tris.len() - 1);
for (idx, tri) in tris.iter().enumerate() {
let pts_2d = project_to_2d_with_basis(&tri.v, &u_axis, &v_axis, &origin);
let signed_area = (pts_2d[1].x - pts_2d[0].x)
* (pts_2d[2].y - pts_2d[0].y)
- (pts_2d[2].x - pts_2d[0].x)
* (pts_2d[1].y - pts_2d[0].y);
let path: Vec<[f64; 2]> = if signed_area >= 0.0 {
pts_2d.iter().map(|p| [p.x, p.y]).collect()
} else {
pts_2d.iter().rev().map(|p| [p.x, p.y]).collect()
};
if idx == 0 {
subject.push(path);
} else {
clip.push(path);
}
}
let shapes = subject.overlay(&clip, OverlayRule::Union, FillRule::NonZero);
if shapes.is_empty() {
for t in tris {
emit_triangle(&mut output, &t.v, &normal);
}
continue;
}
let bucket_area: f64 = tris
.iter()
.map(|t| {
let pts =
project_to_2d_with_basis(&t.v, &u_axis, &v_axis, &origin);
0.5_f64
* ((pts[1].x - pts[0].x) * (pts[2].y - pts[0].y)
- (pts[2].x - pts[0].x) * (pts[1].y - pts[0].y))
.abs()
})
.sum();
let min_significant = (bucket_area * 1.0e-4).max(1.0e-8);
let signed_area_2d = |ring: &[nalgebra::Point2<f64>]| -> f64 {
let n = ring.len();
if n < 3 {
return 0.0;
}
let mut s = 0.0;
for i in 0..n {
let j = (i + 1) % n;
s += ring[i].x * ring[j].y - ring[j].x * ring[i].y;
}
s * 0.5
};
for shape in shapes {
if shape.is_empty() {
continue;
}
let outer_2d: Vec<nalgebra::Point2<f64>> = shape[0]
.iter()
.map(|p| nalgebra::Point2::new(p[0], p[1]))
.collect();
let outer_simplified = simplify_2d_collinear(&outer_2d);
if outer_simplified.len() < 3 {
continue;
}
let outer_area = signed_area_2d(&outer_simplified).abs();
if outer_area < min_significant {
continue;
}
let holes_simplified: Vec<Vec<nalgebra::Point2<f64>>> = shape
.iter()
.skip(1)
.filter_map(|c| {
let pts: Vec<_> = c
.iter()
.map(|p| nalgebra::Point2::new(p[0], p[1]))
.collect();
let simplified = simplify_2d_collinear(&pts);
if simplified.len() < 3 {
return None;
}
let area = signed_area_2d(&simplified).abs();
if area < min_significant {
return None;
}
Some(simplified)
})
.collect();
let indices = match triangulate_polygon_with_holes(
&outer_simplified,
&holes_simplified,
) {
Ok(idx) => idx,
Err(_) => continue,
};
let mut all_2d: Vec<nalgebra::Point2<f64>> =
Vec::with_capacity(outer_simplified.len() + holes_simplified.iter().map(|h| h.len()).sum::<usize>());
all_2d.extend(outer_simplified.iter().copied());
for h in &holes_simplified {
all_2d.extend(h.iter().copied());
}
let lift = |p: nalgebra::Point2<f64>| -> Point3<f64> {
let off = u_axis * p.x + v_axis * p.y;
origin + off
};
let mut verts_3d: Vec<Point3<f64>> = Vec::with_capacity(all_2d.len());
for p in &all_2d {
verts_3d.push(lift(*p));
}
let base = output.vertex_count() as u32;
for vp in &verts_3d {
output.add_vertex(*vp, normal);
}
for tri in indices.chunks_exact(3) {
output.add_triangle(
base + tri[0] as u32,
base + tri[1] as u32,
base + tri[2] as u32,
);
}
}
}
if output.is_empty() {
return mesh;
}
output
}
pub fn union_mesh(&self, mesh_a: &Mesh, mesh_b: &Mesh) -> Result<Mesh> {
if mesh_a.is_empty() {
return Ok(mesh_b.clone());
}
if mesh_b.is_empty() {
return Ok(mesh_a.clone());
}
#[cfg(feature = "manifold-csg")]
{
match crate::manifold_kernel::union(mesh_a, mesh_b) {
Ok(result) if !result.is_empty() => return Ok(result),
Ok(_) => {
self.record_failure(BoolOp::Union, BoolFailureReason::KernelOutputInvalid);
let mut merged = mesh_a.clone();
merged.merge(mesh_b);
return Ok(merged);
}
Err(reason) => {
self.record_failure(BoolOp::Union, reason);
let mut merged = mesh_a.clone();
merged.merge(mesh_b);
return Ok(merged);
}
}
}
#[cfg(not(feature = "manifold-csg"))]
{
let polys_a = Self::mesh_to_polygons(mesh_a);
let polys_b = Self::mesh_to_polygons(mesh_b);
if polys_a.is_empty() || polys_b.is_empty() {
self.record_failure(BoolOp::Union, BoolFailureReason::DegenerateOperand);
let mut merged = mesh_a.clone();
merged.merge(mesh_b);
return Ok(merged);
}
if !Self::can_run_csg_operation(polys_a.len(), polys_b.len()) {
self.record_failure(
BoolOp::Union,
BoolFailureReason::OperandTooLarge {
polys_a: polys_a.len(),
polys_b: polys_b.len(),
},
);
let mut merged = mesh_a.clone();
merged.merge(mesh_b);
return Ok(merged);
}
let result_polys = crate::bsp_csg::union(polys_a, polys_b);
Self::polygons_to_mesh(&result_polys)
}
}
pub fn intersection_mesh(&self, mesh_a: &Mesh, mesh_b: &Mesh) -> Result<Mesh> {
if mesh_a.is_empty() || mesh_b.is_empty() {
return Ok(Mesh::new());
}
#[cfg(feature = "manifold-csg")]
{
match crate::manifold_kernel::intersection(mesh_a, mesh_b) {
Ok(result) => return Ok(result),
Err(reason) => {
self.record_failure(BoolOp::Intersection, reason);
return Ok(Mesh::new());
}
}
}
#[cfg(not(feature = "manifold-csg"))]
{
let polys_a = Self::mesh_to_polygons(mesh_a);
let polys_b = Self::mesh_to_polygons(mesh_b);
if polys_a.is_empty() || polys_b.is_empty() {
self.record_failure(BoolOp::Intersection, BoolFailureReason::DegenerateOperand);
return Ok(Mesh::new());
}
if !Self::can_run_csg_operation(polys_a.len(), polys_b.len()) {
self.record_failure(
BoolOp::Intersection,
BoolFailureReason::OperandTooLarge {
polys_a: polys_a.len(),
polys_b: polys_b.len(),
},
);
return Ok(Mesh::new());
}
let result_polys = crate::bsp_csg::intersection(polys_a, polys_b);
Self::polygons_to_mesh(&result_polys)
}
}
pub fn union_meshes(&self, meshes: &[Mesh]) -> Result<Mesh> {
if meshes.is_empty() {
return Ok(Mesh::new());
}
if meshes.len() == 1 {
return Ok(meshes[0].clone());
}
let mut result = Mesh::new();
let mut found_first = false;
for mesh in meshes {
if mesh.is_empty() {
continue;
}
if !found_first {
result = mesh.clone();
found_first = true;
continue;
}
result = self.union_mesh(&result, mesh)?;
}
Ok(result)
}
pub fn subtract_meshes_batched(&self, host: &Mesh, voids: &[Mesh]) -> Result<Mesh> {
let non_empty_voids: Vec<&Mesh> = voids.iter().filter(|m| !m.is_empty()).collect();
if non_empty_voids.is_empty() {
return Ok(host.clone());
}
if non_empty_voids.len() == 1 {
return self.subtract_mesh(host, non_empty_voids[0]);
}
const BATCH_THRESHOLD: usize = 10;
if non_empty_voids.len() > BATCH_THRESHOLD {
let void_refs: Vec<Mesh> = non_empty_voids.iter().map(|m| (*m).clone()).collect();
let combined = self.union_meshes(&void_refs)?;
self.subtract_mesh(host, &combined)
} else {
let mut result = host.clone();
for void in non_empty_voids {
result = self.subtract_mesh(&result, void)?;
}
Ok(result)
}
}
pub fn subtract_meshes_with_fallback(&self, host: &Mesh, voids: &[Mesh]) -> Mesh {
if host.is_empty() {
return host.clone();
}
match self.subtract_meshes_batched(host, voids) {
Ok(result) => {
if !self.validate_mesh(&result) {
self.record_failure(
BoolOp::Difference,
BoolFailureReason::KernelOutputInvalid,
);
host.clone()
} else {
result
}
}
Err(e) => {
self.record_failure(
BoolOp::Difference,
BoolFailureReason::KernelError(e.to_string()),
);
host.clone()
}
}
}
#[cfg_attr(not(feature = "manifold-csg"), allow(dead_code))]
fn manifold_result_looks_degenerate(host: &Mesh, result: &Mesh) -> bool {
let result_tris = result.indices.len() / 3;
if result_tris == 0 {
return false;
}
if result_tris < 4 {
return true;
}
let host_tris = host.indices.len() / 3;
if host_tris >= 12 && result_tris * 4 < host_tris {
return true;
}
false
}
#[cfg_attr(not(feature = "manifold-csg"), allow(dead_code))]
fn try_bsp_difference(&self, host_mesh: &Mesh, opening_mesh: &Mesh) -> Option<Mesh> {
let host_polys = Self::mesh_to_polygons(host_mesh);
let opening_polys = Self::mesh_to_polygons(opening_mesh);
if host_polys.is_empty() || opening_polys.is_empty() {
return None;
}
if !Self::can_run_csg_operation(host_polys.len(), opening_polys.len()) {
return None;
}
let result_polys = crate::bsp_csg::difference(host_polys, opening_polys);
let _ = host_mesh;
match Self::polygons_to_mesh(&result_polys) {
Ok(mesh) => {
let consolidated = Self::consolidate_coplanar(mesh);
if consolidated.is_empty() {
None
} else {
Some(consolidated)
}
}
Err(_) => None,
}
}
fn validate_mesh(&self, mesh: &Mesh) -> bool {
if mesh.positions.iter().any(|v| !v.is_finite()) {
return false;
}
if mesh.normals.iter().any(|v| !v.is_finite()) {
return false;
}
let vertex_count = mesh.vertex_count();
for idx in &mesh.indices {
if *idx as usize >= vertex_count {
return false;
}
}
true
}
pub fn clip_mesh(&self, mesh: &Mesh, plane: &Plane) -> Result<Mesh> {
let mut result = Mesh::new();
let vert_count = mesh.positions.len() / 3;
for i in (0..mesh.indices.len()).step_by(3) {
if i + 2 >= mesh.indices.len() {
break;
}
let i0 = mesh.indices[i] as usize;
let i1 = mesh.indices[i + 1] as usize;
let i2 = mesh.indices[i + 2] as usize;
if i0 >= vert_count || i1 >= vert_count || i2 >= vert_count {
continue;
}
let v0 = Point3::new(
mesh.positions[i0 * 3] as f64,
mesh.positions[i0 * 3 + 1] as f64,
mesh.positions[i0 * 3 + 2] as f64,
);
let v1 = Point3::new(
mesh.positions[i1 * 3] as f64,
mesh.positions[i1 * 3 + 1] as f64,
mesh.positions[i1 * 3 + 2] as f64,
);
let v2 = Point3::new(
mesh.positions[i2 * 3] as f64,
mesh.positions[i2 * 3 + 1] as f64,
mesh.positions[i2 * 3 + 2] as f64,
);
let triangle = Triangle::new(v0, v1, v2);
match self.clip_triangle(&triangle, plane) {
ClipResult::AllFront(tri) => {
add_triangle_to_mesh(&mut result, &tri);
}
ClipResult::AllBehind => {
}
ClipResult::Split(triangles) => {
for tri in triangles {
add_triangle_to_mesh(&mut result, &tri);
}
}
}
}
Ok(result)
}
}
impl Default for ClippingProcessor {
fn default() -> Self {
Self::new()
}
}
fn add_triangle_to_mesh(mesh: &mut Mesh, triangle: &Triangle) {
let base_idx = mesh.vertex_count() as u32;
let normal = triangle.normal();
mesh.add_vertex(triangle.v0, normal);
mesh.add_vertex(triangle.v1, normal);
mesh.add_vertex(triangle.v2, normal);
mesh.add_triangle(base_idx, base_idx + 1, base_idx + 2);
}
#[cfg(not(target_arch = "wasm32"))]
#[inline]
pub fn calculate_normals(_mesh: &mut Mesh) {}
#[cfg(target_arch = "wasm32")]
#[inline]
pub fn calculate_normals(mesh: &mut Mesh) {
let vertex_count = mesh.vertex_count();
if vertex_count == 0 {
return;
}
let positions_len = mesh.positions.len();
let mut normals = vec![Vector3::zeros(); vertex_count];
for i in (0..mesh.indices.len()).step_by(3) {
if i + 2 >= mesh.indices.len() {
break;
}
let i0 = mesh.indices[i] as usize;
let i1 = mesh.indices[i + 1] as usize;
let i2 = mesh.indices[i + 2] as usize;
if i0 >= vertex_count || i1 >= vertex_count || i2 >= vertex_count {
continue;
}
if i0 * 3 + 2 >= positions_len || i1 * 3 + 2 >= positions_len || i2 * 3 + 2 >= positions_len
{
continue;
}
let v0 = Point3::new(
mesh.positions[i0 * 3] as f64,
mesh.positions[i0 * 3 + 1] as f64,
mesh.positions[i0 * 3 + 2] as f64,
);
let v1 = Point3::new(
mesh.positions[i1 * 3] as f64,
mesh.positions[i1 * 3 + 1] as f64,
mesh.positions[i1 * 3 + 2] as f64,
);
let v2 = Point3::new(
mesh.positions[i2 * 3] as f64,
mesh.positions[i2 * 3 + 1] as f64,
mesh.positions[i2 * 3 + 2] as f64,
);
let edge1 = v1 - v0;
let edge2 = v2 - v0;
let normal = edge1.cross(&edge2);
normals[i0] += normal;
normals[i1] += normal;
normals[i2] += normal;
}
mesh.normals.clear();
mesh.normals.reserve(vertex_count * 3);
for normal in normals {
let normalized = normal
.try_normalize(1e-6)
.unwrap_or_else(|| Vector3::new(0.0, 0.0, 1.0));
mesh.normals.push(normalized.x as f32);
mesh.normals.push(normalized.y as f32);
mesh.normals.push(normalized.z as f32);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bsp_difference_preserves_rounded_rect_cap() {
use crate::bsp_csg::{Polygon, Vertex};
let host_polys = ClippingProcessor::mesh_to_polygons(&aabb_to_mesh(
Point3::new(0.0, 0.0, 0.0),
Point3::new(2.0, 0.8, 0.8),
));
const N: usize = 6;
let mut outline = Vec::with_capacity((N + 1) * 4);
let corners = [
(1.7, 0.3, -std::f64::consts::FRAC_PI_2, 0.0),
(1.7, 0.5, 0.0, std::f64::consts::FRAC_PI_2),
(0.3, 0.5, std::f64::consts::FRAC_PI_2, std::f64::consts::PI),
(0.3, 0.3, std::f64::consts::PI, std::f64::consts::PI * 1.5),
];
let r = 0.2_f64;
for (cx, cy, a0, a1) in corners {
for i in 0..=N {
let t = i as f64 / N as f64;
let a = a0 + (a1 - a0) * t;
outline.push((cx + r * a.cos(), cy + r * a.sin()));
}
}
let n = outline.len();
let z_bot = 0.1_f64;
let z_top = 0.8_f64;
let mut cutter_polys: Vec<Polygon> = Vec::new();
let bot_verts: Vec<Vertex> = outline
.iter()
.rev()
.map(|(x, y)| Vertex::new([*x, *y, z_bot], [0.0, 0.0, -1.0]))
.collect();
cutter_polys.push(Polygon::new(bot_verts));
let top_verts: Vec<Vertex> = outline
.iter()
.map(|(x, y)| Vertex::new([*x, *y, z_top], [0.0, 0.0, 1.0]))
.collect();
cutter_polys.push(Polygon::new(top_verts));
for i in 0..n {
let j = (i + 1) % n;
let (x0, y0) = outline[i];
let (x1, y1) = outline[j];
let ex = x1 - x0;
let ey = y1 - y0;
let mut nx = ey;
let mut ny = -ex;
let nlen = (nx * nx + ny * ny).sqrt();
if nlen > 0.0 {
nx /= nlen;
ny /= nlen;
}
let mx = (x0 + x1) * 0.5 - 1.0;
let my = (y0 + y1) * 0.5 - 0.4;
if nx * mx + ny * my < 0.0 {
nx = -nx;
ny = -ny;
}
let wall = vec![
Vertex::new([x0, y0, z_bot], [nx, ny, 0.0]),
Vertex::new([x1, y1, z_bot], [nx, ny, 0.0]),
Vertex::new([x1, y1, z_top], [nx, ny, 0.0]),
Vertex::new([x0, y0, z_top], [nx, ny, 0.0]),
];
cutter_polys.push(Polygon::new(wall));
}
assert_eq!(
cutter_polys.len(),
2 + n,
"expected 2 caps + {} walls",
n
);
let result_polys =
crate::bsp_csg::difference(host_polys, cutter_polys);
let mut cap_polys: Vec<&Polygon> = result_polys
.iter()
.filter(|p| {
p.vertices
.iter()
.all(|v| (v.pos[2] - z_bot).abs() < 1.0e-4)
})
.collect();
cap_polys.sort_by_key(|p| std::cmp::Reverse(p.vertices.len()));
let largest_cap_verts = cap_polys.first().map(|p| p.vertices.len()).unwrap_or(0);
assert!(
largest_cap_verts >= 20,
"cavity floor cap collapsed: largest polygon at z={} has {} verts (need ≥20 for the rounded rect outline). All cap polys: {:?}",
z_bot,
largest_cap_verts,
cap_polys.iter().map(|p| p.vertices.len()).collect::<Vec<_>>()
);
}
#[test]
fn bsp_difference_preserves_inner_cube_faces() {
let host = aabb_to_mesh(Point3::new(0.0, 0.0, 0.0), Point3::new(2.0, 0.8, 0.8));
let cutter = aabb_to_mesh(Point3::new(0.1, 0.1, 0.1), Point3::new(1.9, 0.7, 0.8));
let host_polys = ClippingProcessor::mesh_to_polygons(&host);
let cutter_polys = ClippingProcessor::mesh_to_polygons(&cutter);
assert_eq!(host_polys.len(), 6, "host should pre-merge to 6 face quads");
assert_eq!(
cutter_polys.len(),
6,
"cutter should pre-merge to 6 face quads"
);
let result_polys = crate::bsp_csg::difference(host_polys, cutter_polys);
let result = ClippingProcessor::polygons_to_mesh(&result_polys)
.expect("polygons_to_mesh ok");
let tris = result.indices.len() / 3;
assert!(
tris >= 12,
"expected ≥ 12 tris (host bottom + 4 sides + annular top + cavity walls + floor); got {}",
tris
);
let mut floor_area = 0.0_f64;
for tri in result.indices.chunks_exact(3) {
let v: Vec<(f32, f32, f32)> = tri
.iter()
.map(|&i| {
let o = i as usize * 3;
(
result.positions[o],
result.positions[o + 1],
result.positions[o + 2],
)
})
.collect();
if v.iter().all(|p| (p.2 - 0.1).abs() < 1.0e-4) {
let ax = v[1].0 - v[0].0;
let ay = v[1].1 - v[0].1;
let bx = v[2].0 - v[0].0;
let by = v[2].1 - v[0].1;
floor_area += 0.5 * ((ax * by) - (ay * bx)).abs() as f64;
}
}
let expected_floor_area = 1.8 * 0.6; assert!(
(floor_area - expected_floor_area).abs() < 1.0e-3,
"cavity floor area = {:.4} m² (expected {:.4})",
floor_area,
expected_floor_area
);
}
#[test]
fn merge_coplanar_collapses_subdivided_quad() {
let mut mesh = Mesh::new();
for p in [
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[1.0, 1.0, 0.0],
[0.0, 1.0, 0.0],
[0.5, 0.5, 0.0],
] {
mesh.add_vertex(
Point3::new(p[0], p[1], p[2]),
Vector3::new(0.0, 0.0, 1.0),
);
}
mesh.add_triangle(0, 1, 4);
mesh.add_triangle(1, 2, 4);
mesh.add_triangle(2, 3, 4);
mesh.add_triangle(3, 0, 4);
let polys = ClippingProcessor::mesh_to_polygons(&mesh);
assert_eq!(
polys.len(),
1,
"expected single merged polygon, got {}",
polys.len()
);
assert_eq!(
polys[0].vertices.len(),
4,
"expected 4-vertex quad after collinear-vertex cleanup, got {} verts",
polys[0].vertices.len()
);
let consolidated = ClippingProcessor::consolidate_coplanar(mesh);
assert_eq!(
consolidated.indices.len() / 3,
2,
"consolidated quad should triangulate to 2 tris, got {}",
consolidated.indices.len() / 3
);
}
#[test]
fn merge_coplanar_collapses_edge_split_quad() {
let mut mesh = Mesh::new();
for p in [
[0.0, 0.0, 0.0],
[0.5, 0.0, 0.0],
[1.5, 0.0, 0.0],
[2.0, 0.0, 0.0],
[2.0, 1.0, 0.0],
[0.0, 1.0, 0.0],
] {
mesh.add_vertex(
Point3::new(p[0], p[1], p[2]),
Vector3::new(0.0, 0.0, 1.0),
);
}
mesh.add_triangle(0, 1, 5);
mesh.add_triangle(1, 2, 5);
mesh.add_triangle(2, 4, 5);
mesh.add_triangle(2, 3, 4);
let consolidated = ClippingProcessor::consolidate_coplanar(mesh);
assert_eq!(
consolidated.indices.len() / 3,
2,
"edge-split quad must collapse to 2 tris after collinear cleanup, got {}",
consolidated.indices.len() / 3
);
}
#[test]
fn test_plane_signed_distance() {
let plane = Plane::new(Point3::new(0.0, 0.0, 0.0), Vector3::new(0.0, 0.0, 1.0));
assert_eq!(plane.signed_distance(&Point3::new(0.0, 0.0, 5.0)), 5.0);
assert_eq!(plane.signed_distance(&Point3::new(0.0, 0.0, -5.0)), -5.0);
assert_eq!(plane.signed_distance(&Point3::new(5.0, 5.0, 0.0)), 0.0);
}
#[test]
fn test_clip_triangle_all_front() {
let processor = ClippingProcessor::new();
let triangle = Triangle::new(
Point3::new(0.0, 0.0, 1.0),
Point3::new(1.0, 0.0, 1.0),
Point3::new(0.5, 1.0, 1.0),
);
let plane = Plane::new(Point3::new(0.0, 0.0, 0.0), Vector3::new(0.0, 0.0, 1.0));
match processor.clip_triangle(&triangle, &plane) {
ClipResult::AllFront(_) => {}
_ => panic!("Expected AllFront"),
}
}
#[test]
fn test_clip_triangle_all_behind() {
let processor = ClippingProcessor::new();
let triangle = Triangle::new(
Point3::new(0.0, 0.0, -1.0),
Point3::new(1.0, 0.0, -1.0),
Point3::new(0.5, 1.0, -1.0),
);
let plane = Plane::new(Point3::new(0.0, 0.0, 0.0), Vector3::new(0.0, 0.0, 1.0));
match processor.clip_triangle(&triangle, &plane) {
ClipResult::AllBehind => {}
_ => panic!("Expected AllBehind"),
}
}
#[test]
fn test_clip_triangle_split_one_front() {
let processor = ClippingProcessor::new();
let triangle = Triangle::new(
Point3::new(0.0, 0.0, 1.0), Point3::new(1.0, 0.0, -1.0), Point3::new(0.5, 1.0, -1.0), );
let plane = Plane::new(Point3::new(0.0, 0.0, 0.0), Vector3::new(0.0, 0.0, 1.0));
match processor.clip_triangle(&triangle, &plane) {
ClipResult::Split(triangles) => {
assert_eq!(triangles.len(), 1);
}
_ => panic!("Expected Split"),
}
}
#[test]
fn test_clip_triangle_split_two_front() {
let processor = ClippingProcessor::new();
let triangle = Triangle::new(
Point3::new(0.0, 0.0, 1.0), Point3::new(1.0, 0.0, 1.0), Point3::new(0.5, 1.0, -1.0), );
let plane = Plane::new(Point3::new(0.0, 0.0, 0.0), Vector3::new(0.0, 0.0, 1.0));
match processor.clip_triangle(&triangle, &plane) {
ClipResult::Split(triangles) => {
assert_eq!(triangles.len(), 2);
}
_ => panic!("Expected Split with 2 triangles"),
}
}
#[test]
fn test_triangle_normal() {
let triangle = Triangle::new(
Point3::new(0.0, 0.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
Point3::new(0.0, 1.0, 0.0),
);
let normal = triangle.normal();
assert!((normal.z - 1.0).abs() < 1e-6);
}
#[test]
fn test_triangle_area() {
let triangle = Triangle::new(
Point3::new(0.0, 0.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
Point3::new(0.0, 1.0, 0.0),
);
let area = triangle.area();
assert!((area - 0.5).abs() < 1e-6);
}
#[test]
fn test_csg_operation_guard_allows_simple_boxes() {
let box_a = aabb_to_mesh(Point3::new(0.0, 0.0, 0.0), Point3::new(1.0, 1.0, 1.0));
let box_b = aabb_to_mesh(Point3::new(0.25, 0.25, 0.25), Point3::new(0.75, 0.75, 0.75));
let polys_a = ClippingProcessor::mesh_to_polygons(&box_a);
let polys_b = ClippingProcessor::mesh_to_polygons(&box_b);
assert!(ClippingProcessor::can_run_csg_operation(polys_a.len(), polys_b.len()));
}
#[test]
fn test_csg_operation_guard_rejects_complex_operands() {
let mut complex_mesh = Mesh::new();
for i in 0..30 {
let offset = i as f64 * 2.0;
complex_mesh.merge(&aabb_to_mesh(
Point3::new(offset, offset, offset),
Point3::new(offset + 1.0, offset + 1.0, offset + 1.0),
));
}
let box_mesh =
aabb_to_mesh(Point3::new(0.0, 0.0, 0.0), Point3::new(1.0, 1.0, 1.0));
let polys_complex = ClippingProcessor::mesh_to_polygons(&complex_mesh);
let polys_box = ClippingProcessor::mesh_to_polygons(&box_mesh);
assert!(
polys_complex.len() > MAX_CSG_POLYGONS_PER_MESH,
"expected > {} polygons, got {} (merge regressed?)",
MAX_CSG_POLYGONS_PER_MESH,
polys_complex.len()
);
assert!(!ClippingProcessor::can_run_csg_operation(
polys_complex.len(),
polys_box.len()
));
}
}