//! Partitions: a design's bodies cut into cells by planes, for meshing cell by cell.
//!
//! A partition cuts bodies of the open design by a plane with SolidCraft's kernel
//! (`split_by_plane`, the operation behind SolidCraft's split body), in memory: the design file is
//! never changed. The partitions live in the FieldCraft model and are applied again, in order,
//! whenever the design is opened, so an edit of the design followed by a re-open replays them.
//!
//! Names are FieldCraft's own, so they do not depend on the order of the design's timeline:
//! - **Cells.** A body cut by a plane gives `
/a` (the piece on the plane's negative side)
//! and `/b`; several disjoint pieces on one side are `/a1`, `/a2`, … ordered
//! by centroid.
//! - **Cut faces** pair across the cut: on a cell on the negative side (its outward normal along
//! the plane's normal) the face is `partition::a`, on the positive side
//! `partition::b`. Several disjoint faces on one cell (a cut through a hole) are `…#0`,
//! `…#1`, ordered along the plane, which a set naming `partition::a` selects together.
//! - **Every other face** inherits the persistent name of the face it is a piece of (found on the
//! same surface under a point of the piece). Where a cell has several pieces of one face they
//! are `#0`, `#1`, …, which sets naming `` select.
use solidcraft_geom::{Plane, Vec3};
use solidcraft_kernel::guard;
use crate::{Design, Error, Face, FaceSurface, FeatureError, Result, Shape, SurfaceMesh};
/// Most partitions a design takes.
pub const MAX_CUTS: usize = 256;
/// Most cells the partitions make in all.
pub const MAX_CELLS: usize = 4096;
/// A cut face lies on the plane within this fraction of its body's size.
const ON_PLANE: f64 = 1e-6;
/// Tessellation tolerance (fraction of a body's size) for matching and ordering faces.
const MATCH_TOLERANCE: f64 = 2e-3;
/// One partition: the bodies it cuts by a plane.
#[derive(Debug, Clone, PartialEq)]
pub struct Cut {
/// Names the cut faces (`partition::a|b`).
pub name: String,
/// A point on the plane and its normal (any length but zero).
pub point: [f64; 3],
pub normal: [f64; 3],
/// The bodies to cut, by name, as they are when this partition comes (the cells of earlier
/// partitions included); empty: every body the plane divides.
pub bodies: Vec,
}
/// What a partition did.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CutReport {
pub name: String,
/// The bodies it cut, each into two or more cells.
pub split: Vec,
/// How many cut faces it named.
pub faces: usize,
}
fn vec3(p: [f64; 3]) -> Vec3 {
Vec3 { x: p[0], y: p[1], z: p[2] }
}
fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
fn cross(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 lexical(a: &[f64; 3], b: &[f64; 3]) -> std::cmp::Ordering {
a.iter().zip(b).map(|(x, y)| x.total_cmp(y)).find(|o| o.is_ne()).unwrap_or(std::cmp::Ordering::Equal)
}
/// The plane of a cut, with its unit normal.
fn plane_of(cut: &Cut) -> Result<(Plane, [f64; 3])> {
let finite = cut.point.iter().chain(&cut.normal).all(|v| v.is_finite() && v.abs() <= 1e9);
let len = dot(cut.normal, cut.normal).sqrt();
if !finite || !(len > 1e-12) {
return Err(Error::Invalid(format!("partition `{}`: the plane needs a finite point and a non-zero normal", cut.name)));
}
let n = cut.normal.map(|v| v / len);
let plane = Plane::from_normal(vec3(cut.point), vec3(n))
.ok_or_else(|| Error::Invalid(format!("partition `{}`: the plane needs a non-zero normal", cut.name)))?;
Ok((plane, n))
}
impl Design {
/// The design as opened, cut by `cuts` in order (the partitions replace any applied before).
/// A cut that names a body the design does not have, that does not divide a body it names,
/// or that the kernel fails to make on any body it applies to, is listed in [`Design::errors`] (feature `partition `), as a feature of the
/// design that failed to rebuild; the rest still apply.
pub fn partitioned(&self, cuts: &[Cut]) -> Result {
if cuts.len() > MAX_CUTS {
return Err(Error::Invalid(format!("a design takes at most {MAX_CUTS} partitions")));
}
let planes: Vec<(Plane, [f64; 3])> = cuts.iter().map(plane_of).collect::>()?;
let mut bodies: Vec = self.source.as_ref().clone();
let mut errors: Vec = self.errors.iter().filter(|e| !e.feature.starts_with("partition ")).cloned().collect();
let mut reports = Vec::with_capacity(cuts.len());
for (cut, (plane, n)) in cuts.iter().zip(&planes) {
let feature = format!("partition {}", cut.name);
for b in &cut.bodies {
if !bodies.iter().any(|x| &x.name == b) {
errors.push(FeatureError { feature: feature.clone(), message: format!("no body `{b}` to cut"), bodies: vec![b.clone()] });
}
}
let mut report = CutReport { name: cut.name.clone(), split: Vec::new(), faces: 0 };
let mut next = Vec::with_capacity(bodies.len() + 1);
for body in bodies {
let listed = cut.bodies.iter().any(|b| b == &body.name);
if !(cut.bodies.is_empty() || listed) {
next.push(body);
continue;
}
match split(&body, plane, *n, cut)? {
Split::Cells(cells, faces) => {
report.split.push(body.name.clone());
report.faces += faces;
next.extend(cells);
}
Split::Misses => {
if listed {
errors.push(FeatureError {
feature: feature.clone(),
message: format!("the plane does not divide `{}`", body.name),
bodies: vec![body.name.clone()],
});
}
next.push(body);
}
// Listed or not: the plane may well divide the body, and a cut that silently
// leaves it whole would be mistaken for one that does not reach it (#283).
Split::Failed(e) => {
errors.push(FeatureError {
feature: feature.clone(),
message: format!("the kernel could not cut `{}` ({e})", body.name),
bodies: vec![body.name.clone()],
});
next.push(body);
}
}
if next.len() > MAX_CELLS {
return Err(Error::Invalid(format!("the partitions make at most {MAX_CELLS} cells")));
}
}
bodies = next;
reports.push(report);
}
Ok(Design { name: self.name.clone(), bodies, errors, cuts: reports, source: std::sync::Arc::clone(&self.source) })
}
}
/// What cutting one body gave.
enum Split {
/// Its cells, named, and how many cut faces they have.
Cells(Vec, usize),
/// The plane does not divide it.
Misses,
/// The kernel failed to cut it: its message.
Failed(String),
}
/// Cut one body.
fn split(body: &Shape, plane: &Plane, n: [f64; 3], cut: &Cut) -> Result {
let parts = match guard("partition", || solidcraft_kernel::split_by_plane(&body.body, plane)) {
Ok(p) if p.len() >= 2 => p,
Ok(_) => return Ok(Split::Misses),
Err(e) => return Ok(Split::Failed(e.to_string())),
};
let size = body.size()?;
let tol = (MATCH_TOLERANCE * size).max(1e-4);
let parent = Faces::of(body, tol)?;
let mut cells: Vec<(bool, [f64; 3], Shape)> = Vec::with_capacity(parts.len());
for part in parts {
let mut shape = Shape { name: String::new(), body: part, faces: Vec::new() };
let m = shape.measure()?;
let negative = dot(sub(m.centroid, cut.point), n) < 0.0;
shape.faces = (0..m.faces).map(|_| Face { name: String::new(), exact: false }).collect();
cells.push((negative, m.centroid, shape));
}
cells.sort_by(|a, b| b.0.cmp(&a.0).then_with(|| lexical(&a.1, &b.1)));
let mut faces = 0;
let count = |neg: bool| cells.iter().filter(|c| c.0 == neg).count();
let (na, nb) = (count(true), count(false));
let (mut ka, mut kb) = (0, 0);
let mut out = Vec::with_capacity(cells.len());
for (negative, _, mut shape) in cells {
let (k, of) = if negative {
ka += 1;
(ka, na)
} else {
kb += 1;
(kb, nb)
};
let side = if negative { "a" } else { "b" };
shape.name = if of > 1 { format!("{}/{side}{k}", body.name) } else { format!("{}/{side}", body.name) };
faces += name_faces(&mut shape, &parent, cut, n, size, tol)?;
out.push(shape);
}
Ok(Split::Cells(out, faces))
}
/// A body's faces as matching needs them: each face's analytic surface, name and triangles.
struct Faces {
surfaces: Vec