Skip to content

Commit 6230d43

Browse files
committed
initial BMesh implementation
1 parent e552863 commit 6230d43

File tree

4 files changed

+267
-1
lines changed

4 files changed

+267
-1
lines changed

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ geo-buf = { version = "0.1.0", optional = true } # straight-skeleton offsetting
4040
chull = { version = "0.2.4", optional = true }
4141
spade = { version = "2.15.0", optional = true }
4242
earcutr = { version = "0.5.0", optional = true }
43+
boolmesh = { version = "0.1.4", optional = true }
4344

4445
rapier3d-f64 = { version = "0.24.0", optional = true }
4546
rapier3d = { version = "0.24.0", optional = true }
@@ -90,7 +91,7 @@ serde = { version = "1.0.228", optional = true }
9091
uuid = { version = "1.18", features = ["js"] }
9192

9293
[features]
93-
default = ["f64", "stl-io", "dxf-io", "obj-io", "ply-io", "amf-io", "gltf-io", "chull-io", "image-io", "metaballs", "sdf", "offset", "delaunay", "truetype-text", "hershey-text"]
94+
default = ["f64", "bmesh", "stl-io", "dxf-io", "obj-io", "ply-io", "amf-io", "gltf-io", "chull-io", "image-io", "metaballs", "sdf", "offset", "delaunay", "truetype-text", "hershey-text"]
9495
parallel = [
9596
"rayon",
9697
"geo/multithreading",
@@ -108,6 +109,9 @@ f32 = [
108109
"rapier3d",
109110
"parry3d",
110111
]
112+
bmesh = [
113+
"boolmesh",
114+
]
111115
# convex hull and minkowski sum
112116
chull-io = [
113117
"chull",

src/bmesh/mod.rs

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
//! BMesh: boolmesh-backed CSG implementation for csgrs
2+
//!
3+
//! This type wraps a `boolmesh::Manifold` and implements the `CSG` trait,
4+
//! so you can use boolmesh’s robust boolean kernel inside csgrs.
5+
6+
use crate::float_types::{
7+
parry3d::bounding_volume::Aabb,
8+
Real,
9+
};
10+
use crate::traits::CSG;
11+
12+
use boolmesh::{
13+
compute_boolean,
14+
Manifold,
15+
OpType,
16+
};
17+
18+
use nalgebra::{Matrix4, Point3};
19+
use std::{fmt::Debug, sync::OnceLock};
20+
21+
/// A solid represented by boolmesh’s `Manifold`, wired into csgrs’ `CSG` trait.
22+
///
23+
/// `metadata` is whole-shape metadata, mirroring `Mesh<S>`.
24+
#[derive(Clone)]
25+
pub struct BMesh<S: Clone + Send + Sync + Debug> {
26+
/// Underlying robust manifold. `None` represents an empty solid.
27+
pub manifold: Option<Manifold>,
28+
/// Lazily computed Parry AABB for the solid.
29+
pub bounding_box: OnceLock<Aabb>,
30+
/// Optional whole-shape metadata.
31+
pub metadata: Option<S>,
32+
}
33+
34+
impl<S: Clone + Send + Sync + Debug> Default for BMesh<S> {
35+
fn default() -> Self {
36+
Self::new()
37+
}
38+
}
39+
40+
impl<S: Clone + Send + Sync + Debug> BMesh<S> {
41+
/// Construct from a boolmesh `Manifold`.
42+
#[inline]
43+
pub fn from_manifold(manifold: Manifold, metadata: Option<S>) -> Self {
44+
BMesh {
45+
manifold: Some(manifold),
46+
bounding_box: OnceLock::new(),
47+
metadata,
48+
}
49+
}
50+
51+
/// Helper: are we the empty solid?
52+
#[inline]
53+
fn is_empty(&self) -> bool {
54+
self.manifold.is_none()
55+
}
56+
57+
/// Core helper for boolean ops, handling empty cases and delegating to boolmesh.
58+
fn boolean(&self, other: &Self, op: OpType) -> Self {
59+
use OpType::*;
60+
61+
match (&self.manifold, &other.manifold) {
62+
// Ø op Ø => Ø
63+
(None, None) => BMesh::new(),
64+
65+
// A op Ø
66+
(Some(_), None) => match op {
67+
Add | Subtract => self.clone(), // A ∪ Ø = A, A − Ø = A
68+
Intersect => BMesh::new(), // A ∩ Ø = Ø
69+
},
70+
71+
// Ø op B
72+
(None, Some(_)) => match op {
73+
Add => other.clone(), // Ø ∪ B = B
74+
Subtract => BMesh::new(), // Ø − B = Ø
75+
Intersect => BMesh::new(), // Ø ∩ B = Ø
76+
},
77+
78+
// A op B, both non-empty
79+
(Some(mp), Some(mq)) => {
80+
let m = compute_boolean(mp, mq, op)
81+
.unwrap_or_else(|e| {
82+
// You may want to change this to a different error strategy.
83+
panic!("BMesh boolean operation failed: {e}");
84+
});
85+
86+
// Follow `Mesh` semantics: keep left-hand side metadata.
87+
BMesh {
88+
manifold: Some(m),
89+
bounding_box: OnceLock::new(),
90+
metadata: self.metadata.clone(),
91+
}
92+
}
93+
}
94+
}
95+
96+
/// Rebuild a manifold after applying a matrix transform to all vertex positions.
97+
///
98+
/// Connectivity is kept by reusing the original triangle indices.
99+
fn transformed_manifold(&self, mat: &Matrix4<Real>) -> Option<Manifold> {
100+
let m = self.manifold.as_ref()?;
101+
102+
// Flatten transformed positions
103+
let mut pos: Vec<Real> = Vec::with_capacity(m.ps.len() * 3);
104+
for v in &m.ps {
105+
let p = Point3::new(v.x, v.y, v.z);
106+
let hp = mat * p.to_homogeneous();
107+
// If homogeneous w is invalid, fall back to original position.
108+
let p_t = Point3::from_homogeneous(hp).unwrap_or(p);
109+
pos.push(p_t.x);
110+
pos.push(p_t.y);
111+
pos.push(p_t.z);
112+
}
113+
114+
// Reuse the current triangle connectivity.
115+
let mut idx: Vec<usize> = Vec::with_capacity(m.nf * 3);
116+
for f in 0..m.nf {
117+
let base = f * 3;
118+
idx.push(m.hs[base].tail);
119+
idx.push(m.hs[base + 1].tail);
120+
idx.push(m.hs[base + 2].tail);
121+
}
122+
123+
Some(
124+
Manifold::new(&pos, &idx).unwrap_or_else(|e| {
125+
panic!("BMesh::transform – boolmesh::Manifold::new failed: {e}");
126+
}),
127+
)
128+
}
129+
130+
/// Rebuild a manifold with flipped triangle winding (geometric complement).
131+
fn inverted_manifold(&self) -> Option<Manifold> {
132+
let m = self.manifold.as_ref()?;
133+
134+
let mut pos: Vec<Real> = Vec::with_capacity(m.ps.len() * 3);
135+
for v in &m.ps {
136+
pos.push(v.x);
137+
pos.push(v.y);
138+
pos.push(v.z);
139+
}
140+
141+
// Flip orientation: (v0, v1, v2) -> (v0, v2, v1)
142+
let mut idx: Vec<usize> = Vec::with_capacity(m.nf * 3);
143+
for f in 0..m.nf {
144+
let base = f * 3;
145+
let v0 = m.hs[base].tail;
146+
let v1 = m.hs[base + 1].tail;
147+
let v2 = m.hs[base + 2].tail;
148+
idx.push(v0);
149+
idx.push(v2);
150+
idx.push(v1);
151+
}
152+
153+
Some(
154+
Manifold::new(&pos, &idx).unwrap_or_else(|e| {
155+
panic!("BMesh::inverse – boolmesh::Manifold::new failed: {e}");
156+
}),
157+
)
158+
}
159+
}
160+
161+
impl<S: Clone + Send + Sync + Debug> CSG for BMesh<S> {
162+
/// New empty BMesh (no manifold).
163+
fn new() -> Self {
164+
BMesh {
165+
manifold: None,
166+
bounding_box: OnceLock::new(),
167+
metadata: None,
168+
}
169+
}
170+
171+
/// Union via boolmesh.
172+
fn union(&self, other: &Self) -> Self {
173+
self.boolean(other, OpType::Add)
174+
}
175+
176+
/// Difference via boolmesh (`self \ other`).
177+
fn difference(&self, other: &Self) -> Self {
178+
self.boolean(other, OpType::Subtract)
179+
}
180+
181+
/// Intersection via boolmesh.
182+
fn intersection(&self, other: &Self) -> Self {
183+
self.boolean(other, OpType::Intersect)
184+
}
185+
186+
/// Symmetric difference: (A \ B) ∪ (B \ A)
187+
fn xor(&self, other: &Self) -> Self {
188+
let a_sub_b = self.difference(other);
189+
let b_sub_a = other.difference(self);
190+
a_sub_b.union(&b_sub_a)
191+
}
192+
193+
/// Apply a 4×4 transform to all vertices and rebuild the boolmesh manifold.
194+
fn transform(&self, mat: &Matrix4<Real>) -> Self {
195+
if self.is_empty() {
196+
return self.clone();
197+
}
198+
199+
let manifold = self
200+
.transformed_manifold(mat)
201+
.expect("BMesh::transform – manifold unexpectedly empty");
202+
203+
BMesh {
204+
manifold: Some(manifold),
205+
bounding_box: OnceLock::new(),
206+
metadata: self.metadata.clone(),
207+
}
208+
}
209+
210+
/// AABB of the solid (derived from the boolmesh manifold’s bounding box).
211+
fn bounding_box(&self) -> Aabb {
212+
*self.bounding_box.get_or_init(|| {
213+
if let Some(m) = &self.manifold {
214+
let bb = &m.bounding_box;
215+
let mins = Point3::new(bb.min.x, bb.min.y, bb.min.z);
216+
let maxs = Point3::new(bb.max.x, bb.max.y, bb.max.z);
217+
Aabb::new(mins, maxs)
218+
} else {
219+
Aabb::new(Point3::origin(), Point3::origin())
220+
}
221+
})
222+
}
223+
224+
/// Reset cached AABB.
225+
fn invalidate_bounding_box(&mut self) {
226+
self.bounding_box = OnceLock::new();
227+
}
228+
229+
/// Geometric complement: flip all triangle windings and rebuild the manifold.
230+
///
231+
/// This is implemented as an orientation flip, which in boolmesh’s pipeline
232+
/// inverts the solid’s inside/outside classification.
233+
fn inverse(&self) -> Self {
234+
if self.is_empty() {
235+
return self.clone();
236+
}
237+
238+
let manifold = self
239+
.inverted_manifold()
240+
.expect("BMesh::inverse – manifold unexpectedly empty");
241+
242+
BMesh {
243+
manifold: Some(manifold),
244+
bounding_box: OnceLock::new(),
245+
metadata: self.metadata.clone(),
246+
}
247+
}
248+
}
249+

src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ pub mod voxels;
4444
#[cfg(feature = "wasm")]
4545
pub mod wasm;
4646

47+
#[cfg(feature = "bmesh")]
48+
pub mod bmesh;
49+
4750
#[cfg(any(
4851
all(feature = "delaunay", feature = "earcut"),
4952
not(any(feature = "delaunay", feature = "earcut"))

0 commit comments

Comments
 (0)