Compare commits

...

3 Commits

Author SHA1 Message Date
Brennen Herbruck b08773ea16 Merge remote-tracking branch 'origin/main' into cancel-check 2026-11-10 12:54:19 -05:00
Brennen Herbruck d38e8e39b2 Cancel stops SMLib's full check and the tight box (solidcore#3247)
The full validity check of a feature's result was most of a long boolean's
time (64% of #3962's 20-turn coil cut) and ran with no token: Cancel left it
running for seconds and it returned a result. solidcore now polls the token
once per entity in the check's topology walk, in the coincident-topology
search and in the tight bounding box.

SolidCraft runs the check and the box under the work's token where the body
is the call's own: the step result or copy a deferred feature check takes
(which now carries the token of the work that detached it onto its thread),
and a copy measured for the tight box. A call on a body others read still
runs to its end. A stopped smlib call returns the typed Error::Cancelled and
never a result solidcore finished first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 12:54:06 -05:00
Brennen Herbruck c5a33b2f71 solidcore-ops: cancel ops calls with a scoped token (solidcore#3247)
solidcore_ops::cancel::within runs work under a CancelToken. Every ops call
it makes on that thread runs with the token on the session's kernel, whose
long loops already poll it; once cancelled, the running call stops at the
kernel's next check and later calls are refused before they wait for the
kernel, each with Error::Cancelled.

A cancelled call leaves no partial state: an edited body is put back by the
session's rollback, no result is returned even if the kernel finished
first, the kernel is replaced as after a panic, and nothing reaches the
failure callback. Nested scopes stop with either token
(CancelToken::either in the kernel).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 11:25:19 -05:00
13 changed files with 226 additions and 19 deletions
+8 -1
View File
@@ -1046,7 +1046,14 @@ impl Body {
if let Some(b) = *slot {
return Ok(b);
}
let (lo, hi) = self.smlib()?.bounds().map_err(|e| KernelError::smlib("SMLib bounds", e))?;
// Under a token (an evaluation that can be cancelled), measured on a copy the token can
// stop part way: a large body's tight box takes a good part of a second, and a call
// stopped part way may leave half-built caches on the body it reads.
let measured = match solidcraft_smlib::stop::current() {
Some(_) => self.smlib()?.try_clone().and_then(solidcraft_smlib::Brep::bounds_owned),
None => self.smlib()?.bounds(),
};
let (lo, hi) = measured.map_err(|e| KernelError::smlib("SMLib bounds", e))?;
let b = Aabb3 { min: v3(lo), max: v3(hi) };
*slot = Some(b);
Ok(b)
+52 -10
View File
@@ -183,8 +183,8 @@ pub fn check_kept(body: &Body) -> Result<()> {
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)),
let (what, check) = match step {
Some((u, r)) => (u.what.as_str(), check_result_owned(r, u.how)),
None => ("SMLib", body.smlib()?.check(solidcraft_smlib::CheckLevel::All).map_err(|e| format!("check: {e}"))),
};
verdict(what, check)
@@ -199,8 +199,26 @@ fn verdict(what: &str, check: std::result::Result<solidcraft_smlib::CheckReport,
}
}
/// [`check_kept`] of a body, ready to run on another thread ([`detach_kept`]).
pub struct KeptCheck(Kept);
/// [`check_result`] of a step's result nobody else reads, which it takes: the token over this
/// thread's work stops it ([`solidcraft_smlib::Brep::check_owned`]).
fn check_result_owned(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))?;
drop(r);
let split = solidcraft_smlib::Brep::from_record(&record, true).map_err(|e| failed("the separate solids")(&e))?;
split.check_owned(solidcraft_smlib::CheckLevel::All).map_err(|e| failed("check")(&e))
}
_ => r.check_owned(solidcraft_smlib::CheckLevel::All).map_err(|e| failed("check")(&e)),
}
}
/// [`check_kept`] of a body, ready to run on another thread ([`detach_kept`]), under the token
/// of the work that detached it: stopping that work stops the check too.
pub struct KeptCheck(Kept, Option<solidcraft_smlib::stop::Stop>);
enum Kept {
/// A mesh body: nothing to check.
@@ -215,22 +233,31 @@ enum Kept {
/// form is copied now, so the check reads nothing the operations after it use and can run on
/// another thread beside them ([`KeptCheck::run`]).
pub fn detach_kept(body: &Body) -> KeptCheck {
let stop = solidcraft_smlib::stop::current();
if body.is_mesh() {
return KeptCheck(Kept::Nothing);
return KeptCheck(Kept::Nothing, stop);
}
if let Some((u, result)) = body.unchecked.as_deref().and_then(|u| u.take(&body.record).map(|r| (u, r))) {
return KeptCheck(Kept::Step { what: u.what.clone(), how: u.how, result });
return KeptCheck(Kept::Step { what: u.what.clone(), how: u.how, result }, stop);
}
KeptCheck(Kept::Copy(body.smlib().and_then(|b| b.try_clone().map_err(|e| KernelError::smlib("SMLib copy", e)))))
KeptCheck(Kept::Copy(body.smlib().and_then(|b| b.try_clone().map_err(|e| KernelError::smlib("SMLib copy", e)))), stop)
}
impl KeptCheck {
/// The check: [`check_kept`]'s result for the body it was detached from.
pub fn run(self) -> Result<()> {
match self.0 {
let KeptCheck(kept, stop) = self;
solidcraft_smlib::stop::scoped(stop, || kept.run())
}
}
impl Kept {
fn run(self) -> Result<()> {
match self {
Kept::Nothing => Ok(()),
Kept::Step { what, how, result } => verdict(&what, check_result(&result, how)),
Kept::Copy(copy) => verdict("SMLib", copy?.check(solidcraft_smlib::CheckLevel::All).map_err(|e| format!("check: {e}"))),
// The step's result and the copy are the check's own: the work's token stops it.
Kept::Step { what, how, result } => verdict(&what, check_result_owned(result, how)),
Kept::Copy(copy) => verdict("SMLib", copy?.check_owned(solidcraft_smlib::CheckLevel::All).map_err(|e| format!("check: {e}"))),
}
}
}
@@ -251,6 +278,21 @@ mod tests {
assert_eq!(step(), Step::Check);
}
/// A check detached under a token runs under it on whatever thread runs it (the feature
/// check beside the evaluation): stopping the work stops the check (solidcore#3247).
#[test]
fn a_detached_check_stops_with_the_work_that_detached_it() {
let body = crate::cylinder(solidcore_record::Vec3::new(0.0, 0.0, 0.0), solidcore_record::Vec3::Z, 3.0, 10.0).unwrap();
let stop = solidcraft_smlib::stop::Stop::new();
let detached = stop.within(|| detach_kept(&body));
stop.stop();
let r = crate::par::kernel_thread("check").spawn(move || detached.run()).unwrap().join().unwrap();
assert!(r.as_ref().is_err_and(|e| e.to_string().contains("cancelled")), "{r:?}");
let free = solidcraft_smlib::stop::Stop::new();
let detached = free.within(|| detach_kept(&body));
assert!(detached.run().is_ok(), "a token never stopped changes nothing");
}
#[test]
fn the_mode_reaches_the_kernels_worker_threads() {
let items = [0, 1, 2, 3];
+26
View File
@@ -55,12 +55,38 @@ impl Brep {
pub fn check(&self, level: CheckLevel) -> Result<CheckReport> {
core::check(self.core(), level)
}
/// [`Brep::check`] of a body nobody else reads, which it takes: the token over this thread's
/// work ([`crate::stop`]) stops it ([`crate::Error::Cancelled`]). A check left part way may
/// leave the body's caches half-built, so it runs only on a body it consumes.
pub fn check_owned(self, level: CheckLevel) -> Result<CheckReport> {
core::check_with(self.core(), level, true)
}
}
#[cfg(test)]
mod tests {
use super::*;
/// A stopped token stops the check of a body the check owns, and never the check of a body
/// others read (solidcore#3247): stopped part way, it could leave that body's caches half-built.
#[test]
fn a_stopped_token_stops_an_owned_check_and_never_a_shared_one() {
let b = Brep::make_box([0.0; 3], [10.0, 20.0, 30.0]).unwrap();
let mine = b.try_clone().unwrap();
let stop = crate::stop::Stop::new();
stop.stop();
let shared = stop.within(|| b.check(CheckLevel::All));
assert!(shared.is_ok_and(|r| r.is_valid()));
let owned = stop.within(|| mine.check_owned(CheckLevel::All));
assert!(matches!(owned, Err(crate::Error::Cancelled("check"))), "{owned:?}");
let mine = b.try_clone().unwrap();
assert!(mine.check_owned(CheckLevel::All).is_ok_and(|r| r.is_valid()), "no token, no stop");
let mine = b.try_clone().unwrap();
let boxed = stop.within(|| mine.bounds_owned());
assert!(matches!(boxed, Err(crate::Error::Cancelled("bounds"))), "{boxed:?}");
}
#[test]
fn a_box_is_valid_at_every_level() {
let b = Brep::make_box([1.0, 2.0, 3.0], [10.0, 20.0, 30.0]).unwrap();
+27 -3
View File
@@ -53,7 +53,9 @@ pub(crate) mod trim;
mod trim_tests;
pub(crate) use convert::{from_arrays, to_arrays};
pub(crate) use measure::{area, bounds, check, classify_point, edge_face_distance, is_manifold_solid, mass_properties, solid_count};
pub(crate) use measure::{
area, bounds, bounds_with, check, check_with, classify_point, edge_face_distance, is_manifold_solid, mass_properties, solid_count,
};
/// A solidcore body. Its own lock: solidcore's operations take `&mut Brep`, even to measure.
pub(crate) struct Body(pub(crate) Mutex<solidcore::topology::Brep>);
@@ -476,7 +478,7 @@ pub(crate) fn run_on<T: dump::Answer>(
) -> Result<T> {
let stop = crate::stop::token();
if stop.as_ref().is_some_and(CancelToken::is_cancelled) {
return Err(Error::Solidcore(format!("{call}: cancelled")));
return Err(Error::Cancelled(call));
}
call_on(call, inputs, stop, f)
}
@@ -519,9 +521,14 @@ fn call_on<T: dump::Answer>(
};
drop(in_flight);
// Stopped, it is no failure to look into, and solidcore may have left the kernel part way.
// No result either, even one solidcore finished first: the caller never has to ask whether
// it came in time (as solidcore-ops' session answers).
if stop.is_some_and(|s| s.is_cancelled()) {
put_kernel(None);
return r;
return match r {
Err(Error::Internal(e)) => Err(Error::Internal(e)),
_ => Err(Error::Cancelled(call)),
};
}
let why = match &r {
Ok(v) => v.refusal(),
@@ -566,6 +573,23 @@ pub(crate) fn with_body<T: dump::Answer>(
call_on(call, &[("brep", body)], None, |k| f(k, &mut body.0.lock().unwrap_or_else(PoisonError::into_inner)))
}
/// [`with_body`] on a body only this call reads (one the caller consumes): the token over this
/// thread's work stops it, since what a stopped call leaves half-changed is never read again.
pub(crate) fn with_own_body<T: dump::Answer>(
call: &'static str,
body: &Body,
f: impl FnOnce(&mut Kernel, &mut solidcore::topology::Brep) -> std::result::Result<T, solidcore::SolidError>,
) -> Result<T> {
if body.0.is_poisoned() {
return Err(Error::Internal(format!("{call}: the body was left unfinished by an earlier panic")));
}
let stop = crate::stop::token();
if stop.as_ref().is_some_and(CancelToken::is_cancelled) {
return Err(Error::Cancelled(call));
}
call_on(call, &[("brep", body)], stop, |k| f(k, &mut body.0.lock().unwrap_or_else(PoisonError::into_inner)))
}
/// An axis-aligned box from its lower corner (`SmApiCreateBox`, as `smlib_make_box` checks it:
/// every edge length positive).
pub(crate) fn make_box(origin: [f64; 3], size: [f64; 3]) -> Result<Body> {
+19 -4
View File
@@ -159,7 +159,13 @@ pub(crate) fn area(body: &Body, relative_accuracy: f64) -> Result<f64> {
/// `smlib_brep_bounds`: `SmBrep::CalculateTightBoundingBox`.
pub(crate) fn bounds(body: &Body) -> Result<([f64; 3], [f64; 3])> {
with_body("bounds", body, |k, b| {
bounds_with(body, false)
}
/// [`bounds`], stopped by the token over this thread's work when `stoppable` (a body the
/// caller consumes: [`super::with_own_body`]).
pub(crate) fn bounds_with(body: &Body, stoppable: bool) -> Result<([f64; 3], [f64; 3])> {
let run = |k: &mut Kernel, b: &mut solidcore::topology::Brep| {
let x = brep_bounding_box(k, b, true)?;
let (lo, hi) = (x.min(), x.max());
let (lo, hi) = ([lo.x, lo.y, lo.z], [hi.x, hi.y, hi.z]);
@@ -167,7 +173,8 @@ pub(crate) fn bounds(body: &Body) -> Result<([f64; 3], [f64; 3])> {
return Err(invalid("no box"));
}
Ok((lo, hi))
})
};
if stoppable { super::with_own_body("bounds", body, run) } else { with_body("bounds", body, run) }
}
/// `smlib_brep_edge_face_distance`: SMLib's own edge/face gap (`SmEdgeuse::CalcMaxEdgeFaceGap`'s
@@ -303,12 +310,18 @@ impl<T> super::dump::Answer for std::result::Result<T, Refusal> {
/// owners' identities the check cannot know, fails the check with "not supported yet": the check
/// never reports a body valid on fewer tests than the C++ kernel runs.
pub(crate) fn check(body: &Body, level: CheckLevel) -> Result<CheckReport> {
check_with(body, level, false)
}
/// [`check`], stopped by the token over this thread's work when `stoppable` (a body the caller
/// consumes: [`super::with_own_body`]).
pub(crate) fn check_with(body: &Body, level: CheckLevel, stoppable: bool) -> Result<CheckReport> {
let lv = match level {
CheckLevel::Basic => SolidcoreCheckLevel::Level0,
CheckLevel::Geometry => SolidcoreCheckLevel::Level1,
CheckLevel::All => SolidcoreCheckLevel::Level2,
};
let c = match with_body("check", body, |k, b| {
let checked = |k: &mut Kernel, b: &mut solidcore::topology::Brep| {
let naming = b.naming();
if naming == NamingScheme::Unknown {
return Ok(Err(Refusal::Naming(naming)));
@@ -317,7 +330,9 @@ pub(crate) fn check(body: &Body, level: CheckLevel) -> Result<CheckReport> {
Err(SolidError::Unported { upstream, unit }) => Ok(Err(Refusal::Unported(upstream, unit))),
r => r.map(Ok),
}
})? {
};
let ran = if stoppable { super::with_own_body("check", body, checked) } else { with_body("check", body, checked) };
let c = match ran? {
Ok(c) => c,
Err(Refusal::Unported(upstream, unit)) => {
return Err(Error::Solidcore(format!("smlib_brep_check: {upstream}: not supported yet: solidcore has not ported it (unit {unit})")));
+4
View File
@@ -90,6 +90,9 @@ pub enum Error {
/// A step of SMLib solidcore has not ported yet, refused rather than built another way: what
/// is missing, and the solidcore issue that ports it.
Unported(&'static str),
/// The work's token was stopped ([`stop::Stop::stop`]) before or during the call: no result,
/// even one solidcore finished first. Never a failure of the operation on its input.
Cancelled(&'static str),
}
/// Why a blend has no room, as solidcore names its refusal (`NoRoomReason`): the reasons a
@@ -227,6 +230,7 @@ impl std::fmt::Display for Error {
Error::Solidcore(e) => write!(f, "solidcore: {e}"),
Error::Internal(e) => write!(f, "solidcore failed internally in {e}"),
Error::Unported(what) => write!(f, "not supported yet: {what}"),
Error::Cancelled(call) => write!(f, "{call}: cancelled"),
}
}
}
+6
View File
@@ -67,6 +67,12 @@ impl Brep {
core::bounds(self.core())
}
/// [`Brep::bounds`] of a body nobody else reads, which it takes: the token over this
/// thread's work ([`crate::stop`]) stops it ([`crate::Error::Cancelled`]).
pub fn bounds_owned(self) -> Result<([f64; 3], [f64; 3])> {
core::bounds_with(self.core(), true)
}
/// How far edge `edge` (its curve over its interval) lies from the surface of face `face`,
/// over that surface's own domain and not beyond it, in mm: SMLib's own edge/face gap, the
/// one it sizes an edge's tolerance by. Indices are in the body's edge and face order (as
+54
View File
@@ -163,3 +163,57 @@ fn a_rod_cut_by_the_union_of_two_counter_wound_channel_sweeps_is_valid() {
.unwrap();
graded("by the union", &r);
}
/// Cancel stops the full check of a long coil cut (solidcore#3247): a 20-turn square coil cut
/// from a cylinder, the case of tangency-labs/solidcraft#3962, whose check takes seconds (most
/// of the cut's time, in the edges' curve analyses). Cancelled from another thread part way,
/// the check returns `Cancelled` within 500 ms (a few ms on a desktop; the bound leaves room
/// for shared CI runners). nextest runs each test in a process of its own, so no other test's
/// call holds the session's kernel meanwhile.
#[test]
fn the_check_of_a_long_coil_cut_stops_on_cancel() {
use solidcore_ops::cancel::{within, CancelToken};
use solidcore_ops::sweep::{screw_sweep, Curve};
use std::time::{Duration, Instant};
let corners = [
[19.0, 0.0, 12.0],
[32.0, 0.0, 12.0],
[32.0, 0.0, 25.0],
[19.0, 0.0, 25.0],
];
let square: Vec<Curve> = (0..4)
.map(|i| Curve::Line {
start: corners[i],
end: corners[(i + 1) % 4],
})
.collect();
let coil = screw_sweep(
&square,
[0.0; 3],
[0.0, 0.0, 1.0],
15.24,
20.0,
None,
15.24e-4,
)
.unwrap();
let stock = solidcore_ops::make_cylinder([0.0; 3], 20.0, 304.5).unwrap();
let cut = boolean(stock, coil, BooleanOp::Subtract).unwrap().unwrap();
let token = CancelToken::new();
let watched = token.clone();
let worker = std::thread::spawn(move || {
let r = within(&watched, || cut.check(CheckLevel::All));
(r.map(|_| ()), Instant::now())
});
std::thread::sleep(Duration::from_millis(300));
let cancelled = Instant::now();
token.cancel();
let (r, returned) = worker.join().unwrap();
let took = returned.saturating_duration_since(cancelled);
eprintln!("the check returned {took:?} after Cancel");
assert_eq!(r, Err(Error::Cancelled));
assert!(
took < Duration::from_millis(500),
"returned {took:?} after Cancel"
);
}
+5 -1
View File
@@ -121,6 +121,7 @@ use crate::topology::brep_cache::{
};
use crate::topology::{edge, face, vertex, Brep, BrepId, NamingScheme, RegionId};
const OP_TIGHT_BOX: &str = "query::brep::calculate_tight_bounding_box";
const OP_CLASSIFY: &str = "query::brep::BrepQuery::point_3d_classify";
const OP_EVAL_POINT: &str = "query::brep::evaluate_surface_point";
const OP_EVAL_NORMAL: &str = "query::brep::evaluate_surface_normal";
@@ -268,8 +269,10 @@ pub fn calculate_tight_bounding_box<'a>(
brep_bbox = brep_bbox.union(&v_bbox);
}
// Union all of the Tight Edge bounding boxes.
// Union all of the Tight Edge bounding boxes. (not upstream) cooperative cancellation
// once per edge and face (solidcore#3247).
for e in bp1::edges(b, None)? {
k.check_cancelled(OP_TIGHT_BOX)?;
// (CalculateBoundingBox initialises the box first)
let mut edge_bbox = ignored(crate::error::passed_on(
qe::calculate_bounding_box(k, b, e, NORMAL_BOX, false),
@@ -294,6 +297,7 @@ pub fn calculate_tight_bounding_box<'a>(
// face).
let mut face_bbox = Extent3d::new();
for f in bp1::faces(b, None)? {
k.check_cancelled(OP_TIGHT_BOX)?;
if let Some(loose) = ignored(crate::error::passed_on(
qf1::calculate_bounding_box(k, lb.get(), f, false),
k.fixes.brep_edit_statuses_pass_on,
+2
View File
@@ -329,6 +329,8 @@ pub fn find_infinite_region(
break;
}
for edge_id in &edges {
// (not upstream) cooperative cancellation once per edge (solidcore#3247)
k.check_cancelled(OP)?;
let record = b.edge(*edge_id)?;
let curve = required(record.curve.as_ref(), "ray target edge without curve")?;
let big_tolerance = record.tolerance > 1.01 * b.tolerance();
@@ -317,6 +317,10 @@ fn validate_and_update_body(
// for every face->edgeuse
for eu in edgeuses {
// (not upstream) cooperative cancellation once per edgeuse (solidcore#3247)
k.check_cancelled(
"topology::brep::validation::assert_valid::validate_and_update_body",
)?;
let mate = eu1::mate(b, eu)?;
// skip marked edgeuses
@@ -520,6 +524,9 @@ fn collect_regions(
/// `SmBrep::AssertSubTopology(*this)`: `AssertValid(SM_NO_WALK)` of every region,
/// shell, face, face loop, edge and vertex of the B-rep, in that order.
fn sub_topology(k: &mut Kernel, b: &mut Brep, cx: &mut Cx<'_>) -> Result<bool> {
// (not upstream) cooperative cancellation once per entity: a check of a large body's
// geometry runs for seconds, the edges' curve analyses most of it (solidcore#3247)
const OP: &str = "topology::brep::validation::assert_valid::sub_topology";
let regions = p1::regions(b, None)?;
let shells = p1::shells(k, b, None)?;
let faces = p1::faces(b, None)?;
@@ -536,18 +543,23 @@ fn sub_topology(k: &mut Kernel, b: &mut Brep, cx: &mut Cx<'_>) -> Result<bool> {
rtn &= shell::region(b, cx, r)?;
}
for s in shells {
k.check_cancelled(OP)?;
rtn &= shell::shell(k, b, cx, &shell_index, s)?;
}
for f in faces {
k.check_cancelled(OP)?;
rtn &= face::face(k, b, cx, f)?;
}
for l in face_loops {
k.check_cancelled(OP)?;
rtn &= loops::loop_(k, b, cx, l)?;
}
for e in edges {
k.check_cancelled(OP)?;
rtn &= edge::edge(k, b, cx, e)?;
}
for v in vertices {
k.check_cancelled(OP)?;
rtn &= vertex::vertex(k, b, cx, v)?;
}
Ok(rtn)
@@ -68,6 +68,9 @@ pub fn contains_coincident_topology(
level: u8,
keys: &dyn Fn(EntityRef) -> crate::geom::object::ObjectRef,
) -> Result<CoincidenceResult> {
// (not upstream) cooperative cancellation once per vertex, edge and face of each search
// (solidcore#3247)
const OP: &str = "topology::brep::validation::coincidence::contains_coincident_topology";
profile_scope!(k, "validation::coincidence");
if level > 4 {
return Err(invalid("invalid assert test level"));
@@ -94,6 +97,7 @@ pub fn contains_coincident_topology(
{
profile_scope!(k, "validation::coincidence::vertex_vertex");
for (i, &v) in vertices.iter().enumerate() {
k.check_cancelled(OP)?;
let point = b.vertex(v)?.point;
let mut previous = i;
for j in point_index
@@ -157,6 +161,7 @@ pub fn contains_coincident_topology(
{
profile_scope!(k, "validation::coincidence::edge_edge");
for (i, &e) in edges.iter().enumerate() {
k.check_cancelled(OP)?;
let v1 = required(edge::vertex(b, e)?)?;
let v2 = required(edge::other_vertex(b, e, Some(v1))?)?;
let candidates: Vec<_> = endpoint_groups
@@ -214,6 +219,7 @@ pub fn contains_coincident_topology(
let mut caches = FaceCaches::new();
let mut face_boxes = Vec::new();
for &f in &faces {
k.check_cancelled(OP)?;
face_boxes.push(ignored(query_face::calculate_bounding_box(
k, &mut *b, f, false,
))?);
@@ -232,6 +238,7 @@ pub fn contains_coincident_topology(
{
profile_scope!(k, "validation::coincidence::face_face");
for (i, &f) in faces.iter().enumerate() {
k.check_cancelled(OP)?;
let key = outer_keys.get(i).ok_or_else(|| invalid("face candidate"))?;
let mut found = None;
for &j in outer_groups
@@ -312,6 +319,7 @@ pub fn contains_coincident_topology(
{
profile_scope!(k, "validation::coincidence::vertex_edge");
for &e in &edges {
k.check_cancelled(OP)?;
let bbox = ignored(crate::query::edge::calculate_bounding_box(
k,
b,
@@ -418,6 +418,9 @@ pub fn validate_pointers(
}
let faces = p1::faces(b, None)?;
for &f in &faces {
// (not upstream) cooperative cancellation once per face: checking a face's trim
// evaluates its curves and surface (solidcore#3247)
k.check_cancelled("topology::brep::validation::pointers::validate_pointers")?;
let tol = b.face(f)?.tolerance;
let checked =
crate::topology::trimming_tools::check_face(k, b, f, tol, r.array.as_deref_mut(), walk);