1use fast_mesh_bvh::{BuildOptions, Bvh};
16use fast_mesh_query::Rigid;
17use fast_mesh_store::{Mesh, StoreError, TriangleId, UV_CHANNEL, VertexId};
18
19use crate::baked::baked_mesh;
20use crate::placement::relative_affine;
21use crate::{MeshId, Result, Scene};
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum BooleanOp {
26 Union,
28 Subtract,
30 Intersect,
32}
33
34#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
36pub struct BooleanReport {
37 pub kept_triangles: usize,
39 pub triangles_added: usize,
42 pub uvs_dropped: bool,
45}
46
47fn has_open_uvs(mesh: &Mesh) -> Result<bool> {
51 Ok(mesh.uvs().is_some() && !crate::ops::boundary_loops(mesh)?.is_empty())
52}
53
54fn welded(mut baked: Mesh, options: &BuildOptions) -> Result<(Mesh, Bvh)> {
57 let mut tx = baked.begin_edit();
58 crate::ops::weld(&mut tx, 0.0)?;
59 tx.commit()?;
60 let bvh = Bvh::build(&baked, options)?;
61 Ok((baked, bvh))
62}
63
64impl Scene {
65 pub fn boolean(&mut self, a: MeshId, b: MeshId, op: BooleanOp) -> Result<BooleanReport> {
91 self.entry(a)?;
92 self.entry(b)?;
93 if a == b {
94 return self.boolean_self(a, op);
95 }
96 self.prepare(Some(&[a, b]))?;
99 let ops_op = match op {
100 BooleanOp::Union => crate::ops::BooleanOp::Union,
101 BooleanOp::Subtract => crate::ops::BooleanOp::Subtract,
102 BooleanOp::Intersect => crate::ops::BooleanOp::Intersect,
103 };
104 let (a_seams, b_seams) = (
107 has_open_uvs(&self.entry(a)?.mesh)?,
108 has_open_uvs(&self.entry(b)?.mesh)?,
109 );
110 let a_copy = if a_seams {
111 let ea = self.entry(a)?;
112 let options = ea.bvh.options();
113 let (baked, _) = baked_mesh(&ea.mesh, |l| l.map(f64::from), options)?;
114 Some(welded(baked, options)?)
115 } else {
116 None
117 };
118 let apply = |tx: &mut fast_mesh_store::Transaction, plan: &crate::ops::BooleanPlan| {
119 if a_seams {
120 tx.drop_channel(UV_CHANNEL)?;
121 crate::ops::weld(tx, 0.0)?;
122 }
123 let mut report = crate::ops::apply_boolean(tx, plan)?;
124 report.uvs_dropped |= a_seams;
125 Ok::<_, crate::ops::OpsError>(report)
126 };
127 if self.is_rigid_pair(a, b)? {
128 let plan = {
129 let (ea, eb) = (self.entry(a)?, self.entry(b)?);
130 let xf = crate::placement::relative_rigid(
131 &ea.placement,
132 ea.mesh.origin(),
133 &eb.placement,
134 eb.mesh.origin(),
135 );
136 let b_copy = if b_seams {
137 let options = eb.bvh.options();
138 let (baked, _) = baked_mesh(&eb.mesh, |l| l.map(f64::from), options)?;
139 Some(welded(baked, options)?)
140 } else {
141 None
142 };
143 let (ma, ba) = a_copy.as_ref().map_or((&ea.mesh, &ea.bvh), |(m, b)| (m, b));
144 let (mb, bb) = b_copy.as_ref().map_or((&eb.mesh, &eb.bvh), |(m, b)| (m, b));
145 crate::ops::boolean_plan(ma, ba, mb, bb, &xf, ops_op)?
146 };
147 let report = self.edit(a, |tx| apply(tx, &plan))?;
148 return Ok(BooleanReport {
149 kept_triangles: report.kept_triangles,
150 triangles_added: report.triangles_added,
151 uvs_dropped: report.uvs_dropped,
152 });
153 }
154 let plan = {
158 let (ea, eb) = (self.entry(a)?, self.entry(b)?);
159 let affine = relative_affine(
160 &ea.placement,
161 ea.mesh.origin(),
162 &eb.placement,
163 eb.mesh.origin(),
164 );
165 let (baked, baked_bvh) = baked_mesh(
166 &eb.mesh,
167 |l| affine.apply(l.map(f64::from)),
168 eb.bvh.options(),
169 )?;
170 let (baked, baked_bvh) = if b_seams {
171 welded(baked, eb.bvh.options())?
172 } else {
173 (baked, baked_bvh)
174 };
175 let (ma, ba) = a_copy.as_ref().map_or((&ea.mesh, &ea.bvh), |(m, b)| (m, b));
176 crate::ops::boolean_plan(ma, ba, &baked, &baked_bvh, &Rigid::IDENTITY, ops_op)?
177 };
178 let report = self.edit(a, |tx| apply(tx, &plan))?;
179 Ok(BooleanReport {
180 kept_triangles: report.kept_triangles,
181 triangles_added: report.triangles_added,
182 uvs_dropped: report.uvs_dropped,
183 })
184 }
185
186 fn boolean_self(&mut self, a: MeshId, op: BooleanOp) -> Result<BooleanReport> {
189 match op {
190 BooleanOp::Union | BooleanOp::Intersect => Ok(BooleanReport {
191 kept_triangles: self.mesh(a)?.triangle_count(),
192 triangles_added: 0,
193 uvs_dropped: false,
194 }),
195 BooleanOp::Subtract => {
196 self.edit(a, |tx| {
197 let all: Vec<TriangleId> = (0..tx.mesh().triangle_slots() as TriangleId)
198 .filter(|&t| tx.mesh().triangle(t).is_some())
199 .collect();
200 if all.is_empty() {
201 return Ok::<(), StoreError>(());
202 }
203 tx.remove_triangles(&all)?;
204 let mut orphans: Vec<VertexId> = Vec::new();
205 for v in 0..tx.mesh().vertex_slots() as VertexId {
206 if tx.mesh().is_vertex_alive(v) && tx.mesh().corner_count(v) == 0 {
207 orphans.push(v);
208 }
209 }
210 if !orphans.is_empty() {
211 tx.remove_vertices(&orphans)?;
212 }
213 Ok::<(), StoreError>(())
214 })?;
215 Ok(BooleanReport {
216 kept_triangles: 0,
217 triangles_added: 0,
218 uvs_dropped: false,
219 })
220 }
221 }
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use fast_mesh_store::Mesh;
228
229 use crate::{MeshOptions, Placement, Scene, SceneError, SceneOptions};
230
231 fn cube() -> Mesh {
233 Mesh::from_buffers(
234 &[
235 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0,
243 ],
244 &[
245 0, 2, 1, 0, 3, 2, 4, 5, 6, 4, 6, 7, 0, 1, 5, 0, 5, 4, 2, 3, 7, 2, 7, 6, 1, 2, 6, 1, 6, 5, 3, 0, 4, 3, 4, 7, ],
252 )
253 .unwrap()
254 }
255
256 fn world_volume6(scene: &Scene, id: crate::MeshId) -> f64 {
258 let mesh = scene.mesh(id).unwrap();
259 let placement = scene.placement(id).unwrap();
260 let origin = mesh.origin();
261 let mut v = 0.0;
262 for t in 0..mesh.triangle_slots() as u32 {
263 let Some(tri) = mesh.triangle(t) else {
264 continue;
265 };
266 let [a, b, c] = tri.map(|x| {
267 let p = mesh.position(x).unwrap().map(f64::from);
268 placement.apply([origin[0] + p[0], origin[1] + p[1], origin[2] + p[2]])
269 });
270 v += a[0] * (b[1] * c[2] - b[2] * c[1]) - a[1] * (b[0] * c[2] - b[2] * c[0])
271 + a[2] * (b[0] * c[1] - b[1] * c[0]);
272 }
273 v
274 }
275
276 fn placed_cube(scene: &mut Scene, placement: Placement) -> crate::MeshId {
277 scene
278 .add_mesh(
279 cube(),
280 MeshOptions {
281 placement,
282 ..MeshOptions::default()
283 },
284 )
285 .unwrap()
286 }
287
288 #[test]
289 fn boolean_respects_placement_and_undoes_in_one_step() {
290 let mut scene = Scene::new(SceneOptions::default());
291 let a = placed_cube(
294 &mut scene,
295 Placement {
296 translation: [10.0, 0.0, 0.0],
297 ..Placement::IDENTITY
298 },
299 );
300 let b = placed_cube(
301 &mut scene,
302 Placement {
303 translation: [10.5, 0.0, 0.0],
304 ..Placement::IDENTITY
305 },
306 );
307 scene.take_changes();
308 let before = scene.mesh(a).unwrap().triangle_count();
309
310 let report = scene.boolean(a, b, crate::BooleanOp::Subtract).unwrap();
311 assert!(
312 crate::ops::boundary_loops(scene.mesh(a).unwrap())
313 .unwrap()
314 .is_empty()
315 );
316 assert_eq!(scene.placement(a).unwrap().translation, [10.0, 0.0, 0.0]);
317 assert_eq!(scene.mesh(b).unwrap().triangle_count(), 12);
319 assert!(
321 (world_volume6(&scene, a) - 6.0 * 0.5).abs() < 1e-3,
322 "{}",
323 world_volume6(&scene, a)
324 );
325 assert_eq!(
326 report.kept_triangles,
327 scene.mesh(a).unwrap().triangle_count()
328 );
329
330 assert!(scene.can_undo());
333 assert!(scene.undo().unwrap());
334 assert_eq!(scene.mesh(a).unwrap().triangle_count(), before);
335 assert!(!scene.can_undo());
336 assert!(scene.redo().unwrap());
337 assert_ne!(scene.mesh(a).unwrap().triangle_count(), before);
338 }
339
340 #[test]
341 fn scaled_boolean_follows_the_scale() {
342 let mut scene = Scene::new(SceneOptions::default());
346 let placement = Placement {
347 translation: [0.0, 3.0, 0.0],
348 scale: [2.0, 1.0, 1.0],
349 ..Placement::IDENTITY
350 };
351 let a = placed_cube(&mut scene, placement);
352 let b = placed_cube(
353 &mut scene,
354 Placement {
355 translation: [1.0, 3.0, 0.0],
356 ..Placement::IDENTITY
357 },
358 );
359 scene.take_changes();
360 let report = scene.boolean(a, b, crate::BooleanOp::Intersect).unwrap();
361 assert!(
362 crate::ops::boundary_loops(scene.mesh(a).unwrap())
363 .unwrap()
364 .is_empty()
365 );
366 assert_eq!(scene.placement(a), Ok(placement));
367 assert!(
370 (world_volume6(&scene, a) - 6.0).abs() < 1e-2,
371 "{}",
372 world_volume6(&scene, a)
373 );
374 assert_eq!(
375 report.kept_triangles,
376 scene.mesh(a).unwrap().triangle_count()
377 );
378 }
379
380 #[test]
381 fn boolean_rejects_unknown_meshes_and_clears_self() {
382 let mut scene = Scene::new(SceneOptions::default());
383 let a = scene.add_mesh(cube(), MeshOptions::default()).unwrap();
384 let b = scene.add_mesh(cube(), MeshOptions::default()).unwrap();
385 scene.take_changes();
386 let missing = scene.remove_mesh(b).unwrap();
388 let _ = missing;
389 assert_eq!(
390 scene.boolean(a, b, crate::BooleanOp::Union),
391 Err(SceneError::UnknownMesh)
392 );
393 assert!(!scene.can_undo());
394 let before = scene.mesh(a).unwrap().triangle_count();
396 let report = scene.boolean(a, a, crate::BooleanOp::Union).unwrap();
397 assert_eq!(report.kept_triangles, before);
398 assert!(!scene.can_undo());
399 scene.boolean(a, a, crate::BooleanOp::Subtract).unwrap();
401 assert_eq!(scene.mesh(a).unwrap().triangle_count(), 0);
402 assert!(scene.undo().unwrap());
403 assert_eq!(scene.mesh(a).unwrap().triangle_count(), before);
404 }
405}