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}