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