Skip to main content

manifold_rust/
boolean_result.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 Result — face assembly from intersection data
16//
17// C++ source: src/boolean_result.cpp (889 lines)
18//
19// Takes intersection data from Boolean3 and assembles the output mesh faces.
20// The algorithm:
21// 1. Convert winding numbers to inclusion values based on operation type
22// 2. Compute vertex remapping via exclusive scan (with absolute sum)
23// 3. Create output vertices (retained + new intersection verts)
24// 4. Build edge maps for partial and new edges
25// 5. Size output (count sides per face, allocate halfedges)
26// 6. Assemble edges (partial, new, whole)
27// 7. Triangulate polygonal faces (Face2Tri)
28// 8. Create properties via barycentric interpolation
29// 9. Finalize: simplify topology, sort geometry
30
31use std::collections::BTreeMap;
32
33use crate::impl_mesh::ManifoldImpl;
34use crate::linalg::{Vec3, dot};
35use crate::types::{Halfedge, TriRef};
36
37// ---------------------------------------------------------------------------
38// EdgePos — position of a vertex along an edge for pairing
39// ---------------------------------------------------------------------------
40
41#[derive(Clone)]
42pub(super) struct EdgePos {
43    edge_pos: f64,
44    vert: i32,
45    collision_id: i32,
46    is_start: bool,
47}
48
49impl EdgePos {
50    fn sort_key(&self) -> (OrderedF64, i32) {
51        (OrderedF64(self.edge_pos), self.collision_id)
52    }
53}
54
55/// Wrapper for f64 that implements Ord (for sorting edge positions)
56#[derive(Clone, Copy, PartialEq)]
57struct OrderedF64(f64);
58
59impl Eq for OrderedF64 {}
60
61impl PartialOrd for OrderedF64 {
62    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
63        Some(self.cmp(other))
64    }
65}
66
67impl Ord for OrderedF64 {
68    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
69        self.0.partial_cmp(&other.0).unwrap_or(std::cmp::Ordering::Equal)
70    }
71}
72
73// ---------------------------------------------------------------------------
74// AbsSum — exclusive scan combiner
75// ---------------------------------------------------------------------------
76
77#[inline]
78pub(super) fn abs_sum(a: i32, b: i32) -> i32 {
79    a.abs() + b.abs()
80}
81
82/// Exclusive scan with abs_sum combiner. Output[i] = init + sum(|input[0..i]|).
83pub(super) fn exclusive_scan_abs(input: &[i32], init: i32) -> Vec<i32> {
84    let mut result = Vec::with_capacity(input.len());
85    let mut acc = init;
86    for &v in input {
87        result.push(acc);
88        acc = abs_sum(acc, v);
89    }
90    result
91}
92
93// ---------------------------------------------------------------------------
94// SizeOutput — compute output face sizes and allocate halfedges
95// ---------------------------------------------------------------------------
96
97/// Counts sides per face and allocates output halfedges.
98/// Returns (face_edge, face_pq2r).
99pub(super) fn size_output(
100    out_r: &mut ManifoldImpl,
101    in_p: &ManifoldImpl,
102    in_q: &ManifoldImpl,
103    i03: &[i32],
104    i30: &[i32],
105    i12: &[i32],
106    i21: &[i32],
107    p1q2: &[[i32; 2]],
108    p2q1: &[[i32; 2]],
109    invert_q: bool,
110) -> (Vec<i32>, Vec<i32>) {
111    let num_tri_p = in_p.num_tri();
112    let num_tri_q = in_q.num_tri();
113    let mut sides_per_face = vec![0i32; num_tri_p + num_tri_q];
114
115    // Count retained vertex contributions
116    for face in 0..num_tri_p {
117        for j in 0..3 {
118            let v = in_p.halfedge[3 * face + j].start_vert as usize;
119            sides_per_face[face] += i03[v].abs();
120        }
121    }
122    for face in 0..num_tri_q {
123        for j in 0..3 {
124            let v = in_q.halfedge[3 * face + j].start_vert as usize;
125            sides_per_face[num_tri_p + face] += i30[v].abs();
126        }
127    }
128
129    // Count new intersection vertex contributions
130    for (idx, pair) in p1q2.iter().enumerate() {
131        let edge_p = pair[0] as usize;
132        let face_q = pair[1] as usize;
133        let inclusion = i12[idx].abs();
134        sides_per_face[num_tri_p + face_q] += inclusion;
135        let half = in_p.halfedge[edge_p];
136        sides_per_face[edge_p / 3] += inclusion;
137        sides_per_face[half.paired_halfedge as usize / 3] += inclusion;
138    }
139    for (idx, pair) in p2q1.iter().enumerate() {
140        let face_p = pair[0] as usize;
141        let edge_q = pair[1] as usize;
142        let inclusion = i21[idx].abs();
143        sides_per_face[face_p] += inclusion;
144        let half = in_q.halfedge[edge_q];
145        sides_per_face[num_tri_p + edge_q / 3] += inclusion;
146        sides_per_face[num_tri_p + half.paired_halfedge as usize / 3] += inclusion;
147    }
148
149    // Build face_pq2r: maps input face → output face index
150    let mut face_pq2r = vec![0i32; num_tri_p + num_tri_q];
151    let mut count = 0i32;
152    for i in 0..sides_per_face.len() {
153        face_pq2r[i] = count;
154        if sides_per_face[i] > 0 {
155            count += 1;
156        }
157    }
158    let num_face_r = count as usize;
159
160    // Build face normals for output
161    out_r.face_normal.resize(num_face_r, Vec3::splat(0.0));
162    let mut out_idx = 0;
163    for i in 0..num_tri_p {
164        if sides_per_face[i] > 0 {
165            out_r.face_normal[out_idx] = in_p.face_normal[i];
166            out_idx += 1;
167        }
168    }
169    for i in 0..num_tri_q {
170        if sides_per_face[num_tri_p + i] > 0 {
171            let normal = in_q.face_normal[i];
172            out_r.face_normal[out_idx] = if invert_q {
173                Vec3::new(-normal.x, -normal.y, -normal.z)
174            } else {
175                normal
176            };
177            out_idx += 1;
178        }
179    }
180
181    // Build face_edge: cumulative edge counts for each output face
182    let active_sides: Vec<i32> = sides_per_face.iter().filter(|&&s| s > 0).copied().collect();
183    let mut face_edge = vec![0i32; active_sides.len() + 1];
184    for (i, &s) in active_sides.iter().enumerate() {
185        face_edge[i + 1] = face_edge[i] + s;
186    }
187    let num_halfedge = *face_edge.last().unwrap() as usize;
188    out_r.halfedge.resize(
189        num_halfedge,
190        Halfedge {
191            start_vert: -1,
192            end_vert: -1,
193            paired_halfedge: -1,
194            prop_vert: -1,
195        },
196    );
197
198    (face_edge, face_pq2r)
199}
200
201// ---------------------------------------------------------------------------
202// AddNewEdgeVerts — populate edge maps with intersection vertices
203// ---------------------------------------------------------------------------
204
205pub(super) fn add_new_edge_verts(
206    edges_p: &mut BTreeMap<i32, Vec<EdgePos>>,
207    edges_new: &mut BTreeMap<(i32, i32), Vec<EdgePos>>,
208    p1q2: &[[i32; 2]],
209    i12: &[i32],
210    v12r: &[i32],
211    halfedge_p: &[Halfedge],
212    forward: bool,
213    offset: usize,
214) {
215    for i in 0..p1q2.len() {
216        let edge_p = p1q2[i][if forward { 0 } else { 1 }];
217        let face_q = p1q2[i][if forward { 1 } else { 0 }];
218        let vert = v12r[i];
219        let inclusion = i12[i];
220
221        let halfedge = halfedge_p[edge_p as usize];
222        let mut key_right = (halfedge.paired_halfedge / 3, face_q);
223        if !forward {
224            key_right = (key_right.1, key_right.0);
225        }
226        let mut key_left = (edge_p / 3, face_q);
227        if !forward {
228            key_left = (key_left.1, key_left.0);
229        }
230
231        let direction = inclusion < 0;
232        let collision_id = (i + offset) as i32;
233
234        // C++ captures all three is_start values at array creation time:
235        // edgesP: direction
236        // edgesNew[keyRight]: direction ^ !forward
237        // edgesNew[keyLeft]: direction ^ forward
238        let dir_p = direction;
239        let dir_right = direction ^ !forward;
240        let dir_left = direction ^ forward;
241
242        // Add to edge P's map
243        let ep = edges_p.entry(edge_p).or_default();
244        for j in 0..inclusion.abs() {
245            ep.push(EdgePos {
246                edge_pos: 0.0,
247                vert: vert + j,
248                collision_id,
249                is_start: dir_p,
250            });
251        }
252
253        // Add to right new edge
254        let er = edges_new.entry(key_right).or_default();
255        for j in 0..inclusion.abs() {
256            er.push(EdgePos {
257                edge_pos: 0.0,
258                vert: vert + j,
259                collision_id,
260                is_start: dir_right,
261            });
262        }
263
264        // Add to left new edge
265        let el = edges_new.entry(key_left).or_default();
266        for j in 0..inclusion.abs() {
267            el.push(EdgePos {
268                edge_pos: 0.0,
269                vert: vert + j,
270                collision_id,
271                is_start: dir_left,
272            });
273        }
274    }
275}
276
277// ---------------------------------------------------------------------------
278// PairUp — pair start/end vertices to form halfedges
279// ---------------------------------------------------------------------------
280
281/// Port of C++ `std::partition` (Hoare two-pointer, bidirectional form used by
282/// MSVC and libstdc++): elements satisfying `pred` end up first. UNSTABLE — the
283/// swap pattern determines the relative order of fully-tied `EdgePos` entries
284/// (identical edgePos AND collisionId, e.g. duplicated retained verts on
285/// coincident geometry), which the subsequent stable sorts preserve. That order
286/// decides how degenerate edges pair up, so it must match C++ exactly.
287fn cpp_partition<T, P: Fn(&T) -> bool>(v: &mut [T], pred: P) -> usize {
288    let mut first = 0usize;
289    let mut last = v.len();
290    loop {
291        loop {
292            if first == last {
293                return first;
294            }
295            if !pred(&v[first]) {
296                break;
297            }
298            first += 1;
299        }
300        loop {
301            last -= 1;
302            if first == last {
303                return first;
304            }
305            if pred(&v[last]) {
306                break;
307            }
308        }
309        v.swap(first, last);
310        first += 1;
311    }
312}
313
314fn pair_up<F: FnMut(Halfedge)>(edge_pos: &mut Vec<EdgePos>, mut f: F) {
315    assert!(
316        edge_pos.len() % 2 == 0,
317        "Non-manifold edge! Not an even number of points. Got {} points: starts={}, ends={}",
318        edge_pos.len(),
319        edge_pos.iter().filter(|e| e.is_start).count(),
320        edge_pos.iter().filter(|e| !e.is_start).count(),
321    );
322    let n_edges = edge_pos.len() / 2;
323
324    // C++ PairUp: unstable partition (starts first), then stable_sort each
325    // half, then pair edgePos[i] with edgePos[i + nEdges].
326    let middle = cpp_partition(edge_pos, |e| e.is_start);
327    debug_assert_eq!(middle, n_edges, "Non-manifold edge!");
328    edge_pos[..n_edges].sort_by_key(|e| e.sort_key());
329    edge_pos[n_edges..].sort_by_key(|e| e.sort_key());
330
331    for i in 0..n_edges {
332        f(Halfedge {
333            start_vert: edge_pos[i].vert,
334            end_vert: edge_pos[i + n_edges].vert,
335            paired_halfedge: -1,
336            prop_vert: -1,
337        });
338    }
339}
340
341// ---------------------------------------------------------------------------
342// AppendPartialEdges — edges of P/Q that have intersections
343// ---------------------------------------------------------------------------
344
345pub(super) fn append_partial_edges(
346    out_r: &mut ManifoldImpl,
347    whole_halfedge_p: &mut Vec<bool>,
348    face_ptr_r: &mut Vec<i32>,
349    edges_p: &mut BTreeMap<i32, Vec<EdgePos>>,
350    halfedge_ref: &mut Vec<TriRef>,
351    in_p: &ManifoldImpl,
352    i03: &[i32],
353    vp2r: &[i32],
354    face_p2r: &[i32],
355    forward: bool,
356) {
357    for (&edge_p, edge_pos_p) in edges_p.iter_mut() {
358        edge_pos_p.sort_by_key(|e| e.sort_key());
359
360        let halfedge = in_p.halfedge[edge_p as usize];
361        whole_halfedge_p[edge_p as usize] = false;
362        whole_halfedge_p[halfedge.paired_halfedge as usize] = false;
363
364        let v_start = halfedge.start_vert as usize;
365        let v_end = halfedge.end_vert as usize;
366
367        // C++ computes edgeVec from the INPUT mesh positions — the output
368        // slots at vp2r[v] hold this vertex's position only when it is
369        // retained (i03 != 0), so projecting along an output-derived vector
370        // would use garbage for non-retained endpoints.
371        let edge_vec = in_p.vert_pos[v_end] - in_p.vert_pos[v_start];
372
373        // Fill in edge positions of existing intersection verts
374        for edge in edge_pos_p.iter_mut() {
375            edge.edge_pos = dot(out_r.vert_pos[edge.vert as usize], edge_vec);
376        }
377
378        // Add start vertex (only retained verts — inclusion != 0 — have
379        // valid output positions at vp2r)
380        let inclusion = i03[v_start];
381        if inclusion != 0 {
382            let start_pos = dot(out_r.vert_pos[vp2r[v_start] as usize], edge_vec);
383            for j in 0..inclusion.abs() {
384                edge_pos_p.push(EdgePos {
385                    edge_pos: start_pos,
386                    vert: vp2r[v_start] + j,
387                    collision_id: i32::MAX,
388                    is_start: inclusion > 0,
389                });
390            }
391        }
392
393        // Add end vertex
394        let inclusion = i03[v_end];
395        if inclusion != 0 {
396            let end_pos = dot(out_r.vert_pos[vp2r[v_end] as usize], edge_vec);
397            for j in 0..inclusion.abs() {
398                edge_pos_p.push(EdgePos {
399                    edge_pos: end_pos,
400                    vert: vp2r[v_end] + j,
401                    collision_id: i32::MAX,
402                    is_start: inclusion < 0,
403                });
404            }
405        }
406
407        // Pair up and add halfedges to result
408        let face_left_p = edge_p as usize / 3;
409        let face_left = face_p2r[face_left_p];
410        let face_right_p = halfedge.paired_halfedge as usize / 3;
411        let face_right = face_p2r[face_right_p];
412
413        let forward_ref = TriRef {
414            mesh_id: if forward { 0 } else { 1 },
415            original_id: -1,
416            face_id: face_left_p as i32,
417            coplanar_id: -1,
418        };
419        let backward_ref = TriRef {
420            mesh_id: if forward { 0 } else { 1 },
421            original_id: -1,
422            face_id: face_right_p as i32,
423            coplanar_id: -1,
424        };
425
426        pair_up(edge_pos_p, |mut e| {
427            let forward_edge = face_ptr_r[face_left as usize];
428            face_ptr_r[face_left as usize] += 1;
429            let backward_edge = face_ptr_r[face_right as usize];
430            face_ptr_r[face_right as usize] += 1;
431
432            e.paired_halfedge = backward_edge;
433            out_r.halfedge[forward_edge as usize] = e;
434            halfedge_ref[forward_edge as usize] = forward_ref;
435
436            let rev = Halfedge {
437                start_vert: e.end_vert,
438                end_vert: e.start_vert,
439                paired_halfedge: forward_edge,
440                prop_vert: -1,
441            };
442            out_r.halfedge[backward_edge as usize] = rev;
443            halfedge_ref[backward_edge as usize] = backward_ref;
444        });
445    }
446}
447
448// ---------------------------------------------------------------------------
449// AppendNewEdges — edges formed at face-face intersections
450// ---------------------------------------------------------------------------
451
452pub(super) fn append_new_edges(
453    out_r: &mut ManifoldImpl,
454    face_ptr_r: &mut Vec<i32>,
455    edges_new: &mut BTreeMap<(i32, i32), Vec<EdgePos>>,
456    halfedge_ref: &mut Vec<TriRef>,
457    face_pq2r: &[i32],
458    num_face_p: usize,
459) {
460    for (&(face_p, face_q), edge_pos) in edges_new.iter_mut() {
461        edge_pos.sort_by_key(|e| e.sort_key());
462
463        // Compute bounding box to find longest dimension
464        let mut min = Vec3::splat(f64::INFINITY);
465        let mut max = Vec3::splat(f64::NEG_INFINITY);
466        for edge in edge_pos.iter() {
467            let p = out_r.vert_pos[edge.vert as usize];
468            min.x = min.x.min(p.x);
469            min.y = min.y.min(p.y);
470            min.z = min.z.min(p.z);
471            max.x = max.x.max(p.x);
472            max.y = max.y.max(p.y);
473            max.z = max.z.max(p.z);
474        }
475        let size = max - min;
476        // Order points along longest dimension
477        let dim = if size.x > size.y && size.x > size.z {
478            0
479        } else if size.y > size.z {
480            1
481        } else {
482            2
483        };
484
485        for edge in edge_pos.iter_mut() {
486            let p = out_r.vert_pos[edge.vert as usize];
487            edge.edge_pos = match dim {
488                0 => p.x,
489                1 => p.y,
490                _ => p.z,
491            };
492        }
493
494        let face_left = face_pq2r[face_p as usize];
495        let face_right = face_pq2r[num_face_p + face_q as usize];
496        let forward_ref = TriRef {
497            mesh_id: 0,
498            original_id: -1,
499            face_id: face_p,
500            coplanar_id: -1,
501        };
502        let backward_ref = TriRef {
503            mesh_id: 1,
504            original_id: -1,
505            face_id: face_q,
506            coplanar_id: -1,
507        };
508
509        pair_up(edge_pos, |mut e| {
510            let forward_edge = face_ptr_r[face_left as usize];
511            face_ptr_r[face_left as usize] += 1;
512            let backward_edge = face_ptr_r[face_right as usize];
513            face_ptr_r[face_right as usize] += 1;
514
515            e.paired_halfedge = backward_edge;
516            out_r.halfedge[forward_edge as usize] = e;
517            halfedge_ref[forward_edge as usize] = forward_ref;
518
519            let rev = Halfedge {
520                start_vert: e.end_vert,
521                end_vert: e.start_vert,
522                paired_halfedge: forward_edge,
523                prop_vert: -1,
524            };
525            out_r.halfedge[backward_edge as usize] = rev;
526            halfedge_ref[backward_edge as usize] = backward_ref;
527        });
528    }
529}
530
531// ---------------------------------------------------------------------------
532// AppendWholeEdges — edges with no intersections (fully retained)
533// ---------------------------------------------------------------------------
534
535pub(super) fn append_whole_edges(
536    out_r: &mut ManifoldImpl,
537    face_ptr_r: &mut Vec<i32>,
538    halfedge_ref: &mut Vec<TriRef>,
539    in_p: &ManifoldImpl,
540    whole_halfedge_p: &[bool],
541    i03: &[i32],
542    vp2r: &[i32],
543    face_p2r: &[i32],
544    forward: bool,
545) {
546    for idx in 0..in_p.halfedge.len() {
547        if !whole_halfedge_p[idx] {
548            continue;
549        }
550        let mut halfedge = in_p.halfedge[idx];
551        if !halfedge.is_forward() {
552            continue;
553        }
554
555        let inclusion = i03[halfedge.start_vert as usize];
556        if inclusion == 0 {
557            continue;
558        }
559        if inclusion < 0 {
560            // Reverse the halfedge
561            std::mem::swap(&mut halfedge.start_vert, &mut halfedge.end_vert);
562        }
563        halfedge.start_vert = vp2r[halfedge.start_vert as usize];
564        halfedge.end_vert = vp2r[halfedge.end_vert as usize];
565
566        let face_left_p = idx / 3;
567        let new_face = face_p2r[face_left_p];
568        let face_right_p = halfedge.paired_halfedge as usize / 3;
569        let face_right = face_p2r[face_right_p];
570
571        let forward_ref = TriRef {
572            mesh_id: if forward { 0 } else { 1 },
573            original_id: -1,
574            face_id: face_left_p as i32,
575            coplanar_id: -1,
576        };
577        let backward_ref = TriRef {
578            mesh_id: if forward { 0 } else { 1 },
579            original_id: -1,
580            face_id: face_right_p as i32,
581            coplanar_id: -1,
582        };
583
584        for i in 0..inclusion.abs() {
585            let forward_edge = face_ptr_r[new_face as usize];
586            face_ptr_r[new_face as usize] += 1;
587            let backward_edge = face_ptr_r[face_right as usize];
588            face_ptr_r[face_right as usize] += 1;
589
590            let he = Halfedge {
591                start_vert: halfedge.start_vert + i,
592                end_vert: halfedge.end_vert + i,
593                paired_halfedge: backward_edge,
594                prop_vert: -1,
595            };
596            out_r.halfedge[forward_edge as usize] = he;
597            halfedge_ref[forward_edge as usize] = forward_ref;
598
599            let rev = Halfedge {
600                start_vert: halfedge.end_vert + i,
601                end_vert: halfedge.start_vert + i,
602                paired_halfedge: forward_edge,
603                prop_vert: -1,
604            };
605            out_r.halfedge[backward_edge as usize] = rev;
606            halfedge_ref[backward_edge as usize] = backward_ref;
607        }
608    }
609}
610
611// Sub-module: update_reference, create_properties, boolean_result entry point
612#[path = "boolean_result_assemble.rs"]
613mod boolean_result_assemble;
614pub use boolean_result_assemble::boolean_result;
615