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
236    let num_prop = meshes.iter().map(|m| m.num_prop).max().unwrap_or(0);
237    let mut vert_pos = Vec::new();
238    let mut properties = Vec::new();
239    let mut tri_vert = Vec::new();
240    let mut tri_prop = Vec::new();
241    let mut vert_offset = 0i32;
242    let mut prop_offset = 0i32;
243
244    for mesh in meshes {
245        vert_pos.extend_from_slice(&mesh.vert_pos);
246
247        let old_tri_vert = extract_tri_vert(mesh);
248        let old_tri_prop = extract_tri_prop(mesh);
249        tri_vert.extend(old_tri_vert.into_iter().map(|t| {
250            IVec3::new(t.x + vert_offset, t.y + vert_offset, t.z + vert_offset)
251        }));
252        tri_prop.extend(old_tri_prop.into_iter().map(|t| {
253            IVec3::new(t.x + prop_offset, t.y + prop_offset, t.z + prop_offset)
254        }));
255
256        if num_prop > 0 {
257            let prop_rows = mesh.num_prop_vert();
258            for row in 0..prop_rows {
259                properties.extend(property_row(mesh, row, num_prop));
260            }
261            prop_offset += prop_rows as i32;
262        } else {
263            prop_offset += mesh.num_prop_vert() as i32;
264        }
265        vert_offset += mesh.num_vert() as i32;
266    }
267
268    // Concatenate tri_refs and merge mesh_id_transforms from all input meshes.
269    // Each mesh's coplanar_id is a triangle-local group index, so offset by tri_offset.
270    let mut all_tri_refs: Vec<TriRef> = Vec::new();
271    let mut merged_transforms = std::collections::BTreeMap::new();
272    let mut tri_offset = 0i32;
273    for mesh in meshes {
274        let mesh_tri_count = mesh.num_tri() as i32;
275        for tri_ref in &mesh.mesh_relation.tri_ref {
276            all_tri_refs.push(TriRef {
277                mesh_id: tri_ref.mesh_id,
278                original_id: tri_ref.original_id,
279                face_id: tri_ref.face_id,
280                coplanar_id: tri_ref.coplanar_id + tri_offset,
281            });
282        }
283        for (id, rel) in &mesh.mesh_relation.mesh_id_transform {
284            merged_transforms.insert(*id, rel.clone());
285        }
286        tri_offset += mesh_tri_count;
287    }
288
289    let mut out = ManifoldImpl::new();
290    out.vert_pos = vert_pos;
291    out.num_prop = num_prop;
292    out.properties = properties;
293    out.create_halfedges(&tri_prop, &tri_vert);
294    // Preserve tri_refs and transforms from input meshes instead of
295    // calling initialize_original(), which would lose mesh transform data.
296    out.mesh_relation.tri_ref = all_tri_refs;
297    out.mesh_relation.mesh_id_transform = merged_transforms;
298    out.mesh_relation.original_id = -1;
299    out.calculate_bbox();
300    out.set_epsilon(-1.0, false);
301    // required to remove parts that are smaller than the tolerance (matches C++)
302    crate::edge_op::remove_degenerates(&mut out, 0);
303    out.sort_geometry();
304    out.increment_mesh_ids();
305    out.set_normals_and_coplanar();
306    out
307}
308
309// ---------------------------------------------------------------------------
310// boolean — public entry point
311// ---------------------------------------------------------------------------
312
313/// Perform a 3D boolean operation on two manifold meshes.
314///
315/// For overlapping meshes, uses the full Boolean3 intersection algorithm.
316/// For disjoint meshes, uses fast-path shortcuts.
317pub fn boolean(mesh_a: &ManifoldImpl, mesh_b: &ManifoldImpl, op: OpType) -> ManifoldImpl {
318    boolean_with_token(mesh_a, mesh_b, op, None)
319}
320
321/// [`boolean`] with cooperative cancellation.
322///
323/// A cancelled operation yields an empty mesh whose `status` is
324/// [`Error::Cancelled`], matching what C++ produces via `MakeEmpty(Cancelled)`
325/// at every checkpoint (execution_impl.h:150-160, boolean_result.cpp:758-770).
326pub fn boolean_with_token(
327    mesh_a: &ManifoldImpl,
328    mesh_b: &ManifoldImpl,
329    op: OpType,
330    token: Option<&CancelToken>,
331) -> ManifoldImpl {
332    // Entry gate: a token cancelled before the call wins over every fast path
333    // below, including the empty-input ones. C++ does the same at its outermost
334    // gates (csg_tree.cpp:172, execution_impl.cpp's static factories), so an
335    // already-cancelled context never reports NoError.
336    if is_cancelled(token) {
337        return cancelled_impl();
338    }
339    if mesh_a.is_empty() {
340        return match op {
341            OpType::Add => mesh_b.clone(),
342            OpType::Intersect => ManifoldImpl::new(),
343            OpType::Subtract => ManifoldImpl::new(),
344        };
345    }
346    if mesh_b.is_empty() {
347        return match op {
348            OpType::Add | OpType::Subtract => mesh_a.clone(),
349            OpType::Intersect => ManifoldImpl::new(),
350        };
351    }
352
353    if !mesh_a.bbox.does_overlap_box(&mesh_b.bbox) {
354        // Non-overlapping fast paths. For Subtract, we still run through the full
355        // boolean_result to preserve both meshes' run metadata (C++ behavior).
356        match op {
357            OpType::Add => return compose_meshes(&[mesh_a.clone(), mesh_b.clone()]),
358            OpType::Intersect => return ManifoldImpl::new(),
359            OpType::Subtract => {} // fall through to full boolean
360        }
361    }
362
363    // Full boolean — compute intersections
364    let Some(bool3) = Boolean3::new_with_token(mesh_a, mesh_b, op, token) else {
365        return cancelled_impl();
366    };
367    if !bool3.valid {
368        return ManifoldImpl::new();
369    }
370
371    crate::boolean_result::boolean_result_with_token(mesh_a, mesh_b, op, &bool3, token)
372}
373
374/// The observable result of an interrupted operation: an empty mesh carrying
375/// [`Error::Cancelled`]. Mirrors C++ `MakeEmpty(Manifold::Error::Cancelled)`.
376pub(crate) fn cancelled_impl() -> ManifoldImpl {
377    let mut out = ManifoldImpl::new();
378    out.make_empty(Error::Cancelled);
379    out
380}
381
382/// Cast a ray segment from `origin` to `endpoint` against `mesh`, returning
383/// all triangle intersections sorted by parametric distance.
384///
385/// Mirrors C++ `Manifold::Impl::RayCast(vec3, vec3)` in boolean3.cpp.
386/// Builds a degenerate single-edge Impl representing the ray, then uses
387/// Kernel12 (edge-face intersection) with the mesh BVH to find hits.
388pub fn ray_cast(mesh: &ManifoldImpl, origin: Vec3, endpoint: Vec3) -> Vec<RayHit> {
389    if mesh.is_empty() {
390        return vec![];
391    }
392    let dir = endpoint - origin;
393    if dot(dir, dir) == 0.0 {
394        return vec![];
395    }
396
397    // Build a minimal single-edge Impl representing the ray segment.
398    // halfedge[0]: forward (0→1), halfedge[1]: backward (1→0).
399    let mut ray_impl = ManifoldImpl::new();
400    ray_impl.vert_pos = vec![origin, endpoint];
401    ray_impl.vert_normal = vec![Vec3::splat(0.0), Vec3::splat(0.0)];
402    ray_impl.halfedge = vec![
403        Halfedge { start_vert: 0, end_vert: 1, paired_halfedge: 1, prop_vert: 0 },
404        Halfedge { start_vert: 1, end_vert: 0, paired_halfedge: 0, prop_vert: 0 },
405    ];
406    ray_impl.face_normal = vec![Vec3::splat(0.0)];
407
408    // Query the mesh's cached face BVH (C++ RayCast uses collider_).
409    let collider = &mesh.collider;
410
411    // Ray AABB for BVH query.
412    let ray_box = BBox::from_points(
413        Vec3::new(origin.x.min(endpoint.x), origin.y.min(endpoint.y), origin.z.min(endpoint.z)),
414        Vec3::new(origin.x.max(endpoint.x), origin.y.max(endpoint.y), origin.z.max(endpoint.z)),
415    );
416
417    // Determine which component axis is largest for stable t computation.
418    let abs_dir = Vec3::new(dir.x.abs(), dir.y.abs(), dir.z.abs());
419    let t_axis = if abs_dir.x > abs_dir.y && abs_dir.x > abs_dir.z {
420        0usize
421    } else if abs_dir.y > abs_dir.z {
422        1
423    } else {
424        2
425    };
426
427    let mut hits: Vec<RayHit> = Vec::new();
428
429    // Query BVH with ray AABB and test each candidate triangle.
430    collider.collisions_with_boxes(std::slice::from_ref(&ray_box), false, |_qi, tri| {
431        // halfedge 0 (forward) vs triangle tri; expand_p=false, forward=true.
432        let (s, v) = kernel12(0, tri, &ray_impl, mesh, &ray_impl, mesh, false, true);
433        if s != 0 && v.x.is_finite() {
434            // Compute parametric t along the ray.
435            let origin_t = [origin.x, origin.y, origin.z][t_axis];
436            let dir_t = [dir.x, dir.y, dir.z][t_axis];
437            let v_t = [v.x, v.y, v.z][t_axis];
438            let t = (v_t - origin_t) / dir_t;
439            if t >= 0.0 && t <= 1.0 {
440                hits.push(RayHit {
441                    face_id: tri as u64,
442                    distance: t,
443                    position: v,
444                    normal: mesh.face_normal[tri],
445                });
446            }
447        }
448    });
449
450    hits.sort_by(|a, b| a.distance.partial_cmp(&b.distance).unwrap_or(std::cmp::Ordering::Equal));
451    hits
452}
453
454#[cfg(test)]
455#[path = "boolean3_tests.rs"]
456mod tests;