ifc_lite_geometry/kernel/
nary_union.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//! Shared N-ary arrangement and an explicit coordinate-preserving candidate.
6
7use super::{mesh_to_tris, orient_outward, tris_to_mesh};
8use crate::csg::ClippingProcessor;
9use crate::kernel::{
10    arrangement::{union_all, Tri},
11    budget,
12    plane_weld::promote_operands_mutually,
13};
14use crate::router::voids::prism_cut::closure_checks::directed_closed;
15use crate::Mesh;
16
17/// Retain the existing mutually reconciled N-ary union. Cutter consumers
18/// validate this result before deciding whether another candidate is needed.
19pub fn union_many(meshes: &[&Mesh]) -> Mesh {
20    arrange(meshes, true)
21}
22
23/// Omit cross-operand promotion, preserving the supplied coordinates apart
24/// from the kernel's existing snap and f32 emission. Used only by consumers
25/// that validate the union through their actual subtraction (#3925).
26pub(crate) fn union_many_preserving_coordinates(meshes: &[&Mesh]) -> Mesh {
27    arrange(meshes, false)
28}
29
30/// #3917: only a 3-operand union has a measured order-dependence (the
31/// #3916-committed 882-configuration sweep is 7x7 corners x 3 `dz` x 6
32/// orderings of exactly 3 operands; #3917's traced mechanism is specific to
33/// `promote_operands_mutually` welding a THIRD operand against two already
34/// reconciled with each other). Retrying every ordering is only attempted at
35/// this exact N, so a 2- or 4+-operand union is completely unaffected by this
36/// function existing.
37const REORDER_SEARCH_OPERAND_COUNT: usize = 3;
38
39/// #3917: cap the retry search to operand sets this small (total triangles
40/// across all 3 operands), so a large real N-ary cutter union never pays for
41/// up to 5 extra `arrange` attempts. `1_536` is generously above the 12
42/// triangles-per-box, 36-triangle-total pinned #3913/#3917 fixture this
43/// mechanism targets (simple box/prism cutters), while still bounding the
44/// worst case: a large union already costs more than this search is worth,
45/// and #3917's own measurements never exercised operands anywhere near this
46/// size.
47const MAX_REORDER_SEARCH_TRIS: usize = 1_536;
48
49/// All 6 permutations of 3 array POSITIONS, applied to whatever order the
50/// caller supplied. Because permuting a caller's own 3-element array through
51/// every bijection of its OWN positions reaches every arrangement of the 3
52/// physical operands regardless of what the caller's original order was
53/// (permutation composition over a fixed 3-element set is the same group
54/// whichever element you start from), this set is the caller-order-agnostic
55/// version of `kernel::issue_3913_sweep_tests::ORDERINGS` — same 6 orderings,
56/// expressed relative to the input array rather than to a fixed physical
57/// labelling.
58const REORDER_POSITIONS: [[usize; 3]; 6] = [
59    [0, 1, 2],
60    [0, 2, 1],
61    [1, 0, 2],
62    [1, 2, 0],
63    [2, 0, 1],
64    [2, 1, 0],
65];
66
67/// #3917: whether `mesh`, taken through the SAME `consolidate_coplanar` pass
68/// every real `union_many` caller already applies (`mesh_bridge_tests`'s own
69/// module doc names `consolidate_coplanar(union_many(..))` as the standing
70/// combination; `coaxial_union3d.rs` and `coaxial_union.rs` both call it
71/// immediately after `union_many`), comes back closed.
72///
73/// This is NOT redundant with checking `mesh` itself: #3914's investigation
74/// on this exact code path measured a raw `union_many` output that was
75/// already closed (0 unmatched edges) and had `consolidate_coplanar`
76/// introduce the tear — plane-bucket boundary straddle, a different
77/// mechanism to this issue's, but the SAME lesson: whether a candidate
78/// ordering is actually good is only decidable after the same
79/// post-processing a real caller runs, not on the raw kernel output alone.
80///
81/// SCALE (#4744): this runs `consolidate_coplanar` at the metre default, while a
82/// FILE-UNIT caller (`processors/boolean`) consolidates at the model's length
83/// unit, so on a millimetre file the ring-noise width gate is inert here and
84/// live there. The disagreement can only pick a worse ORDERING — production
85/// still consolidates at the caller's scale, and its accept gates still run — so
86/// it is recorded rather than fixed in the PR that made the gate physical.
87fn survives_consolidation_closed(mesh: &Mesh) -> bool {
88    #[cfg(feature = "opening-perf-trace")]
89    crate::opening_perf_trace::record(|c| c.union_closure_checks = c.union_closure_checks.saturating_add(1));
90    if mesh.is_empty() {
91        return false;
92    }
93    let consolidated = ClippingProcessor::consolidate_coplanar(mesh.clone());
94    let closed = !consolidated.is_empty() && directed_closed(&consolidated);
95    #[cfg(feature = "opening-perf-trace")]
96    crate::opening_perf_trace::record(|c| c.union_closure_passes = c.union_closure_passes.saturating_add(u64::from(closed)));
97    closed
98}
99
100fn arrange(meshes: &[&Mesh], reconcile: bool) -> Mesh {
101    #[cfg(feature = "opening-perf-trace")]
102    crate::opening_perf_trace::record(|c| {
103        if reconcile { c.union_calls_by_arity[meshes.len().min(4)] = c.union_calls_by_arity[meshes.len().min(4)].saturating_add(1); }
104        else { c.coordinate_preserving_unions = c.coordinate_preserving_unions.saturating_add(1); }
105    });
106    // One public union is one budgeted boolean operation. Retries below are
107    // candidates for that same operation, so they must share its counter.
108    budget::begin();
109    if budget::tripped() {
110        return Mesh::new();
111    }
112    let out = arrange_once(meshes, reconcile);
113    if budget::tripped() {
114        #[cfg(feature = "opening-perf-trace")]
115        crate::opening_perf_trace::record(|c| c.union_initial_budget_trips = c.union_initial_budget_trips.saturating_add(1));
116        return out;
117    }
118    if !reconcile || meshes.len() != REORDER_SEARCH_OPERAND_COUNT {
119        return out;
120    }
121    let total_tris: usize = meshes.iter().map(|m| m.indices.len() / 3).sum();
122    if total_tris > MAX_REORDER_SEARCH_TRIS {
123        #[cfg(feature = "opening-perf-trace")]
124        crate::opening_perf_trace::record(|c| c.union_triangle_cap_exclusions = c.union_triangle_cap_exclusions.saturating_add(1));
125        return out;
126    }
127    #[cfg(feature = "opening-perf-trace")]
128    crate::opening_perf_trace::record(|c| c.union_retry_eligible = c.union_retry_eligible.saturating_add(1));
129    // #3917: `promote_operands_mutually` walks operands in ARRAY order, and
130    // which operand is welded first decides which pair of planes reconcile
131    // before the third is touched (see that function's doc) — so which
132    // physical operand the CALLER placed at index 0 can be the difference
133    // between a closed union and a torn one, even though a closed result is
134    // reachable for the same 3 physical operands under a different order.
135    // Exhaustive measurement on issue #3917 ruled out replacing the walk
136    // order with any single geometric sort key (position, centroid,
137    // AABB-overlap, degree, centroid distance): every key tried either fails
138    // to reproduce the ordering that reconciles a given configuration, or
139    // reaches determinism only by giving up quality the current order
140    // occasionally achieves. So this does not try to PREDICT the right
141    // order — it recognises the wrong one the same way #3440's prism-cut
142    // self-check already does in production (`directed_closed`, the
143    // DIRECTED quantized closed-surface audit — strictly stronger than an
144    // undirected 2-manifold check), and only then retries.
145    //
146    // Left untouched when `out` is already closed: this changes NOTHING for
147    // a configuration this code already gets right (the #3916 sweep's other
148    // 746 of 882 configurations included), so it carries none of the
149    // triangulation-invariance / golden-fixture risk a change that always
150    // re-picks among candidates would.
151    if survives_consolidation_closed(&out) {
152        return out;
153    }
154    for order in REORDER_POSITIONS.into_iter().skip(1) {
155        #[cfg(feature = "opening-perf-trace")]
156        crate::opening_perf_trace::record(|c| c.union_retries = c.union_retries.saturating_add(1));
157        let permuted = [meshes[order[0]], meshes[order[1]], meshes[order[2]]];
158        let candidate = arrange_once(&permuted, reconcile);
159        if survives_consolidation_closed(&candidate) {
160            #[cfg(feature = "opening-perf-trace")]
161            crate::opening_perf_trace::record(|c| c.union_retry_successes = c.union_retry_successes.saturating_add(1));
162            return candidate;
163        }
164    }
165    #[cfg(feature = "opening-perf-trace")]
166    crate::opening_perf_trace::record(|c| c.union_retries_exhausted = c.union_retries_exhausted.saturating_add(1));
167    // No ordering of these 3 operands reconciles: the tear is not an
168    // ordering artefact (#3917's own diagnosis found orderings that DO
169    // reconcile this issue's pinned fixtures; a configuration where none of
170    // the 6 does is outside what this function can fix). Return the
171    // caller's own order, unchanged, exactly as before this function
172    // existed.
173    out
174}
175
176fn arrange_once(meshes: &[&Mesh], reconcile: bool) -> Mesh {
177    #[cfg(feature = "opening-perf-trace")]
178    crate::opening_perf_trace::record(|c| {
179        c.union_arrangements = c.union_arrangements.saturating_add(1);
180        c.union_operand_triangles = c.union_operand_triangles.saturating_add(meshes.iter().fold(0u64, |n, m| n.saturating_add(m.triangle_count() as u64)));
181    });
182    if budget::tripped() {
183        return Mesh::new();
184    }
185    let mut operands: Vec<Vec<Tri>> = meshes.iter().map(|m| mesh_to_tris(m)).collect();
186    if reconcile {
187        promote_operands_mutually(&mut operands);
188    }
189    let operands: Vec<Vec<Tri>> = operands.into_iter().map(orient_outward).collect();
190    let refs: Vec<&[Tri]> = operands.iter().map(Vec::as_slice).collect();
191    let (out, _) = union_all(&refs);
192    if budget::tripped() {
193        Mesh::new()
194    } else {
195        tris_to_mesh(&out)
196    }
197}
198
199#[cfg(test)]
200#[path = "nary_union_tests.rs"]
201mod tests;