From 8fb5742706c3ae5b1d171e7bf781db5d681750b6 Mon Sep 17 00:00:00 2001 From: bherbruck Date: Tue, 13 Oct 2026 20:52:35 -0400 Subject: [PATCH] fix: imprinting a partial contact boxes faces by their control points (#929) #825 chose the faces to split by boxing each face, dividing its curved edges down to the contact tolerance; on the sensor enclosure's lid that division never finished (press/pull and extrude on 18 faces grew past 24 GB and timed out in the action matrix, on CI as here). The boxes now come from the edges' control points, with no division; a face whose extent can't be found counts as near. A test holds the lid's press/pull to a time budget. Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/boolean_regression_tests.rs | 17 ++++++++ crates/kernel/src/coplanar.rs | 39 +++++++++++++++++-- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/crates/engine/src/boolean_regression_tests.rs b/crates/engine/src/boolean_regression_tests.rs index c0222b6e..e11595ae 100644 --- a/crates/engine/src/boolean_regression_tests.rs +++ b/crates/engine/src/boolean_regression_tests.rs @@ -215,3 +215,20 @@ fn ls_extruded_up_and_down_from_one_grid_join() { let v = volume(&s, "Logo"); assert!((v - 6000.0).abs() < 1e-6, "{v}"); } + +/// Press/pull on the sensor enclosure's lid top: joining the pulled face's prism imprints the +/// contact (#825), and boxing the lid's faces by dividing their curved edges down to the +/// contact tolerance ran away (over 24 GB and many minutes). It finishes in seconds. +#[test] +fn press_pull_on_the_enclosure_lid_is_prompt() { + let mut s = Session::default(); + let script: Value = serde_json::from_str(include_str!("../../../examples/acceptance/sensor_enclosure.json")).unwrap(); + s.run_script(&script).unwrap(); + let total = |s: &Session| -> f64 { s.model.state().bodies.iter().map(|b| solidcraft_kernel::measure(&b.body).unwrap().volume).sum() }; + let before = total(&s); + let t = std::time::Instant::now(); + s.execute("solid.press_pull", &json!({"face": [-12.0, 27.4, 23.0], "distance": 5.0})).unwrap(); + // A few seconds here; the bound leaves room for a loaded CI runner. + assert!(t.elapsed().as_secs() < 60, "took {:?}", t.elapsed()); + assert!(total(&s) > before, "{before} -> {}", total(&s)); +} diff --git a/crates/kernel/src/coplanar.rs b/crates/kernel/src/coplanar.rs index c44b45b9..ab0f71ed 100644 --- a/crates/kernel/src/coplanar.rs +++ b/crates/kernel/src/coplanar.rs @@ -546,12 +546,41 @@ fn same_surface(x: &Option, y: &Option, } } -/// The box around faces `faces` of `body` (by their boundaries), grown by `gap`. +/// A box around face `f` from its edges' control points (a B-spline or NURBS curve with positive +/// weights lies in their hull; an edge's curve may run past the edge, which only grows the box). +/// No dividing: for choosing faces it need only contain the face, and dividing a split face's +/// curved edges down to a tolerance ran away on the sensor enclosure's lid (gigabytes, minutes). +/// `None` for an edge on an intersection curve or with weights that don't bound it. +fn hull_box(f: &mt::Face) -> Option { + let mut bb = Aabb3::EMPTY; + for e in f.edge_iter() { + match e.curve() { + mt::Curve::Line(_) => { + bb.add(from_p3(e.front().point())); + bb.add(from_p3(e.back().point())); + } + mt::Curve::BSplineCurve(c) => c.control_points().iter().for_each(|p| bb.add(from_p3(*p))), + mt::Curve::NurbsCurve(c) => { + for p in c.control_points() { + if !(p.w > 0.0 && p.w.is_finite()) { + return None; + } + bb.add(Vec3::new(p.x / p.w, p.y / p.w, p.z / p.w)); + } + } + mt::Curve::IntersectionCurve(_) => return None, + } + } + (!bb.is_empty() && bb.min.is_finite() && bb.max.is_finite()).then_some(bb) +} + +/// The box around faces `faces` of `body` ([`hull_box`]), grown by `gap`. `None` when a face's +/// extent can't be found. fn faces_box(body: &Body, faces: &[usize], gap: f64) -> Option { let fs: Vec<&mt::Face> = body.solid.face_iter().collect(); let mut bb = Aabb3::EMPTY; for f in faces { - let b = face_box(fs.get(*f)?, gap)?; + let b = hull_box(fs.get(*f)?)?; bb.add(b.min); bb.add(b.max); } @@ -565,10 +594,12 @@ fn faces_box(body: &Body, faces: &[usize], gap: f64) -> Option { fn imprinted(body: Body, mut h: Option, n: Vec3, d: f64, tool: &Body, tol: f64) -> Result<(Body, Option)> { let contact = faces_in(tool, -n, -d, tol); let gap = tol * 10.0; - let Some(region) = faces_box(tool, &contact, gap) else { return Ok((body, h)) }; + // A face whose extent can't be found counts as near. + let region = faces_box(tool, &contact, gap); let near = |b: &Body, faces: Vec| -> Vec { + let Some(region) = region else { return faces }; let fs: Vec<&mt::Face> = b.solid.face_iter().collect(); - faces.into_iter().filter(|f| fs.get(*f).and_then(|x| face_box(x, gap)).is_some_and(|bx| !boxes_separate(bx, region, gap))).collect() + faces.into_iter().filter(|f| fs.get(*f).and_then(|x| hull_box(x)).is_none_or(|bx| !boxes_separate(bx, region, gap))).collect() }; let bounds = crate::history::edge_faces(&tool.solid); let mut beside: Vec = bounds