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` is clean-by-default: it picks the faster `Exact` engine only when
397/// correctness is not at risk — i.e. when **both** operands are topologically
398/// manifold (not soup) **and** free of self-intersection — no two of an
399/// operand's own triangles crossing, overlapping, or coinciding. Either
400/// condition failing routes the pair to `Robust`, because the exact engine's
401/// kernels assume complete halfedge pairing and a non-self-intersecting
402/// surface; on self-intersecting-but-manifold inputs it silently
403/// mis-integrates the result (e.g. Thingi10K #92068's triple-wound
404/// concentric shells).
405///
406/// The self-intersection test is cached per impl (see
407/// [`crate::robust::soup::has_self_intersections`]), so an operand pays for
408/// the scan at most once. `Exact` with a soup operand yields an empty result
409/// with `Error::NotManifold` (the guard inside [`boolean_with_token`]); no
410/// panic-catching is involved anywhere — dispatch is input-based only.
411pub fn boolean_dispatch(
412 mesh_a: &ManifoldImpl,
413 mesh_b: &ManifoldImpl,
414 op: OpType,
415 engine: crate::types::BooleanEngine,
416 token: Option<&CancelToken>,
417) -> ManifoldImpl {
418 boolean_dispatch_with_progress(mesh_a, mesh_b, op, engine, token, None)
419}
420
421/// [`boolean_dispatch`] with optional progress reporting (see
422/// [`crate::progress`]).
423///
424/// The robust engine reports its pipeline phases; the exact engine reports a
425/// single indeterminate `ExactBoolean` phase and is otherwise untouched, so
426/// its timing and results are exactly what they were. `None` is byte-for-byte
427/// [`boolean_dispatch`].
428pub fn boolean_dispatch_with_progress(
429 mesh_a: &ManifoldImpl,
430 mesh_b: &ManifoldImpl,
431 op: OpType,
432 engine: crate::types::BooleanEngine,
433 token: Option<&CancelToken>,
434 progress: Option<&crate::progress::ProgressReporter>,
435) -> ManifoldImpl {
436 boolean_dispatch_full(
437 mesh_a,
438 mesh_b,
439 op,
440 engine,
441 crate::types::WindingRule::Positive,
442 token,
443 progress,
444 )
445}
446
447/// [`boolean_dispatch_with_progress`] with an explicit winding rule.
448///
449/// Winding rules are a robust-engine semantic: the exact engine has no cell
450/// labels to reinterpret and **ignores** `rule` entirely. Because of that,
451/// `Auto` with [`WindingRule::Nonzero`] resolves to `Robust` even for two
452/// clean manifold operands — nonzero semantics can only be honored there, and
453/// silently answering with positive-rule geometry would be worse than paying
454/// for the robust pipeline. An explicit `Exact` still runs the exact engine,
455/// rule and all, so a caller who pinned the engine gets exactly what it asked
456/// for.
457///
458/// [`WindingRule::Positive`] is byte-for-byte
459/// [`boolean_dispatch_with_progress`], including `Auto`'s resolution.
460pub fn boolean_dispatch_full(
461 mesh_a: &ManifoldImpl,
462 mesh_b: &ManifoldImpl,
463 op: OpType,
464 engine: crate::types::BooleanEngine,
465 rule: crate::types::WindingRule,
466 token: Option<&CancelToken>,
467 progress: Option<&crate::progress::ProgressReporter>,
468) -> ManifoldImpl {
469 use crate::types::BooleanEngine as E;
470 use crate::types::WindingRule;
471 let resolved = match engine {
472 E::Auto => {
473 use crate::robust::soup::has_self_intersections_with_token as self_isect;
474 if rule == WindingRule::Nonzero
475 || mesh_a.is_soup
476 || mesh_b.is_soup
477 || self_isect(mesh_a, token)
478 || self_isect(mesh_b, token)
479 {
480 E::Robust
481 } else {
482 E::Exact
483 }
484 }
485 other => other,
486 };
487 match resolved {
488 E::Exact | E::Auto => {
489 crate::progress::begin_phase(progress, crate::progress::Phase::ExactBoolean, 0);
490 boolean_with_token(mesh_a, mesh_b, op, token)
491 }
492 E::Robust => {
493 crate::robust::boolean_with_rule(mesh_a, mesh_b, op, rule, token, progress)
494 }
495 }
496}
497
498/// The observable result of an interrupted operation: an empty mesh carrying
499/// [`Error::Cancelled`]. Mirrors C++ `MakeEmpty(Manifold::Error::Cancelled)`.
500pub(crate) fn cancelled_impl() -> ManifoldImpl {
501 let mut out = ManifoldImpl::new();
502 out.make_empty(Error::Cancelled);
503 out
504}
505
506/// Cast a ray segment from `origin` to `endpoint` against `mesh`, returning
507/// all triangle intersections sorted by parametric distance.
508///
509/// Mirrors C++ `Manifold::Impl::RayCast(vec3, vec3)` in boolean3.cpp.
510/// Builds a degenerate single-edge Impl representing the ray, then uses
511/// Kernel12 (edge-face intersection) with the mesh BVH to find hits.
512pub fn ray_cast(mesh: &ManifoldImpl, origin: Vec3, endpoint: Vec3) -> Vec<RayHit> {
513 if mesh.is_empty() {
514 return vec![];
515 }
516 let dir = endpoint - origin;
517 if dot(dir, dir) == 0.0 {
518 return vec![];
519 }
520
521 // Build a minimal single-edge Impl representing the ray segment.
522 // halfedge[0]: forward (0→1), halfedge[1]: backward (1→0).
523 let mut ray_impl = ManifoldImpl::new();
524 ray_impl.vert_pos = vec![origin, endpoint];
525 ray_impl.vert_normal = vec![Vec3::splat(0.0), Vec3::splat(0.0)];
526 ray_impl.halfedge = vec![
527 Halfedge { start_vert: 0, end_vert: 1, paired_halfedge: 1, prop_vert: 0 },
528 Halfedge { start_vert: 1, end_vert: 0, paired_halfedge: 0, prop_vert: 0 },
529 ];
530 ray_impl.face_normal = vec![Vec3::splat(0.0)];
531
532 // Query the mesh's cached face BVH (C++ RayCast uses collider_).
533 let collider = &mesh.collider;
534
535 // Ray AABB for BVH query.
536 let ray_box = BBox::from_points(
537 Vec3::new(origin.x.min(endpoint.x), origin.y.min(endpoint.y), origin.z.min(endpoint.z)),
538 Vec3::new(origin.x.max(endpoint.x), origin.y.max(endpoint.y), origin.z.max(endpoint.z)),
539 );
540
541 // Determine which component axis is largest for stable t computation.
542 let abs_dir = Vec3::new(dir.x.abs(), dir.y.abs(), dir.z.abs());
543 let t_axis = if abs_dir.x > abs_dir.y && abs_dir.x > abs_dir.z {
544 0usize
545 } else if abs_dir.y > abs_dir.z {
546 1
547 } else {
548 2
549 };
550
551 let mut hits: Vec<RayHit> = Vec::new();
552
553 // Query BVH with ray AABB and test each candidate triangle.
554 collider.collisions_with_boxes(std::slice::from_ref(&ray_box), false, |_qi, tri| {
555 // halfedge 0 (forward) vs triangle tri; expand_p=false, forward=true.
556 let (s, v) = kernel12(0, tri, &ray_impl, mesh, &ray_impl, mesh, false, true);
557 if s != 0 && v.x.is_finite() {
558 // Compute parametric t along the ray.
559 let origin_t = [origin.x, origin.y, origin.z][t_axis];
560 let dir_t = [dir.x, dir.y, dir.z][t_axis];
561 let v_t = [v.x, v.y, v.z][t_axis];
562 let t = (v_t - origin_t) / dir_t;
563 if t >= 0.0 && t <= 1.0 {
564 hits.push(RayHit {
565 face_id: tri as u64,
566 distance: t,
567 position: v,
568 normal: mesh.face_normal[tri],
569 });
570 }
571 }
572 });
573
574 hits.sort_by(|a, b| a.distance.partial_cmp(&b.distance).unwrap_or(std::cmp::Ordering::Equal));
575 hits
576}
577
578#[cfg(test)]
579#[path = "boolean3_tests.rs"]
580mod tests;