ifc_lite_geometry/router/voids/
mod.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Void (opening) subtraction: 3D CSG, AABB clipping, and triangle-box intersection.
6
7use super::processing::SourceHygiene;
8use super::GeometryRouter;
9use crate::csg::{ClippingProcessor, GroupCut};
10use crate::mesh::{SubMesh, SubMeshCollection};
11use crate::{Mesh, Point3, Result, TessellationQuality, Vector3};
12use ifc_lite_core::{DecodedEntity, EntityDecoder, IfcType};
13use nalgebra::Matrix3;
14use rustc_hash::FxHashMap;
15
16mod aabb_clip;
17mod batch_cutter;
18mod bool2d_path;
19mod coaxial_union;
20pub(crate) mod geom;
21mod malformed_opening_repair;
22mod local_frame;
23mod frame_snap;
24pub(crate) mod prism_cut;
25mod probe;
26mod representation;
27mod staged;
28mod synthesis;
29pub use bool2d_path::take_bool2d_stats;
30pub use prism_cut::{take_prism_defers, take_prism_stats};
31#[cfg(test)]
32mod reveal_tests;
33
34use geom::*;
35use malformed_opening_repair::{
36    cutter_is_closed_manifold, opening_obb_if_malformed, recut_malformed_openings,
37    translate_cutter_mesh, world_host_bounds, OpeningBox,
38};
39use local_frame::{host_thickness_wall_frame, vertical_depth_wall_frame};
40use sweep::{drop_faces_outside_host, mesh_to_keep};
41mod sweep;
42
43/// Epsilon for normalizing direction vectors (guards against zero-length).
44const NORMALIZE_EPSILON: f64 = 1e-12;
45/// |cos| above which an authored cutter depth counts as the wall normal itself
46/// (matches the vertical selector's 0.98 axis tolerance in `local_frame.rs`).
47const WALL_NORMAL_DEPTH_COS: f64 = 0.98;
48/// Minimum opening volume (m³) below which CSG is skipped (degenerate-void filter).
49/// 0.0001 m³ ≈ 0.1 litre — filters artefacts while allowing small real openings (e.g. sleeves).
50const MIN_OPENING_VOLUME: f64 = 0.0001;
51/// Fraction of pre-CSG triangles the result must retain. CSG outputs with fewer
52/// triangles than `pre_count / CSG_TRIANGLE_RETENTION_DIVISOR` are rejected as
53/// kernel blowups.
54const CSG_TRIANGLE_RETENTION_DIVISOR: usize = 4;
55/// Minimum triangle count for a valid CSG result.
56const MIN_VALID_TRIANGLES: usize = 4;
57/// A mixed 2D + residual composition may inherit a small number of imperfect
58/// IFC seams, but a large absolute increase signals that the second route tore
59/// the re-extruded host. Below this floor the relative comparison still rules.
60const MIXED_ROUTE_DEFECT_FLOOR: usize = 64;
61const MIXED_ROUTE_DEFECT_GROWTH: usize = 4;
62/// Maximum wrapper depth when drilling through mapped/boolean items to find an extrusion.
63const MAX_EXTRUSION_EXTRACT_DEPTH: usize = 32;
64/// Per-axis AABB engulf slack shared by the batched, host-consumed, and
65/// rectangular-fallback engulf checks below, so the three guards can't drift
66/// apart on what counts as "engulfs the host".
67const ENGULF_TOLERANCE: f64 = 0.03;
68const ENGULF_TOLERANCE_F32: f32 = 0.03;
69
70/// Classification of an opening for void subtraction.
71#[derive(Clone)]
72enum OpeningType {
73    /// Rectangular opening with AABB clipping
74    /// Fields: (min_bounds, max_bounds, extrusion_direction)
75    Rectangular(Point3<f64>, Point3<f64>, Option<Vector3<f64>>),
76    /// Diagonal rectangular opening with mesh geometry and a full oriented frame.
77    /// The frame preserves roof-window roll, not just the penetration direction.
78    DiagonalRectangular(Mesh, OpeningFrame),
79    /// Non-rectangular opening (circular, arched, or floor openings with
80    /// rotated footprint). Uses full CSG subtraction with the actual mesh
81    /// geometry. The AABB + extrusion direction are kept so that callers can
82    /// fall back to a rectangular box cut when CSG can't run (issue #635 —
83    /// e.g. circular windows whose triangulated profile blows past
84    /// `MAX_CSG_POLYGONS_PER_MESH`).
85    NonRectangular(Mesh, Point3<f64>, Point3<f64>, Option<Vector3<f64>>),
86}
87
88/// World-space basis for an oriented rectangular opening.
89#[derive(Clone, Copy)]
90struct OpeningFrame {
91    depth: Vector3<f64>,
92    cross_a: Vector3<f64>,
93    cross_b: Vector3<f64>,
94    depth_is_authored: bool,
95}
96
97impl OpeningFrame {
98    fn from_depth(depth: Vector3<f64>) -> Option<Self> {
99        let depth = depth.try_normalize(NORMALIZE_EPSILON)?;
100        let seed = if depth.z.abs() < 0.9 {
101            Vector3::new(0.0, 0.0, 1.0)
102        } else {
103            Vector3::new(0.0, 1.0, 0.0)
104        };
105        let cross_a = seed.cross(&depth).try_normalize(NORMALIZE_EPSILON)?;
106        let cross_b = depth.cross(&cross_a).try_normalize(NORMALIZE_EPSILON)?;
107        Some(Self {
108            depth,
109            cross_a,
110            cross_b,
111            depth_is_authored: true,
112        })
113    }
114
115    fn is_axis_aligned(&self) -> bool {
116        is_axis_aligned_direction(&self.depth)
117            && is_axis_aligned_direction(&self.cross_a)
118            && is_axis_aligned_direction(&self.cross_b)
119    }
120}
121
122/// Pre-computed per-element void subtraction data.
123///
124/// Building this is expensive: `classify_openings` re-runs `process_element`
125/// on each `IfcOpeningElement`, and clipping-plane extraction resolves the
126/// element's representation. Once built, it can be reused across every
127/// sub-mesh of the same element without re-doing any of that work, so the
128/// per-sub-mesh void path in
129/// [`GeometryRouter::process_element_with_submeshes_and_voids`] pays the
130/// classification cost once per element rather than once per sub-mesh.
131pub(super) struct VoidContext {
132    /// All classified openings. The diagonal-opening pass needs the raw list
133    /// (unmerged) so its per-item box rotation stays accurate.
134    openings: Vec<OpeningType>,
135    /// Rectangular openings merged into larger boxes to prevent O(2^N)
136    /// triangle growth when many adjacent openings tile a surface.
137    merged_openings: Vec<OpeningType>,
138    /// Parametric placement-frame cut data (host + reconciled opening boxes),
139    /// captured only when `rect_fast::param_enabled()`. Drives the analytic fast
140    /// path in `apply_void_context`; `None` defers to the exact kernel.
141    param: Option<ParamRectCut>,
142    /// 2D opening-subtraction data (host profile + projected opening footprints),
143    /// captured only when `bool2d_path::enabled()`. Drives the arbitrary-profile
144    /// 2D re-extrude fast path in `apply_void_context`, tried after the rect
145    /// parametric path; `None` defers to the exact kernel.
146    bool2d: Option<bool2d_path::Bool2dCut>,
147}
148
149impl VoidContext {
150    fn is_noop(&self) -> bool {
151        self.openings.is_empty()
152    }
153
154    /// Rectangular openings in the merged set: what every cut path records as
155    /// `HostOpeningDiagnostic::rect_boxes_processed`, so the silent-no-op
156    /// detector reads one quantity whichever path cut the host.
157    fn rect_opening_count(&self) -> usize {
158        self.merged_openings
159            .iter()
160            .filter(|o| matches!(o, OpeningType::Rectangular(..)))
161            .count()
162    }
163
164    /// True iff every cutter mesh is already in world coords (`origin == 0`). When
165    /// so AND the host is world-framed, `apply_void_context` can run the inner CSG
166    /// directly (the native / legacy fast path) without cloning the cutters; if any
167    /// cutter carries its own per-element origin it must be relativized first.
168    fn all_cutters_world_framed(&self) -> bool {
169        let world = |o: &OpeningType| match o {
170            OpeningType::Rectangular(..) => true,
171            OpeningType::DiagonalRectangular(m, _) => m.origin == [0.0, 0.0, 0.0],
172            OpeningType::NonRectangular(m, ..) => m.origin == [0.0, 0.0, 0.0],
173        };
174        self.openings.iter().all(world) && self.merged_openings.iter().all(world)
175    }
176
177    /// Return a copy of this context with every opening (raw + merged)
178    /// translated by `-origin`, i.e. moved from the world frame into the host's
179    /// per-element local frame. Used by [`GeometryRouter::apply_void_context`]
180    /// so the cutters share the host's local frame for the CSG — the missing
181    /// piece that previously made relativizing only the host silently drop cuts.
182    fn relativized_by(&self, origin: [f64; 3]) -> VoidContext {
183        VoidContext {
184            openings: self.openings.iter().map(|o| o.translated(origin)).collect(),
185            merged_openings: self
186                .merged_openings
187                .iter()
188                .map(|o| o.translated(origin))
189                .collect(),
190            // The parametric path runs in the OUTER apply_void_context on the
191            // non-relativized world mesh (it makes its own frame), so the
192            // relativized context never uses `param` — drop it.
193            param: None,
194            // Likewise the 2D path builds its own world frame from the parametrics
195            // and runs in the OUTER apply_void_context; the relativized context
196            // never uses it.
197            bool2d: None,
198        }
199    }
200
201    /// Oriented boxes of the REAL openings whose cutter mesh is MALFORMED —
202    /// self-intersecting tessellated voids carrying garbage "fin" vertices far
203    /// from the actual hole (a broken export). The kernel under-cuts such a
204    /// cutter, leaving a wall flap bridging the opening; these boxes mark the
205    /// region that MUST end up empty so a post-cut pass can drop the flap.
206    /// Empty when every cutter is well-formed, so clean hosts are untouched.
207    fn malformed_opening_boxes(&self) -> Vec<OpeningBox> {
208        self.openings
209            .iter()
210            .filter_map(|o| match o {
211                OpeningType::NonRectangular(m, ..) => opening_obb_if_malformed(m),
212                _ => None,
213            })
214            .collect()
215    }
216}
217
218impl OpeningType {
219    /// The cutter mesh and its extrusion direction, for the two mesh-carrying
220    /// kinds; `None` for `Rectangular`, which has no mesh until synthesised.
221    fn mesh_cutter(&self) -> Option<(&Mesh, Option<Vector3<f64>>)> {
222        match self {
223            OpeningType::Rectangular(..) => None,
224            OpeningType::DiagonalRectangular(m, f) => Some((m, Some(f.depth))),
225            OpeningType::NonRectangular(m, _, _, d) => Some((m, *d)),
226        }
227    }
228
229    /// Return a copy translated by `-origin` (world → host-local frame). Bounds
230    /// (`Point3`) shift; direction vectors and the oriented `OpeningFrame` are
231    /// translation-invariant and pass through unchanged.
232    fn translated(&self, origin: [f64; 3]) -> OpeningType {
233        let sub = |p: &Point3<f64>| Point3::new(p.x - origin[0], p.y - origin[1], p.z - origin[2]);
234        match self {
235            OpeningType::Rectangular(min, max, dir) => {
236                OpeningType::Rectangular(sub(min), sub(max), *dir)
237            }
238            OpeningType::DiagonalRectangular(mesh, frame) => {
239                OpeningType::DiagonalRectangular(translate_cutter_mesh(mesh, origin), *frame)
240            }
241            OpeningType::NonRectangular(mesh, min, max, dir) => {
242                OpeningType::NonRectangular(translate_cutter_mesh(mesh, origin), sub(min), sub(max), *dir)
243            }
244        }
245    }
246}
247
248/// EXACT parametric oriented box of a rectangular extrusion, in WORLD space.
249/// `r` columns are the orthonormal world axes (profile-X', profile-Y', extrude);
250/// `half` are the half-extents along those axes (XDim/2, YDim/2, Depth/2).
251/// Produced by [`GeometryRouter::parametric_rect_probe`].
252#[derive(Clone, Copy)]
253pub struct RectParam {
254    pub r: Matrix3<f64>,
255    pub center: Point3<f64>,
256    pub half: [f64; 3],
257}
258
259/// Captured parametric boxes for a host + its openings (all reconciled to their
260/// meshes and sharing the host frame), enabling the analytic placement-frame cut.
261/// Built in [`GeometryRouter::build_void_context`] only when `param_enabled()`.
262#[derive(Clone)]
263struct ParamRectCut {
264    host: RectParam,
265    openings: Vec<RectParam>,
266}
267
268impl GeometryRouter {
269    /// Tier-conditional minimum opening volume (m³) floor. Shared by the batch
270    /// admission gate and the sequential CSG path so the two can never diverge
271    /// (B11 / issue #976): the 0.1 L default filters legacy-BSP CSG artefacts,
272    /// but at High/Highest quality it relaxes to ~1e-9 m³ so genuine small
273    /// openings (bolt holes / sleeves in thin plates) still get cut instead of
274    /// being silently skipped.
275    #[inline]
276    fn min_opening_volume(quality: TessellationQuality) -> f64 {
277        match quality {
278            TessellationQuality::High | TessellationQuality::Highest => 1e-9,
279            _ => MIN_OPENING_VOLUME,
280        }
281    }
282
283    /// Process element with void subtraction (openings)
284    /// Process element with voids using optimized plane clipping
285    ///
286    /// This approach is more efficient than full 3D CSG for rectangular openings:
287    /// 1. Get chamfered wall mesh (preserves chamfered corners)
288    /// 2. For each opening, use optimized box cutting with internal face generation
289    /// 3. Apply any clipping operations (roof clips) from original representation
290    ///
291    /// Process an element with void subtraction (openings).
292    ///
293    /// This function handles three distinct cases for cutting openings:
294    ///
295    /// 1. **Floor/Slab openings** (vertical Z-extrusion): Uses CSG with actual mesh geometry
296    ///    because the XY footprint may be rotated relative to the slab orientation.
297    ///
298    /// 2. **Wall openings** (horizontal X/Y-extrusion, axis-aligned): Uses AABB clipping
299    ///    for fast, accurate cutting of rectangular openings.
300    ///
301    /// 3. **Diagonal wall openings**: Uses AABB clipping without internal face generation
302    ///    to avoid rotation artifacts.
303    ///
304    /// Reveal faces (inner surfaces of the opening holes) are generated as a
305    /// post-clipping step for rectangular and diagonal openings.  For diagonal
306    /// walls the geometry is computed in a rotated axis-aligned frame and
307    /// rotated back, giving correct results for any wall orientation.
308    ///
309    /// Errors when the element's body items cannot share one f64 frame
310    /// (#6349); [`Self::process_element_with_voids_parts`] keeps them.
311    #[inline]
312    pub fn process_element_with_voids(
313        &self,
314        element: &DecodedEntity,
315        decoder: &mut EntityDecoder,
316        void_index: &FxHashMap<u32, Vec<u32>>,
317    ) -> Result<Mesh> {
318        let parts = self.process_element_with_voids_parts(element, decoder, void_index)?;
319        super::frame_parts::single_frame(element.id, parts)
320    }
321
322    /// [`Self::process_element_with_voids`] over the element's frame parts
323    /// ([`Self::process_element_parts`]): each part is cut by the same openings,
324    /// classified once. An ordinary element has exactly one part (#6349).
325    pub fn process_element_with_voids_parts(
326        &self,
327        element: &DecodedEntity,
328        decoder: &mut EntityDecoder,
329        void_index: &FxHashMap<u32, Vec<u32>>,
330    ) -> Result<Vec<Mesh>> {
331        let opening_ids = match void_index.get(&element.id) {
332            Some(ids) if !ids.is_empty() => ids,
333            _ => {
334                return self.process_element_parts(element, decoder);
335            }
336        };
337
338        let parts =
339            self.process_element_parts_with_hygiene(element, decoder, SourceHygiene::IndexOnly)?;
340        // Opening and clip geometry are resolved in world coordinates, the
341        // frame every part is in once placement has been applied.
342        let ctx = self.build_void_context(element, opening_ids, decoder);
343        Ok(parts
344            .into_iter()
345            .map(|part| {
346                let mut voided = self.apply_void_context(part, &ctx, element.id);
347                // Clean slivers the CSG cut can introduce at opening seams — same
348                // hygiene as the tessellation chokepoints (Mesh::clean_degenerate).
349                voided.clean_degenerate();
350                // Instancing: a void-cut mesh no longer reproduces its representation's
351                // canonical geometry, so it can never be shared. Drop any metadata that
352                // rode along from the (pre-cut) mapped item.
353                voided.instance_meta = None;
354                voided
355            })
356            .collect())
357    }
358
359    /// Classify openings and extract clipping planes for an element.
360    ///
361    /// This is the expensive half of void subtraction — it decodes every
362    /// `IfcOpeningElement` (running `process_element` on each), classifies
363    /// them as rectangular / diagonal / non-rectangular, merges adjacent
364    /// rectangles, and transforms clipping planes to world space. The
365    /// output is reusable across every sub-mesh of the same element.
366    pub(super) fn build_void_context(
367        &self,
368        element: &DecodedEntity,
369        opening_ids: &[u32],
370        decoder: &mut EntityDecoder,
371    ) -> VoidContext {
372        // NOTE (issue #635): we no longer extract `IfcBooleanClippingResult`
373        // planes here. They are applied by `BooleanClippingProcessor::process`
374        // when building the input mesh (the wall-inversion fix in
375        // `processors/boolean.rs` makes the bounded-prism construction
376        // correct per IFC); re-applying them as unbounded planes discarded
377        // the polygonal bound and chopped off gable peaks (see
378        // `apply_void_context` for the full rationale).
379        let openings = self.classify_openings(element, opening_ids, decoder);
380        let merged_openings = Self::merge_rectangular_openings(&openings);
381
382        // PARAMETRIC fast-path capture (flag-gated, zero cost when off): probe the
383        // host + every opening for an exact rectangular box, reconcile each opening
384        // box against its mesh, and require all openings to share the host frame. Any
385        // miss -> `None` -> the host defers to the exact kernel.
386        let param = if crate::rect_fast::param_enabled() {
387            self.capture_param_rect(element, opening_ids, decoder)
388        } else {
389            None
390        };
391
392        // 2D opening-subtraction capture (flag-gated). Cheap when it misses
393        // (profile recovery only, no meshing), tried after the rect parametric
394        // path in `apply_void_context` so rect+rect hosts keep their existing
395        // output byte-identical and only the arbitrary-profile / rect-declined
396        // hosts reach the 2D re-extrude.
397        let bool2d = if bool2d_path::enabled() {
398            self.capture_bool2d(element, opening_ids, decoder)
399        } else {
400            None
401        };
402
403        VoidContext {
404            openings,
405            merged_openings,
406            param,
407            bool2d,
408        }
409    }
410
411    /// Build the parametric-cut data for a host + its openings, or `None` if any
412    /// precondition fails (host/opening not a clean rect extrusion, opening box
413    /// disagrees with its mesh, or an opening does not share the host frame).
414    fn capture_param_rect(
415        &self,
416        element: &DecodedEntity,
417        opening_ids: &[u32],
418        decoder: &mut EntityDecoder,
419    ) -> Option<ParamRectCut> {
420        if opening_ids.is_empty() {
421            return None;
422        }
423        let host = self.parametric_rect_probe(element, decoder)?;
424        let rt = host.r.transpose();
425        let mut openings = Vec::with_capacity(opening_ids.len());
426        for &oid in opening_ids {
427            let opening = decoder.decode_by_id(oid).ok()?;
428            if opening.ifc_type != IfcType::IfcOpeningElement {
429                return None;
430            }
431            // An opening may be a UNION OF RECTANGULAR PRISMS (Tekla multi-solid). Extract
432            // every box; each must share the host frame (signed permutation).
433            let op_boxes = self.parametric_rect_probe_all(&opening, decoder)?;
434            for op in &op_boxes {
435                signed_permutation_map(&(rt * op.r), 1.0e-3)?;
436            }
437            // Reconcile the boxes against the meshed opening by VOLUME: the boxes must
438            // account for the opening solid (catches non-rect / overlapping / partial parts).
439            if !self.opening_boxes_reconcile(&opening, &op_boxes, decoder) {
440                return None;
441            }
442            openings.extend(op_boxes);
443        }
444        Some(ParamRectCut { host, openings })
445    }
446
447    /// True iff the parametric `boxes` account for the actual meshed opening by VOLUME
448    /// (Σ box volume ≈ Σ mesh-shell volume within 3%). For a union of non-overlapping
449    /// rectangular prisms (the Tekla multi-solid opening) the mesh volume equals the
450    /// box-volume sum; a mismatch flags a non-rect / overlapping / partial part → defer.
451    fn opening_boxes_reconcile(
452        &self,
453        opening: &DecodedEntity,
454        boxes: &[RectParam],
455        decoder: &mut EntityDecoder,
456    ) -> bool {
457        if boxes.is_empty() {
458            return false;
459        }
460        let Ok(meshes) = self.get_opening_item_meshes_world(opening, decoder) else {
461            return false;
462        };
463        let mesh_vol: f64 = meshes.iter().map(|m| mesh_signed_volume(m).abs()).sum();
464        if mesh_vol < 1.0e-9 {
465            return false;
466        }
467        let box_vol: f64 = boxes
468            .iter()
469            .map(|b| 8.0 * b.half[0] * b.half[1] * b.half[2])
470            .sum();
471        let ratio = box_vol / mesh_vol;
472        (0.97..1.03).contains(&ratio)
473    }
474
475    /// Apply a pre-built `VoidContext` to a single mesh.
476    ///
477    /// This is the cheap per-mesh half of void subtraction: it re-reads the
478    /// mesh bounds (which differ per sub-mesh), extends rectangular openings
479    /// along their extrusion axis so they fully penetrate the mesh, then
480    /// subtracts every opening through the unified exact-kernel path (with the
481    /// per-opening #635 AABB fallback). All the classification work has
482    /// already been done in [`GeometryRouter::build_void_context`].
483    ///
484    /// `element_id` is the IFC product express ID of the host element. Any
485    /// `BoolFailure` recorded by the inner CSG kernel is attributed to that
486    /// product and stored on the router (drainable via
487    /// [`GeometryRouter::take_csg_failures`]). The router's failure log is
488    /// the only path failures reach the caller; `apply_void_context` itself
489    /// always returns the (possibly un-cut) mesh.
490    /// Apply a pre-built `VoidContext`, honouring the host's per-element local
491    /// frame.
492    ///
493    /// When local-frame precision is on, the host mesh arrives stored relative
494    /// to `mesh.origin` (small, f32-exact). The CSG must run with host AND
495    /// cutters in that SAME frame, or the cutters (resolved in world coords)
496    /// won't overlap the local host and every cut silently drops — the
497    /// 222692→190201 regression from the first attempt. This wrapper strips the
498    /// origin off the host (so the inner body works in a pure origin-0 local
499    /// frame with no origin-aware-merge surprises), relativizes the cutters by
500    /// the same origin, runs the CSG, then re-stamps the origin on the result so
501    /// `world = origin + position` holds for the renderer.
502    /// PARAMETRIC analytic fast path: subtract the openings as EXACT parametric boxes
503    /// in the host's own placement frame (where the rotated wall + windows are
504    /// axis-aligned), using the watertight cellular `rect_fast` cut, then rotate the
505    /// result back to world. Frame + extents come from the IFC parametrics (not the
506    /// mesh), so the cut is the analytic box-minus-boxes solid — ground-truth exact and
507    /// MORE correct than the exact kernel on engulfing-opening walls. Fires only on the
508    /// gated subset (`ctx.param` captured; host/opening reconciliation, shared-frame,
509    /// in-bounds, no-overlap, watertight self-check); any miss returns `None` → exact
510    /// kernel. Deterministic f64 → byte-identical native==wasm.
511    fn try_param_rect_cut(&self, mesh: &Mesh, ctx: &VoidContext) -> Option<Mesh> {
512        let param = ctx.param.as_ref()?;
513        let host = &param.host;
514        let rt = host.r.transpose();
515
516        // Host expressed in F (small coords) + reconciliation against the real mesh.
517        // ORIGIN-AWARE: the mesh may already be in a per-element local frame (wasm
518        // defaults `local_frame_enabled()` ON), where positions are relative to
519        // `mesh.origin` (world = origin + position). Frame the cut around
520        // `center - origin` so host_f = Rᵀ·(world_vertex - center) either way.
521        let o = mesh.origin;
522        let eff_center = Point3::new(
523            host.center.x - o[0],
524            host.center.y - o[1],
525            host.center.z - o[2],
526        );
527        let host_f = rotate_mesh_into_frame(mesh, &rt, &eff_center);
528        if host_f.positions.is_empty() {
529            return None;
530        }
531        let mut hmn = [f64::INFINITY; 3];
532        let mut hmx = [f64::NEG_INFINITY; 3];
533        for c in host_f.positions.chunks_exact(3) {
534            for k in 0..3 {
535                hmn[k] = hmn[k].min(c[k] as f64);
536                hmx[k] = hmx[k].max(c[k] as f64);
537            }
538        }
539        for k in 0..3 {
540            let ext = hmx[k] - hmn[k];
541            let lo = ext.min(2.0 * host.half[k]);
542            let hi = ext.max(2.0 * host.half[k]).max(1.0e-9);
543            if lo / hi < 0.99 {
544                return None;
545            }
546        }
547
548        // Opening boxes in F with the in-bounds + through-cut handling.
549        let mut boxes: Vec<([f64; 3], [f64; 3])> = Vec::with_capacity(param.openings.len());
550        for op in &param.openings {
551            let map = signed_permutation_map(&(rt * op.r), 1.0e-3)?;
552            let cf = rotate_point(
553                &rt,
554                op.center.x - host.center.x,
555                op.center.y - host.center.y,
556                op.center.z - host.center.z,
557            );
558            // The opening must penetrate along the wall's THIN (thickness) axis. If its
559            // extrude axis maps to a length/height axis, extending it across the host
560            // would wipe out a full slab — defer that case to the exact kernel.
561            let pen = (0..3).find(|&i| map[i].0 == 2)?;
562            if host.half[pen] > host.half[thin_axis(&host.half)] * 1.05 {
563                return None;
564            }
565            let mut half_f = [0.0f64; 3];
566            for i in 0..3 {
567                half_f[i] = op.half[map[i].0];
568            }
569            // In-bounds: the opening must lie within the wall on the in-face axes; an
570            // overrun is a partial intersection where box-clamp ≠ exact mesh-intersect.
571            for i in 0..3 {
572                if i == pen {
573                    continue;
574                }
575                let tol = host.half[i] * 0.01 + 1.0e-4;
576                if cf[i] - half_f[i] < -host.half[i] - tol
577                    || cf[i] + half_f[i] > host.half[i] + tol
578                {
579                    return None;
580                }
581            }
582            // Cut the box AS AUTHORED (it is reconciled to equal the opening void), with a
583            // tiny margin on the penetration axis to avoid a flush-face coincidence. The
584            // earlier full-thickness override over-cut multi-prism openings (each authored
585            // box is a thin slice; extending each to the full thickness removes too much).
586            let eps = 1.0e-4;
587            let mut bmin = [cf[0] - half_f[0], cf[1] - half_f[1], cf[2] - half_f[2]];
588            let mut bmax = [cf[0] + half_f[0], cf[1] + half_f[1], cf[2] + half_f[2]];
589            bmin[pen] -= eps;
590            bmax[pen] += eps;
591            boxes.push((bmin, bmax));
592        }
593
594        // No-overlap: overlapping cutter boxes can diverge from the union-subtract.
595        for a in 0..boxes.len() {
596            for b in (a + 1)..boxes.len() {
597                let (amn, amx) = boxes[a];
598                let (bmn, bmx) = boxes[b];
599                if (0..3).all(|i| amn[i] < bmx[i] - 1.0e-4 && bmn[i] < amx[i] - 1.0e-4) {
600                    return None;
601                }
602            }
603        }
604
605        let mut stats = crate::rect_fast::RectFastStats::default();
606        let cut_f = crate::rect_fast::subtract_rect_openings(&host_f, &boxes, &mut stats)?;
607        self.record_rect_fast(&stats);
608        let merged = crate::csg::ClippingProcessor::consolidate_coplanar(cut_f);
609        // Emit as a local-frame mesh (small positions + origin), then run the SAME
610        // hygiene the production output applies — in the small frame, where it is
611        // precise — so the self-check sees exactly what downstream will keep.
612        let mut out = rotate_mesh_from_frame(&merged, &host.r, &host.center);
613        out.clean_degenerate();
614
615        // Self-check: never emit a non-watertight cut; defer to the exact kernel.
616        if !param_cut_watertight(&out) {
617            return None;
618        }
619        crate::rect_fast::param_record_fire();
620        Some(out)
621    }
622
623    pub(super) fn apply_void_context(
624        &self,
625        mut mesh: Mesh,
626        ctx: &VoidContext,
627        element_id: u32,
628    ) -> Mesh {
629        // PARAMETRIC fast path (flag-gated). ORIGIN-AWARE: it handles both the world
630        // mesh (origin 0, native default) AND a per-element local-frame mesh (origin != 0,
631        // the wasm default — the precision-critical case this path is FOR), since it
632        // builds its own frame from the parametrics. Any miss falls through to the exact
633        // kernel below unchanged.
634        if crate::rect_fast::param_enabled() {
635            if let Some(fast) = self.try_param_rect_cut(&mesh, ctx) {
636                return fast;
637            }
638        }
639
640        // 2D OPENING-SUBTRACTION fast path (flag-gated). Generalises the rect
641        // parametric path above to arbitrary extruded-host profiles: subtract the
642        // openings' footprints from the host's 2D profile and re-extrude. Tried
643        // after `try_param_rect_cut` so proven rect+rect outputs are unchanged;
644        // ORIGIN-AWARE (it builds its own world frame from the parametrics and
645        // reconciles against the real host mesh, whatever `mesh.origin`). Any miss
646        // falls through to the exact kernel below unchanged.
647        if let Some(cut) = ctx.bool2d.as_ref() {
648            if let Some(candidate) = self.try_staged_bool2d(&mesh, cut, element_id) {
649                return candidate;
650            }
651        }
652
653        // ANALYTIC PRISM fast path (flag-gated): each opening whose cutter is
654        // a genuine stepped-extrusion prism (plain boxes AND rebated masonry
655        // windows) is subtracted from the host MESH analytically — host-shape-
656        // agnostic, so it fires on the faceted-BREP / clipped / multi-item
657        // hosts the parametric and 2D paths above cannot serve. Tried after
658        // them so their proven outputs stay byte-identical; ORIGIN-AWARE (the
659        // cut runs in the host's own local frame, cutters relativized in f64,
660        // `origin` re-stamped). Ineligible openings come back as a residual
661        // context and are cut by the exact kernel on the analytic result — the
662        // same composition contract as the 2D path (which also routes ITS
663        // residual through this path on recursion, so a 2D host's perpendicular
664        // sleeves take the prism cut too). Any miss falls through to the exact
665        // kernel below with the FULL opening set unchanged.
666        if prism_cut::enabled() {
667            // World bounds + triangle count captured BEFORE the cut so the
668            // per-host diagnostic matches what the exact/rect paths record.
669            let prism_bounds = world_host_bounds(&mesh);
670            let prism_tris_before = mesh.triangle_count();
671            if let Some((cut, residual)) = self.try_prism_cut(&mesh, ctx) {
672                return match residual {
673                    None => {
674                        // Same per-host cut-effect snapshot the exact path
675                        // records, so prism-cut hosts aren't missing from the
676                        // diagnostics.
677                        self.record_host_cut_effect(
678                            element_id,
679                            prism_tris_before,
680                            cut.triangle_count(),
681                            ctx.rect_opening_count(),
682                            prism_bounds,
683                        );
684                        cut
685                    }
686                    // With a residual, the recursive exact pass below records
687                    // the (final) cut-effect snapshot itself.
688                    Some(res) => self.apply_void_context(cut, &res, element_id),
689                };
690            }
691        }
692
693        // WORLD-space host AABB for the per-host diagnostic (`bbox`), captured
694        // HERE — before the local-frame branch below zeroes `mesh.origin` and
695        // before `try_cut_wall_local_frame` rotates the mesh into its own frame.
696        // On the wasm/local-frame default the host is stored relative to a
697        // nonzero `mesh.origin` (world = origin + position), so bounds taken
698        // after the clear would be local/near-zero and misdirect
699        // `diagnose-geometry --product/--type` (the #1474 local-frame-consumer
700        // bug class: every world-space reader must fold `MeshData.origin`). The
701        // origin is folded in f64 so georeferenced hosts report true world
702        // coords, then cast to the f32 diagnostic surface.
703        let host_world_bounds = world_host_bounds(&mesh);
704
705        let origin = mesh.origin;
706        if origin == [0.0, 0.0, 0.0] && ctx.all_cutters_world_framed() {
707            // Legacy/world frame on host AND cutters: no relativization needed.
708            return self.apply_void_context_inner(mesh, ctx, element_id, host_world_bounds, true);
709        }
710        // Work entirely in the host's local frame (origin 0 on every operand).
711        // `relativized_by` folds each cutter's OWN origin and subtracts the host
712        // origin in f64, so a per-element local-frame opening lands at the host
713        // precisely. This also covers a host that snapped to origin 0 while its
714        // openings did not (origin == 0 but cutters are local-framed).
715        mesh.origin = [0.0, 0.0, 0.0];
716        let local_ctx = ctx.relativized_by(origin);
717        let mut result =
718            self.apply_void_context_inner(mesh, &local_ctx, element_id, host_world_bounds, true);
719        // Keep an inner local-frame cut's translation when restoring this frame.
720        result.origin = std::array::from_fn(|i| result.origin[i] + origin[i]);
721        result
722    }
723
724    /// Try the analytic rectangular-opening fast path. Returns the watertight
725    /// cut mesh iff EVERY merged opening is an axis-aligned `Rectangular`
726    /// through-cut and the host is a clean axis-aligned box; otherwise `None`
727    /// (→ the exact kernel handles it). A mixed opening set defers the whole
728    /// host rather than composing analytic + exact cuts.
729    fn try_rect_fast(&self, host: &Mesh, ctx: &VoidContext) -> Option<Mesh> {
730        let (wmn, wmx) = host.bounds();
731        let wall_min = Point3::new(wmn.x as f64, wmn.y as f64, wmn.z as f64);
732        let wall_max = Point3::new(wmx.x as f64, wmx.y as f64, wmx.z as f64);
733        let mut boxes: Vec<([f64; 3], [f64; 3])> =
734            Vec::with_capacity(ctx.merged_openings.len());
735        for op in &ctx.merged_openings {
736            match op {
737                OpeningType::Rectangular(omn, omx, dir) => {
738                    let (fmn, fmx) = match dir {
739                        Some(d) => self.extend_opening_along_direction(
740                            *omn, *omx, wall_min, wall_max, *d,
741                        ),
742                        None => (*omn, *omx),
743                    };
744                    boxes.push(([fmn.x, fmn.y, fmn.z], [fmx.x, fmx.y, fmx.z]));
745                }
746                _ => return None,
747            }
748        }
749        let mut stats = crate::rect_fast::RectFastStats::default();
750        let out = crate::rect_fast::subtract_rect_openings(host, &boxes, &mut stats);
751        self.record_rect_fast(&stats);
752        // The cellular cut conformingly splits EVERY face by ALL grid lines (so
753        // adjacent cells share edges → watertight), which over-fragments faces an
754        // opening doesn't reach. Run the result through the SAME coplanar merge
755        // the exact path uses (i_overlay union per plane) to collapse those back
756        // to minimal triangles — keeps it watertight and un-bloated.
757        out.map(crate::csg::ClippingProcessor::consolidate_coplanar)
758    }
759
760    /// Cut a plan-rotated wall's openings in the wall's own axis-aligned,
761    /// origin-centred frame (issue #1167). Returns `None` (caller uses the
762    /// world path) unless the wall is rotated in plan and every opening carries
763    /// a cutter mesh. The wall normal comes from an opening's non-axis-aligned,
764    /// ~horizontal depth axis; else from vertical strips' frames (#3977); else,
765    /// when no opening authors a depth at all, from the host's own thickness
766    /// axis (#5410).
767    ///
768    /// In the wall frame the host and its openings are axis-aligned and near the
769    /// origin, so the exact subtract runs in the clean, f32-precise regime a
770    /// straight wall enjoys — clean-box openings even reclassify to the
771    /// watertight `rect_fast` path. Curved / brep openings keep their mesh and
772    /// are subtracted there too. The result is rotated back; the orthonormal,
773    /// origin-centred round-trip is identity for untouched geometry. Recurses
774    /// into [`Self::apply_void_context_inner`] with `allow_local_frame: false` (#3641).
775    fn try_cut_wall_local_frame(
776        &self,
777        mesh: &Mesh,
778        ctx: &VoidContext,
779        element_id: u32,
780        host_world_bounds: ((f32, f32, f32), (f32, f32, f32)),
781    ) -> Option<Mesh> {
782        let axes = self.wall_frame_axes(mesh, ctx)?;
783        let (mn, mx) = mesh.bounds();
784        let center = Vector3::new(
785            ((mn.x + mx.x) * 0.5) as f64,
786            ((mn.y + mx.y) * 0.5) as f64,
787            ((mn.z + mx.z) * 0.5) as f64,
788        );
789        let host_local = mesh_to_frame(mesh, &axes, center);
790
791        let z = Vector3::new(0.0, 0.0, 1.0);
792        let mut local_openings: Vec<OpeningType> = Vec::with_capacity(ctx.merged_openings.len());
793        for op in &ctx.merged_openings {
794            let cutter = match op {
795                OpeningType::DiagonalRectangular(m, _) => m,
796                OpeningType::NonRectangular(m, _, _, _) => m,
797                OpeningType::Rectangular(..) => return None,
798            };
799            let (lmn, lmx) = project_aabb_in_frame(cutter, &axes, center)?;
800            let in_frame =
801                |v: Vector3<f64>| Vector3::new(v.dot(&axes[0]), v.dot(&axes[1]), v.dot(&axes[2]));
802            // The cutter's own penetration (depth) axis expressed in the wall
803            // frame. Drives both the alignment test and — for the exact fallback
804            // — the cap-extension direction, which MUST stay faithful to THIS
805            // cutter: hardcoding the wall normal would push a misaligned
806            // opening's flush/short cap along the wrong axis and carve the wrong
807            // prism (#1270 review).
808            let frame_depth = match op {
809                OpeningType::DiagonalRectangular(_, f) => Some(in_frame(f.depth)),
810                OpeningType::NonRectangular(_, _, _, d) => d.map(in_frame),
811                OpeningType::Rectangular(..) => None,
812            };
813            // Whether THIS opening's own oriented box is axis-aligned in the
814            // wall frame. The frame is seeded from the FIRST rotated opening
815            // (#1167); a second opening tilted differently *in plane* is NOT
816            // axis-aligned here, so projecting its frame AABB would over-cut it
817            // (#1259 review). Only a clean box whose full frame aligns with the
818            // wall frame may take the fast rectangular cut; everything else —
819            // brep/curved voids and frame-misaligned boxes alike — keeps its
820            // exact (un-rotated, centred) mesh for the subtract.
821            let frame_aligned = match op {
822                OpeningType::DiagonalRectangular(_, f) => {
823                    is_axis_aligned_direction(&in_frame(f.depth))
824                        && is_axis_aligned_direction(&in_frame(f.cross_a))
825                        && is_axis_aligned_direction(&in_frame(f.cross_b))
826                }
827                _ => false,
828            };
829            if frame_aligned {
830                // Preserve THIS cutter's authored penetration axis. The new
831                // #3977 selector also admits vertically extruded strips, whose
832                // local depth is +Y rather than the wall normal (+Z). Extending
833                // those along +Z would turn a partial-thickness vertical slot
834                // into a full-through cut. An inferred box axis carries no such
835                // intent and must retain the established wall-normal fallback.
836                // A cutter authored along the wall normal itself keeps +Z too:
837                // handing the rectangular cut an antiparallel (-Z) depth flips
838                // its cap extension and tears the host (rvt01 #10191 census).
839                let rect_depth = match op {
840                    OpeningType::DiagonalRectangular(_, frame)
841                        if frame.depth_is_authored
842                            && frame_depth.is_some_and(|d| d.z.abs() < WALL_NORMAL_DEPTH_COS) =>
843                    {
844                        frame_depth
845                    }
846                    _ => Some(z),
847                };
848                local_openings.push(OpeningType::Rectangular(lmn, lmx, rect_depth));
849            } else {
850                let mesh_local = mesh_to_frame(cutter, &axes, center);
851                // Keep this cutter's true depth in the frame; fall back to the
852                // wall normal (+Z) only when the opening carried no direction.
853                let dir = frame_depth.unwrap_or(z);
854                local_openings.push(OpeningType::NonRectangular(mesh_local, lmn, lmx, Some(dir)));
855            }
856        }
857        let local_ctx = VoidContext {
858            merged_openings: Self::merge_rectangular_openings(&local_openings),
859            openings: local_openings,
860            // The local-frame recursion never re-captures the parametric cut
861            // (issue #1209): it operates on already-classified openings, so the
862            // analytic `param` / 2D paths are irrelevant here — defer to the exact
863            // path.
864            param: None,
865            bool2d: None,
866        };
867
868        // Forward the WORLD host bounds captured before this rotation so the
869        // diagnostic reports world coords, not wall-frame (rotated/centred) ones.
870        let diag_before = frame_snap::HostDiagSnapshot::capture(self, element_id);
871        // Whether the host reached us stored relative to a per-element origin
872        // (the wasm default): `apply_void_context` then cleared that origin, so
873        // the world bounds it passed differ from the stored ones. A world-frame
874        // host has identical bounds and keeps its established closure test.
875        let (smn, smx) = mesh.bounds();
876        let frame_relative =
877            host_world_bounds != ((smn.x, smn.y, smn.z), (smx.x, smx.y, smx.z));
878        let frame = Matrix3::from_columns(&axes);
879        let center_point = Point3::from(center);
880        let result_local = self
881            .apply_void_context_inner(host_local, &local_ctx, element_id, host_world_bounds, false);
882        // Rotation-only positions retain the far centre in `Mesh::origin`.
883        let mut result = rotate_mesh_from_frame(&result_local, &frame, &center_point);
884        // #5635: the operands arrive as f32 WORLD positions, so in the frame one
885        // authored face can land on dozens of depth values micrometres apart and
886        // the cut keeps T-junction seams along them. Only when the cut came back
887        // open (and not empty), cut again on operands whose coincident planes are
888        // snapped back onto one value. Keep the retry only if it is strictly
889        // closed and removed the same material as the first cut to within 0.1%
890        // (a retry whose cut silently failed returns the whole host). A cut that
891        // is already closed is never touched, so it stays byte-identical. The
892        // host itself need not be closed: an extruded voided profile carries
893        // T-junctions of its own that the cut consolidates away.
894        //
895        // For a frame-relative host the closure is judged on the cut as it will
896        // be emitted: rotated back and after the degenerate-triangle hygiene.
897        // Stored that way, a cut can be closed on the frame's grid only through
898        // µm slivers that hygiene then drops (#5739). The world frame's test is
899        // unchanged, so its output is too.
900        let closed = if frame_relative {
901            frame_snap::closed_as_emitted(&result)
902        } else {
903            frame_snap::closed_and_consistently_wound(&result_local)
904        };
905        if !result_local.positions.is_empty() && !closed {
906            let first_volume = frame_snap::enclosed_volume(&result_local);
907            // Only the kept run may leave a diagnostics record.
908            let diag_first = frame_snap::HostDiagSnapshot::capture(self, element_id);
909            diag_before.restore(self, element_id);
910            // Rebuilt rather than kept from above: the clean path pays nothing.
911            let mut host_snapped = mesh_to_frame(mesh, &axes, center);
912            let mut openings_snapped = local_ctx.openings.clone();
913            // The WORLD magnitude (a local-frame host's origin folded in), so
914            // the tolerance does not depend on the vertex frame (#5739). For a
915            // world-frame host these are its own bounds, as before.
916            let ((wx0, wy0, wz0), (wx1, wy1, wz1)) = host_world_bounds;
917            let world_magnitude = [wx0, wy0, wz0, wx1, wy1, wz1]
918                .iter()
919                .fold(0.0_f64, |m, v| m.max((*v as f64).abs()));
920            frame_snap::snap_to_frame_planes(
921                &mut host_snapped,
922                &mut openings_snapped,
923                frame_snap::frame_snap_tolerance(world_magnitude),
924            );
925            let snapped_ctx = VoidContext {
926                merged_openings: Self::merge_rectangular_openings(&openings_snapped),
927                openings: openings_snapped,
928                param: None,
929                bool2d: None,
930            };
931            let retry = self.apply_void_context_inner(
932                host_snapped,
933                &snapped_ctx,
934                element_id,
935                host_world_bounds,
936                false,
937            );
938            let retry_volume = frame_snap::enclosed_volume(&retry);
939            let same_cut = (retry_volume - first_volume).abs()
940                <= 1.0e-3 * first_volume.abs().max(retry_volume.abs());
941            let accepted = if !same_cut {
942                None
943            } else if frame_relative {
944                let emitted = rotate_mesh_from_frame(&retry, &frame, &center_point);
945                frame_snap::closed_as_emitted(&emitted).then_some(emitted)
946            } else {
947                frame_snap::closed_and_consistently_wound(&retry)
948                    .then(|| rotate_mesh_from_frame(&retry, &frame, &center_point))
949            };
950            if let Some(emitted) = accepted {
951                result = emitted;
952            } else {
953                diag_first.restore(self, element_id);
954            }
955        }
956        Some(result)
957    }
958
959    /// The #1167 wall frame `[run, height, normal]` for a plan-rotated wall
960    /// whose openings can all be expressed in it, else `None` (the host keeps
961    /// the world path). Translation-invariant, so it gives the same answer for a
962    /// world-stored host and a local-frame one (#5739).
963    fn wall_frame_axes(&self, mesh: &Mesh, ctx: &VoidContext) -> Option<[Vector3<f64>; 3]> {
964        if ctx.merged_openings.is_empty() {
965            return None;
966        }
967        let depth_of = |op: &OpeningType| -> Option<Vector3<f64>> {
968            match op {
969                OpeningType::DiagonalRectangular(_, f) => Some(f.depth),
970                OpeningType::NonRectangular(_, _, _, d) => *d,
971                OpeningType::Rectangular(_, _, d) => *d,
972            }
973        };
974        // Define the wall frame from the first opening whose depth is a
975        // genuinely rotated, ~horizontal axis. Axis-aligned walls find none and
976        // keep their (unchanged) world path.
977        let horizontal_axes = ctx
978            .merged_openings
979            .iter()
980            .filter_map(depth_of)
981            .find(|d| !is_axis_aligned_direction(d) && d.z.abs() <= 0.2)
982            .and_then(wall_frame_from_depth);
983        let axes = match horizontal_axes {
984            Some(axes) => axes,
985            None => vertical_depth_wall_frame(mesh, &ctx.merged_openings)
986                .or_else(|| host_thickness_wall_frame(mesh, &ctx.merged_openings))?,
987        };
988
989        // AABB-only `Rectangular` openings can't be rotated into the frame; a
990        // plan-rotated wall never has them (they'd be diagonal), so bail.
991        if ctx.merged_openings.iter().any(|op| matches!(op, OpeningType::Rectangular(..))) {
992            return None;
993        }
994        Some(axes)
995    }
996
997    // `host_mutated` is set just before an early `break` (final write unread; kept
998    // for readability). `allow_local_frame` (#3641) caps local-frame recursion.
999    #[allow(unused_assignments, clippy::too_many_arguments)]
1000    fn apply_void_context_inner(
1001        &self,
1002        mesh: Mesh,
1003        ctx: &VoidContext,
1004        element_id: u32,
1005        host_bounds_capture: ((f32, f32, f32), (f32, f32, f32)),
1006        allow_local_frame: bool,
1007    ) -> Mesh {
1008        // Capture the input triangle count so the per-host diagnostic can flag
1009        // the "cuts attempted but produced no change" case — the silent-no-op
1010        // signature when an opening box doesn't intersect the host mesh. The
1011        // WORLD-space host AABB (`host_bounds_capture`) is passed in from
1012        // `apply_void_context` (captured before the origin clear / frame
1013        // rotation), not recomputed here from the possibly local-framed `mesh`.
1014        let tris_before = mesh.triangle_count();
1015        if ctx.is_noop() {
1016            return mesh;
1017        }
1018        let original_host = mesh.clone(); // stray-shard sweep parity reference
1019
1020        // LOCAL-FRAME CUT (issue #1167): a vertical wall rotated in plan is cut
1021        // in its own axis-aligned, origin-centred frame — where the exact
1022        // subtract is clean and f32-precise — then the result is rotated back.
1023        // The world-space tilted cut at large coordinates over-cuts and
1024        // fragments badly. Scoped to plan-rotated walls; everything else falls
1025        // through to the world path unchanged. Gated on `allow_local_frame`.
1026        if allow_local_frame {
1027            if let Some(cut) = self.try_cut_wall_local_frame(&mesh, ctx, element_id, host_bounds_capture) {
1028                return cut;
1029            }
1030        }
1031
1032        let clipper = ClippingProcessor::new();
1033        // ROOT-CAUSE FIX (issue #1007, host #1112): correct f32 facet jitter on
1034        // the host triangle soup BEFORE the exact-kernel cut. A faceted-BREP
1035        // roof slope authored as ONE flat plane comes back from the f32 import
1036        // with adjacent facets ~0.09° non-coplanar. That jitter (a) splits the
1037        // slope into many one-triangle plane buckets in `consolidate_coplanar`
1038        // — a single-triangle bucket bypasses the CDT and is emitted as a 25:1
1039        // far-corner sliver fan — and (b) blocks clean coalescing of the cut
1040        // hole. Welding near-coplanar adjacent facets (≤0.15°, well below any
1041        // real roof pitch) to a single least-squares plane makes the slope
1042        // EXACTLY coplanar, so the cut emits one CDT-refined region (rim sliver
1043        // gone) with a clean opening hole. Deterministic + watertight + grid-
1044        // snapped; a no-op for already-planar extrusion hosts. The ordinary
1045        // router has already removed sub-grid SOURCE triangles before placement;
1046        // this whole-position-buffer weld does not replace that hygiene step or
1047        // promise clean output. Fresh cut candidates require path-specific
1048        // downstream handling (#4797).
1049        let mut result = crate::facet_weld::weld_near_coplanar_facets(&mesh);
1050
1051        // ANALYTIC FAST PATH: an axis-aligned box host whose openings are ALL
1052        // axis-aligned rectangular through-cuts is subtracted analytically
1053        // (`rect_fast`), skipping the exact mesh-arrangement kernel — the
1054        // ~80 s memory-bandwidth-bound void-cut window's dominant cost. Pure
1055        // optimization: any precondition miss (non-box host, mixed/non-rect
1056        // openings, near-edge feature) returns `None` and the host falls through
1057        // to the exact path below unchanged. Fired BEFORE the dense-host
1058        // subdivision (that workaround is only for the exact kernel).
1059        if crate::rect_fast::enabled() {
1060            if let Some(fast) = self.try_rect_fast(&result, ctx) {
1061                // Same per-host cut-effect snapshot the exact path records below,
1062                // so fast-path hosts aren't missing from the diagnostics.
1063                self.record_host_cut_effect(
1064                    element_id,
1065                    tris_before,
1066                    fast.triangle_count(),
1067                    ctx.rect_opening_count(),
1068                    host_bounds_capture,
1069                );
1070                return fast;
1071            }
1072        }
1073
1074        // OPENING-DENSE HOST REFINEMENT: when many openings target the same host
1075        // (a window wall is usually 2 big face-triangles per side), every cut's
1076        // intersection segments pile onto those few triangles — the exact
1077        // arrangement then re-triangulates a single triangle carrying dozens of
1078        // constraints (O(k²)), and the batched N-ary subtract leaves unrecovered
1079        // constraints and degrades to the O(N²) sequential path. Pre-subdividing
1080        // the host spreads the segments across many small triangles (small k each)
1081        // so the batched cut recovers. `consolidate_coplanar` re-triangulates each
1082        // coplanar group afterwards, so the temporary interior vertices don't
1083        // bloat the final mesh. Levels are scaled to opening count and capped.
1084        let n_openings = ctx.merged_openings.len();
1085        if n_openings >= 8 {
1086            // Just enough subdivision that each host triangle carries only a few
1087            // intersection segments, so the batched N-ary subtract RECOVERS rather
1088            // than degrading to the O(N²) sequential path — that recovery is the
1089            // win (≈10× on the densest walls), not the per-triangle segment count
1090            // itself. Over-subdividing is counter-productive: the extra triangles
1091            // cost more in the arrangement than the spreading saves (level 3 was
1092            // ~3× slower than level 1 on a 14-opening wall). Aim for ≳ a handful of
1093            // host triangles per opening, capped at 2 levels.
1094            let host_tris = result.triangle_count().max(1);
1095            let target = 4 * n_openings;
1096            let mut levels = 0usize;
1097            while host_tris * (1usize << (2 * (levels + 1))) < target && levels < 2 {
1098                levels += 1;
1099            }
1100            // A COARSE host (a box wall is ~12 triangles) still needs one split
1101            // even when the next level would overshoot the target; an ALREADY-dense
1102            // host (e.g. a faceted-BREP wall whose triangle count already meets the
1103            // target) is left untouched — extra geometry there only slows the cut.
1104            if levels == 0 && host_tris < target {
1105                levels = 1;
1106            }
1107            if levels > 0 {
1108                result = result.subdivided(levels);
1109            }
1110        }
1111
1112        let (wall_min_f32, wall_max_f32) = result.bounds();
1113        let wall_min = Point3::new(
1114            wall_min_f32.x as f64,
1115            wall_min_f32.y as f64,
1116            wall_min_f32.z as f64,
1117        );
1118        let wall_max = Point3::new(
1119            wall_max_f32.x as f64,
1120            wall_max_f32.y as f64,
1121            wall_max_f32.z as f64,
1122        );
1123
1124        let wall_valid = !result.is_empty()
1125            && result.positions.iter().all(|&v| v.is_finite())
1126            && result.triangle_count() >= 4;
1127
1128        if !wall_valid {
1129            return result;
1130        }
1131
1132        // NOTE: there is deliberately NO per-element CSG operation budget here.
1133        // The BSP-era `MAX_CSG_OPERATIONS = 10` cap silently skipped the 11th+
1134        // opening (it `continue`d past BOTH the exact subtract AND the #635 AABB
1135        // fallback), which is exactly the regression `csg_void_test::
1136        // many_tessellated_box_openings_are_all_cut` pins (history: #413/#439).
1137        // On the unified exact path every opening is a cheap box-vs-host cut, so
1138        // a budget-skipped opening is a correctness bug, not a perf guard.
1139
1140        // UNIFIED EXACT PATH (PART B): every opening — axis-aligned RECTANGULAR
1141        // included — is now subtracted by the exact mesh kernel, NOT the legacy
1142        // Sutherland-Hodgman AABB clip. A `Rectangular` opening is materialised as
1143        // a PENETRATING box mesh (its bounds extended through the wall along the
1144        // extrusion axis by `extend_opening_along_direction`, so both caps poke past
1145        // the host ⇒ a transversal cut with no flush-cap sliver — the same robust
1146        // condition PART A guarantees for tilted openings). The exact subtract emits
1147        // the void's interior reveal faces itself, so the explicit reveal/recess
1148        // quad generators are no longer needed on this path.
1149        //
1150        // `synth_rect` owns the synthesised box meshes so they outlive the loop's
1151        // borrowed `&OpeningType`s below.
1152        let mut synth_rect: Vec<OpeningType> = Vec::new();
1153        let mut non_rect_openings: Vec<&OpeningType> = Vec::new();
1154        for opening in &ctx.merged_openings {
1155            match opening {
1156                OpeningType::Rectangular(open_min, open_max, extrusion_dir) => {
1157                    // Penetration axis: the authored extrusion dir when present,
1158                    // else inferred from how the box pierces the host (issue
1159                    // #1337). Carrying a concrete dir downstream keeps the
1160                    // through-host cap-flush extension off the opening's thinnest
1161                    // (in-plane) axis for deep cutters.
1162                    let dir = extrusion_dir.unwrap_or_else(|| {
1163                        infer_box_penetration_dir(open_min, open_max, &wall_min, &wall_max)
1164                    });
1165                    // Only openings with an AUTHORED extrusion dir were extended
1166                    // here before; keep the synthesized box identical for the
1167                    // dirless case (the inferred dir only steers the later
1168                    // through-host extension, not the box bounds).
1169                    let (final_min, final_max) = if extrusion_dir.is_some() {
1170                        self.extend_opening_along_direction(
1171                            *open_min, *open_max, wall_min, wall_max, dir,
1172                        )
1173                    } else {
1174                        (*open_min, *open_max)
1175                    };
1176                    let box_mesh = Self::make_box_mesh(final_min, final_max);
1177                    synth_rect.push(OpeningType::NonRectangular(
1178                        box_mesh,
1179                        final_min,
1180                        final_max,
1181                        Some(dir),
1182                    ));
1183                }
1184                // A MALFORMED (self-intersecting) tessellated cutter is NOT cut
1185                // here: its messy mesh under-cuts the opening, and double-cutting
1186                // it (here AND in the clean-box `recut_malformed_openings` pass)
1187                // leaves overlapping sliver "shards" in the reveals. Skip it; the
1188                // recut performs the single, clean box cut for these openings.
1189                OpeningType::NonRectangular(m, ..) if opening_obb_if_malformed(m).is_some() => {}
1190                other => non_rect_openings.push(other),
1191            }
1192        }
1193        let all_openings: Vec<&OpeningType> =
1194            synth_rect.iter().chain(non_rect_openings.iter().copied()).collect();
1195
1196        // DISJOINT-CUTTER BATCHING: group cutters whose pad-inflated AABBs are
1197        // pairwise disjoint and subtract each group in ONE conforming arrangement
1198        // (`ClippingProcessor::subtract_mesh_many`). Sequential per-opening
1199        // subtraction re-arranges the (growing) host once per cutter, and each
1200        // intermediate f64→f32→snap round-trip re-jitters carve vertices off
1201        // shared planes so cut N+1 re-cracks what cut N reconciled (many-void
1202        // walls' compounding open edges, the 16-void slab's ~3.5 s cost). Batching
1203        // admits only openings passing the SAME guards as the sequential loop plus
1204        // per-component watertightness (#2176). Singletons and any group whose
1205        // batched cut fails its guards — or the kernel conformity gate
1206        // (`subtract_mesh_many` rejects a group whose N-ary arrangement left an
1207        // unrecovered constraint) — fall through to the per-opening sequential
1208        // loop below with its full #635 fallback / engulf / redundant-void machinery.
1209        let mut batch_consumed: Vec<bool> = vec![false; all_openings.len()];
1210        // Disjoint groups of opening indices (len ≥ 2 only); each is cut
1211        // INLINE at its first member's position in the sequential loop below,
1212        // so the relative order of batched vs sequential cutters matches the
1213        // pure sequential pass — only the order WITHIN a group (mutually
1214        // disjoint cutters, the provably order-free case) is collapsed.
1215        let mut batch_groups: Vec<Vec<(usize, Mesh)>> = Vec::new();
1216        let mut batch_group_of: FxHashMap<usize, usize> = FxHashMap::default();
1217        // Set on every successful cut; while false, a group's admission-time
1218        // extended cutters are still valid and reused verbatim.
1219        let mut host_mutated = false;
1220
1221        // COAXIAL FOOTPRINT-UNION for OVERLAPPING clusters (issue #129, flag
1222        // `IFC_LITE_VOID_UNION`). Overlapping cutters can't join the disjoint batch
1223        // below and otherwise fall to the O(N) sequential exact path; this fuses
1224        // each overlapping coaxial cluster into disjoint re-extruded prisms and cuts
1225        // them in one `subtract_mesh_many`. See `coaxial_union`. Marks consumed
1226        // openings so the batch + sequential loops skip them; a cluster that fails
1227        // any guard is left unconsumed for the exact path (never worse than exact).
1228        self.coaxial_union_prepass(
1229            &mut result,
1230            &all_openings,
1231            &mut batch_consumed,
1232            &mut host_mutated,
1233            &clipper,
1234        );
1235
1236        if all_openings.len() >= 2 {
1237            // Inflation pad: ≥ 2×(promote band 8·2⁻¹⁶ ≈ 122 µm + snap radius);
1238            // 1 mm is conservative and far below any real opening separation.
1239            // Touching/overlapping cutters land in DIFFERENT groups, cut in
1240            // sequence — overlap degrades gracefully to sequential behavior.
1241            const BATCH_PAD: f64 = 1.0e-3;
1242            struct Cand {
1243                idx: usize,
1244                /// The admission-time extended cutter (host PRE-cut). Reused at
1245                /// cut time while the host is still unmutated — the common case
1246                /// (groups are cut at their FIRST member, usually before any
1247                /// sequential cut) — so extension isn't paid twice.
1248                mesh: Mesh,
1249                lo: [f64; 3],
1250                hi: [f64; 3],
1251            }
1252            let mut cands: Vec<Cand> = Vec::new();
1253            for (idx, opening) in all_openings.iter().enumerate() {
1254                // Already cut by the coaxial/overlap-union prepass above.
1255                if batch_consumed[idx] {
1256                    continue;
1257                }
1258                let Some((opening_mesh, extrusion_dir)) = opening.mesh_cutter() else { continue };
1259                // Same admission guards as the sequential loop.
1260                let opening_valid = !opening_mesh.is_empty()
1261                    && opening_mesh.positions.iter().all(|&v| v.is_finite())
1262                    && opening_mesh.positions.len() >= 9;
1263                if !opening_valid {
1264                    continue;
1265                }
1266                let (result_min, result_max) = result.bounds();
1267                let (omn, omx) = opening_mesh.bounds();
1268                let no_overlap = omx.x < result_min.x
1269                    || omn.x > result_max.x
1270                    || omx.y < result_min.y
1271                    || omn.y > result_max.y
1272                    || omx.z < result_min.z
1273                    || omn.z > result_max.z;
1274                if no_overlap {
1275                    continue;
1276                }
1277                let open_vol = (omx.x - omn.x) as f64
1278                    * (omx.y - omn.y) as f64
1279                    * (omx.z - omn.z) as f64;
1280                if open_vol < Self::min_opening_volume(self.tessellation_quality) {
1281                    continue;
1282                }
1283                let Some(ext) = Self::batch_cutter(opening_mesh, extrusion_dir, &result) else {
1284                    continue;
1285                };
1286                let (lo, hi) = ext.bounds();
1287                // Engulf-class exclusion: a cutter whose extended AABB covers
1288                // the whole host on EVERY axis (3% slack, the sequential
1289                // engulf test) stays on the sequential path, where the
1290                // near-engulf and redundant-void guards live — batched it can
1291                // shave the host's outer shell (the #559171-family residual).
1292                let engulfs = {
1293                    let tol = ENGULF_TOLERANCE;
1294                    let covers = |omin: f64, omax: f64, wmin: f64, wmax: f64| {
1295                        let slack = (wmax - wmin).abs().max(1.0e-9) * tol;
1296                        omin <= wmin + slack && omax >= wmax - slack
1297                    };
1298                    covers(lo.x as f64, hi.x as f64, wall_min.x, wall_max.x)
1299                        && covers(lo.y as f64, hi.y as f64, wall_min.y, wall_max.y)
1300                        && covers(lo.z as f64, hi.z as f64, wall_min.z, wall_max.z)
1301                };
1302                if engulfs {
1303                    continue;
1304                }
1305                cands.push(Cand {
1306                    idx,
1307                    mesh: ext,
1308                    lo: [
1309                        lo.x as f64 - BATCH_PAD,
1310                        lo.y as f64 - BATCH_PAD,
1311                        lo.z as f64 - BATCH_PAD,
1312                    ],
1313                    hi: [
1314                        hi.x as f64 + BATCH_PAD,
1315                        hi.y as f64 + BATCH_PAD,
1316                        hi.z as f64 + BATCH_PAD,
1317                    ],
1318                });
1319            }
1320            // Greedy disjoint grouping, deterministic in opening order: a
1321            // candidate joins the first group whose EVERY member's inflated
1322            // AABB is disjoint from its own.
1323            let mut groups: Vec<Vec<usize>> = Vec::new();
1324            'cand: for ci in 0..cands.len() {
1325                for g in groups.iter_mut() {
1326                    let disjoint_from_all = g.iter().all(|&cj| {
1327                        let (a, b) = (&cands[ci], &cands[cj]);
1328                        a.hi[0] < b.lo[0]
1329                            || a.lo[0] > b.hi[0]
1330                            || a.hi[1] < b.lo[1]
1331                            || a.lo[1] > b.hi[1]
1332                            || a.hi[2] < b.lo[2]
1333                            || a.lo[2] > b.hi[2]
1334                    });
1335                    if disjoint_from_all {
1336                        g.push(ci);
1337                        continue 'cand;
1338                    }
1339                }
1340                groups.push(vec![ci]);
1341            }
1342            for g in &groups {
1343                if g.len() < 2 {
1344                    continue; // singleton: sequential loop handles it (full guards)
1345                }
1346                let gid = batch_groups.len();
1347                for &ci in g {
1348                    batch_group_of.insert(cands[ci].idx, gid);
1349                }
1350                batch_groups
1351                    .push(g.iter().map(|&ci| (cands[ci].idx, cands[ci].mesh.clone())).collect());
1352            }
1353        }
1354
1355        for (opening_idx, opening) in all_openings.iter().enumerate() {
1356            if batch_consumed[opening_idx] {
1357                continue; // already handled by a batched disjoint group
1358            }
1359            // Batched group cut, attempted ONCE, inline at the group's first
1360            // member — so the relative order of batched vs sequential cutters
1361            // matches the pure sequential pass. On any failure (admission,
1362            // guards, or the kernel's conformity gate) the members fall back
1363            // to the per-opening sequential path below.
1364            if let Some(&gid) = batch_group_of.get(&opening_idx) {
1365                let members = std::mem::take(&mut batch_groups[gid]);
1366                if members.len() >= 2 {
1367                    // While the host is unmutated, the admission-time extended
1368                    // cutters (built against this exact host) are reused as-is;
1369                    // after any cut, members are re-extended against the
1370                    // CURRENT host — matching the sequential loop's reference,
1371                    // which extends each cutter against the (k−1)-cut host.
1372                    let mut extended: Vec<(usize, Mesh)> = Vec::with_capacity(members.len());
1373                    let mut admissible = true;
1374                    if !host_mutated {
1375                        extended = members;
1376                    } else {
1377                        for &(m_idx, _) in &members {
1378                            let ext = all_openings[m_idx]
1379                                .mesh_cutter()
1380                                .and_then(|(m, dir)| Self::batch_cutter(m, dir, &result));
1381                            let Some(ext) = ext else {
1382                                admissible = false;
1383                                break;
1384                            };
1385                            extended.push((m_idx, ext));
1386                        }
1387                    }
1388                    if admissible {
1389                        let cutters: Vec<&Mesh> = extended.iter().map(|(_, m)| m).collect();
1390                        let tri_before = result.triangle_count();
1391                        let outcome = if Self::batch_cutters_match_sequential(&result, &all_openings, &extended) {
1392                            clipper.subtract_mesh_many_retaining_miss(&result, &cutters)
1393                        } else {
1394                            clipper.subtract_mesh_many(&result, &cutters)
1395                        };
1396                        // #6516: retain the group's validated consolidation on
1397                        // a conforming miss; keeping the original host instead
1398                        // can discard useful seam repairs made by the singles.
1399                        if let GroupCut::Cut(csg_result) | GroupCut::Retessellated(csg_result) = outcome {
1400                            let min_tris = (tri_before / CSG_TRIANGLE_RETENTION_DIVISOR)
1401                                .max(MIN_VALID_TRIANGLES);
1402                            if !csg_result.is_empty() && csg_result.triangle_count() >= min_tris {
1403                                result = csg_result;
1404                                host_mutated = true;
1405                                for &(m_idx, _) in &extended {
1406                                    batch_consumed[m_idx] = true;
1407                                }
1408                            }
1409                        }
1410                    }
1411                }
1412                if batch_consumed[opening_idx] {
1413                    continue; // this opening was handled with its group
1414                }
1415            }
1416            // Normalize both exact-subtract variants into the same (mesh, min,
1417            // max, dir) shape. `DiagonalRectangular` (a clean but tilted box —
1418            // e.g. a roof-slope window or a slanted roof opening, #1007 defect B)
1419            // is a REAL solid cutter, so it MUST be subtracted exactly, never
1420            // approximated by a frame-rotated AABB: that legacy path
1421            // (`apply_diagonal_openings`) tore the host (101 boundary edges) and
1422            // left the void uncut. Route it through the same exact-mesh subtract
1423            // as `NonRectangular`; the kernel cuts the tilted box cleanly
1424            // (winding-robust after the defect-A orient-outward fix). The
1425            // mesh-bounds AABB + frame-depth direction still seed the #635
1426            // fallback, which only fires if the exact subtract no-ops.
1427            let normalized: Option<(&Mesh, Point3<f64>, Point3<f64>, Option<Vector3<f64>>)> =
1428                match *opening {
1429                    OpeningType::Rectangular(..) => None,
1430                    OpeningType::DiagonalRectangular(ref opening_mesh, ref frame) => {
1431                        let (mn, mx) = opening_mesh.bounds();
1432                        Some((
1433                            opening_mesh,
1434                            Point3::new(mn.x as f64, mn.y as f64, mn.z as f64),
1435                            Point3::new(mx.x as f64, mx.y as f64, mx.z as f64),
1436                            Some(frame.depth),
1437                        ))
1438                    }
1439                    OpeningType::NonRectangular(
1440                        ref opening_mesh,
1441                        ref open_min_pt,
1442                        ref open_max_pt,
1443                        ref extrusion_dir,
1444                    ) => Some((opening_mesh, *open_min_pt, *open_max_pt, *extrusion_dir)),
1445                };
1446            if let Some((opening_mesh, open_min_pt, open_max_pt, extrusion_dir)) = normalized {
1447                let open_min_pt = &open_min_pt;
1448                let open_max_pt = &open_max_pt;
1449                {
1450                    let opening_valid = !opening_mesh.is_empty()
1451                        && opening_mesh.positions.iter().all(|&v| v.is_finite())
1452                        && opening_mesh.positions.len() >= 9;
1453
1454                    if !opening_valid {
1455                        continue;
1456                    }
1457
1458                    let (result_min, result_max) = result.bounds();
1459                    let (open_min_f32, open_max_f32) = opening_mesh.bounds();
1460                    let no_overlap = open_max_f32.x < result_min.x
1461                        || open_min_f32.x > result_max.x
1462                        || open_max_f32.y < result_min.y
1463                        || open_min_f32.y > result_max.y
1464                        || open_max_f32.z < result_min.z
1465                        || open_min_f32.z > result_max.z;
1466                    if no_overlap {
1467                        continue;
1468                    }
1469
1470                    let open_vol = (open_max_f32.x - open_min_f32.x)
1471                        * (open_max_f32.y - open_min_f32.y)
1472                        * (open_max_f32.z - open_min_f32.z);
1473                    // The 0.1 L volume floor filtered legacy-BSP CSG artefacts but
1474                    // also drops genuine small openings — bolt holes / sleeves in
1475                    // thin plates. At the two highest quality levels keep those
1476                    // holes (the exact kernel is stable on small cutters); only
1477                    // reject numerically degenerate cutters there. (issue #976)
1478                    let min_open_vol = Self::min_opening_volume(self.tessellation_quality) as f32;
1479                    if open_vol < min_open_vol {
1480                        continue;
1481                    }
1482
1483                    // ENGULFING-SOLID VOID: when this opening's real solid
1484                    // CONTAINS the whole host, the exact subtract no-ops on the
1485                    // coincident shared faces and returns the host unchanged in
1486                    // volume (a spurious solid where the opening should be). A
1487                    // cheap AABB-engulf pre-check (false for an ordinary opening,
1488                    // which spans the host on at most its thickness axis) gates
1489                    // the O(host_v · opening_t) containment scan, which confirms
1490                    // TRUE solid containment — so a void whose AABB engulfs the
1491                    // host while its real profile excludes it is not affected.
1492                    // On a hit the host is fully consumed.
1493                    let aabb_engulfs = {
1494                        let tol = ENGULF_TOLERANCE_F32;
1495                        let covers = |omin: f32, omax: f32, hmin: f32, hmax: f32| {
1496                            let slack = (hmax - hmin).abs().max(1.0e-9) * tol;
1497                            omin <= hmin + slack && omax >= hmax - slack
1498                        };
1499                        covers(open_min_f32.x, open_max_f32.x, result_min.x, result_max.x)
1500                            && covers(open_min_f32.y, open_max_f32.y, result_min.y, result_max.y)
1501                            && covers(open_min_f32.z, open_max_f32.z, result_min.z, result_max.z)
1502                    };
1503                    if aabb_engulfs && opening_engulfs_host_solid(&result, opening_mesh) {
1504                        // Mark the host consumed so the element pipeline keeps
1505                        // the empty result instead of falling back to the un-cut
1506                        // host.
1507                        self.record_void_consumed_host(element_id);
1508                        result = Mesh::default();
1509                        host_mutated = true;
1510                        break;
1511                    }
1512
1513                    let tri_before = result.triangle_count();
1514                    let failures_before = clipper.failure_count();
1515                    let mut csg_succeeded = false;
1516                    // PENETRATING CUTTER (PART A): push the opening's caps a hair
1517                    // PAST the host along its depth axis so a flush cap becomes a
1518                    // clean transversal crossing — the exact kernel then cuts the
1519                    // tilted, faceted roof opening with no bridging sliver (#1007
1520                    // host #1112). Falls back to the raw opening mesh when no depth
1521                    // direction is known (the kernel handles a true through-cutter
1522                    // anyway; the extension only matters for the flush-cap case).
1523                    let extended_opening = Self::sequential_cutter(
1524                        opening_mesh, extrusion_dir, &result,
1525                    );
1526                    let cutter = &extended_opening;
1527                    let outcome = clipper.subtract_mesh(&result, cutter);
1528                    // Nothing to cut, so the #635 fallback below must not cut a
1529                    // box either (#5362).
1530                    let kernel_found_no_overlap = outcome.found_no_overlap();
1531                    let kept = mesh_to_keep(outcome, &result);
1532                    // The host is still un-cut: the kernel found no real
1533                    // intersection, or bailed on a grazing/coplanar cutter.
1534                    let csg_unchanged = kept.is_none();
1535                    if let Some(csg_result) = kept {
1536                        let min_tris = (tri_before / CSG_TRIANGLE_RETENTION_DIVISOR)
1537                            .max(MIN_VALID_TRIANGLES);
1538                        if !csg_result.is_empty() && csg_result.triangle_count() >= min_tris {
1539                            result = csg_result;
1540                            host_mutated = true;
1541                            csg_succeeded = true;
1542                        }
1543                    }
1544
1545                    // AABB fallback (issue #635): when CSG can't subtract the
1546                    // opening (most commonly because its triangulated profile
1547                    // exceeds `MAX_CSG_POLYGONS_PER_MESH`, i.e. circular /
1548                    // arched / arbitrary curved openings), cut the opening's
1549                    // axis-aligned bounding box instead. This leaves a square
1550                    // hole in place of a round one, but a square hole is
1551                    // dramatically less wrong than a missing void on a wall
1552                    // that is supposed to host a window or door.
1553                    // A re-tessellation this router refused on its own
1554                    // retention check is not a verdict: fall back as before.
1555                    let kernel_verdict_disjoint = kernel_found_no_overlap && csg_unchanged;
1556                    if !csg_succeeded && !kernel_verdict_disjoint {
1557                        let dir = extrusion_dir.or_else(|| {
1558                            Some(wall_thinnest_axis_dir(&wall_min, &wall_max))
1559                        });
1560                        let (final_min, final_max) = if let Some(dir) = dir {
1561                            self.extend_opening_along_direction(
1562                                *open_min_pt,
1563                                *open_max_pt,
1564                                wall_min,
1565                                wall_max,
1566                                dir,
1567                            )
1568                        } else {
1569                            (*open_min_pt, *open_max_pt)
1570                        };
1571                        // Near-engulf guard. When CSG returned the host *unchanged*
1572                        // (no cut) AND the opening's AABB covers the whole wall on
1573                        // every axis, the rectangular fallback would cut that
1574                        // engulfing box and delete the wall. This is the signature
1575                        // of a non-rectangular opening whose bounding box engulfs
1576                        // the host while its real profile excludes it: the kernel
1577                        // errors on the grazing/coplanar cutter and returns the
1578                        // un-cut host, which is already the correct result vs
1579                        // IfcOpenShell (advanced #555433's facade-scale void
1580                        // #555493). Keep the un-cut host instead of over-cutting.
1581                        // Normal windows/doors — including the issue-635 high-poly
1582                        // round openings the AABB box approximates — sit INSIDE the
1583                        // wall and never engulf it, so they still take the fallback.
1584                        // The 3% per-axis tolerance absorbs an opening that reaches
1585                        // ~flush with a wall face (its near plane).
1586                        let engulfs_host = {
1587                            let tol = ENGULF_TOLERANCE;
1588                            let covers = |omin: f64, omax: f64, wmin: f64, wmax: f64| {
1589                                let slack = (wmax - wmin).abs().max(1.0e-9) * tol;
1590                                omin <= wmin + slack && omax >= wmax - slack
1591                            };
1592                            covers(final_min.x, final_max.x, wall_min.x, wall_max.x)
1593                                && covers(final_min.y, final_max.y, wall_min.y, wall_max.y)
1594                                && covers(final_min.z, final_max.z, wall_min.z, wall_max.z)
1595                        };
1596                        // `capped` keys on the `OperandTooLarge` rejection: the
1597                        // #1109 per-boolean/-element escalation budget records it
1598                        // when a cut trips mid-arrangement and bails to the un-cut
1599                        // host (`csg/mod.rs`), so it is NOT always false — a
1600                        // grazing engulfing cutter that would run long trips the
1601                        // budget just as it errors on the coincident faces.
1602                        // Engulf suppression therefore applies regardless of WHY
1603                        // the CSG produced no change (budget trip or kernel error
1604                        // on the coincident faces): an engulfing cutter's AABB
1605                        // covers the whole host, so the #635 box-cut would delete
1606                        // the entire wall — a silent element deletion strictly
1607                        // worse than leaving a phantom (un-cut) solid. `capped` is
1608                        // now only read by the diagnostic below (it no longer
1609                        // gates the suppression), so it is unused in a release
1610                        // build that compiles out both the tracing and the
1611                        // debug/test `eprintln!` legs of `diag_warn!`.
1612                        #[allow(unused_variables)]
1613                        let capped = clipper.has_operand_too_large_since(failures_before);
1614                        // Issue #964: suppress the destructive AABB box when the
1615                        // host already has this void cut into it (a void
1616                        // double-encoded as both a profile inner curve and a
1617                        // redundant IfcOpeningElement). When every column through
1618                        // the opening footprint is already open in the host,
1619                        // cutting the bounding box would replace a correct
1620                        // round/polygonal hole with a rectangle. Unlike the
1621                        // engulf heuristic this is a positive void detection, so
1622                        // it overrides `capped` too (the void demonstrably
1623                        // exists — there is nothing left to approximate).
1624                        let probe_axis = dir.unwrap_or_else(|| {
1625                            wall_thinnest_axis_dir(&wall_min, &wall_max)
1626                        });
1627                        let redundant_void =
1628                            opening_redundant_with_host(&result, opening_mesh, &probe_axis);
1629                        let suppress_fallback =
1630                            redundant_void || (csg_unchanged && engulfs_host);
1631                        if !suppress_fallback {
1632                            // Diagnostic for issue #635: log the opening
1633                            // triangle count when the AABB fallback actually
1634                            // fires, so round windows (post profile
1635                            // simplification) can be confirmed to hit CSG and
1636                            // only genuinely-uncut voids land on the box cut.
1637                            // A genuine degraded mode, so warn-level under
1638                            // observability; the legacy debug/test-only
1639                            // eprintln is preserved otherwise.
1640                            crate::diag::diag_warn!(
1641                                { opening_tris = opening_mesh.triangle_count(),
1642                                  reason = "csg-produced-no-change",
1643                                  "voids: AABB fallback cut used for opening" }
1644                                else {
1645                                    #[cfg(any(debug_assertions, test))]
1646                                    eprintln!(
1647                                        "[issue-635] AABB fallback used: opening={} tris (CSG produced no change)",
1648                                        opening_mesh.triangle_count()
1649                                    );
1650                                }
1651                            );
1652                            // Deliberate degraded mode: this fallback removes
1653                            // the wall material inside the opening AABB but no
1654                            // longer emits reveal/recess quads (deleted with
1655                            // the legacy clip path), so its output has an open
1656                            // rim. It runs only when the kernel FAILED on an
1657                            // opening that reaches the host, never on one the
1658                            // kernel found disjoint (#5362).
1659                            let aabb_cut =
1660                                self.cut_rectangular_opening(&result, final_min, final_max);
1661                            #[cfg(feature = "opening-perf-trace")]
1662                            crate::opening_perf_trace::record(|c| {
1663                                c.aabb_fallback_attempts = c.aabb_fallback_attempts.saturating_add(1);
1664                                c.aabb_fallback_input_triangles = c.aabb_fallback_input_triangles.saturating_add(result.triangle_count() as u64);
1665                                c.aabb_fallback_output_triangles = c.aabb_fallback_output_triangles.saturating_add(aabb_cut.triangle_count() as u64);
1666                            });
1667                            if !aabb_cut.is_empty() && aabb_cut.triangle_count() != tri_before {
1668                                #[cfg(feature = "opening-perf-trace")]
1669                                crate::opening_perf_trace::record(|c| c.aabb_fallback_commits = c.aabb_fallback_commits.saturating_add(1));
1670                                result = aabb_cut;
1671                                host_mutated = true;
1672                            }
1673                        } else {
1674                            // The engulfing/redundant-void guard suppressed the
1675                            // #635 AABB box-cut. For an engulfing cutter the box
1676                            // covers the whole host, so cutting it would DELETE
1677                            // the wall; leaving the host un-cut (a phantom solid)
1678                            // is the strictly safer degrade. `capped` records
1679                            // whether the CSG bailed via the #1109 budget trip
1680                            // (`OperandTooLarge`) vs a kernel error on the
1681                            // coincident faces.
1682                            crate::diag::diag_warn!(
1683                                { opening_tris = opening_mesh.triangle_count(),
1684                                  capped = capped,
1685                                  reason = "engulfing-cutter-fallback-suppressed",
1686                                  "voids: AABB fallback suppressed for engulfing cutter (host left un-cut)" }
1687                                else {
1688                                    #[cfg(any(debug_assertions, test))]
1689                                    eprintln!(
1690                                        "[issue-635] AABB fallback SUPPRESSED for engulfing cutter: opening={} tris, capped={} (host left un-cut)",
1691                                        opening_mesh.triangle_count(),
1692                                        capped
1693                                    );
1694                                }
1695                            );
1696                        }
1697                    }
1698                }
1699            }
1700        }
1701
1702        // NOTE (issue #635): the clipping planes from `IfcBooleanClippingResult`
1703        // are already applied by `BooleanClippingProcessor::process` during
1704        // `process_element` — the post-clip mesh is the *input* to this
1705        // function. Re-clipping here was a leftover from before that
1706        // processor existed; for `IfcPolygonalBoundedHalfSpace` it actively
1707        // *broke* gable walls, because `extract_half_space_plane` discards
1708        // the polygonal bound and the resulting unbounded plane chops off
1709        // the gable peak. Voids alone are applied here.
1710
1711        // Drain whatever fallbacks the kernel logged during this element's
1712        // void / clip pass, attribute them to the host product, and stash on
1713        // the router so the caller can surface them (e.g. flagged in a
1714        // viewer overlay or asserted in regression tests).
1715        let kernel_failures = clipper.take_failures();
1716        if !kernel_failures.is_empty() {
1717            self.record_host_failure_summary(element_id, &kernel_failures);
1718            self.record_csg_failures(element_id, kernel_failures);
1719        }
1720
1721        // WATERTIGHT SLIVER REFINEMENT (issue #1007): the exact-kernel cut of a
1722        // long, tilted faceted-BREP host facet can emit a high-aspect corner
1723        // sliver (a far-corner triangle fanned to two new rim vertices a few cm
1724        // apart) that lands ALONE in its plane bucket and so bypasses the
1725        // coplanar CDT. Bisect any >8:1 triangle's longest edge at its midpoint,
1726        // splitting BOTH incident triangles in lockstep so the mesh stays
1727        // watertight (no T-junction) and the midpoint lies ON the original edge
1728        // ⇒ cut volume is preserved exactly. A no-op on clean cuts (no triangle
1729        // exceeds 8:1), so it does not perturb the frozen corpus. Only runs when
1730        // a cut was actually attempted (`!ctx.is_noop()` guarantees this path).
1731        // This canonicalizer is likewise not a hygiene boundary: fresh sub-grid
1732        // candidates require handling by the remaining repair/clip/sweep path,
1733        // independently of the router's earlier source cleanup (#4797).
1734        let mut result = crate::facet_weld::refine_high_aspect_slivers(&result);
1735
1736        // UNDER-CUT REPAIR: a self-intersecting tessellated cutter (garbage
1737        // vertices metres from the real opening) makes the kernel UNDER-cut — a
1738        // wall flap bridges the opening. Re-cut each such opening with a clean
1739        // box (removes only the opening prism, taking the flap, while preserving
1740        // and re-triangulating the wall around the hole). Done HERE, in the cut
1741        // frame (host + cutters share it), and BEFORE the spike clip so any
1742        // residual protrusion is still caught. A no-op when no cutter is
1743        // malformed — clean openings are never reshaped.
1744        recut_malformed_openings(&mut result, &ctx.malformed_opening_boxes());
1745
1746        // SPURIOUS-FLAP CLIP: a subtract can only remove material, so the cut is
1747        // mathematically contained in the host's pre-cut AABB (`wall_min/max`);
1748        // any triangle poking past it is provably an artifact — a malformed
1749        // cutter's far-flung leaked flap (self-intersecting / tessellated void,
1750        // surfacing once a SECOND cutter perturbs the arrangement), OR the
1751        // flush-cap through-extension reveal overhang (`extend_opening_mesh_
1752        // through_host` pushes a flush cap ~0.3·depth past the host for a clean
1753        // transversal cut, so the subtract emits the reveal out to it — 0.105 m
1754        // past a 0.35 m floor slab, #1633). `clip_triangles_to_host_aabb` bounds
1755        // its jitter tolerance so a large host cannot leak the overhang. A no-op
1756        // on clean cuts.
1757        result.clip_triangles_to_host_aabb(
1758            [wall_min.x as f32, wall_min.y as f32, wall_min.z as f32],
1759            [wall_max.x as f32, wall_max.y as f32, wall_max.z as f32],
1760        );
1761
1762        // STRAY-SHARD SWEEP: see `sweep::drop_faces_outside_host` (#1788).
1763        if host_mutated {
1764            result = drop_faces_outside_host(result, &original_host);
1765        }
1766
1767        // Per-host cut-effect snapshot: tris_before / tris_after lets the
1768        // diagnostic surface the silent-no-op case (rectangular boxes
1769        // processed but the host mesh came out unchanged — the box
1770        // probably didn't intersect the wall, e.g. wrong placement).
1771        self.record_host_cut_effect(
1772            element_id,
1773            tris_before,
1774            result.triangle_count(),
1775            ctx.rect_opening_count(),
1776            host_bounds_capture,
1777        );
1778
1779        result
1780    }
1781
1782    /// Process an element into per-item sub-meshes with opening subtraction.
1783    ///
1784    /// Mirrors [`process_element_with_voids`] but preserves each
1785    /// `IfcShapeRepresentation` item as its own sub-mesh so that callers can
1786    /// look up a direct `IfcStyledItem` color per geometry item (e.g. the
1787    /// three extrusion layers of a multi-layer wall). The opening(s) are
1788    /// subtracted from each sub-mesh independently so that windows and doors
1789    /// cut through every material layer they intersect.
1790    ///
1791    /// Returns an empty collection when there are no openings (callers should
1792    /// fall back to [`process_element_with_submeshes`]) or when every
1793    /// sub-mesh is destroyed by void subtraction.
1794    pub fn process_element_with_submeshes_and_voids(
1795        &self,
1796        element: &DecodedEntity,
1797        decoder: &mut EntityDecoder,
1798        void_index: &FxHashMap<u32, Vec<u32>>,
1799    ) -> Result<SubMeshCollection> {
1800        // Layered single-solid path: slice the element's base mesh by its
1801        // material-layer buildup AFTER subtracting voids. This produces one
1802        // sub-mesh per layer keyed by IfcMaterial id, so layers show up as
1803        // individual colors even when the underlying geometry is a single
1804        // swept solid.
1805        if let Some(layered) = self.try_layered_sub_meshes(element, decoder, Some(void_index)) {
1806            return Ok(layered);
1807        }
1808
1809        let opening_ids = match void_index.get(&element.id) {
1810            Some(ids) if !ids.is_empty() => ids.clone(),
1811            _ => return Ok(SubMeshCollection::new()),
1812        };
1813
1814        // Voided occurrences materialize cut geometry (#1623 don't-bake off) with no
1815        // texture index (#1781: CSG rebuilds vertices, orphaning UVs — colour wins).
1816        let sub_meshes = self.process_element_with_submeshes_impl(
1817            element,
1818            decoder,
1819            false,
1820            None,
1821            SourceHygiene::IndexOnly,
1822        )?;
1823        if sub_meshes.is_empty() {
1824            return Ok(SubMeshCollection::new());
1825        }
1826
1827        // Classify openings + resolve clipping planes ONCE per element. Doing
1828        // this per sub-mesh would re-run `process_element` on every opening
1829        // and re-extract clipping planes N times, multiplying the expensive
1830        // parsing/CSG setup by the sub-mesh count on the exact elements this
1831        // path targets (multi-layer walls with windows).
1832        let ctx = self.build_void_context(element, &opening_ids, decoder);
1833
1834        // INHERIT the discriminator rather than assuming rep-item ids. This path
1835        // is reached only when `try_layered_sub_meshes` declined above, so the
1836        // flag is false today -- but hard-coding that would make a future
1837        // layered producer silently relabel material ids as representation
1838        // items, which is the exact confusion #3199 removes.
1839        let mut voided = SubMeshCollection {
1840            sub_meshes: Vec::new(),
1841            ids_are_materials: sub_meshes.ids_are_materials,
1842        };
1843        for sub in sub_meshes.sub_meshes {
1844            let geometry_id = sub.geometry_id;
1845            let mut voided_mesh = self.apply_void_context(sub.mesh, &ctx, element.id);
1846            // Same CSG-seam hygiene as the single-mesh void path.
1847            voided_mesh.clean_degenerate();
1848            if !voided_mesh.is_empty() {
1849                voided
1850                    .sub_meshes
1851                    .push(SubMesh::new(geometry_id, voided_mesh));
1852            }
1853        }
1854
1855        Ok(voided)
1856    }
1857}
1858
1859#[cfg(test)]
1860mod flap_clip_tests;
1861#[cfg(test)]
1862mod batch_cutter_tests;
1863#[cfg(test)]
1864mod cut_effect_count_tests;
1865#[cfg(test)]
1866mod single_cut_outcome_tests;