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//   classify           — ring regularization, coincident-piece binding
23//   propagate          — per-mesh component flood fill between cuts
24//   ray_shoot          — exact winding numbers (component tags, wall tests)
25//   soup               — triangle-soup import (closed/orientable validation)
26//
27// Classification is winding-number based: each surface component (bounded
28// by intersection cuts) is inside or outside the other operand uniformly,
29// decided by one exact query; pieces that are interior walls of their own
30// operand (self-overlapping or nested sheets, where the winding exceeds 1)
31// are detected with an own-mesh query just off the piece's outward side and
32// dropped from both outputs — the regularized boolean's boundary only ever
33// lies where the total winding steps between 0 and 1.
34
35pub mod arrangement;
36pub mod assemble;
37pub mod cdt;
38pub mod classify;
39pub mod exact;
40pub mod intersection_graph;
41pub mod propagate;
42pub mod ray_shoot;
43pub mod soup;
44pub mod tri_tri;
45
46use crate::cancel::CancelToken;
47use crate::impl_mesh::ManifoldImpl;
48use crate::linalg::Vec3;
49use crate::types::{Error, OpType};
50
51use classify::Tag;
52use exact::rational::R3;
53use ray_shoot::piece_centroid;
54
55fn is_cancelled(token: Option<&CancelToken>) -> bool {
56    token.is_some_and(|t| t.is_cancelled())
57}
58
59fn cancelled_impl() -> ManifoldImpl {
60    let mut out = ManifoldImpl::new();
61    out.make_empty(Error::Cancelled);
62    out
63}
64
65/// Robust boolean of two impls (manifold or soup). Same observable contract
66/// as `boolean3::boolean_with_token`, computed by the Barki 2015 pipeline:
67/// intersect exactly, arrange + retriangulate, classify pieces into
68/// union/intersection sets by exact winding numbers, assemble the requested
69/// one.
70///
71/// `Subtract` uses the identity P − Q = P ∩ Q^c: Q's winding is flipped on
72/// the working copy and the intersection set is assembled; the winding
73/// queries interpret the flipped operand as its (unbounded) complement.
74pub fn boolean(
75    a: &ManifoldImpl,
76    b: &ManifoldImpl,
77    op: OpType,
78    token: Option<&CancelToken>,
79) -> ManifoldImpl {
80    if is_cancelled(token) {
81        return cancelled_impl();
82    }
83    // Fast paths mirror the exact engine's observable behavior.
84    if a.is_empty() {
85        return match op {
86            OpType::Add => b.clone(),
87            OpType::Intersect | OpType::Subtract => ManifoldImpl::new(),
88        };
89    }
90    if b.is_empty() {
91        return match op {
92            OpType::Add | OpType::Subtract => a.clone(),
93            OpType::Intersect => ManifoldImpl::new(),
94        };
95    }
96    let p_tris = soup::impl_to_tris(a);
97    let mut q_tris = soup::impl_to_tris(b);
98    let p_props = soup::impl_to_corner_props(a);
99    let mut q_props = soup::impl_to_corner_props(b);
100
101    if !a.bbox.does_overlap_box(&b.bbox) {
102        match op {
103            OpType::Add => {
104                // Disjoint union: concatenate the soups and re-import. The
105                // property context tags the two halves so each keeps its own
106                // interpolated properties.
107                let mut tris = p_tris.clone();
108                tris.extend(q_tris.iter().cloned());
109                let mut interner = intersection_graph::VertInterner::default();
110                let pieces: Vec<intersection_graph::Piece> = tris
111                    .iter()
112                    .enumerate()
113                    .map(|(i, t)| intersection_graph::Piece {
114                        mesh: if i < p_tris.len() { 0 } else { 1 },
115                        tri: if i < p_tris.len() { i } else { i - p_tris.len() },
116                        vi: [
117                            interner.intern_f64(t[0]),
118                            interner.intern_f64(t[1]),
119                            interner.intern_f64(t[2]),
120                        ],
121                    })
122                    .collect();
123                let ctx = assemble::PropCtx {
124                    num_prop: [a.num_prop, b.num_prop],
125                    tris: [&p_tris, &q_tris],
126                    props: [&p_props, &q_props],
127                };
128                let props = (ctx.out_num_prop() > 0).then_some(&ctx);
129                return assemble::assemble(
130                    &pieces,
131                    &interner.verts,
132                    &interner.verts_f64,
133                    |_| true,
134                    props,
135                )
136                .into_impl();
137            }
138            OpType::Intersect => return ManifoldImpl::new(),
139            OpType::Subtract => return a.clone(),
140        }
141    }
142
143    let complement = op == OpType::Subtract;
144    if complement {
145        let nq = b.num_prop;
146        for (ti, t) in q_tris.iter_mut().enumerate() {
147            t.swap(1, 2);
148            if nq > 0 {
149                // Keep the corner-property alignment in step with the swap.
150                let base = 3 * ti * nq;
151                for k in 0..nq {
152                    q_props.swap(base + nq + k, base + 2 * nq + k);
153                }
154            }
155        }
156    }
157
158    let graph = intersection_graph::build_graph(&p_tris, &q_tris);
159    if is_cancelled(token) {
160        return cancelled_impl();
161    }
162    let t_cls = crate::timing::start();
163    let cls = classify::classify_rings(&graph);
164    crate::timing::print("robust: classify_rings", t_cls);
165    if is_cancelled(token) {
166        return cancelled_impl();
167    }
168    let t_prop = crate::timing::start();
169    let prop = propagate::propagate(&graph, &cls);
170    crate::timing::print("robust: propagate", t_prop);
171    let mut tags = prop.tags;
172
173    // Winding-based classification of every component the coincident-piece
174    // binding did not decide. Both windings are constant per component:
175    // components never cross an intersection cut, and the graph cuts each
176    // mesh along its own self-intersections as well as along the other
177    // operand's surface. So per component, one query against the other
178    // operand decides ∪ vs ∩, and one query against the component's own
179    // operand decides whether it is real boundary or an interior wall.
180    // Whole-soup rational tables only exist when winding queries actually
181    // run — a pass-through boolean (everything decided by rings/binding)
182    // never converts an input triangle to rationals at all.
183    let to_rational = |tris: &[[Vec3; 3]]| -> Vec<[R3; 3]> {
184        tris.iter()
185            .map(|t| [R3::from_vec3(t[0]), R3::from_vec3(t[1]), R3::from_vec3(t[2])])
186            .collect()
187    };
188    let need_windings = !prop.untagged.is_empty();
189    let own_rational: [Vec<[R3; 3]>; 2] = if need_windings {
190        [to_rational(&p_tris), to_rational(&q_tris)]
191    } else {
192        [Vec::new(), Vec::new()]
193    };
194    let tri_boxes = |tris: &[[Vec3; 3]]| -> Vec<crate::types::Box> {
195        tris.iter()
196            .map(|t| {
197                let mut b = crate::types::Box::from_points(t[0], t[1]);
198                b.union_point(t[2]);
199                b
200            })
201            .collect()
202    };
203    let own_boxes: [Vec<crate::types::Box>; 2] = if need_windings {
204        [tri_boxes(&p_tris), tri_boxes(&q_tris)]
205    } else {
206        [Vec::new(), Vec::new()]
207    };
208
209    let t_winding = crate::timing::start();
210    // BVH per operand, built once for the whole query batch (components can
211    // number in the thousands on self-intersecting scans).
212    let winding_indexes: Option<[ray_shoot::WindingIndex; 2]> = (!prop.untagged.is_empty())
213        .then(|| [ray_shoot::WindingIndex::new(&p_tris), ray_shoot::WindingIndex::new(&q_tris)]);
214    for &(root, rep) in &prop.untagged {
215        if is_cancelled(token) {
216            return cancelled_impl();
217        }
218        let piece = &graph.pieces[rep];
219        let mesh = piece.mesh as usize;
220        let indexes = winding_indexes.as_ref().expect("built when untagged is non-empty");
221        let (other, other_index, other_is_complement): (&[[Vec3; 3]], _, bool) = if mesh == 0 {
222            (&q_tris, &indexes[1], complement)
223        } else {
224            (&p_tris, &indexes[0], false)
225        };
226        let pv = graph.piece_verts(rep);
227        let w = ray_shoot::winding_number_indexed(&piece_centroid(pv), other, other_index);
228        let inside = if other_is_complement { w == 0 } else { w != 0 };
229        let tag = if inside { Tag::Inter } else { Tag::Union };
230        let own_f64: &[[Vec3; 3]] = if mesh == 0 { &p_tris } else { &q_tris };
231        let component_tag =
232            on_own_boundary(pv, &own_rational[mesh], own_f64, &own_boxes[mesh]).then_some(tag);
233        for pi in 0..graph.pieces.len() {
234            if !cls.discarded[pi] && prop.component[pi] == root {
235                tags[pi] = component_tag;
236            }
237        }
238    }
239
240    crate::timing::print(
241        &format!("robust: winding queries ({} components)", prop.untagged.len()),
242        t_winding,
243    );
244
245    let want = match op {
246        OpType::Add => Tag::Union,
247        OpType::Subtract | OpType::Intersect => Tag::Inter,
248    };
249    let ctx = assemble::PropCtx {
250        num_prop: [a.num_prop, b.num_prop],
251        tris: [&p_tris, &q_tris],
252        props: [&p_props, &q_props],
253    };
254    let props = (ctx.out_num_prop() > 0).then_some(&ctx);
255    let t_asm = crate::timing::start();
256    let out = assemble::assemble(
257        &graph.pieces,
258        &graph.verts,
259        &graph.verts_f64,
260        |pi| !cls.discarded[pi] && tags[pi] == Some(want),
261        props,
262    );
263    crate::timing::print("robust: assemble+import", t_asm);
264    out.into_impl()
265}
266
267/// Is this piece on the boundary of the solid its own operand bounds, or an
268/// interior wall (a sheet with material on both sides)?
269///
270/// With the solid defined as `{winding ≠ 0}` (and its complement, for the
271/// orientation-flipped subtraction operand, as `{winding == 0}`), membership
272/// changes across the piece exactly when the winding just off its outward
273/// side is 0 or −1: crossing the piece adds 1 to the winding, and only the
274/// 0↔1 and −1↔0 steps change either membership predicate. Anything else —
275/// e.g. a 1↔2 step inside a self-overlapping operand, or the inner of two
276/// nested same-orientation shells — is an interior wall that no regularized
277/// boolean output may contain.
278fn on_own_boundary(
279    pv: [&R3; 3],
280    own: &[[R3; 3]],
281    own_f64: &[[Vec3; 3]],
282    own_boxes: &[crate::types::Box],
283) -> bool {
284    let normal = pv[1].sub(pv[0]).cross(&pv[2].sub(pv[0]));
285    let w = ray_shoot::winding_off_surface(
286        &piece_centroid(pv),
287        &normal,
288        own,
289        own_f64,
290        own_boxes,
291    );
292    w == 0 || w == -1
293}
294
295/// Import a raw triangle list as a boolean result (used by
296/// `boolean3::compose_meshes` when any input is a soup; positions only —
297/// the property-aware disjoint-union path in `boolean` builds its own
298/// tagged pieces).
299pub(crate) fn assemble_all(tris: &[[Vec3; 3]]) -> ManifoldImpl {
300    let mut interner = intersection_graph::VertInterner::default();
301    let pieces: Vec<intersection_graph::Piece> = tris
302        .iter()
303        .enumerate()
304        .map(|(i, t)| intersection_graph::Piece {
305            mesh: 0,
306            tri: i,
307            vi: [
308                interner.intern_f64(t[0]),
309                interner.intern_f64(t[1]),
310                interner.intern_f64(t[2]),
311            ],
312        })
313        .collect();
314    assemble::assemble(&pieces, &interner.verts, &interner.verts_f64, |_| true, None).into_impl()
315}
316
317#[cfg(test)]
318#[path = "engine_tests.rs"]
319mod engine_tests;
320
321#[cfg(test)]
322#[path = "cross_validation_tests.rs"]
323mod cross_validation_tests;
324
325#[cfg(test)]
326#[path = "nonmanifold_tests.rs"]
327mod nonmanifold_tests;
328
329#[cfg(test)]
330#[path = "property_tests.rs"]
331mod property_tests;
332
333#[cfg(test)]
334#[path = "thingi_tests.rs"]
335mod thingi_tests;