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;