//! 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, or that does not divide a body it
/// names, 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") });
}
}
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)? {
Some((cells, faces)) => {
report.split.push(body.name.clone());
report.faces += faces;
next.extend(cells);
}
None => {
if listed {
errors.push(FeatureError { feature: feature.clone(), message: format!("the plane does not divide `{}`", body.name) });
}
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) })
}
}
/// Cut one body: its cells, named, and how many cut faces they have; `None` when the plane does
/// not divide it.
fn split(body: &Shape, plane: &Plane, n: [f64; 3], cut: &Cut) -> Result