//! 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.
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::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))
})
}
/// A box covering everything beyond the plane (n, d) by up to `depth`.
fn slab_beyond(n: Vec3, d: f64, depth: f64, extent: f64) -> Result {
let u = n.any_perp();
let w = n.cross(u);
let o = n * d;
let pl = solidcraft_geom::Plane::new(o, u, w).ok_or_else(|| KernelError::Failed("slab plane".into()))?;
let sq = solidcraft_geom::Loop2::polygon(&[
solidcraft_geom::Vec2::new(-extent, -extent),
solidcraft_geom::Vec2::new(extent, -extent),
solidcraft_geom::Vec2::new(extent, extent),
solidcraft_geom::Vec2::new(-extent, extent),
]);
let r = solidcraft_geom::Region2 { outer: sq, holes: vec![] };
crate::extrude(&pl, &[r], 0.0, depth)?.pop().ok_or_else(|| KernelError::Failed("slab".into()))
}
/// The box of a face's boundary sampled to within `tol` of each curve: the face, planar, lies
/// within `tol` of it. `None` when the boundary can't be sampled.
fn face_box(f: &mt::Face, tol: f64) -> Option {
use mt::{BoundedCurve, ParameterDivision1D};
const MAX_POINTS: usize = 100_000;
guard("face box", || {
let mut bb = Aabb3::EMPTY;
let mut count = 0usize;
for e in f.edge_iter() {
let points = match e.curve() {
mt::Curve::Line(_) => vec![e.front().point(), e.back().point()],
// The division checks its deviation at midpoints only (as in blend.rs).
c => c.parameter_division(c.range_tuple(), tol * 0.25).1,
};
count = count.saturating_add(points.len());
if count > MAX_POINTS {
return Err(KernelError::Failed("face box: too many boundary points".into()));
}
for p in points {
bb.add(from_p3(p));
}
}
if bb.is_empty() {
return Err(KernelError::Failed("face box: no boundary".into()));
}
Ok(bb)
})
.ok()
}
/// The planes (normal, offset) in which a planar face of `a` and one of `b` coincide, as `a`'s
/// face lies in them, each with whether `b`'s face points the same way. Faces in a common plane but apart from
/// each other (the tops of two posts standing on one floor) don't coincide: they need no
/// pushing apart. A face whose extent can't be found counts as meeting every face in its plane.
pub(crate) fn coincident_planes(a: &Body, b: &Body) -> Vec<(Vec3, f64, bool)> {
let size = a.size().max(b.size());
let tol = (size * 1e-7).max(1e-9) * 10.0;
let sample = (size * 1e-4).max(tol);
let (fa, fb): (Vec<&mt::Face>, Vec<&mt::Face>) = (a.solid.face_iter().collect(), b.solid.face_iter().collect());
let (pa, pb) = (planes(a), planes(b));
let (mut boxes_a, mut boxes_b): (Vec