From d7ad69e64fc003d921fbb3e8377e103e902c82ca Mon Sep 17 00:00:00 2001 From: semmelrinde <124386289+semmelrinde@users.noreply.github.com> Date: Mon, 2 Nov 2026 17:16:51 +0000 Subject: [PATCH] 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 --- crates/doc/src/eval.rs | 14 ++++++------ crates/engine/src/hole_reference_tests.rs | 26 +++++++++++++++++++++++ 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/crates/doc/src/eval.rs b/crates/doc/src/eval.rs index 24c2b4b4a0..9daa63b38d 100644 --- a/crates/doc/src/eval.rs +++ b/crates/doc/src/eval.rs @@ -4665,7 +4665,7 @@ fn hole_spots(vals: &BTreeMap, 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, 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, st: &ModelState, f: &Feature, base: &crate::PointExpr, axis: Vec3) -> Result { 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, st: &ModelState, f: &Feature, at: Vec3, dir: Vec3) -> Result { +/// 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, st: &ModelState, f: &Feature, at: Vec3, dir: Vec3, on_face: bool) -> Result { let references = hole_references(f); if references.is_empty() { return Ok(at); @@ -4765,7 +4767,7 @@ fn with_references(vals: &BTreeMap, 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) diff --git a/crates/engine/src/hole_reference_tests.rs b/crates/engine/src/hole_reference_tests.rs index 48d437e72d..e274f3e43b 100644 --- a/crates/engine/src/hole_reference_tests.rs +++ b/crates/engine/src/hole_reference_tests.rs @@ -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();