Sheet metal: cuts and holes pass over sheets and bends by their exact boxes, never a mesh's

A primitive cut skips the sheets kernel::apart (exact bounds grown by the record tolerance)
finds clear of its tool; the bend check leaves the clear case to kernel::boolean, which answers
it from the same exact boxes with no intersection work. The mesh-box helpers are gone, and their
four lines leave xtask/mesh-decisions.txt (172 sites left).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
This commit is contained in:
jyviko
2026-11-02 02:45:28 -05:00
committed by jyviko
parent 608d07ecac
commit 597b23ba8e
5 changed files with 67 additions and 48 deletions
+15 -41
View File
@@ -5,7 +5,7 @@ use super::*;
use crate::EmbossFace;
use crate::apply_point;
use crate::sheet;
use solidcraft_geom::{Aabb3, Loop2, Ring, Seg2};
use solidcraft_geom::{Loop2, Ring, Seg2};
pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st: &mut ModelState) -> Result<()> {
match &f.kind {
@@ -1025,15 +1025,17 @@ pub(super) fn sheet_primitive_cut(
return Ok(Vec::new());
}
// The first sheet the primitive meets decides the machining direction. Sheets clear of it
// by their boxes are passed over; the one sheet left needs no boolean to show it is met (that
// boolean came out empty against a sheet carrying a label's cut-outs, and the hole was then
// cut into the solid without a place in the flat pattern).
let tool_box = body_box(tool);
let near: Vec<&sheet::SheetBody> = st
.sheets
.iter()
.filter(|sh| sheet_eligible(f, st, &[], sh) && st.body(&sh.body).is_some_and(|mb| !boxes_apart(&mesh_box(mb), &tool_box)))
.collect();
// by their exact boxes ([`kernel::apart`]) are passed over; the one sheet left needs no
// boolean to show it is met (that boolean came out empty against a sheet carrying a label's
// cut-outs, and the hole was then cut into the solid without a place in the flat pattern).
let mut near: Vec<&sheet::SheetBody> = Vec::new();
for sh in st.sheets.iter().filter(|sh| sheet_eligible(f, st, &[], sh)) {
if let Some(mb) = st.body(&sh.body)
&& !kernel::apart(&mb.body, tool)?
{
near.push(sh);
}
}
let mut normal = None;
for sh in &near {
if let Some(mb) = st.body(&sh.body)
@@ -1344,9 +1346,6 @@ fn sheet_prism_cut(
} else {
Vec::new()
};
// Boxes around the tools: a tool clear of a bend by its box needs no boolean to show it (a
// label's letters against every bend and flange of a tray).
let tool_boxes: Vec<Option<Aabb3>> = tools.iter().map(body_box).collect();
let mut plans = Vec::new();
let mut notes = Vec::new();
'sheets: for original in &st.sheets {
@@ -1366,12 +1365,10 @@ fn sheet_prism_cut(
// is exact, and a cut that machines nothing there is checked against the solid after it.
// (That boolean cost each label hundreds of milliseconds, and against a sheet already
// carrying a label's cut-outs it came out empty: the next cut was dropped.)
// A tool clear of a bend by its exact box (a label's letters against every bend and
// flange of a tray) costs no intersection work: the boolean answers it from the boxes.
for bends in sh.unmodeled_solids(plane)? {
let bends_box = body_box(&bends);
for (tool, tool_box) in tools.iter().zip(&tool_boxes) {
if boxes_apart(&bends_box, tool_box) {
continue;
}
for tool in tools {
if kernel::boolean(&bends, tool, kernel::BoolOp::Intersect)?.is_some() {
return Err(DocError::Invalid(
"this machining intersects a folded bend or a nonparallel panel; unfold the sheet before machining that material".into(),
@@ -1556,29 +1553,6 @@ fn sheet_prism_cut(
Ok(handled)
}
/// A box around `body`, grown past its mesh's deflection from the exact faces (and a hair
/// more): bodies whose boxes are apart do not meet. None when it can't be worked out.
fn body_box(body: &Body) -> Option<Aabb3> {
let tol = (body.size() * 1e-3).max(1e-4);
padded_box(&body.mesh(&solidcraft_kernel::MeshTolerance::chord(tol)).ok()?, tol)
}
/// [`body_box`] of a model body, from the mesh it keeps.
fn mesh_box(mb: &ModelBody) -> Option<Aabb3> {
padded_box(&mb.mesh(), mb.display_tol())
}
fn padded_box(mesh: &solidcraft_geom::Mesh, tol: f64) -> Option<Aabb3> {
let b = mesh.bounds();
(!b.is_empty() && b.min.is_finite() && b.max.is_finite()).then(|| b.inflated(2.0 * tol + 1e-6))
}
/// Whether two boxes are known to be apart (an unknown box is never apart).
fn boxes_apart(a: &Option<Aabb3>, b: &Option<Aabb3>) -> bool {
let (Some(a), Some(b)) = (a, b) else { return false };
a.max.x < b.min.x || b.max.x < a.min.x || a.max.y < b.min.y || b.max.y < a.min.y || a.max.z < b.min.z || b.max.z < a.min.z
}
/// A cut's note on the pieces it parted from the base of `sh` (see
/// [`sheet::SheetBody::parted_from_base`]; `material` is the sheet's flat material after the cut,
/// `before` the sheet before it): how many, their area and where they lie in the flat pattern.
+48
View File
@@ -5770,3 +5770,51 @@ fn an_unfolded_sheets_flat_face_is_named_after_its_base_at_every_size() {
}
}
}
/// A primitive cut passes over a sheet its exact box is clear of: that sheet comes back the very
/// same body, and the sheet the cut meets gets the hole.
#[test]
fn a_primitive_cut_passes_over_a_sheet_clear_of_it_by_its_exact_box() {
let mut s = Session::default();
for (name, x) in [("Left", 0.0), ("Right", 100.2)] {
run(&mut s, "solid.box", json!({"corner": [x, 0, 0], "length": 100, "width": 60, "height": 2.5, "body_name": name}));
run(&mut s, "sheet.convert", json!({"body": name, "face": [x + 50.0, 30, 2.5]}));
}
let body = |s: &Session, name: &str| s.model.state().bodies.iter().find(|b| b.name == name).map(|b| b.body.clone()).unwrap();
let left = body(&s, "Left");
let right_volume = body_volume(&mut s, "Right");
// 0.2 mm from the Left sheet's end: within the display mesh's tolerance of it, clear of its exact box.
run(&mut s, "solid.cylinder", json!({"base": [102.9, 30, -1], "radius": 2.5, "height": 5, "operation": "cut"}));
assert!(body(&s, "Left").same(&left), "the sheet clear of the cut is passed over");
let holes = |s: &Session, name: &str| s.model.state().sheets.iter().find(|sh| sh.body == name).map(|sh| sh.holes.len());
assert_eq!(holes(&s, "Left"), Some(0));
assert_eq!(holes(&s, "Right"), Some(1));
assert!((right_volume - body_volume(&mut s, "Right") - PI * 6.25 * 2.5).abs() < 1e-3);
}
/// A cut that only grazes a bend's curved faces near the edge where the bend meets the base is
/// checked against the bend and refused (the sheet is unchanged); one clear of the bend's exact
/// box cuts the base.
#[test]
fn a_cut_grazing_a_bend_near_its_edge_is_refused_and_one_clear_of_its_exact_box_cuts() {
// `plate_with_flange`: the bend (R 2.5, T 2.5) runs from y = 0 to y = 5, where the base
// panel starts.
for (y0, grazes) in [(4.9, true), (5.1, false)] {
let mut s = Session::default();
plate_with_flange(&mut s);
let v0 = volume(&mut s);
rectangle_tool(&mut s, "Slot", [40.0, y0], [60.0, 15.0]);
let cut = json!({"sketch": "Slot", "through_all": true, "direction": "symmetric", "operation": "cut"});
if grazes {
let before = s.doc.to_json();
let e = s.execute("solid.extrude", &cut).unwrap_err().to_string();
assert!(e.contains("intersects a folded bend"), "{e}");
assert_eq!(s.doc.to_json(), before);
assert_eq!(volume(&mut s), v0);
} else {
run(&mut s, "solid.extrude", cut);
assert!((v0 - volume(&mut s) - 20.0 * (15.0 - y0) * 2.5).abs() < 1e-3, "{}", v0 - volume(&mut s));
assert_eq!(flat(&mut s)["cutouts"].as_array().map(Vec::len), Some(1));
}
}
}
+1 -1
View File
@@ -202,7 +202,7 @@ pub use meshbody::{MAX_TRIANGLES, mesh_body};
pub use native::{body_from_smlib, smlib_body};
pub use native::{brep_record, decode_native_body, encode_native_body, native_preview_mesh};
pub use offset::with_same_surface;
pub use ops::{BoolOp, boolean, half_space, join_apart, place_matrix, split_by_plane, transform, transform_matrix, union_all, union_apart};
pub use ops::{BoolOp, apart, boolean, half_space, join_apart, place_matrix, split_by_plane, transform, transform_matrix, union_all, union_apart};
pub use point_path::{
PathPiece, PointPath, SpaceBezier, along_path, curve_path, edge_path, face_section_along, project_onto_face, sketch_record_curve,
sweep_point_path_with,
+3 -2
View File
@@ -56,8 +56,9 @@ pub fn union_all(bodies: &[&Body]) -> Result<Body> {
}
/// Whether B-rep bodies `a` and `b` are apart: their exact boxes, each grown by twice its
/// record's tolerance (as [`union_apart`] takes them), do not meet.
fn apart(a: &Body, b: &Body) -> Result<bool> {
/// record's tolerance (as [`union_apart`] takes them), do not meet. Never for a mesh body. The
/// way to skip work on bodies that cannot meet: a mesh's box is not the body's.
pub fn apart(a: &Body, b: &Body) -> Result<bool> {
if a.is_mesh() || b.is_mesh() {
return Ok(false);
}
-4
View File
@@ -28,10 +28,8 @@ crates/doc/src/eval.rs:tangent_face_chain grows a face pick across tangent edges
crates/doc/src/eval.rs:through_all_distance sizes a Through All extent by the display mesh
crates/doc/src/eval.rs:trim_to_body keeps the tool pieces touching the start by a mesh
crates/doc/src/eval.rs:unrecorded_touches finds touching pieces by a mesh
crates/doc/src/eval_more.rs:body_box skips sheet-metal booleans by a mesh's box
crates/doc/src/eval_more.rs:body_under picks the body under a plastic feature's point by the display mesh
crates/doc/src/eval_more.rs:face_outline finds the wall an outline runs along by mesh triangles
crates/doc/src/eval_more.rs:mesh_box skips sheet-metal booleans by the display mesh's box
crates/doc/src/eval_more.rs:on_sheet_material decides whether a point touches a sheet by the display mesh
crates/doc/src/eval_more.rs:planar_normal_at finds the planar face through a point by a mesh
crates/doc/src/eval_more.rs:plastic_eval places plastic features by a mesh
@@ -39,8 +37,6 @@ crates/doc/src/eval_more.rs:plate_recesses finds a plate's recesses by mesh tria
crates/doc/src/eval_more.rs:rib_tool runs a rib to the body by the display mesh
crates/doc/src/eval_more.rs:rib_tool_normal runs a rib to the body by the display mesh
crates/doc/src/eval_more.rs:sheet_eval places sheet-metal features by a mesh
crates/doc/src/eval_more.rs:sheet_primitive_cut skips sheets clear of a primitive cut by mesh boxes
crates/doc/src/eval_more.rs:sheet_prism_cut skips bends clear of a cut's tools by mesh boxes
crates/doc/src/history_names.rs:OperandNames::edge names an operand's edge by the display mesh
crates/doc/src/hole_reference.rs:on_reference_face decides whether a point lies on a hole's reference face by the display mesh
crates/doc/src/hole_reference.rs:reference_frame finds a hole's reference edges on the display mesh