ifc_lite_geometry/csg/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//! The two union entry points, moved out of `csg/mod.rs` (which sits at its
6//! module-size budget) so the pair-vs-batch split the #3440 audit needs is
7//! visible in one place: `union_pair` does the work and records nothing,
8//! `union_mesh` and `union_meshes` each audit — and, under `csg_topology_gate`
9//! (step 2), gate — exactly the mesh they return, and only when a boolean
10//! actually ran: an empty operand short-circuits both of them past the audit.
11
12use super::{record_csg_op, ClippingProcessor};
13use crate::diagnostics::{BoolFailureReason, BoolOp};
14use crate::error::Result;
15use crate::mesh::Mesh;
16
17/// What [`ClippingProcessor::union_pair`] handed back, so its callers can tell
18/// a boolean RESULT from a mesh no boolean produced.
19///
20/// The #3440 audit and both accept gates speak about kernel output. Three
21/// different things come out of `union_pair` looking like a union: an operand
22/// passed through because the other was empty, the plain merge the
23/// kernel-failure path returns (already recorded as `KernelOutputInvalid`),
24/// and an actual kernel result. Only the last is a boolean's work, and the
25/// returned `Mesh` alone cannot say which it is.
26enum UnionOutcome {
27 /// The exact kernel produced this mesh and it passed `validate_mesh`. The
28 /// audit and the gates speak about it.
29 Kernel(Mesh),
30 /// No boolean produced this mesh: an operand handed straight back, or the
31 /// merge that replaced a discarded kernel result. Not audited, not gated.
32 PassThrough(Mesh),
33}
34
35impl ClippingProcessor {
36 /// Union two meshes together using CSG boolean operations on the
37 /// pure-Rust exact kernel.
38 ///
39 /// Empty operands are handled silently — they have a unique correct answer.
40 /// "Silently" includes the #3440 audit, and so does the kernel-failure
41 /// merge: both come back as [`UnionOutcome::PassThrough`], which this does
42 /// not audit. Auditing a mesh no boolean produced blames this union for
43 /// topology it did not create, and under `csg_manifold_gate` that lands a
44 /// spurious `NonManifoldRejected` in both the public failure list and the
45 /// census the flip decision is read off — on top of the
46 /// `KernelOutputInvalid` the same incident already recorded.
47 pub fn union_mesh(&self, mesh_a: &Mesh, mesh_b: &Mesh) -> Result<Mesh> {
48 match self.union_pair(mesh_a, mesh_b)? {
49 UnionOutcome::PassThrough(mesh) => Ok(mesh),
50 UnionOutcome::Kernel(mesh) => Ok(self.audit_and_gate_union(mesh, || {
51 let mut merged = mesh_a.clone();
52 merged.merge(mesh_b);
53 merged
54 })),
55 }
56 }
57
58 /// [`Self::union_mesh`] minus the #3440 audit, so `union_meshes` can drive
59 /// it in a loop and audit only the mesh it hands back.
60 ///
61 /// Returns WHICH of the two kinds of mesh it produced, because that is the
62 /// fact the audit turns on and this is the only place that knows it. The
63 /// caller cannot re-derive it: "was an operand empty" misses the
64 /// kernel-failure merge, and inspecting the returned mesh cannot tell a
65 /// merge from a kernel result at all.
66 fn union_pair(&self, mesh_a: &Mesh, mesh_b: &Mesh) -> Result<UnionOutcome> {
67 record_csg_op(1, mesh_a.triangle_count(), mesh_b.triangle_count());
68 if mesh_a.is_empty() {
69 return Ok(UnionOutcome::PassThrough(mesh_b.clone()));
70 }
71 if mesh_b.is_empty() {
72 return Ok(UnionOutcome::PassThrough(mesh_a.clone()));
73 }
74
75 // Pure-Rust exact kernel. On an empty/invalid kernel result
76 // fall back to a plain merge (overlap not removed) + record the failure,
77 // preserving the legacy never-Err contract.
78 let raw_u = crate::kernel::mesh_bridge::union(mesh_a, mesh_b);
79 let result = self.consolidate(raw_u);
80 if result.is_empty() || !self.validate_mesh(&result) {
81 self.record_failure(BoolOp::Union, BoolFailureReason::KernelOutputInvalid);
82 let mut merged = mesh_a.clone();
83 merged.merge(mesh_b);
84 return Ok(UnionOutcome::PassThrough(merged));
85 }
86 Ok(UnionOutcome::Kernel(result))
87 }
88
89 /// Union multiple meshes together
90 ///
91 /// Convenience method that sequentially unions all non-empty meshes.
92 /// Skips empty meshes to avoid unnecessary CSG operations. The #3440
93 /// audit runs ONCE, on the mesh handed back — driving `union_mesh` here
94 /// would record a tear per intermediate the caller never sees.
95 pub fn union_meshes(&self, meshes: &[Mesh]) -> Result<Mesh> {
96 if meshes.is_empty() {
97 return Ok(Mesh::new());
98 }
99 if meshes.len() == 1 {
100 return Ok(meshes[0].clone());
101 }
102 // Start with first non-empty mesh
103 let mut result = Mesh::new();
104 let mut found_first = false;
105 // Did a BOOLEAN produce the mesh this is about to hand back? Only the
106 // LAST fold step decides that: an earlier merge fallback is an operand
107 // to the step after it, and that step's kernel output is a kernel
108 // output. `false` also covers "no pair ever met", where `result` is a
109 // pass-through clone of the caller's own mesh.
110 let mut returned_by_kernel = false;
111 for mesh in meshes {
112 if mesh.is_empty() {
113 continue;
114 }
115 if !found_first {
116 result = mesh.clone();
117 found_first = true;
118 continue;
119 }
120 match self.union_pair(&result, mesh)? {
121 UnionOutcome::Kernel(m) => {
122 result = m;
123 returned_by_kernel = true;
124 }
125 UnionOutcome::PassThrough(m) => {
126 result = m;
127 returned_by_kernel = false;
128 }
129 }
130 }
131 // Audit only a mesh a boolean actually produced. Blaming a union that
132 // never ran — or one whose result was already discarded and recorded as
133 // `KernelOutputInvalid` — for the topology of the merge that replaced it
134 // is the same noise as auditing an intermediate the caller never sees.
135 //
136 // Under either gate feature, a rejected fold has no single operand
137 // pair to fall back to — `result` folded however many meshes ran —
138 // so the fallback is the plain merge of every non-empty input, the
139 // N-way generalisation of `union_pair`'s own 2-way fallback above.
140 if returned_by_kernel {
141 result = self.audit_and_gate_union(result, || {
142 let mut merged = Mesh::new();
143 for mesh in meshes.iter().filter(|m| !m.is_empty()) {
144 merged.merge(mesh);
145 }
146 merged
147 });
148 }
149 Ok(result)
150 }
151
152 /// Both callers reach this only with a [`UnionOutcome::Kernel`] mesh, which
153 /// `union_pair` has already run `validate_mesh` over. The check is repeated
154 /// anyway rather than asserted away: the closure predicates index
155 /// `positions` straight through `indices`, so an out-of-bounds index here
156 /// would abort the process rather than be recorded, and that is too sharp
157 /// an edge to leave resting on a caller's discipline. It is diagnostic-only
158 /// either way — a mesh that failed it is still the legacy return value.
159 ///
160 /// #3440 steps 2 and 3: also the ONE place union gates. Each gate inside
161 /// `accept_gates_reject` records on its own hit, so when one does this
162 /// returns `fallback()` WITHOUT also calling `record_topology_tear` —
163 /// recording both would double-count the same tear as two unrelated
164 /// incidents. Without either gate feature `accept_gates_reject` is the
165 /// zero-cost always-`false` stub, so this is exactly the step-1 behaviour
166 /// above, unchanged.
167 fn audit_and_gate_union(&self, mesh: Mesh, fallback: impl FnOnce() -> Mesh) -> Mesh {
168 if !self.validate_mesh(&mesh) {
169 return mesh;
170 }
171 if self.accept_gates_reject(BoolOp::Union, &mesh) {
172 return fallback();
173 }
174 self.record_topology_tear(BoolOp::Union, &mesh);
175 mesh
176 }
177}