use nalgebra::{Matrix4, Point2, Vector3};
use super::geom::{mesh_is_closed_exact, mesh_signed_volume, opening_mesh_thinnest_axis_dir};
use super::sweep::cut_changed_mesh;
use super::{OpeningType, NORMALIZE_EPSILON};
use crate::bool2d::union_contours_to_shapes;
use crate::csg::ClippingProcessor;
use crate::extrusion::{apply_transform, extrude_profile_watertight};
use crate::mesh::Mesh;
use crate::router::GeometryRouter;
#[cfg(test)]
thread_local! {
static ENABLED_OVERRIDE: std::cell::Cell<Option<bool>> = const { std::cell::Cell::new(None) };
}
#[cfg(test)]
pub(super) fn set_enabled_override(v: Option<bool>) {
ENABLED_OVERRIDE.with(|c| c.set(v));
}
pub(super) fn enabled() -> bool {
#[cfg(test)]
{
if let Some(v) = ENABLED_OVERRIDE.with(|c| c.get()) {
return v;
}
}
use std::sync::OnceLock;
static ON: OnceLock<bool> = OnceLock::new();
*ON.get_or_init(|| std::env::var("IFC_LITE_VOID_UNION").as_deref() != Ok("0"))
}
struct UnionCand {
idx: usize,
mesh: Mesh,
dir: Vector3<f64>,
lo: [f64; 3],
hi: [f64; 3],
}
struct Footprint {
contours: Vec<Vec<Point2<f64>>>,
z_lo: f64,
z_hi: f64,
}
impl GeometryRouter {
#[allow(clippy::too_many_arguments)]
pub(super) fn coaxial_union_prepass(
&self,
result: &mut Mesh,
all_openings: &[&OpeningType],
consumed: &mut [bool],
host_mutated: &mut bool,
clipper: &ClippingProcessor,
) {
if !enabled() || all_openings.len() < 2 {
return;
}
const PAD: f64 = 1.0e-3;
let (hmn, hmx) = result.bounds();
let min_vol = Self::min_opening_volume(self.tessellation_quality);
let mut cands: Vec<UnionCand> = Vec::new();
for (idx, opening) in all_openings.iter().enumerate() {
if consumed[idx] {
continue;
}
let (mesh, dir_hint): (&Mesh, Option<Vector3<f64>>) = match **opening {
OpeningType::Rectangular(..) => continue,
OpeningType::DiagonalRectangular(ref m, ref f) => (m, Some(f.depth)),
OpeningType::NonRectangular(ref m, _, _, ref d) => (m, *d),
};
let valid = !mesh.is_empty()
&& mesh.positions.iter().all(|&v| v.is_finite())
&& mesh.positions.len() >= 9;
if !valid {
continue;
}
let (omn, omx) = mesh.bounds();
let overlaps_host = !(omx.x < hmn.x
|| omn.x > hmx.x
|| omx.y < hmn.y
|| omn.y > hmx.y
|| omx.z < hmn.z
|| omn.z > hmx.z);
if !overlaps_host {
continue;
}
let open_vol =
(omx.x - omn.x) as f64 * (omy_span(omn.y, omx.y)) * (omx.z - omn.z) as f64;
if open_vol < min_vol {
continue;
}
let dir = dir_hint
.filter(|d| d.norm() > NORMALIZE_EPSILON)
.map(|d| d.normalize())
.unwrap_or_else(|| opening_mesh_thinnest_axis_dir(mesh).normalize());
if !dir.iter().all(|c| c.is_finite()) || dir.norm() < 0.5 {
continue;
}
let ext = Self::extend_opening_mesh_through_host(mesh, result, dir);
let (elo, ehi) = ext.bounds();
cands.push(UnionCand {
idx,
mesh: mesh.clone(),
dir,
lo: [elo.x as f64 - PAD, elo.y as f64 - PAD, elo.z as f64 - PAD],
hi: [ehi.x as f64 + PAD, ehi.y as f64 + PAD, ehi.z as f64 + PAD],
});
}
if cands.len() < 2 {
return;
}
let mut parent: Vec<usize> = (0..cands.len()).collect();
fn find(parent: &mut [usize], mut x: usize) -> usize {
while parent[x] != x {
parent[x] = parent[parent[x]];
x = parent[x];
}
x
}
for a in 0..cands.len() {
for b in (a + 1)..cands.len() {
if aabb_overlap(&cands[a].lo, &cands[a].hi, &cands[b].lo, &cands[b].hi) {
let ra = find(&mut parent, a);
let rb = find(&mut parent, b);
if ra != rb {
parent[ra.max(rb)] = ra.min(rb);
}
}
}
}
let mut components: Vec<(usize, Vec<usize>)> = Vec::new();
for i in 0..cands.len() {
let r = find(&mut parent, i);
match components.iter_mut().find(|(root, _)| *root == r) {
Some((_, v)) => v.push(i),
None => components.push((r, vec![i])),
}
}
for (_, members) in &components {
if members.len() < 2 {
continue; }
self.cut_overlapping_cluster(result, &cands, members, consumed, host_mutated, clipper);
}
}
#[allow(clippy::too_many_arguments)]
fn cut_overlapping_cluster(
&self,
result: &mut Mesh,
cands: &[UnionCand],
members: &[usize],
consumed: &mut [bool],
host_mutated: &mut bool,
clipper: &ClippingProcessor,
) {
const COAXIAL_DOT_MIN: f64 = 1.0 - 1.0e-6;
let ref_dir = cands[members[0]].dir;
let coaxial = members
.iter()
.all(|&m| cands[m].dir.dot(&ref_dir).abs() >= COAXIAL_DOT_MIN);
if coaxial {
if let Some((prisms, multi_slab, contributors, max_removed)) =
self.build_coaxial_prisms(result, cands, members, &ref_dir)
{
if self.subtract_prisms(result, &prisms, multi_slab, max_removed, clipper) {
for &i in &contributors {
consumed[cands[members[i]].idx] = true;
}
*host_mutated = true;
return;
}
}
}
if self.subtract_union3d(result, cands, members, clipper) {
for &m in members {
consumed[cands[m].idx] = true;
}
*host_mutated = true;
}
}
fn build_coaxial_prisms(
&self,
result: &Mesh,
cands: &[UnionCand],
members: &[usize],
axis: &Vector3<f64>,
) -> Option<(Vec<Mesh>, bool, Vec<usize>, f64)> {
let (u, v, d) = ortho_frame(axis)?;
let mut fps: Vec<Footprint> = Vec::with_capacity(members.len());
for &m in members {
fps.push(cutter_footprint(&cands[m].mesh, &u, &v, &d)?);
}
let span_lo = fps.iter().map(|f| f.z_lo).fold(f64::INFINITY, f64::min);
let span_hi = fps.iter().map(|f| f.z_hi).fold(f64::NEG_INFINITY, f64::max);
if !span_lo.is_finite() || !span_hi.is_finite() || (span_hi - span_lo) <= NORMALIZE_EPSILON {
return None;
}
let z_tol = (span_hi - span_lo) * 0.01 + 1.0e-3;
let mut breaks: Vec<f64> = fps.iter().flat_map(|f| [f.z_lo, f.z_hi]).collect();
breaks.sort_by(f64::total_cmp);
let mut coalesced: Vec<f64> = Vec::with_capacity(breaks.len());
for b in breaks {
if coalesced.last().is_none_or(|&p| (b - p).abs() > z_tol) {
coalesced.push(b);
}
}
if coalesced.len() < 2 || coalesced.len() > 9 {
return None;
}
let m_world = Matrix4::new(
u.x, v.x, d.x, 0.0, u.y, v.y, d.y, 0.0, u.z, v.z, d.z, 0.0, 0.0, 0.0, 0.0, 1.0,
);
let mut prisms: Vec<Mesh> = Vec::new();
let mut contributed = vec![false; fps.len()];
for w in coalesced.windows(2) {
let (slab_lo, slab_hi) = (w[0], w[1]);
let slab_depth = slab_hi - slab_lo;
if slab_depth <= NORMALIZE_EPSILON {
continue;
}
let mid = 0.5 * (slab_lo + slab_hi);
let mut contours: Vec<Vec<Point2<f64>>> = Vec::new();
for (i, f) in fps.iter().enumerate() {
if f.z_lo <= mid && f.z_hi >= mid {
contours.extend(f.contours.iter().cloned());
contributed[i] = true;
}
}
if contours.is_empty() {
continue; }
let shapes = union_contours_to_shapes(&contours);
if shapes.is_empty() {
return None;
}
let base = Matrix4::new_translation(&Vector3::new(0.0, 0.0, slab_lo));
for shape in &shapes {
if shape.outer.len() < 3 || !crate::bool2d::is_valid_contour(&shape.outer) {
return None;
}
let mut prism = extrude_profile_watertight(shape, slab_depth, Some(base)).ok()?;
apply_transform(&mut prism, &m_world);
let ext = Self::extend_opening_mesh_through_host(&prism, result, d);
if !mesh_is_closed_exact(&ext) {
return None;
}
prisms.push(ext);
}
}
if prisms.is_empty() {
return None;
}
let multi_slab = coalesced.len() > 2;
let contributors: Vec<usize> = (0..fps.len()).filter(|&i| contributed[i]).collect();
let max_removed = 1.02
* contributors
.iter()
.map(|&i| {
let ext = Self::extend_opening_mesh_through_host(
&cands[members[i]].mesh,
result,
d,
);
mesh_signed_volume(&ext).abs()
})
.sum::<f64>();
Some((prisms, multi_slab, contributors, max_removed))
}
fn subtract_prisms(
&self,
result: &mut Mesh,
prisms: &[Mesh],
multi_slab: bool,
max_removed: f64,
clipper: &ClippingProcessor,
) -> bool {
let tri_before = result.triangle_count();
let vol_before = mesh_signed_volume(result);
if !multi_slab {
let cutters: Vec<&Mesh> = prisms.iter().collect();
if let Ok(cut) = clipper.subtract_mesh_many(result, &cutters) {
return accept_cut(result, cut, tri_before, vol_before, max_removed);
}
return false;
}
let cutters: Vec<&Mesh> = prisms.iter().collect();
let union = ClippingProcessor::consolidate_coplanar(
crate::kernel::mesh_bridge::union_many(&cutters),
);
if union.is_empty() || !mesh_is_closed_exact(&union) {
return false;
}
let Ok(cut) = clipper.subtract_mesh(result, &union) else {
return false;
};
accept_cut(result, cut, tri_before, vol_before, max_removed)
}
fn subtract_union3d(
&self,
result: &mut Mesh,
cands: &[UnionCand],
members: &[usize],
clipper: &ClippingProcessor,
) -> bool {
let extended: Vec<Mesh> = members
.iter()
.map(|&m| Self::extend_opening_mesh_through_host(&cands[m].mesh, result, cands[m].dir))
.collect();
let refs: Vec<&Mesh> = extended.iter().collect();
let union = ClippingProcessor::consolidate_coplanar(crate::kernel::mesh_bridge::union_many(
&refs,
));
if union.is_empty() || !mesh_is_closed_exact(&union) {
return false;
}
let tri_before = result.triangle_count();
let vol_before = mesh_signed_volume(result);
let Ok(cut) = clipper.subtract_mesh(result, &union) else {
return false;
};
accept_cut(result, cut, tri_before, vol_before, f64::INFINITY)
}
}
#[inline]
fn omy_span(lo: f32, hi: f32) -> f64 {
(hi - lo) as f64
}
fn accept_cut(
result: &mut Mesh,
cut: Mesh,
tri_before: usize,
vol_before: f64,
max_removed: f64,
) -> bool {
use super::{CSG_TRIANGLE_RETENTION_DIVISOR, MIN_VALID_TRIANGLES};
let min_tris = (tri_before / CSG_TRIANGLE_RETENTION_DIVISOR).max(MIN_VALID_TRIANGLES);
let changed = cut_changed_mesh(&cut, tri_before, vol_before);
let removed = vol_before - mesh_signed_volume(&cut);
if !cut.is_empty() && cut.triangle_count() >= min_tris && changed && removed <= max_removed {
*result = cut;
true
} else {
false
}
}
#[inline]
fn aabb_overlap(alo: &[f64; 3], ahi: &[f64; 3], blo: &[f64; 3], bhi: &[f64; 3]) -> bool {
!(ahi[0] < blo[0]
|| alo[0] > bhi[0]
|| ahi[1] < blo[1]
|| alo[1] > bhi[1]
|| ahi[2] < blo[2]
|| alo[2] > bhi[2])
}
fn ortho_frame(axis: &Vector3<f64>) -> Option<(Vector3<f64>, Vector3<f64>, Vector3<f64>)> {
let d = axis.try_normalize(1e-12)?;
let ax = [d.x.abs(), d.y.abs(), d.z.abs()];
let mut min_i = 0usize;
for i in 1..3 {
if ax[i].total_cmp(&ax[min_i]) == std::cmp::Ordering::Less {
min_i = i;
}
}
let helper = match min_i {
0 => Vector3::new(1.0, 0.0, 0.0),
1 => Vector3::new(0.0, 1.0, 0.0),
_ => Vector3::new(0.0, 0.0, 1.0),
};
let u = helper.cross(&d).try_normalize(1e-12)?;
let v = d.cross(&u);
if !u.iter().chain(v.iter()).chain(d.iter()).all(|c| c.is_finite()) {
return None;
}
Some((u, v, d))
}
fn cutter_footprint(
mesh: &Mesh,
u: &Vector3<f64>,
v: &Vector3<f64>,
d: &Vector3<f64>,
) -> Option<Footprint> {
let vc = mesh.positions.len() / 3;
if vc < 3 {
return None;
}
let vat = |i: u32| -> [f64; 3] {
let b = i as usize * 3;
[
mesh.positions[b] as f64,
mesh.positions[b + 1] as f64,
mesh.positions[b + 2] as f64,
]
};
let mut contours: Vec<Vec<Point2<f64>>> = Vec::new();
let mut z_lo = f64::INFINITY;
let mut z_hi = f64::NEG_INFINITY;
for c in mesh.positions.chunks_exact(3) {
let s = c[0] as f64 * d.x + c[1] as f64 * d.y + c[2] as f64 * d.z;
z_lo = z_lo.min(s);
z_hi = z_hi.max(s);
}
if !z_lo.is_finite() || !z_hi.is_finite() || (z_hi - z_lo) <= NORMALIZE_EPSILON {
return None;
}
for t in mesh.indices.chunks_exact(3) {
if (t[0] as usize) >= vc || (t[1] as usize) >= vc || (t[2] as usize) >= vc {
continue;
}
let (a, b, c) = (vat(t[0]), vat(t[1]), vat(t[2]));
let e1 = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
let e2 = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
let n = [
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 nl = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
if nl < 1e-12 {
continue;
}
let nd = (n[0] * d.x + n[1] * d.y + n[2] * d.z) / nl;
if nd.abs() < 0.985 {
continue;
}
let proj = |p: [f64; 3]| {
Point2::new(
p[0] * u.x + p[1] * u.y + p[2] * u.z,
p[0] * v.x + p[1] * v.y + p[2] * v.z,
)
};
let tri = [proj(a), proj(b), proj(c)];
contours.push(crate::bool2d::ensure_ccw(&tri));
}
if contours.is_empty() {
return None;
}
let cap_area_sum: f64 = contours
.iter()
.map(|c| crate::bool2d::compute_signed_area(c).abs())
.sum();
let area = 0.5 * cap_area_sum;
let depth = z_hi - z_lo;
if area <= NORMALIZE_EPSILON || depth <= NORMALIZE_EPSILON {
return None;
}
let mesh_vol = mesh_signed_volume(mesh).abs();
let recon_vol = area * depth;
if (recon_vol - mesh_vol).abs() > 0.05 * recon_vol.max(mesh_vol) {
return None;
}
Some(Footprint {
contours,
z_lo,
z_hi,
})
}
#[cfg(test)]
#[path = "coaxial_union_tests.rs"]
mod tests;