manifold_rust/robust/mod.rs
1// robust/mod.rs — Robust boolean engine for general (possibly non-manifold)
2// closed, orientable triangle meshes.
3//
4// Implements Barki, Guennebaud, Foufou 2015, "Exact, robust, and efficient
5// regularized Booleans on general 3D meshes" (docs/Exact, robust, and
6// efficient booleans.pdf). This engine is a parallel alternative to the
7// ported exact pipeline in src/boolean3.rs: it requires inputs only to be
8// geometrically closed and orientable (triangle soup is fine — connectivity
9// is never trusted), at the cost of exact rational arithmetic on the hard
10// predicate/construction cases.
11//
12// Selection between the two engines is via `types::BooleanEngine`
13// (Exact | Robust | Auto); the exact engine remains the default and its
14// behavior is byte-identical to before this module existed.
15//
16// Submodules (pipeline order):
17// exact — rational points, filtered predicates, constructions
18// tri_tri — exact triangle-triangle intersection (narrow phase)
19// arrangement — per-triangle 2D arrangement of intersection prims
20// cdt — exact constrained Delaunay triangulation
21// intersection_graph — broad phase, prim distribution, piece emission
22// (split helpers: graph_types — edge keys, vertex
23// interner, Piece/IntersectionGraph; graph_geom —
24// boxes, clips, filtered on-segment tests;
25// graph_self_cut — same-mesh narrow phase)
26// cells — arrangement cell complex + winding propagation
27// (cells_extract — containment predicate + boundary
28// extraction, re-exported through `cells`)
29// ray_shoot — exact winding numbers (residual component seeds)
30// soup — triangle-soup import (closed/orientable validation)
31//
32// Classification follows the mesh-arrangement formulation (Zhou, Grinspun,
33// Zorin, Jacobson 2016 — what libigl's mesh_boolean uses), which subsumes
34// both the paper's local Prop 2/3 ring walk and the per-component winding
35// queries this engine used before. The arrangement's cells carry a winding
36// number per operand, propagated combinatorially from the unbounded cell, so
37// adjacent regions cannot disagree; each operand's solid is {w ≥ 1} by
38// default, so a negative winding (an inverted region of a self-intersecting
39// scan) is never material. `types::WindingRule::Nonzero` switches that
40// predicate to {w ≠ 0} per call, which keeps inside-out geometry solid; the
41// winding numbers themselves never depend on the rule. The output keeps a
42// wall exactly where the operation's predicate
43// differs across it, wound from the cell labels rather than from the input
44// face — which is what makes the result closed and consistently oriented no
45// matter how the input was wound.
46//
47// The paper's explicit regularization pass — radial ring cancellation and
48// coincident-piece binding — has no counterpart here: thin material cancels
49// arithmetically in the winding sum, so there is nothing to discard up
50// front.
51
52pub mod arrangement;
53pub mod assemble;
54pub mod cdt;
55pub mod cells;
56pub mod cells_extract;
57pub mod exact;
58mod graph_geom;
59mod graph_self_cut;
60mod graph_types;
61pub mod intersection_graph;
62pub mod pairing;
63pub mod ray_shoot;
64pub mod repair;
65pub mod soup;
66pub mod tri_tri;
67
68use crate::cancel::CancelToken;
69use crate::impl_mesh::ManifoldImpl;
70use crate::linalg::Vec3;
71use crate::types::{Error, OpType, WindingRule};
72
73
74
75
76
77fn is_cancelled(token: Option<&CancelToken>) -> bool {
78 token.is_some_and(|t| t.is_cancelled())
79}
80
81fn cancelled_impl() -> ManifoldImpl {
82 let mut out = ManifoldImpl::new();
83 out.make_empty(Error::Cancelled);
84 out
85}
86
87/// Can this operand be handed to a bbox-disjoint fast path verbatim — that
88/// is, is its surface already exactly the boundary of the solid it denotes,
89/// for either winding rule? Two conditions, both exact and both conservative:
90///
91/// * no self-intersections, so no piece of the surface is interior to its own
92/// body (a doubled or crossing sheet has walls the pipeline dissolves);
93/// * every shell wound the way its nesting demands
94/// ([`repair::shells_well_nested`]), so no inverted body survives that
95/// {w >= 1} would drop, and no nested outward shell hides a wall with
96/// material on both sides.
97///
98/// A `false` verdict only costs the full pipeline on a disjoint pair, which
99/// produces the same answer by construction — it just also classifies.
100fn needs_no_classification(
101 imp: &ManifoldImpl,
102 tris: &[[Vec3; 3]],
103 token: Option<&CancelToken>,
104) -> bool {
105 // Cheapest first, and usually already cached by `Auto`'s dispatch. A
106 // cancelled scan answers "self-intersecting", which routes to the pipeline
107 // and so reports `Error::Cancelled` rather than a bogus pass-through.
108 !soup::has_self_intersections_with_token(imp, token) && repair::shells_well_nested(tris)
109}
110
111/// Robust boolean of two impls (manifold or soup). Same observable contract
112/// as `boolean3::boolean_with_token`: intersect exactly, arrange +
113/// retriangulate, build the arrangement's cell complex, propagate winding
114/// numbers, and keep the walls the operation's predicate separates.
115///
116/// Every operation is one predicate on a cell's winding vector, so `Subtract`
117/// needs no operand flip — it is simply "inside P and not inside Q".
118pub fn boolean(
119 a: &ManifoldImpl,
120 b: &ManifoldImpl,
121 op: OpType,
122 token: Option<&CancelToken>,
123) -> ManifoldImpl {
124 boolean_with_progress(a, b, op, token, None)
125}
126
127/// [`boolean`] with optional progress reporting (see [`crate::progress`]).
128/// `None` is exactly [`boolean`] — the fast paths below are not instrumented
129/// at all, since they return before any measurable work happens.
130pub fn boolean_with_progress(
131 a: &ManifoldImpl,
132 b: &ManifoldImpl,
133 op: OpType,
134 token: Option<&CancelToken>,
135 progress: Option<&crate::progress::ProgressReporter>,
136) -> ManifoldImpl {
137 boolean_with_rule(a, b, op, WindingRule::Positive, token, progress)
138}
139
140/// [`boolean_with_progress`] with an explicit winding rule.
141///
142/// The rule only reinterprets the arrangement's cell labels
143/// ([`cells::in_result`]); intersection, arrangement, cell complex, and
144/// winding propagation are all rule-independent, so
145/// [`WindingRule::Positive`] here is byte-for-byte the historical pipeline.
146///
147/// The bbox-disjoint fast paths do not consult the rule at all — they never
148/// classify anything, they concatenate or return an operand. They therefore
149/// run only when every operand they *keep* is provably already the boundary
150/// of its own solid ([`needs_no_classification`]): both operands for the
151/// union, operand A alone for the difference (B is discarded whatever it is
152/// wound like). Otherwise they fall through to the full pipeline, which finds
153/// no cross intersections but still classifies each operand — so an inverted
154/// body is dropped under [`WindingRule::Positive`] and rewound to positive
155/// material under [`WindingRule::Nonzero`], exactly as it would be if the
156/// boxes overlapped. Disjoint `Intersect` needs no gate: nothing can be
157/// shared, whatever the winding.
158///
159/// The empty-operand fast paths above still return the other operand
160/// unclassified, keeping the historical pass-through of inverted geometry —
161/// they have no two-operand pipeline to fall through to.
162pub fn boolean_with_rule(
163 a: &ManifoldImpl,
164 b: &ManifoldImpl,
165 op: OpType,
166 rule: WindingRule,
167 token: Option<&CancelToken>,
168 progress: Option<&crate::progress::ProgressReporter>,
169) -> ManifoldImpl {
170 use crate::progress::{begin_phase, Phase};
171 if is_cancelled(token) {
172 return cancelled_impl();
173 }
174 // Fast paths mirror the exact engine's observable behavior.
175 if a.is_empty() {
176 return match op {
177 OpType::Add => b.clone(),
178 OpType::Intersect | OpType::Subtract => ManifoldImpl::new(),
179 };
180 }
181 if b.is_empty() {
182 return match op {
183 OpType::Add | OpType::Subtract => a.clone(),
184 OpType::Intersect => ManifoldImpl::new(),
185 };
186 }
187 let p_tris = soup::impl_to_tris(a);
188 let q_tris = soup::impl_to_tris(b);
189 let p_props = soup::impl_to_corner_props(a);
190 let q_props = soup::impl_to_corner_props(b);
191
192 if !a.bbox.does_overlap_box(&b.bbox) {
193 // Only the operands a fast path actually *keeps* need vetting: the
194 // union keeps both, the difference keeps only A.
195 let a_clean = || needs_no_classification(a, &p_tris, token);
196 let b_clean = || needs_no_classification(b, &q_tris, token);
197 match op {
198 OpType::Add if a_clean() && b_clean() => {
199 // Disjoint union: concatenate the soups and re-import. The
200 // property context tags the two halves so each keeps its own
201 // interpolated properties.
202 let mut tris = p_tris.clone();
203 tris.extend(q_tris.iter().cloned());
204 let mut interner = intersection_graph::VertInterner::default();
205 let pieces: Vec<intersection_graph::Piece> = tris
206 .iter()
207 .enumerate()
208 .map(|(i, t)| intersection_graph::Piece {
209 mesh: if i < p_tris.len() { 0 } else { 1 },
210 tri: if i < p_tris.len() { i } else { i - p_tris.len() },
211 vi: [
212 interner.intern_f64(t[0]),
213 interner.intern_f64(t[1]),
214 interner.intern_f64(t[2]),
215 ],
216 })
217 .collect();
218 let ctx = assemble::PropCtx {
219 num_prop: [a.num_prop, b.num_prop],
220 tris: [&p_tris, &q_tris],
221 props: [&p_props, &q_props],
222 };
223 let props = (ctx.out_num_prop() > 0).then_some(&ctx);
224 return assemble::assemble(
225 &pieces,
226 &interner.verts,
227 &interner.verts_f64,
228 |_| true,
229 props,
230 )
231 .into_impl();
232 }
233 OpType::Intersect => return ManifoldImpl::new(),
234 OpType::Subtract if a_clean() => return a.clone(),
235 // A kept operand needs classification: fall through to the full
236 // pipeline, which handles disjoint inputs fine (it simply finds no
237 // cross intersections).
238 OpType::Add | OpType::Subtract => {}
239 }
240 }
241
242 // Subtraction needs no operand flip: the cell predicate expresses it
243 // directly as "inside P and not inside Q", so both operands keep their
244 // own winding and their corner properties stay in their original order.
245 let Some(graph) =
246 intersection_graph::build_graph_with_progress(&p_tris, &q_tris, token, progress)
247 else {
248 return cancelled_impl();
249 };
250 let t_cells = crate::timing::start();
251 let Some(complex) = cells::build_cells_with_progress(&graph, token, progress) else {
252 return cancelled_impl();
253 };
254 crate::timing::print("robust: cell complex", t_cells);
255
256 // One exact query anchors each connected component; the rest of its
257 // cells follow combinatorially. Winding and assembly report as phase
258 // transitions only: neither has a work total the caller could see a
259 // fraction of without instrumenting the exact ray queries themselves.
260 begin_phase(progress, Phase::Winding, 0);
261 let t_winding = crate::timing::start();
262 let wind = cells::windings(&graph, &complex, [&p_tris, &q_tris]);
263 crate::timing::print("robust: winding propagation", t_winding);
264 if is_cancelled(token) {
265 return cancelled_impl();
266 }
267
268 // Boundary of the result, wound from the cell labels.
269 begin_phase(progress, Phase::Assemble, 0);
270 let pieces = cells::extract(&graph, &complex, &wind, op, rule);
271 let ctx = assemble::PropCtx {
272 num_prop: [a.num_prop, b.num_prop],
273 tris: [&p_tris, &q_tris],
274 props: [&p_props, &q_props],
275 };
276 let props = (ctx.out_num_prop() > 0).then_some(&ctx);
277 let t_asm = crate::timing::start();
278 let out = assemble::assemble(&pieces, &graph.verts, &graph.verts_f64, |_| true, props);
279 crate::timing::print("robust: assemble+import", t_asm);
280 out.into_impl()
281}
282
283/// Import a raw triangle list as a boolean result (used by
284/// `boolean3::compose_meshes` when any input is a soup; positions only —
285/// the property-aware disjoint-union path in `boolean` builds its own
286/// tagged pieces).
287pub(crate) fn assemble_all(tris: &[[Vec3; 3]]) -> ManifoldImpl {
288 let mut interner = intersection_graph::VertInterner::default();
289 let pieces: Vec<intersection_graph::Piece> = tris
290 .iter()
291 .enumerate()
292 .map(|(i, t)| intersection_graph::Piece {
293 mesh: 0,
294 tri: i,
295 vi: [
296 interner.intern_f64(t[0]),
297 interner.intern_f64(t[1]),
298 interner.intern_f64(t[2]),
299 ],
300 })
301 .collect();
302 assemble::assemble(&pieces, &interner.verts, &interner.verts_f64, |_| true, None).into_impl()
303}
304
305#[cfg(test)]
306#[path = "engine_tests.rs"]
307mod engine_tests;
308
309#[cfg(test)]
310#[path = "cross_validation_tests.rs"]
311mod cross_validation_tests;
312
313#[cfg(test)]
314#[path = "nonmanifold_tests.rs"]
315mod nonmanifold_tests;
316
317#[cfg(test)]
318#[path = "property_tests.rs"]
319mod property_tests;
320
321#[cfg(test)]
322#[path = "thingi_tests.rs"]
323mod thingi_tests;