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 <[email protected]>
This commit is contained in:
Iraklis KourtisandClaude Opus 5.5 authored and GitHub committed 2026-10-12 14:58:52 -04:00
1 parent afcec4d0bd
commit a5cc1243e3
2 files changed
+54 -1

No files matched your search

+15 -1
View File
@@ -271,6 +271,10 @@ pub struct ModelState {
pub construct: Vec<crate::construct::Construct>,
/// Evaluation-cache keys of the sketches (by sketch feature id).
sketch_keys: BTreeMap<u64, u64>,
/// 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<u64, Vec<Vec3>>,
}
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<Vec3> = edges.iter().map(|p| crate::apply_point(m, *p)).collect();
let moved: Vec<Vec3> = 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<String, Value>, 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 {
+39
View File
@@ -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));
}