//! Booleans of bodies with coincident planar faces, by moving faces apart first.
//!
//! The mesh-based intersection cannot handle faces that lie in the same plane, nor the edges
//! lying in them. A face whose neighbours stand perpendicular to it (any extruded or
//! box-like part) can be pushed along its normal exactly, by rebuilding it and stretching or
//! shrinking its neighbours. Then:
//! - cut and intersect: the tool's coincident face moves out of the way (outward when the
//! bodies are on the same side of the plane, away from the target when they touch back to
//! back), which leaves the result unchanged;
//! - union of faces facing the same way: both faces move outward by different amounts, the
//! union is taken, and the slab beyond the plane is cut off (now a transversal cut);
//! - union of faces back to back: the tool's face moves into the target, which overlaps it.
//!
//! Each of these leaves the result unchanged only where the space a face moves through holds
//! what the boolean leaves there anyway: none of the target for a cut or an intersection, the
//! target's material for a union, nothing of either body beyond a plane whose slab is cut off
//! again. The faces of a coincident plane lie on each other, but nothing says that what lies
//! beside them does: a tool's side in the face of a wall has the neighbouring walls beside it,
//! a channel's top in the face a rib stands on has the rib over it. So a face is asked first
//! what it would move through: a join's face moving into the target at points behind it
//! (`backed`), the others on the other body's faces (see [`crate::clearance`]). A face with
//! no room stays where it is and the boolean fails with [`NO_ROOM`], for the strategies that
//! take the bodies as they lie.
use std::collections::HashMap;
use solidcraft_geom::{Aabb3, Vec3};
use truck_modeling::{self as mt, builder};
use crate::body::shell_groups::boxes_separate;
use crate::body::{Body, Solid, from_p3, p3, v3};
use crate::clearance::{Clearance, Occupancy};
use crate::history::{Change, EdgeOrigin, FaceOrigin, FaceProv, History};
use crate::{BoolOp, KernelError, Result, guard, record};
#[derive(Clone, Copy, Debug)]
struct PlaneFace {
n: Vec3,
d: f64,
}
fn planes(b: &Body) -> Vec<(usize, PlaneFace)> {
b.solid
.face_iter()
.enumerate()
.filter_map(|(i, f)| match f.oriented_surface() {
mt::Surface::Plane(p) => {
let n = p.normal();
let n = Vec3::new(n.x, n.y, n.z).normalized()?;
Some((i, PlaneFace { n, d: n.dot(from_p3(p.origin())) }))
}
_ => None,
})
.collect()
}
/// Push the planar faces of `b` lying in the plane (n, d) — n their outward normal — along n
/// by `delta` (negative: inward). Their neighbours must stand perpendicular to the plane. The
/// result's history is over `[b]`.
pub(crate) fn push_faces(b: &Body, n: Vec3, d: f64, delta: f64) -> Result
{
// The exact rebuild, unless it came out unsound (a curved side it couldn't re-sweep).
match push_faces_exact(b, n, d, delta)
.and_then(|r| if r.validity().is_empty() { Ok(r) } else { Err(KernelError::Failed("pushing a face: an unsound result".into())) })
{
Ok(r) => Ok(r),
// Faces of planes and cylinders: every face follows (any neighbours).
Err(e) => {
let size = b.size();
let tol = (size * 1e-7).max(1e-9) * 10.0;
let planes: Vec = b
.solid
.face_iter()
.map(|f| match f.oriented_surface() {
mt::Surface::Plane(p) => {
let m = p.normal();
let m = Vec3::new(m.x, m.y, m.z);
m.dot(n) > 1.0 - 1e-9 && (n.dot(from_p3(p.origin())) - d).abs() < tol
}
_ => false,
})
.collect();
crate::offset::offset_body(b, |i, _| if planes.get(i).copied().unwrap_or(false) { delta } else { 0.0 }).map_err(|_| e)
}
}
}
pub(crate) fn push_faces_exact(b: &Body, n: Vec3, d: f64, delta: f64) -> Result {
let size = b.size();
let tol = (size * 1e-7).max(1e-9);
let fail = |m: &str| KernelError::Failed(format!("pushing a face: {m}"));
guard("push face", || {
let solid = b.deep_copy();
// The copy's edges are b's, in order; the faces come out in place, one for one.
let mut edge_origins = record::edge_origins(&solid, 0);
let mut faces_from = Vec::new();
let shift = n * delta;
let mat = mt::Matrix4::from_translation(v3(shift));
let mut shells = Vec::new();
for sh in solid.boundaries() {
let faces: Vec = sh.face_iter().cloned().collect();
let on_plane = |f: &mt::Face| match f.oriented_surface() {
mt::Surface::Plane(p) => {
let m = p.normal();
let m = Vec3::new(m.x, m.y, m.z);
m.dot(n) > 1.0 - 1e-9 && (n.dot(from_p3(p.origin())) - d).abs() < tol * 10.0
}
_ => false,
};
let moving: Vec = (0..faces.len()).filter(|i| faces.get(*i).is_some_and(on_plane)).collect();
let first = faces_from.len();
if moving.is_empty() {
faces_from.extend((first..first + faces.len()).map(|i| record::from_face(0, i, Change::Kept)));
shells.push(sh.clone());
continue;
}
// Vertices and edges of the moving faces.
let mut vmap: HashMap = HashMap::new();
let mut emap: HashMap = HashMap::new();
for i in &moving {
let f = faces.get(*i).ok_or_else(|| fail("face"))?;
for v in f.vertex_iter() {
vmap.entry(v.id()).or_insert_with(|| builder::vertex(p3(from_p3(v.point()) + shift)));
}
}
for i in &moving {
let f = faces.get(*i).ok_or_else(|| fail("face"))?;
for e in f.edge_iter() {
if emap.contains_key(&e.id()) {
continue;
}
let (a, bb) = (
vmap.get(&e.absolute_front().id()).ok_or_else(|| fail("vertex"))?,
vmap.get(&e.absolute_back().id()).ok_or_else(|| fail("vertex"))?,
);
let curve = mt::Transformed::transformed(&e.curve(), mat);
emap.insert(e.id(), mt::Edge::new(a, bb, curve));
}
}
// Edges leaving the plane at moved vertices: straight, along n.
for f in &faces {
for e in f.edge_iter() {
if emap.contains_key(&e.id()) {
continue;
}
let (fa, fb) = (e.absolute_front(), e.absolute_back());
let (ma, mb) = (vmap.get(&fa.id()), vmap.get(&fb.id()));
if ma.is_none() && mb.is_none() {
continue;
}
if !matches!(e.curve(), mt::Curve::Line(_)) {
return Err(fail("a neighbouring edge is not straight"));
}
let dir = (from_p3(fb.point()) - from_p3(fa.point())).normalized().ok_or_else(|| fail("zero edge"))?;
if dir.cross(n).len() > 1e-9 {
return Err(fail("a neighbouring face is not perpendicular"));
}
let (na, nb) = (ma.cloned().unwrap_or_else(|| fa.clone()), mb.cloned().unwrap_or_else(|| fb.clone()));
if (from_p3(nb.point()) - from_p3(na.point())).dot(dir) <= tol {
return Err(fail("the face would pass its opposite side"));
}
emap.insert(e.id(), builder::line(&na, &nb));
}
}
let rebuild_wires = |f: &mt::Face| -> Vec {
f.absolute_boundaries()
.iter()
.map(|w| {
w.edge_iter()
.map(|e| match emap.get(&e.id()) {
Some(ne) => {
if e.front() == e.absolute_front() {
ne.clone()
} else {
ne.inverse()
}
}
None => e.clone(),
})
.collect::>()
.into()
})
.collect()
};
// Each moved or stretched edge stands for the one it replaces.
for (old, new) in &emap {
if let Some(o) = edge_origins.get(old).cloned() {
edge_origins.insert(new.id(), o);
}
}
use mt::{ParametricSurface3D, SearchNearestParameter};
let mut out: Vec = Vec::new();
for (i, f) in faces.iter().enumerate() {
let touches = f.edge_iter().any(|e| emap.contains_key(&e.id()));
let change = if touches { Change::Modified } else { Change::Kept };
faces_from.push(record::from_face(0, first + i, change));
if !touches {
out.push(f.clone());
continue;
}
let wires = rebuild_wires(f);
let surface = if moving.contains(&i) {
mt::Transformed::transformed(&f.surface(), mat)
} else if matches!(f.surface(), mt::Surface::Plane(_)) {
f.surface()
} else {
// A curved side: sweep its edge in the plane along the new height.
let base = f.edge_iter().find_map(|e| {
emap.get(&e.id()).filter(|_| {
let (a, bb) = (from_p3(e.absolute_front().point()), from_p3(e.absolute_back().point()));
(n.dot(a) - d).abs() < tol * 10.0 && (n.dot(bb) - d).abs() < tol * 10.0
})
});
let base = base.ok_or_else(|| fail("a curved side without an edge in the plane"))?;
let height = f.vertex_iter().map(|v| n.dot(from_p3(v.point())) - d).fold(0.0f64, |m, x| if x.abs() > m.abs() { x } else { m });
let swept: mt::Face = builder::tsweep(base, v3(n * (height - delta)));
let mut s = swept.surface();
// Same normal as the old side at a point both share.
let probe = f.vertex_iter().map(|v| from_p3(v.point())).fold(Vec3::ZERO, |a, x| a + x) / f.vertex_iter().count().max(1) as f64;
let old = f.surface();
if let (Some((u0, v0)), Some((u1, v1))) =
(old.search_nearest_parameter(p3(probe), None, 100), s.search_nearest_parameter(p3(probe), None, 100))
{
let (a, bb) = (old.normal(u0, v0), s.normal(u1, v1));
if mt::InnerSpace::dot(a, bb) < 0.0 {
s = mt::Invertible::inverse(&s);
}
}
s
};
let mut nf = mt::Face::try_new(wires, surface).map_err(|e| fail(&e.to_string()))?;
if !f.orientation() {
nf.invert();
}
out.push(nf);
}
shells.push(out.into());
}
let s = Solid::try_new(shells).map_err(|e| fail(&e.to_string()))?;
record::build(s, &record::counts(&[b]), Some(faces_from), Some(&edge_origins))
})
}
/// Move the planar faces of `b` that lie in the plane (n, d), n their outward normal, out by `delta`, whatever their
/// neighbours are. [`push_faces`] stretches the neighbours, which must stand square to the face along straight edges (a
/// box, an extruded outline); the neighbours of a lofted tube's end cap curve and lean. Here nothing is stretched: the
/// faces move, and each edge one shares with a face that stays gets a new face between them, the edge swept along `n`
/// (a strip). The neighbours keep their surfaces and meet the strips at a crease along the old edge, so the body is
/// the old one with a short prism of the faces' outline on each of them. Exact, with no boolean. The result has no
/// history.
pub(crate) fn grow_faces_exact(b: &Body, n: Vec3, d: f64, delta: f64) -> Result {
use mt::{BoundedCurve, ParametricCurve, ParametricSurface3D, SearchNearestParameter};
let size = b.size();
let tol = (size * 1e-7).max(1e-9);
let fail = |m: &str| KernelError::Failed(format!("growing a face: {m}"));
guard("grow face", || {
let solid = b.deep_copy();
let shift = n * delta;
let mat = mt::Matrix4::from_translation(v3(shift));
let mut shells = Vec::new();
for sh in solid.boundaries() {
let faces: Vec = sh.face_iter().cloned().collect();
let on_plane = |f: &mt::Face| match f.oriented_surface() {
mt::Surface::Plane(p) => {
let m = p.normal();
let m = Vec3::new(m.x, m.y, m.z);
m.dot(n) > 1.0 - 1e-9 && (n.dot(from_p3(p.origin())) - d).abs() < tol * 10.0
}
_ => false,
};
let moving: Vec = (0..faces.len()).filter(|i| faces.get(*i).is_some_and(on_plane)).collect();
if moving.is_empty() {
shells.push(sh.clone());
continue;
}
let stays = |i: &usize| !moving.contains(i);
// The faces each edge bounds.
let mut bounds: HashMap> = HashMap::new();
for (i, f) in faces.iter().enumerate() {
for e in f.edge_iter() {
bounds.entry(e.id()).or_default().push(i);
}
}
// The moved copies of the moving faces' vertices and edges.
let mut vmap: HashMap = HashMap::new();
let mut emap: HashMap = HashMap::new();
for i in &moving {
let f = faces.get(*i).ok_or_else(|| fail("face"))?;
for v in f.vertex_iter() {
vmap.entry(v.id()).or_insert_with(|| builder::vertex(p3(from_p3(v.point()) + shift)));
}
}
for i in &moving {
let f = faces.get(*i).ok_or_else(|| fail("face"))?;
for e in f.edge_iter() {
if emap.contains_key(&e.id()) {
continue;
}
let (a, bb) = (
vmap.get(&e.absolute_front().id()).ok_or_else(|| fail("vertex"))?,
vmap.get(&e.absolute_back().id()).ok_or_else(|| fail("vertex"))?,
);
emap.insert(e.id(), mt::Edge::new(a, bb, mt::Transformed::transformed(&e.curve(), mat)));
}
}
// The moved copy of an edge, running the way the edge does.
let moved = |e: &mt::Edge| -> Result {
let ne = emap.get(&e.id()).ok_or_else(|| fail("edge"))?;
Ok(if e.front() == e.absolute_front() { ne.clone() } else { ne.inverse() })
};
// A strip on every edge between a moving face and one that stays; the edges from a vertex to its moved copy are
// shared by the two strips that meet there.
let mut lateral: HashMap = HashMap::new();
let mut strips: Vec = Vec::new();
for i in &moving {
let f = faces.get(*i).ok_or_else(|| fail("face"))?;
for w in f.boundaries() {
for e in w.edge_iter() {
let Some(&beside) = bounds.get(&e.id()).and_then(|fs| fs.iter().find(|j| stays(j))) else { continue };
// The moving face runs along the edge from p to q; the strip runs along it the same way.
let (p, q) = (e.front().clone(), e.back().clone());
let mut lat = |v: &mt::Vertex| -> Result {
let to = vmap.get(&v.id()).ok_or_else(|| fail("vertex"))?;
Ok(lateral.entry(v.id()).or_insert_with(|| builder::line(v, to)).clone())
};
let (lp, lq) = (lat(&p)?, lat(&q)?);
let wire: mt::Wire = vec![e.clone(), lq, moved(e)?.inverse(), lp.inverse()].into();
// Its surface is the edge's curve swept along `n`, facing the way the face beside it does; a straight edge
// sweeps to a plane (truck makes that a B-spline patch, which does not mesh).
let curve = e.curve();
let (t0, t1) = curve.range_tuple();
let mid = from_p3(curve.subs(0.5 * (t0 + t1)));
let old = faces.get(beside).ok_or_else(|| fail("face"))?.oriented_surface();
let Some((u0, v0)) = old.search_nearest_parameter(p3(mid), None, 100) else {
return Err(fail("which way a strip faces"));
};
let outward = old.normal(u0, v0);
let outward = Vec3::new(outward.x, outward.y, outward.z);
// (a loft's straight section edge is a B-spline of degree one, so ask the points, not the type)
let straight = [0.2, 0.4, 0.6, 0.8].iter().all(|t| {
let x = from_p3(curve.subs(t0 + t * (t1 - t0)));
let chord = from_p3(q.point()) - from_p3(p.point());
let off = (x - from_p3(p.point())).cross(chord).len();
off <= tol * 10.0 * chord.len()
});
let s = if straight {
let dir = (from_p3(q.point()) - from_p3(p.point())).normalized().ok_or_else(|| fail("a zero edge"))?;
let mut m = dir.cross(n).normalized().ok_or_else(|| fail("an edge along the normal"))?;
if m.dot(outward) < 0.0 {
m = -m;
}
let o = from_p3(p.point());
mt::Surface::Plane(mt::Plane::new(p3(o), p3(o + dir), p3(o + m.cross(dir))))
} else {
let swept: mt::Face = builder::tsweep(e, v3(shift));
let mut s = swept.surface();
let Some((u1, v1)) = s.search_nearest_parameter(p3(mid + shift * 0.5), None, 100) else {
return Err(fail("which way a strip faces"));
};
let now = s.normal(u1, v1);
if outward.dot(Vec3::new(now.x, now.y, now.z)) < 0.0 {
s = mt::Invertible::inverse(&s);
}
s
};
strips.push(mt::Face::try_new(vec![wire], s).map_err(|e| fail(&e.to_string()))?);
}
}
}
let mut out: Vec = Vec::new();
for (i, f) in faces.iter().enumerate() {
if !moving.contains(&i) {
out.push(f.clone());
continue;
}
let wires = f
.absolute_boundaries()
.iter()
.map(|w| w.edge_iter().map(&moved).collect::>>().map(mt::Wire::from))
.collect::>>()?;
let mut nf = mt::Face::try_new(wires, mt::Transformed::transformed(&f.surface(), mat)).map_err(|e| fail(&e.to_string()))?;
if !f.orientation() {
nf.invert();
}
out.push(nf);
}
out.extend(strips);
shells.push(out.into());
}
let s = Solid::try_new(shells).map_err(|e| fail(&e.to_string()))?;
let r = Body::new(s)?;
let problems = r.validity();
if !problems.is_empty() {
return Err(fail(&format!("an unsound result ({})", problems.iter().take(2).cloned().collect::>().join("; "))));
}
Ok(r)
})
}
/// An edge's points from its start to its end along its curve, `k + 1` of them.
fn edge_samples(e: &mt::Edge, k: u32) -> Vec {
use mt::{BoundedCurve, ParametricCurve};
let c = e.curve();
let (t0, t1) = c.range_tuple();
(0..=k).map(|i| from_p3(c.subs(t0 + (t1 - t0) * f64::from(i) / f64::from(k)))).collect()
}
/// Where the edges of `b` cross the plane (n, d), one point for every time (edges are cut there before a body is trimmed).
fn plane_crossings(b: &Body, n: Vec3, d: f64, tol: f64) -> Vec {
use mt::{BoundedCurve, ParametricCurve};
const STEPS: u32 = 64;
let mut out = Vec::new();
for e in b.solid.edge_iter() {
let c = e.curve();
let (t0, t1) = c.range_tuple();
let at = |t: f64| from_p3(c.subs(t));
let h = |t: f64| n.dot(at(t)) - d;
let ts: Vec = (0..=STEPS).map(|i| t0 + (t1 - t0) * f64::from(i) / f64::from(STEPS)).collect();
// The samples clear of the plane, and between each two that lie on different sides the crossing.
let clear: Vec<(f64, f64)> = ts.iter().map(|t| (*t, h(*t))).filter(|(_, x)| x.abs() > tol).collect();
for pair in clear.windows(2) {
let [(ta, ha), (tb, hb)] = pair else { continue };
if (*ha < 0.0) == (*hb < 0.0) {
continue;
}
let (mut lo, mut hi, mut hlo) = (*ta, *tb, *ha);
for _ in 0..80 {
let mid = 0.5 * (lo + hi);
let hm = h(mid);
if (hm < 0.0) == (hlo < 0.0) {
lo = mid;
hlo = hm;
} else {
hi = mid;
}
}
out.push(at(0.5 * (lo + hi)));
}
}
out
}
/// Which side of the plane (n, d) an edge lies on, once edges that cross it are cut: -1 near, +1 far, 0 in it.
fn edge_side(e: &mt::Edge, n: Vec3, d: f64, tol: f64) -> Result {
let hs: Vec = edge_samples(e, 8).iter().map(|p| n.dot(*p) - d).collect();
let (lo, hi) = hs.iter().fold((f64::MAX, f64::MIN), |(l, h), x| (l.min(*x), h.max(*x)));
if hi <= tol && lo >= -tol {
Ok(0)
} else if hi <= tol {
Ok(-1)
} else if lo >= -tol {
Ok(1)
} else {
Err(KernelError::Failed("trimming at a plane: an edge still crosses it".into()))
}
}
/// Whether the point lies on the surface, to within `tol`.
fn on_surface(s: &mt::Surface, p: Vec3, tol: f64) -> bool {
use mt::{ParametricSurface, SearchNearestParameter};
if let mt::Surface::Plane(pl) = s {
let m = pl.normal();
return (Vec3::new(m.x, m.y, m.z).dot(p - from_p3(pl.origin()))).abs() <= tol;
}
s.search_nearest_parameter(p3(p), None, 100).is_some_and(|(u, v)| from_p3(s.subs(u, v)).dist(p) <= tol)
}
/// The edge that closes the boundary of the flat face `face` from the vertex `from` to the vertex `to` (both in the plane) where a run of
/// its edges beyond the plane went: the segment between them, which lies in the face. (A curved face that crosses the plane has been
/// divided along it already.) Nothing when they are the same vertex.
fn bridge(face: &mt::Face, from: &mt::Vertex, to: &mt::Vertex, tol: f64) -> Result> {
if from.id() == to.id() {
return Ok(Vec::new());
}
let (p, q) = (from_p3(from.point()), from_p3(to.point()));
let surface = face.oriented_surface();
if [0.25, 0.5, 0.75].iter().all(|t| on_surface(&surface, p + (q - p) * *t, tol * 10.0)) {
Ok(vec![builder::line(from, to)])
} else {
Err(KernelError::Failed("trimming at a plane: a curved face crosses the plane where it could not be divided".into()))
}
}
/// One boundary of the flat face `face`, which crosses the plane (n, d), clipped to the near side: the edges beyond the plane go and the
/// segment in the plane from where the boundary leaves it to where it comes back takes their place. `None` when nothing of the boundary
/// is on the near side.
fn clip_wire(face: &mt::Face, w: &mt::Wire, n: Vec3, d: f64, tol: f64) -> Result