Skip to main content

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;