kernel: union_all, one SMLib union of many bodies checked once, history over every operand

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
This commit is contained in:
jyviko
2026-10-22 08:46:55 -04:00
parent 0185ccc1e9
commit 0213c3dfa6
4 changed files with 94 additions and 33 deletions
+1 -1
View File
@@ -188,7 +188,7 @@ pub use meshbody::{MAX_TRIANGLES, mesh_body};
pub use native::{body_from_smlib, smlib_body};
pub use native::{brep_record, decode_native_body, encode_native_body, native_preview_mesh};
pub use offset::with_same_surface;
pub use ops::{BoolOp, boolean, half_space, join_apart, place_matrix, split_by_plane, transform, transform_matrix, union_apart};
pub use ops::{BoolOp, boolean, half_space, join_apart, place_matrix, split_by_plane, transform, transform_matrix, union_all, union_apart};
pub use point_path::{
PathPiece, PointPath, SpaceBezier, curve_path, edge_path, face_section_along, iso_curves, project_onto_face, sketch_record_curve,
sweep_point_path_with,
+17 -2
View File
@@ -29,16 +29,31 @@ pub fn boolean(a: &Body, b: &Body, op: BoolOp) -> Result<Option<Body>> {
})
}
/// The union of `bodies` (at least one) in one SMLib call, each body taking part in about
/// log2(n) of its unions, the result checked once. A result SMLib's check refuses is an error.
///
/// The result carries its exact history over `bodies` in order, as [`boolean`]'s does, and
/// what the operands' faces stood for (their regularization protection and the planar roles on
/// supports it kept).
pub fn union_all(bodies: &[&Body]) -> Result<Body> {
crate::profile::timed(crate::profile::Kind::Boolean, || {
if bodies.is_empty() {
return Err(KernelError::Invalid("a union of no bodies".into()));
}
guard("SMLib union", || crate::smlib_boolean::smlib_union_all(bodies))?.with_preserved_union_planar_sources(bodies)
})
}
/// A boolean's result with what its operands' faces stood for carried over: regularization
/// protection and the planar construction and shell roles on supports it kept.
fn with_operand_roles(a: &Body, b: &Body, op: BoolOp, r: Result<Option<Body>>) -> Result<Option<Body>> {
let r = r.map(|result| result.map(|body| body.inherit_regularization_protection(a).inherit_regularization_protection(b)));
match op {
BoolOp::Cut => r.and_then(|result| result.map(|body| body.with_preserved_boolean_planar_sources(a, b, true)).transpose()),
BoolOp::Union => r.and_then(|result| result.map(|body| body.with_preserved_union_planar_sources(a, b)).transpose()),
BoolOp::Union => r.and_then(|result| result.map(|body| body.with_preserved_union_planar_sources(&[a, b])).transpose()),
// Every face of an intersection lies on one operand's face, as a union's does: the same
// roles carry over (#565).
BoolOp::Intersect => r.and_then(|result| result.map(|body| body.with_preserved_union_planar_sources(a, b)).transpose()),
BoolOp::Intersect => r.and_then(|result| result.map(|body| body.with_preserved_union_planar_sources(&[a, b])).transpose()),
}
}
+9 -8
View File
@@ -151,15 +151,15 @@ impl Body {
self_.map_planar_sources(&[input], None, Some(tool), &[], constructed)
}
/// Retain exact construction and shell roles through a Union from both operands, only on
/// unchanged supports. Coplanar supports of the two operands carry distinct identities, so
/// a face merged from both stays without a role.
pub(crate) fn with_preserved_union_planar_sources(self, a: &Body, b: &Body) -> Result<Self> {
let self_ = self.inherit_regularization_protection(a).inherit_regularization_protection(b);
if ![a, b].iter().any(|input| input.face_sources().is_some_and(|sources| sources.iter().flatten().any(|s| boolean_source(s, true)))) {
/// Retain exact construction and shell roles through a Union from every operand, only on
/// unchanged supports. Coplanar supports of different operands carry distinct identities, so
/// a face merged from several stays without a role.
pub(crate) fn with_preserved_union_planar_sources(self, operands: &[&Body]) -> Result<Self> {
let self_ = operands.iter().fold(self, |body, operand| body.inherit_regularization_protection(operand));
if !operands.iter().any(|input| input.face_sources().is_some_and(|sources| sources.iter().flatten().any(|s| boolean_source(s, true)))) {
return Ok(self_);
}
self_.map_planar_sources(&[a, b], None, None, &[], true)
self_.map_planar_sources(operands, None, None, &[], true)
}
fn map_planar_sources(mut self, inputs: &[&Body], shell: Option<f64>, blocker: Option<&Body>, open: &[Vec3], constructed: bool) -> Result<Self> {
@@ -172,7 +172,8 @@ impl Body {
if inputs.iter().all(|input| input.face_sources().is_none()) {
return Ok(self);
}
if inputs.len() > 2
// Each body at most 16 384 faces, and the inputs together at most two such bodies' worth.
if inputs.iter().fold(0usize, |n, input| n.saturating_add(input.face_count())) > 2 * 16_384
|| inputs.iter().any(|input| input.face_count() > 16_384)
|| self.face_count() > 16_384
|| blocker.is_some_and(|b| b.face_count() > 16_384)
+67 -22
View File
@@ -4,7 +4,7 @@
//! ([`crate::history::History`]'s contract). A result SMLib's own check or the record's geometry
//! check refuses is an error, never a body.
pub(crate) use smlib::boolean as smlib_boolean;
pub(crate) use smlib::{boolean as smlib_boolean, union_all as smlib_union_all};
mod smlib {
use solidcraft_smlib::{BooleanOp, CheckLevel};
@@ -34,6 +34,54 @@ mod smlib {
BoolOp::Intersect => BooleanOp::Intersect,
};
let Some(r) = ta.boolean(&tb, kind).map_err(|e| failed("operation", e))? else { return Ok(None) };
checked(&r, &[&sa, &sb])?;
let (body, or) = body_from_smlib_ordered(&r)?;
let history = match (oa, ob, or) {
(Some(oa), Some(ob), Some(or)) => r.history().map_err(|e| failed("history", e))?.history(&[oa, ob], &or).map(Some).unwrap_or_else(|e| {
log::debug!(target: "smlib", "boolean history: {e}");
None
}),
_ => None,
};
Ok(Some(with_history(body, history, a, b)))
}
/// The union of `bodies` in one SMLib call, each tagged as its own operand, checked once:
/// the result's history is over `bodies` in order. At least one body.
pub(crate) fn union_all(bodies: &[&Body]) -> Result<Body> {
if bodies.len() > solidcraft_smlib::MAX_UNION_BREPS {
return Err(KernelError::Invalid(format!("a union takes at most {} bodies", solidcraft_smlib::MAX_UNION_BREPS)));
}
let mut operands = Vec::with_capacity(bodies.len());
let mut tagged = Vec::with_capacity(bodies.len());
let mut orders = Some(Vec::with_capacity(bodies.len()));
for (i, body) in bodies.iter().enumerate() {
body.require_brep("a union")?;
let (s, order) = smlib_body_ordered(body)?;
tagged.push(s.tagged(i).map_err(|e| failed("tag", e))?);
operands.push(s);
orders = orders.zip(order).map(|(mut v, o)| {
v.push(o);
v
});
}
let refs: Vec<&solidcraft_smlib::Brep> = tagged.iter().collect();
let r = solidcraft_smlib::Brep::union_all(&refs).map_err(|e| failed("operation", e))?;
let operand_refs: Vec<&solidcraft_smlib::Brep> = operands.iter().collect();
checked(&r, &operand_refs)?;
let (body, or) = body_from_smlib_ordered(&r)?;
let history = match (orders, or) {
(Some(orders), Some(or)) => r.history().map_err(|e| failed("history", e))?.history(&orders, &or).map(Some).unwrap_or_else(|e| {
log::debug!(target: "smlib", "union history: {e}");
None
}),
_ => None,
};
Ok(body.with_checked_history(history, &record::counts(bodies)))
}
/// An error unless SMLib's check accepts `r`, the result of combining `operands`.
fn checked(r: &solidcraft_smlib::Brep, operands: &[&solidcraft_smlib::Brep]) -> Result<()> {
// Solids touching each other along an edge or at a vertex come back as one non-manifold
// body, which the export splits into separate solids: SMLib's check is of those, imported
// as given (its check of the touching arrangement itself refuses the radial order of
@@ -46,29 +94,26 @@ mod smlib {
let split = solidcraft_smlib::Brep::from_record(&record, true).map_err(|e| failed("the separate solids", e))?;
split.check(CheckLevel::All).map_err(|e| failed("check", e))?
};
if !check.is_valid() {
// Whether an operand failed it already says whose the fault is.
let operands: Vec<&str> = [("the first operand", &sa), ("the second operand", &sb)]
.into_iter()
.filter(|(_, s)| s.check(CheckLevel::All).is_ok_and(|c| !c.is_valid()))
.map(|(name, _)| name)
.collect();
let what = if operands.is_empty() {
"SMLib's check refuses the result".to_string()
} else {
format!("SMLib's check refuses the result ({} already fails it)", operands.join(" and "))
};
return Err(failed(&what, check));
if check.is_valid() {
return Ok(());
}
let (body, or) = body_from_smlib_ordered(&r)?;
let history = match (oa, ob, or) {
(Some(oa), Some(ob), Some(or)) => r.history().map_err(|e| failed("history", e))?.history(&[oa, ob], &or).map(Some).unwrap_or_else(|e| {
log::debug!(target: "smlib", "boolean history: {e}");
None
}),
_ => None,
// Whether an operand failed it already says whose the fault is.
let failing: Vec<String> = operands
.iter()
.enumerate()
.filter(|(_, s)| s.check(CheckLevel::All).is_ok_and(|c| !c.is_valid()))
.map(|(i, _)| match (operands.len(), i) {
(2, 0) => "the first operand".to_string(),
(2, _) => "the second operand".to_string(),
_ => format!("operand {i}"),
})
.collect();
let what = if failing.is_empty() {
"SMLib's check refuses the result".to_string()
} else {
format!("SMLib's check refuses the result ({} already fails it)", failing.join(" and "))
};
Ok(Some(with_history(body, history, a, b)))
Err(failed(&what, check))
}
/// `body` with `history` where it describes `body` made from `a` and `b`.