diff --git a/crates/doc/src/eval.rs b/crates/doc/src/eval.rs index 006def55..abae58f5 100644 --- a/crates/doc/src/eval.rs +++ b/crates/doc/src/eval.rs @@ -1134,6 +1134,20 @@ fn hole_spots(st: &ModelState, f: &Feature) -> Result<(Vec, Vec3)> { Ok((spots.into_iter().map(|p| hole_on_surface(st, p, dir)).collect(), dir)) } +/// A hole's spots as given (not yet moved onto the surface) and its direction. +fn hole_spots_raw(st: &ModelState, f: &Feature) -> Result<(Vec, Vec3)> { + let FeatureKind::Hole { position, direction, points, .. } = &f.kind else { return Err(DocError::Invalid("not a hole".into())) }; + match points { + Some(sp) => { + let ss = st.sketch(sp.sketch).ok_or_else(|| DocError::Unknown(format!("sketch {}", sp.sketch)))?; + let v: Vec = + sp.ids.iter().filter_map(|id| ss.sketch.points.iter().find(|p| &p.id == id)).map(|p| ss.plane.to_world(p.pos)).take(10_000).collect(); + Ok((v, -ss.plane.normal())) + } + None => Ok((vec![*position], direction.normalized().ok_or_else(|| DocError::Invalid("hole direction".into()))?)), + } +} + /// The cutting tools of one hole at `position`, drilling along `dir`. #[allow(clippy::too_many_arguments)] fn hole_tools( @@ -1766,12 +1780,26 @@ fn replay(doc: &Document, vals: &BTreeMap, f: &Feature, st: &mut } continue; } - let tools = feature_tools(vals, src, st)?; let (op, targets) = feature_op(src); let mut copies = Vec::new(); - for m in mats { - for t in &tools { - copies.push(kernel::transform_matrix(t, *m)?); + if let FeatureKind::Hole { diameter, depth, hole, .. } = &src.kind { + // A hole is drilled again at each copy of its spot (found on the surface there), so + // a blind hole's depth counts from the face at the copy, not from the bottom of the + // original hole under the original spot. + let (spots, dir) = hole_spots_raw(st, src)?; + for m in mats { + let d = crate::apply_vector(m, dir).normalized().ok_or_else(|| DocError::Invalid("hole direction".into()))?; + for p in &spots { + let at = hole_on_surface(st, crate::apply_point(m, *p), d); + copies.extend(hole_tools(vals, st, &at, d, diameter, depth, hole)?); + } + } + } else { + let tools = feature_tools(vals, src, st)?; + for m in mats { + for t in &tools { + copies.push(kernel::transform_matrix(t, *m)?); + } } } // New bodies from a pattern join their source when they touch it (as one feature would). diff --git a/crates/engine/src/cmd/patterns_tests.rs b/crates/engine/src/cmd/patterns_tests.rs index 986963f2..91d05573 100644 --- a/crates/engine/src/cmd/patterns_tests.rs +++ b/crates/engine/src/cmd/patterns_tests.rs @@ -114,3 +114,29 @@ fn path_pattern_orientation_follows_the_path() { assert!((width_of_copy("identical") - 2.0).abs() < 1e-6); assert!((width_of_copy("path") - 4.0).abs() < 1e-6); } + +/// Patterns of a blind hole drill every copy to the full depth from the face at the copy +/// (QA s30): n copies remove n times one hole. +#[test] +fn patterns_of_a_blind_hole_drill_every_copy() { + let one = { + let mut s = Session::default(); + run(&mut s, "PrimitiveBox", json!({"length": 50, "width": 25, "height": 15})); + let v0 = volume(&mut s); + run(&mut s, "FusionHoleCommand", json!({"position": [10, 12, 15], "diameter": "5 mm", "depth": "10 mm"})); + v0 - volume(&mut s) + }; + let patterned = |p: (&str, Value)| { + let mut s = Session::default(); + run(&mut s, "PrimitiveBox", json!({"length": 50, "width": 25, "height": 15})); + let v0 = volume(&mut s); + run(&mut s, "FusionHoleCommand", json!({"position": [10, 12, 15], "diameter": "5 mm", "depth": "10 mm"})); + run(&mut s, p.0, p.1); + v0 - volume(&mut s) + }; + let r = patterned(("PatternRectangular", json!({"features": ["Hole1"], "dir1": [1, 0, 0], "count1": 3, "spacing1": "15 mm"}))); + assert!((r - 3.0 * one).abs() < 1e-3 * one, "{r} vs 3 × {one}"); + let c = + patterned(("PatternCircular", json!({"features": ["Hole1"], "axis": {"origin": [25, 12, 0], "dir": [0, 0, 1]}, "count": 2, "angle": 360}))); + assert!((c - 2.0 * one).abs() < 1e-3 * one, "{c} vs 2 × {one}"); +}