Skip to main content

ifc_lite_geometry/
rect_fast.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//! Analytic fast path for axis-aligned rectangular openings cut through an
6//! axis-aligned box host (the dominant case: rectangular windows/doors in a
7//! straight wall). This sidesteps the exact mesh-arrangement CSG kernel — which
8//! is at its single-threaded, memory-bandwidth-bound floor — for openings that
9//! need no exact arithmetic at all.
10//!
11//! WATERTIGHTNESS RECIPE (the make-or-break the prior `subtract_box_fast`
12//! attempt got wrong by recomputing rim coords on an incommensurate grid):
13//!   - Build the cut on ONE canonical grid whose lines are the host edges plus
14//!     every opening edge, each value SNAPPED to the kernel's own power-of-two
15//!     `SNAP_GRID = 1/65536` (the grid the host operand already lives on).
16//!   - CONFORMING-split every face by the crossing grid lines (no T-junctions):
17//!     the front/back faces become a grid of cells with hole cells omitted; the
18//!     side faces are split at the hole grid lines so their shared edge with the
19//!     annulus matches sub-edge for sub-edge.
20//!   - Every vertex reads its position from the SAME snapped grid value, so two
21//!     faces meeting at a shared line emit BIT-IDENTICAL f32 → watertight by
22//!     construction (value identity, not a numeric hash-match).
23//!   - Per-face flat normals; vertices are NEVER welded across creases (#846).
24//!
25//! GATING: this is a PURE OPTIMIZATION. It returns `None` (→ caller falls back to
26//! the exact kernel) the moment any precondition fails — non-box host, non-
27//! through opening, opening off the face, or a NEAR-EDGE feature whose grid lines
28//! would collapse into each other at the host's f32 magnitude (the robustness
29//! gate that replaces a hard dependency on the per-element local frame).
30
31use crate::mesh::Mesh;
32
33/// The kernel's canonical reconcile grid. Power of two ⇒ the snap is an exact
34/// f64 op ⇒ bit-deterministic native==wasm.
35use crate::kernel::mesh_bridge::SNAP_GRID;
36
37#[inline]
38fn snap(c: f64) -> f64 {
39    (c / SNAP_GRID).round() * SNAP_GRID
40}
41
42/// Telemetry: why the fast path fired or deferred, so the real fire-rate on a
43/// void-heavy model is measurable (the prior attempt shipped at fired=0).
44#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)]
45pub struct RectFastStats {
46    pub fired: u64,
47    pub openings_cut: u64,
48    pub defer_host_not_box: u64,
49    pub defer_not_through: u64,
50    pub defer_off_face: u64,
51    pub defer_near_edge: u64,
52    pub defer_no_openings: u64,
53    pub defer_too_many_openings: u64,
54}
55
56/// Escape hatch: `IFC_LITE_RECT_FAST=0` is the GLOBAL rect-fast kill switch.
57/// It forces every opening back through the exact kernel (parity debugging /
58/// bisection) by disabling BOTH the legacy world-axis-aligned path gated here
59/// AND the parametric placement-frame path ([`param_enabled`] honours it), so
60/// setting this single flag is sufficient to route all rectangular openings
61/// through CSG. Default ON: the path is a pure optimization that defers on
62/// any precondition miss. To disable only the parametric path, set
63/// `IFC_LITE_RECT_PARAM=0` instead.
64pub fn enabled() -> bool {
65    use std::sync::OnceLock;
66    static ON: OnceLock<bool> = OnceLock::new();
67    *ON.get_or_init(|| std::env::var("IFC_LITE_RECT_FAST").as_deref() != Ok("0"))
68}
69
70static PARAM_OVERRIDE: std::sync::atomic::AtomicI8 = std::sync::atomic::AtomicI8::new(-1);
71
72/// PARAMETRIC rectangular-opening fast path (placement-frame, ground-truth-exact cut).
73/// DEFAULT ON -- opt out with `IFC_LITE_RECT_PARAM=0` (parametric path only),
74/// `IFC_LITE_RECT_FAST=0` (the GLOBAL rect-fast kill switch: disables this path AND
75/// the legacy [`enabled`] path, preserving the documented single-flag parity/bisection
76/// hatch), or `param_set_enabled_override`. Gated separately from [`enabled`] because
77/// it is a deliberate behaviour change: it emits the analytic box-minus-boxes solid,
78/// which is MORE correct than the exact kernel on engulfing-opening walls.
79/// Corpus-validated ON (tests/rect_param_validate.rs A/B over 5 real models): every
80/// non-firing element byte-identical ON vs OFF, every firing host watertight and
81/// matching the analytic ground truth. wasm has no env, so `std::env::var` errs there
82/// and the default is ON on both targets -- native==wasm stays in lockstep; the
83/// wasm-side `setRectParamFastPath` override remains the programmatic escape hatch
84/// (and, being an explicit per-process call, takes precedence over both env flags).
85pub fn param_enabled() -> bool {
86    match PARAM_OVERRIDE.load(std::sync::atomic::Ordering::Relaxed) {
87        0 => return false,
88        1 => return true,
89        _ => {}
90    }
91    use std::sync::OnceLock;
92    static ON: OnceLock<bool> = OnceLock::new();
93    *ON.get_or_init(|| {
94        param_env_default(enabled(), std::env::var("IFC_LITE_RECT_PARAM").as_deref().ok())
95    })
96}
97
98/// Env-flag decision core for [`param_enabled`], pure so the semantics are
99/// unit-testable without mutating process env: the parametric path is ON unless
100/// its own flag opts out (`IFC_LITE_RECT_PARAM=0`) or the global rect-fast kill
101/// switch is thrown (`IFC_LITE_RECT_FAST=0`, surfaced here as `rect_fast_on ==
102/// false` via [`enabled`]). `IFC_LITE_RECT_PARAM=1` cannot resurrect the path
103/// past the kill switch: the kill switch means "exact kernel for everything".
104fn param_env_default(rect_fast_on: bool, rect_param: Option<&str>) -> bool {
105    rect_fast_on && rect_param != Some("0")
106}
107
108/// Test-only: force `param_enabled()` on/off (or `None` for the env default).
109pub fn param_set_enabled_override(v: Option<bool>) {
110    PARAM_OVERRIDE.store(
111        match v {
112            None => -1,
113            Some(false) => 0,
114            Some(true) => 1,
115        },
116        std::sync::atomic::Ordering::Relaxed,
117    );
118}
119
120static PARAM_FIRES: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
121
122/// Count one parametric fast-path fire (the analytic cut was emitted).
123pub fn param_record_fire() {
124    PARAM_FIRES.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
125}
126/// Read + reset the parametric fast-path fire counter.
127pub fn take_param_fires() -> u64 {
128    PARAM_FIRES.swap(0, std::sync::atomic::Ordering::Relaxed)
129}
130
131// rect_fast engagement counters are now REQUEST-LOCAL: each cut records into its
132// router via `GeometryRouter::record_rect_fast` (drained by `take_rect_fast_stats`),
133// so concurrent native geometry passes get isolated per-load `rectFast` diagnostics.
134// The previous process-global atomic sink (`record_global` / `take_global_stats`)
135// was removed because it cross-contaminated concurrent loads.
136
137/// An axis-aligned box AABB extracted from a host mesh, plus a check that every
138/// face is axis-aligned (the precondition for the world-coord cut).
139struct AlignedBox {
140    min: [f64; 3],
141    max: [f64; 3],
142}
143
144/// Verify `mesh` is an axis-aligned box (every triangle normal ≈ ±X/±Y/±Z, and
145/// the surface spans exactly the AABB on all 6 sides) and return its AABB.
146/// Conservative: any deviation ⇒ `None` ⇒ defer to the exact kernel.
147fn aligned_box(mesh: &Mesh) -> Option<AlignedBox> {
148    if mesh.indices.len() < 36 {
149        // a box is ≥ 12 triangles; fewer can't be a closed box
150        return None;
151    }
152    let p = |i: u32| -> [f64; 3] {
153        let b = i as usize * 3;
154        [
155            mesh.positions[b] as f64,
156            mesh.positions[b + 1] as f64,
157            mesh.positions[b + 2] as f64,
158        ]
159    };
160    let (mn_f, mx_f) = mesh.bounds();
161    let min = [mn_f.x as f64, mn_f.y as f64, mn_f.z as f64];
162    let max = [mx_f.x as f64, mx_f.y as f64, mx_f.z as f64];
163    // Degenerate extent on any axis ⇒ not a 3D box.
164    for k in 0..3 {
165        if max[k] - min[k] <= SNAP_GRID {
166            return None;
167        }
168    }
169    // Every triangle must be axis-aligned (normal along one axis) AND lie on the
170    // corresponding min or max face plane — i.e. the mesh is exactly the AABB
171    // shell, nothing protruding or interior.
172    const PLANE_TOL: f64 = 1e-4;
173    let mut seen_face = [false; 6]; // -x,+x,-y,+y,-z,+z
174    for tri in mesh.indices.chunks_exact(3) {
175        let (a, b, c) = (p(tri[0]), p(tri[1]), p(tri[2]));
176        let e1 = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
177        let e2 = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
178        let n = [
179            e1[1] * e2[2] - e1[2] * e2[1],
180            e1[2] * e2[0] - e1[0] * e2[2],
181            e1[0] * e2[1] - e1[1] * e2[0],
182        ];
183        let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
184        if len == 0.0 {
185            continue; // degenerate tri — ignore (hygiene drops it anyway)
186        }
187        // which axis is the normal along?
188        let mut axis = usize::MAX;
189        for k in 0..3 {
190            if (n[k].abs() / len) > 0.999 {
191                axis = k;
192            }
193        }
194        if axis == usize::MAX {
195            return None; // a non-axis-aligned face ⇒ not an aligned box
196        }
197        // all 3 verts must sit on the min or max plane of that axis
198        let on_min = a[axis] <= min[axis] + PLANE_TOL
199            && b[axis] <= min[axis] + PLANE_TOL
200            && c[axis] <= min[axis] + PLANE_TOL;
201        let on_max = a[axis] >= max[axis] - PLANE_TOL
202            && b[axis] >= max[axis] - PLANE_TOL
203            && c[axis] >= max[axis] - PLANE_TOL;
204        if on_min {
205            seen_face[axis * 2] = true;
206        } else if on_max {
207            seen_face[axis * 2 + 1] = true;
208        } else {
209            return None; // an interior / protruding triangle ⇒ not a clean box
210        }
211    }
212    if seen_face.iter().all(|&s| s) {
213        Some(AlignedBox { min, max })
214    } else {
215        None
216    }
217}
218
219/// Result of the cut: a watertight `Mesh`, or `None` to defer to the exact
220/// kernel. `openings` are world AABBs (min,max) of axis-aligned rectangular
221/// cutters. Handles WINDOWS, DOORS (flush to an edge), recesses, and overlapping
222/// openings uniformly via a 3D CELLULAR decomposition: split the host box by
223/// every opening plane on all three axes, mark each cell solid (in the host,
224/// outside every opening) or void, and emit each cell face that borders a void
225/// cell or the grid boundary. Watertight by construction — adjacent cells share
226/// snapped grid vertices bit-identically, so no T-junctions and no cracks.
227pub fn subtract_rect_openings(
228    host: &Mesh,
229    openings: &[([f64; 3], [f64; 3])],
230    stats: &mut RectFastStats,
231) -> Option<Mesh> {
232    if openings.is_empty() {
233        stats.defer_no_openings += 1;
234        return None;
235    }
236    // build_cellular is O(cells * openings) and cells grow with the opening count,
237    // so a crafted host with thousands of tiny openings is O(N^3) -> unbounded
238    // hang. 128 is far above any real wall (1-20 openings); defer the pathological
239    // case to the exact kernel (bounded per-element, #1121) rather than hang.
240    const MAX_FAST_OPENINGS: usize = 128;
241    if openings.len() > MAX_FAST_OPENINGS {
242        stats.defer_too_many_openings += 1;
243        return None;
244    }
245    let bx = match aligned_box(host) {
246        Some(b) => b,
247        None => {
248            stats.defer_host_not_box += 1;
249            return None;
250        }
251    };
252
253    // Scale-aware near-edge epsilon: two grid lines closer than this would
254    // collapse into one f32 at the host's world magnitude, cracking the cut.
255    // max(|coord|)·2^-21 keeps >= 4 f32 ULP between distinct lines; floored at the
256    // snap grid so origin-scale hosts stay permissive.
257    let mag = (0..3)
258        .map(|k| bx.min[k].abs().max(bx.max[k].abs()))
259        .fold(0.0f64, f64::max);
260    let near_eps = (mag * (1.0 / 2_097_152.0)).max(SNAP_GRID);
261
262    // Clamp every opening to the host shell (a cutter extended through the wall
263    // pokes past) and keep only those that overlap on all 3 axes. A
264    // non-overlapping opening is a no-op (the SILENT NO-OP case) — drop it.
265    let smin = [snap(bx.min[0]), snap(bx.min[1]), snap(bx.min[2])];
266    let smax = [snap(bx.max[0]), snap(bx.max[1]), snap(bx.max[2])];
267    let mut clamped: Vec<[[f64; 3]; 2]> = Vec::with_capacity(openings.len());
268    for (omn, omx) in openings {
269        let mut cmn = [0.0; 3];
270        let mut cmx = [0.0; 3];
271        let mut overlaps = true;
272        let mut covers_full = true;
273        for k in 0..3 {
274            cmn[k] = snap(omn[k].max(bx.min[k]));
275            cmx[k] = snap(omx[k].min(bx.max[k]));
276            if cmx[k] - cmn[k] <= near_eps {
277                overlaps = false;
278            }
279            // Does the clamped opening reach the host edge-to-edge on this axis?
280            if cmn[k] > smin[k] + near_eps || cmx[k] < smax[k] - near_eps {
281                covers_full = false;
282            }
283        }
284        // An opening that CONTAINS the whole host on all three axes is a
285        // degenerate / double-encoded redundant void: the void is already baked
286        // into the wall profile and re-added as an opening element (#964). The
287        // exact kernel treats it as a no-op (coplanar cutter faces → host left
288        // unchanged), so the fast path MUST NOT cut it — doing so erases the
289        // entire wall and leaves the window floating in a giant void (#1167).
290        // Defer the whole host to the exact path to match its behaviour.
291        if covers_full {
292            stats.defer_off_face += 1;
293            return None;
294        }
295        if overlaps {
296            clamped.push([cmn, cmx]);
297        }
298    }
299    if clamped.is_empty() {
300        // Nothing actually cuts — defer the no-op to the exact path (it owns the
301        // SILENT NO-OP diagnostic) rather than returning an unchanged clone.
302        stats.defer_off_face += 1;
303        return None;
304    }
305
306    // Per-axis grid lines: host bounds + every clamped opening edge, snapped,
307    // sorted, deduplicated; near-coincident-but-distinct lines (< near_eps) →
308    // defer (f32-collapse risk).
309    let mut grid: [Vec<f64>; 3] = [Vec::new(), Vec::new(), Vec::new()];
310    for k in 0..3 {
311        let mut edges = vec![snap(bx.min[k]), snap(bx.max[k])];
312        for c in &clamped {
313            edges.push(c[0][k]);
314            edges.push(c[1][k]);
315        }
316        match dedup_axis(edges, near_eps) {
317            Some(g) => grid[k] = g,
318            None => {
319                stats.defer_near_edge += 1;
320                return None;
321            }
322        }
323    }
324
325    let cut = build_cellular(&grid, &clamped);
326    stats.fired += 1;
327    stats.openings_cut += clamped.len() as u64;
328    Some(cut)
329}
330
331/// Sort + dedup grid lines on one axis; `None` if two DISTINCT lines are closer
332/// than `near_eps` (an f32-collapse risk → defer). Identical snapped values
333/// (e.g. a door edge coinciding with the host edge) collapse to one line.
334fn dedup_axis(mut edges: Vec<f64>, near_eps: f64) -> Option<Vec<f64>> {
335    // A non-finite coord (NaN/Inf from a corrupt mesh) would panic the sort under
336    // `panic=abort` and crash the whole batch — defer to the exact kernel instead.
337    if edges.iter().any(|v| !v.is_finite()) {
338        return None;
339    }
340    edges.sort_by(|a, b| a.total_cmp(b));
341    let mut out: Vec<f64> = Vec::with_capacity(edges.len());
342    for c in edges {
343        match out.last() {
344            Some(&last) => {
345                let d = (c - last).abs();
346                if d == 0.0 {
347                    // same line — keep one
348                } else if d < near_eps {
349                    return None; // distinct but too close → collapse risk
350                } else {
351                    out.push(c);
352                }
353            }
354            None => out.push(c),
355        }
356    }
357    if out.len() < 2 {
358        return None;
359    }
360    Some(out)
361}
362
363struct Builder {
364    positions: Vec<f32>,
365    normals: Vec<f32>,
366    indices: Vec<u32>,
367}
368
369impl Builder {
370    fn vert(&mut self, p: [f64; 3], n: [f32; 3]) -> u32 {
371        let idx = (self.positions.len() / 3) as u32;
372        self.positions.push(p[0] as f32);
373        self.positions.push(p[1] as f32);
374        self.positions.push(p[2] as f32);
375        self.normals.extend_from_slice(&n);
376        idx
377    }
378    /// A planar quad (4 coplanar world corners in perimeter order) emitted with
379    /// the winding that makes its front face point along `n` — robust to the
380    /// handedness of the (u,v,w) axis assignment (e.g. a Y-through wall is a
381    /// left-handed frame, which would otherwise invert every face).
382    fn quad(&mut self, c: [[f64; 3]; 4], n: [f32; 3]) {
383        let e1 = [c[1][0] - c[0][0], c[1][1] - c[0][1], c[1][2] - c[0][2]];
384        let e2 = [c[2][0] - c[0][0], c[2][1] - c[0][1], c[2][2] - c[0][2]];
385        let cr = [
386            e1[1] * e2[2] - e1[2] * e2[1],
387            e1[2] * e2[0] - e1[0] * e2[2],
388            e1[0] * e2[1] - e1[1] * e2[0],
389        ];
390        let dot = cr[0] * n[0] as f64 + cr[1] * n[1] as f64 + cr[2] * n[2] as f64;
391        let o = if dot >= 0.0 { [0, 1, 2, 3] } else { [0, 3, 2, 1] };
392        let v0 = self.vert(c[o[0]], n);
393        let v1 = self.vert(c[o[1]], n);
394        let v2 = self.vert(c[o[2]], n);
395        let v3 = self.vert(c[o[3]], n);
396        self.indices.extend_from_slice(&[v0, v1, v2, v0, v2, v3]);
397    }
398}
399
400/// 3D cellular exposed-face extraction. `grid[axis]` is the sorted snapped grid
401/// lines on that axis. A cell is solid iff its centre lies outside every opening
402/// (it is always inside the host); emit every face of a solid cell that borders
403/// a void cell or the grid boundary, with the outward normal — skipping faces
404/// internal to the solid region. Watertight: a shared face between two solid
405/// cells is emitted by neither; every boundary face is emitted exactly once on
406/// the snapped grid, so reverse edges cancel bit-for-bit.
407fn build_cellular(grid: &[Vec<f64>; 3], openings: &[[[f64; 3]; 2]]) -> Mesh {
408    let nc = [grid[0].len() - 1, grid[1].len() - 1, grid[2].len() - 1];
409    let center = |axis: usize, i: usize| (grid[axis][i] + grid[axis][i + 1]) * 0.5;
410    let solid = |c: [usize; 3]| -> bool {
411        let p = [center(0, c[0]), center(1, c[1]), center(2, c[2])];
412        !openings
413            .iter()
414            .any(|o| (0..3).all(|a| p[a] > o[0][a] && p[a] < o[1][a]))
415    };
416    let mut b = Builder {
417        positions: Vec::new(),
418        normals: Vec::new(),
419        indices: Vec::new(),
420    };
421
422    for i in 0..nc[0] {
423        for j in 0..nc[1] {
424            for k in 0..nc[2] {
425                let cell = [i, j, k];
426                if !solid(cell) {
427                    continue;
428                }
429                for axis in 0..3 {
430                    let (a1, a2) = match axis {
431                        0 => (1usize, 2usize),
432                        1 => (0usize, 2usize),
433                        _ => (0usize, 1usize),
434                    };
435                    for &pos in &[false, true] {
436                        let exposed = if pos {
437                            cell[axis] == nc[axis] - 1 || !solid({
438                                let mut n = cell;
439                                n[axis] += 1;
440                                n
441                            })
442                        } else {
443                            cell[axis] == 0 || !solid({
444                                let mut n = cell;
445                                n[axis] -= 1;
446                                n
447                            })
448                        };
449                        if !exposed {
450                            continue;
451                        }
452                        let coord = grid[axis][cell[axis] + usize::from(pos)];
453                        let (lo1, hi1) = (grid[a1][cell[a1]], grid[a1][cell[a1] + 1]);
454                        let (lo2, hi2) = (grid[a2][cell[a2]], grid[a2][cell[a2] + 1]);
455                        let corner = |x1: f64, x2: f64| {
456                            let mut q = [0.0; 3];
457                            q[axis] = coord;
458                            q[a1] = x1;
459                            q[a2] = x2;
460                            q
461                        };
462                        let mut n = [0.0f32; 3];
463                        n[axis] = if pos { 1.0 } else { -1.0 };
464                        b.quad(
465                            [
466                                corner(lo1, lo2),
467                                corner(hi1, lo2),
468                                corner(hi1, hi2),
469                                corner(lo1, hi2),
470                            ],
471                            n,
472                        );
473                    }
474                }
475            }
476        }
477    }
478
479    let mut m = Mesh::new();
480    m.positions = b.positions;
481    m.normals = b.normals;
482    m.indices = b.indices;
483    m
484}
485
486#[cfg(test)]
487mod tests {
488    use super::*;
489
490    /// Closed axis-aligned box, 12 outward triangles.
491    fn box_mesh(min: [f64; 3], max: [f64; 3]) -> Mesh {
492        let c = [
493            [min[0], min[1], min[2]],
494            [max[0], min[1], min[2]],
495            [max[0], max[1], min[2]],
496            [min[0], max[1], min[2]],
497            [min[0], min[1], max[2]],
498            [max[0], min[1], max[2]],
499            [max[0], max[1], max[2]],
500            [min[0], max[1], max[2]],
501        ];
502        let faces: [([usize; 4], [f32; 3]); 6] = [
503            ([0, 3, 2, 1], [0.0, 0.0, -1.0]),
504            ([4, 5, 6, 7], [0.0, 0.0, 1.0]),
505            ([0, 1, 5, 4], [0.0, -1.0, 0.0]),
506            ([2, 3, 7, 6], [0.0, 1.0, 0.0]),
507            ([0, 4, 7, 3], [-1.0, 0.0, 0.0]),
508            ([1, 2, 6, 5], [1.0, 0.0, 0.0]),
509        ];
510        let mut m = Mesh::new();
511        for (idx, n) in faces {
512            let b = (m.positions.len() / 3) as u32;
513            for &i in &idx {
514                m.positions.extend_from_slice(&[c[i][0] as f32, c[i][1] as f32, c[i][2] as f32]);
515                m.normals.extend_from_slice(&n);
516            }
517            m.indices.extend_from_slice(&[b, b + 1, b + 2, b, b + 2, b + 3]);
518        }
519        m
520    }
521
522    /// DIRECTED exact-f32-bit edge audit (the production crack detector,
523    /// mesh_bridge::exact_open_edges): a watertight oriented surface has every
524    /// directed edge cancelled by its reverse — catches both cracks AND
525    /// inconsistent winding.
526    fn open_edges(m: &Mesh) -> usize {
527        use std::collections::HashMap;
528        let key = |i: u32| {
529            let b = i as usize * 3;
530            (m.positions[b].to_bits(), m.positions[b + 1].to_bits(), m.positions[b + 2].to_bits())
531        };
532        let mut edges: HashMap<_, i64> = HashMap::new();
533        for t in m.indices.chunks_exact(3) {
534            for (a, b) in [(t[0], t[1]), (t[1], t[2]), (t[2], t[0])] {
535                *edges.entry((key(a), key(b))).or_insert(0) += 1;
536                *edges.entry((key(b), key(a))).or_insert(0) -= 1;
537            }
538        }
539        edges.values().filter(|&&c| c != 0).count()
540    }
541
542    fn degenerate(m: &Mesh) -> usize {
543        let v = |i: u32| {
544            let b = i as usize * 3;
545            [m.positions[b], m.positions[b + 1], m.positions[b + 2]]
546        };
547        let mut n = 0;
548        for t in m.indices.chunks_exact(3) {
549            let (a, b, c) = (v(t[0]), v(t[1]), v(t[2]));
550            let e1 = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
551            let e2 = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
552            let cr = [
553                e1[1] * e2[2] - e1[2] * e2[1],
554                e1[2] * e2[0] - e1[0] * e2[2],
555                e1[0] * e2[1] - e1[1] * e2[0],
556            ];
557            if cr[0] * cr[0] + cr[1] * cr[1] + cr[2] * cr[2] == 0.0 {
558                n += 1;
559            }
560        }
561        n
562    }
563
564    /// Signed volume × 6 (divergence), about origin.
565    fn vol6(m: &Mesh) -> f64 {
566        let v = |i: u32| {
567            let b = i as usize * 3;
568            [m.positions[b] as f64, m.positions[b + 1] as f64, m.positions[b + 2] as f64]
569        };
570        let mut s = 0.0;
571        for t in m.indices.chunks_exact(3) {
572            let (a, b, c) = (v(t[0]), v(t[1]), v(t[2]));
573            let cr = [
574                b[1] * c[2] - b[2] * c[1],
575                b[2] * c[0] - b[0] * c[2],
576                b[0] * c[1] - b[1] * c[0],
577            ];
578            s += a[0] * cr[0] + a[1] * cr[1] + a[2] * cr[2];
579        }
580        s
581    }
582
583    // 4m × 0.2m × 3m wall (thin along Y). Opening boxes poke through Y.
584    fn wall(base: [f64; 3]) -> Mesh {
585        box_mesh(base, [base[0] + 4.0, base[1] + 0.2, base[2] + 3.0])
586    }
587    fn opening(base: [f64; 3], u0: f64, u1: f64, z0: f64, z1: f64) -> ([f64; 3], [f64; 3]) {
588        (
589            [base[0] + u0, base[1] - 0.1, base[2] + z0],
590            [base[0] + u1, base[1] + 0.3, base[2] + z1],
591        )
592    }
593
594    fn check_watertight(base: [f64; 3], openings: &[([f64; 3], [f64; 3])], label: &str) {
595        let host = wall(base);
596        let mut st = RectFastStats::default();
597        let cut = subtract_rect_openings(&host, openings, &mut st)
598            .unwrap_or_else(|| panic!("{label}: expected fast path to fire, deferred: {st:?}"));
599        assert_eq!(open_edges(&cut), 0, "{label}: not watertight");
600        assert_eq!(degenerate(&cut), 0, "{label}: degenerate triangles");
601        // Removed volume ≈ Σ opening∩host volume.
602        let removed = (vol6(&host) - vol6(&cut)) / 6.0;
603        let mut expect = 0.0;
604        let (hmn, hmx) = (base, [base[0] + 4.0, base[1] + 0.2, base[2] + 3.0]);
605        for (omn, omx) in openings {
606            let mut vv = 1.0;
607            for k in 0..3 {
608                vv *= (omx[k].min(hmx[k]) - omn[k].max(hmn[k])).max(0.0);
609            }
610            expect += vv;
611        }
612        // Volume tolerance scales with f32 ULP at the host's magnitude (a wall
613        // 220 km from origin carries ~12 mm of inherent f32 error per coordinate
614        // — true of ANY f32 mesh there, exact kernel included; the cut is still
615        // watertight). Relative 2% OR absolute scale-aware, whichever is looser.
616        let mag = base[0].abs().max(base[2].abs()).max(1.0);
617        let vtol = (expect * 0.02).max(mag * 2f64.powi(-23) * 6.0);
618        assert!(
619            (removed - expect).abs() < vtol,
620            "{label}: removed {removed} != expected {expect} (tol {vtol})"
621        );
622    }
623
624    /// Flag semantics (pure core, no env mutation, safe under parallel tests):
625    /// `IFC_LITE_RECT_FAST=0` is the GLOBAL kill switch and also disables the
626    /// parametric path (even against an explicit `IFC_LITE_RECT_PARAM=1`);
627    /// `IFC_LITE_RECT_PARAM=0` disables only the parametric path; both unset
628    /// means the parametric path defaults ON.
629    #[test]
630    fn param_env_default_honours_global_kill_switch() {
631        // Global kill switch thrown (IFC_LITE_RECT_FAST=0 => enabled() false).
632        assert!(!param_env_default(false, None));
633        assert!(!param_env_default(false, Some("1")));
634        assert!(!param_env_default(false, Some("0")));
635        // Parametric-only opt-out.
636        assert!(!param_env_default(true, Some("0")));
637        // Defaults ON; explicit "1" is also ON.
638        assert!(param_env_default(true, None));
639        assert!(param_env_default(true, Some("1")));
640    }
641
642    #[test]
643    fn single_opening_watertight_origin() {
644        check_watertight([0.0, 0.0, 0.0], &[opening([0.0, 0.0, 0.0], 1.5, 2.5, 0.5, 2.0)], "single-origin");
645    }
646
647    #[test]
648    fn single_opening_watertight_building_scale() {
649        check_watertight(
650            [221_534.0, 98_210.0, 47_001.0],
651            &[opening([221_534.0, 98_210.0, 47_001.0], 1.5, 2.5, 0.5, 2.0)],
652            "single-building",
653        );
654    }
655
656    #[test]
657    fn multi_opening_watertight() {
658        let base = [10.0, 5.0, 2.0];
659        let ops = [
660            opening(base, 0.4, 1.2, 0.5, 2.4),
661            opening(base, 1.6, 2.4, 0.5, 2.4),
662            opening(base, 2.8, 3.6, 0.5, 1.0),
663        ];
664        check_watertight(base, &ops, "multi-3");
665    }
666
667    #[test]
668    fn defers_non_box_host() {
669        // a tetrahedron-ish non-box host
670        let mut host = Mesh::new();
671        host.positions = vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0];
672        host.normals = vec![0.0; 12];
673        host.indices = vec![0, 2, 1, 0, 1, 3, 0, 3, 2, 1, 2, 3];
674        let mut st = RectFastStats::default();
675        assert!(subtract_rect_openings(&host, &[opening([0.0, 0.0, 0.0], 1.5, 2.5, 0.5, 2.0)], &mut st).is_none());
676        assert_eq!(st.defer_host_not_box, 1);
677    }
678
679    #[test]
680    fn defers_pathological_opening_count() {
681        // A crafted host with hundreds of tiny openings makes build_cellular
682        // O(N^3); the fast path must defer to the exact kernel, not hang.
683        let host = box_mesh([0.0, 0.0, 0.0], [10.0, 3.0, 0.3]);
684        let openings: Vec<([f64; 3], [f64; 3])> = (0..200)
685            .map(|i| {
686                let x = i as f64 * 0.04;
687                ([x, 0.5, -0.1], [x + 0.02, 1.0, 0.4])
688            })
689            .collect();
690        let mut st = RectFastStats::default();
691        assert!(subtract_rect_openings(&host, &openings, &mut st).is_none());
692        assert_eq!(st.defer_too_many_openings, 1);
693    }
694
695    #[test]
696    fn door_flush_to_floor_watertight() {
697        // DOOR: full thickness, z from BELOW the floor up to 2.0 → touches the
698        // wall's bottom edge (no sill reveal); the bottom face gets a hole. The
699        // case the face-based v1 deferred (off_face); cellular cuts it watertight.
700        let base = [0.0, 0.0, 0.0];
701        let door = (
702            [base[0] + 1.5, base[1] - 0.1, base[2] - 0.5],
703            [base[0] + 2.5, base[1] + 0.3, base[2] + 2.0],
704        );
705        check_watertight(base, &[door], "door-flush-floor");
706    }
707
708    #[test]
709    fn edge_notch_watertight() {
710        // opening that exceeds the wall's X extent → a notch flush with the right
711        // edge (no right jamb). Was an off_face defer; cellular handles it.
712        let base = [0.0, 0.0, 0.0];
713        let notch = (
714            [base[0] + 3.8, base[1] - 0.1, base[2] + 0.5],
715            [base[0] + 4.5, base[1] + 0.3, base[2] + 2.0],
716        );
717        check_watertight(base, &[notch], "edge-notch");
718    }
719
720    #[test]
721    fn recess_cut_watertight() {
722        // RECESS: does NOT span the full thickness (a niche/pocket). Was a
723        // not_through defer; cellular cuts the pocket watertight.
724        let base = [0.0, 0.0, 0.0];
725        let recess = (
726            [base[0] + 1.5, base[1] + 0.05, base[2] + 0.5],
727            [base[0] + 2.5, base[1] + 0.15, base[2] + 2.0],
728        );
729        check_watertight(base, &[recess], "recess");
730    }
731
732    #[test]
733    fn opening_fully_outside_defers() {
734        // genuine no-op: opening entirely outside the host → no overlap → defer.
735        let base = [0.0, 0.0, 0.0];
736        let outside = (
737            [base[0] + 10.0, base[1] - 0.1, base[2] + 0.5],
738            [base[0] + 11.0, base[1] + 0.3, base[2] + 2.0],
739        );
740        let mut st = RectFastStats::default();
741        assert!(subtract_rect_openings(&wall(base), &[outside], &mut st).is_none());
742    }
743
744    #[test]
745    fn defers_near_edge_at_building_scale() {
746        // 8 µm reveal at ~220 km: below f32 ULP → must defer (the robustness gate)
747        let base = [221_534.0, 98_210.0, 47_001.0];
748        let tight = opening(base, 8e-6, 4.0 - 8e-6, 8e-6, 3.0 - 8e-6);
749        let mut st = RectFastStats::default();
750        assert!(
751            subtract_rect_openings(&wall(base), &[tight], &mut st).is_none(),
752            "near-edge at building scale must defer, not crack"
753        );
754    }
755
756    #[test]
757    fn deterministic_output() {
758        let base = [10.0, 5.0, 2.0];
759        let ops = [opening(base, 1.5, 2.5, 0.5, 2.0)];
760        let mut s1 = RectFastStats::default();
761        let mut s2 = RectFastStats::default();
762        let a = subtract_rect_openings(&wall(base), &ops, &mut s1).unwrap();
763        let b = subtract_rect_openings(&wall(base), &ops, &mut s2).unwrap();
764        assert_eq!(a.positions, b.positions);
765        assert_eq!(a.indices, b.indices);
766    }
767
768    #[test]
769    fn redundant_whole_wall_opening_defers() {
770        // #1167: a double-encoded void (#964) whose box CONTAINS the whole wall
771        // — full width × full height, through the thickness. The exact kernel
772        // treats this as a no-op (coplanar cutter → host unchanged), so the fast
773        // path MUST defer; cutting it would erase the entire wall and leave the
774        // window floating in a giant void.
775        let base = [10.0, 5.0, 2.0];
776        let whole = opening(base, 0.0, 4.0, 0.0, 3.0); // spans the full 4 m × 3 m face
777        let mut st = RectFastStats::default();
778        assert!(
779            subtract_rect_openings(&wall(base), &[whole], &mut st).is_none(),
780            "opening that contains the whole host must defer, not erase the wall"
781        );
782        assert_eq!(st.defer_off_face, 1);
783
784        // A whole-wall opening MIXED with a genuine window still defers the host
785        // (the redundant void poisons the batch → hand the whole element to the
786        // exact path, which cuts the real window and no-ops the redundant one).
787        let mut st_mix = RectFastStats::default();
788        assert!(
789            subtract_rect_openings(
790                &wall(base),
791                &[whole, opening(base, 0.5, 1.5, 0.4, 2.6)],
792                &mut st_mix
793            )
794            .is_none(),
795            "a redundant whole-wall opening must defer the whole host"
796        );
797
798        // Sanity: a genuine interior window (margin on every in-face axis) still
799        // fires the fast path — the guard is not over-broad.
800        let mut st_ok = RectFastStats::default();
801        assert!(
802            subtract_rect_openings(&wall(base), &[opening(base, 0.5, 3.5, 0.4, 2.6)], &mut st_ok)
803                .is_some(),
804            "an interior window must still fire the fast path"
805        );
806    }
807}