1use std::collections::{BTreeMap, BTreeSet};
32
33use fast_mesh_bvh::{BuildOptions, Bvh};
34use fast_mesh_query::{
35 ClosestOptions, Cull, PairSegment, Ray, RayOptions, Rigid, closest_point,
36 intersecting_segments, raycast_all,
37};
38use fast_mesh_store::{Mesh, StoreError, Transaction, TriangleId, UV_CHANNEL, VertexId};
39
40use crate::analyze::analyze;
41use crate::{OpsError, Result, push, reserve};
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum BooleanOp {
46 Union,
48 Subtract,
50 Intersect,
52}
53
54#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
56pub struct BooleanReport {
57 pub kept_triangles: usize,
59 pub triangles_added: usize,
62 pub uvs_dropped: bool,
65}
66
67#[derive(Debug, Clone, Default)]
70pub struct BooleanPlan {
71 drop_a: Vec<TriangleId>,
72 rewrite: Vec<(TriangleId, Vec<[PRef; 3]>)>,
73 fresh: Vec<[f32; 3]>,
74 tool: Vec<[PRef; 3]>,
75 tol: f32,
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80enum PRef {
81 Live(VertexId),
82 Fresh(u32),
83}
84
85fn oom() -> OpsError {
86 OpsError::Store(StoreError::OutOfMemory)
87}
88
89type F3 = [f32; 3];
90type D3 = [f64; 3];
91
92fn sub(a: D3, b: D3) -> D3 {
93 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
94}
95
96fn cross(a: D3, b: D3) -> D3 {
97 [
98 a[1] * b[2] - a[2] * b[1],
99 a[2] * b[0] - a[0] * b[2],
100 a[0] * b[1] - a[1] * b[0],
101 ]
102}
103
104fn dot(a: D3, b: D3) -> f64 {
105 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
106}
107
108fn area2(a: D3, b: D3, c: D3) -> f64 {
109 let n = cross(sub(b, a), sub(c, a));
110 dot(n, n)
111}
112
113fn max_edge2(t: &[D3; 3]) -> f64 {
114 let mut m = 0.0f64;
115 for (a, b) in [(t[0], t[1]), (t[1], t[2]), (t[2], t[0])] {
116 let d = sub(b, a);
117 m = m.max(dot(d, d));
118 }
119 m
120}
121
122const AREA_EPSILON: f64 = 1e-12;
125
126pub fn boolean_plan(
141 a: &Mesh,
142 bvh_a: &Bvh,
143 b: &Mesh,
144 bvh_b: &Bvh,
145 xf: &Rigid,
146 op: BooleanOp,
147) -> Result<BooleanPlan> {
148 check_input(a, bvh_a)?;
149 check_input(b, bvh_b)?;
150 if std::ptr::eq(a, b) {
151 return Err(OpsError::InvalidInput("cannot combine a mesh with itself"));
152 }
153 let tool = map_tool(b, xf)?;
156 let tool_bvh = Bvh::build(&tool, &BuildOptions::default()).map_err(|e| match e {
157 fast_mesh_bvh::BvhError::OutOfMemory => OpsError::Store(StoreError::OutOfMemory),
158 _ => OpsError::InvalidInput("boolean tool BVH failed to build"),
161 })?;
162 let tol = tolerance(a, &tool);
163 let mut plan = BooleanPlan {
164 tol,
165 ..BooleanPlan::default()
166 };
167 let mut fresh_of: BTreeMap<[u32; 3], u32> = BTreeMap::new();
168 let a_tool = ToolRef {
169 mesh: &tool,
170 bvh: &tool_bvh,
171 };
172 let a_live = ToolRef {
173 mesh: a,
174 bvh: bvh_a,
175 };
176 let pieces_a = split_side(a, bvh_a, &Side::A, &a_tool, &mut plan, &mut fresh_of)?;
177 let pieces_b = split_side(
178 &tool,
179 &tool_bvh,
180 &Side::B,
181 &a_live,
182 &mut plan,
183 &mut fresh_of,
184 )?;
185 classify_side(
186 a,
187 &pieces_a,
188 &Side::A,
189 &a_tool,
190 op,
191 true,
192 &mut plan,
193 &mut fresh_of,
194 )?;
195 classify_side(
196 &tool,
197 &pieces_b,
198 &Side::B,
199 &a_live,
200 op,
201 false,
202 &mut plan,
203 &mut fresh_of,
204 )?;
205 Ok(plan)
206}
207
208pub fn apply_boolean(tx: &mut Transaction, plan: &BooleanPlan) -> Result<BooleanReport> {
213 let uvs_dropped = tx.mesh().uvs().is_some();
214 if uvs_dropped {
215 tx.drop_channel(UV_CHANNEL)?;
216 }
217 if !plan.drop_a.is_empty() {
218 tx.remove_triangles(&plan.drop_a)?;
219 }
220 let fresh = if plan.fresh.is_empty() {
221 Vec::new()
222 } else {
223 tx.add_vertices(&plan.fresh)?
224 };
225 let at = |r: PRef| match r {
226 PRef::Live(v) => v,
227 PRef::Fresh(i) => fresh[i as usize],
228 };
229 let mut added = 0usize;
230 for (t, pieces) in &plan.rewrite {
231 let tris: Vec<[VertexId; 3]> = pieces.iter().map(|p| p.map(at)).collect();
233 tx.set_triangles(&[*t], &[tris[0]])?;
234 if tris.len() > 1 {
235 tx.add_triangles(&tris[1..])?;
236 added += tris.len() - 1;
237 }
238 }
239 let mut tool_ids = Vec::new();
240 if !plan.tool.is_empty() {
241 let tris: Vec<[VertexId; 3]> = plan.tool.iter().map(|p| p.map(at)).collect();
242 tool_ids = tx.add_triangles(&tris)?;
243 added += tris.len();
244 }
245 added += stitch(tx, &tool_ids, plan.tol)?;
246 let welded = crate::weld(tx, plan.tol)?;
247 let cleaned = crate::remove_degenerate(tx, &crate::DegenerateOptions::default())?;
248 added = added.saturating_sub(welded.removed_triangles + cleaned.removed_triangles);
249 Ok(BooleanReport {
250 kept_triangles: tx.mesh().triangle_count(),
251 triangles_added: added,
252 uvs_dropped,
253 })
254}
255
256fn tolerance(a: &Mesh, tool: &Mesh) -> f32 {
260 let mut m = 1.0f32;
261 for mesh in [a, tool] {
262 for p in mesh.positions() {
263 for c in p {
264 m = m.max(c.abs());
265 }
266 }
267 }
268 (1e-6 * m).max(f32::MIN_POSITIVE)
269}
270
271fn check_input(mesh: &Mesh, bvh: &Bvh) -> Result<()> {
273 let options = crate::AnalyzeOptions {
274 weld_tolerance: 0.0,
275 self_intersection: Some(bvh),
276 max_listed: 1,
277 };
278 let r = analyze(mesh, &options)?;
279 if !r.loops.is_empty() {
280 return Err(OpsError::InvalidInput("boolean needs closed meshes"));
281 }
282 if r.non_manifold_edges > 0 || !r.pinch_vertices.is_empty() {
283 return Err(OpsError::InvalidInput("boolean needs manifold meshes"));
284 }
285 if r.degenerate > 0 {
286 return Err(OpsError::InvalidInput(
287 "boolean input has degenerate triangles",
288 ));
289 }
290 if r.duplicate > 0 {
291 return Err(OpsError::InvalidInput(
292 "boolean input has duplicate triangles",
293 ));
294 }
295 if r.orientation_conflicts > 0 {
296 return Err(OpsError::InvalidInput(
297 "boolean input is inconsistently wound",
298 ));
299 }
300 if r.self_intersects == Some(true) {
301 return Err(OpsError::InvalidInput("boolean input self-intersects"));
302 }
303 Ok(())
304}
305
306fn map_tool(b: &Mesh, xf: &Rigid) -> Result<Mesh> {
308 let mut positions: Vec<f32> = Vec::new();
309 reserve(&mut positions, b.vertex_slots() * 3)?;
310 for p in b.positions() {
311 let q = xf.apply(*p);
312 let f = [q[0] as f32, q[1] as f32, q[2] as f32];
313 if !f.iter().all(|c| c.is_finite()) {
314 return Err(OpsError::InvalidInput("boolean tool leaves f32 range"));
315 }
316 positions.extend_from_slice(&f);
317 }
318 let mut indices: Vec<u32> = Vec::new();
319 reserve(&mut indices, b.triangle_slots() * 3)?;
320 for t in b.triangles() {
321 indices.extend_from_slice(t);
322 }
323 let mut mapped = Mesh::from_buffers(&positions, &indices)?;
324 let dead_tris: Vec<TriangleId> = (0..b.triangle_slots() as TriangleId)
327 .filter(|&t| b.triangle(t).is_none())
328 .collect();
329 let dead_verts: Vec<VertexId> = (0..b.vertex_slots() as VertexId)
330 .filter(|&v| !b.is_vertex_alive(v))
331 .collect();
332 if !dead_tris.is_empty() || !dead_verts.is_empty() {
333 let mut tx = mapped.begin_edit();
334 if !dead_tris.is_empty() {
335 tx.remove_triangles(&dead_tris)?;
336 }
337 if !dead_verts.is_empty() {
338 tx.remove_vertices(&dead_verts)?;
339 }
340 let _ = tx.commit()?;
341 }
342 Ok(mapped)
343}
344
345#[derive(Clone, Copy, PartialEq, Eq)]
347enum Side {
348 A,
350 B,
352}
353
354#[derive(Clone, Copy)]
356struct ToolRef<'m> {
357 mesh: &'m Mesh,
358 bvh: &'m Bvh,
359}
360
361fn tri_pos(mesh: &Mesh, t: TriangleId) -> [D3; 3] {
364 mesh.triangle(t)
365 .expect("split of a live triangle")
366 .map(|v| mesh.position(v).expect("live corner").map(f64::from))
367}
368
369fn fresh(p: D3, plan: &mut BooleanPlan, fresh_of: &mut BTreeMap<[u32; 3], u32>) -> Result<PRef> {
371 let f = [p[0] as f32, p[1] as f32, p[2] as f32];
372 if !f.iter().all(|c| c.is_finite()) {
373 return Err(OpsError::InvalidInput("boolean point leaves f32 range"));
374 }
375 let bits = f.map(f32::to_bits);
376 if let Some(&i) = fresh_of.get(&bits) {
377 return Ok(PRef::Fresh(i));
378 }
379 let i = u32::try_from(plan.fresh.len()).map_err(|_| oom())?;
380 reserve(&mut plan.fresh, 1)?;
381 plan.fresh.push(f);
382 fresh_of.insert(bits, i);
383 Ok(PRef::Fresh(i))
384}
385
386fn point_seg(p: D3, a: D3, b: D3) -> (f64, f64) {
388 let ab = sub(b, a);
389 let len2 = dot(ab, ab);
390 if len2 == 0.0 {
391 let d = sub(p, a);
392 return (dot(d, d).sqrt(), 0.0);
393 }
394 let t = dot(sub(p, a), ab) / len2;
395 let c = std::array::from_fn(|k| a[k] + t.clamp(0.0, 1.0) * ab[k]);
396 let d = sub(p, c);
397 (dot(d, d).sqrt(), t)
398}
399
400fn key(a: VertexId, b: VertexId) -> (VertexId, VertexId) {
402 (a.min(b), b.max(a))
403}
404
405#[derive(Debug, Clone, Copy)]
407struct WPt {
408 pos: D3,
409 v: PRef,
410}
411
412#[derive(Debug, Clone)]
415struct Piece {
416 ring: Vec<usize>,
417 inner: Vec<usize>,
418 segs: Vec<(usize, usize)>,
419}
420
421struct TriWork {
423 corners: [D3; 3],
424 corner_v: [PRef; 3],
425 edge: [Vec<WPt>; 3],
427 inner: Vec<WPt>,
429 segs: Vec<(usize, usize)>,
432}
433
434struct SplitTris {
437 pieces: Vec<[PRef; 3]>,
438 edge_split: bool,
439}
440
441fn split_side(
446 mesh: &Mesh,
447 bvh: &Bvh,
448 side: &Side,
449 tool: &ToolRef<'_>,
450 plan: &mut BooleanPlan,
451 fresh_of: &mut BTreeMap<[u32; 3], u32>,
452) -> Result<BTreeMap<TriangleId, SplitTris>> {
453 let mut out: BTreeMap<TriangleId, SplitTris> = BTreeMap::new();
454 let mut segs: Vec<PairSegment> = Vec::new();
455 intersecting_segments(mesh, bvh, tool.mesh, tool.bvh, &Rigid::IDENTITY, &mut segs)?;
456 if segs.is_empty() {
457 return Ok(out);
458 }
459 let mut by_tri: BTreeMap<TriangleId, Vec<(F3, F3)>> = BTreeMap::new();
463 for s in &segs {
464 let (t, from, to) = (s.a, s.from, s.to);
465 if mesh.triangle(t).is_none() {
466 continue;
467 }
468 let e = by_tri.entry(t).or_default();
469 if !e
470 .iter()
471 .any(|&(a, b)| (a == from && b == to) || (a == to && b == from))
472 {
473 push(e, (from, to))?;
474 }
475 }
476 let mut edge_pts: BTreeMap<(VertexId, VertexId), Vec<(f64, u32)>> = BTreeMap::new();
480 let mut ends: BTreeMap<TriangleId, Vec<(PtLoc, PtLoc)>> = BTreeMap::new();
487 for (t, ss) in &by_tri {
488 let e = locate_ends(mesh, side, *t, ss, &mut edge_pts, plan, fresh_of)?;
489 ends.insert(*t, e);
490 }
491 let mut todo: BTreeSet<TriangleId> = by_tri.keys().copied().collect();
492 for t in 0..mesh.triangle_slots() as TriangleId {
493 if mesh.triangle(t).is_none() {
494 continue;
495 }
496 let tri = mesh.triangle(t).expect("live");
497 let touched = [0, 1, 2].iter().any(|&i| {
498 edge_pts
499 .get(&key(tri[i], tri[(i + 1) % 3]))
500 .is_some_and(|l| !l.is_empty())
501 });
502 if touched {
505 todo.insert(t);
506 }
507 }
508 for t in todo {
509 let e = ends.remove(&t).unwrap_or_default();
510 let work = assemble(mesh, side, t, &e, &edge_pts, plan, fresh_of)?;
511 let edge_split = work.edge.iter().any(|e| !e.is_empty());
512 let tris = split_triangle(work, plan, fresh_of)?;
513 if !tris.is_empty() {
514 out.insert(
515 t,
516 SplitTris {
517 pieces: tris,
518 edge_split,
519 },
520 );
521 }
522 }
523 Ok(out)
524}
525
526#[derive(Debug, Clone, Copy)]
528enum PtLoc {
529 Corner(usize),
530 Edge {
531 edge: usize,
532 t: f64,
533 pos: D3,
534 v: PRef,
535 },
536 Inner {
537 pos: D3,
538 v: PRef,
539 },
540}
541
542#[allow(clippy::too_many_arguments)]
547fn locate_ends(
548 mesh: &Mesh,
549 _side: &Side,
550 t: TriangleId,
551 ss: &[(F3, F3)],
552 edge_pts: &mut BTreeMap<(VertexId, VertexId), Vec<(f64, u32)>>,
553 plan: &mut BooleanPlan,
554 fresh_of: &mut BTreeMap<[u32; 3], u32>,
555) -> Result<Vec<(PtLoc, PtLoc)>> {
556 let corners = tri_pos(mesh, t);
557 let c32: [F3; 3] = corners.map(|p| p.map(|c| c as f32));
558 let elen: [f64; 3] = std::array::from_fn(|i| {
559 let d = sub(corners[(i + 1) % 3], corners[i]);
560 dot(d, d).sqrt()
561 });
562 let etol = |i: usize| 1e-6 * elen[i].max(1e-12);
563 let mut ends: Vec<(PtLoc, PtLoc)> = Vec::new();
564 for (from, to) in ss {
565 let mut pair: Vec<PtLoc> = Vec::new();
566 for e in [from, to] {
567 if let Some(c) = c32.iter().position(|c| *c == *e) {
568 pair.push(PtLoc::Corner(c));
569 continue;
570 }
571 let p = e.map(f64::from);
572 let mut corner = None;
574 for (c, cp) in corners.iter().enumerate() {
575 let d = sub(p, *cp);
576 if dot(d, d).sqrt() <= etol(c).max(etol((c + 2) % 3)) {
577 corner = Some(c);
578 break;
579 }
580 }
581 if let Some(c) = corner {
582 pair.push(PtLoc::Corner(c));
583 continue;
584 }
585 let mut best: Option<(usize, f64)> = None;
587 for (i, (a, b)) in [
588 (corners[0], corners[1]),
589 (corners[1], corners[2]),
590 (corners[2], corners[0]),
591 ]
592 .into_iter()
593 .enumerate()
594 {
595 let (d, tpar) = point_seg(p, a, b);
596 if d <= etol(i) && best.is_none_or(|(_, bt)| tpar < bt) {
597 best = Some((i, tpar));
598 }
599 }
600 if let Some((i, tpar)) = best {
601 let tri = mesh.triangle(t).expect("live");
602 let ekey = key(tri[i], tri[(i + 1) % 3]);
603 let tkey = if tri[i] < tri[(i + 1) % 3] {
604 tpar
605 } else {
606 1.0 - tpar
607 };
608 let list = edge_pts.entry(ekey).or_default();
609 let mut found = None;
610 for &(pt, v) in list.iter() {
611 if (pt - tkey).abs() <= 1e-9 {
612 found = Some(v);
613 break;
614 }
615 }
616 let v = match found {
617 Some(v) => PRef::Fresh(v),
618 None => {
619 let v = fresh(p, plan, fresh_of)?;
620 let PRef::Fresh(idx) = v else {
621 unreachable!("fresh is fresh")
622 };
623 push(list, (tkey, idx))?;
624 list.sort_by(|x, y| x.0.total_cmp(&y.0));
626 v
627 }
628 };
629 pair.push(PtLoc::Edge {
630 edge: i,
631 t: tpar,
632 pos: p,
633 v,
634 });
635 continue;
636 }
637 let v = fresh(p, plan, fresh_of)?;
638 pair.push(PtLoc::Inner { pos: p, v });
639 }
640 push(&mut ends, (pair[0], pair[1]))?;
641 }
642 Ok(ends)
643}
644
645#[allow(clippy::too_many_arguments)]
648fn assemble(
649 mesh: &Mesh,
650 side: &Side,
651 t: TriangleId,
652 ends: &[(PtLoc, PtLoc)],
653 edge_pts: &BTreeMap<(VertexId, VertexId), Vec<(f64, u32)>>,
654 plan: &mut BooleanPlan,
655 fresh_of: &mut BTreeMap<[u32; 3], u32>,
656) -> Result<TriWork> {
657 let corners = tri_pos(mesh, t);
658 let corner_v: [PRef; 3] = match side {
659 Side::A => mesh.triangle(t).expect("live").map(PRef::Live),
660 Side::B => {
661 let mut v = [PRef::Fresh(0); 3];
662 for (k, p) in corners.iter().enumerate() {
663 v[k] = fresh(*p, plan, fresh_of)?;
664 }
665 v
666 }
667 };
668 let mut pts: Vec<WPt> = Vec::new();
670 for (k, p) in corners.iter().enumerate() {
671 push(
672 &mut pts,
673 WPt {
674 pos: *p,
675 v: corner_v[k],
676 },
677 )?;
678 }
679 let tri = mesh.triangle(t).expect("live");
680 let mut edge_idx: [Vec<usize>; 3] = [Vec::new(), Vec::new(), Vec::new()];
681 let edge_frames = [
682 (0, corners[0], corners[1], tri[0], tri[1]),
683 (1, corners[1], corners[2], tri[1], tri[2]),
684 (2, corners[2], corners[0], tri[2], tri[0]),
685 ];
686 for ((i, c0, c1, t0, t1), edge) in edge_frames.into_iter().zip(edge_idx.iter_mut()) {
687 let mut ordered: Vec<(f64, WPt)> = Vec::new();
690 for loc in ends.iter().flat_map(|(a, b)| [*a, *b]) {
691 if let PtLoc::Edge { edge, t, pos, v } = loc
692 && edge == i
693 && !ordered.iter().any(|&(_, w): &(f64, WPt)| w.v == v)
694 {
695 ordered.push((t, WPt { pos, v }));
696 }
697 }
698 let ekey = key(t0, t1);
699 if let Some(list) = edge_pts.get(&ekey) {
700 let axis_len = dot(sub(c1, c0), sub(c1, c0)).sqrt().max(1e-12);
701 for &(_, idx) in list {
702 let v = PRef::Fresh(idx);
703 if ordered.iter().any(|&(_, w): &(f64, WPt)| w.v == v) {
704 continue;
705 }
706 let pos = plan.fresh[idx as usize].map(f64::from);
707 let (d, tpar) = point_seg(pos, c0, c1);
708 if d <= 1e-6 * axis_len {
709 ordered.push((tpar, WPt { pos, v }));
710 }
711 }
712 }
713 ordered.sort_by(|x, y| x.0.total_cmp(&y.0));
714 for (_, w) in ordered {
715 edge.push(pts.len());
716 push(&mut pts, w)?;
717 }
718 }
719 let mut inner_idx: Vec<usize> = Vec::new();
721 for loc in ends.iter().flat_map(|(a, b)| [*a, *b]) {
722 if let PtLoc::Inner { pos, v } = loc
723 && !pts.iter().any(|w| w.v == v)
724 {
725 inner_idx.push(pts.len());
726 push(&mut pts, WPt { pos, v })?;
727 }
728 }
729 let mut segs: Vec<(usize, usize)> = Vec::new();
731 for (a, b) in ends {
732 let (ia, ib) = (loc_index(*a, &pts), loc_index(*b, &pts));
733 if ia != ib {
734 push(&mut segs, (ia.min(ib), ia.max(ib)))?;
735 }
736 }
737 segs.sort_unstable();
738 segs.dedup();
739 let mut edge_out: [Vec<WPt>; 3] = [Vec::new(), Vec::new(), Vec::new()];
741 for (src, dst) in edge_idx.iter().zip(edge_out.iter_mut()) {
742 for &at in src {
743 push(dst, pts[at])?;
744 }
745 }
746 let mut inner_out: Vec<WPt> = Vec::new();
747 for &at in &inner_idx {
748 push(&mut inner_out, pts[at])?;
749 }
750 Ok(TriWork {
751 corners,
752 corner_v,
753 edge: edge_out,
754 inner: inner_out,
755 segs,
756 })
757}
758
759fn loc_index(loc: PtLoc, pts: &[WPt]) -> usize {
762 match loc {
763 PtLoc::Corner(c) => c,
764 PtLoc::Edge { v, .. } | PtLoc::Inner { v, .. } => {
765 pts.iter().position(|w| w.v == v).expect("located point")
766 }
767 }
768}
769
770fn split_triangle(
773 work: TriWork,
774 plan: &mut BooleanPlan,
775 fresh_of: &mut BTreeMap<[u32; 3], u32>,
776) -> Result<Vec<[PRef; 3]>> {
777 let mut pts: Vec<WPt> = Vec::new();
781 for (k, p) in work.corners.iter().enumerate() {
782 push(
783 &mut pts,
784 WPt {
785 pos: *p,
786 v: work.corner_v[k],
787 },
788 )?;
789 }
790 let mut edge_idx: [Vec<usize>; 3] = [Vec::new(), Vec::new(), Vec::new()];
791 for (src, dst) in work.edge.iter().zip(edge_idx.iter_mut()) {
792 for w in src {
793 dst.push(pts.len());
794 push(&mut pts, *w)?;
795 }
796 }
797 let mut inner_idx: Vec<usize> = Vec::new();
798 for w in &work.inner {
799 inner_idx.push(pts.len());
800 push(&mut pts, *w)?;
801 }
802 let mut segs: Vec<(usize, usize)> = Vec::new();
803 for &(a, b) in &work.segs {
804 if a != b {
805 push(&mut segs, (a.min(b), a.max(b)))?;
806 }
807 }
808 segs.sort_unstable();
809 segs.dedup();
810 let n = cross(
812 sub(work.corners[1], work.corners[0]),
813 sub(work.corners[2], work.corners[0]),
814 );
815 let axis = [n[0].abs(), n[1].abs(), n[2].abs()]
816 .iter()
817 .enumerate()
818 .max_by(|x, y| x.1.total_cmp(y.1))
819 .map(|(i, _)| i)
820 .unwrap_or(2);
821 let flat2 = |p: D3| -> [f64; 2] { [p[(axis + 1) % 3], p[(axis + 2) % 3]] };
822 split_crossings(&mut pts, &mut segs, &mut inner_idx, flat2, plan, fresh_of)?;
824 let mut ring = Vec::new();
826 for (i, edge) in edge_idx.iter().enumerate() {
827 push(&mut ring, i)?;
828 ring.extend_from_slice(edge);
829 }
830 let mut pieces = vec![Piece {
834 ring,
835 inner: inner_idx,
836 segs,
837 }];
838 loop {
843 let before: (usize, usize) = pieces.iter().fold((0, 0), |(ni, ns), p| {
844 (ni + p.inner.len(), ns + p.segs.len())
845 });
846 let mut hugged: Option<Vec<Piece>> = None;
851 for (pi, piece) in pieces.iter().enumerate() {
852 if let Some(seg) = piece
853 .segs
854 .iter()
855 .find(|&&(a, b)| hugs_ring(a, b, piece, &pts, flat2))
856 {
857 let mut out = Vec::new();
858 for (qi, q) in pieces.iter().enumerate() {
859 if qi == pi {
860 let mut p = q.clone();
861 p.segs.retain(|&s| s != *seg);
862 push(&mut out, p)?;
863 } else {
864 push(&mut out, q.clone())?;
865 }
866 }
867 hugged = Some(out);
868 break;
869 }
870 }
871 if let Some(out) = hugged {
872 pieces = out;
873 continue;
874 }
875 let mut next: Option<Vec<Piece>> = None;
876 for (pi, piece) in pieces.iter().enumerate() {
877 if let Some((p1, p2)) = full_split(piece, &pts, flat2)? {
878 let mut out = Vec::new();
879 for (qi, q) in pieces.iter().enumerate() {
880 if qi == pi {
881 push(&mut out, p1.clone())?;
882 push(&mut out, p2.clone())?;
883 } else {
884 push(&mut out, q.clone())?;
885 }
886 }
887 next = Some(out);
888 break;
889 }
890 if let Some(&q) = piece
891 .inner
892 .iter()
893 .find(|q| piece.segs.iter().any(|&(a, b)| a == **q || b == **q))
894 {
895 let fanned = fan_split(piece, q, &mut pts, flat2, plan, fresh_of)?;
896 let mut out = Vec::new();
897 for (qi, qpiece) in pieces.iter().enumerate() {
898 if qi == pi {
899 for f in &fanned {
900 push(&mut out, f.clone())?;
901 }
902 } else {
903 push(&mut out, qpiece.clone())?;
904 }
905 }
906 next = Some(out);
907 break;
908 }
909 }
910 match next {
911 Some(out) => pieces = out,
912 None => break,
913 }
914 let after: (usize, usize) = pieces.iter().fold((0, 0), |(ni, ns), p| {
915 (ni + p.inner.len(), ns + p.segs.len())
916 });
917 if after.0 > before.0 || (after.0 == before.0 && after.1 >= before.1) {
918 return Err(OpsError::InvalidInput("boolean intersection too complex"));
921 }
922 }
923 let mut tris: Vec<[PRef; 3]> = Vec::new();
927 for piece in &pieces {
928 if piece.ring.len() < 3 {
929 continue;
930 }
931 let n = piece.ring.len();
936 let ext: Vec<usize> = piece
938 .ring
939 .iter()
940 .chain(piece.ring.iter().take(2))
941 .copied()
942 .collect();
943 let mut is_corner = vec![true; n];
944 for (k, w) in ext.windows(3).enumerate() {
945 let (a, b, c) = (
946 flat2(pts[w[0]].pos),
947 flat2(pts[w[1]].pos),
948 flat2(pts[w[2]].pos),
949 );
950 if colinear3(a, b, c) {
951 is_corner[(k + 1) % n] = false;
953 }
954 }
955 let mut corners: Vec<usize> = Vec::new();
956 for (k, &x) in piece.ring.iter().enumerate() {
957 if is_corner[k] {
958 push(&mut corners, x)?;
959 }
960 }
961 if corners.len() < 3 {
962 continue;
963 }
964 let mut runs: BTreeMap<(usize, usize), Vec<usize>> = BTreeMap::new();
965 for (ci, &c) in corners.iter().enumerate() {
966 let cn = corners[(ci + 1) % corners.len()];
967 let mut mid = Vec::new();
968 let mut k = piece.ring.iter().position(|&x| x == c).unwrap();
969 loop {
970 k = (k + 1) % n;
971 if piece.ring[k] == cn {
972 break;
973 }
974 push(&mut mid, piece.ring[k])?;
975 }
976 if !mid.is_empty() {
977 runs.insert((c, cn), mid);
978 }
979 }
980 let mut out: Vec<[usize; 3]> = Vec::new();
983 let mut ring = corners.clone();
984 while ring.len() > 3 {
985 let tri = [ring[ring.len() - 1], ring[0], ring[1]];
986 push(&mut out, tri)?;
987 ring.remove(0);
988 }
989 push(&mut out, [ring[0], ring[1], ring[2]])?;
990 for ((a, b), qs) in &runs {
991 let at = out
992 .iter()
993 .position(|tri| tri.contains(a) && tri.contains(b))
994 .expect("collapsed edge of a live piece");
995 let tri = out.remove(at);
996 split_chain(tri, *a, *b, qs, &mut out)?;
997 }
998 for tri in out {
1000 let t = [pts[tri[0]].v, pts[tri[1]].v, pts[tri[2]].v];
1001 if t[0] == t[1] || t[1] == t[2] || t[0] == t[2] {
1002 continue;
1003 }
1004 let p = [pts[tri[0]].pos, pts[tri[1]].pos, pts[tri[2]].pos];
1005 if area2(p[0], p[1], p[2]) > AREA_EPSILON * max_edge2(&p).max(1e-300) {
1006 push(&mut tris, t)?;
1007 }
1008 }
1009 }
1010 Ok(tris)
1011}
1012
1013fn orient2(a: [f64; 2], b: [f64; 2], c: [f64; 2]) -> f64 {
1015 (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])
1016}
1017
1018fn colinear3(a: [f64; 2], b: [f64; 2], c: [f64; 2]) -> bool {
1023 let o = orient2(a, b, c).abs();
1024 let m = (b[0] - a[0]).powi(2) + (b[1] - a[1]).powi(2);
1025 let m = m.max((c[0] - b[0]).powi(2) + (c[1] - b[1]).powi(2));
1026 o * o <= 1e-12 * m * m.max(1e-300)
1027}
1028
1029enum Cross {
1032 Point(f64, f64),
1033 Colinear,
1034 None,
1035}
1036
1037fn seg_cross(p: [f64; 2], q: [f64; 2], r: [f64; 2], s: [f64; 2]) -> Cross {
1038 let d = orient2(p, q, r);
1039 let e = orient2(p, q, s);
1040 let f = orient2(r, s, p);
1041 let g = orient2(r, s, q);
1042 if d == 0.0 && e == 0.0 && f == 0.0 && g == 0.0 {
1043 return Cross::Colinear;
1044 }
1045 if d * e >= 0.0 || f * g >= 0.0 {
1046 return Cross::None;
1047 }
1048 let denom = (q[0] - p[0]) * (s[1] - r[1]) - (q[1] - p[1]) * (s[0] - r[0]);
1049 if denom == 0.0 {
1050 return Cross::None;
1051 }
1052 let t = ((r[0] - p[0]) * (s[1] - r[1]) - (r[1] - p[1]) * (s[0] - r[0])) / denom;
1053 let u = ((r[0] - p[0]) * (q[1] - p[1]) - (r[1] - p[1]) * (q[0] - p[0])) / denom;
1054 if t > 1e-9 && t < 1.0 - 1e-9 && u > 1e-9 && u < 1.0 - 1e-9 {
1055 Cross::Point(t, u)
1056 } else {
1057 Cross::None
1058 }
1059}
1060
1061fn on_seg_tol(p: [f64; 2], a: [f64; 2], b: [f64; 2], tol: f64) -> bool {
1064 let ab = [b[0] - a[0], b[1] - a[1]];
1065 let len2 = ab[0] * ab[0] + ab[1] * ab[1];
1066 if len2 == 0.0 {
1067 return false;
1068 }
1069 let t = ((p[0] - a[0]) * ab[0] + (p[1] - a[1]) * ab[1]) / len2;
1070 if t <= 1e-9 || t >= 1.0 - 1e-9 {
1071 return false;
1072 }
1073 let c = [a[0] + t * ab[0], a[1] + t * ab[1]];
1074 let d = (p[0] - c[0]) * (p[0] - c[0]) + (p[1] - c[1]) * (p[1] - c[1]);
1075 d <= tol * tol * len2
1076}
1077
1078fn on_seg(p: [f64; 2], a: [f64; 2], b: [f64; 2]) -> bool {
1081 on_seg_tol(p, a, b, 1e-9)
1082}
1083
1084#[allow(clippy::too_many_arguments)]
1088fn split_crossings(
1089 pts: &mut Vec<WPt>,
1090 segs: &mut Vec<(usize, usize)>,
1091 inner: &mut Vec<usize>,
1092 flat2: impl Copy + Fn(D3) -> [f64; 2],
1093 plan: &mut BooleanPlan,
1094 fresh_of: &mut BTreeMap<[u32; 3], u32>,
1095) -> Result<()> {
1096 let mut passes = segs.len() * 4 + 64;
1100 loop {
1101 if passes == 0 {
1102 return Err(OpsError::InvalidInput("boolean intersection too complex"));
1103 }
1104 passes -= 1;
1105 let mut snapped: Option<(usize, usize)> = None;
1111 for (i, &(a, b)) in segs.iter().enumerate() {
1112 for (j, &(c, d)) in segs.iter().enumerate().take(i) {
1113 if a == c || a == d || b == c || b == d {
1114 continue;
1115 }
1116 let (pa, pb) = (flat2(pts[a].pos), flat2(pts[b].pos));
1117 let dab = [(pb[0] - pa[0]), (pb[1] - pa[1])];
1118 let len2 = dab[0] * dab[0] + dab[1] * dab[1];
1119 for (e, (o, oo), si) in [
1120 (a, (c, d), j),
1121 (b, (c, d), j),
1122 (c, (a, b), i),
1123 (d, (a, b), i),
1124 ] {
1125 let pe = flat2(pts[e].pos);
1126 let o2 = orient2(pa, pb, pe);
1129 if o2 * o2 > 1e-12 * len2 * len2 {
1130 continue;
1131 }
1132 if on_seg_tol(pe, flat2(pts[o].pos), flat2(pts[oo].pos), 1e-6) {
1133 snapped = Some((si, e));
1134 break;
1135 }
1136 }
1137 if snapped.is_some() {
1138 break;
1139 }
1140 }
1141 if snapped.is_some() {
1142 break;
1143 }
1144 }
1145 if let Some((si, e)) = snapped {
1146 let (x, y) = segs[si];
1147 segs[si] = (x.min(e), x.max(e));
1148 push(segs, (e.min(y), e.max(y)))?;
1149 segs.sort_unstable();
1150 segs.dedup();
1151 continue;
1152 }
1153 let mut crossing: Option<(usize, usize, f64)> = None;
1154 let mut colinear: Option<(usize, usize)> = None;
1155 for (i, &(a, b)) in segs.iter().enumerate() {
1156 for (j, &(c, d)) in segs.iter().enumerate().take(i) {
1157 if a == c || a == d || b == c || b == d {
1158 continue;
1159 }
1160 let (pa, pb, pc, pd) = (
1161 flat2(pts[a].pos),
1162 flat2(pts[b].pos),
1163 flat2(pts[c].pos),
1164 flat2(pts[d].pos),
1165 );
1166 match seg_cross(pa, pb, pc, pd) {
1167 Cross::Point(t, _) => {
1168 crossing = Some((i, j, t));
1169 break;
1170 }
1171 Cross::Colinear => {
1172 for (e, si) in [(a, j), (b, j), (c, i), (d, i)] {
1173 let (o, oo) = if si == j { (c, d) } else { (a, b) };
1174 if on_seg(flat2(pts[e].pos), flat2(pts[o].pos), flat2(pts[oo].pos)) {
1175 colinear = Some((si, e));
1176 break;
1177 }
1178 }
1179 if colinear.is_some() {
1180 break;
1181 }
1182 return Err(OpsError::InvalidInput("boolean intersection too complex"));
1183 }
1184 Cross::None => {}
1185 }
1186 }
1187 if crossing.is_some() || colinear.is_some() {
1188 break;
1189 }
1190 }
1191 if let Some((si, e)) = colinear {
1192 let (x, y) = segs[si];
1196 segs[si] = (x.min(e), x.max(e));
1197 push(segs, (e.min(y), e.max(y)))?;
1198 } else if let Some((i, j, t)) = crossing {
1199 let (a, b) = segs[i];
1200 let (c, d) = segs[j];
1201 let p = std::array::from_fn(|k| pts[a].pos[k] + t * (pts[b].pos[k] - pts[a].pos[k]));
1202 let v = fresh(p, plan, fresh_of)?;
1203 let at = pts.len();
1204 push(pts, WPt { pos: p, v })?;
1205 push(inner, at)?;
1206 segs[i] = (a.min(at), a.max(at));
1207 push(segs, (at.min(b), at.max(b)))?;
1208 segs[j] = (c.min(at), c.max(at));
1209 push(segs, (at.min(d), at.max(d)))?;
1210 } else {
1211 break;
1212 }
1213 segs.sort_unstable();
1214 segs.dedup();
1215 }
1216 Ok(())
1217}
1218
1219fn hugs_ring(
1223 a: usize,
1224 b: usize,
1225 piece: &Piece,
1226 pts: &[WPt],
1227 flat2: impl Copy + Fn(D3) -> [f64; 2],
1228) -> bool {
1229 let pa = piece.ring.iter().position(|&x| x == a);
1230 let pb = piece.ring.iter().position(|&x| x == b);
1231 let (Some(ia), Some(ib)) = (pa, pb) else {
1232 return false;
1233 };
1234 if ia == ib {
1235 return true;
1236 }
1237 let (pa, pb) = (flat2(pts[a].pos), flat2(pts[b].pos));
1238 let dab = [(pb[0] - pa[0]), (pb[1] - pa[1])];
1239 let len2 = dab[0] * dab[0] + dab[1] * dab[1];
1240 let tol = 1e-7 * len2.max(1e-300);
1241 let (lo, hi) = if ia < ib { (ia, ib) } else { (ib, ia) };
1242 let colinear = |range: &[usize]| {
1243 range.iter().all(|&x| {
1244 let p = flat2(pts[x].pos);
1245 orient2(pa, pb, p).abs() <= tol
1246 })
1247 };
1248 colinear(&piece.ring[lo + 1..hi])
1249 || colinear(&piece.ring[hi + 1..]) && colinear(&piece.ring[..lo])
1250}
1251
1252fn full_split(
1255 piece: &Piece,
1256 pts: &[WPt],
1257 flat2: impl Copy + Fn(D3) -> [f64; 2],
1258) -> Result<Option<(Piece, Piece)>> {
1259 for &(a, b) in &piece.segs {
1260 let pa = piece.ring.iter().position(|&x| x == a);
1261 let pb = piece.ring.iter().position(|&x| x == b);
1262 let (Some(ia), Some(ib)) = (pa, pb) else {
1263 continue;
1264 };
1265 if ia == ib {
1266 continue;
1267 }
1268 let (lo, hi) = if ia < ib { (ia, ib) } else { (ib, ia) };
1270 let r1: Vec<usize> = piece.ring[lo..=hi].to_vec();
1271 let mut r2: Vec<usize> = piece.ring[hi..].to_vec();
1272 r2.extend_from_slice(&piece.ring[..=lo]);
1273 if r1.len() < 3 || r2.len() < 3 {
1274 continue;
1275 }
1276 let mut p1 = Piece {
1277 ring: r1,
1278 inner: Vec::new(),
1279 segs: Vec::new(),
1280 };
1281 let mut p2 = Piece {
1282 ring: r2,
1283 inner: Vec::new(),
1284 segs: Vec::new(),
1285 };
1286 for &(c, d) in &piece.segs {
1289 if (c == a && d == b) || (c == b && d == a) {
1290 continue;
1291 }
1292 let in1 = |x: usize| point_in_piece(flat2(pts[x].pos), &p1, pts, flat2);
1293 if in1(c) && in1(d) {
1294 push(&mut p1.segs, (c, d))?;
1295 } else {
1296 push(&mut p2.segs, (c, d))?;
1297 }
1298 }
1299 for &q in &piece.inner {
1300 if point_in_piece(flat2(pts[q].pos), &p1, pts, flat2) {
1301 push(&mut p1.inner, q)?;
1302 } else {
1303 push(&mut p2.inner, q)?;
1304 }
1305 }
1306 return Ok(Some((p1, p2)));
1307 }
1308 Ok(None)
1309}
1310
1311fn point_in_piece(
1313 p: [f64; 2],
1314 piece: &Piece,
1315 pts: &[WPt],
1316 flat2: impl Copy + Fn(D3) -> [f64; 2],
1317) -> bool {
1318 let n = piece.ring.len();
1319 if n < 3 {
1320 return false;
1321 }
1322 let p0 = flat2(pts[piece.ring[0]].pos);
1324 for k in 1..n - 1 {
1325 let a = flat2(pts[piece.ring[k]].pos);
1326 let b = flat2(pts[piece.ring[k + 1]].pos);
1327 let o = orient2(p0, a, b);
1328 let inside = if o >= 0.0 {
1329 orient2(p0, a, p) >= -1e-9 && orient2(a, b, p) >= -1e-9 && orient2(b, p0, p) >= -1e-9
1330 } else {
1331 orient2(p0, a, p) <= 1e-9 && orient2(a, b, p) <= 1e-9 && orient2(b, p0, p) <= 1e-9
1332 };
1333 if inside {
1334 return true;
1335 }
1336 }
1337 false
1338}
1339
1340#[allow(clippy::too_many_arguments)]
1344fn fan_split(
1345 piece: &Piece,
1346 q: usize,
1347 pts: &mut Vec<WPt>,
1348 flat2: impl Copy + Fn(D3) -> [f64; 2],
1349 plan: &mut BooleanPlan,
1350 fresh_of: &mut BTreeMap<[u32; 3], u32>,
1351) -> Result<Vec<Piece>> {
1352 let n = piece.ring.len();
1353 let mut spoke_pts: Vec<Vec<usize>> = vec![Vec::new(); n];
1355 let mut cuts: Vec<(usize, usize, D3)> = Vec::new();
1358 for &(a, b) in &piece.segs {
1359 if a == q || b == q {
1360 continue;
1361 }
1362 for (i, &r) in piece.ring.iter().enumerate() {
1363 let pa = flat2(pts[a].pos);
1364 let pb = flat2(pts[b].pos);
1365 let pq = flat2(pts[q].pos);
1366 let pr = flat2(pts[r].pos);
1367 if let Cross::Point(t, u) = seg_cross(pa, pb, pq, pr)
1368 && t > 1e-9
1369 && t < 1.0 - 1e-9
1370 && u > 1e-9
1371 && u < 1.0 - 1e-9
1372 {
1373 let p =
1374 std::array::from_fn(|k| pts[a].pos[k] + t * (pts[b].pos[k] - pts[a].pos[k]));
1375 push(&mut cuts, (i, a, p))?;
1376 }
1377 }
1378 }
1379 let mut per_spoke: Vec<Vec<(f64, D3)>> = vec![Vec::new(); n];
1381 for (i, _, p) in &cuts {
1382 let (_, u) = point_seg2(flat2(*p), flat2(pts[q].pos), flat2(pts[piece.ring[*i]].pos));
1384 per_spoke[*i].push((u, *p));
1385 }
1386 for sp in per_spoke.iter_mut() {
1387 sp.sort_by(|x, y| x.0.total_cmp(&y.0));
1388 }
1389 let mut seg_list = piece.segs.clone();
1391 for (i, sp) in per_spoke.iter().enumerate() {
1392 for &(_, p) in sp {
1393 let v = fresh(p, plan, fresh_of)?;
1394 let at = pts.len();
1395 push(pts, WPt { pos: p, v })?;
1396 let mut k = 0;
1399 while k < seg_list.len() {
1400 let (x, y) = seg_list[k];
1401 if x == at || y == at {
1402 k += 1;
1403 continue;
1404 }
1405 if on_seg(flat2(p), flat2(pts[x].pos), flat2(pts[y].pos)) {
1406 seg_list[k] = (x.min(at), x.max(at));
1407 push(&mut seg_list, (at.min(y), at.max(y)))?;
1408 }
1409 k += 1;
1410 }
1411 spoke_pts[i].push(at);
1412 }
1413 }
1414 seg_list.sort_unstable();
1415 seg_list.dedup();
1416 let mut out: Vec<Piece> = Vec::new();
1418 let mut chains: Vec<Vec<usize>> = Vec::new();
1420 for (spoke, &r) in spoke_pts.iter().zip(piece.ring.iter()) {
1421 let mut chain = vec![q];
1422 chain.extend_from_slice(spoke);
1423 chain.push(r);
1424 chains.push(chain);
1425 }
1426 for i in 0..n {
1427 let (c0, c1) = (&chains[i], &chains[(i + 1) % n]);
1428 let rows = c0.len().max(c1.len());
1429 for r in 0..rows - 1 {
1430 let a0 = c0[r.min(c0.len() - 1)];
1431 let a1 = c0[(r + 1).min(c0.len() - 1)];
1432 let b0 = c1[r.min(c1.len() - 1)];
1433 let b1 = c1[(r + 1).min(c1.len() - 1)];
1434 for tri in [[a0, a1, b1], [a0, b1, b0]] {
1435 if tri[0] == tri[1] || tri[1] == tri[2] || tri[0] == tri[2] {
1436 continue;
1437 }
1438 let p = [pts[tri[0]].pos, pts[tri[1]].pos, pts[tri[2]].pos];
1439 if area2(p[0], p[1], p[2]) <= AREA_EPSILON * max_edge2(&p).max(1e-300) {
1440 continue;
1441 }
1442 push(
1443 &mut out,
1444 Piece {
1445 ring: vec![tri[0], tri[1], tri[2]],
1446 inner: Vec::new(),
1447 segs: Vec::new(),
1448 },
1449 )?;
1450 }
1451 }
1452 }
1453 for (a, b) in &seg_list {
1458 let mid = std::array::from_fn(|k| 0.5 * (pts[*a].pos[k] + pts[*b].pos[k]));
1459 let mut at = None;
1460 for (fi, fan) in out.iter().enumerate() {
1461 if fan_contains_pos(fan, flat2(mid), pts, flat2) {
1462 at = Some(fi);
1463 break;
1464 }
1465 }
1466 let Some(fi) = at else {
1467 return Err(OpsError::InvalidInput("boolean intersection too complex"));
1468 };
1469 for &x in [a, b] {
1470 complete_endpoint(&mut out[fi], x, pts, flat2)?;
1471 }
1472 push(&mut out[fi].segs, (*a, *b))?;
1473 }
1474 for &p in &piece.inner {
1475 if p == q {
1476 continue;
1477 }
1478 let mut placed = false;
1479 for fan in out.iter_mut() {
1480 if fan_contains(fan, p, pts, flat2) {
1481 if !fan.ring.contains(&p) && !fan.inner.contains(&p) {
1482 push(&mut fan.inner, p)?;
1483 }
1484 placed = true;
1485 break;
1486 }
1487 }
1488 if !placed {
1489 return Err(OpsError::InvalidInput("boolean intersection too complex"));
1490 }
1491 }
1492 Ok(out)
1493}
1494
1495fn split_chain<T: Copy + PartialEq>(
1499 tri: [T; 3],
1500 u: T,
1501 v: T,
1502 qs: &[T],
1503 out: &mut Vec<[T; 3]>,
1504) -> Result<()> {
1505 let iu = tri.iter().position(|&x| x == u).expect("edge of triangle");
1506 let (a, b, o, qs): (T, T, T, Vec<T>) = if tri[(iu + 1) % 3] == v {
1508 (u, v, tri[(iu + 2) % 3], qs.to_vec())
1509 } else {
1510 debug_assert!(tri[(iu + 1) % 3] != u && tri[(iu + 2) % 3] == v);
1511 let o = tri[(iu + 1) % 3];
1512 let mut r = qs.to_vec();
1513 r.reverse();
1514 (v, u, o, r)
1515 };
1516 let mut prev = a;
1517 for q in qs.iter().chain(std::iter::once(&b)) {
1518 push(out, [prev, *q, o])?;
1519 prev = *q;
1520 }
1521 Ok(())
1522}
1523
1524fn fan_contains_pos(
1526 fan: &Piece,
1527 p: [f64; 2],
1528 pts: &[WPt],
1529 flat2: impl Copy + Fn(D3) -> [f64; 2],
1530) -> bool {
1531 if fan.ring.len() != 3 {
1532 return point_in_piece(p, fan, pts, flat2);
1533 }
1534 let t = [
1535 flat2(pts[fan.ring[0]].pos),
1536 flat2(pts[fan.ring[1]].pos),
1537 flat2(pts[fan.ring[2]].pos),
1538 ];
1539 let o = orient2(t[0], t[1], t[2]);
1540 let (s0, s1, s2) = (
1541 orient2(t[0], t[1], p),
1542 orient2(t[1], t[2], p),
1543 orient2(t[2], t[0], p),
1544 );
1545 if o >= 0.0 {
1546 s0 >= -1e-9 && s1 >= -1e-9 && s2 >= -1e-9
1547 } else {
1548 s0 <= 1e-9 && s1 <= 1e-9 && s2 <= 1e-9
1549 }
1550}
1551
1552fn complete_endpoint(
1556 fan: &mut Piece,
1557 x: usize,
1558 pts: &[WPt],
1559 flat2: impl Copy + Fn(D3) -> [f64; 2],
1560) -> Result<()> {
1561 if fan.ring.contains(&x) || fan.inner.contains(&x) {
1562 return Ok(());
1563 }
1564 let p = flat2(pts[x].pos);
1565 let mut on_edge = None;
1567 for i in 0..fan.ring.len() {
1568 let (a, b) = (
1569 flat2(pts[fan.ring[i]].pos),
1570 flat2(pts[fan.ring[(i + 1) % fan.ring.len()]].pos),
1571 );
1572 let (d, t) = point_seg2(p, a, b);
1573 let len = (b[0] - a[0]).hypot(b[1] - a[1]).max(1e-300);
1574 if d <= 1e-9 * len && t > 1e-9 && t < 1.0 - 1e-9 {
1575 on_edge = Some(i);
1576 break;
1577 }
1578 }
1579 if let Some(i) = on_edge {
1580 let mut at = i + 1;
1582 while at < fan.ring.len() + i {
1583 let prev = fan.ring[at % fan.ring.len()];
1584 let (d, _) = point_seg2(p, flat2(pts[fan.ring[i]].pos), flat2(pts[prev].pos));
1585 let prev_p = flat2(pts[prev].pos);
1587 let edge_p = flat2(pts[fan.ring[i]].pos);
1588 let along = (p[0] - edge_p[0]) * (prev_p[0] - edge_p[0])
1589 + (p[1] - edge_p[1]) * (prev_p[1] - edge_p[1]);
1590 let _ = d;
1591 if along <= 0.0 {
1592 break;
1593 }
1594 at += 1;
1595 if at - i - 1 > fan.ring.len() {
1596 break;
1597 }
1598 }
1599 fan.ring.insert(at % (fan.ring.len() + 1), x);
1600 return Ok(());
1601 }
1602 push(&mut fan.inner, x)
1603}
1604fn fan_contains(fan: &Piece, p: usize, pts: &[WPt], flat2: impl Copy + Fn(D3) -> [f64; 2]) -> bool {
1605 if fan.ring.len() != 3 {
1606 return point_in_piece(flat2(pts[p].pos), fan, pts, flat2);
1607 }
1608 let q = flat2(pts[p].pos);
1609 let t = [
1610 flat2(pts[fan.ring[0]].pos),
1611 flat2(pts[fan.ring[1]].pos),
1612 flat2(pts[fan.ring[2]].pos),
1613 ];
1614 let o = orient2(t[0], t[1], t[2]);
1615 let (s0, s1, s2) = (
1616 orient2(t[0], t[1], q),
1617 orient2(t[1], t[2], q),
1618 orient2(t[2], t[0], q),
1619 );
1620 if o >= 0.0 {
1621 s0 >= -1e-9 && s1 >= -1e-9 && s2 >= -1e-9
1622 } else {
1623 s0 <= 1e-9 && s1 <= 1e-9 && s2 <= 1e-9
1624 }
1625}
1626
1627fn point_seg2(p: [f64; 2], a: [f64; 2], b: [f64; 2]) -> (f64, f64) {
1629 let ab = [b[0] - a[0], b[1] - a[1]];
1630 let len2 = ab[0] * ab[0] + ab[1] * ab[1];
1631 if len2 == 0.0 {
1632 return (((p[0] - a[0]).powi(2) + (p[1] - a[1]).powi(2)).sqrt(), 0.0);
1633 }
1634 let t = ((p[0] - a[0]) * ab[0] + (p[1] - a[1]) * ab[1]) / len2;
1635 let c = [
1636 a[0] + t.clamp(0.0, 1.0) * ab[0],
1637 a[1] + t.clamp(0.0, 1.0) * ab[1],
1638 ];
1639 (((p[0] - c[0]).powi(2) + (p[1] - c[1]).powi(2)).sqrt(), t)
1640}
1641
1642#[allow(clippy::too_many_arguments)]
1645fn classify_side(
1646 mesh: &Mesh,
1647 pieces: &BTreeMap<TriangleId, SplitTris>,
1648 side: &Side,
1649 tool: &ToolRef<'_>,
1650 op: BooleanOp,
1651 is_a: bool,
1652 plan: &mut BooleanPlan,
1653 fresh_of: &mut BTreeMap<[u32; 3], u32>,
1654) -> Result<()> {
1655 for t in 0..mesh.triangle_slots() as TriangleId {
1656 let Some(tri) = mesh.triangle(t) else {
1657 continue;
1658 };
1659 let whole: [PRef; 3] = match side {
1660 Side::A => tri.map(PRef::Live),
1661 Side::B => {
1662 let mut v = [PRef::Fresh(0); 3];
1663 for (k, p) in tri_pos(mesh, t).iter().enumerate() {
1664 v[k] = fresh(*p, plan, fresh_of)?;
1665 }
1666 v
1667 }
1668 };
1669 let owned;
1670 let (candidates, edge_split): (&[[PRef; 3]], bool) = match pieces.get(&t) {
1671 Some(split) => (&split.pieces, split.edge_split),
1672 None => {
1673 owned = vec![whole];
1674 (&owned, false)
1675 }
1676 };
1677 let mut kept: Vec<[PRef; 3]> = Vec::new();
1679 for c in candidates {
1680 let (centroid, normal) = piece_geom(mesh, c, plan)?;
1681 if keep_piece(tool, centroid, normal, op, is_a, plan.tol)? {
1682 push(&mut kept, *c)?;
1683 }
1684 }
1685 match side {
1686 Side::A => {
1687 if kept.is_empty() {
1688 push(&mut plan.drop_a, t)?;
1689 } else if pieces.contains_key(&t) && (kept.len() < candidates.len() || edge_split) {
1690 push(&mut plan.rewrite, (t, kept))?;
1693 }
1694 }
1697 Side::B => {
1698 for mut k in kept {
1699 if op == BooleanOp::Subtract {
1700 k.swap(1, 2);
1702 }
1703 push(&mut plan.tool, k)?;
1704 }
1705 }
1706 }
1707 }
1708 Ok(())
1709}
1710
1711fn piece_geom(mesh: &Mesh, tri: &[PRef; 3], plan: &BooleanPlan) -> Result<(D3, D3)> {
1713 let mut p = [[0.0; 3]; 3];
1714 for (k, r) in tri.iter().enumerate() {
1715 p[k] = match r {
1716 PRef::Live(v) => mesh
1717 .position(*v)
1718 .ok_or(OpsError::InvalidInput("boolean triangle lost a vertex"))?
1719 .map(f64::from),
1720 PRef::Fresh(i) => plan
1721 .fresh
1722 .get(*i as usize)
1723 .ok_or(OpsError::InvalidInput("boolean plan is not ready"))?
1724 .map(f64::from),
1725 };
1726 }
1727 let c = std::array::from_fn(|k| (p[0][k] + p[1][k] + p[2][k]) / 3.0);
1728 Ok((c, cross(sub(p[1], p[0]), sub(p[2], p[0]))))
1729}
1730
1731fn keep_piece(
1733 tool: &ToolRef<'_>,
1734 c: D3,
1735 n: D3,
1736 op: BooleanOp,
1737 is_a: bool,
1738 tol: f32,
1739) -> Result<bool> {
1740 let tol = tol as f64;
1741 let (inside, on_surface) = parity(tool, c, tol)?;
1742 if !on_surface {
1743 return Ok(match (op, is_a) {
1744 (BooleanOp::Union, _) => !inside,
1745 (BooleanOp::Subtract, true) => !inside,
1746 (BooleanOp::Subtract, false) => inside,
1747 (BooleanOp::Intersect, _) => inside,
1748 });
1749 }
1750 let len = dot(n, n).sqrt();
1754 if len == 0.0 {
1755 return Err(OpsError::InvalidInput("boolean piece lost its normal"));
1756 }
1757 let u = n.map(|x| x / len);
1758 let eps = 64.0 * tol;
1759 let nt = tool_normal(tool, c)?;
1760 let flush = dot(u, nt) > 0.0;
1761 let p_out = std::array::from_fn(|k| c[k] + eps * u[k]);
1762 let (out_inside, _) = parity(tool, p_out, tol)?;
1763 let p_in = std::array::from_fn(|k| c[k] - eps * u[k]);
1764 let (in_inside, _) = parity(tool, p_in, tol)?;
1765 Ok(match (op, is_a) {
1766 (BooleanOp::Union, true) => !out_inside,
1771 (BooleanOp::Union, false) => false,
1772 (BooleanOp::Subtract, true) => {
1773 if flush {
1774 !in_inside
1775 } else {
1776 true
1777 }
1778 }
1779 (BooleanOp::Subtract, false) => false,
1780 (BooleanOp::Intersect, true) => true,
1781 (BooleanOp::Intersect, false) => false,
1782 })
1783}
1784
1785fn parity(tool: &ToolRef<'_>, p: D3, tol: f64) -> Result<(bool, bool)> {
1788 let origin = [p[0] as f32, p[1] as f32, p[2] as f32];
1789 if !origin.iter().all(|c| c.is_finite()) {
1790 return Err(OpsError::InvalidInput("boolean point leaves f32 range"));
1791 }
1792 let mut inside_votes = 0;
1793 let mut on_surface = false;
1794 let mut hits: Vec<fast_mesh_query::RayHit> = Vec::new();
1795 for dir in [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]] {
1796 hits.clear();
1797 let ray = Ray { origin, dir };
1798 let opts = RayOptions {
1799 min_t: 0.0,
1800 max_t: f32::INFINITY,
1801 cull: Cull::None,
1802 };
1803 raycast_all(tool.mesh, tool.bvh, &ray, &opts, &mut hits)?;
1804 hits.sort_by(|x, y| x.t.total_cmp(&y.t));
1806 let mut groups = 0;
1807 let mut first = f32::INFINITY;
1808 let mut prev = f32::NEG_INFINITY;
1809 for h in &hits {
1810 if (h.t - prev) as f64 > 1e-6 * (1.0 + h.t as f64) {
1811 groups += 1;
1812 if first.is_infinite() {
1813 first = h.t;
1814 }
1815 }
1816 prev = h.t;
1817 }
1818 if groups % 2 == 1 {
1819 inside_votes += 1;
1820 }
1821 if (first as f64) <= tol {
1822 on_surface = true;
1823 }
1824 }
1825 Ok((inside_votes >= 2, on_surface))
1826}
1827
1828fn tool_normal(tool: &ToolRef<'_>, p: D3) -> Result<D3> {
1830 let q = [p[0] as f32, p[1] as f32, p[2] as f32];
1831 let hit = closest_point(
1832 tool.mesh,
1833 tool.bvh,
1834 q,
1835 &ClosestOptions {
1836 max_distance: f32::INFINITY,
1837 },
1838 )?
1839 .ok_or(OpsError::InvalidInput("boolean tool has no triangles"))?;
1840 let tri = tool
1841 .mesh
1842 .triangle(hit.triangle)
1843 .ok_or(OpsError::InvalidInput("boolean tool lost a triangle"))?;
1844 let pos = tri.map(|v| {
1845 tool.mesh
1846 .position(v)
1847 .expect("tool triangle is live")
1848 .map(f64::from)
1849 });
1850 let n = cross(sub(pos[1], pos[0]), sub(pos[2], pos[0]));
1851 if dot(n, n) == 0.0 {
1852 return Err(OpsError::InvalidInput("boolean tool lost its normal"));
1853 }
1854 Ok(n)
1855}
1856
1857fn stitch(tx: &mut Transaction, tool_ids: &[TriangleId], tol: f32) -> Result<usize> {
1861 let mesh = tx.mesh();
1862 let loops = crate::boundary_loops(mesh)?;
1863 if loops.is_empty() {
1864 return Ok(0);
1865 }
1866 let tool_set: BTreeMap<TriangleId, ()> = tool_ids.iter().map(|&t| (t, ())).collect();
1867 let mut edges: Vec<((VertexId, VertexId), TriangleId, bool)> = Vec::new();
1869 for l in &loops {
1870 let n = l.vertices.len();
1871 if n < 2 {
1872 continue;
1873 }
1874 let end = if l.closed { n } else { n - 1 };
1875 for i in 0..end {
1876 let (u, v) = (l.vertices[i], l.vertices[(i + 1) % n]);
1877 let mut user = None;
1879 for t in 0..mesh.triangle_slots() as TriangleId {
1880 if let Some(tri) = mesh.triangle(t)
1881 && tri.contains(&u)
1882 && tri.contains(&v)
1883 {
1884 user = Some(t);
1885 break;
1886 }
1887 }
1888 let Some(t) = user else { continue };
1889 push(&mut edges, ((u, v), t, tool_set.contains_key(&t)))?;
1890 }
1891 }
1892 let mut rim_a: Vec<VertexId> = Vec::new();
1894 let mut rim_b: Vec<VertexId> = Vec::new();
1895 for ((u, v), _, is_tool) in &edges {
1896 for w in [*u, *v] {
1897 let rim = if *is_tool { &mut rim_b } else { &mut rim_a };
1898 if !rim.contains(&w) {
1899 push(rim, w)?;
1900 }
1901 }
1902 }
1903 let mut positions: BTreeMap<VertexId, D3> = BTreeMap::new();
1906 for w in rim_a.iter().chain(rim_b.iter()) {
1907 if let Some(p) = mesh.position(*w) {
1908 positions.insert(*w, p.map(f64::from));
1909 }
1910 }
1911 let pos = |v: VertexId| positions[&v];
1912 let mut splits: BTreeMap<TriangleId, Vec<(VertexId, VertexId, VertexId)>> = BTreeMap::new();
1914 for ((u, v), t, is_tool) in &edges {
1915 let others = if *is_tool { &rim_a } else { &rim_b };
1916 let (pu, pv) = (pos(*u), pos(*v));
1917 for &w in others {
1918 let (du, dv) = (sub(pos(w), pu), sub(pos(w), pv));
1920 if dot(du, du).sqrt() <= tol as f64 || dot(dv, dv).sqrt() <= tol as f64 {
1921 continue;
1922 }
1923 let (d, _) = point_seg(pos(w), pu, pv);
1924 if d <= tol as f64 {
1925 splits.entry(*t).or_default().push((*u, *v, w));
1926 }
1927 }
1928 }
1929 if splits.is_empty() {
1930 return Ok(0);
1931 }
1932 let mut added = 0usize;
1933 for (t, ss) in splits {
1934 let tri = tx.mesh().triangle(t).expect("split of a live triangle");
1935 let mut tris = vec![tri];
1936 let mut by_edge: BTreeMap<(VertexId, VertexId), Vec<VertexId>> = BTreeMap::new();
1938 for (u, v, w) in ss {
1939 by_edge.entry((u, v)).or_default().push(w);
1940 }
1941 for ((u, v), mut ws) in by_edge {
1942 let (pu, pv) = (pos(u), pos(v));
1943 ws.sort_by(|&x, &y| {
1944 let tx_ = dot(sub(pos(x), pu), sub(pv, pu));
1945 let ty_ = dot(sub(pos(y), pu), sub(pv, pu));
1946 tx_.total_cmp(&ty_)
1947 });
1948 ws.dedup();
1949 let at = tris
1951 .iter()
1952 .position(|tri| tri.contains(&u) && tri.contains(&v))
1953 .expect("split edge of a live triangle");
1954 let tri = tris.remove(at);
1955 split_chain(tri, u, v, &ws, &mut tris)?;
1956 }
1957 if tris.len() <= 1 {
1958 continue;
1959 }
1960 tx.set_triangles(&[t], &[tris[0]])?;
1961 tx.add_triangles(&tris[1..])?;
1962 added += tris.len() - 1;
1963 }
1964 Ok(added)
1965}
1966
1967#[cfg(test)]
1968mod tests {
1969 use super::*;
1970 use crate::test_util::{box_mesh, edit, grid, uv_sphere, with_tetra};
1971 use fast_mesh_query::Rigid;
1972
1973 fn cube() -> Mesh {
1975 box_mesh([0.0, 0.0, 0.0], [1.0, 1.0, 1.0])
1976 }
1977
1978 fn volume6(mesh: &Mesh) -> f64 {
1980 let mut v = 0.0;
1981 for t in 0..mesh.triangle_slots() as TriangleId {
1982 let Some(tri) = mesh.triangle(t) else {
1983 continue;
1984 };
1985 let [a, b, c] = tri.map(|x| mesh.position(x).unwrap().map(f64::from));
1986 v += a[0] * (b[1] * c[2] - b[2] * c[1]) - a[1] * (b[0] * c[2] - b[2] * c[0])
1987 + a[2] * (b[0] * c[1] - b[1] * c[0]);
1988 }
1989 v
1990 }
1991
1992 fn closed(mesh: &Mesh) -> bool {
1993 crate::boundary_loops(mesh).unwrap().is_empty()
1994 }
1995
1996 fn run(a: &mut Mesh, b: &Mesh, op: BooleanOp) -> BooleanReport {
1997 let bvh_a = Bvh::build(a, &BuildOptions::default()).unwrap();
1998 let bvh_b = Bvh::build(b, &BuildOptions::default()).unwrap();
1999 let (report, _) = edit(a, |tx| {
2000 let plan = boolean_plan(tx.mesh(), &bvh_a, b, &bvh_b, &Rigid::IDENTITY, op)?;
2001 apply_boolean(tx, &plan)
2002 });
2003 report
2004 }
2005
2006 #[test]
2007 fn union_overlapping_cubes() {
2008 let mut a = cube();
2010 let b = box_mesh([0.5, 0.0, 0.0], [1.5, 1.0, 1.0]);
2011 let report = run(&mut a, &b, BooleanOp::Union);
2012 assert!(closed(&a), "union must be closed");
2013 assert!(
2014 (volume6(&a) - 6.0 * 1.5).abs() < 1e-3,
2015 "vol {}",
2016 volume6(&a)
2017 );
2018 assert_eq!(report.kept_triangles, a.triangle_count());
2019 }
2020
2021 #[test]
2022 fn subtract_overlapping_cubes() {
2023 let mut a = cube();
2024 let b = box_mesh([0.5, 0.0, 0.0], [1.5, 1.0, 1.0]);
2025 run(&mut a, &b, BooleanOp::Subtract);
2026 assert!(closed(&a), "subtract must be closed");
2027 assert!(
2028 (volume6(&a) - 6.0 * 0.5).abs() < 1e-3,
2029 "vol {}",
2030 volume6(&a)
2031 );
2032 }
2033
2034 #[test]
2035 fn intersect_overlapping_cubes() {
2036 let mut a = cube();
2037 let b = box_mesh([0.5, 0.0, 0.0], [1.5, 1.0, 1.0]);
2038 run(&mut a, &b, BooleanOp::Intersect);
2039 assert!(closed(&a), "intersect must be closed");
2040 assert!(
2041 (volume6(&a) - 6.0 * 0.5).abs() < 1e-3,
2042 "vol {}",
2043 volume6(&a)
2044 );
2045 }
2046
2047 #[test]
2048 fn asymmetric_overlap_needs_interior_points() {
2049 let mut a = cube();
2053 let b = box_mesh([0.5, 0.3, 0.7], [1.5, 1.3, 1.7]);
2054 run(&mut a, &b, BooleanOp::Subtract);
2055 assert!(closed(&a), "subtract must be closed");
2056 assert!(
2057 (volume6(&a) - 6.0 * 0.895).abs() < 1e-3,
2058 "vol {}",
2059 volume6(&a)
2060 );
2061 let mut u = cube();
2062 run(&mut u, &b, BooleanOp::Union);
2063 assert!(closed(&u), "union must be closed");
2064 assert!(
2065 (volume6(&u) - 6.0 * 1.895).abs() < 1e-3,
2066 "vol {}",
2067 volume6(&u)
2068 );
2069 }
2070
2071 #[test]
2072 fn stacked_cubes_share_a_face() {
2073 let mut a = cube();
2075 let b = box_mesh([0.0, 0.0, 1.0], [1.0, 1.0, 2.0]);
2076 run(&mut a, &b, BooleanOp::Union);
2077 assert!(closed(&a), "union must be closed");
2078 assert!((volume6(&a) - 12.0).abs() < 1e-3, "vol {}", volume6(&a));
2079 assert_eq!(a.triangle_count(), 20);
2080 }
2081
2082 #[test]
2083 fn nested_spheres_classify_whole() {
2084 let outer = uv_sphere(8, 12, 1.0);
2085 let inner = uv_sphere(6, 10, 0.5);
2086 let (vo, vi) = (volume6(&outer), volume6(&inner));
2087 let mut u = uv_sphere(8, 12, 1.0);
2088 run(&mut u, &inner, BooleanOp::Union);
2089 assert!(closed(&u));
2090 assert!((volume6(&u) - vo).abs() < 1e-3 * vo.abs());
2091 let mut i = uv_sphere(8, 12, 1.0);
2092 run(&mut i, &inner, BooleanOp::Intersect);
2093 assert!(closed(&i));
2094 assert!((volume6(&i) - vi).abs() < 1e-3 * vi.abs());
2095 let mut s = uv_sphere(8, 12, 1.0);
2096 run(&mut s, &inner, BooleanOp::Subtract);
2097 assert!(closed(&s));
2098 assert!((volume6(&s) - (vo - vi)).abs() < 1e-3 * vo.abs());
2099 }
2100
2101 #[test]
2102 fn overlapping_spheres() {
2103 let sphere_at = |x: f32| {
2106 let (mut positions, triangles) = crate::test_util::parts(&uv_sphere(8, 12, 1.0));
2107 for p in positions.as_chunks_mut::<3>().0 {
2108 p[0] += x;
2109 }
2110 let indices: Vec<u32> = triangles.iter().flatten().copied().collect();
2111 Mesh::from_buffers(&positions, &indices).unwrap()
2112 };
2113 let lens = 6.0 * 5.0 * std::f64::consts::PI / 12.0;
2115 let single = volume6(&sphere_at(0.0));
2116 let mut u = sphere_at(0.0);
2117 run(&mut u, &sphere_at(1.0), BooleanOp::Union);
2118 assert!(closed(&u), "union must be closed");
2119 assert!(
2120 (volume6(&u) - (2.0 * single - lens)).abs() < 0.05 * single.abs(),
2121 "union vol {}",
2122 volume6(&u)
2123 );
2124 assert!((volume6(&u) - (2.0 * single - lens)).abs() < 0.05 * single.abs());
2125 let mut i = sphere_at(0.0);
2126 run(&mut i, &sphere_at(1.0), BooleanOp::Intersect);
2127 assert!(closed(&i), "intersect must be closed");
2128 assert!((volume6(&i) - lens).abs() < 0.05 * single.abs());
2129 let mut s = sphere_at(0.0);
2130 run(&mut s, &sphere_at(1.0), BooleanOp::Subtract);
2131 assert!(closed(&s), "subtract must be closed");
2132 assert!((volume6(&s) - (single - lens)).abs() < 0.05 * single.abs());
2133 }
2134
2135 #[test]
2136 fn disjoint_cubes() {
2137 let b = box_mesh([5.0, 0.0, 0.0], [6.0, 1.0, 1.0]);
2138 let mut u = cube();
2139 run(&mut u, &b, BooleanOp::Union);
2140 assert!(closed(&u));
2141 assert_eq!(u.triangle_count(), 24);
2142 assert!((volume6(&u) - 12.0).abs() < 1e-9);
2143 let mut i = cube();
2144 let report = run(&mut i, &b, BooleanOp::Intersect);
2145 assert_eq!(i.triangle_count(), 0);
2146 assert_eq!(report.kept_triangles, 0);
2147 let mut s = cube();
2148 run(&mut s, &b, BooleanOp::Subtract);
2149 assert_eq!(s.triangle_count(), 12);
2150 assert!((volume6(&s) - 6.0).abs() < 1e-9);
2151 }
2152
2153 #[test]
2154 fn mesh_with_itself_is_rejected() {
2155 let mut a = cube();
2156 let bvh_a = Bvh::build(&a, &BuildOptions::default()).unwrap();
2157 assert_eq!(
2160 boolean_plan(&a, &bvh_a, &a, &bvh_a, &Rigid::IDENTITY, BooleanOp::Union).unwrap_err(),
2161 OpsError::InvalidInput("cannot combine a mesh with itself")
2162 );
2163 let _ = &mut a;
2164 }
2165
2166 #[test]
2167 fn rejects_bad_inputs() {
2168 let open = grid(2);
2170 let a = cube();
2171 let bvh_a = Bvh::build(&a, &BuildOptions::default()).unwrap();
2172 let bvh_o = Bvh::build(&open, &BuildOptions::default()).unwrap();
2173 assert_eq!(
2174 boolean_plan(
2175 &a,
2176 &bvh_a,
2177 &open,
2178 &bvh_o,
2179 &Rigid::IDENTITY,
2180 BooleanOp::Union
2181 )
2182 .unwrap_err(),
2183 OpsError::InvalidInput("boolean needs closed meshes")
2184 );
2185 let spike = with_tetra(&a, 0, 1, [[2.0, 0.0, 0.0], [2.0, 1.0, 0.0]]);
2188 let bvh_s = Bvh::build(&spike, &BuildOptions::default()).unwrap();
2189 assert_eq!(
2190 boolean_plan(
2191 &spike,
2192 &bvh_s,
2193 &a,
2194 &bvh_a,
2195 &Rigid::IDENTITY,
2196 BooleanOp::Union
2197 )
2198 .unwrap_err(),
2199 OpsError::InvalidInput("boolean needs manifold meshes")
2200 );
2201 let mut needle = cube();
2204 let n = edit(&mut needle, |tx| {
2205 tx.add_vertices(&[[9.0, 9.0, 9.0], [9.0, 9.0, 9.0], [9.0, 9.0, 8.0]])?;
2206 Ok(())
2207 });
2208 let _ = n;
2209 let base = needle.vertex_slots() as u32 - 3;
2210 edit(&mut needle, |tx| {
2211 tx.add_triangles(&[[base, base, base + 1]])?;
2212 Ok(())
2213 });
2214 let bvh_n = Bvh::build(&needle, &BuildOptions::default()).unwrap();
2215 assert_eq!(
2216 boolean_plan(
2217 &needle,
2218 &bvh_n,
2219 &a,
2220 &bvh_a,
2221 &Rigid::IDENTITY,
2222 BooleanOp::Union
2223 )
2224 .unwrap_err(),
2225 OpsError::InvalidInput("boolean input has degenerate triangles")
2226 );
2227 let dup = Mesh::from_buffers(
2230 &[0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0],
2231 &[0, 1, 2, 0, 1, 2],
2232 )
2233 .unwrap();
2234 let bvh_d = Bvh::build(&dup, &BuildOptions::default()).unwrap();
2235 assert_eq!(
2236 boolean_plan(&dup, &bvh_d, &a, &bvh_a, &Rigid::IDENTITY, BooleanOp::Union).unwrap_err(),
2237 OpsError::InvalidInput("boolean input has duplicate triangles")
2238 );
2239 }
2240}