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