use crate::diagnostics::{BoolFailure, BoolFailureReason, BoolOp};
use crate::error::Result;
use crate::mesh::Mesh;
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()
}
}
#[derive(Clone, Copy, Debug)]
pub struct CsgOpRecord {
pub op: u8,
pub a_tris: u32,
pub b_tris: u32,
}
static CSG_CENSUS: std::sync::Mutex<Vec<CsgOpRecord>> = std::sync::Mutex::new(Vec::new());
pub fn reset_csg_census() {
if let Ok(mut g) = CSG_CENSUS.lock() {
g.clear();
}
}
pub fn take_csg_census() -> Vec<CsgOpRecord> {
CSG_CENSUS
.lock()
.map(|mut g| std::mem::take(&mut *g))
.unwrap_or_default()
}
#[inline]
fn record_csg_op(op: u8, a_tris: usize, b_tris: usize) {
if let Ok(mut g) = CSG_CENSUS.lock() {
g.push(CsgOpRecord {
op,
a_tris: a_tris as u32,
b_tris: b_tris as u32,
});
}
}
pub struct ClippingProcessor {
pub epsilon: f64,
failures: RefCell<Vec<BoolFailure>>,
}
pub(crate) fn tri_is_needle(v: &[Point3<f64>; 3]) -> bool {
let d = |a: &Point3<f64>, b: &Point3<f64>| (a - b).norm();
let (e0, e1, e2) = (d(&v[0], &v[1]), d(&v[1], &v[2]), d(&v[2], &v[0]));
let mn = e0.min(e1).min(e2);
let mx = e0.max(e1).max(e2);
if !mx.is_finite() || mx <= 0.0 {
return true; }
mn < floor_pow2(mx) * 2.0_f64.powi(-13)
}
fn emit_triangle(mesh: &mut Mesh, v: &[Point3<f64>; 3], normal: &Vector3<f64>) {
if tri_is_needle(v) {
return;
}
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);
}
fn count_open_boundary_edges(mesh: &Mesh) -> usize {
if mesh.positions.len() < 9 || mesh.indices.len() < 3 {
return 0;
}
let q = |v: f32| (v as f64 * 1.0e3).round() as i64;
let mut vid: FxHashMap<(i64, i64, i64), u32> = FxHashMap::default();
let mut id_of = |i: usize| -> u32 {
let k = (
q(mesh.positions[i * 3]),
q(mesh.positions[i * 3 + 1]),
q(mesh.positions[i * 3 + 2]),
);
let next = vid.len() as u32;
*vid.entry(k).or_insert(next)
};
let mut bal: FxHashMap<(u32, u32), i32> = FxHashMap::default();
for tri in mesh.indices.chunks_exact(3) {
let (a, b, c) = (
id_of(tri[0] as usize),
id_of(tri[1] as usize),
id_of(tri[2] as usize),
);
for (x, y) in [(a, b), (b, c), (c, a)] {
let (key, s) = if x < y { ((x, y), 1) } else { ((y, x), -1) };
*bal.entry(key).or_insert(0) += s;
}
}
bal.values().filter(|&&v| v != 0).count()
}
fn count_spike_triangles(mesh: &Mesh) -> usize {
let mut n = 0usize;
for tri in mesh.indices.chunks_exact(3) {
let p = |i: u32| {
let i = i as usize;
[
mesh.positions[i * 3],
mesh.positions[i * 3 + 1],
mesh.positions[i * 3 + 2],
]
};
let (a, b, c) = (p(tri[0]), p(tri[1]), p(tri[2]));
let d = |u: [f32; 3], v: [f32; 3]| {
((u[0] - v[0]).powi(2) + (u[1] - v[1]).powi(2) + (u[2] - v[2]).powi(2)).sqrt()
};
let (e0, e1, e2) = (d(a, b), d(b, c), d(c, a));
let mn = e0.min(e1).min(e2);
let mx = e0.max(e1).max(e2);
if mn > 1.0e-6 && mx / mn > 50.0 {
n += 1;
}
}
n
}
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()
}
#[inline]
fn floor_pow2(x: f64) -> f64 {
if !x.is_finite() || x <= 0.0 {
return 0.0;
}
let exp = x.to_bits() >> 52 & 0x7ff; let unbiased = exp as i64 - 1023;
2.0_f64.powi(unbiased as i32)
}
fn weld_near_coincident_2d(ring: &[nalgebra::Point2<f64>]) -> Vec<nalgebra::Point2<f64>> {
let n = ring.len();
if n < 4 {
return ring.to_vec();
}
let (mut minx, mut miny, mut maxx, mut maxy) =
(f64::MAX, f64::MAX, f64::MIN, f64::MIN);
for p in ring {
minx = minx.min(p.x);
miny = miny.min(p.y);
maxx = maxx.max(p.x);
maxy = maxy.max(p.y);
}
let extent = (maxx - minx).max(maxy - miny);
if !extent.is_finite() || extent <= 0.0 {
return ring.to_vec();
}
let eps = (floor_pow2(extent) * 2.0_f64.powi(-13)).min(2.0_f64.powi(-12));
let eps2 = eps * eps;
let mut kept: Vec<nalgebra::Point2<f64>> = Vec::with_capacity(n);
for &p in ring {
let dup = kept.last().is_some_and(|q| {
let dx = p.x - q.x;
let dy = p.y - q.y;
dx * dx + dy * dy < eps2
});
if !dup {
kept.push(p);
}
}
if kept.len() >= 2 {
let (first, last) = (kept[0], *kept.last().unwrap());
let dx = last.x - first.x;
let dy = last.y - first.y;
if dx * dx + dy * dy < eps2 {
kept.pop();
}
}
if kept.len() >= 3 {
kept
} else {
ring.to_vec()
}
}
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 {
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 has_operand_too_large_since(&self, since: usize) -> bool {
let failures = self.failures.borrow();
let since = since.min(failures.len());
failures[since..]
.iter()
.any(|f| matches!(f.reason, BoolFailureReason::OperandTooLarge { .. }))
}
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)
}
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 span = (host_max.x - host_min.x)
.max(host_max.y - host_min.y)
.max(host_max.z - host_min.z)
.max(open_max.x - open_min.x)
.max(open_max.y - open_min.y)
.max(open_max.z - open_min.z);
let eps = span * 1e-6;
let overlap_x = open_min.x - eps <= host_max.x && open_max.x + eps >= host_min.x;
let overlap_y = open_min.y - eps <= host_max.y && open_max.y + eps >= host_min.y;
let overlap_z = open_min.z - eps <= host_max.z && open_max.z + eps >= host_min.z;
overlap_x && overlap_y && overlap_z
}
pub fn subtract_mesh(&self, host_mesh: &Mesh, opening_mesh: &Mesh) -> Result<Mesh> {
record_csg_op(0, host_mesh.triangle_count(), opening_mesh.triangle_count());
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());
}
crate::kernel::budget::begin();
let raw = crate::kernel::mesh_bridge::subtract(host_mesh, opening_mesh);
if crate::kernel::budget::tripped() {
self.record_failure(
BoolOp::Difference,
BoolFailureReason::OperandTooLarge {
polys_a: host_mesh.triangle_count(),
polys_b: opening_mesh.triangle_count(),
},
);
return Ok(host_mesh.clone());
}
let result = Self::consolidate_coplanar(raw);
if !result.is_empty() && !self.validate_mesh(&result) {
self.record_failure(BoolOp::Difference, BoolFailureReason::KernelOutputInvalid);
return Ok(host_mesh.clone());
}
Ok(result)
}
pub fn subtract_mesh_many(&self, host_mesh: &Mesh, cutters: &[&Mesh]) -> Result<Mesh> {
let total: usize = cutters.iter().map(|c| c.triangle_count()).sum();
record_csg_op(0, host_mesh.triangle_count(), total);
if host_mesh.is_empty() {
return Ok(Mesh::new());
}
let live: Vec<&Mesh> = cutters
.iter()
.copied()
.filter(|c| !c.is_empty() && Self::bounds_overlap(host_mesh, c))
.collect();
if live.is_empty() {
return Ok(host_mesh.clone()); }
crate::kernel::budget::begin();
let raw = crate::kernel::mesh_bridge::subtract_many(host_mesh, &live);
if crate::kernel::budget::tripped() {
return Ok(host_mesh.clone());
}
let Some(raw) = raw else {
return Ok(host_mesh.clone());
};
let result = Self::consolidate_coplanar(raw);
if !result.is_empty() && !self.validate_mesh(&result) {
self.record_failure(BoolOp::Difference, BoolFailureReason::KernelOutputInvalid);
return Ok(host_mesh.clone());
}
Ok(result)
}
pub(crate) fn consolidate_coplanar(mesh: Mesh) -> Mesh {
use crate::triangulation::{
project_to_2d_with_basis, triangulate_polygon_with_holes_refined,
};
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_welded = weld_near_coincident_2d(&outer_2d);
let outer_simplified = simplify_2d_collinear(&outer_welded);
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 welded = weld_near_coincident_2d(&pts);
let simplified = simplify_2d_collinear(&welded);
if simplified.len() < 3 {
return None;
}
let area = signed_area_2d(&simplified).abs();
if area < min_significant {
return None;
}
Some(simplified)
})
.collect();
let (all_2d, indices) = match triangulate_polygon_with_holes_refined(
&outer_simplified,
&holes_simplified,
false,
) {
Ok((pts, idx)) => (pts, idx),
Err(_) => continue,
};
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) {
let v = [
verts_3d[tri[0]],
verts_3d[tri[1]],
verts_3d[tri[2]],
];
if tri_is_needle(&v) {
continue;
}
output.add_triangle(
base + tri[0] as u32,
base + tri[1] as u32,
base + tri[2] as u32,
);
}
}
}
if output.is_empty() {
return mesh;
}
let mut bnorms: Vec<Vector3<f64>> = Vec::new();
for tris in buckets.values() {
if let Some(t0) = tris.first() {
if !bnorms.iter().any(|m| m.dot(&t0.normal).abs() > 0.99999) {
bnorms.push(t0.normal);
}
}
}
let nonorthogonal = (0..bnorms.len()).any(|i| {
((i + 1)..bnorms.len()).any(|j| {
let d = bnorms[i].dot(&bnorms[j]).abs();
d > 0.01 && d < 0.9999 })
});
let offset_jittered = buckets.len() > 4 * bnorms.len().max(1);
if nonorthogonal || offset_jittered {
let cons_open = count_open_boundary_edges(&output);
if cons_open > 0 {
let raw_bad = count_open_boundary_edges(&mesh) + count_spike_triangles(&mesh);
let cons_bad = cons_open + count_spike_triangles(&output);
if raw_bad < cons_bad {
return mesh;
}
}
}
output
}
pub fn union_mesh(&self, mesh_a: &Mesh, mesh_b: &Mesh) -> Result<Mesh> {
record_csg_op(1, mesh_a.triangle_count(), mesh_b.triangle_count());
if mesh_a.is_empty() {
return Ok(mesh_b.clone());
}
if mesh_b.is_empty() {
return Ok(mesh_a.clone());
}
let raw_u = crate::kernel::mesh_bridge::union(mesh_a, mesh_b);
let result = Self::consolidate_coplanar(raw_u);
if result.is_empty() || !self.validate_mesh(&result) {
self.record_failure(BoolOp::Union, BoolFailureReason::KernelOutputInvalid);
let mut merged = mesh_a.clone();
merged.merge(mesh_b);
return Ok(merged);
}
Ok(result)
}
pub fn intersection_mesh(&self, mesh_a: &Mesh, mesh_b: &Mesh) -> Result<Mesh> {
record_csg_op(2, mesh_a.triangle_count(), mesh_b.triangle_count());
if mesh_a.is_empty() || mesh_b.is_empty() {
return Ok(Mesh::new());
}
let result =
Self::consolidate_coplanar(crate::kernel::mesh_bridge::intersection(mesh_a, mesh_b));
if !result.is_empty() && !self.validate_mesh(&result) {
self.record_failure(BoolOp::Intersection, BoolFailureReason::KernelOutputInvalid);
return Ok(Mesh::new());
}
Ok(result)
}
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()
}
}
}
pub(crate) fn difference_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;
}
let (host_min, host_max) = host.bounds();
let (res_min, res_max) = result.bounds();
let slack = (host_max - host_min).abs() * 0.01;
if res_min.x + slack.x < host_min.x
|| res_min.y + slack.y < host_min.y
|| res_min.z + slack.z < host_min.z
|| res_max.x > host_max.x + slack.x
|| res_max.y > host_max.y + slack.y
|| res_max.z > host_max.z + slack.z
{
return true;
}
false
}
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> {
record_csg_op(3, mesh.triangle_count(), 0);
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);
}
#[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);
}
}
pub fn smooth_normals_with_creases(mesh: &mut Mesh, crease_cos: f64) {
use rustc_hash::FxHashMap;
let vertex_count = mesh.vertex_count();
let tri_count = mesh.indices.len() / 3;
if vertex_count == 0 || tri_count == 0 {
return;
}
let mut face_normals: Vec<Vector3<f64>> = Vec::with_capacity(tri_count);
for tri in mesh.indices.chunks_exact(3) {
let i0 = tri[0] as usize;
let i1 = tri[1] as usize;
let i2 = tri[2] as usize;
if i0 >= vertex_count || i1 >= vertex_count || i2 >= vertex_count {
face_normals.push(Vector3::zeros());
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 e1 = v1 - v0;
let e2 = v2 - v0;
face_normals.push(e1.cross(&e2));
}
let mut vert_to_tris: Vec<smallvec::SmallVec<[(u32, u8); 6]>> =
vec![smallvec::SmallVec::new(); vertex_count];
for (t, tri) in mesh.indices.chunks_exact(3).enumerate() {
for k in 0..3 {
let v = tri[k] as usize;
if v < vertex_count {
vert_to_tris[v].push((t as u32, k as u8));
}
}
}
let mut new_positions: Vec<f32> = Vec::with_capacity(mesh.positions.len());
let mut new_normals: Vec<f32> = Vec::with_capacity(mesh.positions.len());
let mut corner_to_new_vertex: Vec<u32> = vec![0; tri_count * 3];
for (v, incident) in vert_to_tris.iter().enumerate() {
if incident.is_empty() {
continue;
}
let n = incident.len();
let mut parent: smallvec::SmallVec<[u32; 6]> = (0..n as u32).collect();
let find = |parent: &mut [u32], mut i: u32| -> u32 {
while parent[i as usize] != i {
parent[i as usize] = parent[parent[i as usize] as usize]; i = parent[i as usize];
}
i
};
let other_corners = |corner_idx: u8, t: u32| -> [u32; 2] {
let tri = &mesh.indices[(t as usize) * 3..(t as usize) * 3 + 3];
let a = tri[((corner_idx + 1) % 3) as usize];
let b = tri[((corner_idx + 2) % 3) as usize];
[a, b]
};
for i in 0..n {
let (t_i, k_i) = incident[i];
let n_i = face_normals[t_i as usize]
.try_normalize(1e-12)
.unwrap_or_else(Vector3::zeros);
if n_i == Vector3::zeros() {
continue;
}
let oc_i = other_corners(k_i, t_i);
for j in (i + 1)..n {
let (t_j, k_j) = incident[j];
let n_j = face_normals[t_j as usize]
.try_normalize(1e-12)
.unwrap_or_else(Vector3::zeros);
if n_j == Vector3::zeros() {
continue;
}
let oc_j = other_corners(k_j, t_j);
let shares_edge = oc_i[0] == oc_j[0]
|| oc_i[0] == oc_j[1]
|| oc_i[1] == oc_j[0]
|| oc_i[1] == oc_j[1];
if !shares_edge {
continue;
}
if n_i.dot(&n_j) < crease_cos {
continue;
}
let ri = find(&mut parent, i as u32);
let rj = find(&mut parent, j as u32);
if ri != rj {
parent[ri as usize] = rj;
}
}
}
let mut group_to_new_vertex: FxHashMap<u32, u32> = FxHashMap::default();
for i in 0..n {
let root = find(&mut parent, i as u32);
let new_v = *group_to_new_vertex.entry(root).or_insert_with(|| {
let new_idx = (new_positions.len() / 3) as u32;
new_positions.push(mesh.positions[v * 3]);
new_positions.push(mesh.positions[v * 3 + 1]);
new_positions.push(mesh.positions[v * 3 + 2]);
new_normals.push(0.0);
new_normals.push(0.0);
new_normals.push(0.0);
new_idx
});
let (t_i, _k_i) = incident[i];
let n_i = face_normals[t_i as usize];
new_normals[new_v as usize * 3] += n_i.x as f32;
new_normals[new_v as usize * 3 + 1] += n_i.y as f32;
new_normals[new_v as usize * 3 + 2] += n_i.z as f32;
let (t, k) = incident[i];
corner_to_new_vertex[t as usize * 3 + k as usize] = new_v;
}
}
for chunk in new_normals.chunks_exact_mut(3) {
let len_sq = (chunk[0] * chunk[0] + chunk[1] * chunk[1] + chunk[2] * chunk[2]) as f64;
if len_sq > 1e-24 {
let inv = (1.0 / len_sq.sqrt()) as f32;
chunk[0] *= inv;
chunk[1] *= inv;
chunk[2] *= inv;
} else {
chunk[2] = 1.0;
}
}
let mut new_indices: Vec<u32> = Vec::with_capacity(mesh.indices.len());
for t in 0..tri_count {
new_indices.push(corner_to_new_vertex[t * 3]);
new_indices.push(corner_to_new_vertex[t * 3 + 1]);
new_indices.push(corner_to_new_vertex[t * 3 + 2]);
}
mesh.positions = new_positions;
mesh.normals = new_normals;
mesh.indices = new_indices;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn floor_pow2_is_exact_and_deterministic() {
assert_eq!(floor_pow2(1.0), 1.0);
assert_eq!(floor_pow2(2.0), 2.0);
assert_eq!(floor_pow2(8.0), 8.0);
assert_eq!(floor_pow2(1.9), 1.0);
assert_eq!(floor_pow2(5.657), 4.0);
assert_eq!(floor_pow2(0.2), 0.125);
assert_eq!(floor_pow2(0.0), 0.0);
assert_eq!(floor_pow2(-3.0), 0.0);
for x in [0.3_f64, 1.7, 3.0, 17.9, 1024.0, 1e-3, 1e6] {
let p = floor_pow2(x);
assert!(p > 0.0 && p <= x);
assert_eq!(p.to_bits() & 0x000f_ffff_ffff_ffff, 0, "floor_pow2({x}) not a clean power of two");
}
}
#[test]
fn tri_is_needle_flags_hairline_slivers_not_real_thin_faces() {
let needle = [
Point3::new(4.672253608703613, -1.0, 12.385885238647461),
Point3::new(1.047027587890625, -5.0, 14.07635498046875),
Point3::new(4.672259330749512, -1.0, 12.385882377624512),
];
assert!(tri_is_needle(&needle), "the #1007 diagonal sliver was not flagged");
let real_thin = [
Point3::new(0.0, 0.0, 0.0),
Point3::new(2.0, 0.0, 0.0),
Point3::new(2.0, 0.2, 0.0),
];
assert!(!tri_is_needle(&real_thin), "a real 0.2×2 m sliver was wrongly flagged");
let healthy = [
Point3::new(0.0, 0.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
Point3::new(0.5, 0.9, 0.0),
];
assert!(!tri_is_needle(&healthy));
let collapsed = [Point3::new(1.0, 1.0, 1.0); 3];
assert!(tri_is_needle(&collapsed));
}
#[test]
fn weld_near_coincident_2d_collapses_um_rim_duplicates() {
use nalgebra::Point2;
let ring = vec![
Point2::new(0.0, 0.0),
Point2::new(1.9, 0.0),
Point2::new(1.9, 1.0),
Point2::new(0.000_006_6, 1.0),
Point2::new(0.0, 1.0),
];
let welded = weld_near_coincident_2d(&ring);
assert_eq!(welded.len(), 4, "near-coincident rim duplicate not welded: {welded:?}");
let clean = vec![
Point2::new(0.0, 0.0),
Point2::new(2.0, 0.0),
Point2::new(2.0, 0.2),
Point2::new(0.0, 0.2),
];
assert_eq!(weld_near_coincident_2d(&clean).len(), 4, "a clean ring was over-welded");
}
#[test]
fn weld_near_coincident_2d_keeps_mm_features_on_large_rings() {
use nalgebra::Point2;
let chamfered = vec![
Point2::new(0.0, 0.0),
Point2::new(12.0, 0.0),
Point2::new(12.0, 0.999),
Point2::new(11.999, 1.0), Point2::new(0.0, 1.0),
];
let welded = weld_near_coincident_2d(&chamfered);
assert_eq!(
welded.len(),
5,
"1 mm chamfer on a 12 m ring was over-welded: {welded:?}"
);
let ring = vec![
Point2::new(0.0, 0.0),
Point2::new(12.0, 0.0),
Point2::new(12.0, 1.0),
Point2::new(0.000_02, 1.0), Point2::new(0.0, 1.0),
];
assert_eq!(
weld_near_coincident_2d(&ring).len(),
4,
"µm rim duplicate on a large ring not welded"
);
}
#[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 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);
}
fn cube_for_crease_tests() -> Mesh {
let mut m = Mesh::with_capacity(8, 36);
let n = Vector3::new(0.0, 0.0, 0.0);
let v = |x: f64, y: f64, z: f64| Point3::new(x, y, z);
let corners = [
v(0.0, 0.0, 0.0),
v(1.0, 0.0, 0.0),
v(1.0, 1.0, 0.0),
v(0.0, 1.0, 0.0),
v(0.0, 0.0, 1.0),
v(1.0, 0.0, 1.0),
v(1.0, 1.0, 1.0),
v(0.0, 1.0, 1.0),
];
for p in corners.iter() {
m.add_vertex(*p, n);
}
for tri in [
[0u32, 2, 1],
[0, 3, 2],
[4, 5, 6],
[4, 6, 7],
[0, 1, 5],
[0, 5, 4],
[2, 3, 7],
[2, 7, 6],
[1, 2, 6],
[1, 6, 5],
[3, 0, 4],
[3, 4, 7],
] {
m.add_triangle(tri[0], tri[1], tri[2]);
}
m
}
fn curved_wall(n: usize, r: f64, t: f64, h: f64) -> Mesh {
use std::f64::consts::PI;
let mut m = Mesh::with_capacity(0, 0);
let nrm = Vector3::new(0.0, 0.0, 0.0);
let mut verts = Vec::new();
for i in 0..=n {
let a = (i as f64) / (n as f64) * (PI / 2.0);
let (c, s) = (a.cos(), a.sin());
verts.push(Point3::new(r * c, r * s, 0.0)); verts.push(Point3::new(r * c, r * s, h)); verts.push(Point3::new((r - t) * c, (r - t) * s, 0.0)); verts.push(Point3::new((r - t) * c, (r - t) * s, h)); }
for p in &verts {
m.add_vertex(*p, nrm);
}
let (ob, ot, ib, it) = (
|i: usize| 4 * i as u32,
|i: usize| 4 * i as u32 + 1,
|i: usize| 4 * i as u32 + 2,
|i: usize| 4 * i as u32 + 3,
);
let mut quad = |a: u32, b: u32, c: u32, d: u32, m: &mut Mesh| {
m.add_triangle(a, b, c);
m.add_triangle(a, c, d);
};
for i in 0..n {
quad(ob(i), ob(i + 1), ot(i + 1), ot(i), &mut m); quad(ib(i + 1), ib(i), it(i), it(i + 1), &mut m); quad(ot(i), ot(i + 1), it(i + 1), it(i), &mut m); quad(ib(i), ib(i + 1), ob(i + 1), ob(i), &mut m); }
quad(ob(0), ot(0), it(0), ib(0), &mut m); quad(ib(n), it(n), ot(n), ob(n), &mut m); m
}
fn axis_box(lo: [f64; 3], hi: [f64; 3]) -> Mesh {
let mut m = Mesh::with_capacity(8, 36);
let n = Vector3::new(0.0, 0.0, 0.0);
let c = [
Point3::new(lo[0], lo[1], lo[2]),
Point3::new(hi[0], lo[1], lo[2]),
Point3::new(hi[0], hi[1], lo[2]),
Point3::new(lo[0], hi[1], lo[2]),
Point3::new(lo[0], lo[1], hi[2]),
Point3::new(hi[0], lo[1], hi[2]),
Point3::new(hi[0], hi[1], hi[2]),
Point3::new(lo[0], hi[1], hi[2]),
];
for p in c.iter() {
m.add_vertex(*p, n);
}
for tri in [
[0u32, 2, 1], [0, 3, 2], [4, 5, 6], [4, 6, 7],
[0, 1, 5], [0, 5, 4], [2, 3, 7], [2, 7, 6],
[1, 2, 6], [1, 6, 5], [3, 0, 4], [3, 4, 7],
] {
m.add_triangle(tri[0], tri[1], tri[2]);
}
m
}
fn count_open_edges(mesh: &Mesh) -> usize {
use std::collections::HashMap;
let q = |v: f32| (v as f64 * 1.0e6).round() as i64;
let mut vid: HashMap<(i64, i64, i64), u32> = HashMap::new();
let mut id = |i: usize| -> u32 {
let k = (
q(mesh.positions[i * 3]),
q(mesh.positions[i * 3 + 1]),
q(mesh.positions[i * 3 + 2]),
);
let n = vid.len() as u32;
*vid.entry(k).or_insert(n)
};
let mut edge: HashMap<(u32, u32), i32> = HashMap::new();
for tri in mesh.indices.chunks_exact(3) {
let (a, b, c) = (id(tri[0] as usize), id(tri[1] as usize), id(tri[2] as usize));
for (x, y) in [(a, b), (b, c), (c, a)] {
let (k, s) = if x < y { ((x, y), 1) } else { ((y, x), -1) };
*edge.entry(k).or_insert(0) += s;
}
}
edge.values().filter(|&&v| v != 0).count()
}
#[test]
fn curved_wall_opening_seam_is_watertight() {
let host = curved_wall(8, 5.0, 0.3, 3.0); assert_eq!(count_open_edges(&host), 0, "host must be watertight");
let cutter = axis_box([2.4, 2.4, 1.0], [4.4, 4.4, 2.0]);
let raw = crate::kernel::mesh_bridge::subtract(&host, &cutter);
let raw_open = count_open_edges(&raw);
let consolidated = ClippingProcessor::consolidate_coplanar(raw.clone());
let cons_open = count_open_edges(&consolidated);
eprintln!(
"SEAMTEST raw_tris={} raw_open={} cons_tris={} cons_open={}",
raw.triangle_count(),
raw_open,
consolidated.triangle_count(),
cons_open
);
assert_eq!(raw_open, 0, "raw kernel output must be watertight");
assert_eq!(
cons_open, 0,
"consolidate must preserve the curved-wall opening seam (was torn)"
);
}
#[test]
fn crease_split_keeps_cube_corners_crisp() {
let mut cube = cube_for_crease_tests();
smooth_normals_with_creases(&mut cube, 0.866); assert_eq!(
cube.positions.len() / 3,
24,
"expected one vertex per (corner, face): 8 corners × 3 faces = 24, got {}",
cube.positions.len() / 3,
);
for chunk in cube.normals.chunks_exact(3) {
let nx = chunk[0].abs();
let ny = chunk[1].abs();
let nz = chunk[2].abs();
let nontrivial = [nx, ny, nz].iter().filter(|&&v| v > 0.5).count();
assert_eq!(
nontrivial, 1,
"vertex normal ({nx:.3}, {ny:.3}, {nz:.3}) leaked across crease",
);
}
}
#[test]
fn crease_keeps_coplanar_quad_as_4_verts() {
let mut quad = Mesh::with_capacity(4, 6);
let n = Vector3::new(0.0, 0.0, 0.0);
let v = |x: f64, y: f64| Point3::new(x, y, 0.0);
quad.add_vertex(v(0.0, 0.0), n);
quad.add_vertex(v(1.0, 0.0), n);
quad.add_vertex(v(1.0, 1.0), n);
quad.add_vertex(v(0.0, 1.0), n);
quad.add_triangle(0, 1, 2);
quad.add_triangle(0, 2, 3);
smooth_normals_with_creases(&mut quad, 0.866);
assert_eq!(
quad.positions.len() / 3,
4,
"coplanar quad must keep 4 shared verts, got {}",
quad.positions.len() / 3,
);
for chunk in quad.normals.chunks_exact(3) {
assert!((chunk[0]).abs() < 1e-5);
assert!((chunk[1]).abs() < 1e-5);
assert!((chunk[2] - 1.0).abs() < 1e-5);
}
}
}