Skip to main content

manifold_rust/
boolean3.rs

1// Copyright 2026 Lars Brubaker
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15// Phase 11: Boolean Operations (Core)
16//
17// C++ sources: src/boolean3.cpp (531 lines), src/boolean_result.cpp (889 lines)
18//
19// This module implements the edge-face intersection detection algorithm from
20// boolean3.cpp. The result is consumed by boolean_result.rs to assemble the
21// output mesh.
22//
23// Key notation (from the C++ source):
24// - P and Q are the two input manifolds, R is the output
25// - Dimensions: vert=0, edge=1, face=2, solid=3
26// - X = winding-number quantity, S = "shadow" subset of X
27// - p1q2 = edges of P intersecting faces of Q
28// - x12 = winding contribution at each intersection
29// - v12 = 3D position of each intersection vertex
30
31use crate::cancel::{is_cancelled, CancelToken};
32use crate::impl_mesh::ManifoldImpl;
33use crate::linalg::{dot, IVec3, Vec3};
34use crate::types::{Box as BBox, Error, Halfedge, OpType, RayHit, TriRef};
35
36// The floating-point kernels (shadow01, kernel11/02/12) and the broadphase
37// drivers (intersect12, winding03) live in boolean3_kernels.rs.
38#[path = "boolean3_kernels.rs"]
39mod boolean3_kernels;
40use boolean3_kernels::{intersect12, kernel12, winding03};
41
42// ---------------------------------------------------------------------------
43// Intersections — sparse intersection data between two meshes
44// ---------------------------------------------------------------------------
45
46/// Stores the intersections of edges of one mesh with faces of the other.
47/// In forward mode: edges of P with faces of Q.
48/// In reverse mode: edges of Q with faces of P.
49#[derive(Clone, Default)]
50pub struct Intersections {
51    /// Pairs [edge_idx, face_idx] — in forward mode [p1, q2], reverse [q1, p2]
52    pub p1q2: Vec<[i32; 2]>,
53    /// Winding number contribution at each intersection
54    pub x12: Vec<i32>,
55    /// 3D position of each intersection vertex
56    pub v12: Vec<Vec3>,
57}
58
59// ---------------------------------------------------------------------------
60// Boolean3 — the core intersection computation
61// ---------------------------------------------------------------------------
62
63/// Computes all edge-face intersections and winding numbers between two meshes.
64pub struct Boolean3 {
65    pub xv12: Intersections,
66    pub xv21: Intersections,
67    pub w03: Vec<i32>,
68    pub w30: Vec<i32>,
69    pub expand_p: bool,
70    pub valid: bool,
71}
72
73
74// ---------------------------------------------------------------------------
75// Boolean3 constructor
76// ---------------------------------------------------------------------------
77
78impl Boolean3 {
79    /// Compute all intersections between meshes inP and inQ for the given op.
80    pub fn new(in_p: &ManifoldImpl, in_q: &ManifoldImpl, op: OpType) -> Self {
81        match Self::new_with_token(in_p, in_q, op, None) {
82            Some(b3) => b3,
83            // Unreachable: `is_cancelled(None)` is always false, so none of the
84            // cancellation arms below can be taken. The debug assert makes a
85            // future refactor that breaks that reasoning fail loudly in tests,
86            // while release stays total — degrading to an invalid
87            // (empty-result) Boolean3 rather than panicking in production.
88            None => {
89                debug_assert!(
90                    false,
91                    "Boolean3::new_with_token returned None for a None token; \
92                     only a cancelled token can produce None"
93                );
94                Boolean3 {
95                    xv12: Intersections::default(),
96                    xv21: Intersections::default(),
97                    w03: Vec::new(),
98                    w30: Vec::new(),
99                    expand_p: op == OpType::Add,
100                    valid: false,
101                }
102            }
103        }
104    }
105
106    /// [`Boolean3::new`] with cooperative cancellation. `None` means `token`
107    /// was cancelled; no usable intersection data was produced.
108    ///
109    /// The check placement mirrors C++ `Boolean3::Boolean3`
110    /// (boolean3.cpp:497-560): one phase-boundary check before launching each
111    /// of the four heavy stages, plus the intra-stage checks that
112    /// [`intersect12`] and [`winding03`] carry.
113    pub fn new_with_token(
114        in_p: &ManifoldImpl,
115        in_q: &ManifoldImpl,
116        op: OpType,
117        token: Option<&CancelToken>,
118    ) -> Option<Self> {
119        let expand_p = op == OpType::Add;
120
121        if in_p.is_empty() || in_q.is_empty() || !in_p.bbox.does_overlap_box(&in_q.bbox) {
122            return Some(Boolean3 {
123                xv12: Intersections::default(),
124                xv21: Intersections::default(),
125                w03: vec![0; in_p.num_vert()],
126                w30: vec![0; in_q.num_vert()],
127                expand_p,
128                valid: true,
129            });
130        }
131
132        // Level 3: find all edge-face intersections in both directions
133        let t_total = crate::timing::start();
134        let t = crate::timing::start();
135        // Phase-boundary fast-path: skip launching the next stage if cancel
136        // fired between stages (C++ boolean3.cpp:530/536/552/558).
137        if is_cancelled(token) {
138            return None;
139        }
140        let xv12 = intersect12(in_p, in_q, expand_p, true, token)?;
141        crate::timing::print("  Intersect12 P->Q", t);
142        let t = crate::timing::start();
143        if is_cancelled(token) {
144            return None;
145        }
146        let xv21 = intersect12(in_p, in_q, expand_p, false, token)?;
147        crate::timing::print("  Intersect12 Q->P", t);
148
149        if xv12.x12.len() > i32::MAX as usize || xv21.x12.len() > i32::MAX as usize {
150            return Some(Boolean3 {
151                xv12: Intersections::default(),
152                xv21: Intersections::default(),
153                w03: Vec::new(),
154                w30: Vec::new(),
155                expand_p,
156                valid: false,
157            });
158        }
159
160        // Compute winding numbers via flood fill
161        let t = crate::timing::start();
162        if is_cancelled(token) {
163            return None;
164        }
165        let w03 = winding03(in_p, in_q, &xv12.p1q2, expand_p, true, token)?;
166        crate::timing::print("  Winding03 P", t);
167        let t = crate::timing::start();
168        if is_cancelled(token) {
169            return None;
170        }
171        let w30 = winding03(in_p, in_q, &xv21.p1q2, expand_p, false, token)?;
172        crate::timing::print("  Winding03 Q", t);
173        crate::timing::print("Intersections (total)", t_total);
174
175        Some(Boolean3 {
176            xv12,
177            xv21,
178            w03,
179            w30,
180            expand_p,
181            valid: true,
182        })
183    }
184}
185
186// ---------------------------------------------------------------------------
187// compose_meshes — concatenate disjoint meshes (unchanged from before)
188// ---------------------------------------------------------------------------
189
190fn extract_tri_vert(mesh: &ManifoldImpl) -> Vec<IVec3> {
191    (0..mesh.num_tri())
192        .map(|tri| {
193            IVec3::new(
194                mesh.halfedge[3 * tri].start_vert,
195                mesh.halfedge[3 * tri + 1].start_vert,
196                mesh.halfedge[3 * tri + 2].start_vert,
197            )
198        })
199        .collect()
200}
201
202fn extract_tri_prop(mesh: &ManifoldImpl) -> Vec<IVec3> {
203    (0..mesh.num_tri())
204        .map(|tri| {
205            IVec3::new(
206                mesh.halfedge[3 * tri].prop_vert,
207                mesh.halfedge[3 * tri + 1].prop_vert,
208                mesh.halfedge[3 * tri + 2].prop_vert,
209            )
210        })
211        .collect()
212}
213
214fn property_row(mesh: &ManifoldImpl, row: usize, width: usize) -> Vec<f64> {
215    if mesh.num_prop == 0 {
216        vec![0.0; width]
217    } else {
218        let mut out = vec![0.0; width];
219        let src = &mesh.properties[row * mesh.num_prop..(row + 1) * mesh.num_prop];
220        out[..src.len()].copy_from_slice(src);
221        out
222    }
223}
224
225/// Concatenate multiple disjoint meshes into one. This is a genuine utility
226/// used by both boolean operations and CSG compose. It does NOT perform any
227/// boolean intersection — the meshes must be non-overlapping for correct results.
228pub fn compose_meshes(meshes: &[ManifoldImpl]) -> ManifoldImpl {
229    if meshes.is_empty() {
230        return ManifoldImpl::new();
231    }
232    if meshes.len() == 1 {
233        return meshes[0].clone();
234    }
235    // Soup inputs (robust non-manifold import) cannot go through
236    // create_halfedges' strict pairing below; concatenate them geometrically
237    // instead. Mesh relations are not preserved on this path — soups carry
238    // none that survive a boolean anyway.
239    if meshes.iter().any(|m| m.is_soup) {
240        let mut tris = Vec::new();
241        for m in meshes {
242            tris.extend(crate::robust::soup::impl_to_tris(m));
243        }
244        return crate::robust::assemble_all(&tris);
245    }
246
247    let num_prop = meshes.iter().map(|m| m.num_prop).max().unwrap_or(0);
248    let mut vert_pos = Vec::new();
249    let mut properties = Vec::new();
250    let mut tri_vert = Vec::new();
251    let mut tri_prop = Vec::new();
252    let mut vert_offset = 0i32;
253    let mut prop_offset = 0i32;
254
255    for mesh in meshes {
256        vert_pos.extend_from_slice(&mesh.vert_pos);
257
258        let old_tri_vert = extract_tri_vert(mesh);
259        let old_tri_prop = extract_tri_prop(mesh);
260        tri_vert.extend(old_tri_vert.into_iter().map(|t| {
261            IVec3::new(t.x + vert_offset, t.y + vert_offset, t.z + vert_offset)
262        }));
263        tri_prop.extend(old_tri_prop.into_iter().map(|t| {
264            IVec3::new(t.x + prop_offset, t.y + prop_offset, t.z + prop_offset)
265        }));
266
267        if num_prop > 0 {
268            let prop_rows = mesh.num_prop_vert();
269            for row in 0..prop_rows {
270                properties.extend(property_row(mesh, row, num_prop));
271            }
272            prop_offset += prop_rows as i32;
273        } else {
274            prop_offset += mesh.num_prop_vert() as i32;
275        }
276        vert_offset += mesh.num_vert() as i32;
277    }
278
279    // Concatenate tri_refs and merge mesh_id_transforms from all input meshes.
280    // Each mesh's coplanar_id is a triangle-local group index, so offset by tri_offset.
281    let mut all_tri_refs: Vec<TriRef> = Vec::new();
282    let mut merged_transforms = std::collections::BTreeMap::new();
283    let mut tri_offset = 0i32;
284    for mesh in meshes {
285        let mesh_tri_count = mesh.num_tri() as i32;
286        for tri_ref in &mesh.mesh_relation.tri_ref {
287            all_tri_refs.push(TriRef {
288                mesh_id: tri_ref.mesh_id,
289                original_id: tri_ref.original_id,
290                face_id: tri_ref.face_id,
291                coplanar_id: tri_ref.coplanar_id + tri_offset,
292            });
293        }
294        for (id, rel) in &mesh.mesh_relation.mesh_id_transform {
295            merged_transforms.insert(*id, rel.clone());
296        }
297        tri_offset += mesh_tri_count;
298    }
299
300    let mut out = ManifoldImpl::new();
301    out.vert_pos = vert_pos;
302    out.num_prop = num_prop;
303    out.properties = properties;
304    out.create_halfedges(&tri_prop, &tri_vert);
305    // Preserve tri_refs and transforms from input meshes instead of
306    // calling initialize_original(), which would lose mesh transform data.
307    out.mesh_relation.tri_ref = all_tri_refs;
308    out.mesh_relation.mesh_id_transform = merged_transforms;
309    out.mesh_relation.original_id = -1;
310    out.calculate_bbox();
311    out.set_epsilon(-1.0, false);
312    // required to remove parts that are smaller than the tolerance (matches C++)
313    crate::edge_op::remove_degenerates(&mut out, 0);
314    out.sort_geometry();
315    out.increment_mesh_ids();
316    out.set_normals_and_coplanar();
317    out
318}
319
320// ---------------------------------------------------------------------------
321// boolean — public entry point
322// ---------------------------------------------------------------------------
323
324/// Perform a 3D boolean operation on two manifold meshes.
325///
326/// For overlapping meshes, uses the full Boolean3 intersection algorithm.
327/// For disjoint meshes, uses fast-path shortcuts.
328pub fn boolean(mesh_a: &ManifoldImpl, mesh_b: &ManifoldImpl, op: OpType) -> ManifoldImpl {
329    boolean_with_token(mesh_a, mesh_b, op, None)
330}
331
332/// [`boolean`] with cooperative cancellation.
333///
334/// A cancelled operation yields an empty mesh whose `status` is
335/// [`Error::Cancelled`], matching what C++ produces via `MakeEmpty(Cancelled)`
336/// at every checkpoint (execution_impl.h:150-160, boolean_result.cpp:758-770).
337pub fn boolean_with_token(
338    mesh_a: &ManifoldImpl,
339    mesh_b: &ManifoldImpl,
340    op: OpType,
341    token: Option<&CancelToken>,
342) -> ManifoldImpl {
343    // Entry gate: a token cancelled before the call wins over every fast path
344    // below, including the empty-input ones. C++ does the same at its outermost
345    // gates (csg_tree.cpp:172, execution_impl.cpp's static factories), so an
346    // already-cancelled context never reports NoError.
347    if is_cancelled(token) {
348        return cancelled_impl();
349    }
350    // The exact engine's kernels assume complete halfedge pairing; soup
351    // impls (robust import of non-manifold geometry) must use the robust
352    // engine instead. Unreachable for all pre-existing callers: is_soup is
353    // false everywhere outside the from_mesh_gl_robust path.
354    if mesh_a.is_soup || mesh_b.is_soup {
355        let mut out = ManifoldImpl::new();
356        out.make_empty(Error::NotManifold);
357        return out;
358    }
359    if mesh_a.is_empty() {
360        return match op {
361            OpType::Add => mesh_b.clone(),
362            OpType::Intersect => ManifoldImpl::new(),
363            OpType::Subtract => ManifoldImpl::new(),
364        };
365    }
366    if mesh_b.is_empty() {
367        return match op {
368            OpType::Add | OpType::Subtract => mesh_a.clone(),
369            OpType::Intersect => ManifoldImpl::new(),
370        };
371    }
372
373    if !mesh_a.bbox.does_overlap_box(&mesh_b.bbox) {
374        // Non-overlapping fast paths. For Subtract, we still run through the full
375        // boolean_result to preserve both meshes' run metadata (C++ behavior).
376        match op {
377            OpType::Add => return compose_meshes(&[mesh_a.clone(), mesh_b.clone()]),
378            OpType::Intersect => return ManifoldImpl::new(),
379            OpType::Subtract => {} // fall through to full boolean
380        }
381    }
382
383    // Full boolean — compute intersections
384    let Some(bool3) = Boolean3::new_with_token(mesh_a, mesh_b, op, token) else {
385        return cancelled_impl();
386    };
387    if !bool3.valid {
388        return ManifoldImpl::new();
389    }
390
391    crate::boolean_result::boolean_result_with_token(mesh_a, mesh_b, op, &bool3, token)
392}
393
394/// Route a boolean to the requested engine (`types::BooleanEngine`).
395///
396/// `Auto` resolves per pair: `Robust` iff either operand carries soup
397/// geometry, else `Exact`. `Exact` with a soup operand yields an empty
398/// result with `Error::NotManifold` (the guard inside
399/// [`boolean_with_token`]); no panic-catching is involved anywhere —
400/// dispatch is input-based only.
401pub fn boolean_dispatch(
402    mesh_a: &ManifoldImpl,
403    mesh_b: &ManifoldImpl,
404    op: OpType,
405    engine: crate::types::BooleanEngine,
406    token: Option<&CancelToken>,
407) -> ManifoldImpl {
408    use crate::types::BooleanEngine as E;
409    let resolved = match engine {
410        E::Auto => {
411            if mesh_a.is_soup || mesh_b.is_soup {
412                E::Robust
413            } else {
414                E::Exact
415            }
416        }
417        other => other,
418    };
419    match resolved {
420        E::Exact | E::Auto => boolean_with_token(mesh_a, mesh_b, op, token),
421        E::Robust => crate::robust::boolean(mesh_a, mesh_b, op, token),
422    }
423}
424
425/// The observable result of an interrupted operation: an empty mesh carrying
426/// [`Error::Cancelled`]. Mirrors C++ `MakeEmpty(Manifold::Error::Cancelled)`.
427pub(crate) fn cancelled_impl() -> ManifoldImpl {
428    let mut out = ManifoldImpl::new();
429    out.make_empty(Error::Cancelled);
430    out
431}
432
433/// Cast a ray segment from `origin` to `endpoint` against `mesh`, returning
434/// all triangle intersections sorted by parametric distance.
435///
436/// Mirrors C++ `Manifold::Impl::RayCast(vec3, vec3)` in boolean3.cpp.
437/// Builds a degenerate single-edge Impl representing the ray, then uses
438/// Kernel12 (edge-face intersection) with the mesh BVH to find hits.
439pub fn ray_cast(mesh: &ManifoldImpl, origin: Vec3, endpoint: Vec3) -> Vec<RayHit> {
440    if mesh.is_empty() {
441        return vec![];
442    }
443    let dir = endpoint - origin;
444    if dot(dir, dir) == 0.0 {
445        return vec![];
446    }
447
448    // Build a minimal single-edge Impl representing the ray segment.
449    // halfedge[0]: forward (0→1), halfedge[1]: backward (1→0).
450    let mut ray_impl = ManifoldImpl::new();
451    ray_impl.vert_pos = vec![origin, endpoint];
452    ray_impl.vert_normal = vec![Vec3::splat(0.0), Vec3::splat(0.0)];
453    ray_impl.halfedge = vec![
454        Halfedge { start_vert: 0, end_vert: 1, paired_halfedge: 1, prop_vert: 0 },
455        Halfedge { start_vert: 1, end_vert: 0, paired_halfedge: 0, prop_vert: 0 },
456    ];
457    ray_impl.face_normal = vec![Vec3::splat(0.0)];
458
459    // Query the mesh's cached face BVH (C++ RayCast uses collider_).
460    let collider = &mesh.collider;
461
462    // Ray AABB for BVH query.
463    let ray_box = BBox::from_points(
464        Vec3::new(origin.x.min(endpoint.x), origin.y.min(endpoint.y), origin.z.min(endpoint.z)),
465        Vec3::new(origin.x.max(endpoint.x), origin.y.max(endpoint.y), origin.z.max(endpoint.z)),
466    );
467
468    // Determine which component axis is largest for stable t computation.
469    let abs_dir = Vec3::new(dir.x.abs(), dir.y.abs(), dir.z.abs());
470    let t_axis = if abs_dir.x > abs_dir.y && abs_dir.x > abs_dir.z {
471        0usize
472    } else if abs_dir.y > abs_dir.z {
473        1
474    } else {
475        2
476    };
477
478    let mut hits: Vec<RayHit> = Vec::new();
479
480    // Query BVH with ray AABB and test each candidate triangle.
481    collider.collisions_with_boxes(std::slice::from_ref(&ray_box), false, |_qi, tri| {
482        // halfedge 0 (forward) vs triangle tri; expand_p=false, forward=true.
483        let (s, v) = kernel12(0, tri, &ray_impl, mesh, &ray_impl, mesh, false, true);
484        if s != 0 && v.x.is_finite() {
485            // Compute parametric t along the ray.
486            let origin_t = [origin.x, origin.y, origin.z][t_axis];
487            let dir_t = [dir.x, dir.y, dir.z][t_axis];
488            let v_t = [v.x, v.y, v.z][t_axis];
489            let t = (v_t - origin_t) / dir_t;
490            if t >= 0.0 && t <= 1.0 {
491                hits.push(RayHit {
492                    face_id: tri as u64,
493                    distance: t,
494                    position: v,
495                    normal: mesh.face_normal[tri],
496                });
497            }
498        }
499    });
500
501    hits.sort_by(|a, b| a.distance.partial_cmp(&b.distance).unwrap_or(std::cmp::Ordering::Equal));
502    hits
503}
504
505#[cfg(test)]
506#[path = "boolean3_tests.rs"]
507mod tests;