Kernel: a validation mode decides when sweeps, booleans and blends are checked

Validation::EachStep checks every solid sweep, boolean and blend result
in SMLib as before; EachFeature leaves those checks to the end of the
feature and says a check was left; Off skips them. The evaluation installs
the mode around a feature (validating), as watch installs a watch; outside
it every step is checked as always, and the kernel's worker threads run
under the caller's mode.

Under EachFeature a body decoded from a step keeps that step's SMLib
result (for its own record only), and check_kept checks it as the step's
own check would have, then lets it go; any other kept body is checked as
SMLib imports its record. The record can pass where the result fails: a
fillet's surface of revolution whose polar curve SMLib's check refuses
(fillets/1080-fillet-sloped-basin-1-mm-rim-r0-5-4-edges) is rebuilt on
import, and its round meets a wall at 72 degrees; checking the record
alone kept that round.

The largest-size search of a blend checks every candidate as it is built,
whatever the mode: a candidate fits only as a body the check accepts.
Surface operations and stitching keep their checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-30 09:36:05 -04:00
committed by jyviko
parent 051a7b714c
commit 4030b795c4
8 changed files with 354 additions and 46 deletions
+3 -1
View File
@@ -502,7 +502,9 @@ fn largest_that_fits(size: f64, blend: impl Fn(f64) -> Result<Body>) -> Result<O
break;
}
let mid = lo + (hi - lo) / 2;
match blend(mid as f64 * q) {
// A candidate fits only as a body SMLib's check accepts: each is checked as it is built,
// whatever the evaluation's validation mode (`crate::validation`).
match crate::validation::validating(crate::Validation::EachStep, || blend(mid as f64 * q)).0 {
Ok(b) => {
lo = mid;
best = Some(b);
+21 -7
View File
@@ -357,12 +357,20 @@ mod imp {
SmError::NoRoom => KernelError::TooLarge(format!("the {what} is larger than the faces beside its edges allow")),
e => failed(what, "operation", e),
})?;
let check = r.check(CheckLevel::All).map_err(|e| failed(what, "check", e))?;
if !check.is_valid() {
let input_fails = sb.check(CheckLevel::All).is_ok_and(|c| !c.is_valid());
let step =
if input_fails { "SMLib's check refuses the result (the input already fails it)" } else { "SMLib's check refuses the result" };
return Err(failed(what, step, check));
// Checked here unless the evaluation's validation mode leaves it to the feature's end
// (`crate::validation`).
let step = crate::validation::step();
if step == crate::validation::Step::Check {
let check = r.check(CheckLevel::All).map_err(|e| failed(what, "check", e))?;
if !check.is_valid() {
let input_fails = sb.check(CheckLevel::All).is_ok_and(|c| !c.is_valid());
let step = if input_fails {
"SMLib's check refuses the result (the input already fails it)"
} else {
"SMLib's check refuses the result"
};
return Err(failed(what, step, check));
}
}
let (out, result_order) = body_from_smlib_ordered(&r)?;
let history = match (order, &result_order) {
@@ -380,7 +388,13 @@ mod imp {
},
_ => None,
};
Ok((out.with_checked_history(history, &record::counts(&[body])).inherit_regularization_protection(body), extra, result_order))
let out = out.with_checked_history(history, &record::counts(&[body])).inherit_regularization_protection(body);
let out = if step == crate::validation::Step::Defer {
out.left_unchecked(&format!("SMLib {what}"), crate::validation::Checked::AsIs, r)
} else {
out
};
Ok((out, extra, result_order))
})
}
+12
View File
@@ -55,6 +55,10 @@ pub struct Body {
/// Meshes of this record kept for reuse, set by [`Body::keeping_meshes`]: a check that
/// intersects one large body with many small ones meshes it at the same tolerance each time.
pub(crate) meshes: Option<std::sync::Arc<MeshMemo>>,
/// The SMLib result this body was decoded from, when the step that made it left SMLib's
/// check to the end of its feature ([`crate::validation`]): checked there as the step's own
/// check would have, then let go. Answers only for this record.
pub(crate) unchecked: Option<std::sync::Arc<crate::validation::Unchecked>>,
}
/// The meshes made of one record (see [`Body::keeping_meshes`]), by tolerance and angle. They
@@ -473,9 +477,17 @@ impl Body {
history: None,
display_of: None,
meshes: None,
unchecked: None,
}
}
/// This body with `result`, the SMLib result of the step `what` it was decoded from, whose
/// check that step left to the end of its feature ([`crate::validation::check_kept`]).
pub(crate) fn left_unchecked(self, what: &str, how: crate::validation::Checked, result: solidcraft_smlib::Brep) -> Body {
let unchecked = crate::validation::Unchecked::new(&self.record, what, how, result);
Body { unchecked: Some(std::sync::Arc::new(unchecked)), ..self }
}
/// The body of a schema 4 record (a solid, or a surface body of open shells): checked for
/// structure (references, loops, regions, canonical order; a record from a file is checked
/// against the file budgets where it is read). Its faces face as the
+2
View File
@@ -151,6 +151,7 @@ mod topo;
mod tspline;
#[cfg(test)]
mod untouched_faces_tests;
mod validation;
mod validity;
pub mod watch;
@@ -232,6 +233,7 @@ pub use sweep_path::{RailFit, SweepControl, SweepOrientation, sweep_path, sweep_
pub use topo::{CylinderFace, cylinder_end_at, cylinder_face, cylinder_face_at, material_in_ring};
pub use topo::{TopoCounts, merged_topology, seam_flags};
pub use tspline::tspline_sheet;
pub use validation::{Validation, check_kept, validating};
pub use validity::{SmlibCheck, Validity, smlib_built, smlib_dump_dir, validity};
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
+8 -3
View File
@@ -8,7 +8,9 @@ use crate::{Result, guard};
/// thread. Waking the pool's workers costs tens of microseconds, so callers ask for it only
/// when the items carry more work than that (decided from the work, not the item count).
pub(crate) fn map<T: Sync, R: Send>(what: &'static str, items: &[T], parallel: bool, f: impl Fn(&T) -> Result<R> + Sync) -> Vec<Result<R>> {
let one = |item: &T| guard(what, || f(item));
// The workers run under the caller's validation mode (`crate::validation`).
let scope = crate::validation::current();
let one = |item: &T| crate::validation::within(scope.clone(), || guard(what, || f(item)));
// The browser build has no worker pool: everything runs on the calling thread.
#[cfg(target_arch = "wasm32")]
let _ = parallel;
@@ -53,7 +55,8 @@ pub fn map_with_local_pools<T: Sync, R: Send>(items: &[T], max_threads: usize, f
let _ = cell.set(pool);
}
let pool = cell.get().ok_or_else(|| crate::KernelError::Failed("missing mesh worker".into()))?;
pool.install(|| guard("mesh worker", || f(item)))
let scope = crate::validation::current();
pool.install(|| crate::validation::within(scope, || guard("mesh worker", || f(item))))
})
};
#[cfg(target_arch = "wasm32")]
@@ -67,7 +70,9 @@ pub fn map_with_local_pools<T: Sync, R: Send>(items: &[T], max_threads: usize, f
/// `f` over `items` as [`for_each_on_threads`] runs it, results in item order (`None` where an
/// item panicked).
pub fn map_on_threads<T: Sync, R: Send>(items: &[T], max_threads: usize, f: impl Fn(&T) -> R + Sync) -> Vec<Option<R>> {
let one = |item: &T| guard("parallel item", || Ok(f(item))).ok();
// The threads run under the caller's validation mode (`crate::validation`).
let scope = crate::validation::current();
let one = |item: &T| crate::validation::within(scope.clone(), || guard("parallel item", || Ok(f(item))).ok());
#[cfg(not(target_arch = "wasm32"))]
{
use std::sync::atomic::{AtomicUsize, Ordering};
+21 -15
View File
@@ -13,6 +13,7 @@ mod smlib {
use crate::history::History;
use crate::native::{body_from_smlib_ordered, smlib_body_ordered};
use crate::ops::BoolOp;
use crate::validation::{Checked, Step};
use crate::{KernelError, Result, record};
fn failed(what: &str, e: impl std::fmt::Display) -> KernelError {
@@ -34,7 +35,7 @@ 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 step = 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| {
@@ -43,7 +44,7 @@ mod smlib {
}),
_ => None,
};
Ok(Some(with_history(body, history, a, b)))
Ok(Some(left(with_history(body, history, a, b), step, r)))
}
/// The union of `bodies` in one SMLib call, each tagged as its own operand, checked once:
@@ -68,7 +69,7 @@ mod smlib {
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 step = 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| {
@@ -77,25 +78,30 @@ mod smlib {
}),
_ => None,
};
Ok(body.with_checked_history(history, &record::counts(bodies)))
Ok(left(body.with_checked_history(history, &record::counts(bodies)), step, r))
}
/// An error unless SMLib's check accepts `r`, the result of combining `operands`.
fn checked(r: &solidcraft_smlib::Brep, operands: &[&solidcraft_smlib::Brep]) -> Result<()> {
/// `body`, decoded from `r`, keeping `r` for the check at the end of its feature when the
/// step left it there.
fn left(body: Body, step: Step, r: solidcraft_smlib::Brep) -> Body {
if step == Step::Defer { body.left_unchecked("SMLib boolean", Checked::Combined, r) } else { body }
}
/// An error unless SMLib's check accepts `r`, the result of combining `operands` (checked
/// here unless the evaluation's validation mode leaves it to the feature's end,
/// `crate::validation`): what was done with the check.
fn checked(r: &solidcraft_smlib::Brep, operands: &[&solidcraft_smlib::Brep]) -> Result<Step> {
let step = crate::validation::step();
if step != Step::Check {
return Ok(step);
}
// 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
// faces round an edge they share).
let manifold = r.is_manifold_solid().map_err(|e| failed("check", e))?;
let check = if manifold {
r.check(CheckLevel::All).map_err(|e| failed("check", e))?
} else {
let record = r.to_native().map_err(|e| failed("export", e))?;
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))?
};
let check = crate::validation::check_result(r, Checked::Combined).map_err(|e| KernelError::Failed(format!("SMLib boolean: {e}")))?;
if check.is_valid() {
return Ok(());
return Ok(step);
}
// Whether an operand failed it already says whose the fault is.
let failing: Vec<String> = operands
+26 -20
View File
@@ -155,20 +155,26 @@ mod imp {
segs.iter().map(|s| curve(plane, s)).collect()
}
/// The kernel body of an SMLib result SMLib's own check finds valid.
fn body_of(brep: &Brep, what: &str) -> Result<Body> {
let check = brep.check(CheckLevel::All).map_err(|e| failed(what, e))?;
if !check.is_valid() {
return Err(failed(what, format!("SMLib's check refuses the result: {check}")));
/// The kernel body of an SMLib result SMLib's own check finds valid (checked here unless the
/// evaluation's validation mode leaves it to the feature's end, `crate::validation`).
fn body_of(brep: Brep, what: &str) -> Result<Body> {
use crate::validation::{Checked, Step};
let step = crate::validation::step();
if step == Step::Check {
let check = brep.check(CheckLevel::All).map_err(|e| failed(what, e))?;
if !check.is_valid() {
return Err(failed(what, format!("SMLib's check refuses the result: {check}")));
}
}
crate::native::body_from_smlib(brep)
let body = crate::native::body_from_smlib(&brep)?;
Ok(if step == Step::Defer { body.left_unchecked(&format!("SMLib {what}"), Checked::AsIs, brep) } else { body })
}
/// The body (a solid or a sheet) a schema 4 record describes, taken by SMLib as a
/// modeller's own record (no healing) and checked there.
pub(crate) fn body_from_record(record: &solidcraft_geom::brep::Brep, what: &str) -> Result<Body> {
let brep = Brep::from_record(record, true).map_err(|e| failed(what, e))?;
body_of(&brep, what)
body_of(brep, what)
}
/// The sketch segments `segs` on `plane` projected along `direction` onto the face of
@@ -236,7 +242,7 @@ mod imp {
/// a sphere revolves.
pub(crate) fn sphere(center: Vec3, radius: f64) -> Result<Body> {
let brep = Brep::make_sphere(p(center), radius).map_err(|e| failed("sphere", e))?;
let body = body_of(&brep, "sphere")?;
let body = body_of(brep, "sphere")?;
let faces = body.face_count();
Ok(record::attach(body, &[], Some(vec![record::profile(&[0usize]); faces]), &[]))
}
@@ -415,7 +421,7 @@ mod imp {
Brep::extrude(&curves(&knee, pieces)?, p(dir), distance - foot, lean, corner(d.corner), true).map_err(|e| failed("extrude", e))?;
let brep = prism.boolean(&slope, solidcraft_smlib::BooleanOp::Union).map_err(|e| failed("extrude", e))?;
let brep = brep.ok_or_else(|| failed("extrude", "nothing is left"))?;
let body = body_of(&brep, "extrude")?;
let body = body_of(brep, "extrude")?;
// The caps and the foot's walls from the base; the sloping walls (and the foot's again)
// from the edges at the knee, which both stand on.
let low = roles(&body, &base, Some(&top), &edges, &[], false)?;
@@ -438,7 +444,7 @@ mod imp {
let lean = draft.map_or(0.0, |d| -d.taper);
let rule = draft.map_or(DraftCorner::default(), |d| corner(d.corner));
let brep = Brep::extrude(&c, p(dir), distance, lean, rule, true).map_err(|e| failed("extrude", e))?;
let body = body_of(&brep, "extrude")?;
let body = body_of(brep, "extrude")?;
// Half-way along a straight wall lies over its segment; a drafted one has moved.
let inside = if draft.is_none() { seeds(&base, pieces, |q| q + dir * (distance * 0.5)) } else { Vec::new() };
let roles = roles(&body, &base, Some(&top), &edges, &inside, false)?;
@@ -495,7 +501,7 @@ mod imp {
.map_err(|e| failed("extrude", e))?
.ok_or_else(|| KernelError::Invalid("the draft leaves nothing of the profile".into()))?;
}
let body = body_of(&brep, "extrude")?;
let body = body_of(brep, "extrude")?;
let base = plane.offset(from);
let top = plane.offset(from + sign.signum() * distance);
let low = roles(&body, &base, Some(&top), &seeds(&base, all, |q| q), &[], false)?;
@@ -574,7 +580,7 @@ mod imp {
let turn = turn.min(std::f64::consts::TAU);
let c = curves(plane, pieces)?;
let brep = Brep::revolve(&c, p(origin), p(k), turn, true).map_err(|e| failed("revolve", e))?;
let body = body_of(&brep, "revolve")?;
let body = body_of(brep, "revolve")?;
let size = body.size().max(1e-9);
let off_axis = |q: Vec3| {
let w = q - origin;
@@ -639,7 +645,7 @@ mod imp {
return cornered(plane, pieces, path, &runs, translate);
}
let (brep, [_, middle, last]) = swept(plane, pieces, path, translate)?;
let body = body_of(&brep, "sweep")?;
let body = body_of(brep, "sweep")?;
// The walls known by the profile carried half way (past the start's edges: a wall the
// start touches by no edge, as round a closed path, and walls along later path segments).
let inside = seeds(plane, pieces, |q| apply(&middle, q));
@@ -852,7 +858,7 @@ mod imp {
}
}
let brep = body.ok_or_else(|| KernelError::Invalid("empty path".into()))?;
let body = body_of(&brep, "sweep")?;
let body = body_of(brep, "sweep")?;
let roles = roles(&body, plane, (!closed).then_some(&pose), &seeds(plane, pieces, |q| q), &inside, false)?;
with_history(body, &roles, pieces)
}
@@ -1015,7 +1021,7 @@ mod imp {
})
.collect();
let brep = Brep::sweep_stations(&c, &maps, if closed { Ends::Joined } else { Ends::Capped }).map_err(|e| failed("sweep", e))?;
let body = body_of(&brep, "sweep")?;
let body = body_of(brep, "sweep")?;
let mid = st.get(st.len() / 2).copied().unwrap_or(*last);
let inside = seeds(plane, &pieces, |q| mid.at + mid.apply(q - s0));
let roles = if closed {
@@ -1227,7 +1233,7 @@ mod imp {
crate::sweep_path::closing_offset(plane, l, &[to_station(&IDENTITY), to_station(&at_start)])?;
}
}
let body = body_of(&brep, "sweep")?;
let body = body_of(brep, "sweep")?;
let roles = if closed {
roles(&body, &plane.offset(size * 10.0), None, &[], &inside, false)?
} else {
@@ -1382,7 +1388,7 @@ mod imp {
/// across them; `sections` and `first` as [`loft`] takes them name the faces.
fn loft_through(sections: &[(Plane, Vec<Piece>)], first: usize, curves: &[Vec<Curve>], degree: u32) -> Result<Body> {
let brep = Brep::loft(curves, degree, true).map_err(|e| failed("loft", e))?;
let body = body_of(&brep, "loft")?;
let body = body_of(brep, "loft")?;
let (Some((start, lead)), Some(last)) = (sections.get(first), sections.get(if first == 0 { sections.len().saturating_sub(1) } else { 0 }))
else {
return Err(KernelError::Invalid("loft sections".into()));
@@ -1442,7 +1448,7 @@ mod imp {
pub(crate) fn loft_to_point(plane: &Plane, pieces: &[Piece], apex: Vec3) -> Result<Body> {
let c = curves(plane, pieces)?;
let brep = Brep::loft_to_point(&c, p(apex)).map_err(|e| failed("loft to a point", e))?;
let body = body_of(&brep, "loft to a point")?;
let body = body_of(brep, "loft to a point")?;
let roles = roles(&body, plane, None, &seeds(plane, pieces, |q| q), &[], false)?;
with_history(body, &roles, pieces)
}
@@ -1454,7 +1460,7 @@ mod imp {
// Walls within a ten-thousandth of the pitch of the true screw surfaces.
let tolerance = (pitch.abs() * 1e-4).clamp(1e-6, 1e-2);
let brep = Brep::screw_sweep(&c, p(origin), p(axis), pitch, turns, tolerance).map_err(|e| failed("screw sweep", e))?;
let body = body_of(&brep, "screw sweep")?;
let body = body_of(brep, "screw sweep")?;
let roles = roles(&body, plane, None, &seeds(plane, pieces, |q| q), &[], true)?;
with_history(body, &roles, pieces)
}
@@ -1474,7 +1480,7 @@ mod imp {
let tolerance = (pitch.abs() * 1e-4).clamp(1e-6, 1e-2);
let brep =
Brep::screw_sweep_radial(&c, p(origin), p(axis), pitch, turns, law, tolerance).map_err(|e| failed("screw sweep with a radial law", e))?;
let body = body_of(&brep, "screw sweep")?;
let body = body_of(brep, "screw sweep")?;
let roles = roles(&body, plane, None, &seeds(plane, pieces, |q| q), &[], true)?;
with_history(body, &roles, pieces)
}
+261
View File
@@ -0,0 +1,261 @@
//! How often SMLib's full check runs while a model is evaluated. Every sweep, boolean and blend
//! checks its result in SMLib before it is decoded ([`Validation::EachStep`]); an evaluation can
//! instead leave those checks to one check of the bodies each feature keeps
//! ([`Validation::EachFeature`]), or skip them ([`Validation::Off`]). A body kept straight from
//! a step is checked at the end as that step would have checked it, on the step's own result
//! (kept with the body until then): the record a result is decoded to can pass SMLib's check
//! where the result itself fails it (a surface of revolution's polar curve, rebuilt on import).
//!
//! The evaluation installs its mode around a feature with [`validating`], as [`crate::watch`]
//! installs a watch: the operations a feature runs are reached through hundreds of public kernel
//! functions, and the mode is the evaluation's, not each operation's. Outside [`validating`]
//! (the kernel called directly, a preview, a test) every step is checked, as it always was. The
//! worker threads the kernel starts for a step run under the same mode ([`crate::par`]).
use std::cell::RefCell;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use serde::{Deserialize, Serialize};
use crate::body::Body;
use crate::{KernelError, Result};
/// When SMLib's full check of a result runs during a model evaluation.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Validation {
/// After every sweep, boolean and blend: a step whose result fails the check fails the
/// feature there (how every evaluation checked before the mode existed).
EachStep,
/// Once per feature: the steps inside a feature are not checked, and each body the feature
/// makes or changes is checked before it is kept, so a feature that makes a body the check
/// refuses still fails, with the check's reason.
#[default]
EachFeature,
/// Never (experimental): a body the check would refuse is kept.
Off,
}
impl Validation {
pub const ALL: [Validation; 3] = [Validation::EachStep, Validation::EachFeature, Validation::Off];
/// The mode's name: `each_step`, `each_feature` or `off` (as it is saved and sent).
pub fn id(self) -> &'static str {
match self {
Validation::EachStep => "each_step",
Validation::EachFeature => "each_feature",
Validation::Off => "off",
}
}
/// The mode named `id` ([`Validation::id`]), `None` for anything else.
pub fn parse(id: &str) -> Option<Validation> {
Validation::ALL.into_iter().find(|v| v.id() == id)
}
}
/// One feature's work under a mode, shared with the worker threads it starts.
#[derive(Debug)]
pub(crate) struct Scope {
mode: Validation,
/// A step's check was left for the end of the feature ([`Validation::EachFeature`]).
deferred: AtomicBool,
}
thread_local! {
static CURRENT: RefCell<Option<Arc<Scope>>> = const { RefCell::new(None) };
}
/// Run `f` with `mode` deciding which checks the kernel work it does on this thread runs. Also
/// gives whether a step's check was left for later under [`Validation::EachFeature`]: the
/// caller then checks the bodies it keeps ([`check_kept`]).
pub fn validating<R>(mode: Validation, f: impl FnOnce() -> R) -> (R, bool) {
let scope = Arc::new(Scope { mode, deferred: AtomicBool::new(false) });
let r = within(Some(scope.clone()), f);
(r, scope.deferred.load(Ordering::Relaxed))
}
/// The scope this thread's work runs in, to hand to the worker threads it starts.
pub(crate) fn current() -> Option<Arc<Scope>> {
CURRENT.with(|c| c.borrow().clone())
}
/// Run `f` in `scope` (one [`current`] gave on another thread), restoring this thread's own
/// afterwards, also when `f` panics.
pub(crate) fn within<R>(scope: Option<Arc<Scope>>, f: impl FnOnce() -> R) -> R {
struct Restore(Option<Option<Arc<Scope>>>);
impl Drop for Restore {
fn drop(&mut self) {
if let Some(previous) = self.0.take() {
CURRENT.with(|c| *c.borrow_mut() = previous);
}
}
}
let previous = CURRENT.with(|c| c.replace(scope));
let _restore = Restore(Some(previous));
f()
}
/// What a step does with SMLib's check of its result ([`step`]).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Step {
/// Check it now (outside [`validating`], and under [`Validation::EachStep`]).
Check,
/// Leave it to the end of the feature (under [`Validation::EachFeature`]): the body decoded
/// from the result keeps it ([`Body::left_unchecked`]) for [`check_kept`].
Defer,
/// Never check it (under [`Validation::Off`]).
Skip,
}
/// What the step about to check its result does. [`Step::Defer`] also tells the feature a check
/// was left for its end.
pub(crate) fn step() -> Step {
CURRENT.with(|c| match c.borrow().as_deref() {
None | Some(Scope { mode: Validation::EachStep, .. }) => Step::Check,
Some(s @ Scope { mode: Validation::EachFeature, .. }) => {
s.deferred.store(true, Ordering::Relaxed);
Step::Defer
}
Some(Scope { mode: Validation::Off, .. }) => Step::Skip,
})
}
/// How a step's own check takes its result.
#[derive(Clone, Copy, Debug)]
pub(crate) enum Checked {
/// As it is.
AsIs,
/// A boolean's: solids touching each other along an edge or at a vertex come back as one
/// non-manifold body, checked as the separate solids its export holds (the boolean's rule).
Combined,
}
/// A step's SMLib result whose check was left to the end of its feature: kept with the body
/// decoded from it (and only for that body's record) until [`check_kept`] takes it, so the check
/// at the end is the step's own, of the result the step made.
#[derive(Debug)]
pub(crate) struct Unchecked {
record: std::sync::Weak<solidcraft_geom::brep::Brep>,
/// The step, as its own refusal names it (`SMLib fillet`).
what: String,
how: Checked,
result: std::sync::Mutex<Option<solidcraft_smlib::Brep>>,
}
impl Unchecked {
pub(crate) fn new(record: &Arc<solidcraft_geom::brep::Brep>, what: &str, how: Checked, result: solidcraft_smlib::Brep) -> Unchecked {
Unchecked { record: Arc::downgrade(record), what: what.to_string(), how, result: std::sync::Mutex::new(Some(result)) }
}
/// The step's result, taken (once), when this is `record`'s.
fn take(&self, record: &Arc<solidcraft_geom::brep::Brep>) -> Option<solidcraft_smlib::Brep> {
if !std::ptr::eq(self.record.as_ptr(), Arc::as_ptr(record)) {
return None;
}
self.result.lock().ok().and_then(|mut r| r.take())
}
}
/// SMLib's full check of `r` as a step takes it (`how`): the check's report.
pub(crate) fn check_result(r: &solidcraft_smlib::Brep, how: Checked) -> std::result::Result<solidcraft_smlib::CheckReport, String> {
fn failed(what: &'static str) -> impl Fn(&dyn std::fmt::Display) -> String {
move |e| format!("{what}: {e}")
}
match how {
Checked::Combined if !r.is_manifold_solid().map_err(|e| failed("check")(&e))? => {
let record = r.to_native().map_err(|e| failed("export")(&e))?;
let split = solidcraft_smlib::Brep::from_record(&record, true).map_err(|e| failed("the separate solids")(&e))?;
split.check(solidcraft_smlib::CheckLevel::All).map_err(|e| failed("check")(&e))
}
_ => r.check(solidcraft_smlib::CheckLevel::All).map_err(|e| failed("check")(&e)),
}
}
/// SMLib's full check of a body a feature keeps, once a step of the feature left its check to
/// the end: an error with the check's reason unless it passes. A body decoded from a step's
/// result is checked as that step would have checked it, on its result; any other (a copy moved
/// into place, pieces joined without an operation) as SMLib takes it for the operations after it.
/// A mesh body has nothing to check.
pub fn check_kept(body: &Body) -> Result<()> {
if body.is_mesh() {
return Ok(());
}
let step = body.unchecked.as_deref().and_then(|u| u.take(&body.record).map(|r| (u, r)));
let (what, check) = match &step {
Some((u, r)) => (u.what.as_str(), check_result(r, u.how)),
None => ("SMLib", body.smlib()?.check(solidcraft_smlib::CheckLevel::All).map_err(|e| format!("check: {e}"))),
};
match check {
Ok(c) if c.is_valid() => Ok(()),
Ok(c) => Err(KernelError::Failed(format!("{what}: SMLib's check refuses the result: {c}"))),
Err(e) => Err(KernelError::Failed(format!("{what}: {e}"))),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_step_is_checked_outside_an_evaluation_and_under_each_step() {
assert_eq!(step(), Step::Check);
assert_eq!(validating(Validation::EachStep, step), (Step::Check, false));
assert_eq!(validating(Validation::EachFeature, step), (Step::Defer, true));
assert_eq!(validating(Validation::Off, step), (Step::Skip, false));
// The thread's own mode comes back, also after a nested scope.
let ((inner, outer), deferred) = validating(Validation::Off, || (validating(Validation::EachStep, step).0, step()));
assert!(inner == Step::Check && outer == Step::Skip && !deferred);
assert_eq!(step(), Step::Check);
}
#[test]
fn the_mode_reaches_the_kernels_worker_threads() {
let items = [0, 1, 2, 3];
let (checked, deferred) = validating(Validation::EachFeature, || {
crate::par::map("validation", &items, true, |_| Ok(step())).into_iter().collect::<Result<Vec<Step>>>()
});
assert_eq!(checked.ok(), Some(vec![Step::Defer; 4]));
assert!(deferred);
let (on_threads, deferred) = validating(Validation::EachFeature, || crate::par::map_on_threads(&items, 4, |_| step()));
assert_eq!(on_threads, vec![Some(Step::Defer); 4]);
assert!(deferred);
}
/// A box with one corner moved off its three edges: SMLib's check refuses it. Under each_step
/// the boolean that takes it refuses its result there; under each_feature it is not checked,
/// says the check was left for later, and the check of the body kept refuses it.
#[cfg(feature = "smlib")]
#[test]
fn a_body_the_check_refuses_is_refused_at_the_step_or_at_the_end() {
use solidcraft_geom::Vec3;
let good = crate::box_solid(Vec3::new(0.0, 0.0, 0.0), Vec3::new(10.0, 10.0, 10.0)).unwrap();
assert!(check_kept(&good).is_ok());
let mut record = good.record().clone();
record.vertices[0].point[2] += 0.5;
let bad = Body::from_record(record).unwrap();
let e = check_kept(&bad).unwrap_err().to_string();
assert!(e.contains("SMLib's check refuses the result"), "{e}");
let tool = crate::box_solid(Vec3::new(5.0, 5.0, 5.0), Vec3::new(20.0, 20.0, 20.0)).unwrap();
let (step, deferred) = validating(Validation::EachStep, || crate::boolean(&bad, &tool, crate::BoolOp::Union));
let e = step.unwrap_err().to_string();
assert!(e.contains("SMLib's check refuses the result (the first operand already fails it)") && !deferred, "{e}");
let (joined, deferred) = validating(Validation::EachFeature, || crate::boolean(&bad, &tool, crate::BoolOp::Union));
assert!(deferred);
let joined = joined.unwrap().unwrap();
assert!(check_kept(&joined).unwrap_err().to_string().contains("SMLib's check refuses the result"));
let (off, deferred) = validating(Validation::Off, || crate::boolean(&bad, &tool, crate::BoolOp::Union));
assert!(off.is_ok_and(|b| b.is_some()) && !deferred);
}
#[test]
fn modes_are_named_as_they_are_saved() {
for v in Validation::ALL {
assert_eq!(Validation::parse(v.id()), Some(v));
assert_eq!(serde_json::to_value(v).ok(), Some(serde_json::json!(v.id())));
}
assert_eq!(Validation::parse("each step"), None);
assert_eq!(Validation::default(), Validation::EachFeature);
}
}