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