From 321add294bca601b652bb75d6610e25dc86e164b Mon Sep 17 00:00:00 2001
From: jyviko <2501648+jyviko@users.noreply.github.com>
Date: Wed, 14 Oct 2026 19:15:25 -0400
Subject: [PATCH] Geometry: partition a design's bodies by planes, with paired
cut-face names
Design::partitioned cuts the bodies as opened by planes in order (SolidCraft's
split_by_plane, in memory). Cells are
/a and /b by side of the plane;
the two faces of each cut are partition::a and :b, and every other face
inherits the name of the face it is a piece of (#k when a cell holds
several pieces). A cut that misses or names an unknown body is reported as a
design error, not fatal.
Co-Authored-By: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
---
crates/geometry/src/design.rs | 6 +-
crates/geometry/src/lib.rs | 2 +
crates/geometry/src/partition.rs | 477 +++++++++++++++++++++++++++++++
3 files changed, 484 insertions(+), 1 deletion(-)
create mode 100644 crates/geometry/src/partition.rs
diff --git a/crates/geometry/src/design.rs b/crates/geometry/src/design.rs
index 9ffe3e2..277c4b4 100644
--- a/crates/geometry/src/design.rs
+++ b/crates/geometry/src/design.rs
@@ -23,6 +23,10 @@ pub struct Design {
/// Features that failed to rebuild. The design still opens with the bodies the rest made,
/// as it does in SolidCraft.
pub errors: Vec,
+ /// The partitions applied ([`Design::partitioned`]), in order; empty for the design as opened.
+ pub cuts: Vec,
+ /// The bodies as opened, before any partition: what [`Design::partitioned`] starts from.
+ pub(crate) source: std::sync::Arc>,
}
/// A feature of the design that failed to rebuild.
@@ -68,7 +72,7 @@ pub(crate) fn rebuild(doc: &Document) -> Result {
})?;
bodies.push(Shape { name: b.name.clone(), body: b.body.clone(), faces });
}
- Ok(Design { name: doc.name.clone(), bodies, errors })
+ Ok(Design { name: doc.name.clone(), source: std::sync::Arc::new(bodies.clone()), bodies, errors, cuts: Vec::new() })
}
#[cfg(test)]
diff --git a/crates/geometry/src/lib.rs b/crates/geometry/src/lib.rs
index 4aec077..0fe32ac 100644
--- a/crates/geometry/src/lib.rs
+++ b/crates/geometry/src/lib.rs
@@ -18,11 +18,13 @@
#![forbid(unsafe_code)]
mod design;
+pub mod partition;
pub mod sample;
#[cfg(feature = "scripts")]
mod script;
pub use design::{Design, FeatureError, MAX_DESIGN_BYTES, open_design};
+pub use partition::{Cut, CutReport};
pub mod tagged_mesh;
#[cfg(feature = "scripts")]
pub use script::{MAX_SCRIPT_BYTES, script_design};
diff --git a/crates/geometry/src/partition.rs b/crates/geometry/src/partition.rs
new file mode 100644
index 0000000..044e23a
--- /dev/null
+++ b/crates/geometry/src/partition.rs
@@ -0,0 +1,477 @@
+//! 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