M2: exact planar boolean fallback for coincident faces (BSP polygons rebuilt into B-rep)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
c555f6f693
commit
d585afd006
8 files changed
+552
-18
No files matched your search
@@ -0,0 +1,457 @@
|
||||
//! Exact-plane booleans for bodies whose faces are all planar (BSP-tree CSG on polygons, in
|
||||
//! the classic style), used when the B-rep boolean fails — typically on coincident faces, such
|
||||
//! as a wall flush with the side of its base. The polygons are rebuilt into a B-rep: coplanar
|
||||
//! pieces are merged into faces, T-junctions are split so neighbouring faces share edges.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use solvecraft_geom::Vec3;
|
||||
use truck_modeling::{self as mt, builder};
|
||||
|
||||
use crate::body::{Body, Solid, p3};
|
||||
use crate::{BoolOp, KernelError, Result, guard};
|
||||
|
||||
const MAX_DEPTH: usize = 4000;
|
||||
const MAX_POLYS: usize = 200_000;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct PlaneEq {
|
||||
n: Vec3,
|
||||
w: f64,
|
||||
}
|
||||
|
||||
impl PlaneEq {
|
||||
fn flipped(self) -> PlaneEq {
|
||||
PlaneEq { n: -self.n, w: -self.w }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Poly {
|
||||
v: Vec<Vec3>,
|
||||
plane: PlaneEq,
|
||||
}
|
||||
|
||||
impl Poly {
|
||||
fn flip(&mut self) {
|
||||
self.v.reverse();
|
||||
self.plane = self.plane.flipped();
|
||||
}
|
||||
}
|
||||
|
||||
const COPLANAR: u8 = 0;
|
||||
const FRONT: u8 = 1;
|
||||
const BACK: u8 = 2;
|
||||
const SPANNING: u8 = 3;
|
||||
|
||||
/// Split `p` by `pl` into the four buckets.
|
||||
fn split(pl: PlaneEq, p: &Poly, eps: f64, cf: &mut Vec<Poly>, cb: &mut Vec<Poly>, f: &mut Vec<Poly>, b: &mut Vec<Poly>) {
|
||||
let mut ptype = 0u8;
|
||||
let types: Vec<u8> =
|
||||
p.v.iter()
|
||||
.map(|v| {
|
||||
let t = pl.n.dot(*v) - pl.w;
|
||||
let ty = if t < -eps {
|
||||
BACK
|
||||
} else if t > eps {
|
||||
FRONT
|
||||
} else {
|
||||
COPLANAR
|
||||
};
|
||||
ptype |= ty;
|
||||
ty
|
||||
})
|
||||
.collect();
|
||||
match ptype {
|
||||
COPLANAR => {
|
||||
if pl.n.dot(p.plane.n) > 0.0 {
|
||||
cf.push(p.clone());
|
||||
} else {
|
||||
cb.push(p.clone());
|
||||
}
|
||||
}
|
||||
FRONT => f.push(p.clone()),
|
||||
BACK => b.push(p.clone()),
|
||||
_ => {
|
||||
let (mut fv, mut bv) = (Vec::new(), Vec::new());
|
||||
let n = p.v.len();
|
||||
for i in 0..n {
|
||||
let j = (i + 1) % n;
|
||||
let (Some(&ti), Some(&tj), Some(&vi), Some(&vj)) = (types.get(i), types.get(j), p.v.get(i), p.v.get(j)) else { continue };
|
||||
if ti != BACK {
|
||||
fv.push(vi);
|
||||
}
|
||||
if ti != FRONT {
|
||||
bv.push(vi);
|
||||
}
|
||||
if (ti | tj) == SPANNING {
|
||||
let den = pl.n.dot(vj - vi);
|
||||
if den.abs() > 1e-300 {
|
||||
let t = (pl.w - pl.n.dot(vi)) / den;
|
||||
let x = vi.lerp(vj, t);
|
||||
fv.push(x);
|
||||
bv.push(x);
|
||||
}
|
||||
}
|
||||
}
|
||||
if fv.len() >= 3 {
|
||||
f.push(Poly { v: fv, plane: p.plane });
|
||||
}
|
||||
if bv.len() >= 3 {
|
||||
b.push(Poly { v: bv, plane: p.plane });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Node {
|
||||
plane: Option<PlaneEq>,
|
||||
front: Option<Box<Node>>,
|
||||
back: Option<Box<Node>>,
|
||||
polys: Vec<Poly>,
|
||||
}
|
||||
|
||||
impl Node {
|
||||
fn build(&mut self, polys: Vec<Poly>, eps: f64, depth: usize) -> Result<()> {
|
||||
if polys.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
if depth > MAX_DEPTH {
|
||||
return Err(KernelError::Failed("planar boolean: tree too deep".into()));
|
||||
}
|
||||
let pl = match self.plane {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
let p = polys.first().map(|p| p.plane).ok_or_else(|| KernelError::Failed("bsp".into()))?;
|
||||
self.plane = Some(p);
|
||||
p
|
||||
}
|
||||
};
|
||||
let (mut f, mut b) = (Vec::new(), Vec::new());
|
||||
let mut cf = Vec::new();
|
||||
let mut cb = Vec::new();
|
||||
for p in &polys {
|
||||
split(pl, p, eps, &mut cf, &mut cb, &mut f, &mut b);
|
||||
}
|
||||
self.polys.extend(cf);
|
||||
self.polys.extend(cb);
|
||||
if !f.is_empty() {
|
||||
self.front.get_or_insert_with(Default::default).build(f, eps, depth + 1)?;
|
||||
}
|
||||
if !b.is_empty() {
|
||||
self.back.get_or_insert_with(Default::default).build(b, eps, depth + 1)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn invert(&mut self) {
|
||||
for p in &mut self.polys {
|
||||
p.flip();
|
||||
}
|
||||
self.plane = self.plane.map(PlaneEq::flipped);
|
||||
if let Some(f) = &mut self.front {
|
||||
f.invert();
|
||||
}
|
||||
if let Some(b) = &mut self.back {
|
||||
b.invert();
|
||||
}
|
||||
std::mem::swap(&mut self.front, &mut self.back);
|
||||
}
|
||||
fn clip_polys(&self, polys: Vec<Poly>, eps: f64) -> Vec<Poly> {
|
||||
let Some(pl) = self.plane else { return polys };
|
||||
let (mut f, mut b) = (Vec::new(), Vec::new());
|
||||
let (mut cf, mut cb) = (Vec::new(), Vec::new());
|
||||
for p in &polys {
|
||||
split(pl, p, eps, &mut cf, &mut cb, &mut f, &mut b);
|
||||
}
|
||||
f.extend(cf);
|
||||
b.extend(cb);
|
||||
let f = match &self.front {
|
||||
Some(n) => n.clip_polys(f, eps),
|
||||
None => f,
|
||||
};
|
||||
let b = match &self.back {
|
||||
Some(n) => n.clip_polys(b, eps),
|
||||
None => Vec::new(),
|
||||
};
|
||||
let mut out = f;
|
||||
out.extend(b);
|
||||
out
|
||||
}
|
||||
fn clip_to(&mut self, other: &Node, eps: f64) {
|
||||
let polys = std::mem::take(&mut self.polys);
|
||||
self.polys = other.clip_polys(polys, eps);
|
||||
if let Some(f) = &mut self.front {
|
||||
f.clip_to(other, eps);
|
||||
}
|
||||
if let Some(b) = &mut self.back {
|
||||
b.clip_to(other, eps);
|
||||
}
|
||||
}
|
||||
fn all(&self, out: &mut Vec<Poly>) {
|
||||
out.extend(self.polys.iter().cloned());
|
||||
if let Some(f) = &self.front {
|
||||
f.all(out);
|
||||
}
|
||||
if let Some(b) = &self.back {
|
||||
b.all(out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Polygons of a body whose faces are all planar (None otherwise).
|
||||
fn planar_polys(b: &Body) -> Option<Vec<Poly>> {
|
||||
let tol = (b.size() * 1e-3).max(1e-3);
|
||||
let faces = b.faces(tol).ok()?;
|
||||
let mesh = b.tessellate(tol).ok()?;
|
||||
let planes: Vec<PlaneEq> = faces.iter().map(|f| f.plane_normal.map(|n| PlaneEq { n, w: n.dot(f.centroid) })).collect::<Option<_>>()?;
|
||||
let mut out = Vec::new();
|
||||
for (t, fi) in mesh.triangles.iter().zip(&mesh.tri_face) {
|
||||
let [a, bb, c] = mesh.tri(t)?;
|
||||
let plane = *planes.get(*fi as usize)?;
|
||||
// Snap the vertices onto the exact face plane.
|
||||
let snap = |p: Vec3| p - plane.n * (plane.n.dot(p) - plane.w);
|
||||
let mut v = vec![snap(a), snap(bb), snap(c)];
|
||||
if (v[1] - v[0]).cross(v[2] - v[0]).dot(plane.n) < 0.0 {
|
||||
v.reverse();
|
||||
}
|
||||
out.push(Poly { v, plane });
|
||||
}
|
||||
(out.len() <= MAX_POLYS).then_some(out)
|
||||
}
|
||||
|
||||
/// Boolean of two all-planar bodies.
|
||||
pub fn planar_boolean(a: &Body, b: &Body, op: BoolOp) -> Result<Option<Body>> {
|
||||
let (Some(pa), Some(pb)) = (planar_polys(a), planar_polys(b)) else {
|
||||
return Err(KernelError::Failed("not supported yet: coincident faces on curved bodies".into()));
|
||||
};
|
||||
let size = a.size().max(b.size());
|
||||
let eps = size * 1e-9;
|
||||
let (mut na, mut nb) = (Node::default(), Node::default());
|
||||
na.build(pa, eps, 0)?;
|
||||
nb.build(pb, eps, 0)?;
|
||||
match op {
|
||||
BoolOp::Union => {
|
||||
na.clip_to(&nb, eps);
|
||||
nb.clip_to(&na, eps);
|
||||
nb.invert();
|
||||
nb.clip_to(&na, eps);
|
||||
nb.invert();
|
||||
let mut extra = Vec::new();
|
||||
nb.all(&mut extra);
|
||||
na.build(extra, eps, 0)?;
|
||||
}
|
||||
BoolOp::Cut => {
|
||||
na.invert();
|
||||
na.clip_to(&nb, eps);
|
||||
nb.clip_to(&na, eps);
|
||||
nb.invert();
|
||||
nb.clip_to(&na, eps);
|
||||
nb.invert();
|
||||
let mut extra = Vec::new();
|
||||
nb.all(&mut extra);
|
||||
na.build(extra, eps, 0)?;
|
||||
na.invert();
|
||||
}
|
||||
BoolOp::Intersect => {
|
||||
na.invert();
|
||||
nb.clip_to(&na, eps);
|
||||
nb.invert();
|
||||
na.clip_to(&nb, eps);
|
||||
nb.clip_to(&na, eps);
|
||||
let mut extra = Vec::new();
|
||||
nb.all(&mut extra);
|
||||
na.build(extra, eps, 0)?;
|
||||
na.invert();
|
||||
}
|
||||
}
|
||||
let mut polys = Vec::new();
|
||||
na.all(&mut polys);
|
||||
if polys.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
rebuild(polys, size).map(Some)
|
||||
}
|
||||
|
||||
/// Position key for vertex merging.
|
||||
fn key(p: Vec3, q: f64) -> (i64, i64, i64) {
|
||||
((p.x / q).round() as i64, (p.y / q).round() as i64, (p.z / q).round() as i64)
|
||||
}
|
||||
|
||||
/// Turn polygons into a B-rep solid: merge by plane, split T-junctions, chain boundary loops.
|
||||
fn rebuild(polys: Vec<Poly>, size: f64) -> Result<Body> {
|
||||
let q = size * 1e-7;
|
||||
// Global vertex table.
|
||||
let mut index: HashMap<(i64, i64, i64), usize> = HashMap::new();
|
||||
let mut verts: Vec<Vec3> = Vec::new();
|
||||
let mut id = |p: Vec3, verts: &mut Vec<Vec3>| -> usize {
|
||||
let k = key(p, q);
|
||||
*index.entry(k).or_insert_with(|| {
|
||||
verts.push(p);
|
||||
verts.len() - 1
|
||||
})
|
||||
};
|
||||
let mut loops: Vec<(PlaneEq, Vec<usize>)> = Vec::new();
|
||||
for p in &polys {
|
||||
let mut l: Vec<usize> = p.v.iter().map(|v| id(*v, &mut verts)).collect();
|
||||
l.dedup();
|
||||
if l.first() == l.last() && l.len() > 1 {
|
||||
l.pop();
|
||||
}
|
||||
if l.len() >= 3 {
|
||||
loops.push((p.plane, l));
|
||||
}
|
||||
}
|
||||
// Split every polygon edge at vertices lying on it (T-junctions).
|
||||
let tol = size * 1e-7;
|
||||
let mut directed: Vec<(usize, usize, usize)> = Vec::new(); // (plane group, from, to)
|
||||
let mut groups: Vec<PlaneEq> = Vec::new();
|
||||
for (pl, l) in &loops {
|
||||
let g = match groups.iter().position(|g| g.n.dot(pl.n) > 1.0 - 1e-9 && (g.w - pl.w).abs() < tol * 10.0) {
|
||||
Some(g) => g,
|
||||
None => {
|
||||
groups.push(*pl);
|
||||
groups.len() - 1
|
||||
}
|
||||
};
|
||||
let n = l.len();
|
||||
for i in 0..n {
|
||||
let (Some(&a), Some(&b)) = (l.get(i), l.get((i + 1) % n)) else { continue };
|
||||
let (Some(&pa), Some(&pb)) = (verts.get(a), verts.get(b)) else { continue };
|
||||
let d = pb - pa;
|
||||
let len2 = d.len2();
|
||||
let mut on: Vec<(f64, usize)> = verts
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(k, _)| *k != a && *k != b)
|
||||
.filter_map(|(k, v)| {
|
||||
let t = (*v - pa).dot(d) / len2.max(1e-300);
|
||||
(t > 1e-9 && t < 1.0 - 1e-9 && v.dist_to_segment(pa, pb) < tol).then_some((t, k))
|
||||
})
|
||||
.collect();
|
||||
on.sort_by(|x, y| x.0.total_cmp(&y.0));
|
||||
let mut prev = a;
|
||||
for (_, k) in on {
|
||||
directed.push((g, prev, k));
|
||||
prev = k;
|
||||
}
|
||||
directed.push((g, prev, b));
|
||||
}
|
||||
}
|
||||
// Interior edges cancel against their reverse within the same plane group.
|
||||
let mut count: HashMap<(usize, usize, usize), i64> = HashMap::new();
|
||||
for (g, a, b) in &directed {
|
||||
*count.entry((*g, *a, *b)).or_insert(0) += 1;
|
||||
}
|
||||
let mut boundary: Vec<(usize, usize, usize)> = Vec::new();
|
||||
for (&(g, a, b), &c) in &count {
|
||||
let rev = count.get(&(g, b, a)).copied().unwrap_or(0);
|
||||
for _ in 0..(c - rev).max(0) {
|
||||
boundary.push((g, a, b));
|
||||
}
|
||||
}
|
||||
boundary.sort_unstable();
|
||||
guard("planar boolean rebuild", || {
|
||||
let tv: Vec<mt::Vertex> = verts.iter().map(|v| builder::vertex(p3(*v))).collect();
|
||||
let mut edges: HashMap<(usize, usize), mt::Edge> = HashMap::new();
|
||||
let mut faces: Vec<mt::Face> = Vec::new();
|
||||
for (g, plane) in groups.iter().enumerate() {
|
||||
let mut segs: Vec<(usize, usize)> = boundary.iter().filter(|e| e.0 == g).map(|e| (e.1, e.2)).collect();
|
||||
let mut wires: Vec<mt::Wire> = Vec::new();
|
||||
let mut loops_g: Vec<Vec<usize>> = Vec::new();
|
||||
while let Some((s, t)) = segs.pop() {
|
||||
let mut lp = vec![s, t];
|
||||
let mut cur = t;
|
||||
let mut guard_n = 0;
|
||||
while cur != s && guard_n < 100_000 {
|
||||
guard_n += 1;
|
||||
let Some(k) = segs.iter().position(|(a, _)| *a == cur) else { break };
|
||||
let (_, nb) = segs.remove(k);
|
||||
lp.push(nb);
|
||||
cur = nb;
|
||||
}
|
||||
if cur == s {
|
||||
lp.pop();
|
||||
loops_g.push(lp);
|
||||
}
|
||||
}
|
||||
for lp in &loops_g {
|
||||
let n = lp.len();
|
||||
let mut es = Vec::with_capacity(n);
|
||||
for i in 0..n {
|
||||
let (Some(&a), Some(&b)) = (lp.get(i), lp.get((i + 1) % n)) else { continue };
|
||||
let k = (a.min(b), a.max(b));
|
||||
let e = match edges.get(&k) {
|
||||
Some(e) => e.clone(),
|
||||
None => {
|
||||
let (Some(va), Some(vb)) = (tv.get(k.0), tv.get(k.1)) else { continue };
|
||||
let e = builder::line(va, vb);
|
||||
edges.insert(k, e.clone());
|
||||
e
|
||||
}
|
||||
};
|
||||
es.push(if a < b { e } else { e.inverse() });
|
||||
}
|
||||
wires.push(es.into());
|
||||
}
|
||||
if wires.is_empty() {
|
||||
continue;
|
||||
}
|
||||
// Outer loops are counter-clockwise about the plane normal; holes go with the
|
||||
// outer loop that contains them.
|
||||
let area = |lp: &Vec<usize>| -> f64 {
|
||||
let n = lp.len();
|
||||
let mut s = Vec3::ZERO;
|
||||
for i in 0..n {
|
||||
if let (Some(a), Some(b)) = (lp.get(i).and_then(|k| verts.get(*k)), lp.get((i + 1) % n).and_then(|k| verts.get(*k))) {
|
||||
s += a.cross(*b);
|
||||
}
|
||||
}
|
||||
s.dot(plane.n) * 0.5
|
||||
};
|
||||
let (outer, holes): (Vec<usize>, Vec<usize>) = (0..loops_g.len()).partition(|i| loops_g.get(*i).is_some_and(|l| area(l) > 0.0));
|
||||
let u = plane.n.any_perp();
|
||||
let w = plane.n.cross(u);
|
||||
let to2 = |p: Vec3| (p.dot(u), p.dot(w));
|
||||
let inside = |pt: Vec3, lp: &Vec<usize>| {
|
||||
let (px, py) = to2(pt);
|
||||
let n = lp.len();
|
||||
let mut c = false;
|
||||
for i in 0..n {
|
||||
let (Some(a), Some(b)) = (lp.get(i).and_then(|k| verts.get(*k)), lp.get((i + 1) % n).and_then(|k| verts.get(*k))) else {
|
||||
continue;
|
||||
};
|
||||
let ((ax, ay), (bx, by)) = (to2(*a), to2(*b));
|
||||
if (ay > py) != (by > py) && px < ax + (py - ay) / (by - ay) * (bx - ax) {
|
||||
c = !c;
|
||||
}
|
||||
}
|
||||
c
|
||||
};
|
||||
for o in &outer {
|
||||
let Some(ol) = loops_g.get(*o) else { continue };
|
||||
let mut ws = vec![wires.get(*o).cloned().ok_or_else(|| KernelError::Failed("wire".into()))?];
|
||||
for h in &holes {
|
||||
let Some(hl) = loops_g.get(*h) else { continue };
|
||||
let probe = hl.first().and_then(|k| verts.get(*k)).copied().unwrap_or_default();
|
||||
// The innermost outer loop containing the hole.
|
||||
let owner = outer.iter().filter(|x| loops_g.get(**x).is_some_and(|l| inside(probe, l))).min_by(|x, y| {
|
||||
let ax = loops_g.get(**x).map(area).unwrap_or(0.0);
|
||||
let ay = loops_g.get(**y).map(area).unwrap_or(0.0);
|
||||
ax.total_cmp(&ay)
|
||||
});
|
||||
if owner == Some(o) {
|
||||
ws.push(wires.get(*h).cloned().ok_or_else(|| KernelError::Failed("wire".into()))?);
|
||||
}
|
||||
}
|
||||
let _ = ol;
|
||||
let face = builder::try_attach_plane(&ws).map_err(|e| KernelError::Failed(format!("planar boolean face: {e}")))?;
|
||||
faces.push(face);
|
||||
}
|
||||
}
|
||||
let shell: mt::Shell = faces.into();
|
||||
let parts = shell.connected_components();
|
||||
let solid = Solid::try_new(parts).map_err(|e| KernelError::Failed(format!("planar boolean: {e}")))?;
|
||||
Body::new(solid)
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user