1mod classify;
78mod exact;
79mod pairs;
80mod points;
81mod split;
82
83use ogeom_core::{
84 Exact, FastMap, FastSet, OgeomResult, Predicates, Sign, Tolerances, ogeom_bail, parallel,
85};
86use ogeom_math::{Aabb, Point};
87use ogeom_topo::{Model, Shape, Triangulation};
88
89use self::pairs::{Key, Side, Touch, Tri};
90use self::points::{Def, Mid, P3, Pt, by_weight, normal_approx, normal_sign, orient2d};
91use crate::{Bvh, Deflection};
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
95pub enum BoolKind {
96 Union,
98 Intersection,
100 Difference,
102}
103
104#[derive(Debug, Clone, PartialEq)]
106pub struct MeshBoolean {
107 pub mesh: Triangulation,
113 pub cut: Vec<bool>,
117 pub first_new_vertex: usize,
119 pub rounding: f64,
122}
123
124pub fn boolean(a: &Triangulation, b: &Triangulation, op: BoolKind) -> OgeomResult<MeshBoolean> {
148 let [first, second]: [OgeomResult<Operand<'_>>; 2] = parallel::map_ordered(
149 &[(a, 0, "first"), (b, 1, "second")],
150 |_, &(m, side, name)| Operand::new(m, side, name),
151 )
152 .try_into()
153 .map_err(|_| ogeom_core::ogeom_err!(Invariant, "two operands made other than two"))?;
154 let ops = [first?, second?];
155 for o in &ops {
156 o.check_embedded(0)?;
157 }
158 for o in &ops {
159 o.check_wound(o.name, None, 0)?;
160 }
161 let touches = meet_all(&ops[0], &ops[1]);
162 let mut table = Table::new(&ops);
163 let mut marks: [Vec<Mark>; 2] = [
164 vec![Mark::default(); ops[0].tris.len()],
165 vec![Mark::default(); ops[1].tris.len()],
166 ];
167 for (ta, tb, touch) in &touches {
168 table.take(&mut marks, *ta, *tb, touch);
169 }
170 table.merge(&ops);
171 let flat: [Vec<usize>; 2] = [ops[0].drops()?, ops[1].drops()?];
172 for side in 0..2 {
173 table.settle(&ops[side], &flat[side], &mut marks[side]);
174 }
175 let mut fixed: FastSet<(u32, u32)> = FastSet::default();
176 let cuts = [
177 table.cut(&ops[0], &flat[0], &marks[0], &mut fixed)?,
178 table.cut(&ops[1], &flat[1], &marks[1], &mut fixed)?,
179 ];
180 let mut labels: [Vec<(Vec<usize>, Label)>; 2] = [Vec::new(), Vec::new()];
181 for side in 0..2 {
182 let regions = regions(&ops[side], &cuts[side], &fixed)?;
183 labels[side] = table.label(
184 &ops,
185 side,
186 &flat[side],
187 &marks[side],
188 &cuts[side].pieces,
189 regions,
190 )?;
191 }
192 let meets = !touches.is_empty()
193 || labels
194 .iter()
195 .flatten()
196 .any(|(_, label)| *label != Label::Out);
197 if !meets {
198 ogeom_bail!(
199 Construction,
200 "the operands do not meet: neither touches or holds the other"
201 );
202 }
203 assemble(
204 &table,
205 &cuts,
206 &labels,
207 op,
208 a.deflection_met && b.deflection_met,
209 )
210}
211
212pub fn boolean_with_shape(
223 mesh: &Triangulation,
224 model: &Model,
225 shape: &Shape,
226 op: BoolKind,
227 chord: f64,
228 tol: Tolerances,
229) -> OgeomResult<MeshBoolean> {
230 let solid = crate::triangulate(model, shape, Deflection::with_chord(chord)?, tol)?;
231 boolean(mesh, &solid, op)
232}
233
234pub(crate) fn check_solid(
244 mesh: &Triangulation,
245 name: &'static str,
246 first: usize,
247) -> OgeomResult<Bvh> {
248 let operand = Operand::new(mesh, 0, name)?;
249 operand.check_embedded(first)?;
250 operand.check_wound(name, None, 0)?;
251 Ok(operand.bvh)
252}
253
254pub(crate) struct Solid<'a>(Operand<'a>);
256
257pub(crate) fn check_solid_kept<'a>(
263 mesh: &'a Triangulation,
264 name: &'static str,
265) -> OgeomResult<Solid<'a>> {
266 let operand = Operand::new(mesh, 0, name)?;
267 operand.check_embedded(0)?;
268 operand.check_wound(name, None, 0)?;
269 Ok(Solid(operand))
270}
271
272impl Solid<'_> {
273 pub(crate) const fn bvh(&self) -> &Bvh {
275 &self.0.bvh
276 }
277
278 pub(crate) fn check_cavity(
293 &self,
294 cavity: &Triangulation,
295 name: &'static str,
296 ) -> OgeomResult<()> {
297 let part = &self.0;
298 let inner = Operand::new(cavity, 0, name)?;
299 let base = id(part.mesh.positions.len());
300 let first = part.tris.len();
301 let shifted = |t: &Tri, side: Side| Tri {
302 side,
303 v: t.v.map(|v| v + base),
304 ..*t
305 };
306 let chunks: Vec<std::ops::Range<usize>> = (0..inner.tris.len())
307 .step_by(CHUNK)
308 .map(|s| s..(s + CHUNK).min(inner.tris.len()))
309 .collect();
310 let found = parallel::map_ordered(&chunks, |_, range| {
311 for t in range.clone() {
312 let a = shifted(&inner.tris[t], 0);
313 let (lo, hi) = Operand::bounds(&a);
314 for u in part.near(lo, hi) {
315 let b = Tri {
316 side: 1,
317 ..part.tris[u as usize]
318 };
319 if !meet_at_shared_only(&a, &b) {
320 return Some((t + first, u as usize));
321 }
322 }
323 for u in inner.near(lo, hi) {
324 if (u as usize) <= t {
325 continue;
326 }
327 if !meet_at_shared_only(&a, &shifted(&inner.tris[u as usize], 1)) {
328 return Some((t + first, u as usize + first));
329 }
330 }
331 }
332 None
333 });
334 if let Some((t, u)) = found.into_iter().flatten().next() {
335 ogeom_bail!(
336 Construction,
337 "the {name} mesh crosses itself: triangles {t} and {u} meet away from their shared corners"
338 );
339 }
340 part.check_wound(name, Some(&inner), 0)?;
341 inner.check_wound(name, Some(part), first)
342 }
343}
344
345struct Operand<'a> {
347 side: Side,
348 name: &'static str,
349 mesh: &'a Triangulation,
350 tris: Vec<Tri>,
351 bvh: Bvh,
352 across: Vec<[u32; 3]>,
355}
356
357fn coords(p: Point) -> P3 {
358 [p.x + 0.0, p.y + 0.0, p.z + 0.0]
361}
362
363fn id(i: usize) -> u32 {
364 u32::try_from(i).unwrap_or(u32::MAX)
365}
366
367impl<'a> Operand<'a> {
368 fn new(mesh: &'a Triangulation, side: Side, name: &'static str) -> OgeomResult<Self> {
369 if mesh.triangles.is_empty() {
370 ogeom_bail!(NullObject, "the {name} mesh has no triangles");
371 }
372 if u32::try_from(mesh.positions.len()).is_err() {
373 ogeom_bail!(Range, "the {name} mesh has more than four billion vertices");
374 }
375 let bvh = Bvh::build(mesh)?;
376 let mut tris = Vec::with_capacity(mesh.triangles.len());
377 let mut edges: FastMap<(u32, u32), u32> = FastMap::default();
378 edges.reserve(3 * mesh.triangles.len());
379 for (t, &v) in mesh.triangles.iter().enumerate() {
380 let p = v.map(|k| coords(mesh.positions[k as usize]));
381 let flat = (0..3).all(|k| normal_sign(&p, k).is_zero());
382 if flat {
383 ogeom_bail!(
384 Construction,
385 "triangle {t} of the {name} mesh is degenerate: its corners are collinear"
386 );
387 }
388 for k in 0..3 {
389 if edges.insert((v[k], v[(k + 1) % 3]), id(t)).is_some() {
390 ogeom_bail!(
391 Construction,
392 "the {name} mesh is not 2-manifold: edge {}-{} is run the same way twice",
393 v[k],
394 v[(k + 1) % 3]
395 );
396 }
397 }
398 tris.push(Tri {
399 side,
400 id: id(t),
401 v,
402 p,
403 });
404 }
405 let chunks: Vec<std::ops::Range<usize>> = (0..tris.len())
406 .step_by(CHUNK)
407 .map(|s| s..(s + CHUNK).min(tris.len()))
408 .collect();
409 let across: Vec<Vec<Option<[u32; 3]>>> = parallel::map_ordered(&chunks, |_, range| {
410 range
411 .clone()
412 .map(|t| {
413 let v: [u32; 3] = tris[t].v;
414 let mut out = [0; 3];
415 for k in 0..3 {
416 out[k] = *edges.get(&(v[(k + 1) % 3], v[k]))?;
417 }
418 Some(out)
419 })
420 .collect()
421 });
422 let across: Vec<[u32; 3]> = across
423 .into_iter()
424 .flatten()
425 .collect::<Option<_>>()
426 .map_or_else(
427 || {
428 let (lo, hi) = edges
429 .keys()
430 .filter(|&&(u, w)| !edges.contains_key(&(w, u)))
431 .map(|&(u, w)| (u.min(w), u.max(w)))
432 .min()
433 .unwrap_or((0, 0));
434 Err(ogeom_core::ogeom_err!(
435 Construction,
436 "the {name} mesh is open: edge {lo}-{hi} has one triangle"
437 ))
438 },
439 Ok,
440 )?;
441 drop(edges);
442 let operand = Self {
443 side,
444 name,
445 mesh,
446 tris,
447 bvh,
448 across,
449 };
450 operand.check_fans()?;
451 Ok(operand)
452 }
453
454 fn check_fans(&self) -> OgeomResult<()> {
456 let mut corners = vec![0_u32; self.mesh.positions.len()];
457 let mut first = vec![u32::MAX; self.mesh.positions.len()];
458 for (t, tri) in self.tris.iter().enumerate() {
459 for &v in &tri.v {
460 corners[v as usize] += 1;
461 if first[v as usize] == u32::MAX {
462 first[v as usize] = id(t);
463 }
464 }
465 }
466 for (v, &start) in first.iter().enumerate() {
467 if start == u32::MAX {
468 continue;
469 }
470 let v = id(v);
471 let mut at = start;
472 let mut count = 0;
473 loop {
474 count += 1;
475 let t = self.tris[at as usize].v;
476 let k = t.iter().position(|&x| x == v).unwrap_or(0);
477 let next = self.across[at as usize][(k + 2) % 3];
479 if next == start || count > corners[v as usize] {
480 break;
481 }
482 at = next;
483 }
484 if count != corners[v as usize] {
485 ogeom_bail!(
486 Construction,
487 "the {} mesh is not 2-manifold at vertex {v}: its triangles form more than one fan",
488 self.name
489 );
490 }
491 }
492 Ok(())
493 }
494
495 fn bounds(t: &Tri) -> (P3, P3) {
496 let mut lo = t.p[0];
497 let mut hi = t.p[0];
498 for q in &t.p[1..] {
499 for k in 0..3 {
500 lo[k] = lo[k].min(q[k]);
501 hi[k] = hi[k].max(q[k]);
502 }
503 }
504 (lo, hi)
505 }
506
507 fn near(&self, lo: P3, hi: P3) -> Vec<u32> {
509 self.bvh.triangles_where(|b: &Aabb| {
510 let (Some(bl), Some(bh)) = (b.low(), b.high()) else {
511 return false;
512 };
513 let (bl, bh) = (coords(bl), coords(bh));
514 (0..3).all(|k| bl[k] <= hi[k] && lo[k] <= bh[k])
515 })
516 }
517
518 fn check_embedded(&self, first: usize) -> OgeomResult<()> {
522 let chunks: Vec<std::ops::Range<usize>> = (first..self.tris.len())
523 .step_by(CHUNK)
524 .map(|s| s..(s + CHUNK).min(self.tris.len()))
525 .collect();
526 let found = parallel::map_ordered(&chunks, |_, range| {
527 for t in range.clone() {
528 let a = &self.tris[t];
529 let (lo, hi) = Self::bounds(a);
530 for u in self.near(lo, hi) {
531 let u_at = u as usize;
532 if u_at == t || (u_at >= first && u_at < t) {
533 continue;
534 }
535 let b = Tri {
536 side: 1 - self.side,
537 ..self.tris[u as usize]
538 };
539 if !meet_at_shared_only(a, &b) {
540 return Some((t, u));
541 }
542 }
543 }
544 None
545 });
546 if let Some((t, u)) = found.into_iter().flatten().next() {
547 ogeom_bail!(
548 Construction,
549 "the {} mesh crosses itself: triangles {t} and {u} meet away from their shared corners",
550 self.name
551 );
552 }
553 Ok(())
554 }
555
556 fn check_wound(&self, name: &str, also: Option<&Operand<'_>>, base: usize) -> OgeomResult<()> {
563 let mut parent: Vec<u32> = (0..id(self.tris.len())).collect();
564 for (t, near) in self.across.iter().enumerate().take(self.tris.len()) {
565 for &u in near {
566 union(&mut parent, id(t), u);
567 }
568 }
569 let mut seen = FastSet::default();
570 for t in 0..self.tris.len() {
571 if !seen.insert(find(&mut parent, id(t))) {
572 continue;
573 }
574 let tri = &self.tris[t];
575 let axis = by_weight(normal_approx(&tri.p))
576 .into_iter()
577 .find(|&k| !normal_sign(&tri.p, k).is_zero())
578 .unwrap_or(0);
579 let corner = tri.p.map(|q| Pt::new(Def::Vertex(q)));
580 let mid = Mid([&corner[0], &corner[1], &corner[2]]);
581 let out = normal_sign(&tri.p, axis);
582 let mut w = classify::winding(&self.tris, &self.bvh, &mid, axis, out, Some(id(t)))?;
583 if let Some(o) = also {
584 w += classify::winding(&o.tris, &o.bvh, &mid, axis, out, None)?;
585 }
586 if w != 0 {
587 ogeom_bail!(
588 Construction,
589 "the {name} mesh is not wound outward: triangle {} faces into the solid",
590 t + base
591 );
592 }
593 }
594 Ok(())
595 }
596
597 fn drops(&self) -> OgeomResult<Vec<usize>> {
600 self.tris
601 .iter()
602 .map(|t| {
603 by_weight(normal_approx(&t.p))
604 .into_iter()
605 .find(|&k| !normal_sign(&t.p, k).is_zero())
606 .ok_or_else(|| ogeom_core::ogeom_err!(Invariant, "a triangle has no normal"))
607 })
608 .collect()
609 }
610}
611
612fn meet_at_shared_only(a: &Tri, b: &Tri) -> bool {
623 let off = |x: &Tri, y: &Tri| {
626 let mut sign = None;
627 for k in 0..3 {
628 if x.v.contains(&y.v[k]) {
629 continue;
630 }
631 let s = Exact::orient3d(x.p[0], x.p[1], x.p[2], y.p[k]);
632 if s.is_zero() || sign.is_some_and(|t| t != s) {
633 return false;
634 }
635 sign = Some(s);
636 }
637 true
638 };
639 let settled = match (0..3).filter(|&k| b.v.contains(&a.v[k])).count() {
640 2 => off(a, b),
641 0 | 1 => off(a, b) || off(b, a),
642 _ => false,
643 };
644 settled || meet_at_shared_only_built(a, b)
645}
646
647fn meet_at_shared_only_built(a: &Tri, b: &Tri) -> bool {
649 let shared: Vec<u32> = a.v.iter().copied().filter(|v| b.v.contains(v)).collect();
650 shared.len() < 3
651 && (apart_but_shared(a, b, &shared)
652 || pairs::meet(a, b).is_none_or(|touch| {
653 touch
654 .points
655 .iter()
656 .all(|(key, _)| matches!(key, Key::Vertex(_, v) if shared.contains(v)))
657 }))
658}
659
660pub(crate) fn apart_but_at_shared(a: ([u32; 3], [Point; 3]), b: ([u32; 3], [Point; 3])) -> bool {
664 let tri = |side: Side, (v, p): ([u32; 3], [Point; 3])| Tri {
665 side,
666 id: u32::from(side),
667 v,
668 p: p.map(coords),
669 };
670 meet_at_shared_only(&tri(0, a), &tri(1, b))
671}
672
673fn apart_but_shared(a: &Tri, b: &Tri, shared: &[u32]) -> bool {
681 let others = |t: &Tri| -> Vec<P3> {
682 (0..3)
683 .filter(|&k| !shared.contains(&t.v[k]))
684 .map(|k| t.p[k])
685 .collect()
686 };
687 let against = |t: &Tri, q: P3| Exact::orient3d(t.p[0], t.p[1], t.p[2], q);
688 let apart = match shared.len() {
689 2 => others(b).first().is_some_and(|&w| !against(a, w).is_zero()),
690 1 => [(a, b), (b, a)].iter().any(|(x, y)| {
691 let o = others(y);
692 let (s, t) = (against(x, o[0]), against(x, o[1]));
693 !s.is_zero() && s == t
694 }),
695 _ => false,
696 };
697 apart || (others(b).iter().all(|&q| against(a, q).is_zero()) && flat_apart(a, b, shared))
698}
699
700fn flat_apart(a: &Tri, b: &Tri, shared: &[u32]) -> bool {
704 let Some(drop) = by_weight(normal_approx(&a.p))
705 .into_iter()
706 .find(|&k| !normal_sign(&a.p, k).is_zero())
707 else {
708 return false;
709 };
710 let (i, j) = points::axes(drop);
711 let flat = |q: P3| [q[i], q[j]];
712 let others = |t: &Tri| -> Vec<[f64; 2]> {
713 (0..3)
714 .filter(|&k| !shared.contains(&t.v[k]))
715 .map(|k| flat(t.p[k]))
716 .collect()
717 };
718 let at = |t: &Tri, v: u32| t.v.iter().position(|&x| x == v).map(|k| flat(t.p[k]));
719 let splits = |p: [f64; 2], q: [f64; 2], mine: &[[f64; 2]], theirs: &[[f64; 2]]| {
720 let side = |r: [f64; 2]| Exact::orient2d(p, q, r);
721 let Some(&first) = mine.first() else {
722 return false;
723 };
724 let s = side(first);
725 !s.is_zero()
726 && mine.iter().all(|&r| side(r) == s)
727 && theirs.iter().all(|&r| side(r) == s.reversed())
728 };
729 match shared {
730 [u, w] => match (at(a, *u), at(a, *w)) {
731 (Some(p), Some(q)) => splits(p, q, &others(a), &others(b)),
732 _ => false,
733 },
734 [v] => [(a, b), (b, a)].iter().any(|(x, y)| {
735 let Some(p) = at(x, *v) else {
736 return false;
737 };
738 let mine = others(x);
739 (0..2).any(|k| splits(p, mine[k], &mine[1 - k..2 - k], &others(y)))
740 }),
741 _ => false,
742 }
743}
744
745const CHUNK: usize = 256;
747
748fn find(parent: &mut [u32], mut x: u32) -> u32 {
749 while parent[x as usize] != x {
750 let up = parent[parent[x as usize] as usize];
751 parent[x as usize] = up;
752 x = up;
753 }
754 x
755}
756
757fn union(parent: &mut [u32], a: u32, b: u32) {
759 let (ra, rb) = (find(parent, a), find(parent, b));
760 if ra < rb {
761 parent[rb as usize] = ra;
762 } else if rb < ra {
763 parent[ra as usize] = rb;
764 }
765}
766
767fn meet_all(a: &Operand<'_>, b: &Operand<'_>) -> Vec<(u32, u32, Touch)> {
770 let chunks: Vec<std::ops::Range<usize>> = (0..a.tris.len())
771 .step_by(CHUNK)
772 .map(|s| s..(s + CHUNK).min(a.tris.len()))
773 .collect();
774 parallel::map_ordered(&chunks, |_, range| {
775 let mut out = Vec::new();
776 for t in range.clone() {
777 let ta = &a.tris[t];
778 let (lo, hi) = Operand::bounds(ta);
779 for u in b.near(lo, hi) {
780 if let Some(touch) = pairs::meet(ta, &b.tris[u as usize]) {
781 out.push((id(t), u, touch));
782 }
783 }
784 }
785 out
786 })
787 .into_iter()
788 .flatten()
789 .collect()
790}
791
792#[derive(Debug, Clone, Default)]
794struct Mark {
795 points: Vec<u32>,
797 segments: Vec<[u32; 2]>,
798 coplanar: Vec<u32>,
800}
801
802struct Table {
805 pts: Vec<Pt>,
806 names: FastMap<Key, u32>,
807 keys: Vec<Key>,
809 counts: [usize; 2],
811 rep: Vec<u32>,
814}
815
816impl Table {
817 fn new(ops: &[Operand<'_>; 2]) -> Self {
818 let mut pts = Vec::with_capacity(ops[0].mesh.positions.len() + ops[1].mesh.positions.len());
819 for o in ops {
820 pts.extend(
821 o.mesh
822 .positions
823 .iter()
824 .map(|&p| Pt::new(Def::Vertex(coords(p)))),
825 );
826 }
827 Self {
828 pts,
829 names: FastMap::default(),
830 keys: Vec::new(),
831 counts: [ops[0].mesh.positions.len(), ops[1].mesh.positions.len()],
832 rep: Vec::new(),
833 }
834 }
835
836 fn index(&mut self, key: Key, pt: &Pt) -> u32 {
837 match key {
838 Key::Vertex(0, v) => v,
839 Key::Vertex(_, v) => id(self.counts[0]) + v,
840 _ => {
841 if let Some(&at) = self.names.get(&key) {
842 return at;
843 }
844 let at = id(self.pts.len());
845 self.pts.push(Pt::new(pt.def));
846 self.names.insert(key, at);
847 self.keys.push(key);
848 at
849 }
850 }
851 }
852
853 fn take(&mut self, marks: &mut [Vec<Mark>; 2], ta: u32, tb: u32, touch: &Touch) {
854 let ids: Vec<u32> = touch
855 .points
856 .iter()
857 .map(|(key, pt)| self.index(*key, pt))
858 .collect();
859 for (side, t, other) in [(0, ta, tb), (1, tb, ta)] {
860 let m = &mut marks[side][t as usize];
861 m.points.extend(&ids);
862 m.segments
863 .extend(touch.segments.iter().map(|s| [ids[s[0]], ids[s[1]]]));
864 if touch.coplanar {
865 m.coplanar.push(other);
866 }
867 }
868 }
869
870 fn merge(&mut self, ops: &[Operand<'_>; 2]) {
872 let n = self.pts.len();
873 let mut parent: Vec<u32> = (0..id(n)).collect();
874 let explicit = self.counts[0] + self.counts[1];
875 let mut used = vec![false; explicit];
876 for (side, o) in ops.iter().enumerate() {
877 let base = if side == 0 { 0 } else { self.counts[0] };
878 for t in &o.tris {
879 for &v in &t.v {
880 used[base + v as usize] = true;
881 }
882 }
883 }
884 let bits = |p: &P3| p.map(f64::to_bits);
886 let mut at: FastMap<[u64; 3], u32> = FastMap::default();
887 let mut sorted: Vec<(P3, u32)> = Vec::new();
888 for (i, _) in used.iter().enumerate().filter(|(_, u)| **u) {
889 let Def::Vertex(p) = self.pts[i].def else {
890 continue;
891 };
892 match at.get(&bits(&p)) {
893 Some(&j) => union(&mut parent, j, id(i)),
894 None => {
895 at.insert(bits(&p), id(i));
896 sorted.push((p, id(i)));
897 }
898 }
899 }
900 sorted.sort_by(|x, y| {
901 x.0[0]
902 .total_cmp(&y.0[0])
903 .then(x.0[1].total_cmp(&y.0[1]))
904 .then(x.0[2].total_cmp(&y.0[2]))
905 .then(x.1.cmp(&y.1))
906 });
907 type Bounds = Option<[(f64, f64); 3]>;
909 let made: Vec<(u32, Bounds)> = (explicit..n)
910 .map(|i| {
911 let b = self.pts[i].bounds().map(|b| b.map(|v| (v.lo, v.hi)));
912 (id(i), b)
913 })
914 .collect();
915 let pairs_found: Vec<Vec<(u32, u32)>> = parallel::map_ordered(&made, |_, &(i, b)| {
916 let mut out = Vec::new();
917 let pt = &self.pts[i as usize];
918 match b {
919 Some(b) => {
920 let start = sorted.partition_point(|e| e.0[0] < b[0].0);
921 for e in &sorted[start..] {
922 if e.0[0] > b[0].1 {
923 break;
924 }
925 let inside = (1..3).all(|k| b[k].0 <= e.0[k] && e.0[k] <= b[k].1);
926 if inside && points::same(pt, &self.pts[e.1 as usize]) {
927 out.push((e.1, i));
928 }
929 }
930 }
931 None => {
932 for e in &sorted {
933 if points::same(pt, &self.pts[e.1 as usize]) {
934 out.push((e.1, i));
935 }
936 }
937 }
938 }
939 out
940 });
941 for (x, y) in pairs_found.into_iter().flatten() {
942 union(&mut parent, x, y);
943 }
944 let mut boxed: Vec<(u32, [(f64, f64); 3])> = Vec::new();
946 let mut loose: Vec<u32> = Vec::new();
947 for &(i, b) in &made {
948 match b {
949 Some(b) => boxed.push((i, b)),
950 None => loose.push(i),
951 }
952 }
953 boxed.sort_by(|x, y| x.1[0].0.total_cmp(&y.1[0].0).then(x.0.cmp(&y.0)));
954 let mut pairs_found = Vec::new();
955 for (k, (i, b)) in boxed.iter().enumerate() {
956 for (j, c) in &boxed[k + 1..] {
957 if c[0].0 > b[0].1 {
958 break;
959 }
960 let overlap = (1..3).all(|d| c[d].0 <= b[d].1 && b[d].0 <= c[d].1);
961 if overlap && points::same(&self.pts[*i as usize], &self.pts[*j as usize]) {
962 pairs_found.push((*i, *j));
963 }
964 }
965 }
966 for &i in &loose {
967 for &(j, _) in &made {
968 if j != i && points::same(&self.pts[i as usize], &self.pts[j as usize]) {
969 pairs_found.push((i, j));
970 }
971 }
972 }
973 for (x, y) in pairs_found {
974 union(&mut parent, x, y);
975 }
976 self.rep = (0..id(n)).map(|i| find(&mut parent, i)).collect();
977 }
978
979 fn settle(&self, o: &Operand<'_>, flat: &[usize], marks: &mut [Mark]) {
983 let base = if o.side == 0 { 0 } else { self.counts[0] };
984 let corners = |t: &Tri| t.v.map(|v| self.rep[base + v as usize]);
985 type Settled = (Vec<u32>, Vec<[u32; 2]>, Vec<(u32, u32)>);
986 let which: Vec<usize> = (0..marks.len())
987 .filter(|&t| !(marks[t].points.is_empty() && marks[t].segments.is_empty()))
988 .collect();
989 let settled: Vec<Settled> = parallel::map_ordered(&which, |_, &t| {
990 let m = &marks[t];
991 let tri = &o.tris[t];
992 let c = corners(tri);
993 let mut raw: Vec<(u32, u32)> = m
994 .points
995 .iter()
996 .map(|&x| (self.rep[x as usize], x))
997 .filter(|(r, _)| !c.contains(r))
998 .collect();
999 raw.sort_unstable();
1000 raw.dedup();
1001 let mut points = Vec::new();
1002 let mut shares = Vec::new();
1003 for group in raw.chunk_by(|x, y| x.0 == y.0) {
1004 let r = group[0].0;
1005 points.push(r);
1006 let on = group
1007 .iter()
1008 .find_map(|&(_, x)| self.edge_of(o.side, tri, x))
1009 .or_else(|| {
1010 (0..3).find(|&k| {
1011 orient2d(
1012 flat[t],
1013 &self.pts[c[k] as usize],
1014 &self.pts[c[(k + 1) % 3] as usize],
1015 &self.pts[r as usize],
1016 )
1017 .is_zero()
1018 })
1019 });
1020 if let Some(k) = on {
1021 shares.push((o.across[t][k], r));
1022 }
1023 }
1024 let mut segments: Vec<[u32; 2]> = m
1025 .segments
1026 .iter()
1027 .map(|s| {
1028 let (a, b) = (self.rep[s[0] as usize], self.rep[s[1] as usize]);
1029 if a < b { [a, b] } else { [b, a] }
1030 })
1031 .filter(|s| s[0] != s[1])
1032 .collect();
1033 segments.sort_unstable();
1034 segments.dedup();
1035 (points, segments, shares)
1036 });
1037 let mut shares = Vec::new();
1038 for (&t, (points, segments, share)) in which.iter().zip(settled) {
1039 let m = &mut marks[t];
1040 m.points = points;
1041 m.segments = segments;
1042 m.coplanar.sort_unstable();
1043 m.coplanar.dedup();
1044 shares.extend(share);
1045 }
1046 for (u, x) in shares {
1047 let m = &mut marks[u as usize];
1048 if let Err(at) = m.points.binary_search(&x) {
1049 m.points.insert(at, x);
1050 }
1051 }
1052 }
1053
1054 fn edge_of(&self, side: Side, tri: &Tri, x: u32) -> Option<usize> {
1057 let made = (x as usize).checked_sub(self.counts[0] + self.counts[1])?;
1058 let edge = match self.keys[made] {
1059 Key::Lpi { side: s, edge, .. } if s == side => edge,
1060 Key::Ssi { a, b } => {
1061 if side == 0 {
1062 a
1063 } else {
1064 b
1065 }
1066 }
1067 _ => return None,
1068 };
1069 (0..3).find(|&k| {
1070 let (u, w) = (tri.v[k], tri.v[(k + 1) % 3]);
1071 [u.min(w), u.max(w)] == edge
1072 })
1073 }
1074
1075 fn cut(
1079 &self,
1080 o: &Operand<'_>,
1081 flat: &[usize],
1082 marks: &[Mark],
1083 fixed: &mut FastSet<(u32, u32)>,
1084 ) -> OgeomResult<Cut> {
1085 let marked: Vec<bool> = marks
1086 .iter()
1087 .map(|m| !(m.points.is_empty() && m.segments.is_empty()))
1088 .collect();
1089 let which: Vec<usize> = (0..marks.len()).filter(|&t| marked[t]).collect();
1090 let cuts = parallel::map_ordered(&which, |_, &t| {
1091 split::split(
1092 &self.pts,
1093 flat[t],
1094 self.corners(o, t),
1095 &marks[t].points,
1096 &marks[t].segments,
1097 )
1098 });
1099 let mut done = which.iter().zip(cuts).peekable();
1100 let mut pieces = Vec::with_capacity(o.tris.len());
1101 let mut start = Vec::with_capacity(o.tris.len() + 1);
1102 for t in 0..o.tris.len() {
1103 start.push(pieces.len());
1104 if done.peek().is_some_and(|(u, _)| **u == t) {
1105 let Some((_, c)) = done.next() else {
1106 continue;
1107 };
1108 let c = c?;
1109 fixed.extend(c.fixed.iter().map(|&[a, b]| (a, b)));
1110 let cut = c.triangles.len() > 1;
1111 pieces.extend(c.triangles.into_iter().map(|v| Piece {
1112 v,
1113 parent: id(t),
1114 cut,
1115 }));
1116 } else {
1117 pieces.push(Piece {
1118 v: self.corners(o, t),
1119 parent: id(t),
1120 cut: false,
1121 });
1122 }
1123 }
1124 start.push(pieces.len());
1125 Ok(Cut {
1126 pieces,
1127 start,
1128 marked,
1129 })
1130 }
1131
1132 fn corners(&self, o: &Operand<'_>, t: usize) -> [u32; 3] {
1134 let base = if o.side == 0 { 0 } else { self.counts[0] };
1135 o.tris[t].v.map(|v| self.rep[base + v as usize])
1136 }
1137
1138 fn label(
1141 &self,
1142 ops: &[Operand<'_>; 2],
1143 side: usize,
1144 flat: &[usize],
1145 marks: &[Mark],
1146 pieces: &[Piece],
1147 regions: Vec<Vec<usize>>,
1148 ) -> OgeomResult<Vec<(Vec<usize>, Label)>> {
1149 let (own, other) = (&ops[side], &ops[1 - side]);
1150 let labels = parallel::map_ordered(®ions, |_, region| -> OgeomResult<Label> {
1151 let piece = &pieces[region[0]];
1152 let t = piece.parent as usize;
1153 let drop = flat[t];
1154 let corner = piece.v.map(|v| &self.pts[v as usize]);
1155 let mid = Mid(corner);
1156 let parent = &own.tris[t];
1157 for &u in &marks[t].coplanar {
1158 let q = &other.tris[u as usize];
1159 let qc = q.p.map(|p| Pt::new(Def::Vertex(p)));
1160 let facing = normal_sign(&q.p, drop);
1161 let o = [0, 1, 2].map(|k| orient2d(drop, &qc[k], &qc[(k + 1) % 3], &mid));
1162 if o.contains(&facing.reversed()) {
1166 continue;
1167 }
1168 if o.contains(&Sign::Zero) {
1169 ogeom_bail!(Invariant, "a piece straddles a coplanar triangle's edge");
1170 }
1171 return Ok(if facing == normal_sign(&parent.p, drop) {
1172 Label::OnSame
1173 } else {
1174 Label::OnOpposite
1175 });
1176 }
1177 let w = classify::winding(&other.tris, &other.bvh, &mid, drop, Sign::Positive, None)?;
1178 match w {
1179 0 => Ok(Label::Out),
1180 1 => Ok(Label::In),
1181 _ => ogeom_bail!(
1182 Invariant,
1183 "the {} mesh winds {w} times about a point",
1184 other.name
1185 ),
1186 }
1187 });
1188 regions
1189 .into_iter()
1190 .zip(labels)
1191 .map(|(r, l)| Ok((r, l?)))
1192 .collect()
1193 }
1194}
1195
1196#[derive(Debug, Clone, Copy)]
1198struct Piece {
1199 v: [u32; 3],
1200 parent: u32,
1201 cut: bool,
1202}
1203
1204#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1206enum Label {
1207 In,
1208 Out,
1209 OnSame,
1211 OnOpposite,
1213}
1214
1215struct Cut {
1217 pieces: Vec<Piece>,
1218 start: Vec<usize>,
1220 marked: Vec<bool>,
1222}
1223
1224fn regions(
1234 o: &Operand<'_>,
1235 cut: &Cut,
1236 fixed: &FastSet<(u32, u32)>,
1237) -> OgeomResult<Vec<Vec<usize>>> {
1238 let pieces = &cut.pieces;
1239 let near_cut = |t: usize| o.across[t].iter().any(|&u| cut.marked[u as usize]);
1240 let mut edges: FastMap<(u32, u32), u32> = FastMap::default();
1241 for t in 0..o.tris.len() {
1242 if !(cut.marked[t] || near_cut(t)) {
1243 continue;
1244 }
1245 for (i, p) in pieces
1246 .iter()
1247 .enumerate()
1248 .take(cut.start[t + 1])
1249 .skip(cut.start[t])
1250 {
1251 for k in 0..3 {
1252 if edges.insert((p.v[k], p.v[(k + 1) % 3]), id(i)).is_some() {
1253 ogeom_bail!(Invariant, "the cut {} mesh runs an edge twice", o.name);
1254 }
1255 }
1256 }
1257 }
1258 let mut parent: Vec<u32> = (0..id(pieces.len())).collect();
1259 for t in 0..o.tris.len() {
1260 for (i, p) in pieces
1261 .iter()
1262 .enumerate()
1263 .take(cut.start[t + 1])
1264 .skip(cut.start[t])
1265 {
1266 for k in 0..3 {
1267 if !cut.marked[t] {
1268 let u = o.across[t][k];
1269 if !cut.marked[u as usize] {
1270 union(&mut parent, id(i), id(cut.start[u as usize]));
1271 continue;
1272 }
1273 }
1274 let (a, b) = (p.v[k], p.v[(k + 1) % 3]);
1275 let Some(&j) = edges.get(&(b, a)) else {
1276 ogeom_bail!(Invariant, "the cut {} mesh is open", o.name);
1277 };
1278 if !fixed.contains(&(a.min(b), a.max(b))) {
1279 union(&mut parent, id(i), j);
1280 }
1281 }
1282 }
1283 }
1284 let mut slot = vec![usize::MAX; pieces.len()];
1285 let mut out: Vec<Vec<usize>> = Vec::new();
1286 for i in 0..pieces.len() {
1287 let r = find(&mut parent, id(i)) as usize;
1288 if slot[r] == usize::MAX {
1289 slot[r] = out.len();
1290 out.push(Vec::new());
1291 }
1292 out[slot[r]].push(i);
1293 }
1294 Ok(out)
1295}
1296
1297const fn keep(op: BoolKind, side: usize, label: Label) -> Option<bool> {
1300 let kept = matches!(
1301 (op, side, label),
1302 (BoolKind::Union, _, Label::Out)
1303 | (BoolKind::Union | BoolKind::Intersection, 0, Label::OnSame)
1304 | (BoolKind::Intersection, _, Label::In)
1305 | (BoolKind::Difference, 0, Label::Out | Label::OnOpposite)
1306 | (BoolKind::Difference, 1, Label::In)
1307 );
1308 if kept {
1309 Some(matches!(op, BoolKind::Difference) && side == 1)
1310 } else {
1311 None
1312 }
1313}
1314
1315fn assemble(
1316 table: &Table,
1317 cuts: &[Cut; 2],
1318 labels: &[Vec<(Vec<usize>, Label)>; 2],
1319 op: BoolKind,
1320 deflection_met: bool,
1321) -> OgeomResult<MeshBoolean> {
1322 let mut kept: Vec<([u32; 3], bool, (usize, usize))> = Vec::new();
1323 for side in 0..2 {
1324 for (region, label) in &labels[side] {
1325 let Some(flip) = keep(op, side, *label) else {
1326 continue;
1327 };
1328 for &i in region {
1329 let p = &cuts[side].pieces[i];
1330 let v = if flip { [p.v[0], p.v[2], p.v[1]] } else { p.v };
1331 kept.push((v, p.cut, (side, i)));
1332 }
1333 }
1334 }
1335 kept.sort_unstable_by_key(|k| k.2);
1336 let mut focus = vec![false; table.pts.len()];
1340 for cut in cuts {
1341 for p in &cut.pieces {
1342 if cut.marked[p.parent as usize] {
1343 for &v in &p.v {
1344 focus[v as usize] = true;
1345 }
1346 }
1347 }
1348 }
1349 for (i, &r) in table.rep.iter().enumerate() {
1350 if r as usize != i {
1351 focus[r as usize] = true;
1352 }
1353 }
1354 let mut slot = vec![u32::MAX; table.pts.len()];
1355 for k in &kept {
1356 for &v in &k.0 {
1357 slot[v as usize] = 0;
1358 }
1359 }
1360 let explicit = table.counts[0] + table.counts[1];
1361 let mut mesh = Triangulation::new();
1362 let mut rounding = 0.0_f64;
1363 let mut first_new_vertex = 0;
1364 let mut near = Vec::new();
1365 for (v, s) in slot.iter_mut().enumerate() {
1366 if *s == u32::MAX {
1367 continue;
1368 }
1369 *s = id(mesh.positions.len());
1370 if v < explicit {
1371 first_new_vertex += 1;
1372 }
1373 near.push(focus[v]);
1374 let (p, err) = table.pts[v].rounded();
1375 rounding = rounding.max(err);
1376 mesh.positions.push(Point::new(p[0], p[1], p[2]));
1377 }
1378 mesh.triangles = kept.iter().map(|k| k.0.map(|v| slot[v as usize])).collect();
1379 mesh.deflection_met = deflection_met;
1380 check_result(&mesh, &near)?;
1381 Ok(MeshBoolean {
1382 cut: kept.iter().map(|k| k.1).collect(),
1383 mesh,
1384 first_new_vertex,
1385 rounding,
1386 })
1387}
1388
1389fn check_result(mesh: &Triangulation, focus: &[bool]) -> OgeomResult<()> {
1392 let at = |v: u32| focus[v as usize];
1393 let near: Vec<usize> = (0..mesh.triangles.len())
1394 .filter(|&t| mesh.triangles[t].iter().any(|&v| at(v)))
1395 .collect();
1396 let mut edges: FastMap<(u32, u32), u32> = FastMap::default();
1397 for &t in &near {
1398 let v = mesh.triangles[t];
1399 for k in 0..3 {
1400 if edges.insert((v[k], v[(k + 1) % 3]), id(t)).is_some() {
1401 ogeom_bail!(
1402 Construction,
1403 "the result would not be 2-manifold: the operands touch along an edge without overlapping there"
1404 );
1405 }
1406 }
1407 }
1408 if edges
1409 .keys()
1410 .any(|&(a, b)| (at(a) || at(b)) && !edges.contains_key(&(b, a)))
1411 {
1412 ogeom_bail!(Invariant, "the result is open");
1413 }
1414 let mut corners = vec![0_u32; mesh.positions.len()];
1415 for &t in &near {
1416 for &v in &mesh.triangles[t] {
1417 corners[v as usize] += 1;
1418 }
1419 }
1420 let mut seen = vec![false; mesh.positions.len()];
1421 for &t in &near {
1422 for &v in &mesh.triangles[t] {
1423 if !at(v) || seen[v as usize] {
1424 continue;
1425 }
1426 seen[v as usize] = true;
1427 let (start, mut here, mut count) = (id(t), id(t), 0);
1428 loop {
1429 count += 1;
1430 let w = mesh.triangles[here as usize];
1431 let k = w.iter().position(|&x| x == v).unwrap_or(0);
1432 match edges.get(&(v, w[(k + 2) % 3])) {
1433 Some(&next) if next != start && count <= corners[v as usize] => here = next,
1434 _ => break,
1435 }
1436 }
1437 if count != corners[v as usize] {
1438 ogeom_bail!(
1439 Construction,
1440 "the result would not be 2-manifold: the operands touch at a point without overlapping there"
1441 );
1442 }
1443 }
1444 }
1445 Ok(())
1446}
1447
1448#[cfg(test)]
1449mod tests;