ifc_lite_geometry/router/voids/
prism_cut.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 prism subtraction on the host MESH.
6//!
7//! The dominant expensive void cut on CSG-heavy models (ISSUE_098 / ISSUE_129)
8//! is a PERPENDICULAR prism opening — a window / door / sleeve — on a host the
9//! other fast paths cannot serve: faceted-BREP walls, clipped extrusions,
10//! multi-item bodies. The exact mesh-arrangement kernel is at its
11//! single-threaded floor (~15 ms per cut), yet the cutter is just a polygonal
12//! profile swept through the wall (masonry openings are REBATED — stepped
13//! multi-vertex profiles, not plain boxes). This path subtracts such a cutter
14//! analytically from ANY host mesh:
15//!
16//! 1. DETECT that the cutter mesh is a genuine prism: a closed 2-manifold with
17//!    one depth axis `d` such that every facet is either parallel (the two end
18//!    caps, each on one extremal plane) or perpendicular (the sides) to `d`;
19//!    the profile polygon is stitched from the cap facet's boundary loop and
20//!    the enclosed volume must reconcile with `area × depth`. Any deviation
21//!    (curved-cap void, open shell, annular profile, self-intersecting
22//!    garbage) is left to the exact kernel.
23//! 2. EXTEND each cap past the host surface when (and only when) the swept
24//!    slab is provably empty of host material — a flush cap becomes a clean
25//!    transversal crossing (the #1112 lesson) while a genuine blind recess
26//!    keeps its authored bottom.
27//! 3. DECOMPOSE only the host triangles that actually reach the prism: each
28//!    such triangle T gets the exact `T ∩ ∂prism` seam segments (cap-plane
29//!    chords clipped to the profile in 2D; side-strip chords clipped to the
30//!    strip rectangle) as constraints of a per-triangle conforming CDT, whose
31//!    sub-triangles are classified inside/outside the prism by centroid.
32//!    Shared edges subdivide identically across neighbours (every edge×plane
33//!    crossing is computed once through a canonical-order cache and admitted
34//!    by a point-deterministic face-membership gate), so the kept surface
35//!    stays T-junction-free. Triangles away from the prism pass through
36//!    UNTOUCHED — original coordinates and normals.
37//! 4. CAP the reveal: each prism face (2 profile caps + one rectangle per
38//!    profile edge) is triangulated by a CDT of the face polygon constrained
39//!    by the seams from step 3, each sub-triangle kept iff its centroid lies
40//!    inside the (pre-cut) host solid by ray parity. Cap boundaries reuse the
41//!    exact seam coordinates, so caps weld to the kept host fragments.
42//!
43//! SELF-CHECKS (failed candidates remain for the exact kernel; a final-surface
44//! failure defers the full opening set):
45//! * retained and discarded host fragments must conserve the source surface
46//!   within host-scale clipping tolerance, independently of the reference;
47//! * a geometric envelope and the consolidated result must permit measurable
48//!   removal; open fragment divergence is never used as a removed-solid volume;
49//! * the host must arrive as a consistently-wound closed solid and the final
50//!   emitted mesh must pass the same DIRECTED quantized closed-surface audit
51//!   (which also catches doubled coincident faces and flipped caps).
52//!
53//! The whole computation runs in f64 (host f32 promoted once, stored f32 once
54//! at the end), with no FMA-sensitive matrix products and deterministic
55//! iteration order, so native == wasm stays byte-identical (#1297). Vertices
56//! are never welded (#846); per-element `origin` is preserved exactly.
57//!
58//! Gate: `IFC_LITE_PRISM_CUT=0` forces every host back through the exact
59//! kernel (A/B measurement). Default ON; wasm has no env, so the default holds
60//! on both targets.
61
62use std::sync::OnceLock;
63
64use super::geom::opening_mesh_thinnest_axis_dir;
65use super::{cutter_is_closed_manifold, GeometryRouter, OpeningType, VoidContext};
66use crate::cdt::triangulate_pslg;
67use crate::mesh::Mesh;
68use nalgebra::Point2;
69use rustc_hash::FxHashMap;
70
71/// `IFC_LITE_PRISM_CUT=0` disables the analytic prism-subtraction path (exact
72/// kernel for every host). Default ON; read once.
73pub(crate) mod closure_checks;
74mod vertex_dedup;
75mod finish;
76mod tjunction_close;
77mod planar_correction;
78#[cfg(test)]
79#[path = "prism_cut_tests.rs"]
80mod tests;
81use closure_checks::{closed_enough_to_emit, directed_closed, ClosureVerdict};
82#[cfg(test)]
83use closure_checks::closed_or_hairline;
84pub(crate) use vertex_dedup::dedup_cut_vertices;
85pub(in crate::router::voids) use finish::finish_cut_verdict;
86pub(super) use planar_correction::correct_planar_overlap;
87
88pub(super) fn enabled() -> bool {
89    static ON: OnceLock<bool> = OnceLock::new();
90    *ON.get_or_init(|| std::env::var("IFC_LITE_PRISM_CUT").as_deref() != Ok("0"))
91}
92
93// ─────────────────────────── diagnostic counters ────────────────────────────
94// Per the geometry-crate convention, telemetry is compiled in ONLY under an
95// observability feature (`observability` / `csg_capture` / `debug_geometry`).
96// The default build carries no process-global atomics on the hot path — no
97// per-opening atomic traffic, and no shared mutable state for concurrent unit
98// tests to race on. `take_prism_stats` / `take_prism_defers` stay public in
99// both configurations (return zeros when the feature is off) so the crate's
100// exported surface is stable; unit tests assert path coverage through
101// `try_prism_cut`'s RETURN VALUE, not these counters.
102
103#[cfg(any(feature = "observability", feature = "csg_capture", feature = "debug_geometry"))]
104mod diag {
105    use std::sync::atomic::{AtomicU64, Ordering};
106
107    static FIRES: AtomicU64 = AtomicU64::new(0);
108    static OPENINGS_ANALYTIC: AtomicU64 = AtomicU64::new(0);
109    static OPENINGS_RESIDUAL: AtomicU64 = AtomicU64::new(0);
110
111    /// Defer-reason counters (diagnostic only): indices into [`DEFER_NAMES`].
112    const DEFER_NAMES: [&str; 10] = [
113        "host_not_closed",
114        "host_promote_fail",
115        "op_not_prism",
116        "op_tiny",
117        "op_no_overlap",
118        "op_engulf",
119        "op_veto_overlap",
120        "cut_cdt_fail",
121        "cut_partition_fail",
122        "out_not_closed",
123    ];
124    static DEFERS: [AtomicU64; 10] = [
125        AtomicU64::new(0),
126        AtomicU64::new(0),
127        AtomicU64::new(0),
128        AtomicU64::new(0),
129        AtomicU64::new(0),
130        AtomicU64::new(0),
131        AtomicU64::new(0),
132        AtomicU64::new(0),
133        AtomicU64::new(0),
134        AtomicU64::new(0),
135    ];
136
137    #[inline]
138    pub(super) fn defer(i: usize) {
139        #[cfg(feature = "opening-perf-trace")]
140        crate::opening_perf_trace::record(|c| c.prism_defers[i] = c.prism_defers[i].saturating_add(1));
141        crate::telemetry_transaction::record(move || {
142            DEFERS[i].fetch_add(1, Ordering::Relaxed);
143        });
144    }
145
146    /// Record one analytic-cut host: (fires, openings cut, openings residual).
147    #[inline]
148    pub(super) fn record_cut(committed_ops: usize, residual: usize) {
149        #[cfg(feature = "opening-perf-trace")]
150        crate::opening_perf_trace::record(|c| {
151            c.prism_candidate_successes = c.prism_candidate_successes.saturating_add(1);
152            c.prism_candidate_openings = c.prism_candidate_openings.saturating_add(committed_ops as u64);
153            c.prism_residual_openings = c.prism_residual_openings.saturating_add(residual as u64);
154        });
155        crate::telemetry_transaction::record(move || {
156            FIRES.fetch_add(1, Ordering::Relaxed);
157            OPENINGS_ANALYTIC.fetch_add(committed_ops as u64, Ordering::Relaxed);
158            OPENINGS_RESIDUAL.fetch_add(residual as u64, Ordering::Relaxed);
159        });
160    }
161
162    /// Read + reset the per-reason defer counters as (name, count) pairs.
163    pub fn take_prism_defers() -> Vec<(&'static str, u64)> {
164        DEFER_NAMES
165            .iter()
166            .enumerate()
167            .map(|(i, n)| (*n, DEFERS[i].swap(0, Ordering::Relaxed)))
168            .collect()
169    }
170
171    /// Read + reset the prism-path telemetry: (hosts cut via the prism path,
172    /// openings subtracted analytically, openings routed to the exact residual
173    /// on prism-cut hosts). Process-global; the perf harness reports the
174    /// fast-path hit-rate from it. Relaxed atomics — a stale read under
175    /// concurrency only mis-reports a diagnostic count, never geometry.
176    pub fn take_prism_stats() -> (u64, u64, u64) {
177        (
178            FIRES.swap(0, Ordering::Relaxed),
179            OPENINGS_ANALYTIC.swap(0, Ordering::Relaxed),
180            OPENINGS_RESIDUAL.swap(0, Ordering::Relaxed),
181        )
182    }
183}
184
185#[cfg(not(any(feature = "observability", feature = "csg_capture", feature = "debug_geometry")))]
186mod diag {
187    #[inline]
188    pub(super) fn defer(_i: usize) {
189        #[cfg(feature = "opening-perf-trace")]
190        crate::opening_perf_trace::record(|c| c.prism_defers[_i] = c.prism_defers[_i].saturating_add(1));
191    }
192    #[inline]
193    pub(super) fn record_cut(_committed_ops: usize, _residual: usize) {
194        #[cfg(feature = "opening-perf-trace")]
195        crate::opening_perf_trace::record(|c| {
196            c.prism_candidate_successes = c.prism_candidate_successes.saturating_add(1);
197            c.prism_candidate_openings = c.prism_candidate_openings.saturating_add(_committed_ops as u64);
198            c.prism_residual_openings = c.prism_residual_openings.saturating_add(_residual as u64);
199        });
200    }
201    /// Telemetry disabled in the default build; the perf harness must enable an
202    /// observability feature to read real counts.
203    pub fn take_prism_defers() -> Vec<(&'static str, u64)> {
204        Vec::new()
205    }
206    pub fn take_prism_stats() -> (u64, u64, u64) {
207        (0, 0, 0)
208    }
209}
210
211use diag::{defer, record_cut};
212pub use diag::{take_prism_defers, take_prism_stats};
213
214// ─────────────────────────── small f64 vector kit ───────────────────────────
215// Explicit component arithmetic (no nalgebra matrix products) so no FMA can
216// sneak in and break native==wasm byte identity.
217
218pub(super) type V3 = [f64; 3];
219type V2 = [f64; 2];
220
221#[inline]
222pub(super) fn dot(a: V3, b: V3) -> f64 {
223    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
224}
225#[inline]
226pub(super) fn sub(a: V3, b: V3) -> V3 {
227    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
228}
229#[inline]
230pub(super) fn add(a: V3, b: V3) -> V3 {
231    [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
232}
233#[inline]
234pub(super) fn scale(a: V3, s: f64) -> V3 {
235    [a[0] * s, a[1] * s, a[2] * s]
236}
237#[inline]
238pub(super) fn cross(a: V3, b: V3) -> V3 {
239    [
240        a[1] * b[2] - a[2] * b[1],
241        a[2] * b[0] - a[0] * b[2],
242        a[0] * b[1] - a[1] * b[0],
243    ]
244}
245#[inline]
246pub(super) fn norm(a: V3) -> f64 {
247    dot(a, a).sqrt()
248}
249#[inline]
250pub(super) fn normalize(a: V3) -> Option<V3> {
251    let n = norm(a);
252    if !n.is_finite() || n < 1.0e-12 {
253        None
254    } else {
255        Some(scale(a, 1.0 / n))
256    }
257}
258#[inline]
259pub(super) fn lerp(a: V3, b: V3, t: f64) -> V3 {
260    [
261        a[0] + (b[0] - a[0]) * t,
262        a[1] + (b[1] - a[1]) * t,
263        a[2] + (b[2] - a[2]) * t,
264    ]
265}
266#[inline]
267pub(super) fn bits(p: V3) -> [u64; 3] {
268    [p[0].to_bits(), p[1].to_bits(), p[2].to_bits()]
269}
270
271/// Even-odd point-in-polygon (half-open crossing rule — deterministic on
272/// boundary-adjacent points, and identical for every caller testing the same
273/// point, which is what the neighbour-consistency argument needs).
274fn pip(pt: V2, poly: &[V2]) -> bool {
275    let (x, y) = (pt[0], pt[1]);
276    let n = poly.len();
277    let mut inside = false;
278    let mut j = n - 1;
279    for i in 0..n {
280        let (xi, yi) = (poly[i][0], poly[i][1]);
281        let (xj, yj) = (poly[j][0], poly[j][1]);
282        if (yi > y) != (yj > y) {
283            let xc = xi + (y - yi) / (yj - yi) * (xj - xi);
284            if x < xc {
285                inside = !inside;
286            }
287        }
288        j = i;
289    }
290    inside
291}
292
293/// Parameter `t ∈ [0, 1]` on segment `p→q` where it crosses segment `a→b`
294/// (proper or endpoint-touching crossing), or `None` when parallel / disjoint.
295fn seg_cross_param(p: V2, q: V2, a: V2, b: V2) -> Option<f64> {
296    let r = [q[0] - p[0], q[1] - p[1]];
297    let s = [b[0] - a[0], b[1] - a[1]];
298    let denom = r[0] * s[1] - r[1] * s[0];
299    if denom.abs() < 1.0e-18 {
300        return None;
301    }
302    let ap = [a[0] - p[0], a[1] - p[1]];
303    let t = (ap[0] * s[1] - ap[1] * s[0]) / denom;
304    let u = (ap[0] * r[1] - ap[1] * r[0]) / denom;
305    const E: f64 = 1.0e-12;
306    if (-E..=1.0 + E).contains(&t) && (-E..=1.0 + E).contains(&u) {
307        Some(t.clamp(0.0, 1.0))
308    } else {
309        None
310    }
311}
312
313/// One host triangle in f64 (positions + per-vertex normals), host-local frame.
314#[derive(Clone)]
315struct PTri {
316    p: [V3; 3],
317    n: [V3; 3],
318}
319
320impl PTri {
321    #[inline]
322    fn aabb(&self) -> (V3, V3) {
323        let mut lo = self.p[0];
324        let mut hi = self.p[0];
325        for v in &self.p[1..] {
326            for k in 0..3 {
327                lo[k] = lo[k].min(v[k]);
328                hi[k] = hi[k].max(v[k]);
329            }
330        }
331        (lo, hi)
332    }
333}
334
335/// The retained and discarded host fragments must reconstruct the source surface.
336/// Their combined divergence residual is meaningful even when either fragment
337/// set is open: this is a partition identity, not an enclosed-volume reading.
338///
339/// Check its value at the host AABB centre and its maximum variation across the
340/// AABB. The residual is affine in the reference, so twice the sum of absolute
341/// centre-to-face differences gives its exact range over all eight corners.
342/// Both checks allow the established one-part-per-million clipping error. The
343/// absolute floor uses the full host extent; a remote or long cutter cannot
344/// enlarge it. Caps are excluded because their opposite contributions cancel.
345fn partition_consistent(host: &[PTri], out: &[PTri], inside: &[PTri], bounds: &[(V3, V3)]) -> bool {
346    let mut lo = [f64::INFINITY; 3];
347    let mut hi = [f64::NEG_INFINITY; 3];
348    for (a, b) in bounds {
349        for k in 0..3 {
350            lo[k] = lo[k].min(a[k]);
351            hi[k] = hi[k].max(b[k]);
352        }
353    }
354    let origin: V3 = std::array::from_fn(|k| (lo[k] + hi[k]) * 0.5);
355    let extent = (0..3).map(|k| hi[k] - lo[k]).fold(0.0, f64::max);
356    let sum = |ts: &[PTri], reference: &V3| {
357        ts.iter()
358            .map(|t| {
359                crate::kernel::signed_volume::tetra_volume6(&t.p[0], &t.p[1], &t.p[2], reference)
360            })
361            .sum::<f64>()
362            / 6.0
363    };
364    let tolerance =
365        (1.0e-12 * (1.0 + extent).powi(3) + 1.0e-9).max(sum(host, &origin).abs() * 1.0e-6);
366    let residual =
367        |reference: &V3| sum(out, reference) + sum(inside, reference) - sum(host, reference);
368    let center = residual(&origin);
369    let range = (0..3)
370        .map(|axis| {
371            let mut reference = origin;
372            reference[axis] = hi[axis];
373            2.0 * (residual(&reference) - center).abs()
374        })
375        .sum::<f64>();
376    center.is_finite() && range.is_finite() && center.abs() <= tolerance && range <= tolerance
377}
378
379/// A containing box is a geometric upper bound on any possible removed volume;
380/// it does not require the discarded fragments and caps to form a closed shell.
381/// Use a face-aligned box so an exactly planar contact has zero thickness even
382/// when it is oblique to the world axes. Keep the existing 1e-9 m³ measurable
383/// removal threshold without rejecting every thin but wide authored cut.
384fn has_measurable_region(inside: &[PTri], caps: &[PTri]) -> bool {
385    let faces: Vec<PTri> = inside.iter().chain(caps).cloned().collect();
386    let mut geometry = mesh_from_ptris(&faces, &Mesh::new());
387    geometry.clean_degenerate();
388    if geometry.indices.is_empty() {
389        return false;
390    }
391    let normal = |t: &PTri| cross(sub(t.p[1], t.p[0]), sub(t.p[2], t.p[0]));
392    let Some(face) = faces
393        .iter()
394        .max_by(|a, b| norm(normal(a)).total_cmp(&norm(normal(b))))
395    else {
396        return false;
397    };
398    let origin = face.p[0];
399    let Some(u) = normalize(sub(face.p[1], origin)) else {
400        return false;
401    };
402    let Some(n) = normalize(normal(face)) else {
403        return false;
404    };
405    let axes = [u, cross(n, u), n];
406    let mut lo = [0.0f64; 3];
407    let mut hi = [0.0f64; 3];
408    for t in &faces {
409        for p in t.p {
410            for k in 0..3 {
411                let coordinate = dot(sub(p, origin), axes[k]);
412                lo[k] = lo[k].min(coordinate);
413                hi[k] = hi[k].max(coordinate);
414            }
415        }
416    }
417    let envelope = (0..3).map(|k| hi[k] - lo[k]).product::<f64>();
418    envelope.is_finite() && envelope >= 1.0e-9
419}
420
421/// Reject a consolidated no-op before optional refinement can perturb it.
422/// Only call this after auditing the result surface. A shared host reference
423/// cancels untouched hairline flux; the AABB drift bounds any dependence left
424/// in the before/after difference. Reject only when the ENTIRE change interval
425/// is below the established 1e-9 m³ measurable-removal threshold.
426fn consolidated_change_is_measurable(before: &Mesh, after: &Mesh) -> bool {
427    let (_, delta, drift) = mesh_change_interval(before, after);
428    delta.is_finite() && drift.is_finite() && delta.abs() + drift >= 1.0e-9
429}
430
431fn mesh_change_interval(before: &Mesh, after: &Mesh) -> (f64, f64, f64) {
432    use super::geom::{mesh_signed_volume_about, volume_reference};
433    let reference = volume_reference(before);
434    let volume = mesh_signed_volume_about(before, &reference);
435    let delta = mesh_signed_volume_about(after, &reference) - volume;
436    let (_, hi) = before.bounds();
437    let drift = (0..3)
438        .map(|axis| {
439            let mut probe = reference;
440            probe[axis] = hi[axis] as f64;
441            (mesh_signed_volume_about(after, &probe)
442                - mesh_signed_volume_about(before, &probe)
443                - delta)
444                .abs()
445        })
446        .sum::<f64>();
447    (volume, delta, drift)
448}
449
450/// Final subtraction bounds use audited result/host surfaces and an independent
451/// cutter ceiling. Every committed cutter is measured AFTER cap/profile extension;
452/// summing those prism volumes conservatively bounds their union, and the host
453/// volume independently limits removal even for a very long cutter.
454///
455/// The local quantization model documented by `kernel::mesh_volume::mesh_volume`
456/// is surface_area * SNAP_GRID. Both boundaries can move, so their areas add;
457/// no world-coordinate magnitude or candidate-fragment envelope sets this bound.
458fn result_volume_within_bounds(before: &Mesh, after: &Mesh, cutter_volume: f64) -> bool {
459    let (volume, raw_delta, drift) = mesh_change_interval(before, after);
460    let delta = raw_delta * volume.signum();
461    let area = |mesh: &Mesh| {
462        let p =
463            |i: u32| -> V3 { std::array::from_fn(|k| mesh.positions[i as usize * 3 + k] as f64) };
464        mesh.indices
465            .chunks_exact(3)
466            .map(|t| norm(cross(sub(p(t[1]), p(t[0])), sub(p(t[2]), p(t[0])))) * 0.5)
467            .sum::<f64>()
468    };
469    let (lo, hi) = before.bounds();
470    let extent = (0..3)
471        .map(|k| hi[k] as f64 - lo[k] as f64)
472        .fold(0.0, f64::max);
473    let floor = (1.0e-12 * (1.0 + extent).powi(3) + 1.0e-9).max(volume.abs() * 1.0e-6);
474    let tolerance = (area(before) + area(after)) * crate::kernel::mesh_bridge::SNAP_GRID + floor;
475    let ceiling = cutter_volume.min(volume.abs());
476    volume.is_finite()
477        && volume != 0.0
478        && cutter_volume.is_finite()
479        && cutter_volume >= 0.0
480        && delta.is_finite()
481        && drift.is_finite()
482        && tolerance.is_finite()
483        && delta + drift <= tolerance
484        && -delta + drift <= ceiling + tolerance
485}
486
487/// Promote a `Mesh` to the f64 triangle list, folding nothing (the mesh is
488/// taken in its own frame — `origin` semantics are preserved by the caller).
489fn ptris_from_mesh(mesh: &Mesh) -> Option<Vec<PTri>> {
490    // Reject structurally malformed meshes outright: `chunks_exact(3)` below
491    // silently drops any trailing partial triple, so a positions/indices length
492    // that is not a multiple of 3 would rebuild the solid missing its tail data
493    // while every emitted index still passed its bounds check.
494    if !mesh.positions.len().is_multiple_of(3) || !mesh.indices.len().is_multiple_of(3) {
495        return None;
496    }
497    let vc = mesh.positions.len() / 3;
498    let have_normals = mesh.normals.len() == mesh.positions.len();
499    let mut out = Vec::with_capacity(mesh.indices.len() / 3);
500    for t in mesh.indices.chunks_exact(3) {
501        if t.iter().any(|&i| i as usize >= vc) {
502            return None;
503        }
504        let read = |i: u32| -> V3 {
505            let b = i as usize * 3;
506            [
507                mesh.positions[b] as f64,
508                mesh.positions[b + 1] as f64,
509                mesh.positions[b + 2] as f64,
510            ]
511        };
512        let p = [read(t[0]), read(t[1]), read(t[2])];
513        if p.iter().any(|v| v.iter().any(|c| !c.is_finite())) {
514            return None;
515        }
516        let n = if have_normals {
517            let rn = |i: u32| -> V3 {
518                let b = i as usize * 3;
519                [
520                    mesh.normals[b] as f64,
521                    mesh.normals[b + 1] as f64,
522                    mesh.normals[b + 2] as f64,
523                ]
524            };
525            [rn(t[0]), rn(t[1]), rn(t[2])]
526        } else {
527            let fnrm =
528                normalize(cross(sub(p[1], p[0]), sub(p[2], p[0]))).unwrap_or([0.0, 0.0, 1.0]);
529            [fnrm, fnrm, fnrm]
530        };
531        out.push(PTri { p, n });
532    }
533    Some(out)
534}
535
536/// Store the f64 triangle list back to an f32 `Mesh` in the host's frame.
537/// Per-triangle vertices (no sharing, no welding — #846); normals normalized
538/// with the triangle's geometric normal as fallback.
539fn mesh_from_ptris(tris: &[PTri], template: &Mesh) -> Mesh {
540    let mut positions = Vec::with_capacity(tris.len() * 9);
541    let mut normals = Vec::with_capacity(tris.len() * 9);
542    let mut indices = Vec::with_capacity(tris.len() * 3);
543    for (i, t) in tris.iter().enumerate() {
544        let face = normalize(cross(sub(t.p[1], t.p[0]), sub(t.p[2], t.p[0])))
545            .unwrap_or([0.0, 0.0, 1.0]);
546        for v in 0..3 {
547            positions.push(t.p[v][0] as f32);
548            positions.push(t.p[v][1] as f32);
549            positions.push(t.p[v][2] as f32);
550            let n = normalize(t.n[v]).unwrap_or(face);
551            normals.push(n[0] as f32);
552            normals.push(n[1] as f32);
553            normals.push(n[2] as f32);
554        }
555        let base = (i * 3) as u32;
556        indices.extend_from_slice(&[base, base + 1, base + 2]);
557    }
558    Mesh {
559        positions,
560        normals,
561        indices,
562        rtc_applied: template.rtc_applied,
563        origin: template.origin,
564        instance_meta: None,
565        local_bounds: None,
566        local_to_world: None,
567        welded_in_object_frame: false,
568        plane_tags: None,
569    }
570}
571
572// ─────────────────────────────── prism cutter ───────────────────────────────
573
574/// Maximum profile vertex count admitted to the analytic path (face ids must
575/// fit `u8`, and a pathological many-gon belongs to the exact kernel anyway).
576const MAX_PROFILE_VERTS: usize = 120;
577
578/// A stepped-extrusion cutter: a stack of K depth slabs along axis `d`, slab
579/// `k` spanning `planes[k]..planes[k+1]` with constant CCW cross-section
580/// `profiles[k]`. K = 1 is a plain prism; K > 1 is the REBATED masonry opening
581/// (the ISSUE_098 window voids: outer box + inner box sharing a step plane).
582/// `(u, v, d)` is orthonormal right-handed (`u × v = d`).
583#[derive(Clone)]
584struct PrismFrame {
585    u: V3,
586    v: V3,
587    d: V3,
588    /// K+1 sorted depth planes.
589    planes: Vec<f64>,
590    /// K CCW simple polygons (no holes), one per slab.
591    profiles: Vec<Vec<V2>>,
592    /// Per-slab |area|.
593    slab_area: Vec<f64>,
594    /// Per-slab strictly interior point (max-area CDT triangle centroid —
595    /// robust for non-convex rebated profiles).
596    slab_interior: Vec<V2>,
597    /// Overall profile bounding box (lo, hi) in `(u, v)`.
598    bb: (V2, V2),
599}
600
601impl PrismFrame {
602    #[inline]
603    fn d0(&self) -> f64 {
604        self.planes[0]
605    }
606    #[inline]
607    fn d1(&self) -> f64 {
608        *self.planes.last().unwrap()
609    }
610    #[inline]
611    fn volume(&self) -> f64 {
612        (0..self.profiles.len())
613            .map(|k| self.slab_area[k] * (self.planes[k + 1] - self.planes[k]))
614            .sum()
615    }
616    /// Canonical lift `(u, v, depth) → 3D`. FIXED summation order so every
617    /// face computing the same point produces identical bits.
618    #[inline]
619    fn lift(&self, uu: f64, vv: f64, w: f64) -> V3 {
620        add(add(scale(self.u, uu), scale(self.v, vv)), scale(self.d, w))
621    }
622    #[inline]
623    fn to2(&self, p: V3) -> V2 {
624        [dot(p, self.u), dot(p, self.v)]
625    }
626    /// Strict interior test (open solid).
627    #[inline]
628    fn contains(&self, p: V3) -> bool {
629        let w = dot(p, self.d);
630        if w <= self.d0() || w >= self.d1() {
631            return false;
632        }
633        let q = self.to2(p);
634        for k in 0..self.profiles.len() {
635            if w < self.planes[k + 1] {
636                return pip(q, &self.profiles[k]);
637            }
638        }
639        false
640    }
641    /// Largest coordinate magnitude over the swept profile corners (epsilon
642    /// scaling).
643    fn corner_mag(&self) -> f64 {
644        let mut mag = 0.0f64;
645        for prof in &self.profiles {
646            for &[uu, vv] in prof {
647                for w in [self.d0(), self.d1()] {
648                    for c in self.lift(uu, vv, w) {
649                        mag = mag.max(c.abs());
650                    }
651                }
652            }
653        }
654        mag
655    }
656    /// Conservative OBB of the solid (profile bbox × depth) as (axes, lo, hi)
657    /// intervals for the SAT overlap veto.
658    fn obb(&self) -> ([V3; 3], V3, V3) {
659        (
660            [self.u, self.v, self.d],
661            [self.bb.0[0], self.bb.0[1], self.d0()],
662            [self.bb.1[0], self.bb.1[1], self.d1()],
663        )
664    }
665}
666
667/// One face of the stepped solid.
668#[derive(Clone, Copy)]
669enum Face {
670    /// The depth plane `planes[j]`; its exposed region is the XOR of the
671    /// adjacent slab profiles (`∅` beyond the ends).
672    Cap { j: usize },
673    /// Side strip of slab `k`, profile edge `i`, over
674    /// `planes[k]..planes[k+1]`.
675    Strip { k: usize, i: usize },
676}
677
678/// Enumerate the faces of a stack deterministically.
679fn stack_faces(pf: &PrismFrame) -> Vec<Face> {
680    let mut out = Vec::new();
681    for j in 0..pf.planes.len() {
682        out.push(Face::Cap { j });
683    }
684    for k in 0..pf.profiles.len() {
685        for i in 0..pf.profiles[k].len() {
686            out.push(Face::Strip { k, i });
687        }
688    }
689    out
690}
691
692/// Whether a 2D point lies in cap `j`'s exposed region: inside exactly one of
693/// the two adjacent slab profiles (XOR; the region a stepped shoulder or an
694/// end cap exposes). Point-deterministic — every caller agrees.
695fn cap_xor(pf: &PrismFrame, j: usize, q: V2) -> bool {
696    let in_prev = j > 0 && pip(q, &pf.profiles[j - 1]);
697    let in_next = j < pf.profiles.len() && pip(q, &pf.profiles[j]);
698    in_prev != in_next
699}
700
701/// Oriented-box overlap via the separating-axis theorem (15 axes), with a
702/// positive `margin` treating near-contact as overlap. Boxes given as
703/// (orthonormal axes, per-axis projection interval).
704fn obb_overlaps(a: &([V3; 3], V3, V3), b: &([V3; 3], V3, V3), margin: f64) -> bool {
705    let centre = |x: &([V3; 3], V3, V3)| -> V3 {
706        let mid = [
707            (x.1[0] + x.2[0]) * 0.5,
708            (x.1[1] + x.2[1]) * 0.5,
709            (x.1[2] + x.2[2]) * 0.5,
710        ];
711        add(
712            add(scale(x.0[0], mid[0]), scale(x.0[1], mid[1])),
713            scale(x.0[2], mid[2]),
714        )
715    };
716    let half = |x: &([V3; 3], V3, V3), k: usize| (x.2[k] - x.1[k]) * 0.5;
717    let dvec = sub(centre(b), centre(a));
718    let mut axes: Vec<V3> = Vec::with_capacity(15);
719    for k in 0..3 {
720        axes.push(a.0[k]);
721        axes.push(b.0[k]);
722    }
723    for i in 0..3 {
724        for j in 0..3 {
725            let c = cross(a.0[i], b.0[j]);
726            if norm(c) > 1.0e-9 {
727                axes.push(c);
728            }
729        }
730    }
731    for l in axes {
732        let Some(l) = normalize(l) else { continue };
733        let ra: f64 = (0..3).map(|k| half(a, k) * dot(a.0[k], l).abs()).sum();
734        let rb: f64 = (0..3).map(|k| half(b, k) * dot(b.0[k], l).abs()).sum();
735        if dot(dvec, l).abs() > ra + rb + margin {
736            return false;
737        }
738    }
739    true
740}
741
742/// Deterministic orthonormal right-handed basis `(u, v)` for a depth axis `d`
743/// (`u × v = d`), using the same seed convention as `OpeningFrame::from_depth`.
744fn basis_from_depth(d: V3) -> Option<(V3, V3)> {
745    let seed = if d[2].abs() < 0.9 {
746        [0.0, 0.0, 1.0]
747    } else {
748        [0.0, 1.0, 0.0]
749    };
750    let u = normalize(cross(seed, d))?;
751    let v = normalize(cross(d, u))?;
752    Some((u, v))
753}
754
755/// Maximum number of depth slabs admitted (a rebated opening has 2-3; more
756/// belongs to the exact kernel).
757const MAX_SLABS: usize = 6;
758
759/// Remove collinear run vertices from a ring (the cross-section stitcher emits
760/// one vertex per crossed cutter edge, subdividing straight profile sides).
761/// Beyond bloating the face set, they defeat the profile-edge extension: every
762/// edge's neighbours look collinear, so the corner re-derivation guard skips
763/// it — the #1112 flush-strip roofs never extended.
764fn simplify_collinear(profile: &mut Vec<V2>, tol: f64) {
765    loop {
766        let n = profile.len();
767        if n <= 3 {
768            return;
769        }
770        let mut removed = false;
771        let mut i = 0;
772        while profile.len() > 3 && i < profile.len() {
773            let m = profile.len();
774            let a = profile[(i + m - 1) % m];
775            let b = profile[i];
776            let c = profile[(i + 1) % m];
777            let ab = [b[0] - a[0], b[1] - a[1]];
778            let ac = [c[0] - a[0], c[1] - a[1]];
779            let lac = (ac[0] * ac[0] + ac[1] * ac[1]).sqrt();
780            if lac > 1.0e-12 {
781                let perp = (ab[0] * ac[1] - ab[1] * ac[0]).abs() / lac;
782                // Only drop a vertex whose removal keeps the ring within `tol`
783                // AND that lies BETWEEN its neighbours along the run (a true
784                // subdivision point, not a hairpin spike).
785                let t = (ab[0] * ac[0] + ab[1] * ac[1]) / (lac * lac);
786                if perp <= tol && (0.0..=1.0).contains(&t) {
787                    profile.remove(i);
788                    removed = true;
789                    continue;
790                }
791            }
792            i += 1;
793        }
794        if !removed {
795            return;
796        }
797    }
798}
799
800/// CCW normalization + area of a 2D ring. `None` when degenerate.
801fn ccw_area(profile: &mut Vec<V2>) -> Option<f64> {
802    profile.dedup_by(|a, b| (a[0] - b[0]).abs() < 1.0e-9 && (a[1] - b[1]).abs() < 1.0e-9);
803    while profile.len() > 1 {
804        let first = profile[0];
805        let last = *profile.last().unwrap();
806        if (first[0] - last[0]).abs() < 1.0e-9 && (first[1] - last[1]).abs() < 1.0e-9 {
807            profile.pop();
808        } else {
809            break;
810        }
811    }
812    if profile.len() < 3 {
813        return None;
814    }
815    let mut signed2 = 0.0;
816    for i in 0..profile.len() {
817        let a = profile[i];
818        let b = profile[(i + 1) % profile.len()];
819        signed2 += a[0] * b[1] - a[1] * b[0];
820    }
821    if signed2 < 0.0 {
822        profile.reverse();
823        signed2 = -signed2;
824    }
825    let area = signed2 * 0.5;
826    if area < 1.0e-8 {
827        None
828    } else {
829        Some(area)
830    }
831}
832
833/// Strictly interior point of a CCW ring: the max-area CDT triangle centroid
834/// (a non-convex rebated profile's vertex centroid can lie outside).
835fn ring_interior(profile: &[V2]) -> Option<V2> {
836    let pts: Vec<Point2<f64>> = profile.iter().map(|p| Point2::new(p[0], p[1])).collect();
837    let (tp, ti) = crate::cdt::triangulate_constrained(&pts, &[])?;
838    let mut best = (0.0, [0.0, 0.0]);
839    for t in ti.chunks_exact(3) {
840        let (a, b, c) = (tp[t[0]], tp[t[1]], tp[t[2]]);
841        let ar = ((b.x - a.x) * (c.y - a.y) - (c.x - a.x) * (b.y - a.y)).abs();
842        if ar > best.0 {
843            best = (ar, [(a.x + b.x + c.x) / 3.0, (a.y + b.y + c.y) / 3.0]);
844        }
845    }
846    (best.0 > 0.0).then_some(best.1)
847}
848
849/// Cross-section ring of the welded cutter at depth `w_mid`: chord segments of
850/// every triangle crossing the plane, endpoints computed once per welded EDGE
851/// (bit-identical across the two incident triangles) and stitched into one
852/// loop by exact bit equality. `None` for anything but a single clean loop.
853fn cross_section_ring(
854    verts: &[V3],
855    tris: &[[usize; 3]],
856    d: V3,
857    u: V3,
858    v: V3,
859    w_mid: f64,
860) -> Option<Vec<V2>> {
861    let mut edge_cross: FxHashMap<(usize, usize), V3> = FxHashMap::default();
862    let mut crossing = |a: usize, b: usize| -> V3 {
863        let key = if a < b { (a, b) } else { (b, a) };
864        *edge_cross.entry(key).or_insert_with(|| {
865            let (pa, pb) = (verts[key.0], verts[key.1]);
866            let (da, db) = (dot(pa, d) - w_mid, dot(pb, d) - w_mid);
867            lerp(pa, pb, (da / (da - db)).clamp(0.0, 1.0))
868        })
869    };
870    let mut segs: Vec<(V3, V3)> = Vec::new();
871    for t in tris {
872        let dist = [
873            dot(verts[t[0]], d) - w_mid,
874            dot(verts[t[1]], d) - w_mid,
875            dot(verts[t[2]], d) - w_mid,
876        ];
877        let mut pts: Vec<V3> = Vec::new();
878        for e in 0..3 {
879            let (da, db) = (dist[e], dist[(e + 1) % 3]);
880            if (da > 0.0) != (db > 0.0) {
881                pts.push(crossing(t[e], t[(e + 1) % 3]));
882            }
883        }
884        if pts.len() == 2 {
885            segs.push((pts[0], pts[1]));
886        }
887    }
888    if segs.len() < 3 {
889        return None;
890    }
891    // Stitch by exact endpoint bits (undirected), walking one loop.
892    let mut adj: FxHashMap<[u64; 3], Vec<(usize, [u64; 3])>> = FxHashMap::default();
893    for (i, (a, b)) in segs.iter().enumerate() {
894        adj.entry(bits(*a)).or_default().push((i, bits(*b)));
895        adj.entry(bits(*b)).or_default().push((i, bits(*a)));
896    }
897    if adj.values().any(|v| v.len() != 2) {
898        return None; // branch / open curve — not a clean single section
899    }
900    let start = *adj.keys().min()?;
901    let mut ring_keys: Vec<[u64; 3]> = vec![start];
902    let mut used = vec![false; segs.len()];
903    let mut cur = start;
904    loop {
905        let nbrs = adj.get(&cur)?;
906        let Some(&(si, nxt)) = nbrs.iter().find(|(si, _)| !used[*si]) else {
907            break;
908        };
909        used[si] = true;
910        if nxt == start {
911            break;
912        }
913        ring_keys.push(nxt);
914        if ring_keys.len() > segs.len() {
915            return None;
916        }
917        cur = nxt;
918    }
919    if used.iter().any(|&x| !x) {
920        return None; // more than one loop (annular / multi-void section)
921    }
922    let to_v3 = |k: [u64; 3]| -> V3 {
923        [
924            f64::from_bits(k[0]),
925            f64::from_bits(k[1]),
926            f64::from_bits(k[2]),
927        ]
928    };
929    Some(
930        ring_keys
931            .iter()
932            .map(|&k| {
933                let p = to_v3(k);
934                [dot(p, u), dot(p, v)]
935            })
936            .collect(),
937    )
938}
939
940/// Detect that the (welded) cutter is a stepped extrusion and extract its
941/// stack. `verts`/`tris` are the welded cutter in host-local coordinates;
942/// `mesh_vol` is the enclosed volume of the (unwelded) cutter. Conservative:
943/// any doubt returns `None` → the exact kernel keeps the opening.
944fn detect_prism(verts: &[V3], tris: &[[usize; 3]], mesh_vol: f64) -> Option<PrismFrame> {
945    if tris.len() < 8 || tris.len() > 4096 {
946        return None;
947    }
948    // Facet normals; cluster into candidate depth directions.
949    let mut normals: Vec<Option<V3>> = Vec::with_capacity(tris.len());
950    for t in tris {
951        let (a, b, c) = (verts[t[0]], verts[t[1]], verts[t[2]]);
952        normals.push(normalize(cross(sub(b, a), sub(c, a))));
953    }
954    let mut clusters: Vec<V3> = Vec::new();
955    for n in normals.iter().flatten() {
956        if !clusters.iter().any(|c| dot(*n, *c).abs() > 0.999) {
957            if clusters.len() > 64 {
958                return None; // way past any stepped extrusion — bail early
959            }
960            clusters.push(*n);
961        }
962    }
963    let mut mag = 0.0f64;
964    for v in verts {
965        for c in v {
966            mag = mag.max(c.abs());
967        }
968    }
969    let plane_tol = 1.0e-4_f64.max(mag * 1.0e-6);
970    // Try each cluster direction as the depth axis: every facet must be
971    // parallel (a cap/step, on one of a few depth planes) or perpendicular
972    // (a side) to it.
973    'cand: for dc in &clusters {
974        let d = *dc;
975        let mut wlo = f64::INFINITY;
976        let mut whi = f64::NEG_INFINITY;
977        for v in verts {
978            let w = dot(*v, d);
979            wlo = wlo.min(w);
980            whi = whi.max(w);
981        }
982        if whi - wlo < 1.0e-4 {
983            continue;
984        }
985        // Collect the depth planes carried by parallel facets.
986        let mut plane_ws: Vec<f64> = vec![wlo, whi];
987        for (t, n) in tris.iter().zip(&normals) {
988            let Some(n) = n else { continue };
989            let a = dot(*n, d).abs();
990            if a > 0.9999 {
991                let w = dot(verts[t[0]], d);
992                // The whole facet must be planar in depth.
993                if t
994                    .iter()
995                    .any(|&i| (dot(verts[i], d) - w).abs() > plane_tol)
996                {
997                    continue 'cand;
998                }
999                if !plane_ws.iter().any(|p| (p - w).abs() <= plane_tol) {
1000                    plane_ws.push(w);
1001                }
1002            } else if a > 0.002 {
1003                // Neither exactly parallel nor exactly perpendicular ⇒ the
1004                // cross-section varies along the depth ⇒ not a stepped
1005                // extrusion about this axis. The tight band matters: a ~1°
1006                // slanted side would make the mid-slab section drift ~0.5 mm
1007                // across the slab, poisoning the analytic cut at the weld
1008                // grid.
1009                continue 'cand;
1010            }
1011        }
1012        plane_ws.sort_by(f64::total_cmp);
1013        if plane_ws.len() > MAX_SLABS + 1 {
1014            continue;
1015        }
1016        let (u, v) = basis_from_depth(d)?;
1017        // One cross-section ring per slab, sliced at the slab midpoint.
1018        let n_slabs = plane_ws.len() - 1;
1019        let mut raw_rings: Vec<Vec<V2>> = Vec::with_capacity(n_slabs);
1020        for k in 0..n_slabs {
1021            let (w_a, w_b) = (plane_ws[k], plane_ws[k + 1]);
1022            if w_b - w_a < plane_tol * 2.0 {
1023                continue 'cand; // sliver slab — numerically fragile, defer
1024            }
1025            let w_mid = (w_a + w_b) * 0.5;
1026            let Some(ring) = cross_section_ring(verts, tris, d, u, v, w_mid) else {
1027                continue 'cand;
1028            };
1029            if ring.len() > MAX_PROFILE_VERTS {
1030                continue 'cand;
1031            }
1032            raw_rings.push(ring);
1033        }
1034        if raw_rings.is_empty() {
1035            continue;
1036        }
1037        // SNAP profile coordinates across slabs: the per-slab cross-sections
1038        // carry independent f32 storage jitter, so a jamb side SHARED by two
1039        // slabs (no rebate step there) comes back as two near-coincident but
1040        // unequal edges — µm-wide XOR slivers that wreck the cap CDT and the
1041        // classification. Cluster all u values (and all v values) across the
1042        // rings within the f32-jitter bound and remap every ring onto the
1043        // canonical (first-seen) cluster value. Distinct REAL features (≥ mm)
1044        // stay distinct; only jitter twins collapse.
1045        let snap_tol = 3.0e-5_f64.max(mag * 6.0e-7);
1046        for axis in 0..2 {
1047            let mut vals: Vec<f64> = raw_rings
1048                .iter()
1049                .flat_map(|r| r.iter().map(|p| p[axis]))
1050                .collect();
1051            vals.sort_by(f64::total_cmp);
1052            let mut canon: Vec<f64> = Vec::new();
1053            for x in vals {
1054                match canon.last() {
1055                    Some(&last) if (x - last).abs() <= snap_tol => {}
1056                    _ => canon.push(x),
1057                }
1058            }
1059            for ring in raw_rings.iter_mut() {
1060                for pnt in ring.iter_mut() {
1061                    // Nearest canonical value (canon sorted; linear scan is
1062                    // fine at these sizes, deterministic).
1063                    let mut best = canon[0];
1064                    for &cv in &canon {
1065                        if (pnt[axis] - cv).abs() < (pnt[axis] - best).abs() {
1066                            best = cv;
1067                        }
1068                    }
1069                    pnt[axis] = best;
1070                }
1071            }
1072        }
1073        let mut profiles: Vec<Vec<V2>> = Vec::with_capacity(n_slabs);
1074        let mut slab_area: Vec<f64> = Vec::with_capacity(n_slabs);
1075        let mut slab_interior: Vec<V2> = Vec::with_capacity(n_slabs);
1076        let mut vol = 0.0;
1077        for (k, mut ring) in raw_rings.into_iter().enumerate() {
1078            let (w_a, w_b) = (plane_ws[k], plane_ws[k + 1]);
1079            simplify_collinear(&mut ring, 1.0e-6 * (1.0 + mag));
1080            let Some(area) = ccw_area(&mut ring) else {
1081                continue 'cand;
1082            };
1083            // SLIVER-EDGE gate: a sub-half-millimetre profile edge sits at the
1084            // 0.1 mm weld/audit grid where its cut features collapse to
1085            // degenerate triangles and crack the closed-surface audit. Such
1086            // micro-features belong to the exact kernel.
1087            let nr = ring.len();
1088            for ii in 0..nr {
1089                let pa = ring[ii];
1090                let pb = ring[(ii + 1) % nr];
1091                let dx = pb[0] - pa[0];
1092                let dy = pb[1] - pa[1];
1093                if (dx * dx + dy * dy).sqrt() < 5.0e-4 {
1094                    continue 'cand;
1095                }
1096            }
1097            let Some(interior) = ring_interior(&ring) else {
1098                continue 'cand;
1099            };
1100            vol += area * (w_b - w_a);
1101            profiles.push(ring);
1102            slab_area.push(area);
1103            slab_interior.push(interior);
1104        }
1105        if profiles.is_empty() {
1106            continue;
1107        }
1108        // Volume reconcile: enclosed mesh volume ≈ Σ area × slab depth
1109        // (catches a section that missed part of the shape, hollow shells,
1110        // twisted sides, etc.).
1111        if !(0.98..1.02).contains(&(mesh_vol.abs() / vol)) {
1112            continue;
1113        }
1114        let mut lo = [f64::INFINITY; 2];
1115        let mut hi = [f64::NEG_INFINITY; 2];
1116        for prof in &profiles {
1117            for p in prof {
1118                for k in 0..2 {
1119                    lo[k] = lo[k].min(p[k]);
1120                    hi[k] = hi[k].max(p[k]);
1121                }
1122            }
1123        }
1124        return Some(PrismFrame {
1125            u,
1126            v,
1127            d,
1128            planes: plane_ws,
1129            profiles,
1130            slab_area,
1131            slab_interior,
1132            bb: (lo, hi),
1133        });
1134    }
1135    None
1136}
1137
1138/// Build the analytic prism for one classified opening, expressed in the
1139/// host's local frame (`origin` subtracted in f64). `None` ⇒ the opening is
1140/// not a clean prism and stays with the exact kernel.
1141fn prepare_prism(op: &OpeningType, origin: [f64; 3]) -> Option<PrismFrame> {
1142    match op {
1143        OpeningType::Rectangular(mn, mx, dir) => {
1144            let lo = [mn.x - origin[0], mn.y - origin[1], mn.z - origin[2]];
1145            let hi = [mx.x - origin[0], mx.y - origin[1], mx.z - origin[2]];
1146            if (0..3).any(|k| hi[k] - lo[k] < 1.0e-4 || !lo[k].is_finite() || !hi[k].is_finite())
1147            {
1148                return None;
1149            }
1150            // Depth axis: the authored (axis-aligned) extrusion dir, else the
1151            // thinnest extent (the classic wall-thickness cutter).
1152            let k = match dir {
1153                Some(d) => {
1154                    let a = [d.x.abs(), d.y.abs(), d.z.abs()];
1155                    let mut k = 0;
1156                    for i in 1..3 {
1157                        if a[i] > a[k] {
1158                            k = i;
1159                        }
1160                    }
1161                    k
1162                }
1163                None => {
1164                    let ext = [hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]];
1165                    let mut k = 0;
1166                    for i in 1..3 {
1167                        if ext[i] < ext[k] {
1168                            k = i;
1169                        }
1170                    }
1171                    k
1172                }
1173            };
1174            let mut d = [0.0; 3];
1175            d[k] = 1.0;
1176            let (u, v) = basis_from_depth(d)?;
1177            // Rectangle profile from the AABB corners projected to (u, v).
1178            let mut plo = [f64::INFINITY; 2];
1179            let mut phi = [f64::NEG_INFINITY; 2];
1180            for ci in 0..8 {
1181                let c = [
1182                    if ci & 1 == 0 { lo[0] } else { hi[0] },
1183                    if ci & 2 == 0 { lo[1] } else { hi[1] },
1184                    if ci & 4 == 0 { lo[2] } else { hi[2] },
1185                ];
1186                let uu = dot(c, u);
1187                let vv = dot(c, v);
1188                plo[0] = plo[0].min(uu);
1189                plo[1] = plo[1].min(vv);
1190                phi[0] = phi[0].max(uu);
1191                phi[1] = phi[1].max(vv);
1192            }
1193            let profile = vec![
1194                [plo[0], plo[1]],
1195                [phi[0], plo[1]],
1196                [phi[0], phi[1]],
1197                [plo[0], phi[1]],
1198            ];
1199            let area = (phi[0] - plo[0]) * (phi[1] - plo[1]);
1200            Some(PrismFrame {
1201                u,
1202                v,
1203                d,
1204                planes: vec![lo[k], hi[k]],
1205                profiles: vec![profile],
1206                slab_area: vec![area],
1207                slab_interior: vec![[(plo[0] + phi[0]) * 0.5, (plo[1] + phi[1]) * 0.5]],
1208                bb: (plo, phi),
1209            })
1210        }
1211        OpeningType::DiagonalRectangular(m, _) | OpeningType::NonRectangular(m, _, _, _) => {
1212            // A genuine prism must weld to a closed 2-manifold; the
1213            // self-intersecting "fin" cutters (#1007) and open shells fail
1214            // here and keep their exact-kernel (+ malformed-recut) treatment.
1215            // An opening authored as glued extrusions carries a back-to-back
1216            // cap membrane mid-cutter that breaks manifoldness — deseam it
1217            // first (the same repair every exact void path funnels through).
1218            let deseamed;
1219            let m = if cutter_is_closed_manifold(m) {
1220                m
1221            } else {
1222                let dir = match op {
1223                    OpeningType::DiagonalRectangular(_, frame) => frame.depth,
1224                    OpeningType::NonRectangular(_, _, _, Some(d)) => *d,
1225                    _ => opening_mesh_thinnest_axis_dir(m),
1226                };
1227                deseamed = GeometryRouter::remove_internal_membrane(m, dir);
1228                if !cutter_is_closed_manifold(&deseamed) {
1229                    return None;
1230                }
1231                &deseamed
1232            };
1233            // Host-local f64 verts (cutter origin folded, host origin removed).
1234            let o = m.origin;
1235            let vc = m.positions.len() / 3;
1236            let mut raw: Vec<V3> = Vec::with_capacity(vc);
1237            for c in m.positions.chunks_exact(3) {
1238                let p = [
1239                    c[0] as f64 + o[0] - origin[0],
1240                    c[1] as f64 + o[1] - origin[1],
1241                    c[2] as f64 + o[2] - origin[2],
1242                ];
1243                if p.iter().any(|x| !x.is_finite()) {
1244                    return None;
1245                }
1246                raw.push(p);
1247            }
1248            // Enclosed volume from the raw (per-face-vertex) soup.
1249            let mut vol6 = 0.0;
1250            for t in m.indices.chunks_exact(3) {
1251                if t.iter().any(|&i| i as usize >= vc) {
1252                    return None;
1253                }
1254                let (a, b, c) = (
1255                    raw[t[0] as usize],
1256                    raw[t[1] as usize],
1257                    raw[t[2] as usize],
1258                );
1259                vol6 += dot(a, cross(b, c));
1260            }
1261            // Weld by position for exact edge stitching of the cap boundary.
1262            let mut wverts: Vec<V3> = Vec::new();
1263            let mut wmap: FxHashMap<(i64, i64, i64), usize> = FxHashMap::default();
1264            let q = |x: f64| (x / 1.0e-6).round() as i64;
1265            let mut idx_of = vec![0usize; raw.len()];
1266            for (i, p) in raw.iter().enumerate() {
1267                let key = (q(p[0]), q(p[1]), q(p[2]));
1268                let id = *wmap.entry(key).or_insert_with(|| {
1269                    wverts.push(*p);
1270                    wverts.len() - 1
1271                });
1272                idx_of[i] = id;
1273            }
1274            let mut wtris: Vec<[usize; 3]> = Vec::with_capacity(m.indices.len() / 3);
1275            for t in m.indices.chunks_exact(3) {
1276                let a = idx_of[t[0] as usize];
1277                let b = idx_of[t[1] as usize];
1278                let c = idx_of[t[2] as usize];
1279                if a != b && b != c && c != a {
1280                    wtris.push([a, b, c]);
1281                }
1282            }
1283            detect_prism(&wverts, &wtris, vol6 / 6.0)
1284        }
1285    }
1286}
1287
1288/// Cyclic CCW-ring equality within `tol`.
1289fn rings_equal(a: &[V2], b: &[V2], tol: f64) -> bool {
1290    let n = a.len();
1291    if b.len() != n {
1292        return false;
1293    }
1294    'off: for o in 0..n {
1295        for i in 0..n {
1296            let pa = a[i];
1297            let pb = b[(i + o) % n];
1298            if (pa[0] - pb[0]).abs() > tol || (pa[1] - pb[1]).abs() > tol {
1299                continue 'off;
1300            }
1301        }
1302        return true;
1303    }
1304    false
1305}
1306
1307/// Merge two stacks whose union is exactly one stack: identical depth axis
1308/// (hence identical deterministic basis) and NUMERICALLY-COINCIDENT depth
1309/// ranges (touching within `weld` plane jitter, NOT the geometric `tol`); the
1310/// stacks concatenate at the touching plane, and equal adjacent profiles fuse
1311/// into one slab (`tol` gates only that profile-equality fusion). This is the
1312/// wall-leaf half-void pattern (FZK / masonry leaf walls): one window void
1313/// split into abutting halves along the wall thickness — cutting the union in
1314/// one pass sidesteps their coplanar interface entirely.
1315fn try_merge_prisms(a: &PrismFrame, b: &PrismFrame, tol: f64) -> Option<PrismFrame> {
1316    // The merged stack concatenates `hi`'s (u, v)-expressed profiles onto `lo`'s
1317    // planes/profiles verbatim, so the two frames must share the SAME basis:
1318    // not just a coincident depth axis but coincident in-plane axes too. A
1319    // frame rotated about `d` (or with u/v swapped) expresses identical
1320    // geometry in incompatible 2D coordinates — stitching them silently
1321    // mis-cuts, and the volume self-check (computed in the merged basis) cannot
1322    // see it. Require all three basis vectors to align within a tight tol.
1323    const BASIS_TOL: f64 = 1.0 - 1.0e-8;
1324    if dot(a.d, b.d) < BASIS_TOL || dot(a.u, b.u) < BASIS_TOL || dot(a.v, b.v) < BASIS_TOL {
1325        return None; // differing basis/frame ⇒ leave to the veto / exact path
1326    }
1327    // Concatenate ONLY at a NUMERICALLY-COINCIDENT interface: depth ranges must
1328    // touch within PLANE JITTER (f32 + snap-grid roundoff), never the geometric
1329    // `tol`. A real gap back-fills cutter volume and a real overlap reassigns a
1330    // differing-profile slab — both keep the synthesized prism self-consistent so
1331    // the self-check misses the over/under-cut. Non-touching / overlapping pairs
1332    // stay unmerged for the overlap veto / exact path. 8·SNAP_GRID (~122 µm) is
1333    // the kernel's per-axis-snap scatter: ≫ f32 roundoff, ≪ any authored gap.
1334    let weld = 8.0 * crate::kernel::mesh_bridge::SNAP_GRID;
1335    let (lo, hi) = if (a.d1() - b.d0()).abs() <= weld {
1336        (a, b)
1337    } else if (b.d1() - a.d0()).abs() <= weld {
1338        (b, a)
1339    } else {
1340        return None;
1341    };
1342    if lo.profiles.len() + hi.profiles.len() > MAX_SLABS {
1343        return None;
1344    }
1345    let mut m = lo.clone();
1346    // Stitch the interface: hi's first plane is re-anchored on lo's last.
1347    m.planes.extend(hi.planes[1..].iter().copied());
1348    m.profiles.extend(hi.profiles.iter().cloned());
1349    m.slab_area.extend(hi.slab_area.iter().copied());
1350    m.slab_interior.extend(hi.slab_interior.iter().copied());
1351    // Fuse the two slabs meeting at the interface when their profiles match
1352    // (the plain leaf-half case collapses back to a single slab).
1353    let k = lo.profiles.len(); // index of hi's first slab in the merged stack
1354    if k > 0 && k < m.profiles.len() && rings_equal(&m.profiles[k - 1], &m.profiles[k], tol) {
1355        m.planes.remove(k);
1356        m.profiles.remove(k);
1357        m.slab_area.remove(k);
1358        m.slab_interior.remove(k);
1359    }
1360    m.bb = {
1361        let mut lo2 = [f64::INFINITY; 2];
1362        let mut hi2 = [f64::NEG_INFINITY; 2];
1363        for prof in &m.profiles {
1364            for p in prof {
1365                for k in 0..2 {
1366                    lo2[k] = lo2[k].min(p[k]);
1367                    hi2[k] = hi2[k].max(p[k]);
1368                }
1369            }
1370        }
1371        (lo2, hi2)
1372    };
1373    Some(m)
1374}
1375
1376/// World-axis AABB of an opening that did NOT yield a prism, as an
1377/// identity-frame OBB in the host's local frame — an overlap "blocker": a
1378/// candidate prism abutting an exact-kernel opening must not be cut
1379/// analytically (the residual exact cut would land flush on its caps).
1380fn opening_aabb_obb(op: &OpeningType, origin: [f64; 3]) -> Option<([V3; 3], V3, V3)> {
1381    let (mn, mx): (V3, V3) = match op {
1382        OpeningType::Rectangular(mn, mx, _) | OpeningType::NonRectangular(_, mn, mx, _) => {
1383            ([mn.x, mn.y, mn.z], [mx.x, mx.y, mx.z])
1384        }
1385        OpeningType::DiagonalRectangular(m, _) => {
1386            let (a, b) = m.bounds();
1387            let o = m.origin;
1388            (
1389                [a.x as f64 + o[0], a.y as f64 + o[1], a.z as f64 + o[2]],
1390                [b.x as f64 + o[0], b.y as f64 + o[1], b.z as f64 + o[2]],
1391            )
1392        }
1393    };
1394    let lo = [mn[0] - origin[0], mn[1] - origin[1], mn[2] - origin[2]];
1395    let hi = [mx[0] - origin[0], mx[1] - origin[1], mx[2] - origin[2]];
1396    if (0..3).any(|k| !(hi[k] > lo[k]) || !lo[k].is_finite() || !hi[k].is_finite()) {
1397        return None;
1398    }
1399    Some((
1400        [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
1401        lo,
1402        hi,
1403    ))
1404}
1405
1406// ─────────────────────────── ray parity (point in solid) ─────────────────────
1407
1408/// Fixed skewed ray direction (same constants as `geom::point_inside_mesh`) —
1409/// avoids exact grazes on the axis-dominated faces of IFC geometry.
1410const RAY_DIR: V3 = [0.573_257_1, 0.665_412_3, 0.477_889_5];
1411
1412/// Möller–Trumbore crossing parity of `point` against the triangle list (with
1413/// precomputed AABBs). Odd ⇒ inside the solid.
1414fn point_inside(tris: &[PTri], aabbs: &[(V3, V3)], point: V3) -> bool {
1415    let mut crossings = 0usize;
1416    for (t, (_lo, hi)) in tris.iter().zip(aabbs) {
1417        // Cheap reject: every component of `RAY_DIR` is positive, so a hit
1418        // point has all coordinates strictly greater than the origin's — a
1419        // triangle entirely below the origin on ANY axis cannot be crossed.
1420        if hi[0] < point[0] || hi[1] < point[1] || hi[2] < point[2] {
1421            continue;
1422        }
1423        let e1 = sub(t.p[1], t.p[0]);
1424        let e2 = sub(t.p[2], t.p[0]);
1425        let pvec = cross(RAY_DIR, e2);
1426        let det = dot(e1, pvec);
1427        if det.abs() < 1.0e-12 {
1428            continue;
1429        }
1430        let inv = 1.0 / det;
1431        let tvec = sub(point, t.p[0]);
1432        let u = dot(tvec, pvec) * inv;
1433        if !(-1.0e-9..=1.0 + 1.0e-9).contains(&u) {
1434            continue;
1435        }
1436        let qvec = cross(tvec, e1);
1437        let v = dot(RAY_DIR, qvec) * inv;
1438        if v < -1.0e-9 || u + v > 1.0 + 1.0e-9 {
1439            continue;
1440        }
1441        let dist = dot(e2, qvec) * inv;
1442        if dist > 1.0e-9 {
1443            crossings += 1;
1444        }
1445    }
1446    crossings % 2 == 1
1447}
1448
1449// ────────────────────────────── cap extension ───────────────────────────────
1450
1451/// Push each prism CAP outward past the host surface when the swept slab is
1452/// PROVABLY empty of host material: no host triangle intersects (profile
1453/// footprint × slab) AND a probe point inside the slab lies outside the host
1454/// solid. A flush window cap then becomes a clean transversal crossing, while
1455/// a genuine blind-recess bottom (host material beyond the cap) keeps its
1456/// authored plane.
1457fn extend_prism_caps(pf: &mut PrismFrame, tris: &[PTri], aabbs: &[(V3, V3)]) {
1458    let span = pf.d1() - pf.d0();
1459    let delta = (span * 0.1).clamp(0.005, 0.5);
1460    let mag = pf.corner_mag();
1461    // Flush-exclusion band: a host FACE lying (near-)flush ON the cap plane —
1462    // the flush window/sleeve case, or a cutter authored a hair SHORT of the
1463    // surface (the #1112 roof skylights) — is not material BEYOND the plane
1464    // and must not block the extension; only geometry clearly past the band
1465    // does. Matches the exact kernel's flush-cap detection band
1466    // (`extend_opening_mesh_through_host`: `open_span.max(1.0) * 1e-3`), so
1467    // the analytic path pushes through exactly where the exact path would.
1468    let band = (span.max(1.0) * 1.0e-3).max(mag * 4.0e-7);
1469    let pad = 1.0e-6 * (1.0 + mag);
1470    for hi_side in [false, true] {
1471        let (slab_lo, slab_hi) = if hi_side {
1472            (pf.d1() + band, pf.d1() + delta)
1473        } else {
1474            (pf.d0() - delta, pf.d0() - band)
1475        };
1476        if slab_hi <= slab_lo {
1477            continue;
1478        }
1479        let (k, interior) = if hi_side {
1480            (pf.profiles.len() - 1, pf.slab_interior[pf.profiles.len() - 1])
1481        } else {
1482            (0, pf.slab_interior[0])
1483        };
1484        let profile = &pf.profiles[k];
1485        let occupied = tris
1486            .iter()
1487            .any(|t| tri_touches_slab_footprint(t, pf, profile, slab_lo, slab_hi, pad));
1488        if occupied {
1489            continue;
1490        }
1491        let probe = pf.lift(interior[0], interior[1], (slab_lo + slab_hi) * 0.5);
1492        if point_inside(tris, aabbs, probe) {
1493            continue;
1494        }
1495        let last = pf.planes.len() - 1;
1496        if hi_side {
1497            pf.planes[last] += delta;
1498        } else {
1499            pf.planes[0] -= delta;
1500        }
1501    }
1502}
1503
1504/// Push a profile EDGE outward past the host surface when the swept band
1505/// beyond it is provably empty of host material — the side-face analog of the
1506/// cap extension. The canonical case: a DOOR whose bottom profile edge is
1507/// flush with the wall's bottom face; without extension that strip is
1508/// coplanar with host skin (parity garbage), with it the threshold band falls
1509/// cleanly inside the cutter. Corners are re-derived by intersecting the
1510/// translated edge line with the (unchanged) neighbour edge lines.
1511/// Point-in-or-on-polygon: inside by ray parity OR within `eps` of any edge.
1512/// The boundary tolerance matters for the extension containment check: a
1513/// re-derived corner is COLLINEAR with the authored corner it replaces (both
1514/// lie on the shared neighbour-edge line), so the authored corner sits exactly
1515/// on `cand`'s boundary — a strict interior test would wrongly reject it.
1516fn point_in_or_on(p: V2, poly: &[V2], eps: f64) -> bool {
1517    let n = poly.len();
1518    let eps2 = eps * eps;
1519    for i in 0..n {
1520        let a = poly[i];
1521        let b = poly[(i + 1) % n];
1522        let ab = [b[0] - a[0], b[1] - a[1]];
1523        let ap = [p[0] - a[0], p[1] - a[1]];
1524        let len2 = ab[0] * ab[0] + ab[1] * ab[1];
1525        if len2 < 1.0e-20 {
1526            if ap[0] * ap[0] + ap[1] * ap[1] < eps2 {
1527                return true;
1528            }
1529            continue;
1530        }
1531        let t = ((ap[0] * ab[0] + ap[1] * ab[1]) / len2).clamp(0.0, 1.0);
1532        let proj = [a[0] + ab[0] * t, a[1] + ab[1] * t];
1533        let dx = p[0] - proj[0];
1534        let dy = p[1] - proj[1];
1535        if dx * dx + dy * dy < eps2 {
1536            return true;
1537        }
1538    }
1539    pip(p, poly)
1540}
1541
1542/// Whether `outer` contains every vertex of `inner` (boundary-inclusive).
1543fn ring_contains_ring(outer: &[V2], inner: &[V2], eps: f64) -> bool {
1544    inner.iter().all(|&p| point_in_or_on(p, outer, eps))
1545}
1546
1547/// Whether a 2D ring is a SIMPLE polygon: no pair of non-adjacent edges
1548/// crosses. Adjacent edges (sharing a vertex) are exempt. Used to reject a
1549/// profile-edge extension that folded a non-convex ring back on itself.
1550fn ring_is_simple(ring: &[V2]) -> bool {
1551    let n = ring.len();
1552    if n < 3 {
1553        return false;
1554    }
1555    for i in 0..n {
1556        let (a, b) = (ring[i], ring[(i + 1) % n]);
1557        for j in (i + 1)..n {
1558            // Skip edges adjacent to edge i (they legitimately share a vertex).
1559            if j == (i + 1) % n || (j + 1) % n == i {
1560                continue;
1561            }
1562            let (c, d) = (ring[j], ring[(j + 1) % n]);
1563            if seg_cross_param(a, b, c, d).is_some() {
1564                return false;
1565            }
1566        }
1567    }
1568    true
1569}
1570
1571fn extend_profile_edges(pf: &mut PrismFrame, tris: &[PTri], aabbs: &[(V3, V3)]) {
1572    let mag = pf.corner_mag();
1573    let band = 2.0e-4_f64.max(mag * 4.0e-7);
1574    for k in 0..pf.profiles.len() {
1575        let (w_a, w_b) = (pf.planes[k], pf.planes[k + 1]);
1576        // Authored ring for this slab: every extension must keep containing it
1577        // (a moved edge on a non-convex ring can otherwise shrink the profile).
1578        let authored = pf.profiles[k].clone();
1579        let mut i = 0usize;
1580        while i < pf.profiles[k].len() {
1581            let prof = pf.profiles[k].clone();
1582            let n = prof.len();
1583            let a = prof[i];
1584            let b = prof[(i + 1) % n];
1585            let e = [b[0] - a[0], b[1] - a[1]];
1586            let len = (e[0] * e[0] + e[1] * e[1]).sqrt();
1587            if len < 1.0e-6 {
1588                i += 1;
1589                continue;
1590            }
1591            let ed = [e[0] / len, e[1] / len];
1592            let n2 = [ed[1], -ed[0]]; // outward for a CCW ring
1593            let delta = (len * 0.1).clamp(0.01, 0.5);
1594            // Neighbour edge lines must be genuinely transversal to move the
1595            // corners; skip near-collinear neighbours.
1596            let ap = prof[(i + n - 1) % n];
1597            let bn = prof[(i + 2) % n];
1598            let d_prev = [a[0] - ap[0], a[1] - ap[1]];
1599            let d_next = [bn[0] - b[0], bn[1] - b[1]];
1600            let cr_prev = ed[0] * d_prev[1] - ed[1] * d_prev[0];
1601            let cr_next = ed[0] * d_next[1] - ed[1] * d_next[0];
1602            let lp = (d_prev[0] * d_prev[0] + d_prev[1] * d_prev[1]).sqrt();
1603            let ln = (d_next[0] * d_next[0] + d_next[1] * d_next[1]).sqrt();
1604            if lp < 1.0e-9 || ln < 1.0e-9 || cr_prev.abs() / lp < 0.1 || cr_next.abs() / ln < 0.1
1605            {
1606                i += 1;
1607                continue;
1608            }
1609            // Emptiness of the swept band (band..delta beyond the edge, this
1610            // slab's depth interval), then an outside-host probe.
1611            let quad = [
1612                [a[0] + n2[0] * band, a[1] + n2[1] * band],
1613                [b[0] + n2[0] * band, b[1] + n2[1] * band],
1614                [b[0] + n2[0] * delta, b[1] + n2[1] * delta],
1615                [a[0] + n2[0] * delta, a[1] + n2[1] * delta],
1616            ];
1617            let pad = 1.0e-6 * (1.0 + mag);
1618            let occupied = tris
1619                .iter()
1620                .any(|t| tri_touches_slab_footprint(t, pf, &quad, w_a, w_b, pad));
1621            if occupied {
1622                i += 1;
1623                continue;
1624            }
1625            let mid_off = (band + delta) * 0.5;
1626            let probe2 = [
1627                (a[0] + b[0]) * 0.5 + n2[0] * mid_off,
1628                (a[1] + b[1]) * 0.5 + n2[1] * mid_off,
1629            ];
1630            let probe = pf.lift(probe2[0], probe2[1], (w_a + w_b) * 0.5);
1631            if point_inside(tris, aabbs, probe) {
1632                i += 1;
1633                continue;
1634            }
1635            // Translate the edge line by delta and re-derive its two corners
1636            // as intersections with the neighbour edge lines.
1637            let line_pt = [a[0] + n2[0] * delta, a[1] + n2[1] * delta];
1638            let isect = |p0: V2, dir0: V2| -> Option<V2> {
1639                // line_pt + t·ed  ==  p0 + s·dir0
1640                let den = ed[0] * dir0[1] - ed[1] * dir0[0];
1641                if den.abs() < 1.0e-12 {
1642                    return None;
1643                }
1644                let dx = [p0[0] - line_pt[0], p0[1] - line_pt[1]];
1645                let t = (dx[0] * dir0[1] - dx[1] * dir0[0]) / den;
1646                Some([line_pt[0] + ed[0] * t, line_pt[1] + ed[1] * t])
1647            };
1648            let (Some(na), Some(nb)) = (
1649                isect(ap, [d_prev[0] / lp, d_prev[1] / lp]),
1650                isect(b, [d_next[0] / ln, d_next[1] / ln]),
1651            ) else {
1652                i += 1;
1653                continue;
1654            };
1655            // Commit the extension only if the resulting ring stays SIMPLE and
1656            // still contains the authored profile. `ccw_area` accepts a
1657            // self-intersecting ring (positive signed area), so the downstream
1658            // volume self-check cannot catch an over/under-cut from a folded
1659            // non-convex ring — validate here and keep the un-extended edge
1660            // otherwise.
1661            let mut cand = pf.profiles[k].clone();
1662            cand[i] = na;
1663            cand[(i + 1) % n] = nb;
1664            // Boundary-tolerant containment: the authored corners are collinear
1665            // with (and on the boundary of) `cand`, so a strict interior test
1666            // is unusable — `contain_eps` (≫ the collinearity float error, ≪
1667            // any real shrink) accepts a genuine outward growth and rejects a
1668            // reflex corner that pulled the profile inward.
1669            let contain_eps = 1.0e-6 * (1.0 + mag);
1670            if ring_is_simple(&cand) && ring_contains_ring(&cand, &authored, contain_eps) {
1671                pf.profiles[k] = cand;
1672            }
1673            i += 1;
1674        }
1675        // Refresh the slab area (the interior point stays valid — the profile
1676        // only grew).
1677        let mut ring = pf.profiles[k].clone();
1678        if let Some(area) = ccw_area(&mut ring) {
1679            pf.profiles[k] = ring;
1680            pf.slab_area[k] = area;
1681        }
1682    }
1683    // Refresh the overall bbox.
1684    let mut lo = [f64::INFINITY; 2];
1685    let mut hi = [f64::NEG_INFINITY; 2];
1686    for prof in &pf.profiles {
1687        for p in prof {
1688            for k in 0..2 {
1689                lo[k] = lo[k].min(p[k]);
1690                hi[k] = hi[k].max(p[k]);
1691            }
1692        }
1693    }
1694    pf.bb = (lo, hi);
1695}
1696
1697/// Whether triangle `t` has a non-degenerate piece inside the region
1698/// `{depth ∈ [slab_lo, slab_hi]} ∩ {(u,v) within the (pad-inflated) profile}`.
1699/// Conservative in the safe direction: any doubt reports "occupied" (⇒ no cap
1700/// extension ⇒ at worst a later self-check fallback, never an over-cut).
1701fn tri_touches_slab_footprint(
1702    t: &PTri,
1703    pf: &PrismFrame,
1704    profile: &[V2],
1705    slab_lo: f64,
1706    slab_hi: f64,
1707    pad: f64,
1708) -> bool {
1709    // Clip the triangle to the depth slab (two parallel planes) — convex S-H.
1710    let mut poly: Vec<V3> = t.p.to_vec();
1711    for hi_side in [false, true] {
1712        if poly.len() < 3 {
1713            return false;
1714        }
1715        let mut next: Vec<V3> = Vec::with_capacity(poly.len() + 1);
1716        let dist = |p: V3| -> f64 {
1717            let w = dot(p, pf.d);
1718            if hi_side {
1719                w - slab_hi
1720            } else {
1721                slab_lo - w
1722            }
1723        };
1724        for i in 0..poly.len() {
1725            let j = (i + 1) % poly.len();
1726            let (da, db) = (dist(poly[i]), dist(poly[j]));
1727            if da <= 0.0 {
1728                next.push(poly[i]);
1729            }
1730            if (da <= 0.0) != (db <= 0.0) {
1731                next.push(lerp(poly[i], poly[j], da / (da - db)));
1732            }
1733        }
1734        poly = next;
1735    }
1736    if poly.len() < 3 {
1737        return false;
1738    }
1739    // 2D overlap of the clipped (convex) polygon with the profile: vertex of
1740    // one inside the other, or any edge pair crossing.
1741    let poly2: Vec<V2> = poly.iter().map(|p| pf.to2(*p)).collect();
1742    // bbox prefilter with pad — against the SUPPLIED `profile` footprint, NOT
1743    // `pf.bb`. The profile-edge extension passes an outward band that can lie
1744    // entirely OUTSIDE the prism's overall bbox; prefiltering on pf.bb would
1745    // wrongly report "empty" and let the extension sweep through nearby host
1746    // material.
1747    let mut lo = [f64::INFINITY; 2];
1748    let mut hi = [f64::NEG_INFINITY; 2];
1749    for p in &poly2 {
1750        for k in 0..2 {
1751            lo[k] = lo[k].min(p[k]);
1752            hi[k] = hi[k].max(p[k]);
1753        }
1754    }
1755    let mut footprint_lo = [f64::INFINITY; 2];
1756    let mut footprint_hi = [f64::NEG_INFINITY; 2];
1757    for p in profile {
1758        for k in 0..2 {
1759            footprint_lo[k] = footprint_lo[k].min(p[k]);
1760            footprint_hi[k] = footprint_hi[k].max(p[k]);
1761        }
1762    }
1763    if hi[0] < footprint_lo[0] - pad
1764        || lo[0] > footprint_hi[0] + pad
1765        || hi[1] < footprint_lo[1] - pad
1766        || lo[1] > footprint_hi[1] + pad
1767    {
1768        return false;
1769    }
1770    if poly2.iter().any(|p| pip(*p, profile)) {
1771        return true;
1772    }
1773    if profile.iter().any(|p| pip(*p, &poly2)) {
1774        return true;
1775    }
1776    let np = poly2.len();
1777    let nq = profile.len();
1778    for i in 0..np {
1779        let (a, b) = (poly2[i], poly2[(i + 1) % np]);
1780        for j in 0..nq {
1781            let (c, d2) = (profile[j], profile[(j + 1) % nq]);
1782            if seg_cross_param(a, b, c, d2).is_some() {
1783                return true;
1784            }
1785        }
1786    }
1787    false
1788}
1789
1790// ─────────────────────────────── the prism cut ──────────────────────────────
1791
1792/// Canonical-order edge×face-plane crossing cache. Both triangles sharing an
1793/// edge compute the crossing from the SAME endpoint order, so the point is
1794/// bit-identical on both sides — the T-junction-freedom cornerstone.
1795type CrossCache = FxHashMap<([u64; 3], [u64; 3], u16), V3>;
1796
1797/// A seam sub-segment on a prism face: exact 3D endpoints plus their
1798/// face-local 2D coordinates.
1799struct Seam {
1800    a3: V3,
1801    b3: V3,
1802    a2: V2,
1803    b2: V2,
1804}
1805
1806/// Plane of a face: (unit normal, offset) with the plane `{x : n·x = c}`.
1807fn face_plane(pf: &PrismFrame, face: Face) -> Option<(V3, f64)> {
1808    match face {
1809        Face::Cap { j } => Some((pf.d, pf.planes[j])),
1810        Face::Strip { k, i } => {
1811            let prof = &pf.profiles[k];
1812            let n = prof.len();
1813            let a = prof[i];
1814            let b = prof[(i + 1) % n];
1815            let e = [b[0] - a[0], b[1] - a[1]];
1816            let len = (e[0] * e[0] + e[1] * e[1]).sqrt();
1817            if len < 1.0e-9 {
1818                return None;
1819            }
1820            // Outward 2D normal of a CCW ring edge.
1821            let n2 = [e[1] / len, -e[0] / len];
1822            let n3 = add(scale(pf.u, n2[0]), scale(pf.v, n2[1]));
1823            let c = n2[0] * a[0] + n2[1] * a[1];
1824            Some((n3, c))
1825        }
1826    }
1827}
1828
1829/// Intersection of segment (a, b) with face plane `f` (`{x: m·x = c}`),
1830/// computed in canonical endpoint order and cached. Endpoint-clamped crossings
1831/// return the endpoint EXACTLY (bit-equal), so they intern to the same pooled
1832/// vertex instead of a 1-ulp duplicate.
1833fn cached_crossing(cache: &mut CrossCache, f: u16, m: V3, c: f64, a: V3, b: V3) -> V3 {
1834    let (ka, kb) = (bits(a), bits(b));
1835    let (first, second, key) = if ka <= kb {
1836        (a, b, (ka, kb, f))
1837    } else {
1838        (b, a, (kb, ka, f))
1839    };
1840    if let Some(p) = cache.get(&key) {
1841        return *p;
1842    }
1843    let da = dot(first, m) - c;
1844    let db = dot(second, m) - c;
1845    let denom = da - db;
1846    let t = if denom.abs() < 1.0e-300 {
1847        0.5
1848    } else {
1849        (da / denom).clamp(0.0, 1.0)
1850    };
1851    let p = if t <= 0.0 {
1852        first
1853    } else if t >= 1.0 {
1854        second
1855    } else {
1856        lerp(first, second, t)
1857    };
1858    cache.insert(key, p);
1859    p
1860}
1861
1862/// Face-local 2D coordinates of a 3D point.
1863fn face_coords(pf: &PrismFrame, face: Face, p: V3) -> V2 {
1864    match face {
1865        Face::Cap { .. } => pf.to2(p),
1866        Face::Strip { k, i } => {
1867            let prof = &pf.profiles[k];
1868            let n = prof.len();
1869            let a = prof[i];
1870            let b = prof[(i + 1) % n];
1871            let e = [b[0] - a[0], b[1] - a[1]];
1872            let len = (e[0] * e[0] + e[1] * e[1]).sqrt();
1873            let ed = [e[0] / len, e[1] / len];
1874            let q = pf.to2(p);
1875            [
1876                (q[0] - a[0]) * ed[0] + (q[1] - a[1]) * ed[1],
1877                dot(p, pf.d),
1878            ]
1879        }
1880    }
1881}
1882
1883/// Subtract the stepped solid `pf` from the host triangle list. `Err(defer
1884/// index)` ⇒ a gate or self-check failed ⇒ the caller must leave the host
1885/// untouched and route this opening to the exact kernel.
1886fn cut_prism(tris: &[PTri], pf: &PrismFrame) -> Result<Vec<PTri>, usize> {
1887    let faces = stack_faces(pf);
1888    if faces.len() > u16::MAX as usize {
1889        return Err(7);
1890    }
1891    let mut planes: Vec<(V3, f64)> = Vec::with_capacity(faces.len());
1892    for &face in &faces {
1893        planes.push(face_plane(pf, face).ok_or(7usize)?);
1894    }
1895
1896    let aabbs: Vec<(V3, V3)> = tris.iter().map(PTri::aabb).collect();
1897    let mut cache: CrossCache = CrossCache::default();
1898
1899    let mut out: Vec<PTri> = Vec::with_capacity(tris.len() + 16);
1900    let mut inside: Vec<PTri> = Vec::new();
1901    let mut seams: Vec<Vec<Seam>> = (0..faces.len()).map(|_| Vec::new()).collect();
1902    let mut any_cut = false;
1903
1904    for t in tris {
1905        // Quick reject: depth interval and 2D bbox versus the overall bbox.
1906        let w0 = dot(t.p[0], pf.d);
1907        let w1 = dot(t.p[1], pf.d);
1908        let w2 = dot(t.p[2], pf.d);
1909        if w0.max(w1).max(w2) < pf.d0() || w0.min(w1).min(w2) > pf.d1() {
1910            out.push(t.clone());
1911            continue;
1912        }
1913        {
1914            let q0 = pf.to2(t.p[0]);
1915            let q1 = pf.to2(t.p[1]);
1916            let q2 = pf.to2(t.p[2]);
1917            let lo = [q0[0].min(q1[0]).min(q2[0]), q0[1].min(q1[1]).min(q2[1])];
1918            let hi = [q0[0].max(q1[0]).max(q2[0]), q0[1].max(q1[1]).max(q2[1])];
1919            if hi[0] < pf.bb.0[0]
1920                || lo[0] > pf.bb.1[0]
1921                || hi[1] < pf.bb.0[1]
1922                || lo[1] > pf.bb.1[1]
1923            {
1924                out.push(t.clone());
1925                continue;
1926            }
1927        }
1928
1929        // Gather T ∩ ∂solid seam sub-segments + T-edge subdivision points.
1930        let mut segs: Vec<(usize, V3, V3)> = Vec::new(); // (face idx, a3, b3)
1931        let mut edge_pts: [Vec<V3>; 3] = Default::default();
1932        for (fi, (&face, &(m, c))) in faces.iter().zip(&planes).enumerate() {
1933            // Chord of T with this face plane (strict >0 side test — symmetric
1934            // and vertex-exact, so neighbours agree bit-for-bit).
1935            let dist = [
1936                dot(t.p[0], m) - c,
1937                dot(t.p[1], m) - c,
1938                dot(t.p[2], m) - c,
1939            ];
1940            let mut chord: Vec<(V3, usize)> = Vec::new(); // (point, tri edge)
1941            for e in 0..3 {
1942                let (da, db) = (dist[e], dist[(e + 1) % 3]);
1943                if (da > 0.0) != (db > 0.0) {
1944                    let p = cached_crossing(
1945                        &mut cache,
1946                        fi as u16,
1947                        m,
1948                        c,
1949                        t.p[e],
1950                        t.p[(e + 1) % 3],
1951                    );
1952                    chord.push((p, e));
1953                }
1954            }
1955            if chord.len() != 2 {
1956                continue;
1957            }
1958            let (q0, e0) = chord[0];
1959            let (q1, e1) = chord[1];
1960            // Face-local membership gate + clipping of the chord to the face's
1961            // exposed region.
1962            match face {
1963                Face::Cap { j } => {
1964                    let a2 = pf.to2(q0);
1965                    let b2 = pf.to2(q1);
1966                    // T-edge subdivision membership (point-deterministic —
1967                    // both neighbours agree).
1968                    if cap_xor(pf, j, a2) {
1969                        edge_pts[e0].push(q0);
1970                    }
1971                    if cap_xor(pf, j, b2) {
1972                        edge_pts[e1].push(q1);
1973                    }
1974                    // Subdivide the 2D chord at crossings with BOTH adjacent
1975                    // profiles' edges; keep sub-segments inside the XOR region.
1976                    let mut ts: Vec<f64> = vec![0.0, 1.0];
1977                    for prof in [
1978                        (j > 0).then(|| &pf.profiles[j - 1]),
1979                        (j < pf.profiles.len()).then(|| &pf.profiles[j]),
1980                    ]
1981                    .into_iter()
1982                    .flatten()
1983                    {
1984                        let npr = prof.len();
1985                        for i in 0..npr {
1986                            let (pa, pb) = (prof[i], prof[(i + 1) % npr]);
1987                            if let Some(tt) = seg_cross_param(a2, b2, pa, pb) {
1988                                ts.push(tt);
1989                            }
1990                        }
1991                    }
1992                    ts.sort_by(f64::total_cmp);
1993                    ts.dedup_by(|x, y| (*x - *y).abs() < 1.0e-12);
1994                    for wnd in ts.windows(2) {
1995                        let (ta, tb) = (wnd[0], wnd[1]);
1996                        if tb - ta < 1.0e-12 {
1997                            continue;
1998                        }
1999                        let mid = [
2000                            a2[0] + (b2[0] - a2[0]) * (ta + tb) * 0.5,
2001                            a2[1] + (b2[1] - a2[1]) * (ta + tb) * 0.5,
2002                        ];
2003                        if cap_xor(pf, j, mid) {
2004                            let sa = if ta == 0.0 { q0 } else { lerp(q0, q1, ta) };
2005                            let sb = if tb == 1.0 { q1 } else { lerp(q0, q1, tb) };
2006                            segs.push((fi, sa, sb));
2007                        }
2008                    }
2009                }
2010                Face::Strip { k, i } => {
2011                    let prof = &pf.profiles[k];
2012                    let npr = prof.len();
2013                    let a = prof[i];
2014                    let b = prof[(i + 1) % npr];
2015                    let e2 = [b[0] - a[0], b[1] - a[1]];
2016                    let len = (e2[0] * e2[0] + e2[1] * e2[1]).sqrt();
2017                    let ed = [e2[0] / len, e2[1] / len];
2018                    let (w_a, w_b) = (pf.planes[k], pf.planes[k + 1]);
2019                    let sparam = |p: V3| -> f64 {
2020                        let q = pf.to2(p);
2021                        (q[0] - a[0]) * ed[0] + (q[1] - a[1]) * ed[1]
2022                    };
2023                    let wparam = |p: V3| dot(p, pf.d);
2024                    let (s0, s1) = (sparam(q0), sparam(q1));
2025                    let (wc0, wc1) = (wparam(q0), wparam(q1));
2026                    // Membership gate for T-edge subdivision.
2027                    let in_strip =
2028                        |s: f64, w: f64| (0.0..=len).contains(&s) && (w_a..=w_b).contains(&w);
2029                    if in_strip(s0, wc0) {
2030                        edge_pts[e0].push(q0);
2031                    }
2032                    if in_strip(s1, wc1) {
2033                        edge_pts[e1].push(q1);
2034                    }
2035                    // τ interval on the chord where both params are in range.
2036                    let mut t_lo = 0.0f64;
2037                    let mut t_hi = 1.0f64;
2038                    for (p0, p1, lo, hi) in [(s0, s1, 0.0, len), (wc0, wc1, w_a, w_b)] {
2039                        let dp = p1 - p0;
2040                        if dp.abs() < 1.0e-15 {
2041                            if p0 < lo || p0 > hi {
2042                                t_lo = 1.0;
2043                                t_hi = 0.0;
2044                            }
2045                        } else {
2046                            let (mut ta, mut tb) = ((lo - p0) / dp, (hi - p0) / dp);
2047                            if ta > tb {
2048                                std::mem::swap(&mut ta, &mut tb);
2049                            }
2050                            t_lo = t_lo.max(ta);
2051                            t_hi = t_hi.min(tb);
2052                        }
2053                    }
2054                    if t_hi - t_lo > 1.0e-12 {
2055                        let sa = if t_lo == 0.0 { q0 } else { lerp(q0, q1, t_lo) };
2056                        let sb = if t_hi == 1.0 { q1 } else { lerp(q0, q1, t_hi) };
2057                        segs.push((fi, sa, sb));
2058                    }
2059                }
2060            }
2061        }
2062
2063        if segs.is_empty() && edge_pts.iter().all(|v| v.is_empty()) {
2064            // No boundary reaches this triangle: it is wholly inside or wholly
2065            // outside the (open) solid.
2066            let centroid = scale(add(add(t.p[0], t.p[1]), t.p[2]), 1.0 / 3.0);
2067            if pf.contains(centroid) {
2068                inside.push(t.clone());
2069                any_cut = true;
2070            } else {
2071                out.push(t.clone());
2072            }
2073            continue;
2074        }
2075
2076        // Conforming decomposition of T by the seam constraints.
2077        if !decompose_tri(t, pf, &faces, &segs, &edge_pts, &mut out, &mut inside, &mut seams) {
2078            return Err(7);
2079        }
2080        any_cut = true;
2081    }
2082
2083    if !any_cut {
2084        return Err(4); // nothing to remove — a silent no-op belongs to the exact path
2085    }
2086
2087    // Canonical per-cap-plane breakpoints: the vertices of both adjacent
2088    // profiles plus every proper crossing between their ring edges. Injected
2089    // into the cap AND every strip bordering that plane, so the faces sharing
2090    // a cutter-edge line subdivide it IDENTICALLY (T-junction freedom across
2091    // faces — the cap knows about profile corners the strip alone does not).
2092    let plane_pts: Vec<Vec<V2>> = (0..pf.planes.len())
2093        .map(|j| cap_plane_breakpoints(pf, j))
2094        .collect();
2095
2096    // Caps: one CDT per face, constrained by that face's seams, classified by
2097    // ray parity against the PRE-CUT host.
2098    let mut caps: Vec<PTri> = Vec::new();
2099    for (fi, &face) in faces.iter().enumerate() {
2100        if !build_face_cap(pf, face, &seams[fi], &plane_pts, tris, &aabbs, &mut caps) {
2101            return Err(7);
2102        }
2103    }
2104
2105    if !partition_consistent(tris, &out, &inside, &aabbs) {
2106        return Err(8);
2107    }
2108    if !has_measurable_region(&inside, &caps) {
2109        return Err(4);
2110    }
2111
2112    // The inside fragments and caps are not independently closed surfaces.
2113    // Their divergence sums cannot measure removal (#4627). The extended
2114    // cutter volumes instead bound the final audited result in the driver.
2115    // The driver consolidates and audits the actual result before committing.
2116    out.append(&mut caps);
2117    Ok(out)
2118}
2119
2120/// 2D point pool with tolerance merging: canonical 3D/normal = first-seen.
2121/// Merging kills the 1e-12 doublets a cutter-edge junction produces when two
2122/// faces' clips compute the same geometric point through different arithmetic.
2123struct PointPool {
2124    pts2: Vec<Point2<f64>>,
2125    pts3: Vec<V3>,
2126    nrm: Vec<V3>,
2127    buckets: FxHashMap<(i64, i64), Vec<usize>>,
2128    eps: f64,
2129}
2130
2131impl PointPool {
2132    fn new(eps: f64) -> Self {
2133        Self {
2134            pts2: Vec::new(),
2135            pts3: Vec::new(),
2136            nrm: Vec::new(),
2137            buckets: FxHashMap::default(),
2138            eps: eps.max(1.0e-15),
2139        }
2140    }
2141    fn intern(&mut self, q: V2, p3: V3, n: V3) -> usize {
2142        let cell = |x: f64| (x / (self.eps * 4.0)).floor() as i64;
2143        let (cx, cy) = (cell(q[0]), cell(q[1]));
2144        for dx in -1..=1 {
2145            for dy in -1..=1 {
2146                if let Some(ids) = self.buckets.get(&(cx + dx, cy + dy)) {
2147                    for &i in ids {
2148                        let d0 = self.pts2[i].x - q[0];
2149                        let d1 = self.pts2[i].y - q[1];
2150                        if d0.abs() <= self.eps && d1.abs() <= self.eps {
2151                            return i;
2152                        }
2153                    }
2154                }
2155            }
2156        }
2157        let idx = self.pts2.len();
2158        self.pts2.push(Point2::new(q[0], q[1]));
2159        self.pts3.push(p3);
2160        self.nrm.push(n);
2161        self.buckets.entry((cx, cy)).or_default().push(idx);
2162        idx
2163    }
2164}
2165
2166/// Decompose one host triangle by the cutter-boundary seams via a conforming
2167/// per-triangle CDT; classify each sub-triangle by its centroid; record the
2168/// seams (with face-local 2D coords) for the cap builder. Returns false when
2169/// the CDT cannot be built (→ exact kernel for the whole opening).
2170#[allow(clippy::too_many_arguments)]
2171fn decompose_tri(
2172    t: &PTri,
2173    pf: &PrismFrame,
2174    faces: &[Face],
2175    segs: &[(usize, V3, V3)],
2176    edge_pts: &[Vec<V3>; 3],
2177    out: &mut Vec<PTri>,
2178    inside: &mut Vec<PTri>,
2179    seams: &mut [Vec<Seam>],
2180) -> bool {
2181    // 2D basis in the triangle's plane (CCW = triangle winding).
2182    let Some(u) = normalize(sub(t.p[1], t.p[0])) else {
2183        out.push(t.clone());
2184        return true;
2185    };
2186    let Some(w) = normalize(cross(sub(t.p[1], t.p[0]), sub(t.p[2], t.p[0]))) else {
2187        out.push(t.clone());
2188        return true;
2189    };
2190    let v = cross(w, u);
2191    let to2 = |x: V3| -> V2 {
2192        let d = sub(x, t.p[0]);
2193        [dot(d, u), dot(d, v)]
2194    };
2195
2196    let mut mag = 0.0f64;
2197    for p in &t.p {
2198        for c in p {
2199            mag = mag.max(c.abs());
2200        }
2201    }
2202    let mut pool = PointPool::new(1.0e-9 * (1.0 + mag));
2203
2204    let tri_idx = [
2205        pool.intern(to2(t.p[0]), t.p[0], t.n[0]),
2206        pool.intern(to2(t.p[1]), t.p[1], t.n[1]),
2207        pool.intern(to2(t.p[2]), t.p[2], t.n[2]),
2208    ];
2209
2210    let mut segments: Vec<(usize, usize)> = Vec::new();
2211    // T-edge chains subdivided at the admitted crossing points.
2212    for e in 0..3 {
2213        let a3 = t.p[e];
2214        let b3 = t.p[(e + 1) % 3];
2215        let dir = sub(b3, a3);
2216        let len2 = dot(dir, dir);
2217        if len2 <= 0.0 {
2218            continue;
2219        }
2220        let mut on_edge: Vec<(f64, usize)> = Vec::new();
2221        for p in &edge_pts[e] {
2222            // Normal interpolated along the edge.
2223            let tt = (dot(sub(*p, a3), dir) / len2).clamp(0.0, 1.0);
2224            let nn = lerp(t.n[e], t.n[(e + 1) % 3], tt);
2225            let idx = pool.intern(to2(*p), *p, nn);
2226            on_edge.push((tt, idx));
2227        }
2228        on_edge.sort_by(|x, y| x.0.total_cmp(&y.0));
2229        let mut prev = tri_idx[e];
2230        for &(_, idx) in &on_edge {
2231            if idx != prev {
2232                segments.push((prev, idx));
2233                prev = idx;
2234            }
2235        }
2236        let last = tri_idx[(e + 1) % 3];
2237        if last != prev {
2238            segments.push((prev, last));
2239        }
2240    }
2241    // Seam constraints (+ record for the cap builder). Interior seam points
2242    // carry the triangle's geometric normal (flat-facet hosts: exact).
2243    let face_n = normalize(cross(sub(t.p[1], t.p[0]), sub(t.p[2], t.p[0]))).unwrap_or(w);
2244    for &(fi, a3, b3) in segs {
2245        let ia = pool.intern(to2(a3), a3, face_n);
2246        let ib = pool.intern(to2(b3), b3, face_n);
2247        if ia == ib {
2248            continue;
2249        }
2250        segments.push((ia, ib));
2251        seams[fi].push(Seam {
2252            a3,
2253            b3,
2254            a2: face_coords(pf, faces[fi], a3),
2255            b2: face_coords(pf, faces[fi], b3),
2256        });
2257    }
2258    segments.sort();
2259    segments.dedup();
2260    let t0 = t.p[0];
2261    // `resolve_crossings` interns any new seam×seam / T-junction points with a
2262    // placeholder [0,0,1] normal. These points land on the RETAINED host
2263    // surface (`out`), so a Z-up placeholder on a non-horizontal face shades
2264    // wrong. Overwrite every point it created with the host triangle's own
2265    // face normal.
2266    let existing_points = pool.nrm.len();
2267    resolve_crossings(
2268        &mut pool,
2269        |q| add(t0, add(scale(u, q[0]), scale(v, q[1]))),
2270        &mut segments,
2271    );
2272    for n in pool.nrm.iter_mut().skip(existing_points) {
2273        *n = face_n;
2274    }
2275
2276    let Some((pts2, idx2)) = triangulate_pslg(&pool.pts2, &segments) else {
2277        return false;
2278    };
2279    if pts2.len() != pool.pts2.len() {
2280        return false; // CDT must not invent points — mapping back requires identity
2281    }
2282
2283    for tri in idx2.chunks_exact(3) {
2284        let (a, b, c) = (tri[0], tri[1], tri[2]);
2285        let p = [pool.pts3[a], pool.pts3[b], pool.pts3[c]];
2286        let centroid = scale(add(add(p[0], p[1]), p[2]), 1.0 / 3.0);
2287        let piece = PTri {
2288            p,
2289            n: [pool.nrm[a], pool.nrm[b], pool.nrm[c]],
2290        };
2291        if pf.contains(centroid) {
2292            inside.push(piece);
2293        } else {
2294            out.push(piece);
2295        }
2296    }
2297    true
2298}
2299
2300/// Canonical 2D breakpoints of cap plane `j`: all vertices of the adjacent
2301/// slab profiles plus every proper pairwise crossing between the two rings.
2302fn cap_plane_breakpoints(pf: &PrismFrame, j: usize) -> Vec<V2> {
2303    let mut profs: Vec<&[V2]> = Vec::new();
2304    if j > 0 {
2305        profs.push(&pf.profiles[j - 1]);
2306    }
2307    if j < pf.profiles.len() {
2308        profs.push(&pf.profiles[j]);
2309    }
2310    let mut out: Vec<V2> = Vec::new();
2311    for prof in &profs {
2312        out.extend_from_slice(prof);
2313    }
2314    if profs.len() == 2 {
2315        let (a, b) = (profs[0], profs[1]);
2316        let (na, nb) = (a.len(), b.len());
2317        for i in 0..na {
2318            let (p, q) = (a[i], a[(i + 1) % na]);
2319            for k in 0..nb {
2320                let (c, d) = (b[k], b[(k + 1) % nb]);
2321                if let Some(t) = seg_cross_param(p, q, c, d) {
2322                    if (1.0e-9..=1.0 - 1.0e-9).contains(&t) {
2323                        out.push([p[0] + (q[0] - p[0]) * t, p[1] + (q[1] - p[1]) * t]);
2324                    }
2325                }
2326            }
2327        }
2328    }
2329    out
2330}
2331
2332/// Build the cap (reveal face) for one cutter face: CDT of the face domain
2333/// constrained by the reveal seams (and, for a stepped cap, the adjacent
2334/// profiles' rings), each sub-triangle kept iff its centroid lies in the
2335/// face's exposed region AND inside the (pre-cut) host solid by ray parity.
2336/// Emitted oriented for the RESULT (normal pointing INTO the removed void).
2337/// Returns false on CDT failure.
2338fn build_face_cap(
2339    pf: &PrismFrame,
2340    face: Face,
2341    seams: &[Seam],
2342    plane_pts: &[Vec<V2>],
2343    host: &[PTri],
2344    aabbs: &[(V3, V3)],
2345    caps: &mut Vec<PTri>,
2346) -> bool {
2347    match face {
2348        Face::Strip { k, i } => {
2349            let prof = &pf.profiles[k];
2350            let npr = prof.len();
2351            let a = prof[i];
2352            let b = prof[(i + 1) % npr];
2353            let e2 = [b[0] - a[0], b[1] - a[1]];
2354            let len = (e2[0] * e2[0] + e2[1] * e2[1]).sqrt();
2355            if len < 1.0e-9 {
2356                return true;
2357            }
2358            let ed = [e2[0] / len, e2[1] / len];
2359            let (w_a, w_b) = (pf.planes[k], pf.planes[k + 1]);
2360            let lift_s =
2361                |s: f64, wd: f64| -> V3 { pf.lift(a[0] + ed[0] * s, a[1] + ed[1] * s, wd) };
2362            let ring2: Vec<V2> = vec![[0.0, w_a], [len, w_a], [len, w_b], [0.0, w_b]];
2363            let ring3: Vec<V3> = vec![
2364                lift_s(0.0, w_a),
2365                lift_s(len, w_a),
2366                lift_s(len, w_b),
2367                lift_s(0.0, w_b),
2368            ];
2369            // Outward 2D normal of the CCW ring edge; the result-cap normal is
2370            // its opposite (into the void). CCW-in-(s, w) lifts to the OUTWARD
2371            // strip normal ⇒ always flip.
2372            let n2 = [e2[1] / len, -e2[0] / len];
2373            let outward = add(scale(pf.u, n2[0]), scale(pf.v, n2[1]));
2374            let desired = scale(outward, -1.0);
2375            // Inject the canonical breakpoints of BOTH bounding cap planes
2376            // that lie on this strip's edge line, so the strip subdivides its
2377            // shared boundary exactly like the neighbouring caps do.
2378            let mut inject: Vec<(V2, V3)> = Vec::new();
2379            let on_line_tol = 1.0e-6 * (1.0 + pf.corner_mag());
2380            for (jj, wj) in [(k, w_a), (k + 1, w_b)] {
2381                for q in &plane_pts[jj] {
2382                    let ap = [q[0] - a[0], q[1] - a[1]];
2383                    let ss = ap[0] * ed[0] + ap[1] * ed[1];
2384                    if !(0.0..=len).contains(&ss) {
2385                        continue;
2386                    }
2387                    let perp = (ap[0] * ed[1] - ap[1] * ed[0]).abs();
2388                    if perp <= on_line_tol {
2389                        inject.push(([ss, wj], lift_s(ss, wj)));
2390                    }
2391                }
2392            }
2393            if seams.is_empty() && inject.is_empty() {
2394                // Entirely inside the host (whole-face reveal) or entirely
2395                // outside — one parity probe decides.
2396                let mid = |kk: usize| (ring3[0][kk] + ring3[2][kk]) * 0.5;
2397                if !point_inside(host, aabbs, [mid(0), mid(1), mid(2)]) {
2398                    return true;
2399                }
2400                emit_cap(caps, ring3[0], ring3[1], ring3[2], desired, true);
2401                emit_cap(caps, ring3[0], ring3[2], ring3[3], desired, true);
2402                return true;
2403            }
2404            if seams.is_empty() {
2405                // Injected points but no seams: same parity probe decides
2406                // whole-face vs nothing, but the emission must still be the
2407                // subdivided CDT so the shared boundary matches the caps.
2408                let mid = |kk: usize| (ring3[0][kk] + ring3[2][kk]) * 0.5;
2409                if !point_inside(host, aabbs, [mid(0), mid(1), mid(2)]) {
2410                    return true;
2411                }
2412            }
2413            cap_cdt(
2414                pf,
2415                &ring2,
2416                &ring3,
2417                &[],
2418                seams,
2419                &inject,
2420                desired,
2421                true,
2422                |q| pip(q, &ring2),
2423                host,
2424                aabbs,
2425                caps,
2426            )
2427        }
2428        Face::Cap { j } => {
2429            let wdepth = pf.planes[j];
2430            // Domain ring: the union bbox of the adjacent profiles, slightly
2431            // inflated so profile rings are strictly interior constraints.
2432            let mut lo = [f64::INFINITY; 2];
2433            let mut hi = [f64::NEG_INFINITY; 2];
2434            let mut aux: Vec<&[V2]> = Vec::new();
2435            if j > 0 {
2436                aux.push(&pf.profiles[j - 1]);
2437            }
2438            if j < pf.profiles.len() {
2439                aux.push(&pf.profiles[j]);
2440            }
2441            for prof in &aux {
2442                for p in prof.iter() {
2443                    for k in 0..2 {
2444                        lo[k] = lo[k].min(p[k]);
2445                        hi[k] = hi[k].max(p[k]);
2446                    }
2447                }
2448            }
2449            let m = 1.0e-3 * (1.0 + (hi[0] - lo[0]).abs().max((hi[1] - lo[1]).abs()));
2450            let (lo, hi) = ([lo[0] - m, lo[1] - m], [hi[0] + m, hi[1] + m]);
2451            let ring2: Vec<V2> = vec![
2452                [lo[0], lo[1]],
2453                [hi[0], lo[1]],
2454                [hi[0], hi[1]],
2455                [lo[0], hi[1]],
2456            ];
2457            let ring3: Vec<V3> = ring2
2458                .iter()
2459                .map(|&[uu, vv]| pf.lift(uu, vv, wdepth))
2460                .collect();
2461            // Orientation per sub-region: where only the LOWER slab exists the
2462            // removed void is below ⇒ normal −d; where only the UPPER slab
2463            // exists ⇒ +d. Resolved per emitted triangle inside `cap_cdt` via
2464            // the classifier below; the `desired`/flip passed in cover the
2465            // "+d" branch and the emitter flips for the −d case.
2466            let in_lower = |q: V2| j > 0 && pip(q, &pf.profiles[j - 1]);
2467            let in_upper = |q: V2| j < pf.profiles.len() && pip(q, &pf.profiles[j]);
2468            // Fast skip for the OUTERMOST caps with no seams (the common
2469            // extended-cap case): probe the adjacent slab interior.
2470            if seams.is_empty() && (j == 0 || j == pf.profiles.len()) {
2471                let k = if j == 0 { 0 } else { pf.profiles.len() - 1 };
2472                let interior = pf.slab_interior[k];
2473                let probe = pf.lift(interior[0], interior[1], wdepth);
2474                if !point_inside(host, aabbs, probe) {
2475                    return true;
2476                }
2477            }
2478            let inject: Vec<(V2, V3)> = plane_pts[j]
2479                .iter()
2480                .map(|&q| (q, pf.lift(q[0], q[1], wdepth)))
2481                .collect();
2482            cap_cdt_xor(
2483                pf,
2484                &ring2,
2485                &ring3,
2486                &aux,
2487                seams,
2488                &inject,
2489                wdepth,
2490                in_lower,
2491                in_upper,
2492                host,
2493                aabbs,
2494                caps,
2495            )
2496        }
2497    }
2498}
2499
2500/// Emit one cap triangle with the RESULT orientation: `flip` swaps the winding
2501/// so the geometric normal matches `desired`.
2502fn emit_cap(caps: &mut Vec<PTri>, a: V3, b: V3, c: V3, desired: V3, flip: bool) {
2503    let (b, c) = if flip { (c, b) } else { (b, c) };
2504    caps.push(PTri {
2505        p: [a, b, c],
2506        n: [desired, desired, desired],
2507    });
2508}
2509
2510/// Shared cap CDT for a SINGLE-region face (side strips): domain ring +
2511/// seams, keep = `region(centroid)` && host parity.
2512#[allow(clippy::too_many_arguments)]
2513fn cap_cdt(
2514    pf: &PrismFrame,
2515    ring2: &[V2],
2516    ring3: &[V3],
2517    aux: &[&[V2]],
2518    seams: &[Seam],
2519    inject: &[(V2, V3)],
2520    desired: V3,
2521    flip: bool,
2522    region: impl Fn(V2) -> bool,
2523    host: &[PTri],
2524    aabbs: &[(V3, V3)],
2525    caps: &mut Vec<PTri>,
2526) -> bool {
2527    cap_cdt_impl(pf, ring2, ring3, aux, seams, inject, host, aabbs, caps, |q| {
2528        region(q).then_some((desired, flip))
2529    })
2530}
2531
2532/// Cap CDT for a stepped cap plane: keep = XOR of the adjacent profiles, with
2533/// per-region orientation (−d where only the lower slab exists, +d where only
2534/// the upper does).
2535#[allow(clippy::too_many_arguments)]
2536fn cap_cdt_xor(
2537    pf: &PrismFrame,
2538    ring2: &[V2],
2539    ring3: &[V3],
2540    aux: &[&[V2]],
2541    seams: &[Seam],
2542    inject: &[(V2, V3)],
2543    _wdepth: f64,
2544    in_lower: impl Fn(V2) -> bool,
2545    in_upper: impl Fn(V2) -> bool,
2546    host: &[PTri],
2547    aabbs: &[(V3, V3)],
2548    caps: &mut Vec<PTri>,
2549) -> bool {
2550    let d = pf.d;
2551    cap_cdt_impl(pf, ring2, ring3, aux, seams, inject, host, aabbs, caps, move |q| {
2552        let lo = in_lower(q);
2553        let up = in_upper(q);
2554        if lo == up {
2555            return None; // outside the exposed XOR region
2556        }
2557        if lo {
2558            // Cutter below the plane ⇒ solid above ⇒ normal −d. CCW-in-(u,v)
2559            // lifts to +d ⇒ flip.
2560            Some((scale(d, -1.0), true))
2561        } else {
2562            Some((d, false))
2563        }
2564    })
2565}
2566
2567
2568/// Make a segment soup CDT-admissible: split every pair of properly-crossing
2569/// constraints at their intersection (interned through `lift3`), and split
2570/// every constraint at any pooled point lying strictly in its interior.
2571/// Needed because adjacent slab profiles of a rebated opening genuinely CROSS
2572/// (a sill / lintel detail poking past the other slab's outline), and seam
2573/// endpoints can land in the interior of a ring edge.
2574fn resolve_crossings(
2575    pool: &mut PointPool,
2576    lift3: impl Fn(V2) -> V3,
2577    segs: &mut Vec<(usize, usize)>,
2578) {
2579    let orig = segs.clone();
2580    let mut splits: Vec<Vec<(f64, usize)>> = vec![Vec::new(); orig.len()];
2581    // Pairwise proper crossings on the ORIGINAL segments (sub-segment
2582    // crossings are a subset, so one pass suffices).
2583    for i in 0..orig.len() {
2584        for j in (i + 1)..orig.len() {
2585            let (a, b) = orig[i];
2586            let (c, d) = orig[j];
2587            if a == c || a == d || b == c || b == d {
2588                continue;
2589            }
2590            let pa = [pool.pts2[a].x, pool.pts2[a].y];
2591            let pb = [pool.pts2[b].x, pool.pts2[b].y];
2592            let pc = [pool.pts2[c].x, pool.pts2[c].y];
2593            let pd = [pool.pts2[d].x, pool.pts2[d].y];
2594            let r = [pb[0] - pa[0], pb[1] - pa[1]];
2595            let sv = [pd[0] - pc[0], pd[1] - pc[1]];
2596            let den = r[0] * sv[1] - r[1] * sv[0];
2597            if den.abs() < 1.0e-18 {
2598                continue;
2599            }
2600            let ap = [pc[0] - pa[0], pc[1] - pa[1]];
2601            let t = (ap[0] * sv[1] - ap[1] * sv[0]) / den;
2602            let u = (ap[0] * r[1] - ap[1] * r[0]) / den;
2603            const E: f64 = 1.0e-9;
2604            if !(E..=1.0 - E).contains(&t) || !(E..=1.0 - E).contains(&u) {
2605                continue;
2606            }
2607            let q = [pa[0] + r[0] * t, pa[1] + r[1] * t];
2608            let idx = pool.intern(q, lift3(q), [0.0, 0.0, 1.0]);
2609            splits[i].push((t, idx));
2610            splits[j].push((u, idx));
2611        }
2612    }
2613    // Pooled points strictly interior to a segment (T-junctions).
2614    let n_pts = pool.pts2.len();
2615    for k in 0..n_pts {
2616        let p = [pool.pts2[k].x, pool.pts2[k].y];
2617        for (i, &(a, b)) in orig.iter().enumerate() {
2618            if a == k || b == k {
2619                continue;
2620            }
2621            let pa = [pool.pts2[a].x, pool.pts2[a].y];
2622            let pb = [pool.pts2[b].x, pool.pts2[b].y];
2623            let e = [pb[0] - pa[0], pb[1] - pa[1]];
2624            let len2 = e[0] * e[0] + e[1] * e[1];
2625            if len2 < 1.0e-18 {
2626                continue;
2627            }
2628            let t = ((p[0] - pa[0]) * e[0] + (p[1] - pa[1]) * e[1]) / len2;
2629            if !(1.0e-9..=1.0 - 1.0e-9).contains(&t) {
2630                continue;
2631            }
2632            let perp = ((p[0] - pa[0]) * e[1] - (p[1] - pa[1]) * e[0]).abs() / len2.sqrt();
2633            if perp <= pool.eps * 4.0 {
2634                splits[i].push((t, k));
2635            }
2636        }
2637    }
2638    let mut out: Vec<(usize, usize)> = Vec::with_capacity(orig.len() + 8);
2639    for (i, &(a, b)) in orig.iter().enumerate() {
2640        if splits[i].is_empty() {
2641            out.push((a, b));
2642            continue;
2643        }
2644        let mut sp = std::mem::take(&mut splits[i]);
2645        sp.sort_by(|x, y| x.0.total_cmp(&y.0));
2646        let mut prev = a;
2647        for &(_, idx) in &sp {
2648            if idx != prev {
2649                out.push((prev, idx));
2650                prev = idx;
2651            }
2652        }
2653        if prev != b {
2654            out.push((prev, b));
2655        }
2656    }
2657    out.sort();
2658    out.dedup();
2659    *segs = out;
2660}
2661
2662/// The cap CDT core: pool the ring, aux-profile rings and seams; chain every
2663/// straight constraint through the pooled points lying on it; run the
2664/// conforming CDT; classify each sub-triangle by `classify(centroid)` (which
2665/// returns the emit orientation) && host parity.
2666#[allow(clippy::too_many_arguments)]
2667fn cap_cdt_impl(
2668    pf: &PrismFrame,
2669    ring2: &[V2],
2670    ring3: &[V3],
2671    aux: &[&[V2]],
2672    seams: &[Seam],
2673    inject: &[(V2, V3)],
2674    host: &[PTri],
2675    aabbs: &[(V3, V3)],
2676    caps: &mut Vec<PTri>,
2677    classify: impl Fn(V2) -> Option<(V3, bool)>,
2678) -> bool {
2679    let snap_eps = 1.0e-9 * (1.0 + pf.corner_mag());
2680    let mut pool = PointPool::new(snap_eps);
2681    let placeholder_n = [0.0, 0.0, 1.0];
2682    let ring_idx: Vec<usize> = ring2
2683        .iter()
2684        .zip(ring3)
2685        .map(|(&q, &p3)| pool.intern(q, p3, placeholder_n))
2686        .collect();
2687    // Aux profile rings (cap planes only): 2D verts lifted on the face plane.
2688    // Their 3D lift shares the plane depth of ring3[0] along d.
2689    let wdepth = dot(ring3[0], pf.d);
2690    let mut aux_idx: Vec<Vec<usize>> = Vec::new();
2691    for prof in aux {
2692        aux_idx.push(
2693            prof.iter()
2694                .map(|&[uu, vv]| pool.intern([uu, vv], pf.lift(uu, vv, wdepth), placeholder_n))
2695                .collect(),
2696        );
2697    }
2698    // Canonical shared-boundary breakpoints (pooled so the chains below pick
2699    // them up — the cross-face T-junction freedom).
2700    for &(q, p3) in inject {
2701        pool.intern(q, p3, placeholder_n);
2702    }
2703    let mut seam_segs: Vec<(usize, usize)> = Vec::new();
2704    for s in seams {
2705        let ia = pool.intern(s.a2, s.a3, placeholder_n);
2706        let ib = pool.intern(s.b2, s.b3, placeholder_n);
2707        if ia != ib {
2708            seam_segs.push((ia, ib));
2709        }
2710    }
2711    // Chain every straight constraint (ring edges + aux profile edges) through
2712    // the pooled points lying on it, so seam endpoints subdivide them and the
2713    // CDT stays conforming.
2714    let n_pool = pool.pts2.len();
2715    let mut seg_list: Vec<(usize, usize)> = seam_segs;
2716    let chain = |ia: usize, ib: usize, seg_list: &mut Vec<(usize, usize)>| {
2717        let a = [pool.pts2[ia].x, pool.pts2[ia].y];
2718        let b = [pool.pts2[ib].x, pool.pts2[ib].y];
2719        let eb = [b[0] - a[0], b[1] - a[1]];
2720        let len2 = eb[0] * eb[0] + eb[1] * eb[1];
2721        if len2 < 1.0e-18 {
2722            return;
2723        }
2724        let edge_tol = snap_eps * 4.0;
2725        let mut on_edge: Vec<(f64, usize)> = Vec::new();
2726        for i in 0..n_pool {
2727            if i == ia || i == ib {
2728                continue;
2729            }
2730            let p = [pool.pts2[i].x, pool.pts2[i].y];
2731            let ap = [p[0] - a[0], p[1] - a[1]];
2732            let tt = (ap[0] * eb[0] + ap[1] * eb[1]) / len2;
2733            if !(0.0..=1.0).contains(&tt) {
2734                continue;
2735            }
2736            let perp = (ap[0] * eb[1] - ap[1] * eb[0]).abs() / len2.sqrt();
2737            if perp <= edge_tol {
2738                on_edge.push((tt, i));
2739            }
2740        }
2741        on_edge.sort_by(|x, y| x.0.total_cmp(&y.0));
2742        let mut prev = ia;
2743        for &(_, i) in &on_edge {
2744            if i != prev {
2745                seg_list.push((prev, i));
2746                prev = i;
2747            }
2748        }
2749        if prev != ib {
2750            seg_list.push((prev, ib));
2751        }
2752    };
2753    let nr = ring_idx.len();
2754    for e in 0..nr {
2755        chain(ring_idx[e], ring_idx[(e + 1) % nr], &mut seg_list);
2756    }
2757    for ridx in &aux_idx {
2758        let n = ridx.len();
2759        for e in 0..n {
2760            chain(ridx[e], ridx[(e + 1) % n], &mut seg_list);
2761        }
2762    }
2763    seg_list.sort();
2764    seg_list.dedup();
2765    resolve_crossings(&mut pool, |q| pf.lift(q[0], q[1], wdepth), &mut seg_list);
2766
2767    let Some((pts2, idx2)) = triangulate_pslg(&pool.pts2, &seg_list) else {
2768        return false;
2769    };
2770    if pts2.len() != pool.pts2.len() {
2771        return false;
2772    }
2773    for tri in idx2.chunks_exact(3) {
2774        let (a, b, c) = (tri[0], tri[1], tri[2]);
2775        let cu = (pts2[a].x + pts2[b].x + pts2[c].x) / 3.0;
2776        let cv = (pts2[a].y + pts2[b].y + pts2[c].y) / 3.0;
2777        let Some((desired, flip)) = classify([cu, cv]) else {
2778            continue;
2779        };
2780        // Lift the centroid to 3D for the host-parity test.
2781        let c3 = scale(
2782            add(add(pool.pts3[a], pool.pts3[b]), pool.pts3[c]),
2783            1.0 / 3.0,
2784        );
2785        if point_inside(host, aabbs, c3) {
2786            let (pa, pb, pc) = (pool.pts3[a], pool.pts3[b], pool.pts3[c]);
2787            emit_cap(caps, pa, pb, pc, desired, flip);
2788        }
2789    }
2790    true
2791}
2792
2793// ─────────────────────────────── the driver ─────────────────────────────────
2794
2795impl GeometryRouter {
2796    /// Try the analytic prism subtraction on `mesh`.
2797    ///
2798    /// Iterates the merged openings in order; each opening whose cutter is a
2799    /// clean prism AND whose cut passes every self-check is subtracted
2800    /// analytically; every other opening lands in the returned residual
2801    /// context (cut by the exact kernel on the analytic result — the caller
2802    /// recurses through [`GeometryRouter::apply_void_context`], mirroring the
2803    /// 2D path's residual composition). `None` ⇒ no opening was cut and the
2804    /// caller must run the FULL original context through the exact kernel.
2805    pub(super) fn try_prism_cut(
2806        &self,
2807        mesh: &Mesh,
2808        ctx: &VoidContext,
2809    ) -> Option<(Mesh, Option<VoidContext>)> {
2810        #[cfg(feature = "opening-perf-trace")]
2811        crate::opening_perf_trace::record(|c| c.prism_attempts = c.prism_attempts.saturating_add(1));
2812        if ctx.merged_openings.is_empty() || mesh.is_empty() || mesh.indices.len() < 12 {
2813            return None;
2814        }
2815        // Promote to the f64 triangle list FIRST: `ptris_from_mesh` bounds-checks
2816        // every index, whereas the closure audits index `mesh.positions` straight
2817        // from `mesh.indices`. Auditing first would PANIC (abort under
2818        // panic=abort) on a malformed host index instead of recording
2819        // `host_promote_fail` and deferring. Post-promotion all indices are valid.
2820        let origin = mesh.origin;
2821        let Some(mut tris) = ptris_from_mesh(mesh) else {
2822            defer(1);
2823            return None;
2824        };
2825        // The volume identity, the parity classification, and the closed
2826        // self-check are only meaningful on a host that arrives as a
2827        // consistently-wound closed solid (hairline subdivision mismatches
2828        // tolerated — tessellated hosts routinely carry them).
2829        if !closed_enough_to_emit(mesh) {
2830            defer(0);
2831            return None;
2832        }
2833        let min_vol = Self::min_opening_volume(self.tessellation_quality);
2834
2835        // ── Candidate prisms ────────────────────────────────────────────────
2836        // Extract a prism per opening, then MERGE prisms whose union is one
2837        // prism (the wall-leaf half-void pattern: two abutting halves of one
2838        // window — cutting the union in one pass sidesteps their coplanar
2839        // interface). Openings without a prism become AABB "blockers".
2840        struct Cand {
2841            pf: PrismFrame,
2842            ops: Vec<usize>, // indices into ctx.merged_openings
2843        }
2844        let mut cands: Vec<Cand> = Vec::new();
2845        let mut blockers: Vec<([V3; 3], V3, V3)> = Vec::new();
2846        let mut residual_idx: Vec<usize> = Vec::new();
2847        for (i, op) in ctx.merged_openings.iter().enumerate() {
2848            match prepare_prism(op, origin) {
2849                Some(pf) => cands.push(Cand { pf, ops: vec![i] }),
2850                None => {
2851                    defer(2);
2852                    residual_idx.push(i);
2853                    if let Some(blk) = opening_aabb_obb(op, origin) {
2854                        blockers.push(blk);
2855                    }
2856                }
2857            }
2858        }
2859        // Profile-equality fusion must weld on NUMERICAL coincidence, not a 2 mm
2860        // geometric slack: at 2 mm two genuinely distinct leaf profiles could be
2861        // declared equal and fused, silently collapsing a real slab boundary. Use
2862        // the same 8·SNAP_GRID weld the depth-concatenation gate already uses.
2863        const PROFILE_FUSE_TOL: f64 = 8.0 * crate::kernel::mesh_bridge::SNAP_GRID;
2864        loop {
2865            let mut merged_pair: Option<(usize, usize, PrismFrame)> = None;
2866            'search: for i in 0..cands.len() {
2867                for j in (i + 1)..cands.len() {
2868                    if let Some(m) = try_merge_prisms(&cands[i].pf, &cands[j].pf, PROFILE_FUSE_TOL) {
2869                        merged_pair = Some((i, j, m));
2870                        break 'search;
2871                    }
2872                }
2873            }
2874            let Some((i, j, m)) = merged_pair else { break };
2875            let mut absorbed = cands.remove(j);
2876            cands[i].pf = m;
2877            cands[i].ops.append(&mut absorbed.ops);
2878        }
2879        // Overlap vetoes, decided BEFORE any cut so an abutting-but-unmergeable
2880        // pair is never half-cut (cutting one against the other's flush caps —
2881        // here or in the residual exact pass — is the coplanar minefield the
2882        // exact kernel owns). 1 mm margin, matching the batch pad.
2883        let mut vetoed = vec![false; cands.len()];
2884        for i in 0..cands.len() {
2885            let obb_i = cands[i].pf.obb();
2886            for (j, cj) in cands.iter().enumerate().skip(i + 1) {
2887                if obb_overlaps(&obb_i, &cj.pf.obb(), 1.0e-3) {
2888                    vetoed[i] = true;
2889                    vetoed[j] = true;
2890                }
2891            }
2892            if blockers.iter().any(|b| obb_overlaps(&obb_i, b, 1.0e-3)) {
2893                vetoed[i] = true;
2894            }
2895        }
2896
2897        // ── Sequential analytic cuts ────────────────────────────────────────
2898        let mut committed_ops = 0usize;
2899        let mut committed_cutter_volume = 0.0;
2900        // AABB (host-local frame) of every COMMITTED cutter prism, captured
2901        // post-extension: the region the cut actually re-triangulated. Feeds the
2902        // scoped sliver refinement below (#1007 targets rim slivers, not the
2903        // host's own authored long-thin faces). 1 mm pad = the batch/veto margin,
2904        // comfortably covering the seam vertices shared with kept host fragments.
2905        const REFINE_REGION_PAD: f64 = 1.0e-3;
2906        let mut refine_boxes: Vec<([f64; 3], [f64; 3])> = Vec::new();
2907        for (cand, veto) in cands.iter().zip(&vetoed) {
2908            crate::progress::tick();
2909            let mut ok = false;
2910            if *veto {
2911                defer(6);
2912            } else if cand.pf.volume() < min_vol {
2913                // Tiny cutters keep the exact path's (identical) skip
2914                // semantics.
2915                defer(3);
2916            } else {
2917                let mut pf = cand.pf.clone();
2918                // Host projection intervals on the prism axes: no-overlap ⇒
2919                // silent no-op (the exact path owns that diagnostic); engulf ⇒
2920                // the exact path owns the redundant-void machinery (#964,
2921                // #635).
2922                let axes = [pf.u, pf.v, pf.d];
2923                let mut hlo = [f64::INFINITY; 3];
2924                let mut hhi = [f64::NEG_INFINITY; 3];
2925                for t in &tris {
2926                    for p in &t.p {
2927                        for k in 0..3 {
2928                            let s = dot(*p, axes[k]);
2929                            hlo[k] = hlo[k].min(s);
2930                            hhi[k] = hhi[k].max(s);
2931                        }
2932                    }
2933                }
2934                let plo = [pf.bb.0[0], pf.bb.0[1], pf.d0()];
2935                let phi = [pf.bb.1[0], pf.bb.1[1], pf.d1()];
2936                let overlap = (0..3).all(|k| phi[k] > hlo[k] && plo[k] < hhi[k]);
2937                if !overlap {
2938                    defer(4);
2939                } else {
2940                    let aabbs: Vec<(V3, V3)> = tris.iter().map(PTri::aabb).collect();
2941                    extend_prism_caps(&mut pf, &tris, &aabbs);
2942                    extend_profile_edges(&mut pf, &tris, &aabbs);
2943                    let plo = [pf.bb.0[0], pf.bb.0[1], pf.d0()];
2944                    let phi = [pf.bb.1[0], pf.bb.1[1], pf.d1()];
2945                    let engulfs = (0..3).all(|k| {
2946                        let slack =
2947                            (hhi[k] - hlo[k]).abs().max(1.0e-9) * super::ENGULF_TOLERANCE;
2948                        plo[k] <= hlo[k] + slack && phi[k] >= hhi[k] - slack
2949                    });
2950                    if engulfs {
2951                        defer(5);
2952                    } else {
2953                        match cut_prism(&tris, &pf) {
2954                            Ok(outcome) => {
2955                                committed_cutter_volume += pf.volume();
2956                                tris = outcome;
2957                                committed_ops += cand.ops.len();
2958                                ok = true;
2959                                // Committed-cutter AABB (extended pf): lift the
2960                                // 4 profile-bb corners at both depth planes.
2961                                let mut lo = [f64::INFINITY; 3];
2962                                let mut hi = [f64::NEG_INFINITY; 3];
2963                                for &uu in &[pf.bb.0[0], pf.bb.1[0]] {
2964                                    for &vv in &[pf.bb.0[1], pf.bb.1[1]] {
2965                                        for &w in &[pf.d0(), pf.d1()] {
2966                                            let p = pf.lift(uu, vv, w);
2967                                            for k in 0..3 {
2968                                                lo[k] = lo[k].min(p[k]);
2969                                                hi[k] = hi[k].max(p[k]);
2970                                            }
2971                                        }
2972                                    }
2973                                }
2974                                refine_boxes.push((
2975                                    [
2976                                        lo[0] - REFINE_REGION_PAD,
2977                                        lo[1] - REFINE_REGION_PAD,
2978                                        lo[2] - REFINE_REGION_PAD,
2979                                    ],
2980                                    [
2981                                        hi[0] + REFINE_REGION_PAD,
2982                                        hi[1] + REFINE_REGION_PAD,
2983                                        hi[2] + REFINE_REGION_PAD,
2984                                    ],
2985                                ));
2986                            }
2987                            Err(reason) => defer(reason),
2988                        }
2989                    }
2990                }
2991            }
2992            if !ok {
2993                residual_idx.extend(cand.ops.iter().copied());
2994            }
2995        }
2996        if committed_ops == 0 {
2997            return None;
2998        }
2999
3000        let mut out = mesh_from_ptris(&tris, mesh);
3001        // COPLANAR CONSOLIDATION: the conforming per-triangle CDT deliberately
3002        // over-fragments faces (every decomposed host triangle keeps its own
3003        // sub-triangulation, and caps carry their constraint scaffolding). Run
3004        // the SAME coplanar merge the exact kernel and the rect/param fast
3005        // paths apply (i_overlay union per plane bucket + CDT re-triangulation,
3006        // watertight-preserving), collapsing each planar face back to a few
3007        // large triangles — 2-3x fewer output triangles on the advanced_model
3008        // walls, matching the exact kernel's tessellation density.
3009        out = crate::csg::ClippingProcessor::consolidate_coplanar(out);
3010        let consolidated_closure = ClosureVerdict::for_mesh(&out);
3011        if consolidated_closure.closed_enough_to_emit()
3012            && !consolidated_change_is_measurable(mesh, &out)
3013        {
3014            defer(4);
3015            return None;
3016        }
3017        // WATERTIGHT SLIVER REFINEMENT (#1007): the per-triangle CDT can emit a
3018        // high-aspect corner sliver at an opening rim (a far-corner triangle
3019        // fanned to two rim vertices) — the visible roof-slope chamfer the
3020        // exact path also produces and repairs. Run the SAME lockstep-bisection
3021        // pass the exact kernel output gets (targets aspect <= 8:1, watertight
3022        // by construction), so the analytic cut meets the same rim-quality bar.
3023        // The pass fixes one edge per internal round (64 max); the analytic
3024        // cut can carry more rim fans than that, so drive it to a (bounded)
3025        // fixpoint — each iteration is a no-op clone once no sliver remains.
3026        // SELF-CHECKED application, and ONLY when this cut is FINAL (no
3027        // residual): with a residual, the exact kernel still cuts this mesh and
3028        // runs its own sliver refinement on ITS output — refining first would
3029        // hand the arrangement midpoint-split geometry it re-fragments (#1167).
3030        // On a hairline-tolerated output the lockstep split can also see just
3031        // one side of an overlapping chain pair and amplify the sub-grid
3032        // mismatch, and the split slivers can fall below the clean-degenerate
3033        // grid — so accept the refined mesh only when it is STRICTLY closed
3034        // and a cleaned probe stays at least hairline-closed.
3035        if residual_idx.is_empty() && consolidated_closure.is_directed_closed() {
3036            // SCOPED to the committed cutters' padded AABBs: #1007's target is
3037            // the high-aspect corner sliver the CUT emits at an opening rim, and
3038            // every rim-incident sliver touches its cutter's box. The unscoped
3039            // pass also bisected the host's own authored long-thin faces (a thin
3040            // steel wall is legitimately full of >8:1 quads), inflating output
3041            // triangles and paying the full lockstep fixpoint on every
3042            // analytic-cut host (the Holter 4.1.x regression).
3043            //
3044            // Refine straight off `out` first: the scoped pass returns the input
3045            // unchanged when no in-region sliver fires (the common clean-cut
3046            // case), and for a NO-OP fixpoint `refined == out` byte-for-byte, so
3047            // the upfront clone, the probe clean, and the closure audits decided
3048            // nothing. Only a fixpoint that actually split an edge pays the
3049            // probe + self-check audits. Same 6-refine budget as before
3050            // (1 here + up to 5 below). This refinement is not a replacement
3051            // for the router's source-triangle cleanup and does not promise
3052            // hygienic output. The probe informs acceptance; `finish_cut`
3053            // closure-audits and prefers a clean result, but may return its
3054            // explicit compatibility result (#4797).
3055            let mut refined =
3056                crate::facet_weld::refine_high_aspect_slivers_within(&out, &refine_boxes);
3057            if refined.indices.len() != out.indices.len() {
3058                for _ in 0..5 {
3059                    let next =
3060                        crate::facet_weld::refine_high_aspect_slivers_within(&refined, &refine_boxes);
3061                    if next.indices.len() == refined.indices.len() {
3062                        break;
3063                    }
3064                    refined = next;
3065                }
3066                let mut probe = refined.clone();
3067                probe.clean_degenerate();
3068                if directed_closed(&refined) && closed_enough_to_emit(&probe) {
3069                    out = refined;
3070                }
3071            }
3072        }
3073        // The finalizer cleans before the ulp weld so discarded slivers cannot
3074        // choose a surviving seam coordinate, then audits the exact mesh it
3075        // returns. A closed pre-clean result remains the compatibility fallback.
3076        let Some((finished, directed)) = finish_cut_verdict(out, mesh) else {
3077            defer(9);
3078            return None;
3079        };
3080        out = finished;
3081        // A host stored relative to a per-element origin can keep hairline
3082        // seams its world-frame twin does not (#5739): its weld is sized by the
3083        // small stored coordinates. Close them when that leaves the cut
3084        // strictly closed, else keep it. World-frame cuts are untouched.
3085        if mesh.origin != [0.0; 3] && !directed {
3086            if let Some(closed) = tjunction_close::close_seams(&out, mesh) {
3087                out = closed;
3088            }
3089        }
3090
3091        // Residual openings in their original classification order.
3092        residual_idx.sort_unstable();
3093        let residual: Vec<OpeningType> = residual_idx
3094            .iter()
3095            .map(|&i| ctx.merged_openings[i].clone())
3096            .collect();
3097
3098        if !result_volume_within_bounds(mesh, &out, committed_cutter_volume) {
3099            defer(8);
3100            return None;
3101        }
3102        record_cut(committed_ops, residual.len());
3103
3104        let residual_ctx = if residual.is_empty() {
3105            None
3106        } else {
3107            Some(VoidContext {
3108                openings: residual.clone(),
3109                merged_openings: residual,
3110                param: None,
3111                bool2d: None,
3112            })
3113        };
3114        Some((out, residual_ctx))
3115    }
3116}