Pattern and Mirror: a hole placed by edge distances can be copied (#8055) (#8231)

A pattern or mirror replays its source hole's definition, so a hole with
reference edges finds its centre again from them. That happens after the
hole is drilled: through the part, the centre is then in the opening, and
the check that the centre lies on the face refused every copy. The replay
now skips that check; placing the hole itself still makes it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
semmelrinde
2026-11-02 17:16:51 +00:00
committed by GitHub
parent a0ec7dbdf0
commit d7ad69e64f
2 changed files with 34 additions and 6 deletions
+8 -6
View File
@@ -4665,7 +4665,7 @@ fn hole_spots(vals: &BTreeMap<String, Value>, st: &ModelState, f: &Feature) -> R
_ => {
let dir = direction.normalized().ok_or_else(|| DocError::Invalid("hole direction".into()))?;
let at = hole_on_surface(st, single_hole_position(vals, st, position, at, points)?, dir, &[]);
let at = with_references(vals, st, f, at, dir)?;
let at = with_references(vals, st, f, at, dir, true)?;
let dir = hole_dir_into_face(st, at, dir);
return Ok((vec![hole_on_surface(st, at, dir, &[])], dir));
}
@@ -4713,7 +4713,7 @@ fn hole_spots_raw(vals: &BTreeMap<String, Value>, st: &ModelState, f: &Feature)
_ => {
let dir = direction.normalized().ok_or_else(|| DocError::Invalid("hole direction".into()))?;
let p = single_hole_position(vals, st, position, at, points)?;
let p = if hole_references(f).is_empty() { p } else { with_references(vals, st, f, hole_on_surface(st, p, dir, &[]), dir)? };
let p = if hole_references(f).is_empty() { p } else { with_references(vals, st, f, hole_on_surface(st, p, dir, &[]), dir, false)? };
Ok((vec![p], hole_dir_into_face(st, hole_on_surface(st, p, dir, &[]), dir)))
}
}
@@ -4747,13 +4747,15 @@ fn hole_references(f: &Feature) -> &[crate::HoleReference] {
/// plane of the flat face at its base, square to its axis.
fn cylinder_base(vals: &BTreeMap<String, Value>, st: &ModelState, f: &Feature, base: &crate::PointExpr, axis: Vec3) -> Result<Vec3> {
let at = point(vals, base)?;
with_references(vals, st, f, at, -axis)
with_references(vals, st, f, at, -axis, true)
}
/// Where a Single hole (or a cylinder) with reference edges (#37) is placed: at its distances
/// from the edges' lines, in the plane of the face at `at` (on the surface), the edges found by
/// their names, else their points. A feature without reference edges stays at `at`.
fn with_references(vals: &BTreeMap<String, Value>, st: &ModelState, f: &Feature, at: Vec3, dir: Vec3) -> Result<Vec3> {
/// their names, else their points. A feature without reference edges stays at `at`. With
/// `on_face` the centre must lie on that face; a pattern or mirror replaying a hole that is
/// already drilled finds its centre in the opening and doesn't ask (#8055).
fn with_references(vals: &BTreeMap<String, Value>, st: &ModelState, f: &Feature, at: Vec3, dir: Vec3, on_face: bool) -> Result<Vec3> {
let references = hole_references(f);
if references.is_empty() {
return Ok(at);
@@ -4765,7 +4767,7 @@ fn with_references(vals: &BTreeMap<String, Value>, st: &ModelState, f: &Feature,
let edges: Vec<(Vec3, Option<&str>)> = references.iter().enumerate().map(|(i, r)| (r.edge.0, f.edge_names.get(i).map(String::as_str))).collect();
let frame = crate::hole_reference::reference_frame(st, at, dir, &edges, false)?;
let centre = frame.place(at, &distances)?;
if !crate::hole_reference::on_reference_face(st, &frame, centre) {
if on_face && !crate::hole_reference::on_reference_face(st, &frame, centre) {
return Err(DocError::Invalid("the reference distances put the hole's centre off its face".into()));
}
Ok(centre)
+26
View File
@@ -105,6 +105,32 @@ fn a_hole_follows_its_edges_when_the_box_grows() {
assert_eq!(hole(&t).1, hole(&s).1);
}
#[test]
fn a_referenced_hole_can_be_patterned_and_mirrored() {
// #8055, #6754: a pattern or mirror places its copies from the hole's own spot, found again
// after the hole has been drilled. Through the plate, its centre is then in the opening, with
// no face under it.
let mut s = plate();
let mut through = referenced(json!([{"edge": [0, 20, 10], "distance": 10}, {"edge": [25, 0, 10], "distance": 12}]));
through.as_object_mut().unwrap().remove("depth");
run(&mut s, "solid.hole", through);
run(
&mut s,
"solid.pattern.rectangular",
json!({"features": ["H"], "dir1": [1, 0, 0], "count1": 2, "spacing1": 12, "dir2": [0, 1, 0], "count2": 2, "spacing2": 12, "name": "P"}),
);
for (x, y) in [(10.0, 12.0), (22.0, 12.0), (10.0, 24.0), (22.0, 24.0)] {
bore_at(&s, x, y, 10.0);
}
run(&mut s, "solid.mirror", json!({"features": ["H"], "plane": {"origin": [25, 0, 0], "normal": [1, 0, 0]}, "name": "M"}));
bore_at(&s, 40.0, 12.0, 10.0);
// The copies still follow the hole's edges.
run(&mut s, "parameters.change", json!({"name": "len", "expression": "60 mm"}));
let p = s.doc.find_feature("P").unwrap().id;
assert!(s.model.result(p).is_some_and(|r| r.error.is_none()), "{:?}", s.model.result(p).map(|r| &r.error));
bore_at(&s, 22.0, 24.0, 10.0);
}
#[test]
fn a_bad_reference_is_refused() {
let mut s = plate();