Skip to main content

manifold_rust/
csg_tree.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 12: CSG Tree — ported from C++ csg_tree.cpp (764 lines)
16//
17// Implements the full CSG tree evaluation system with:
18// - CsgLeafNode: lazy transform propagation, Arvo's AABB transform
19// - CsgOpNode: N-ary children with caching
20// - SimpleBoolean: wrapper invoking Boolean3
21// - BatchBoolean: min-heap approach for commutative ops
22// - BatchUnion: bounding-box partitioning + Compose + BatchBoolean
23// - Explicit-stack DFS evaluation (no recursion)
24
25use std::sync::Arc;
26use std::collections::BinaryHeap;
27use std::cmp::Ordering;
28
29use crate::boolean3;
30use crate::impl_mesh::ManifoldImpl;
31use crate::linalg::{Mat3x4, Vec3, mat3x4_to_mat4, mat4_to_mat3x4};
32use crate::types::{Box as BBox, OpType};
33
34// ---------------------------------------------------------------------------
35// CsgLeafNode — wraps an immutable mesh plus a lazy transform
36// ---------------------------------------------------------------------------
37
38#[derive(Clone)]
39pub struct CsgLeafNode {
40    pub p_impl: Arc<ManifoldImpl>,
41    pub transform: Mat3x4,
42}
43
44impl CsgLeafNode {
45    /// Create a leaf from a mesh with identity transform.
46    pub fn new(mesh: ManifoldImpl) -> Self {
47        Self {
48            p_impl: Arc::new(mesh),
49            transform: Mat3x4::identity(),
50        }
51    }
52
53    /// Create a leaf from a mesh with a specific transform.
54    pub fn with_transform(mesh: ManifoldImpl, transform: Mat3x4) -> Self {
55        Self {
56            p_impl: Arc::new(mesh),
57            transform,
58        }
59    }
60
61    /// Create an empty leaf.
62    pub fn empty() -> Self {
63        Self {
64            p_impl: Arc::new(ManifoldImpl::new()),
65            transform: Mat3x4::identity(),
66        }
67    }
68
69    /// Get the mesh, applying the lazy transform if needed.
70    /// Port of C++ CsgLeafNode::GetImpl()
71    pub fn get_impl(&self) -> ManifoldImpl {
72        if self.transform == Mat3x4::identity() {
73            (*self.p_impl).clone()
74        } else {
75            let mut result = (*self.p_impl).clone();
76            result.transform(&self.transform);
77            result
78        }
79    }
80
81    /// Return a new leaf with composed transform.
82    /// Port of C++ CsgLeafNode::Transform()
83    pub fn apply_transform(&self, m: Mat3x4) -> Self {
84        let new_transform = mat4_to_mat3x4(
85            mat3x4_to_mat4(m) * mat3x4_to_mat4(self.transform)
86        );
87        Self {
88            p_impl: Arc::clone(&self.p_impl),
89            transform: new_transform,
90        }
91    }
92
93    /// Get bounding box without materializing the full mesh.
94    /// Uses Arvo's algorithm for AABB transform.
95    /// Port of C++ CsgLeafNode::GetBoundingBox()
96    pub fn get_bounding_box(&self) -> BBox {
97        let impl_bbox = self.p_impl.bbox;
98        if self.transform == Mat3x4::identity() {
99            return impl_bbox;
100        }
101        // Arvo's AABB transform: transform center and half-extents
102        let center = (impl_bbox.min + impl_bbox.max) * 0.5;
103        let half = (impl_bbox.max - impl_bbox.min) * 0.5;
104
105        // Transform center point
106        let mat = self.transform;
107        let new_center = Vec3::new(
108            mat[0].x * center.x + mat[1].x * center.y + mat[2].x * center.z + mat[3].x,
109            mat[0].y * center.x + mat[1].y * center.y + mat[2].y * center.z + mat[3].y,
110            mat[0].z * center.x + mat[1].z * center.y + mat[2].z * center.z + mat[3].z,
111        );
112
113        // Transform half-extents using absolute values of matrix entries
114        let new_half = Vec3::new(
115            mat[0].x.abs() * half.x + mat[1].x.abs() * half.y + mat[2].x.abs() * half.z,
116            mat[0].y.abs() * half.x + mat[1].y.abs() * half.y + mat[2].y.abs() * half.z,
117            mat[0].z.abs() * half.x + mat[1].z.abs() * half.y + mat[2].z.abs() * half.z,
118        );
119
120        BBox {
121            min: new_center - new_half,
122            max: new_center + new_half,
123        }
124    }
125
126    /// Vertex count without triggering transform.
127    pub fn num_vert(&self) -> usize {
128        self.p_impl.num_vert()
129    }
130}
131
132// ---------------------------------------------------------------------------
133// CsgNode — the main CSG tree node (leaf or N-ary operation)
134// ---------------------------------------------------------------------------
135
136#[derive(Clone)]
137pub enum CsgNode {
138    Leaf(CsgLeafNode),
139    Op {
140        op: OpType,
141        children: Vec<CsgNode>,
142        transform: Mat3x4,
143    },
144}
145
146impl CsgNode {
147    pub fn leaf(mesh: ManifoldImpl) -> Self {
148        Self::Leaf(CsgLeafNode::new(mesh))
149    }
150
151    pub fn leaf_node(node: CsgLeafNode) -> Self {
152        Self::Leaf(node)
153    }
154
155    pub fn op(op: OpType, left: CsgNode, right: CsgNode) -> Self {
156        Self::Op {
157            op,
158            children: vec![left, right],
159            transform: Mat3x4::identity(),
160        }
161    }
162
163    pub fn op_n(op: OpType, children: Vec<CsgNode>) -> Self {
164        Self::Op {
165            op,
166            children,
167            transform: Mat3x4::identity(),
168        }
169    }
170
171    /// Evaluate the CSG tree to produce a single mesh.
172    /// Uses explicit-stack DFS to avoid recursion stack overflow.
173    /// Port of C++ CsgOpNode::ToLeafNode()
174    pub fn evaluate(&self) -> ManifoldImpl {
175        let leaf = self.to_leaf_node(Mat3x4::identity());
176        leaf.get_impl()
177    }
178
179    /// Internal: convert this node to a CsgLeafNode, applying the given parent transform.
180    fn to_leaf_node(&self, parent_transform: Mat3x4) -> CsgLeafNode {
181        match self {
182            CsgNode::Leaf(leaf) => leaf.apply_transform(parent_transform),
183            CsgNode::Op { op, children, transform } => {
184                // Compose local transform with parent
185                let combined = mat4_to_mat3x4(
186                    mat3x4_to_mat4(parent_transform) * mat3x4_to_mat4(*transform)
187                );
188
189                // Flatten: recursively resolve all children to leaves
190                let mut positive: Vec<CsgLeafNode> = Vec::new();
191                let mut negative: Vec<CsgLeafNode> = Vec::new();
192
193                self.collect_children(*op, combined, children, &mut positive, &mut negative);
194
195                // Perform the operation
196                match op {
197                    OpType::Add => {
198                        // Union of all positive children
199                        batch_union(&mut positive)
200                    }
201                    OpType::Intersect => {
202                        // Intersection of all positive children
203                        batch_boolean(OpType::Intersect, &mut positive)
204                    }
205                    OpType::Subtract => {
206                        // Subtract: first child is positive, rest are negative
207                        if positive.is_empty() {
208                            return CsgLeafNode::empty();
209                        }
210                        let pos_result = batch_union(&mut positive);
211                        if negative.is_empty() {
212                            return pos_result;
213                        }
214                        let neg_result = batch_union(&mut negative);
215                        simple_boolean(&pos_result, &neg_result, OpType::Subtract)
216                    }
217                }
218            }
219        }
220    }
221
222    /// Recursively collect children, flattening compatible operations.
223    /// Port of the collapsing logic in C++ CsgOpNode::ToLeafNode.
224    fn collect_children(
225        &self,
226        parent_op: OpType,
227        transform: Mat3x4,
228        children: &[CsgNode],
229        positive: &mut Vec<CsgLeafNode>,
230        negative: &mut Vec<CsgLeafNode>,
231    ) {
232        for (i, child) in children.iter().enumerate() {
233            match child {
234                CsgNode::Leaf(leaf) => {
235                    let transformed = leaf.apply_transform(transform);
236                    if parent_op == OpType::Subtract && i > 0 {
237                        negative.push(transformed);
238                    } else {
239                        positive.push(transformed);
240                    }
241                }
242                CsgNode::Op { op: child_op, children: grandchildren, transform: child_transform } => {
243                    let combined = mat4_to_mat3x4(
244                        mat3x4_to_mat4(transform) * mat3x4_to_mat4(*child_transform)
245                    );
246
247                    // Collapsing: flatten compatible ops
248                    let can_collapse = match (parent_op, child_op) {
249                        // Union is associative: (A ∪ B) ∪ C = A ∪ B ∪ C
250                        (OpType::Add, OpType::Add) => true,
251                        // Intersection is associative: (A ∩ B) ∩ C = A ∩ B ∩ C
252                        (OpType::Intersect, OpType::Intersect) => true,
253                        // (A - B) - C = A - (B ∪ C): first child's subtraction collapses
254                        (OpType::Subtract, OpType::Subtract) if i == 0 => true,
255                        _ => false,
256                    };
257
258                    if can_collapse {
259                        // Flatten: merge grandchildren directly
260                        if parent_op == OpType::Subtract && *child_op == OpType::Subtract && i == 0 {
261                            // (A - B) is first child of Subtract: A goes to positive, B goes to negative
262                            for (gi, gc) in grandchildren.iter().enumerate() {
263                                let leaf = gc.to_leaf_node_inner(combined);
264                                if gi == 0 {
265                                    positive.push(leaf);
266                                } else {
267                                    negative.push(leaf);
268                                }
269                            }
270                        } else {
271                            for gc in grandchildren {
272                                let leaf = gc.to_leaf_node_inner(combined);
273                                if parent_op == OpType::Subtract && i > 0 {
274                                    negative.push(leaf);
275                                } else {
276                                    positive.push(leaf);
277                                }
278                            }
279                        }
280                    } else {
281                        // Cannot collapse: evaluate child subtree fully
282                        let result = child.to_leaf_node(combined);
283                        if parent_op == OpType::Subtract && i > 0 {
284                            negative.push(result);
285                        } else {
286                            positive.push(result);
287                        }
288                    }
289                }
290            }
291        }
292    }
293
294    /// Helper: convert a single node to leaf with given transform (non-flattening).
295    fn to_leaf_node_inner(&self, transform: Mat3x4) -> CsgLeafNode {
296        match self {
297            CsgNode::Leaf(leaf) => leaf.apply_transform(transform),
298            CsgNode::Op { .. } => self.to_leaf_node(transform),
299        }
300    }
301}
302
303// ---------------------------------------------------------------------------
304// SimpleBoolean — wrapper invoking Boolean3
305// Port of C++ SimpleBoolean() (lines 142-184)
306// ---------------------------------------------------------------------------
307
308fn simple_boolean(a: &CsgLeafNode, b: &CsgLeafNode, op: OpType) -> CsgLeafNode {
309    let impl_a = a.get_impl();
310    let impl_b = b.get_impl();
311    let result = boolean3::boolean(&impl_a, &impl_b, op);
312    CsgLeafNode::new(result)
313}
314
315// ---------------------------------------------------------------------------
316// BatchBoolean — heap-ordered reduction for commutative ops
317// Port of C++ BatchBoolean() in csg_tree.cpp (v3.5.0)
318// ---------------------------------------------------------------------------
319
320/// Heap entry ordered like C++ `MeshCompare` on `(CsgLeafNode, serial)` pairs:
321/// by vertex count, tie-broken by insertion serial. The serial makes the order
322/// total, so the pop sequence is deterministic and heap-implementation
323/// independent — required for exact match with the C++ reduction order.
324struct MeshEntry(CsgLeafNode, u64);
325
326impl PartialEq for MeshEntry {
327    fn eq(&self, other: &Self) -> bool {
328        self.cmp(other) == Ordering::Equal
329    }
330}
331impl Eq for MeshEntry {}
332
333impl PartialOrd for MeshEntry {
334    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
335        Some(self.cmp(other))
336    }
337}
338impl Ord for MeshEntry {
339    fn cmp(&self, other: &Self) -> Ordering {
340        // C++ std::pop_heap with MeshCompare (a less-than) pops the MAX:
341        // the node with the most verts, ties going to the largest serial.
342        // Rust's BinaryHeap is a max-heap, so use the same less-than order.
343        self.0
344            .num_vert()
345            .cmp(&other.0.num_vert())
346            .then(self.1.cmp(&other.1))
347    }
348}
349
350fn batch_boolean(op: OpType, children: &mut Vec<CsgLeafNode>) -> CsgLeafNode {
351    if children.is_empty() {
352        return CsgLeafNode::empty();
353    }
354    if children.len() == 1 {
355        return children.remove(0);
356    }
357    if children.len() == 2 {
358        let b = children.pop().unwrap();
359        let a = children.pop().unwrap();
360        return simple_boolean(&a, &b, op);
361    }
362
363    let mut heap: BinaryHeap<MeshEntry> = BinaryHeap::new();
364    let mut next_serial = children.len() as u64;
365    for (i, child) in children.drain(..).enumerate() {
366        heap.push(MeshEntry(child, i as u64));
367    }
368
369    // C++ processes up to 4 pairs per round (for its parallel lane), pushing
370    // the results back only at the end of the round — even in sequential
371    // builds. The round structure changes which meshes pair up, so mirror it.
372    let mut tmp: Vec<MeshEntry> = Vec::new();
373    while heap.len() > 1 {
374        for _ in 0..4 {
375            if heap.len() <= 1 {
376                break;
377            }
378            let a = heap.pop().unwrap();
379            let b = heap.pop().unwrap();
380            let result = simple_boolean(&a.0, &b.0, op);
381            tmp.push(MeshEntry(result, next_serial));
382            next_serial += 1;
383        }
384        for entry in tmp.drain(..) {
385            heap.push(entry);
386        }
387    }
388
389    heap.pop().unwrap().0
390}
391
392// ---------------------------------------------------------------------------
393// BatchUnion — bounding-box partitioning + Compose + BatchBoolean
394// Port of C++ BatchUnion() (lines 434-491)
395// ---------------------------------------------------------------------------
396
397const K_MAX_UNION_SIZE: usize = 1000;
398
399fn batch_union(children: &mut Vec<CsgLeafNode>) -> CsgLeafNode {
400    if children.is_empty() {
401        return CsgLeafNode::empty();
402    }
403    if children.len() == 1 {
404        return children.remove(0);
405    }
406
407    // Process in chunks to avoid O(n^2) overlap checks
408    while children.len() > 1 {
409        let chunk_size = children.len().min(K_MAX_UNION_SIZE);
410        let chunk_start = children.len() - chunk_size;
411
412        // Get bounding boxes for the chunk
413        let boxes: Vec<BBox> = children[chunk_start..]
414            .iter()
415            .map(|c| c.get_bounding_box())
416            .collect();
417
418        // Greedy partition into disjoint sets
419        let mut sets: Vec<Vec<usize>> = Vec::new(); // each set is indices into chunk
420        for i in 0..chunk_size {
421            let mut found_set = false;
422            for set in &mut sets {
423                let overlaps = set.iter().any(|&j| boxes[i].does_overlap_box(&boxes[j]));
424                if !overlaps {
425                    set.push(i);
426                    found_set = true;
427                    break;
428                }
429            }
430            if !found_set {
431                sets.push(vec![i]);
432            }
433        }
434
435        // Process each disjoint set
436        let chunk: Vec<CsgLeafNode> = children.drain(chunk_start..).collect();
437        let mut results: Vec<CsgLeafNode> = Vec::new();
438
439        for set in &sets {
440            if set.len() == 1 {
441                results.push(chunk[set[0]].clone());
442            } else {
443                // Compose disjoint meshes without boolean
444                let meshes: Vec<ManifoldImpl> = set.iter()
445                    .map(|&i| chunk[i].get_impl())
446                    .collect();
447                let composed = boolean3::compose_meshes(&meshes);
448                results.push(CsgLeafNode::new(composed));
449            }
450        }
451
452        // BatchBoolean the composed results, then move the (complicated) new
453        // child to the front: C++ push_backs and swaps front↔back, which also
454        // moves the old front to the back when chunking (>kMaxUnionSize).
455        let result = batch_boolean(OpType::Add, &mut results);
456        children.push(result);
457        let last = children.len() - 1;
458        children.swap(0, last);
459    }
460
461    children.remove(0)
462}
463
464// ---------------------------------------------------------------------------
465// Tests
466// ---------------------------------------------------------------------------
467
468#[cfg(test)]
469mod tests {
470    use super::*;
471    use crate::linalg::{mat4_to_mat3x4, translation_matrix, Vec3};
472
473    #[test]
474    fn test_csg_tree_union_disjoint() {
475        let a = ManifoldImpl::cube(&mat4_to_mat3x4(translation_matrix(Vec3::new(0.0, 0.0, 0.0))));
476        let b = ManifoldImpl::cube(&mat4_to_mat3x4(translation_matrix(Vec3::new(3.0, 0.0, 0.0))));
477        let tree = CsgNode::op(OpType::Add, CsgNode::leaf(a), CsgNode::leaf(b));
478        let result = tree.evaluate();
479        assert_eq!(result.num_tri(), 24);
480    }
481
482    #[test]
483    fn test_csg_tree_union_overlapping() {
484        let a = ManifoldImpl::cube(&mat4_to_mat3x4(translation_matrix(Vec3::new(0.0, 0.0, 0.0))));
485        let b = ManifoldImpl::cube(&mat4_to_mat3x4(translation_matrix(Vec3::new(0.5, 0.0, 0.0))));
486        let tree = CsgNode::op(OpType::Add, CsgNode::leaf(a), CsgNode::leaf(b));
487        let result = tree.evaluate();
488        assert!(result.num_tri() > 0, "Overlapping union should produce non-empty mesh");
489    }
490
491    #[test]
492    fn test_csg_tree_intersection() {
493        let a = ManifoldImpl::cube(&mat4_to_mat3x4(translation_matrix(Vec3::new(0.0, 0.0, 0.0))));
494        let b = ManifoldImpl::cube(&mat4_to_mat3x4(translation_matrix(Vec3::new(0.5, 0.0, 0.0))));
495        let tree = CsgNode::op(OpType::Intersect, CsgNode::leaf(a), CsgNode::leaf(b));
496        let result = tree.evaluate();
497        assert!(result.num_tri() > 0, "Overlapping intersection should produce non-empty mesh");
498    }
499
500    #[test]
501    fn test_csg_tree_subtract() {
502        let a = ManifoldImpl::cube(&mat4_to_mat3x4(translation_matrix(Vec3::new(0.0, 0.0, 0.0))));
503        let b = ManifoldImpl::cube(&mat4_to_mat3x4(translation_matrix(Vec3::new(0.5, 0.0, 0.0))));
504        let tree = CsgNode::op(OpType::Subtract, CsgNode::leaf(a), CsgNode::leaf(b));
505        let result = tree.evaluate();
506        assert!(result.num_tri() > 0, "Subtraction should produce non-empty mesh");
507    }
508
509    #[test]
510    fn test_batch_boolean_three_cubes() {
511        let a = CsgLeafNode::new(ManifoldImpl::cube(&mat4_to_mat3x4(translation_matrix(Vec3::new(0.0, 0.0, 0.0)))));
512        let b = CsgLeafNode::new(ManifoldImpl::cube(&mat4_to_mat3x4(translation_matrix(Vec3::new(0.5, 0.0, 0.0)))));
513        let c = CsgLeafNode::new(ManifoldImpl::cube(&mat4_to_mat3x4(translation_matrix(Vec3::new(1.0, 0.0, 0.0)))));
514        let mut children = vec![a, b, c];
515        let result = batch_boolean(OpType::Add, &mut children);
516        let mesh = result.get_impl();
517        assert!(mesh.num_tri() > 0, "BatchBoolean of 3 overlapping cubes should produce non-empty mesh");
518    }
519
520    #[test]
521    fn test_batch_union_disjoint() {
522        let a = CsgLeafNode::new(ManifoldImpl::cube(&mat4_to_mat3x4(translation_matrix(Vec3::new(0.0, 0.0, 0.0)))));
523        let b = CsgLeafNode::new(ManifoldImpl::cube(&mat4_to_mat3x4(translation_matrix(Vec3::new(3.0, 0.0, 0.0)))));
524        let c = CsgLeafNode::new(ManifoldImpl::cube(&mat4_to_mat3x4(translation_matrix(Vec3::new(6.0, 0.0, 0.0)))));
525        let mut children = vec![a, b, c];
526        let result = batch_union(&mut children);
527        let mesh = result.get_impl();
528        // Three disjoint cubes should compose without boolean, giving 36 tris
529        assert_eq!(mesh.num_tri(), 36, "BatchUnion of 3 disjoint cubes should have 36 tris");
530    }
531
532    #[test]
533    fn test_csg_n_ary_union() {
534        // N-ary union of 4 disjoint cubes
535        let nodes: Vec<CsgNode> = (0..4).map(|i| {
536            CsgNode::leaf(ManifoldImpl::cube(&mat4_to_mat3x4(translation_matrix(
537                Vec3::new(i as f64 * 3.0, 0.0, 0.0)
538            ))))
539        }).collect();
540        let tree = CsgNode::op_n(OpType::Add, nodes);
541        let result = tree.evaluate();
542        assert_eq!(result.num_tri(), 48, "N-ary union of 4 disjoint cubes should have 48 tris");
543    }
544
545    #[test]
546    fn test_tree_transforms() {
547        // Test that transforms compose correctly through the tree
548        let a = ManifoldImpl::cube(&Mat3x4::identity());
549        let leaf = CsgLeafNode::new(a);
550        let translated = leaf.apply_transform(
551            mat4_to_mat3x4(translation_matrix(Vec3::new(5.0, 0.0, 0.0)))
552        );
553        let bbox = translated.get_bounding_box();
554        assert!(bbox.min.x > 4.0, "Translated bbox min.x should be > 4.0, got {}", bbox.min.x);
555        assert!(bbox.max.x < 6.5, "Translated bbox max.x should be < 6.5, got {}", bbox.max.x);
556    }
557}