use crate::mesh::Mesh;
use nalgebra::{Point3, Vector3};
mod conform;
mod ring_ops;
use super::ClippingProcessor;
use conform::{build_seam_map, conform_plans, count_open_boundary_edges_at, emit_plans, PlanBucket, PlanRegion};
use ring_ops::{floor_pow2, simplify_2d_collinear, weld_near_coincident_2d};
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)
}
pub(super) 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 {
count_open_boundary_edges_at(mesh, 1.0e3)
}
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
}
impl ClippingProcessor {
pub(crate) fn consolidate_coplanar(mesh: Mesh) -> Mesh {
use crate::grid::NORMAL_QUANT_F64 as NORMAL_QUANT;
use crate::triangulation::project_to_2d_with_basis;
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;
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: std::collections::BTreeMap<(i64, i64, i64, i64), Vec<PlaneTri>> =
std::collections::BTreeMap::new();
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 plans: Vec<PlanBucket> = Vec::with_capacity(buckets.len());
for (bid, tris) in buckets.values().enumerate() {
if tris.is_empty() {
continue;
}
let bid = bid as u32;
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();
let mut plan = PlanBucket {
bid,
normal,
origin,
u_axis,
v_axis,
raw: Vec::new(),
regions: Vec::new(),
};
if tris.len() == 1 {
plan.raw.push(tris[0].v);
plans.push(plan);
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 {
plan.raw.push(t.v);
}
plans.push(plan);
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();
plan.regions.push(PlanRegion {
changed: false,
cached: None,
outer_conformed: outer_simplified.clone(),
holes_conformed: holes_simplified.clone(),
outer: outer_simplified,
holes: holes_simplified,
});
}
plans.push(plan);
}
let (mut output, base_complete) = emit_plans(&mut plans, false);
if !base_complete {
return mesh;
}
if count_open_boundary_edges_at(&output, 1.0e4) > 0 {
let seam = build_seam_map(&plans);
if conform_plans(&mut plans, &seam) {
let (candidate, complete) = emit_plans(&mut plans, true);
if complete
&& !candidate.is_empty()
&& count_open_boundary_edges_at(&candidate, 1.0e4) == 0
{
output = candidate;
}
}
}
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.rtc_applied = mesh.rtc_applied;
output.origin = mesh.origin;
output.local_bounds = mesh.local_bounds;
output.local_to_world = mesh.local_to_world;
output
}
}
#[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 consolidate_preserves_local_frame_origin() {
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);
mesh.origin = [11.85, 5.0, 1.35];
mesh.rtc_applied = true;
let consolidated = ClippingProcessor::consolidate_coplanar(mesh);
assert_eq!(
consolidated.indices.len() / 3,
2,
"test precondition: the mesh must consolidate so the rebuilt-output frame carry is exercised"
);
assert_eq!(
consolidated.origin,
[11.85, 5.0, 1.35],
"consolidate_coplanar dropped the local-frame origin — the cut host would render at the world origin"
);
assert!(
consolidated.rtc_applied,
"consolidate_coplanar dropped rtc_applied"
);
}
#[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
);
}
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 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)"
);
}
}