From a5cc1243e3ce3a19d11130274b3180d88ff25d28 Mon Sep 17 00:00:00 2001 From: Iraklis Kourtis Date: Mon, 12 Oct 2026 14:58:52 -0400 Subject: [PATCH] fix(doc): a pattern repeats a fillet on the edges it found, not its picks (#363) A rectangular or circular pattern of a fillet or chamfer repeated it at the points picked when the feature was made. The feature itself finds its edges by name on every evaluation, but a parameter that moves the edges (a taller pocket) leaves the old points on other geometry, and the copies then blended the wrong edges or failed ("the faces at the edge ends must be planar"). The model state now records, per fillet or chamfer, the edges it blended in this evaluation (part of the state's digest), and the pattern repeats those. Fixes #219 Claude-Session: https://claude.ai/code/session_01TWc1smHYwTSVrVc8SNvS44 Co-authored-by: Claude Opus 5.5 --- crates/doc/src/eval.rs | 16 +++++++++++++++- crates/engine/src/tests.rs | 39 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/crates/doc/src/eval.rs b/crates/doc/src/eval.rs index 63ff6d31..f71b9efb 100644 --- a/crates/doc/src/eval.rs +++ b/crates/doc/src/eval.rs @@ -271,6 +271,10 @@ pub struct ModelState { pub construct: Vec, /// Evaluation-cache keys of the sketches (by sketch feature id). sketch_keys: BTreeMap, + /// The edges each fillet or chamfer blended, as found in this evaluation (by feature id): + /// a pattern of the feature repeats these, not the points picked when it was made, which a + /// parameter change may have moved off the edges. + blend_edges: BTreeMap>, } impl ModelState { @@ -290,6 +294,12 @@ impl ModelState { self.sketch_keys.get(&s.feature).hash(&mut h); } self.body_counter.hash(&mut h); + for (feature, points) in &self.blend_edges { + feature.hash(&mut h); + for p in points { + [p.x, p.y, p.z].map(f64::to_bits).hash(&mut h); + } + } serde_json::to_string(&self.threads).unwrap_or_default().hash(&mut h); serde_json::to_string(&self.sheets).unwrap_or_default().hash(&mut h); h.finish() @@ -2735,8 +2745,11 @@ fn replay( // A fillet or chamfer repeats at the copied edges. if let FeatureKind::Fillet { edges, radius, .. } | FeatureKind::Chamfer { edges, distance: radius, .. } = &src.kind { let r = val(vals, radius, Kind::Length)?; + // The edges the feature blended in this evaluation (found by name), falling back to + // its picked points. + let found = st.blend_edges.get(&src.id).cloned().unwrap_or_else(|| edges.clone()); for m in mats { - let moved: Vec = edges.iter().map(|p| crate::apply_point(m, *p)).collect(); + let moved: Vec = found.iter().map(|p| crate::apply_point(m, *p)).collect(); let ti = blend_target(st, &None, &moved)?; let Some(mb) = st.bodies.get(ti).cloned() else { continue }; let nb = match &src.kind { @@ -3466,6 +3479,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap, f: &Feature, st: } else { resolve_edges(&mb.body, edges, &f.edge_refs, warning)? }; + st.blend_edges.insert(f.id, edges.clone()); let input = mb.traced(); // The blended body, and the lineage of what the last blend started from. let (nb, from) = match &f.kind { diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index f4b14723..077f66ce 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -2677,3 +2677,42 @@ fn feature_params_docs_list_shared_keys() { assert!(params(id).contains("targets?"), "{id}: {}", params(id)); } } + +#[test] +fn a_pattern_repeats_a_fillet_on_the_edges_it_found() { + // #219: a filleted pocket patterned along the plate, its height from a parameter. A taller + // pocket moves its far corner edges away from the points picked for the fillet; the pattern + // repeats the edges the fillet found (by name), not those points. + let mut s = Session::default(); + run(&mut s, "parameters.add", json!({"name": "n", "expression": "14", "unit": ""})); + run(&mut s, "parameters.add", json!({"name": "h", "expression": "(419.1 mm - 3.175 mm - (n - 1) * 3.175 mm) / n", "unit": "mm"})); + run(&mut s, "sketch.create", json!({"plane": "XY"})); + run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [150, 422.275]})); + run(&mut s, "sketch.finish", json!({})); + run(&mut s, "solid.extrude", json!({"distance": 19.05, "name": "Plate"})); + run(&mut s, "construct.plane.offset", json!({"base": "XY", "offset": 19.05, "name": "PocketPlane"})); + run(&mut s, "sketch.create", json!({"plane": "PocketPlane"})); + run(&mut s, "sketch.rectangle.two_point", json!({"p0": [3.175, 3.175], "p1": [146.825, 29.9357]})); + run(&mut s, "sketch.dimension", json!({"entities": ["origin", "p1"], "type": "horizontal", "value": "3.175"})); + run(&mut s, "sketch.dimension", json!({"entities": ["origin", "p1"], "type": "vertical", "value": "3.175"})); + run(&mut s, "sketch.dimension", json!({"entities": ["l1"], "type": "length", "value": "143.65"})); + run(&mut s, "sketch.dimension", json!({"entities": ["l4"], "type": "length", "value": "h"})); + run(&mut s, "sketch.finish", json!({})); + run(&mut s, "solid.extrude", json!({"distance": 12.7, "direction": "negative", "operation": "cut", "name": "Pocket"})); + run( + &mut s, + "solid.fillet", + json!({"edges": [[3.175, 3.175, 12.700000000000001], [146.825, 3.175, 12.700000000000001], [146.825, 29.9357, 12.700000000000001], [3.175, 29.9357, 12.700000000000001]], "radius": 3.2}), + ); + run( + &mut s, + "solid.pattern.rectangular", + json!({"features": ["Pocket", "Fillet1"], "dir1": [0, 1, 0], "count1": "n", "spacing1": "h + 3.175 mm"}), + ); + let r = run(&mut s, "parameters.change", json!({"name": "n", "expression": "10"})); + assert_eq!(r["errors"].as_array().map(Vec::len), Some(0), "{r}"); + let (n, h) = (10.0, (419.1 - 3.175 - 9.0 * 3.175) / 10.0); + let pocket = 143.65 * h * 12.7 - 4.0 * (1.0 - PI / 4.0) * 3.2 * 3.2 * 12.7; + let expect = 150.0 * 422.275 * 19.05 - n * pocket; + assert!(rel(volume(&mut s), expect) < 1e-5, "{} vs {expect}", volume(&mut s)); +}