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 std::collections::HashSet;
32
33use crate::collider::Collider;
34use crate::disjoint_sets::DisjointSets;
35use crate::impl_mesh::ManifoldImpl;
36use crate::linalg::{dot, IVec3, Vec2, Vec3, Vec4};
37use crate::sort::get_face_box_morton;
38use crate::types::{Box as BBox, Halfedge, OpType, RayHit, TriRef};
39
40// ---------------------------------------------------------------------------
41// Intersections — sparse intersection data between two meshes
42// ---------------------------------------------------------------------------
43
44/// Stores the intersections of edges of one mesh with faces of the other.
45/// In forward mode: edges of P with faces of Q.
46/// In reverse mode: edges of Q with faces of P.
47#[derive(Clone, Default)]
48pub struct Intersections {
49    /// Pairs [edge_idx, face_idx] — in forward mode [p1, q2], reverse [q1, p2]
50    pub p1q2: Vec<[i32; 2]>,
51    /// Winding number contribution at each intersection
52    pub x12: Vec<i32>,
53    /// 3D position of each intersection vertex
54    pub v12: Vec<Vec3>,
55}
56
57// ---------------------------------------------------------------------------
58// Boolean3 — the core intersection computation
59// ---------------------------------------------------------------------------
60
61/// Computes all edge-face intersections and winding numbers between two meshes.
62pub struct Boolean3 {
63    pub xv12: Intersections,
64    pub xv21: Intersections,
65    pub w03: Vec<i32>,
66    pub w30: Vec<i32>,
67    pub expand_p: bool,
68    pub valid: bool,
69}
70
71// ---------------------------------------------------------------------------
72// Geometric kernel functions
73// ---------------------------------------------------------------------------
74// These are the ONLY places where floating-point operations occur in the
75// boolean algorithm. They are carefully designed to minimize rounding error
76// and to eliminate it at edge cases. The branch structure must exactly match
77// the C++ to produce identical results.
78
79#[inline]
80fn with_sign(pos: bool, v: f64) -> f64 {
81    if pos { v } else { -v }
82}
83
84/// Interpolate along edge (aL, aR) at x-coordinate `x`.
85/// Returns (y, z) at the interpolated point.
86/// Uses the closer endpoint as the base to minimize rounding error.
87fn interpolate(a_l: Vec3, a_r: Vec3, x: f64) -> Vec2 {
88    let dx_l = x - a_l.x;
89    let dx_r = x - a_r.x;
90    debug_assert!(
91        dx_l * dx_r <= 0.0,
92        "Boolean manifold error: not in domain"
93    );
94    let use_l = dx_l.abs() < dx_r.abs();
95    let d_lr = a_r - a_l;
96    let lambda = (if use_l { dx_l } else { dx_r }) / d_lr.x;
97    if !lambda.is_finite() || !d_lr.y.is_finite() || !d_lr.z.is_finite() {
98        return Vec2::new(a_l.y, a_l.z);
99    }
100    Vec2::new(
101        lambda * d_lr.y + if use_l { a_l.y } else { a_r.y },
102        lambda * d_lr.z + if use_l { a_l.z } else { a_r.z },
103    )
104}
105
106/// Find the intersection of two edges projected onto the yz-plane, parameterized
107/// by their y-coordinates. Returns (x, y, z_a, z_b) at the intersection.
108fn intersect_edges(a_l: Vec3, a_r: Vec3, b_l: Vec3, b_r: Vec3) -> Vec4 {
109    let dy_l = b_l.y - a_l.y;
110    let dy_r = b_r.y - a_r.y;
111    debug_assert!(
112        dy_l * dy_r <= 0.0,
113        "Boolean manifold error: no intersection"
114    );
115    let use_l = dy_l.abs() < dy_r.abs();
116    let dx = a_r.x - a_l.x;
117    let mut lambda = (if use_l { dy_l } else { dy_r }) / (dy_l - dy_r);
118    if !lambda.is_finite() {
119        lambda = 0.0;
120    }
121    let x = lambda * dx + if use_l { a_l.x } else { a_r.x };
122    let a_dy = a_r.y - a_l.y;
123    let b_dy = b_r.y - b_l.y;
124    let use_a = a_dy.abs() < b_dy.abs();
125    let y = lambda * (if use_a { a_dy } else { b_dy })
126        + if use_l {
127            if use_a { a_l.y } else { b_l.y }
128        } else if use_a {
129            a_r.y
130        } else {
131            b_r.y
132        };
133    let z = lambda * (a_r.z - a_l.z) + if use_l { a_l.z } else { a_r.z };
134    let w = lambda * (b_r.z - b_l.z) + if use_l { b_l.z } else { b_r.z };
135    Vec4::new(x, y, z, w)
136}
137
138/// Symbolic perturbation shadow predicate.
139/// When p == q, the tie is broken by the sign of dir.
140#[inline]
141fn shadows(p: f64, q: f64, dir: f64) -> bool {
142    if p == q { dir < 0.0 } else { p < q }
143}
144
145// ---------------------------------------------------------------------------
146// Shadow01 — vertex-edge shadow test
147// ---------------------------------------------------------------------------
148// Tests whether vertex a0 of mesh A shadows edge b1 of mesh B.
149// Returns (winding contribution, (y,z) interpolated position).
150
151fn shadow01(
152    a0: usize,
153    b1: usize,
154    in_a: &ManifoldImpl,
155    in_b: &ManifoldImpl,
156    expand_p: bool,
157    forward: bool,
158) -> (i32, Vec2) {
159    let b1s = in_b.halfedge[b1].start_vert as usize;
160    let b1e = in_b.halfedge[b1].end_vert as usize;
161    let a0x = in_a.vert_pos[a0].x;
162    let b1sx = in_b.vert_pos[b1s].x;
163    let b1ex = in_b.vert_pos[b1e].x;
164    let a0xp = in_a.vert_normal[a0].x;
165    let b1sxp = in_b.vert_normal[b1s].x;
166    let b1exp = in_b.vert_normal[b1e].x;
167
168    let mut s01 = if forward {
169        shadows(a0x, b1ex, with_sign(expand_p, a0xp) - b1exp) as i32
170            - shadows(a0x, b1sx, with_sign(expand_p, a0xp) - b1sxp) as i32
171    } else {
172        shadows(b1sx, a0x, with_sign(expand_p, b1sxp) - a0xp) as i32
173            - shadows(b1ex, a0x, with_sign(expand_p, b1exp) - a0xp) as i32
174    };
175
176    let mut yz01 = Vec2::new(f64::NAN, f64::NAN);
177
178    if s01 != 0 {
179        yz01 = interpolate(in_b.vert_pos[b1s], in_b.vert_pos[b1e], in_a.vert_pos[a0].x);
180        let b1pair = in_b.halfedge[b1].paired_halfedge as usize;
181        let dir = in_b.face_normal[b1 / 3].y + in_b.face_normal[b1pair / 3].y;
182        if forward {
183            if !shadows(in_a.vert_pos[a0].y, yz01.x, -dir) {
184                s01 = 0;
185            }
186        } else if !shadows(yz01.x, in_a.vert_pos[a0].y, with_sign(expand_p, dir)) {
187            s01 = 0;
188        }
189    }
190    (s01, yz01)
191}
192
193// ---------------------------------------------------------------------------
194// Kernel11 — edge-edge intersection
195// ---------------------------------------------------------------------------
196
197fn kernel11(
198    p1: usize,
199    q1: usize,
200    in_p: &ManifoldImpl,
201    in_q: &ManifoldImpl,
202    expand_p: bool,
203) -> (i32, Vec4) {
204    let mut xyzz11 = Vec4::splat(f64::NAN);
205    let mut s11: i32 = 0;
206
207    let mut k: usize = 0;
208    let mut p_rl = [Vec3::splat(0.0); 2];
209    let mut q_rl = [Vec3::splat(0.0); 2];
210    let mut shadow_state = false;
211
212    let p0 = [
213        in_p.halfedge[p1].start_vert as usize,
214        in_p.halfedge[p1].end_vert as usize,
215    ];
216    for i in 0..2 {
217        let (s01, yz01) = shadow01(p0[i], q1, in_p, in_q, expand_p, true);
218        if yz01.x.is_finite() {
219            s11 += s01 * if i == 0 { -1 } else { 1 };
220            if k < 2 && (k == 0 || (s01 != 0) != shadow_state) {
221                shadow_state = s01 != 0;
222                p_rl[k] = in_p.vert_pos[p0[i]];
223                q_rl[k] = Vec3::new(p_rl[k].x, yz01.x, yz01.y);
224                k += 1;
225            }
226        }
227    }
228
229    let q0 = [
230        in_q.halfedge[q1].start_vert as usize,
231        in_q.halfedge[q1].end_vert as usize,
232    ];
233    for i in 0..2 {
234        let (s10, yz10) = shadow01(q0[i], p1, in_q, in_p, expand_p, false);
235        if yz10.x.is_finite() {
236            s11 += s10 * if i == 0 { -1 } else { 1 };
237            if k < 2 && (k == 0 || (s10 != 0) != shadow_state) {
238                shadow_state = s10 != 0;
239                q_rl[k] = in_q.vert_pos[q0[i]];
240                p_rl[k] = Vec3::new(q_rl[k].x, yz10.x, yz10.y);
241                k += 1;
242            }
243        }
244    }
245
246    if s11 == 0 {
247        xyzz11 = Vec4::splat(f64::NAN);
248    } else {
249        debug_assert_eq!(k, 2, "Boolean manifold error: s11");
250        xyzz11 = intersect_edges(p_rl[0], p_rl[1], q_rl[0], q_rl[1]);
251
252        let p1pair = in_p.halfedge[p1].paired_halfedge as usize;
253        let dir_p = in_p.face_normal[p1 / 3].z + in_p.face_normal[p1pair / 3].z;
254        let q1pair = in_q.halfedge[q1].paired_halfedge as usize;
255        let dir_q = in_q.face_normal[q1 / 3].z + in_q.face_normal[q1pair / 3].z;
256        if !shadows(xyzz11.z, xyzz11.w, with_sign(expand_p, dir_p) - dir_q) {
257            s11 = 0;
258        }
259    }
260
261    (s11, xyzz11)
262}
263
264// ---------------------------------------------------------------------------
265// Kernel02 — vertex-face intersection
266// ---------------------------------------------------------------------------
267
268fn kernel02(
269    a0: usize,
270    b2: usize,
271    in_a: &ManifoldImpl,
272    in_b: &ManifoldImpl,
273    expand_p: bool,
274    forward: bool,
275) -> (i32, f64) {
276    let mut s02: i32 = 0;
277    let mut z02: f64 = 0.0;
278
279    let mut k: usize = 0;
280    let mut yzz_rl = [Vec3::splat(0.0); 2];
281    let mut shadow_state = false;
282
283    for i in 0..3 {
284        let b1 = 3 * b2 + i;
285        let edge_b = in_b.halfedge[b1];
286        let b1f = if edge_b.is_forward() {
287            b1
288        } else {
289            edge_b.paired_halfedge as usize
290        };
291
292        let (s01, yz01) = shadow01(a0, b1f, in_a, in_b, expand_p, forward);
293        if yz01.x.is_finite() {
294            s02 += s01 * if forward == edge_b.is_forward() { -1 } else { 1 };
295            if k < 2 && (k == 0 || (s01 != 0) != shadow_state) {
296                shadow_state = s01 != 0;
297                yzz_rl[k] = Vec3::new(yz01.x, yz01.y, yz01.y);
298                k += 1;
299            }
300        }
301    }
302
303    if s02 == 0 {
304        z02 = f64::NAN;
305    } else {
306        debug_assert_eq!(k, 2, "Boolean manifold error: s02");
307        let vert_pos_a = in_a.vert_pos[a0];
308        z02 = interpolate(yzz_rl[0], yzz_rl[1], vert_pos_a.y).y;
309        if forward {
310            if !shadows(vert_pos_a.z, z02, -in_b.face_normal[b2].z) {
311                s02 = 0;
312            }
313        } else if !shadows(z02, vert_pos_a.z, with_sign(expand_p, in_b.face_normal[b2].z)) {
314            s02 = 0;
315        }
316    }
317    (s02, z02)
318}
319
320// ---------------------------------------------------------------------------
321// Kernel12 — edge-face intersection
322// ---------------------------------------------------------------------------
323
324fn kernel12(
325    a1: usize,
326    b2: usize,
327    in_a: &ManifoldImpl,
328    in_b: &ManifoldImpl,
329    in_p: &ManifoldImpl,
330    in_q: &ManifoldImpl,
331    expand_p: bool,
332    forward: bool,
333) -> (i32, Vec3) {
334    let mut x12: i32 = 0;
335    let mut v12 = Vec3::splat(f64::NAN);
336
337    let mut k: usize = 0;
338    let mut xzy_lr0 = [Vec3::splat(0.0); 2];
339    let mut xzy_lr1 = [Vec3::splat(0.0); 2];
340    let mut shadow_state = false;
341
342    let edge_a = in_a.halfedge[a1];
343
344    for vert_a in [edge_a.start_vert as usize, edge_a.end_vert as usize] {
345        let (s, z) = kernel02(vert_a, b2, in_a, in_b, expand_p, forward);
346        if z.is_finite() {
347            x12 += s * if (vert_a == edge_a.start_vert as usize) == forward { 1 } else { -1 };
348            if k < 2 && (k == 0 || (s != 0) != shadow_state) {
349                shadow_state = s != 0;
350                let pos = in_a.vert_pos[vert_a];
351                xzy_lr0[k] = Vec3::new(pos.x, pos.z, pos.y);
352                xzy_lr1[k] = xzy_lr0[k];
353                xzy_lr1[k].y = z;
354                k += 1;
355            }
356        }
357    }
358
359    for i in 0..3 {
360        let b1 = 3 * b2 + i;
361        let edge_b = in_b.halfedge[b1];
362        let b1f = if edge_b.is_forward() {
363            b1
364        } else {
365            edge_b.paired_halfedge as usize
366        };
367        let (s, xyzz) = if forward {
368            kernel11(a1, b1f, in_p, in_q, expand_p)
369        } else {
370            kernel11(b1f, a1, in_p, in_q, expand_p)
371        };
372        if xyzz.x.is_finite() {
373            x12 -= s * if edge_b.is_forward() { 1 } else { -1 };
374            if k < 2 && (k == 0 || (s != 0) != shadow_state) {
375                shadow_state = s != 0;
376                xzy_lr0[k] = Vec3::new(xyzz.x, xyzz.z, xyzz.y);
377                xzy_lr1[k] = xzy_lr0[k];
378                xzy_lr1[k].y = xyzz.w;
379                if !forward {
380                    let tmp = xzy_lr0[k].y;
381                    xzy_lr0[k].y = xzy_lr1[k].y;
382                    xzy_lr1[k].y = tmp;
383                }
384                k += 1;
385            }
386        }
387    }
388
389    if x12 == 0 {
390        v12 = Vec3::splat(f64::NAN);
391    } else {
392        debug_assert_eq!(k, 2, "Boolean manifold error: v12");
393        let xzyy = intersect_edges(xzy_lr0[0], xzy_lr0[1], xzy_lr1[0], xzy_lr1[1]);
394        v12.x = xzyy.x;
395        v12.y = xzyy.z;
396        v12.z = xzyy.y;
397    }
398    (x12, v12)
399}
400
401// ---------------------------------------------------------------------------
402// Intersect12 — find all edge-face intersections using collider broadphase
403// ---------------------------------------------------------------------------
404
405fn intersect12(
406    in_p: &ManifoldImpl,
407    in_q: &ManifoldImpl,
408    expand_p: bool,
409    forward: bool,
410) -> Intersections {
411    // a: edge mesh, b: face mesh
412    let (a, b) = if forward { (in_p, in_q) } else { (in_q, in_p) };
413
414    let (face_box, face_morton) = get_face_box_morton(b);
415    let collider = Collider::new(face_box, face_morton);
416
417    let mut result = Intersections::default();
418
419    // For each forward edge of a, query its bounding box against b's face BVH
420    // and run kernel12 on each candidate. Per-edge work is independent and the
421    // final stable sort below fully orders the unique (edge, face) pairs, so
422    // the parallel path is bit-identical to the sequential one.
423    let n = a.halfedge.len();
424    let per_edge: Vec<Vec<([i32; 2], i32, Vec3)>> =
425        crate::par::maybe_par_map(n, 10_000, |query_idx| {
426            let mut local: Vec<([i32; 2], i32, Vec3)> = Vec::new();
427            if !a.halfedge[query_idx].is_forward() {
428                return local;
429            }
430            let query = BBox::from_points(
431                a.vert_pos[a.halfedge[query_idx].start_vert as usize],
432                a.vert_pos[a.halfedge[query_idx].end_vert as usize],
433            );
434            collider.collisions_one(&query, query_idx, |query_idx, face_idx| {
435                let (x, v) =
436                    kernel12(query_idx, face_idx, a, b, in_p, in_q, expand_p, forward);
437                if v.x.is_finite() {
438                    let pair = if forward {
439                        [query_idx as i32, face_idx as i32]
440                    } else {
441                        [face_idx as i32, query_idx as i32]
442                    };
443                    local.push((pair, x, v));
444                }
445            });
446            local
447        });
448    for local in per_edge {
449        for (pair, x, v) in local {
450            result.p1q2.push(pair);
451            result.x12.push(x);
452            result.v12.push(v);
453        }
454    }
455
456    // Sort by edge index for deterministic results
457    let mut indices: Vec<usize> = (0..result.p1q2.len()).collect();
458    let sort_idx = if forward { 0 } else { 1 };
459    indices.sort_by(|&a, &b| {
460        let pa = result.p1q2[a];
461        let pb = result.p1q2[b];
462        pa[sort_idx]
463            .cmp(&pb[sort_idx])
464            .then(pa[1 - sort_idx].cmp(&pb[1 - sort_idx]))
465    });
466
467    let old_p1q2 = result.p1q2.clone();
468    let old_x12 = result.x12.clone();
469    let old_v12 = result.v12.clone();
470    for (new_i, &old_i) in indices.iter().enumerate() {
471        result.p1q2[new_i] = old_p1q2[old_i];
472        result.x12[new_i] = old_x12[old_i];
473        result.v12[new_i] = old_v12[old_i];
474    }
475
476    result
477}
478
479// ---------------------------------------------------------------------------
480// Winding03 — compute winding numbers via flood-fill
481// ---------------------------------------------------------------------------
482// Groups vertices into connected components along unbroken edges (edges not
483// cut by any intersection). For each component, picks a representative vertex
484// and computes its winding number via kernel02 against all overlapping faces
485// of the other mesh. Then flood-fills that winding number to all vertices in
486// the component.
487
488fn winding03(
489    in_p: &ManifoldImpl,
490    in_q: &ManifoldImpl,
491    p1q2: &[[i32; 2]],
492    expand_p: bool,
493    forward: bool,
494) -> Vec<i32> {
495    let (a, b) = if forward { (in_p, in_q) } else { (in_q, in_p) };
496    let sort_idx = if forward { 0 } else { 1 };
497
498    // Build union-find: unite vertices along unbroken edges
499    let u_a = DisjointSets::new(a.vert_pos.len() as u32);
500    for edge in 0..a.halfedge.len() {
501        let he = &a.halfedge[edge];
502        if !he.is_forward() {
503            continue;
504        }
505        // Check if this edge is broken (has an intersection)
506        let is_broken = p1q2
507            .binary_search_by(|pair| pair[sort_idx].cmp(&(edge as i32)))
508            .is_ok();
509        if !is_broken {
510            u_a.unite(he.start_vert as u32, he.end_vert as u32);
511        }
512    }
513
514    // Find unique component representatives
515    let mut components = HashSet::new();
516    for v in 0..a.vert_pos.len() {
517        components.insert(u_a.find(v as u32));
518    }
519    let verts: Vec<usize> = components.into_iter().map(|v| v as usize).collect();
520
521    // Build face collider for mesh b
522    let (face_box, face_morton) = get_face_box_morton(b);
523    let collider = Collider::new(face_box, face_morton);
524
525    // For each representative vertex, compute winding number via kernel02
526    let mut w03 = vec![0i32; a.vert_pos.len()];
527
528    // Use BVH for winding number queries.
529    // The winding number shoots a Z-ray, so we need XY overlap with infinite Z.
530    // Build query boxes with the vertex XY position and infinite Z extent.
531    let query_boxes: Vec<(usize, BBox)> = verts.iter().map(|&vi| {
532        let pt = a.vert_pos[vi];
533        let qbox = BBox::from_points(
534            Vec3::new(pt.x, pt.y, f64::NEG_INFINITY),
535            Vec3::new(pt.x, pt.y, f64::INFINITY),
536        );
537        (vi, qbox)
538    }).collect();
539
540    // For each representative vert, query the BVH and sum kernel02 winding
541    // contributions. The sums are integers, so accumulation order is
542    // irrelevant and the per-vert work can run in parallel bit-exactly.
543    let sums: Vec<(usize, i32)> =
544        crate::par::maybe_par_map(query_boxes.len(), 1_000, |qi| {
545            let (vi, ref qbox) = query_boxes[qi];
546            let mut sum = 0i32;
547            collider.collisions_one(qbox, 0, |_qi, face_idx| {
548                let (s02, z02) = kernel02(vi, face_idx, a, b, expand_p, forward);
549                if z02.is_finite() {
550                    sum += s02 * if forward { 1 } else { -1 };
551                }
552            });
553            (vi, sum)
554        });
555    for (vi, sum) in sums {
556        w03[vi] += sum;
557    }
558
559    // Flood fill: propagate representative's winding number to all component members
560    for i in 0..w03.len() {
561        let root = u_a.find(i as u32) as usize;
562        if root != i {
563            w03[i] = w03[root];
564        }
565    }
566
567    w03
568}
569
570// ---------------------------------------------------------------------------
571// Boolean3 constructor
572// ---------------------------------------------------------------------------
573
574impl Boolean3 {
575    /// Compute all intersections between meshes inP and inQ for the given op.
576    pub fn new(in_p: &ManifoldImpl, in_q: &ManifoldImpl, op: OpType) -> Self {
577        let expand_p = op == OpType::Add;
578
579        if in_p.is_empty() || in_q.is_empty() || !in_p.bbox.does_overlap_box(&in_q.bbox) {
580            return Boolean3 {
581                xv12: Intersections::default(),
582                xv21: Intersections::default(),
583                w03: vec![0; in_p.num_vert()],
584                w30: vec![0; in_q.num_vert()],
585                expand_p,
586                valid: true,
587            };
588        }
589
590        // Level 3: find all edge-face intersections in both directions
591        let xv12 = intersect12(in_p, in_q, expand_p, true);
592        let xv21 = intersect12(in_p, in_q, expand_p, false);
593
594        if xv12.x12.len() > i32::MAX as usize || xv21.x12.len() > i32::MAX as usize {
595            return Boolean3 {
596                xv12: Intersections::default(),
597                xv21: Intersections::default(),
598                w03: Vec::new(),
599                w30: Vec::new(),
600                expand_p,
601                valid: false,
602            };
603        }
604
605        // Compute winding numbers via flood fill
606        let w03 = winding03(in_p, in_q, &xv12.p1q2, expand_p, true);
607        let w30 = winding03(in_p, in_q, &xv21.p1q2, expand_p, false);
608
609        Boolean3 {
610            xv12,
611            xv21,
612            w03,
613            w30,
614            expand_p,
615            valid: true,
616        }
617    }
618}
619
620// ---------------------------------------------------------------------------
621// compose_meshes — concatenate disjoint meshes (unchanged from before)
622// ---------------------------------------------------------------------------
623
624fn extract_tri_vert(mesh: &ManifoldImpl) -> Vec<IVec3> {
625    (0..mesh.num_tri())
626        .map(|tri| {
627            IVec3::new(
628                mesh.halfedge[3 * tri].start_vert,
629                mesh.halfedge[3 * tri + 1].start_vert,
630                mesh.halfedge[3 * tri + 2].start_vert,
631            )
632        })
633        .collect()
634}
635
636fn extract_tri_prop(mesh: &ManifoldImpl) -> Vec<IVec3> {
637    (0..mesh.num_tri())
638        .map(|tri| {
639            IVec3::new(
640                mesh.halfedge[3 * tri].prop_vert,
641                mesh.halfedge[3 * tri + 1].prop_vert,
642                mesh.halfedge[3 * tri + 2].prop_vert,
643            )
644        })
645        .collect()
646}
647
648fn property_row(mesh: &ManifoldImpl, row: usize, width: usize) -> Vec<f64> {
649    if mesh.num_prop == 0 {
650        vec![0.0; width]
651    } else {
652        let mut out = vec![0.0; width];
653        let src = &mesh.properties[row * mesh.num_prop..(row + 1) * mesh.num_prop];
654        out[..src.len()].copy_from_slice(src);
655        out
656    }
657}
658
659/// Concatenate multiple disjoint meshes into one. This is a genuine utility
660/// used by both boolean operations and CSG compose. It does NOT perform any
661/// boolean intersection — the meshes must be non-overlapping for correct results.
662pub fn compose_meshes(meshes: &[ManifoldImpl]) -> ManifoldImpl {
663    if meshes.is_empty() {
664        return ManifoldImpl::new();
665    }
666    if meshes.len() == 1 {
667        return meshes[0].clone();
668    }
669
670    let num_prop = meshes.iter().map(|m| m.num_prop).max().unwrap_or(0);
671    let mut vert_pos = Vec::new();
672    let mut properties = Vec::new();
673    let mut tri_vert = Vec::new();
674    let mut tri_prop = Vec::new();
675    let mut vert_offset = 0i32;
676    let mut prop_offset = 0i32;
677
678    for mesh in meshes {
679        vert_pos.extend_from_slice(&mesh.vert_pos);
680
681        let old_tri_vert = extract_tri_vert(mesh);
682        let old_tri_prop = extract_tri_prop(mesh);
683        tri_vert.extend(old_tri_vert.into_iter().map(|t| {
684            IVec3::new(t.x + vert_offset, t.y + vert_offset, t.z + vert_offset)
685        }));
686        tri_prop.extend(old_tri_prop.into_iter().map(|t| {
687            IVec3::new(t.x + prop_offset, t.y + prop_offset, t.z + prop_offset)
688        }));
689
690        if num_prop > 0 {
691            let prop_rows = mesh.num_prop_vert();
692            for row in 0..prop_rows {
693                properties.extend(property_row(mesh, row, num_prop));
694            }
695            prop_offset += prop_rows as i32;
696        } else {
697            prop_offset += mesh.num_prop_vert() as i32;
698        }
699        vert_offset += mesh.num_vert() as i32;
700    }
701
702    // Concatenate tri_refs and merge mesh_id_transforms from all input meshes.
703    // Each mesh's coplanar_id is a triangle-local group index, so offset by tri_offset.
704    let mut all_tri_refs: Vec<TriRef> = Vec::new();
705    let mut merged_transforms = std::collections::BTreeMap::new();
706    let mut tri_offset = 0i32;
707    for mesh in meshes {
708        let mesh_tri_count = mesh.num_tri() as i32;
709        for tri_ref in &mesh.mesh_relation.tri_ref {
710            all_tri_refs.push(TriRef {
711                mesh_id: tri_ref.mesh_id,
712                original_id: tri_ref.original_id,
713                face_id: tri_ref.face_id,
714                coplanar_id: tri_ref.coplanar_id + tri_offset,
715            });
716        }
717        for (id, rel) in &mesh.mesh_relation.mesh_id_transform {
718            merged_transforms.insert(*id, rel.clone());
719        }
720        tri_offset += mesh_tri_count;
721    }
722
723    let mut out = ManifoldImpl::new();
724    out.vert_pos = vert_pos;
725    out.num_prop = num_prop;
726    out.properties = properties;
727    out.create_halfedges(&tri_prop, &tri_vert);
728    // Preserve tri_refs and transforms from input meshes instead of
729    // calling initialize_original(), which would lose mesh transform data.
730    out.mesh_relation.tri_ref = all_tri_refs;
731    out.mesh_relation.mesh_id_transform = merged_transforms;
732    out.mesh_relation.original_id = -1;
733    out.calculate_bbox();
734    out.set_epsilon(-1.0, false);
735    // required to remove parts that are smaller than the tolerance (matches C++)
736    crate::edge_op::remove_degenerates(&mut out, 0);
737    out.sort_geometry();
738    out.increment_mesh_ids();
739    out.set_normals_and_coplanar();
740    out
741}
742
743// ---------------------------------------------------------------------------
744// boolean — public entry point
745// ---------------------------------------------------------------------------
746
747/// Perform a 3D boolean operation on two manifold meshes.
748///
749/// For overlapping meshes, uses the full Boolean3 intersection algorithm.
750/// For disjoint meshes, uses fast-path shortcuts.
751pub fn boolean(mesh_a: &ManifoldImpl, mesh_b: &ManifoldImpl, op: OpType) -> ManifoldImpl {
752    if mesh_a.is_empty() {
753        return match op {
754            OpType::Add => mesh_b.clone(),
755            OpType::Intersect => ManifoldImpl::new(),
756            OpType::Subtract => ManifoldImpl::new(),
757        };
758    }
759    if mesh_b.is_empty() {
760        return match op {
761            OpType::Add | OpType::Subtract => mesh_a.clone(),
762            OpType::Intersect => ManifoldImpl::new(),
763        };
764    }
765
766    if !mesh_a.bbox.does_overlap_box(&mesh_b.bbox) {
767        // Non-overlapping fast paths. For Subtract, we still run through the full
768        // boolean_result to preserve both meshes' run metadata (C++ behavior).
769        match op {
770            OpType::Add => return compose_meshes(&[mesh_a.clone(), mesh_b.clone()]),
771            OpType::Intersect => return ManifoldImpl::new(),
772            OpType::Subtract => {} // fall through to full boolean
773        }
774    }
775
776    // Full boolean — compute intersections
777    let bool3 = Boolean3::new(mesh_a, mesh_b, op);
778    if !bool3.valid {
779        return ManifoldImpl::new();
780    }
781
782    let result = crate::boolean_result::boolean_result(mesh_a, mesh_b, op, &bool3);
783    result
784}
785
786/// Cast a ray segment from `origin` to `endpoint` against `mesh`, returning
787/// all triangle intersections sorted by parametric distance.
788///
789/// Mirrors C++ `Manifold::Impl::RayCast(vec3, vec3)` in boolean3.cpp.
790/// Builds a degenerate single-edge Impl representing the ray, then uses
791/// Kernel12 (edge-face intersection) with the mesh BVH to find hits.
792pub fn ray_cast(mesh: &ManifoldImpl, origin: Vec3, endpoint: Vec3) -> Vec<RayHit> {
793    if mesh.is_empty() {
794        return vec![];
795    }
796    let dir = endpoint - origin;
797    if dot(dir, dir) == 0.0 {
798        return vec![];
799    }
800
801    // Build a minimal single-edge Impl representing the ray segment.
802    // halfedge[0]: forward (0→1), halfedge[1]: backward (1→0).
803    let mut ray_impl = ManifoldImpl::new();
804    ray_impl.vert_pos = vec![origin, endpoint];
805    ray_impl.vert_normal = vec![Vec3::splat(0.0), Vec3::splat(0.0)];
806    ray_impl.halfedge = vec![
807        Halfedge { start_vert: 0, end_vert: 1, paired_halfedge: 1, prop_vert: 0 },
808        Halfedge { start_vert: 1, end_vert: 0, paired_halfedge: 0, prop_vert: 0 },
809    ];
810    ray_impl.face_normal = vec![Vec3::splat(0.0)];
811
812    // Build BVH over mesh triangles.
813    let (face_box, face_morton) = get_face_box_morton(mesh);
814    let collider = Collider::new(face_box, face_morton);
815
816    // Ray AABB for BVH query.
817    let ray_box = BBox::from_points(
818        Vec3::new(origin.x.min(endpoint.x), origin.y.min(endpoint.y), origin.z.min(endpoint.z)),
819        Vec3::new(origin.x.max(endpoint.x), origin.y.max(endpoint.y), origin.z.max(endpoint.z)),
820    );
821
822    // Determine which component axis is largest for stable t computation.
823    let abs_dir = Vec3::new(dir.x.abs(), dir.y.abs(), dir.z.abs());
824    let t_axis = if abs_dir.x > abs_dir.y && abs_dir.x > abs_dir.z {
825        0usize
826    } else if abs_dir.y > abs_dir.z {
827        1
828    } else {
829        2
830    };
831
832    let mut hits: Vec<RayHit> = Vec::new();
833
834    // Query BVH with ray AABB and test each candidate triangle.
835    collider.collisions_with_boxes(std::slice::from_ref(&ray_box), false, |_qi, tri| {
836        // halfedge 0 (forward) vs triangle tri; expand_p=false, forward=true.
837        let (s, v) = kernel12(0, tri, &ray_impl, mesh, &ray_impl, mesh, false, true);
838        if s != 0 && v.x.is_finite() {
839            // Compute parametric t along the ray.
840            let origin_t = [origin.x, origin.y, origin.z][t_axis];
841            let dir_t = [dir.x, dir.y, dir.z][t_axis];
842            let v_t = [v.x, v.y, v.z][t_axis];
843            let t = (v_t - origin_t) / dir_t;
844            if t >= 0.0 && t <= 1.0 {
845                hits.push(RayHit {
846                    face_id: tri as u64,
847                    distance: t,
848                    position: v,
849                    normal: mesh.face_normal[tri],
850                });
851            }
852        }
853    });
854
855    hits.sort_by(|a, b| a.distance.partial_cmp(&b.distance).unwrap_or(std::cmp::Ordering::Equal));
856    hits
857}
858
859#[cfg(test)]
860#[path = "boolean3_tests.rs"]
861mod tests;