Hole: say when a hole removes no material, or all of a body (#10701, #10702) (#10836)

* Hole: a hole moved off the material says it removes no material (#10701)

A hole whose position, parameter or sketch point moves it off the part
cut nothing and said nothing: the feature had no warning, and a
parameter sweep listed the value as fine. A hole whose tool is clear of
every body it cuts now warns "the hole removes no material" ("1 of 2
holes remove no material" for several), as a pattern does of its
copies. The check compares boxes only, from the meshes the hole already
found its face on, so a large hole pattern costs no extra booleans.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Hole: a hole wider than the part says it removes all of it (#10702)

A through hole wider than the part removed the whole body with no
message, also when a parameter widened it later. The hole now warns
"the hole removes all of Body1" when a body it cuts is gone after the
cut.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
semmelrinde
2026-11-02 07:13:00 +00:00
committed by GitHub
parent a169ad4194
commit 29a6bbdb43
2 changed files with 89 additions and 0 deletions
+45
View File
@@ -6672,6 +6672,35 @@ fn path_at(segs: &[kernel::PathSeg], d: f64) -> (Vec3, Vec3) {
}
/// Whether a copy has no material to cut in any of its eligible targets.
/// The bodies a hole cuts: those its feature names, else its component's and its own (as
/// `apply_traced` picks a cut's targets).
fn hole_targets<'a>(st: &'a ModelState, f: &Feature) -> Vec<&'a ModelBody> {
let all = f.participants.len() == 1 && f.participants.first().is_some_and(|t| t == "*");
st.bodies
.iter()
.filter(|b| all || if f.participants.is_empty() { b.component == f.component || b.feature == f.id } else { f.participants.contains(&b.name) })
.collect()
}
/// Whether hole tool `tool` is clear of every body the hole cuts, by their boxes (#10701). Boxes
/// only: a probe boolean for every hole of a large hole pattern would double its work, and the
/// bodies' meshes are there already (the hole found its face on them). A body of another
/// component is in another frame: the hole is then not known to miss.
fn hole_misses(st: &ModelState, f: &Feature, tool: &Body) -> bool {
let targets = hole_targets(st, f);
let bounds = tool.outline_bounds();
if targets.is_empty()
|| targets.iter().any(|b| b.component != f.component)
|| bounds.is_empty()
|| !bounds.min.is_finite()
|| !bounds.max.is_finite()
{
return false;
}
let tool_box = Some(bounds.inflated(1e-6));
targets.iter().all(|b| more::boxes_apart(&tool_box, &more::mesh_box(b)))
}
fn copy_misses(st: &ModelState, f: &Feature, targets: &[String], tools: &[Traced]) -> bool {
let targets = if targets.is_empty() { &f.participants } else { targets };
let all = targets.len() == 1 && targets.first().is_some_and(|t| t == "*");
@@ -7598,7 +7627,23 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
let handled = sheet_hole_cut(st, f, &tools, warning)?;
let tools: Vec<Traced> = tools.into_iter().map(|(tool, _)| tool).collect();
if handled.is_empty() {
// A hole moved off the material (a changed position, parameter or sketch point)
// cuts nothing: it says so, as a pattern does of its copies (#10701).
let missed = tools.iter().filter(|tool| hole_misses(st, f, &tool.body)).count();
let total = tools.len();
let targets: Vec<String> = hole_targets(st, f).iter().map(|b| b.name.clone()).collect();
apply_traced(doc, st, f, tools, Operation::Cut, &[])?;
match (missed, total) {
(0, _) => {}
(_, 1) => append_warning(warning, "the hole removes no material".into()),
_ => append_warning(warning, format!("{missed} of {total} holes remove no material")),
}
// A hole wider than the part takes all of it away (#10702): almost never what a
// hole means, so it says which bodies are gone.
let gone: Vec<&str> = targets.iter().map(String::as_str).filter(|n| st.body(n).is_none()).collect();
if !gone.is_empty() {
append_warning(warning, format!("the hole removes all of {}", gone.join(", ")));
}
} else {
apply_beside_sheet_cut(doc, st, f, tools, Operation::Cut, &[], &handled)?;
}
+44
View File
@@ -450,3 +450,47 @@ fn a_hole_may_start_over_the_mouth_of_an_existing_bore() {
let e = s.execute("solid.hole", &json!({"position": [-1, 15, 20], "diameter": 8})).unwrap_err().to_string();
assert!(e.contains("is not on a face of a body"), "{e}");
}
/// A hole moved off the part cuts nothing and says so (#10701), as a pattern's copies do: by a
/// parameter of its position or a sketch point moved away. Back on the part it is silent.
#[test]
fn a_hole_moved_off_the_part_says_it_removes_no_material() {
let mut s = Session::default();
run(&mut s, "parameters.add", json!({"name": "hx", "expression": "10 mm"}));
run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 10, "name": "Plate"}));
run(&mut s, "solid.hole", json!({"position": ["hx", "10 mm", "10 mm"], "diameter": 5, "name": "H1"}));
assert_eq!(said(&s, "H1"), "");
change(&mut s, "hx", "100 mm");
assert!((volume(&mut s) - 12_000.0).abs() < 1e-6, "nothing is cut");
assert!(said(&s, "H1").contains("the hole removes no material"), "{}", said(&s, "H1"));
change(&mut s, "hx", "30 mm");
assert_eq!(said(&s, "H1"), "");
// Holes at two sketch points, one of them moved off the plate.
run(&mut s, "sketch.create", json!({"plane": "XY", "offset": 10, "name": "HP"}));
run(&mut s, "sketch.point", json!({"point": [10, 20], "id": "a"}));
run(&mut s, "sketch.point", json!({"point": [20, 20], "id": "b"}));
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "solid.hole", json!({"sketch": "HP", "points": ["a", "b"], "diameter": 4, "name": "H2"}));
assert_eq!(said(&s, "H2"), "");
run(&mut s, "sketch.edit", json!({"sketch": "HP"}));
run(&mut s, "sketch.move_point", json!({"point": "a", "to": [100, 100]}));
run(&mut s, "sketch.finish", json!({}));
assert!(said(&s, "H2").contains("1 of 2 holes remove no material"), "{}", said(&s, "H2"));
}
/// A hole wider than the part takes all of it away and says so (#10702): at creation, and when
/// a parameter widens it later. A hole across the part's width keeps the rest, silently.
#[test]
fn a_hole_wider_than_the_part_says_it_removes_all_of_it() {
let mut s = Session::default();
run(&mut s, "parameters.add", json!({"name": "hd", "expression": "40 mm"}));
run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 10, "name": "Plate"}));
run(&mut s, "solid.hole", json!({"position": [10, 10, 10], "diameter": "hd", "name": "H1"}));
assert_eq!(said(&s, "H1"), "");
assert_eq!(s.model.state().bodies.len(), 1);
change(&mut s, "hd", "100 mm");
assert!(s.model.state().bodies.is_empty());
assert!(said(&s, "H1").contains("the hole removes all of Body1"), "{}", said(&s, "H1"));
change(&mut s, "hd", "5 mm");
assert_eq!(said(&s, "H1"), "");
}