use super::coplanar::coplanar_clip;
use super::interner::{Interner, Vid};
use super::predicates::{cmp_lex, orient2d_any, orient3d};
use super::rational::point_of;
use super::retriangulate::{projection_axis, triangulate, Constraint, RetriInput};
use super::tritri::{tri_tri_intersection, TriTri};
use super::{DropAxis, ImplicitPoint, Sign, Tpi};
use num_traits::ToPrimitive;
use std::cmp::Ordering;
pub type Tri = [[f64; 3]; 3];
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum BoolOp {
Difference,
Union,
Intersection,
}
pub struct Arrangement {
pub interner: Interner,
pub tris_a: Vec<[Vid; 3]>,
pub tris_b: Vec<[Vid; 3]>,
pub unrecovered: usize,
pub coplanar_a: Vec<bool>,
pub coplanar_b: Vec<bool>,
}
struct RawSeg {
a: ImplicitPoint,
b: ImplicitPoint,
cutter: Tri,
}
#[inline]
fn tri_plane(t: &Tri) -> [[f64; 3]; 3] {
*t
}
fn segments_cross(a1: &ImplicitPoint, b1: &ImplicitPoint, a2: &ImplicitPoint, b2: &ImplicitPoint, axis: super::DropAxis) -> bool {
let s1 = orient2d_any(a1, b1, a2, axis);
let s2 = orient2d_any(a1, b1, b2, axis);
let s3 = orient2d_any(a2, b2, a1, axis);
let s4 = orient2d_any(a2, b2, b1, axis);
s1 != Sign::Zero && s2 != Sign::Zero && s1 != s2 && s3 != Sign::Zero && s4 != Sign::Zero && s3 != s4
}
fn split_crossings(t: &Tri, raws: &[RawSeg]) -> Vec<Constraint> {
let axis = match projection_axis(t) {
Some((a, _)) => a,
None => return Vec::new(),
};
let n = raws.len();
let mut splits: Vec<Vec<ImplicitPoint>> = vec![Vec::new(); n];
for k in 0..n {
for l in (k + 1)..n {
if segments_cross(&raws[k].a, &raws[k].b, &raws[l].a, &raws[l].b, axis) {
let x = ImplicitPoint::Tpi(Tpi {
planes: [tri_plane(t), tri_plane(&raws[k].cutter), tri_plane(&raws[l].cutter)],
});
splits[k].push(x.clone());
splits[l].push(x);
}
}
}
let mut out = Vec::new();
for k in 0..n {
let mut chain = vec![raws[k].a.clone()];
chain.append(&mut splits[k]);
chain.push(raws[k].b.clone());
chain.sort_by(|p, q| match cmp_lex(p, q) {
Sign::Negative => Ordering::Less,
Sign::Positive => Ordering::Greater,
Sign::Zero => Ordering::Equal,
});
chain.dedup_by(|p, q| cmp_lex(p, q) == Sign::Zero);
for w in chain.windows(2) {
out.push(Constraint { a: w[0].clone(), b: w[1].clone() });
}
}
out
}
pub fn arrange(a: &[Tri], b: &[Tri]) -> Arrangement {
let mut raw_a: Vec<Vec<RawSeg>> = (0..a.len()).map(|_| Vec::new()).collect();
let mut raw_b: Vec<Vec<RawSeg>> = (0..b.len()).map(|_| Vec::new()).collect();
let mut cop_a: Vec<Vec<Constraint>> = (0..a.len()).map(|_| Vec::new()).collect();
let mut cop_b: Vec<Vec<Constraint>> = (0..b.len()).map(|_| Vec::new()).collect();
let mut pt_a: Vec<Vec<ImplicitPoint>> = (0..a.len()).map(|_| Vec::new()).collect();
let mut pt_b: Vec<Vec<ImplicitPoint>> = (0..b.len()).map(|_| Vec::new()).collect();
let pairs = super::broadphase::candidate_pairs(a, b);
for (i, j) in pairs {
if super::budget::tripped() {
break;
}
let (ta, tb) = (&a[i], &b[j]);
match tri_tri_intersection(ta, tb) {
TriTri::Segment([s, t]) => {
raw_a[i].push(RawSeg { a: s.clone(), b: t.clone(), cutter: *tb });
raw_b[j].push(RawSeg { a: s, b: t, cutter: *ta });
}
TriTri::Coplanar => {
cop_a[i].extend(coplanar_clip(ta, tb).into_iter().map(|(a, b)| Constraint { a, b }));
cop_b[j].extend(coplanar_clip(tb, ta).into_iter().map(|(a, b)| Constraint { a, b }));
}
TriTri::Point(p) => {
pt_a[i].push(p.clone());
pt_b[j].push(p);
}
TriTri::None => {}
}
}
let build = |tris: &[Tri], raw: &[Vec<RawSeg>], cop: &mut [Vec<Constraint>]| -> Vec<Vec<Constraint>> {
(0..tris.len())
.map(|i| {
let mut c = split_crossings(&tris[i], &raw[i]);
c.append(&mut cop[i]);
c
})
.collect()
};
let cop_parent_a: Vec<bool> = cop_a.iter().map(|c| !c.is_empty()).collect();
let cop_parent_b: Vec<bool> = cop_b.iter().map(|c| !c.is_empty()).collect();
let ca = build(a, &raw_a, &mut cop_a);
let cb = build(b, &raw_b, &mut cop_b);
let mut interner = Interner::new();
let mut unrecovered = 0usize;
let (tris_a, coplanar_a) =
retriangulate_each(a, &ca, &pt_a, &cop_parent_a, &mut interner, &mut unrecovered);
let (tris_b, coplanar_b) =
retriangulate_each(b, &cb, &pt_b, &cop_parent_b, &mut interner, &mut unrecovered);
Arrangement { interner, tris_a, tris_b, coplanar_a, coplanar_b, unrecovered }
}
pub struct MultiArrangement {
pub interner: Interner,
pub subtris: Vec<Vec<[Vid; 3]>>,
}
pub fn arrange_many(meshes: &[&[Tri]]) -> MultiArrangement {
let n = meshes.len();
let mut raw: Vec<Vec<Vec<RawSeg>>> =
meshes.iter().map(|m| (0..m.len()).map(|_| Vec::new()).collect()).collect();
let mut cop: Vec<Vec<Vec<Constraint>>> =
meshes.iter().map(|m| (0..m.len()).map(|_| Vec::new()).collect()).collect();
let mut pts: Vec<Vec<Vec<ImplicitPoint>>> =
meshes.iter().map(|m| (0..m.len()).map(|_| Vec::new()).collect()).collect();
for i in 0..n {
if super::budget::tripped() {
break;
}
for j in (i + 1)..n {
let pairs = super::broadphase::candidate_pairs(meshes[i], meshes[j]);
for (ti, tj) in pairs {
if super::budget::tripped() {
break;
}
let (ta, tb) = (&meshes[i][ti], &meshes[j][tj]);
match tri_tri_intersection(ta, tb) {
TriTri::Segment([s, t]) => {
raw[i][ti].push(RawSeg { a: s.clone(), b: t.clone(), cutter: *tb });
raw[j][tj].push(RawSeg { a: s, b: t, cutter: *ta });
}
TriTri::Coplanar => {
cop[i][ti].extend(
coplanar_clip(ta, tb).into_iter().map(|(a, b)| Constraint { a, b }),
);
cop[j][tj].extend(
coplanar_clip(tb, ta).into_iter().map(|(a, b)| Constraint { a, b }),
);
}
TriTri::Point(p) => {
pts[i][ti].push(p.clone());
pts[j][tj].push(p);
}
TriTri::None => {}
}
}
}
}
let mut interner = Interner::new();
let mut subtris = Vec::with_capacity(n);
for k in 0..n {
let cop_parent: Vec<bool> = cop[k].iter().map(|c| !c.is_empty()).collect();
let cons: Vec<Vec<Constraint>> = (0..meshes[k].len())
.map(|t| {
let mut c = split_crossings(&meshes[k][t], &raw[k][t]);
c.append(&mut cop[k][t]);
c
})
.collect();
let mut _unrecovered = 0usize;
let (tris, _coplanar) =
retriangulate_each(meshes[k], &cons, &pts[k], &cop_parent, &mut interner, &mut _unrecovered);
subtris.push(tris);
}
MultiArrangement { interner, subtris }
}
pub fn union_all(meshes: &[&[Tri]]) -> Vec<Tri> {
if meshes.is_empty() {
return Vec::new();
}
if meshes.len() == 1 {
return meshes[0].to_vec();
}
use std::collections::HashMap;
let arr = arrange_many(meshes);
let exts: Vec<f64> = meshes.iter().map(|m| operand_extent(m)).collect();
let mut owner: HashMap<[Vid; 3], usize> = HashMap::new();
let mut keep: Vec<Vec<bool>> = Vec::with_capacity(meshes.len());
for (k, sub) in arr.subtris.iter().enumerate() {
let mut kk = Vec::with_capacity(sub.len());
for &tri in sub {
let c = centroid_multi(&arr, tri);
let n = tri_normal_multi(&arr, tri);
let outer = offset_point(c, n, exts[k]);
let on_boundary = (0..meshes.len())
.filter(|&m| m != k)
.all(|m| !point_inside(outer, meshes[m], exts[m]));
let mut keep_this = on_boundary;
if keep_this {
let key = rotate_min_first(tri);
match owner.get(&key) {
Some(&o) if o != k => keep_this = false, _ => {
owner.entry(key).or_insert(k);
}
}
}
kk.push(keep_this);
}
keep.push(kk);
}
let mut out = Vec::new();
for (k, sub) in arr.subtris.iter().enumerate() {
for (t, &tri) in sub.iter().enumerate() {
if keep[k][t] {
out.push([
point_via_interner(&arr.interner, tri[0]),
point_via_interner(&arr.interner, tri[1]),
point_via_interner(&arr.interner, tri[2]),
]);
}
}
}
out
}
fn offset_point(c: [f64; 3], dir: [f64; 3], far_l: f64) -> [f64; 3] {
let len = (dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]).sqrt();
if len == 0.0 {
return c;
}
let step = far_l * (1.0 / 1_048_576.0);
[
c[0] + dir[0] / len * step,
c[1] + dir[1] / len * step,
c[2] + dir[2] / len * step,
]
}
fn centroid_multi(arr: &MultiArrangement, tri: [Vid; 3]) -> [f64; 3] {
let c = [
point_via_interner(&arr.interner, tri[0]),
point_via_interner(&arr.interner, tri[1]),
point_via_interner(&arr.interner, tri[2]),
];
[
(c[0][0] + c[1][0] + c[2][0]) / 3.0,
(c[0][1] + c[1][1] + c[2][1]) / 3.0,
(c[0][2] + c[1][2] + c[2][2]) / 3.0,
]
}
fn tri_normal_multi(arr: &MultiArrangement, tri: [Vid; 3]) -> [f64; 3] {
let (a, b, c) = (
point_via_interner(&arr.interner, tri[0]),
point_via_interner(&arr.interner, tri[1]),
point_via_interner(&arr.interner, tri[2]),
);
cross3(sub_f64(b, a), sub_f64(c, a))
}
#[inline]
fn point_via_interner(it: &Interner, v: Vid) -> [f64; 3] {
let pt = it.get(v);
if let Some(f) = super::fixed::point_to_f64(pt) {
return f;
}
let p = point_of(pt);
[p[0].to_f64().unwrap(), p[1].to_f64().unwrap(), p[2].to_f64().unwrap()]
}
fn retriangulate_each(
tris: &[Tri],
cons: &[Vec<Constraint>],
pts: &[Vec<ImplicitPoint>],
cop_parent: &[bool],
it: &mut Interner,
unrecovered: &mut usize,
) -> (Vec<[Vid; 3]>, Vec<bool>) {
let mut out = Vec::new();
let mut coplanar = Vec::new();
for (i, t) in tris.iter().enumerate() {
if super::budget::tripped() {
break;
}
let parent_cop = cop_parent.get(i).copied().unwrap_or(false);
let passthrough = |it: &mut Interner| {
[
it.intern(ImplicitPoint::Explicit(t[0])),
it.intern(ImplicitPoint::Explicit(t[1])),
it.intern(ImplicitPoint::Explicit(t[2])),
]
};
let before = out.len();
let tri_pts = pts.get(i).cloned().unwrap_or_default();
if cons[i].is_empty() && tri_pts.is_empty() {
out.push(passthrough(it));
} else if let Some(mesh) = triangulate(
&RetriInput { tri: *t, constraints: cons[i].clone(), points: tri_pts },
it,
) {
*unrecovered += mesh.unrecovered;
out.extend(mesh.tris);
} else {
out.push(passthrough(it)); }
coplanar.resize(out.len(), parent_cop);
debug_assert!(coplanar.len() == out.len() && before <= out.len());
}
(out, coplanar)
}
fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
}
fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
#[inline]
fn e(p: [f64; 3]) -> ImplicitPoint {
ImplicitPoint::Explicit(p)
}
fn exact_seg_hits_tri(q1: [f64; 3], q2: [f64; 3], t: &Tri) -> bool {
let s1 = orient3d(&e(t[0]), &e(t[1]), &e(t[2]), &e(q1));
let s2 = orient3d(&e(t[0]), &e(t[1]), &e(t[2]), &e(q2));
if s1 == Sign::Zero || s2 == Sign::Zero || s1 == s2 {
return false;
}
let ea = orient3d(&e(q1), &e(q2), &e(t[0]), &e(t[1]));
let eb = orient3d(&e(q1), &e(q2), &e(t[1]), &e(t[2]));
let ec = orient3d(&e(q1), &e(q2), &e(t[2]), &e(t[0]));
ea != Sign::Zero && ea == eb && eb == ec
}
fn operand_extent(tris: &[Tri]) -> f64 {
let mut hi = 1.0f64;
for t in tris {
for v in t {
for &c in v {
hi = hi.max(c.abs());
}
}
}
2.0 * hi + 1.0
}
fn ray_dir() -> [f64; 3] {
[0.301_511_3, 0.557_328_1, 0.773_890_1]
}
fn point_inside(p: [f64; 3], tris: &[Tri], far_l: f64) -> bool {
let dir = ray_dir();
let far = [p[0] + dir[0] * far_l, p[1] + dir[1] * far_l, p[2] + dir[2] * far_l];
tris.iter().filter(|t| exact_seg_hits_tri(p, far, t)).count() % 2 == 1
}
fn to_f64_pt(arr: &Arrangement, v: Vid) -> [f64; 3] {
let pt = arr.interner.get(v);
if let Some(f) = super::fixed::point_to_f64(pt) {
return f;
}
let p = point_of(pt);
[p[0].to_f64().unwrap(), p[1].to_f64().unwrap(), p[2].to_f64().unwrap()]
}
fn sub_f64(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
fn centroid(arr: &Arrangement, tri: [Vid; 3]) -> [f64; 3] {
let c = [to_f64_pt(arr, tri[0]), to_f64_pt(arr, tri[1]), to_f64_pt(arr, tri[2])];
[
(c[0][0] + c[1][0] + c[2][0]) / 3.0,
(c[0][1] + c[1][1] + c[2][1]) / 3.0,
(c[0][2] + c[1][2] + c[2][2]) / 3.0,
]
}
fn tri_normal(arr: &Arrangement, tri: [Vid; 3]) -> [f64; 3] {
let (a, b, c) = (to_f64_pt(arr, tri[0]), to_f64_pt(arr, tri[1]), to_f64_pt(arr, tri[2]));
cross3(sub_f64(b, a), sub_f64(c, a))
}
fn drop_axis_of(n: [f64; 3]) -> DropAxis {
let an = [n[0].abs(), n[1].abs(), n[2].abs()];
if an[0] >= an[1] && an[0] >= an[2] {
DropAxis::X
} else if an[1] >= an[2] {
DropAxis::Y
} else {
DropAxis::Z
}
}
fn on_surface_normal(c: [f64; 3], others: &[Tri]) -> Option<[f64; 3]> {
for t in others {
if orient3d(&e(t[0]), &e(t[1]), &e(t[2]), &e(c)) != Sign::Zero {
continue; }
let n = cross3(sub_f64(t[1], t[0]), sub_f64(t[2], t[0]));
let axis = drop_axis_of(n);
let w0 = orient2d_any(&e(t[0]), &e(t[1]), &e(t[2]), axis);
if w0 == Sign::Zero {
continue; }
let inside = |u: [f64; 3], v: [f64; 3]| orient2d_any(&e(u), &e(v), &e(c), axis) != w0.flip();
if inside(t[0], t[1]) && inside(t[1], t[2]) && inside(t[2], t[0]) {
return Some(n);
}
}
None
}
use super::mesh_bridge::SNAP_GRID;
fn near_on_surface_normal(c: [f64; 3], others: &[Tri]) -> Option<[f64; 3]> {
let mut extent = 1.0f64;
for &x in &c {
extent = extent.max(x.abs());
}
for t in others {
for v in t {
for &x in v {
extent = extent.max(x.abs());
}
}
}
let band = (8.0 * SNAP_GRID).max(extent * (1.0 / 4_194_304.0));
let band2 = band * band;
for t in others {
let n = cross3(sub_f64(t[1], t[0]), sub_f64(t[2], t[0]));
let nn = n[0] * n[0] + n[1] * n[1] + n[2] * n[2];
if nn <= 0.0 || !nn.is_finite() {
continue; }
let d = dot3(sub_f64(c, t[0]), n);
if (d * d) / nn > band2 {
continue; }
let axis = drop_axis_of(n);
let w0 = orient2d_any(&e(t[0]), &e(t[1]), &e(t[2]), axis);
if w0 == Sign::Zero {
continue; }
let inside = |u: [f64; 3], v: [f64; 3]| orient2d_any(&e(u), &e(v), &e(c), axis) != w0.flip();
if inside(t[0], t[1]) && inside(t[1], t[2]) && inside(t[2], t[0]) {
return Some(n);
}
}
None
}
fn solid_side(c: [f64; 3], dir: [f64; 3], other: &[Tri], far_l: f64) -> (bool, bool) {
let len = (dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]).sqrt();
if len == 0.0 {
return (false, false);
}
let step = far_l * (1.0 / 1_048_576.0);
let u = [dir[0] / len * step, dir[1] / len * step, dir[2] / len * step];
let p_plus = [c[0] + u[0], c[1] + u[1], c[2] + u[2]];
let p_minus = [c[0] - u[0], c[1] - u[1], c[2] - u[2]];
(point_inside(p_plus, other, far_l), point_inside(p_minus, other, far_l))
}
struct BComponents<'a> {
comps: &'a [&'a [Tri]],
aabbs: Vec<([f64; 3], [f64; 3])>,
exts: Vec<f64>,
}
impl<'a> BComponents<'a> {
fn new(comps: &'a [&'a [Tri]]) -> Self {
let exts: Vec<f64> = comps.iter().map(|c| operand_extent(c)).collect();
let max_ext = exts.iter().cloned().fold(1.0f64, f64::max);
let pad = 4.0 * (8.0 * SNAP_GRID).max(max_ext * (1.0 / 4_194_304.0));
let aabbs = comps
.iter()
.map(|c| {
let mut lo = [f64::MAX; 3];
let mut hi = [f64::MIN; 3];
for t in c.iter() {
for v in t {
for k in 0..3 {
lo[k] = lo[k].min(v[k]);
hi[k] = hi[k].max(v[k]);
}
}
}
for k in 0..3 {
lo[k] -= pad;
hi[k] += pad;
}
(lo, hi)
})
.collect();
Self { comps, aabbs, exts }
}
fn ray_may_hit(&self, k: usize, p: [f64; 3]) -> bool {
let dir = ray_dir();
let far_l = self.exts[k];
let q = [p[0] + dir[0] * far_l, p[1] + dir[1] * far_l, p[2] + dir[2] * far_l];
let (lo, hi) = (&self.aabbs[k].0, &self.aabbs[k].1);
let (mut t0, mut t1) = (0.0f64, 1.0f64);
for i in 0..3 {
let d = q[i] - p[i];
if d == 0.0 {
if p[i] < lo[i] || p[i] > hi[i] {
return false;
}
continue;
}
let (a, b) = ((lo[i] - p[i]) / d, (hi[i] - p[i]) / d);
let (a, b) = if a <= b { (a, b) } else { (b, a) };
t0 = t0.max(a);
t1 = t1.min(b);
if t0 > t1 {
return false;
}
}
true
}
#[inline]
fn point_in_aabb(&self, k: usize, p: [f64; 3]) -> bool {
let (lo, hi) = (&self.aabbs[k].0, &self.aabbs[k].1);
(0..3).all(|i| p[i] >= lo[i] && p[i] <= hi[i])
}
fn inside(&self, p: [f64; 3]) -> bool {
self.comps
.iter()
.enumerate()
.any(|(k, comp)| self.ray_may_hit(k, p) && point_inside(p, comp, self.exts[k]))
}
fn surface_normal(&self, c: [f64; 3]) -> Option<[f64; 3]> {
for (k, comp) in self.comps.iter().enumerate() {
if self.point_in_aabb(k, c) {
if let Some(n) = on_surface_normal(c, comp) {
return Some(n);
}
}
}
for (k, comp) in self.comps.iter().enumerate() {
if self.point_in_aabb(k, c) {
if let Some(n) = near_on_surface_normal(c, comp) {
return Some(n);
}
}
}
None
}
fn solid_side(&self, c: [f64; 3], dir: [f64; 3]) -> (bool, bool) {
let len = (dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]).sqrt();
if len == 0.0 {
return (false, false);
}
let max_ext = self.exts.iter().cloned().fold(1.0f64, f64::max);
let step = max_ext * (1.0 / 1_048_576.0);
let u = [dir[0] / len * step, dir[1] / len * step, dir[2] / len * step];
let p_plus = [c[0] + u[0], c[1] + u[1], c[2] + u[2]];
let p_minus = [c[0] - u[0], c[1] - u[1], c[2] - u[2]];
(self.inside(p_plus), self.inside(p_minus))
}
}
fn on_interface_keep(
arr: &Arrangement,
tri: [Vid; 3],
c: [f64; 3],
other: &[Tri],
ext_other: f64,
op: BoolOp,
_a_side: bool,
) -> Option<bool> {
let n = tri_normal(arr, tri);
let (op_plus, op_minus) = solid_side(c, n, other, ext_other);
if op_plus == op_minus {
return None;
}
Some(match op {
BoolOp::Union => !op_plus,
BoolOp::Intersection => op_minus,
BoolOp::Difference => !op_minus,
})
}
fn boolean_vids(arr: &Arrangement, a: &[Tri], b: &[Tri], op: BoolOp) -> Vec<[Vid; 3]> {
boolean_vids_components(arr, a, &BComponents::new(&[b]), op)
}
fn boolean_vids_components(
arr: &Arrangement,
a: &[Tri],
bc: &BComponents,
op: BoolOp,
) -> Vec<[Vid; 3]> {
use std::collections::HashSet;
let ext_a = operand_extent(a);
let dedup = matches!(op, BoolOp::Union | BoolOp::Intersection);
let mut a_kept: HashSet<[Vid; 3]> = HashSet::new();
let mut out = Vec::new();
for (i, &tri) in arr.tris_a.iter().enumerate() {
let c = centroid(arr, tri);
let cop_parent = arr.coplanar_a.get(i).copied().unwrap_or(false);
let keep;
if let Some(n_other) = bc.surface_normal(c) {
let co_oriented = dot3(tri_normal(arr, tri), n_other) > 0.0;
keep = match op {
BoolOp::Union | BoolOp::Intersection => co_oriented,
BoolOp::Difference => !co_oriented,
};
} else if let Some(k) = cop_parent
.then(|| {
let n = tri_normal(arr, tri);
let (op_plus, op_minus) = bc.solid_side(c, n);
if op_plus == op_minus {
return None; }
Some(match op {
BoolOp::Union => !op_plus,
BoolOp::Intersection => op_minus,
BoolOp::Difference => !op_minus,
})
})
.flatten()
{
keep = k;
} else {
let inside_b = bc.inside(c);
keep = match op {
BoolOp::Intersection => inside_b,
_ => !inside_b,
};
}
if keep {
if dedup {
a_kept.insert(rotate_min_first(tri));
}
out.push(tri);
}
}
for (i, &tri) in arr.tris_b.iter().enumerate() {
if dedup && a_kept.contains(&rotate_min_first(tri)) {
continue;
}
let c = centroid(arr, tri);
let cop_parent = arr.coplanar_b.get(i).copied().unwrap_or(false);
if on_surface_normal(c, a).is_some() || near_on_surface_normal(c, a).is_some() {
continue; }
if cop_parent {
if let Some(keep) = on_interface_keep(arr, tri, c, a, ext_a, op, false) {
if keep {
let flip = matches!(op, BoolOp::Difference);
out.push(if flip { [tri[0], tri[2], tri[1]] } else { tri });
}
continue;
}
}
let inside_a = point_inside(c, a, ext_a);
let (keep, flip) = match op {
BoolOp::Difference => (inside_a, true),
BoolOp::Union => (!inside_a, false),
BoolOp::Intersection => (inside_a, false),
};
if keep {
out.push(if flip { [tri[0], tri[2], tri[1]] } else { tri });
}
}
out
}
pub fn boolean(a: &[Tri], b: &[Tri], op: BoolOp) -> Vec<Tri> {
let arr = arrange(a, b);
let vids = boolean_vids(&arr, a, b, op);
vids.into_iter()
.map(|t| [to_f64_pt(&arr, t[0]), to_f64_pt(&arr, t[1]), to_f64_pt(&arr, t[2])])
.collect()
}
pub fn difference_all(a: &[Tri], comps: &[&[Tri]]) -> Option<Vec<Tri>> {
if comps.is_empty() {
return Some(a.to_vec());
}
let b_all: Vec<Tri> = comps.iter().flat_map(|c| c.iter().copied()).collect();
let arr = arrange(a, &b_all);
if arr.unrecovered > 0 {
return None;
}
let bc = BComponents::new(comps);
let vids = boolean_vids_components(&arr, a, &bc, BoolOp::Difference);
Some(
vids.into_iter()
.map(|t| [to_f64_pt(&arr, t[0]), to_f64_pt(&arr, t[1]), to_f64_pt(&arr, t[2])])
.collect(),
)
}
#[inline]
fn rotate_min_first(t: [Vid; 3]) -> [Vid; 3] {
let i = (0..3).min_by_key(|&k| t[k]).unwrap();
[t[i], t[(i + 1) % 3], t[(i + 2) % 3]]
}
pub fn boolean_topology_hash(a: &[Tri], b: &[Tri], op: BoolOp) -> u64 {
let arr = arrange(a, b);
let mut tris: Vec<[Vid; 3]> =
boolean_vids(&arr, a, b, op).into_iter().map(rotate_min_first).collect();
tris.sort_unstable();
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for t in tris {
for v in t {
h ^= v as u64;
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
}
h
}
pub fn box_mesh(lo: [f64; 3], hi: [f64; 3]) -> Vec<Tri> {
let p = [
[lo[0], lo[1], lo[2]], [hi[0], lo[1], lo[2]], [hi[0], hi[1], lo[2]], [lo[0], hi[1], lo[2]],
[lo[0], lo[1], hi[2]], [hi[0], lo[1], hi[2]], [hi[0], hi[1], hi[2]], [lo[0], hi[1], hi[2]],
];
let idx = [
[0, 3, 2], [0, 2, 1], [4, 5, 6], [4, 6, 7],
[0, 4, 7], [0, 7, 3], [1, 2, 6], [1, 6, 5],
[0, 1, 5], [0, 5, 4], [3, 7, 6], [3, 6, 2],
];
idx.iter().map(|f| [p[f[0]], p[f[1]], p[f[2]]]).collect()
}
pub fn cube_mesh(lo: f64, hi: f64) -> Vec<Tri> {
box_mesh([lo, lo, lo], [hi, hi, hi])
}
pub fn boolean_manifest() -> u64 {
boolean_topology_hash(&cube_mesh(0.0, 2.0), &cube_mesh(1.0, 3.0), BoolOp::Difference)
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeSet;
fn cube(lo: f64, hi: f64) -> Vec<Tri> {
super::cube_mesh(lo, hi)
}
fn volume(m: &[Tri]) -> f64 {
m.iter().map(|t| dot3(t[0], cross3(t[1], t[2]))).sum::<f64>() / 6.0
}
#[test]
fn coplanar_channel_swallowed_corner_survives_552611() {
let x = 7.7642822265625;
let host: Vec<Tri> = vec![
[[x, 7.7840423583984375, 0.09417724609375], [x, -0.6199951171875, 0.09417724609375], [x, -0.6199951171875, 10.600006103515625]],
[[x, 7.7840423583984375, 0.09417724609375], [x, -0.6199951171875, 10.600006103515625], [x, 7.7840423583984375, 10.600006103515625]],
];
let cutter: Vec<Tri> = vec![
[[x, -0.160003662109375, 3.8000030517578125], [x, 5.02655029296875, 3.8000030517578125], [x, 5.02655029296875, 3.5]],
[[x, -0.160003662109375, 3.8000030517578125], [x, 5.02655029296875, 3.5], [x, -0.160003662109375, 3.5]],
[[x, 5.02655029296875, 3.8000030517578125], [x, 5.14154052734375, 3.8000030517578125], [x, 5.14154052734375, 3.5]],
[[x, 5.02655029296875, 3.8000030517578125], [x, 5.14154052734375, 3.5], [x, 5.02655029296875, 3.5]],
[[x, 5.14154052734375, 3.8000030517578125], [x, 7.7840423583984375, 3.8000030517578125], [x, 7.7840423583984375, 3.5]],
[[x, 5.14154052734375, 3.8000030517578125], [x, 7.7840423583984375, 3.5], [x, 5.14154052734375, 3.5]],
];
let arr = arrange(&host, &cutter);
for ct in &cutter {
for corner in ct {
let found = arr.tris_a.iter().flatten().any(|&v| {
let p = to_f64_pt(&arr, v);
p[1] == corner[1] && p[2] == corner[2]
});
assert!(
found,
"cutter corner ({}, {}) lost from the host conforming triangulation",
corner[1], corner[2]
);
}
}
}
#[test]
fn arrange_two_crossing_triangles_conform_along_the_intersection() {
let ta: Tri = [[-2., 0., -1.], [2., 0., -1.], [0., 0., 2.]]; let tb: Tri = [[1., -2., 1.], [1., 2., 1.], [1., 0.5, -3.]]; let arr = arrange(&[ta], &[tb]);
assert!(arr.tris_a.len() >= 2, "operand A not subdivided");
assert!(arr.tris_b.len() >= 2, "operand B not subdivided");
let va: BTreeSet<Vid> = arr.tris_a.iter().flatten().copied().collect();
let vb: BTreeSet<Vid> = arr.tris_b.iter().flatten().copied().collect();
let shared: Vec<Vid> = va.intersection(&vb).copied().collect();
assert_eq!(
shared.len(),
2,
"operands must share exactly the 2 intersection-segment vertices (conformity)"
);
}
#[test]
fn arrange_disjoint_meshes_are_untouched() {
let ta: Tri = [[0., 0., 0.], [1., 0., 0.], [0., 1., 0.]];
let tb: Tri = [[0., 0., 10.], [1., 0., 10.], [0., 1., 10.]]; let arr = arrange(&[ta], &[tb]);
assert_eq!(arr.tris_a.len(), 1, "disjoint A should pass through");
assert_eq!(arr.tris_b.len(), 1, "disjoint B should pass through");
}
#[test]
fn cube_helper_has_outward_winding() {
assert!((volume(&cube(0., 2.)) - 8.0).abs() < 1e-9, "cube volume wrong (winding?)");
}
#[test]
fn boolean_containment_cases_have_exact_volumes() {
let a = cube(1., 2.); let b = cube(0., 3.); let diff = boolean(&a, &b, BoolOp::Difference);
assert!(volume(&diff).abs() < 1e-9, "A−B should be empty, vol={}", volume(&diff));
let inter = boolean(&a, &b, BoolOp::Intersection);
assert!((volume(&inter) - 1.0).abs() < 1e-9, "A∩B should be A (vol 1), got {}", volume(&inter));
let uni = boolean(&a, &b, BoolOp::Union);
assert!((volume(&uni) - 27.0).abs() < 1e-9, "A∪B should be B (vol 27), got {}", volume(&uni));
}
#[test]
fn box_minus_box_real_cut_has_exact_volume() {
let a = cube(0., 2.); let b = cube(1., 3.); let diff = volume(&boolean(&a, &b, BoolOp::Difference));
assert!((diff - 7.0).abs() < 1e-6, "A−B volume = {diff}, expected 7");
let inter = volume(&boolean(&a, &b, BoolOp::Intersection));
assert!((inter - 1.0).abs() < 1e-6, "A∩B volume = {inter}, expected 1");
let uni = volume(&boolean(&a, &b, BoolOp::Union));
assert!((uni - 15.0).abs() < 1e-6, "A∪B volume = {uni}, expected 15");
}
#[test]
fn abutting_boxes_union_is_manifold_and_correct_volume() {
use num_rational::BigRational;
use num_traits::ToPrimitive;
use std::collections::BTreeMap;
let a = box_mesh([0., 0., 0.], [1., 1., 1.]);
let b = box_mesh([1., 0., 0.], [2., 1., 1.]);
let arr = arrange(&a, &b);
let result = boolean_vids(&arr, &a, &b, BoolOp::Union);
assert!(!result.is_empty(), "union is empty");
let mut edges: BTreeMap<(Vid, Vid), u32> = BTreeMap::new();
for t in &result {
for (u, v) in [(t[0], t[1]), (t[1], t[2]), (t[2], t[0])] {
*edges.entry(if u < v { (u, v) } else { (v, u) }).or_insert(0) += 1;
}
}
assert!(
edges.values().all(|&c| c == 2),
"abutting union is non-manifold (the shared x=1 face was not deduped)"
);
let co = |v: Vid| {
let p = point_of(arr.interner.get(v));
[
BigRational::to_f64(&p[0]).unwrap(),
BigRational::to_f64(&p[1]).unwrap(),
BigRational::to_f64(&p[2]).unwrap(),
]
};
let vol: f64 = result
.iter()
.map(|t| dot3(co(t[0]), cross3(co(t[1]), co(t[2]))))
.sum::<f64>()
/ 6.0;
assert!((vol - 2.0).abs() < 1e-6, "abutting-boxes union volume = {vol}, expected 2");
}
fn edge_audit(tris: &[Tri]) -> (usize, usize) {
use std::collections::BTreeMap;
let key = |p: [f64; 3]| {
(
(p[0] * 65536.0).round() as i64,
(p[1] * 65536.0).round() as i64,
(p[2] * 65536.0).round() as i64,
)
};
let mut edges: BTreeMap<((i64, i64, i64), (i64, i64, i64)), u32> = BTreeMap::new();
for t in tris {
let k = [key(t[0]), key(t[1]), key(t[2])];
for (u, v) in [(k[0], k[1]), (k[1], k[2]), (k[2], k[0])] {
*edges.entry(if u < v { (u, v) } else { (v, u) }).or_insert(0) += 1;
}
}
(
edges.values().filter(|&&c| c == 1).count(),
edges.values().filter(|&&c| c > 2).count(),
)
}
fn bounds(tris: &[Tri]) -> ([f64; 3], [f64; 3]) {
let mut lo = [f64::MAX; 3];
let mut hi = [f64::MIN; 3];
for t in tris {
for v in t {
for k in 0..3 {
lo[k] = lo[k].min(v[k]);
hi[k] = hi[k].max(v[k]);
}
}
}
(lo, hi)
}
#[test]
fn abutting_boxes_union_is_watertight_combined_hull() {
let a = box_mesh([0., 0., 0.], [1., 1., 1.]);
let b = box_mesh([1., 0., 0.], [2., 1., 1.]);
let u = boolean(&a, &b, BoolOp::Union);
let (boundary, nonmanifold) = edge_audit(&u);
assert_eq!(boundary, 0, "abutting-boxes union has {boundary} open boundary edges (torn)");
assert_eq!(nonmanifold, 0, "abutting-boxes union has {nonmanifold} non-manifold edges (the shared x=1 face was not dissolved)");
let (lo, hi) = bounds(&u);
assert_eq!(lo, [0., 0., 0.], "union lower bound should span both boxes");
assert_eq!(hi, [2., 1., 1.], "union upper bound should span both boxes");
}
#[test]
fn partial_coplanar_seam_union_is_watertight() {
let a = box_mesh([0., 0., 0.], [1., 2., 1.]);
let b = box_mesh([1., 1., 0.], [2., 3., 1.]);
let u = boolean(&a, &b, BoolOp::Union);
let (boundary, nonmanifold) = edge_audit(&u);
assert_eq!(boundary, 0, "partial-seam union has {boundary} open boundary edges");
assert_eq!(nonmanifold, 0, "partial-seam union has {nonmanifold} non-manifold edges");
}
#[test]
fn identical_solids_union_is_the_single_solid() {
let a = box_mesh([0., 0., 0.], [2., 2., 2.]);
let u = boolean(&a, &a, BoolOp::Union);
let (boundary, nonmanifold) = edge_audit(&u);
assert_eq!((boundary, nonmanifold), (0, 0), "self-union is not watertight: b={boundary} nm={nonmanifold}");
assert_eq!(u.len(), 12, "self-union should be the single 12-triangle box, got {}", u.len());
}
#[test]
fn nary_union_of_abutting_and_duplicate_prisms_is_watertight() {
let b0 = box_mesh([0., 0., 0.], [1., 1., 1.]);
let b1 = box_mesh([1., 0., 0.], [2., 1., 1.]);
let b2 = box_mesh([2., 0., 0.], [3., 1., 1.]);
let b1_dup = b1.clone();
let meshes: Vec<&[Tri]> = vec![&b0, &b1, &b2, &b1_dup];
let u = super::union_all(&meshes);
let (boundary, nonmanifold) = edge_audit(&u);
assert_eq!(boundary, 0, "N-ary union has {boundary} open boundary edges");
assert_eq!(nonmanifold, 0, "N-ary union has {nonmanifold} non-manifold edges");
let (lo, hi) = bounds(&u);
assert_eq!((lo, hi), ([0., 0., 0.], [3., 1., 1.]), "N-ary union bounds wrong");
}
#[test]
fn boolean_manifest_is_pinned() {
const PINNED: u64 = 0x0465_b83a_5fdb_8b2b;
let m = super::boolean_manifest();
assert_eq!(m, PINNED, "boolean topology manifest changed: 0x{m:016x}");
}
}