Pattern and Mirror dialogs copy picked bodies, fillets included (#193)

With Object Type = Bodies, the Mirror, Rectangular Pattern and Circular
Pattern dialogs sent the feature that created each picked body
({"features": ["Extrude1"]}), so the copies replayed only that feature and
lost everything done to the body afterwards (a fillet, a chamfer, a cut).
They now send the bodies themselves ({"bodies": [...]}), which the commands
already support and the Path Pattern dialog already did. Picking Features
is unchanged.

Editing a body Mirror or Pattern from the timeline now reopens the dialog
with its bodies selected.

Copies made this way take the body-copy names ("Body1 (2)"), as the
commands already give; pattern_direction_edge now looks up "Body1 (3)".

Tests: UI scenarios mirror_body_keeps_fillets and pattern_body_keeps_fillets
(filleted cube, copy and edit through the dialogs); both fail without the
fix with the copies' sharp 8000 mm3 volume.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
lucas-aguilar89andClaude Opus 5.5 authored and bherbruck committed 2026-10-12 10:06:18 -04:00
1 parent cab698bf12
commit 8eda982489
4 files changed
+72 -15

No files matched your search

+31 -14
View File
@@ -2526,9 +2526,10 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result<Vec<(String, Value)>, S
Kind::Mirror => {
need(0, "objects")?;
need(1, "the mirror plane")?;
let features = pattern_features(s, sels(d, 0));
let plane = plane_value(s, sels(d, 1).first()).ok_or("the mirror plane must be a plane or a planar face")?;
("solid.mirror", json!({"features": features, "plane": plane}))
let mut p = json!({"plane": plane});
pattern_objects(s, sels(d, 0), &mut p);
("solid.mirror", p)
}
Kind::PatternRect { count, spacing, count2, spacing2 } => {
need(0, "objects")?;
@@ -2538,7 +2539,8 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result<Vec<(String, Value)>, S
(None, Some(k)) => k,
(None, None) => return Err("select a direction (an axis, a sketch line or a straight edge) first".into()),
};
let mut p = json!({"features": pattern_features(s, sels(d, 0)), "dir1": pt(d1), "count1": count, "spacing1": spacing});
let mut p = json!({"dir1": pt(d1), "count1": count, "spacing1": spacing});
pattern_objects(s, sels(d, 0), &mut p);
if let Some((_, d2)) = sels(d, 2).first().and_then(|x| axis_of(app, x)) {
p["dir2"] = pt(d2);
p["count2"] = json!(count2);
@@ -2557,7 +2559,9 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result<Vec<(String, Value)>, S
}
None => return Err("select an axis".into()),
};
("solid.pattern.circular", json!({"features": pattern_features(s, sels(d, 0)), "axis": axis, "count": count, "angle": angle}))
let mut p = json!({"axis": axis, "count": count, "angle": angle});
pattern_objects(s, sels(d, 0), &mut p);
("solid.pattern.circular", p)
}
Kind::Loft { operation } => {
need(0, "profiles of two or more sketches")?;
@@ -2676,11 +2680,7 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result<Vec<(String, Value)>, S
let path = curve_ids(d, 1);
let sketch = curves_sketch(app, &path, None).ok_or("the path curves must be in one sketch")?;
let mut p = json!({"path_sketch": sketch, "path": path, "count": count, "spacing": spacing});
if sels(d, 0).iter().any(|x| matches!(x, Sel::Feature { .. })) {
p["features"] = json!(pattern_features(s, sels(d, 0)));
} else {
p["bodies"] = json!(body_names(0));
}
pattern_objects(s, sels(d, 0), &mut p);
("solid.pattern.path", p)
}
Kind::Pipe { diameter, wall } => {
@@ -2874,6 +2874,17 @@ fn curve_ids(d: &Dialog, i: usize) -> Vec<String> {
/// The features that made these bodies (what patterns and mirrors copy).
/// The features a pattern repeats: picked features as they are, picked bodies by the features
/// that made them.
/// What a pattern or mirror copies: picked features are replayed; picked bodies are copied as
/// they are now, with every feature that shaped them (a fillet after the extrude included).
fn pattern_objects(s: &Session, objects: &[Sel], p: &mut Value) {
if objects.iter().any(|x| matches!(x, Sel::Feature { .. })) {
p["features"] = json!(pattern_features(s, objects));
} else {
let bodies: Vec<&str> = objects.iter().filter_map(|x| if let Sel::Body { name } = x { Some(name.as_str()) } else { None }).collect();
p["bodies"] = json!(bodies);
}
}
fn pattern_features(s: &Session, objects: &[Sel]) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
let bodies: Vec<String> = objects.iter().filter_map(|x| if let Sel::Body { name } = x { Some(name.clone()) } else { None }).collect();
@@ -3387,13 +3398,15 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
}
d
}
FeatureKind::Mirror { features, plane, .. } => {
FeatureKind::Mirror { features, plane, bodies, .. } => {
let mut d = start("solid.mirror")?;
let ids: Vec<u64> = features.iter().filter_map(|n| s.doc.find_feature(n).map(|f| f.id)).collect();
// Features that made no body of their own (a hole) were picked as features.
let as_features = ids.iter().any(|id| !st.bodies.iter().any(|b| b.feature == *id));
if let Some(inp) = d.inputs.get_mut(0) {
if as_features {
if !bodies.is_empty() {
inp.items = bodies.iter().map(|name| Sel::Body { name: name.clone() }).collect();
} else if as_features {
inp.accept = selection::FEATURES;
inp.label = objects_label(inp.accept);
inp.items = ids.iter().map(|id| Sel::Feature { id: *id }).collect();
@@ -3406,9 +3419,13 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
}
d
}
FeatureKind::Pattern { features, pattern, .. } => {
FeatureKind::Pattern { features, pattern, bodies: copied } => {
let ids: Vec<u64> = features.iter().filter_map(|n| s.doc.find_feature(n).map(|f| f.id)).collect();
let bodies: Vec<Sel> = st.bodies.iter().filter(|b| ids.contains(&b.feature)).map(|b| Sel::Body { name: b.name.clone() }).collect();
let bodies: Vec<Sel> = if copied.is_empty() {
st.bodies.iter().filter(|b| ids.contains(&b.feature)).map(|b| Sel::Body { name: b.name.clone() }).collect()
} else {
copied.iter().map(|name| Sel::Body { name: name.clone() }).collect()
};
let mut d = match pattern {
PatternKind::Rectangular { dir1, count1, spacing1, dir2, count2, spacing2 } => {
let mut d = start("solid.pattern.rectangular")?;
@@ -3453,7 +3470,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
PatternKind::Path { .. } => return None,
};
// Features that made no body of their own (a hole, a fillet) were picked as features.
let as_features = ids.iter().any(|id| !st.bodies.iter().any(|b| b.feature == *id));
let as_features = copied.is_empty() && ids.iter().any(|id| !st.bodies.iter().any(|b| b.feature == *id));
if let Some(inp) = d.inputs.first_mut() {
if as_features {
inp.accept = selection::FEATURES;
@@ -0,0 +1,20 @@
[
{"note": "#193: copies of a body keep the fillets made after its extrude."},
{"call": "engine.execute", "params": {"command": "sketch.create", "params": {"plane": "XY"}}},
{"call": "engine.execute", "params": {"command": "sketch.rectangle.two_point", "params": {"p0": [0, 0], "p1": [20, 20]}}},
{"call": "engine.execute", "params": {"command": "sketch.finish", "params": {}}},
{"call": "engine.execute", "params": {"command": "solid.extrude", "params": {"distance": 20}}},
{"call": "engine.execute", "params": {"command": "solid.fillet", "params": {"edges": [[0, 0, 10], [20, 0, 10], [0, 20, 10], [20, 20, 10]], "radius": 3}}},
{"call": "ui.view", "params": {"view": "iso"}},
{"expect": {"bodies": 1, "volume": [7845.39, 0.001]}},
{"start": "solid.mirror"},
{"click": {"world": [10, 10, 20]}},
{"call": "ui.dialogInput", "params": {"index": 1}},
{"click": {"plane": "YZ"}},
{"key": "Enter"},
{"expect": {"bodies": 2, "errors": 0, "dialog": null, "volume": [15690.78, 0.001]}},
{"call": "ui.editFeature", "params": {"feature": "Mirror1"}},
{"expect": {"dialog": "Mirror"}},
{"key": "Enter"},
{"expect": {"bodies": 2, "errors": 0, "dialog": null, "volume": [15690.78, 0.001]}}
]
@@ -0,0 +1,20 @@
[
{"note": "#193: copies of a body keep the fillets made after its extrude."},
{"call": "engine.execute", "params": {"command": "sketch.create", "params": {"plane": "XY"}}},
{"call": "engine.execute", "params": {"command": "sketch.rectangle.two_point", "params": {"p0": [0, 0], "p1": [20, 20]}}},
{"call": "engine.execute", "params": {"command": "sketch.finish", "params": {}}},
{"call": "engine.execute", "params": {"command": "solid.extrude", "params": {"distance": 20}}},
{"call": "engine.execute", "params": {"command": "solid.fillet", "params": {"edges": [[0, 0, 10], [20, 0, 10], [0, 20, 10], [20, 20, 10]], "radius": 3}}},
{"call": "ui.view", "params": {"view": "iso"}},
{"expect": {"bodies": 1, "volume": [7845.39, 0.001]}},
{"start": "solid.pattern.rectangular"},
{"click": {"world": [10, 10, 20]}},
{"call": "ui.dialogInput", "params": {"index": 1}},
{"click": {"axis": "X"}},
{"key": "Enter"},
{"expect": {"bodies": 3, "errors": 0, "dialog": null, "volume": [23536.17, 0.001]}},
{"call": "ui.editFeature", "params": {"feature": "RectangularPattern1"}},
{"expect": {"dialog": "PatternRect"}},
{"key": "Enter"},
{"expect": {"bodies": 3, "errors": 0, "dialog": null, "volume": [23536.17, 0.001]}}
]
@@ -62,7 +62,7 @@
{
"expect": {
"bbox": {
"body": "Body3",
"body": "Body1 (3)",
"min": [
40,
0,