use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::OnceLock;
use super::geom::{mesh_signed_volume, param_cut_watertight};
use super::{world_host_bounds, GeometryRouter, VoidContext};
use crate::bool2d::{compute_signed_area, subtract_multiple_2d_counted};
use crate::extrusion::{apply_transform, extrude_profile, extrude_profile_watertight};
use crate::mesh::Mesh;
use crate::profile::Profile2D;
use ifc_lite_core::{DecodedEntity, EntityDecoder, IfcType};
use nalgebra::{Matrix4, Point2, Point3, Vector3};
pub(super) fn enabled() -> bool {
static ON: OnceLock<bool> = OnceLock::new();
*ON.get_or_init(|| std::env::var("IFC_LITE_VOID_2D").as_deref() != Ok("0"))
}
static FIRES: AtomicU64 = AtomicU64::new(0);
static FOOTPRINTS: AtomicU64 = AtomicU64::new(0);
pub fn take_bool2d_stats() -> (u64, u64) {
(
FIRES.swap(0, Ordering::Relaxed),
FOOTPRINTS.swap(0, Ordering::Relaxed),
)
}
pub(super) struct Bool2dCut {
host_profile: Profile2D,
depth: f64,
dir_sign: f64,
wt: Matrix4<f64>,
footprints: Vec<Vec<Point2<f64>>>,
residual: Option<Box<VoidContext>>,
}
#[inline]
fn area_abs(poly: &[Point2<f64>]) -> f64 {
compute_signed_area(poly).abs()
}
fn footprint_interior(fp: &[Point2<f64>], profile: &Profile2D) -> bool {
fp.iter().all(|v| {
crate::bool2d::point_in_contour(v, &profile.outer)
&& profile
.holes
.iter()
.all(|h| !crate::bool2d::point_in_contour(v, h))
})
}
impl GeometryRouter {
pub(super) fn capture_bool2d(
&self,
element: &DecodedEntity,
opening_ids: &[u32],
decoder: &mut EntityDecoder,
) -> Option<Bool2dCut> {
if opening_ids.is_empty() {
return None;
}
let host = self.host_extruded_solid(element, decoder)?;
let hm_inv = host.m.try_inverse()?;
let host_rot = host.m.fixed_view::<3, 3>(0, 0).into_owned();
let host_axis = (host_rot * Vector3::new(0.0, 0.0, host.dir_sign)).try_normalize(1e-9)?;
let (hz_min, hz_max) = if host.dir_sign >= 0.0 {
(0.0, host.depth)
} else {
(-host.depth, 0.0)
};
let z_tol = 1.0e-6 * host.depth.abs() + 1.0e-9;
let mut footprints: Vec<Vec<Point2<f64>>> = Vec::new();
let mut residual_ids: Vec<u32> = Vec::new();
for &oid in opening_ids {
let opening = match decoder.decode_by_id(oid) {
Ok(e) if e.ifc_type == IfcType::IfcOpeningElement => e,
_ => {
residual_ids.push(oid);
continue;
}
};
let mut opening_footprints: Vec<Vec<Point2<f64>>> = Vec::new();
let eligible = self
.opening_extruded_solids(&opening, decoder)
.map(|solids| {
solids.iter().all(|op| {
match opening_solid_footprint(
op, &hm_inv, &host_axis, hz_min, hz_max, z_tol,
) {
Some(fp) if footprint_interior(&fp, &host.profile) => {
opening_footprints.push(fp);
true
}
_ => false,
}
})
})
.unwrap_or(false);
if eligible && !opening_footprints.is_empty() {
footprints.append(&mut opening_footprints);
} else {
residual_ids.push(oid);
}
}
if footprints.is_empty() {
return None;
}
let s = self.unit_scale;
let (rx, ry, rz) = self.rtc_offset;
let mut wt = host.m;
for i in 0..3 {
for j in 0..4 {
wt[(i, j)] *= s;
}
}
wt[(0, 3)] -= rx;
wt[(1, 3)] -= ry;
wt[(2, 3)] -= rz;
let residual = if residual_ids.is_empty() {
None
} else {
let openings = self.classify_openings_quiet(element, &residual_ids, decoder);
if openings.is_empty() {
None
} else {
let merged = Self::merge_rectangular_openings(&openings);
Some(Box::new(VoidContext {
openings,
merged_openings: merged,
param: None,
bool2d: None,
}))
}
};
Some(Bool2dCut {
host_profile: host.profile,
depth: host.depth,
dir_sign: host.dir_sign,
wt,
footprints,
residual,
})
}
pub(super) fn try_bool2d_cut(&self, mesh: &Mesh, cut: &Bool2dCut) -> Option<Mesh> {
let transform = if cut.dir_sign >= 0.0 {
None
} else {
Some(Matrix4::new_translation(&Vector3::new(
0.0, 0.0, -cut.depth,
)))
};
let mut solid = extrude_profile(&cut.host_profile, cut.depth, transform).ok()?;
apply_transform(&mut solid, &cut.wt);
if !reconcile_solid(mesh, &solid) {
return None;
}
let (holed, n_shapes) =
subtract_multiple_2d_counted(&cut.host_profile, &cut.footprints).ok()?;
if n_shapes != 1 {
return None;
}
if holed.holes.len() < cut.host_profile.holes.len() + 1 {
return None;
}
let origin = mesh.origin;
let mut wt = cut.wt;
wt[(0, 3)] -= origin[0];
wt[(1, 3)] -= origin[1];
wt[(2, 3)] -= origin[2];
let mut out = extrude_profile_watertight(&holed, cut.depth, transform).ok()?;
apply_transform(&mut out, &wt);
out.origin = origin;
out.clean_degenerate();
if !param_cut_watertight(&out) {
return None;
}
FIRES.fetch_add(1, Ordering::Relaxed);
FOOTPRINTS.fetch_add(cut.footprints.len() as u64, Ordering::Relaxed);
Some(out)
}
pub(super) fn bool2d_residual<'a>(&self, cut: &'a Bool2dCut) -> Option<&'a VoidContext> {
cut.residual.as_deref()
}
}
fn opening_solid_footprint(
op: &ExtrudedSolidLike,
hm_inv: &Matrix4<f64>,
host_axis: &Vector3<f64>,
hz_min: f64,
hz_max: f64,
z_tol: f64,
) -> Option<Vec<Point2<f64>>> {
if !op.profile.holes.is_empty() {
return None;
}
let op_rot = op.m.fixed_view::<3, 3>(0, 0).into_owned();
let op_axis = (op_rot * Vector3::new(0.0, 0.0, op.dir_sign)).try_normalize(1e-9)?;
if host_axis.dot(&op_axis).abs() < 1.0 - 1.0e-6 {
return None; }
let to_host = hm_inv * op.m;
let mut fp: Vec<Point2<f64>> = Vec::with_capacity(op.profile.outer.len());
let mut zmin = f64::INFINITY;
let mut zmax = f64::NEG_INFINITY;
let lat_tol = 1.0e-4 * (hz_max - hz_min).abs() + 1.0e-6;
for p in &op.profile.outer {
let base = to_host.transform_point(&Point3::new(p.x, p.y, 0.0));
let far = to_host.transform_point(&Point3::new(p.x, p.y, op.dir_sign * op.depth));
if (far.x - base.x).abs() > lat_tol || (far.y - base.y).abs() > lat_tol {
return None;
}
fp.push(Point2::new(base.x, base.y));
zmin = zmin.min(base.z.min(far.z));
zmax = zmax.max(base.z.max(far.z));
}
if zmin > hz_min + z_tol || zmax < hz_max - z_tol {
return None;
}
if area_abs(&fp) <= 0.0 {
return None;
}
Some(fp)
}
use super::probe::ExtrudedSolidInfo as ExtrudedSolidLike;
fn reconcile_solid(host: &Mesh, solid: &Mesh) -> bool {
let (hmn, hmx) = world_host_bounds(host); let (smn, smx) = solid.bounds(); let hmn = [hmn.0, hmn.1, hmn.2];
let hmx = [hmx.0, hmx.1, hmx.2];
let smn = [smn.x, smn.y, smn.z];
let smx = [smx.x, smx.y, smx.z];
for k in 0..3 {
let ext = (hmx[k] - hmn[k]).abs().max(1e-3);
let tol = ext * 0.02 + 5e-3; if (hmn[k] - smn[k]).abs() > tol || (hmx[k] - smx[k]).abs() > tol {
return false;
}
}
let hv = mesh_signed_volume(host).abs();
let sv = mesh_signed_volume(solid).abs();
if hv < 1e-9 || sv < 1e-9 {
return false;
}
let r = sv / hv;
(0.97..1.03).contains(&r)
}
#[cfg(test)]
#[path = "bool2d_path_tests.rs"]
mod tests;