mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 02:25:55 +00:00
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:
@@ -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,
|
||||
|
||||
@@ -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()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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`.
|
||||
|
||||
Reference in New Issue
Block a user