ifc_lite_geometry/kernel/
mesh_bridge.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//! Bridge between the pure-Rust kernel (which works on `Tri = [[f64;3];3]`) and
6//! ifc-lite's `Mesh` (f32 positions/normals/indices). `subtract`/`union`/
7//! `intersection` here are what the `ClippingProcessor` seam calls.
8
9use super::arrangement::{
10    boolean, boolean_with_conformity, difference_all, difference_all_lenient,
11    difference_all_lenient_with_conformity, BoolOp,
12    Tri,
13};
14use super::signed_volume::{signed_volume6, signed_volume6_about, volume_reference};
15use crate::mesh::Mesh;
16
17/// f32-near-coplanar reconciliation snap grid, in the CALLER's unit — NOT
18/// metres (#2684): 15 µm on the METRE path (`router/voids`), 15 nm on the
19/// FILE-UNIT boolean path. Past |c| = 128 CALLER UNITS the f32 spacing is
20/// itself a multiple of the grid, so every f32 is already on it and the snap
21/// is INERT — 12.8 cm in a millimetre file (so all of it), 128 m in a metre
22/// one. `csg/plane_eps.rs` records the same divergence for the clipper's
23/// floor; `tests/snap_grid_unit_denomination.rs` measures both. A POWER OF TWO
24/// so `(c/G).round()*G` is EXACT f64 ⇒ bit-deterministic across
25/// x86_64/aarch64/wasm. Real IFC is f32-authored, so an intended-flush face is
26/// NOT coplanar after import; the grid is what makes it so.
27///
28/// Canonical definition — `tritri` and `arrangement` size their near-coplanar
29/// bands to the scatter envelope this snap produces, so they import this
30/// constant rather than mirroring it.
31pub(crate) const SNAP_GRID: f64 = 1.0 / 65536.0;
32
33#[inline]
34fn snap(c: f64) -> f64 {
35    (c / SNAP_GRID).round() * SNAP_GRID
36}
37
38// The cross-operand near-coincidence weld lives in `super::plane_weld`: it was
39// split out of this module when #3353 made it a boolean-wide concern rather
40// than a subtraction-only one, and this module was at its size budget.
41use super::plane_weld::{promote_cutter_verts_onto_host_faces, promote_operands_mutually};
42
43/// `Mesh` → the kernel's triangle list (f32 → f64, snapped to the reconcile
44/// grid). Panic-free: an out-of-range index OR a non-finite (NaN/Inf) coord drops
45/// that triangle rather than indexing past the end or crashing
46/// `BigRational::from_float` deep in the predicates (the two empirically-found
47/// reachable panic sites).
48pub fn mesh_to_tris(m: &Mesh) -> Vec<Tri> {
49    let vertex = |i: u32| -> Option<[f64; 3]> {
50        let b = (i as usize) * 3;
51        let c = [
52            *m.positions.get(b)? as f64,
53            *m.positions.get(b + 1)? as f64,
54            *m.positions.get(b + 2)? as f64,
55        ];
56        if !c.iter().all(|v| v.is_finite()) {
57            return None;
58        }
59        Some([snap(c[0]), snap(c[1]), snap(c[2])])
60    };
61    m.indices
62        .chunks_exact(3)
63        .filter_map(|c| Some([vertex(c[0])?, vertex(c[1])?, vertex(c[2])?]))
64        .collect()
65}
66
67/// The triangle's supporting plane in f64: unit normal + offset (`n . v0`),
68/// BEFORE any f32 cast. `(0,0,1,0)`-ish degenerate fallback for a
69/// zero-area triangle, matching `face_normal`'s old `[0,0,1]` default.
70fn face_plane(t: &Tri) -> ([f64; 3], f64) {
71    let e1 = [t[1][0] - t[0][0], t[1][1] - t[0][1], t[1][2] - t[0][2]];
72    let e2 = [t[2][0] - t[0][0], t[2][1] - t[0][1], t[2][2] - t[0][2]];
73    let n = [
74        e1[1] * e2[2] - e1[2] * e2[1],
75        e1[2] * e2[0] - e1[0] * e2[2],
76        e1[0] * e2[1] - e1[1] * e2[0],
77    ];
78    let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
79    if len > 0.0 {
80        let n = [n[0] / len, n[1] / len, n[2] / len];
81        let d = n[0] * t[0][0] + n[1] * t[0][1] + n[2] * t[0][2];
82        (n, d)
83    } else {
84        ([0.0, 0.0, 1.0], t[0][2])
85    }
86}
87
88/// The kernel's triangle list → a `Mesh` (per-face flat normals, f64 → f32).
89///
90/// Also tags every output triangle with its f64 supporting plane
91/// ([`PlaneTag`], issue #3914) — the exact plane this function's own f64
92/// arithmetic derived, before the `positions`/`normals` f32 cast below loses
93/// precision. Union output carries the tags through this entry point;
94/// difference/intersection output uses [`tris_to_mesh_without_plane_tags`]
95/// because its consolidated result can become another boolean operand (#5012).
96pub fn tris_to_mesh(tris: &[Tri]) -> Mesh {
97    tris_to_mesh_impl(tris, true)
98}
99
100fn tris_to_mesh_without_plane_tags(tris: &[Tri]) -> Mesh {
101    tris_to_mesh_impl(tris, false)
102}
103
104fn tris_to_mesh_impl(tris: &[Tri], carry_plane_tags: bool) -> Mesh {
105    use crate::mesh::PlaneTag;
106    let mut m = Mesh::with_capacity(tris.len() * 3, tris.len() * 3);
107    let mut tags = Vec::with_capacity(tris.len());
108    for t in tris {
109        let (n64, d64) = face_plane(t);
110        let n = [n64[0] as f32, n64[1] as f32, n64[2] as f32];
111        let base = (m.positions.len() / 3) as u32;
112        for p in t {
113            m.positions
114                .extend_from_slice(&[p[0] as f32, p[1] as f32, p[2] as f32]);
115            m.normals.extend_from_slice(&n);
116        }
117        m.indices.extend_from_slice(&[base, base + 1, base + 2]);
118        tags.push(PlaneTag { n: n64, d: d64 });
119    }
120    if carry_plane_tags {
121        m.plane_tags = Some(tags);
122    }
123    m
124}
125
126/// Orient a closed operand OUTWARD before it enters the arrangement.
127///
128/// The kernel boolean (`boolean_vids` / `union_all`) derives its keep/flip rules
129/// from the OUTWARD-normal convention (own-solid on `−n`; the difference flips the
130/// kept B faces so their caps seam with A). Real IFC winding is NOT reliably
131/// outward — a CW profile extruded along `+Z`, or a faceted brep with inconsistent
132/// face loops, yields an INWARD-wound (negative-signed-volume) closed solid. Fed
133/// in as-is it tears the result: open boundary edges along the cut rim + an
134/// inverted-volume surface (the 1007 gable-wall slivers; #1007 defect A).
135///
136/// We flip winding (`[a,b,c] → [a,c,b]`, an EXACT index swap) iff the signed
137/// volume is negative, so every operand the kernel sees is outward. The flip is a
138/// no-op for already-outward inputs (every pinned box−box manifest: `cube_mesh`
139/// has volume `+8`/`+27`), so determinism manifests are unperturbed.
140pub(crate) fn orient_outward(mut tris: Vec<Tri>) -> Vec<Tri> {
141    if signed_volume6(&tris) < 0.0 {
142        for t in &mut tris {
143            t.swap(1, 2);
144        }
145    }
146    tris
147}
148
149/// `host − cutter` as a `Mesh`.
150pub fn subtract(host: &Mesh, cutter: &Mesh) -> Mesh {
151    subtract_with_change(host, cutter).0
152}
153
154/// Like [`subtract`], but also returns the classifier's `changed` bit (#4692):
155/// `false` means no host sub-triangle was dropped and no cutter face was kept,
156/// so the triangles are the host re-tessellated, not a cut. [`subtract_many`]
157/// reads the same bit.
158///
159/// A one-component [`difference_all_lenient`] is the binary
160/// `boolean(.., Difference)`: the same arrangement over the same operands,
161/// classified through the same one-component `BComponents`, and neither gates
162/// on conformity. The third element says whether the arrangement conformed.
163pub(crate) fn subtract_with_change(host: &Mesh, cutter: &Mesh) -> (Mesh, bool, bool) {
164    #[cfg(feature = "csg_capture")]
165    crate::csg_capture::record_single(host, cutter);
166    let h = orient_outward(mesh_to_tris(host));
167    let mut c = mesh_to_tris(cutter);
168    promote_cutter_verts_onto_host_faces(&mut c, &h);
169    let c = orient_outward(c);
170    let (tris, changed, conforming) = difference_all_lenient_with_conformity(&h, &[&c]);
171    (tris_to_mesh_without_plane_tags(&tris), changed, conforming)
172}
173
174/// What [`subtract_many`] made of a cutter group.
175#[must_use]
176#[derive(Debug, Clone)]
177pub enum BatchSubtract {
178    /// The arrangement conformed (or its lenient batch passed the volume
179    /// oracle) and at least one cutter reaches the host solid. Empty when the
180    /// cutters engulf the host.
181    Cut(Mesh),
182    /// The arrangement conformed but no cutter reaches the host solid: every
183    /// host sub-triangle was kept and no cutter face was. The host is unchanged.
184    Unchanged,
185    /// No trustworthy arrangement: an unrecovered constraint remained and the
186    /// lenient batch failed the volume oracle, or the oracle itself tripped the
187    /// budget. The caller falls back to sequential per-cutter subtraction.
188    Nonconforming,
189}
190
191impl BatchSubtract {
192    /// `Cut` when the classifier changed the host, `Unchanged` otherwise.
193    fn classified(tris: &[Tri], changed: bool) -> Self {
194        if changed {
195            Self::Cut(tris_to_mesh_without_plane_tags(tris))
196        } else {
197            Self::Unchanged
198        }
199    }
200}
201
202/// `host − (∪ cutters)` as a `Mesh` — the batched void-group subtract.
203///
204/// The cutters MUST be pairwise disjoint (the router groups by snap-band-
205/// inflated AABBs) and each per-component watertight. Every component is
206/// promoted onto the host faces and oriented outward INDIVIDUALLY — the global
207/// signed-volume orientation of [`subtract`] cannot fix mixed per-component
208/// winding of a multi-component operand (the #2176 lesson) — then the whole
209/// group is subtracted in ONE arrangement (`difference_all_volume_safe`), so
210/// there is no per-cutter f64→f32→snap round-trip to re-jitter and re-crack the
211/// previous cut's seams. Component order is the caller's (deterministic).
212/// See [`BatchSubtract`] for the three outcomes.
213pub fn subtract_many(host: &Mesh, cutters: &[&Mesh]) -> BatchSubtract {
214    subtract_many_with_conformity(host, cutters, false).0
215}
216
217/// Keep the provenance of an unchanged batch for the void router (#6516).
218/// A volume-checked nonconforming miss must retain the sequential fallback.
219pub(crate) fn subtract_many_with_conformity(
220    host: &Mesh, cutters: &[&Mesh], retain_conforming_miss: bool,
221) -> (BatchSubtract, bool, Option<Mesh>) {
222    #[cfg(feature = "csg_capture")]
223    crate::csg_capture::record_many(host, cutters);
224    let h = orient_outward(mesh_to_tris(host));
225    let comp_tris: Vec<Vec<Tri>> = cutters
226        .iter()
227        .map(|m| {
228            let mut c = mesh_to_tris(m);
229            promote_cutter_verts_onto_host_faces(&mut c, &h);
230            orient_outward(c)
231        })
232        .collect();
233    let refs: Vec<&[Tri]> = comp_tris.iter().map(|c| c.as_slice()).collect();
234    // Conforming batch: the fast, exact, byte-identical common path.
235    if let Some((r, changed)) = difference_all(&h, &refs) {
236        #[cfg(feature = "opening-perf-trace")]
237        crate::opening_perf_trace::record(|c| {
238            c.batch_conforming_arrangements = c.batch_conforming_arrangements.saturating_add(1);
239            c.batch_conforming_misses = c.batch_conforming_misses.saturating_add(u64::from(!changed));
240        });
241        // Only the private void-group caller requests this: an unchanged
242        // arrangement still carries useful seams for topology consolidation.
243        let miss = (retain_conforming_miss && !changed)
244            .then(|| tris_to_mesh_without_plane_tags(&r));
245        return (BatchSubtract::classified(&r, changed), true, miss);
246    }
247    #[cfg(feature = "opening-perf-trace")]
248    crate::opening_perf_trace::record(|c| c.batch_nonconforming_arrangements = c.batch_nonconforming_arrangements.saturating_add(1));
249    // Non-conforming batch (an unrecovered constraint remains after the robust
250    // traversal recovery). Its exact topology is CLEANER than sequential per-cutter
251    // re-jitter on dense faceted-reveal walls (issue #098 V5C: 532→108 open edges),
252    // but a straddling misclassification can over/under-cut VOLUME (#559171/#1167).
253    // Trust the lenient batch ONLY when its removed volume matches the true removed
254    // volume; else Nonconforming, so the caller runs its full sequential path.
255    let (batch, changed) = difference_all_lenient(&h, &refs);
256    // ORACLE for the volume comparison (#1788): batched cutters are pairwise
257    // disjoint (this fn's contract), so the TRUE removed volume is
258    // Σ |host ∩ cutterᵢ| — each a small single boolean against the PRISTINE
259    // host, the well-conditioned regime. The previous oracle re-ran the
260    // sequential subtract chain, but that chain re-jitters its own seams
261    // cut-over-cut and UNDER-cuts on multi-void walls — on the ISSUE_098
262    // Poroton wall its "reference" removed 2% less volume than the (correct)
263    // batch, so a perfect batch was rejected in favour of the broken
264    // sequential fallback, leaving an opening uncut (T6 fail:opening-not-cut).
265    // Budget snapshot/restore: oracle work isn't charged to the caller's
266    // batch budget (codex P2 on #1660); a trip DURING an oracle intersection
267    // makes its volume untrustworthy ⇒ reject the group (as before).
268    let budget_snap = super::budget::snapshot_counters();
269    let mut inter_sum = 0.0f64;
270    let mut oracle_tripped = false;
271    for c in &comp_tris {
272        super::budget::begin();
273        let i = boolean(&h, c, BoolOp::Intersection);
274        if super::budget::tripped() {
275            oracle_tripped = true;
276            break;
277        }
278        inter_sum += signed_volume6(&i).abs();
279    }
280    super::budget::restore_counters(budget_snap);
281    if oracle_tripped {
282        return (BatchSubtract::Nonconforming, false, None);
283    }
284    // Both readings about the HOST's reference point (#4693). A host can arrive
285    // open (the router does not require a closed host), and an open surface's
286    // sum moves with the reference point: read the batch about its own centre
287    // and a cut that trims the host's bounding box leaves the untouched crack's
288    // flux in `batch_removed`.
289    let reference = volume_reference(&h);
290    let host_v = signed_volume6_about(&h, &reference).abs();
291    let batch_removed = host_v - signed_volume6_about(&batch, &reference).abs();
292    // 1% agreement — above f64/FMA noise (parity-stable branch), tight enough to
293    // reject the #1167 gross under-cut (3.7 m³ vs 13 m³).
294    let tol = inter_sum.abs().max(1.0e-9) * 0.01;
295    if (batch_removed - inter_sum).abs() <= tol {
296        #[cfg(feature = "opening-perf-trace")]
297        crate::opening_perf_trace::record(|c| c.batch_volume_checked_misses = c.batch_volume_checked_misses.saturating_add(u64::from(!changed)));
298        (BatchSubtract::classified(&batch, changed), false, None)
299    } else {
300        (BatchSubtract::Nonconforming, false, None)
301    }
302}
303
304/// `a ∪ b` as a `Mesh`.
305pub fn union(a: &Mesh, b: &Mesh) -> Mesh {
306    union_with_conformity(a, b).0
307}
308
309/// Like [`union`], but also reports arrangement conformity (`unrecovered == 0`).
310/// Diagnostics only (#3353); `union()` computes and discards this same signal.
311pub fn union_with_conformity(a: &Mesh, b: &Mesh) -> (Mesh, bool) {
312    // #1109 budget, as `subtract` does: fresh PER-BOOLEAN count, per-ELEMENT accumulator
313    // intact — `begin()` resets only the per-op counter, so a union inside an over-budget
314    // element STILL trips (see `budget::begin`). Without it a union after a tripped
315    // subtract starts tripped and `arrange` bails at its first pair.
316    super::budget::begin();
317    // Reconcile the two operands' near-coplanar faces onto shared planes BEFORE
318    // the arrangement (#3353). Mutual, not cutter-onto-host: union has no host,
319    // and the one-directional form left `union_mesh(b, a)` tearing on the same
320    // fixture `union_mesh(a, b)` handled. See `promote_operands_mutually`.
321    let mut operands = [mesh_to_tris(a), mesh_to_tris(b)];
322    promote_operands_mutually(&mut operands);
323    let [ta, tb] = operands;
324    let (a, b) = (orient_outward(ta), orient_outward(tb));
325    let (out, conforming) = boolean_with_conformity(&a, &b, BoolOp::Union);
326    // On a trip `out` is PARTIAL: discard it, return empty — the graceful fallback callers
327    // handle (`csg::union_mesh` merges plainly; #960 goes sequential), never a poisoned
328    // mesh. The trip point is a pure function of the snapped operands, so wasm agrees.
329    if super::budget::tripped() {
330        return (Mesh::new(), conforming);
331    }
332    (tris_to_mesh(&out), conforming)
333}
334
335#[path = "nary_union.rs"]
336mod nary_union;
337pub use nary_union::union_many;
338pub(crate) use nary_union::union_many_preserving_coordinates;
339
340/// `a ∩ b` as the kernel's own exact f64 triangles, WITHOUT the `Mesh` round-trip.
341///
342/// `Mesh` stores positions as `f32`, so `intersection` below loses ~1e-7 of
343/// relative precision on the way out. That is irrelevant for a render buffer and
344/// very relevant for anything that measures the result: on the analytic
345/// rotated-box oracle the f64 triangles give exactly 9.375 m³ where the `Mesh`
346/// round-trip gives 9.374999882. Callers that report a VOLUME (the clash
347/// intersection solid, `crate::clash_solid`) take this entry; callers that only
348/// need triangles to draw take `intersection`.
349///
350/// Empty on a #1109 budget trip, exactly like `intersection`.
351pub fn intersection_tris(a: &Mesh, b: &Mesh) -> Vec<Tri> {
352    // Participate in the #1109 budget like `subtract` / `union` — fresh per-boolean
353    // count, per-element accumulator preserved (see `union`).
354    super::budget::begin();
355    let a = orient_outward(mesh_to_tris(a));
356    let b = orient_outward(mesh_to_tris(b));
357    let out = boolean(&a, &b, BoolOp::Intersection);
358    // On a budget trip `arrange` bailed and `out` is a PARTIAL arrangement. Return
359    // empty — `csg::intersection_mesh` treats empty as the graceful (disjoint-like)
360    // degrade rather than consuming a poisoned partial intersection.
361    if super::budget::tripped() {
362        return Vec::new();
363    }
364    out
365}
366
367/// `a ∩ b` as a `Mesh`.
368pub fn intersection(a: &Mesh, b: &Mesh) -> Mesh {
369    tris_to_mesh_without_plane_tags(&intersection_tris(a, b))
370}
371
372#[cfg(test)]
373#[path = "mesh_bridge_tests.rs"]
374mod tests;
375
376#[cfg(test)]
377#[path = "issue_3913_sweep_tests.rs"]
378mod issue_3913_sweep_tests;