fix(ui): Extrude and Revolve leave Profiles empty when the last sketch is already used (#684) (#705)

With nothing selected, Extrude and Revolve no longer pre-pick the last sketch's profile when a feature already builds from it, so OK no longer cuts into a part nobody picked. Fixes #684.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
semmelrindeandClaude Opus 5.5 authored and GitHub committed 2026-10-13 10:28:17 -04:00
1 parent 4ff8f3e14d
commit 2c4d197bf4
2 files changed
+104 -3

No files matched your search

+13 -3
View File
@@ -572,7 +572,8 @@ impl Dialog {
_ => None,
});
}
// With nothing pre-selected, a single profile in the sketch being used is taken.
// With nothing pre-selected, a single profile in the sketch being used is taken (#684:
// not one a feature already builds from).
if matches!(d.kind, Kind::Extrude { .. } | Kind::Revolve { .. })
&& let Some(inp) = d.inputs.first_mut()
&& inp.items.is_empty()
@@ -785,16 +786,25 @@ impl Dialog {
}
}
/// Profiles of the sketch a feature would use (active, else the last), when there is just one.
/// Profiles of the sketch a feature would use, when there is just one: the active sketch's, else
/// the last sketch's while no feature builds from it yet. A used profile (a cube's sketch on its
/// bottom face) is only taken when it is picked.
fn default_profiles(s: &Session) -> Vec<Sel> {
let st = s.world_state();
let sid = s.active_sketch.or_else(|| s.doc.features.iter().rev().find(|f| matches!(f.kind, FeatureKind::Sketch { .. })).map(|f| f.id));
let sid = s.active_sketch.or_else(|| {
s.doc.features.iter().rev().find(|f| matches!(f.kind, FeatureKind::Sketch { .. })).map(|f| f.id).filter(|&id| !profiles_used(s, id))
});
match sid.and_then(|id| st.sketch(id).map(|ss| (id, ss.profiles.len()))) {
Some((id, 1)) => vec![Sel::Profile { sketch: id, index: 0 }],
_ => Vec::new(),
}
}
/// Does a feature build from this sketch's profiles (extrude, revolve, sweep, loft, emboss…)?
fn profiles_used(s: &Session, sketch: u64) -> bool {
s.doc.features.iter().any(|f| f.kind.profile_sels().iter().any(|(id, _)| *id == sketch))
}
/// The plane of a planar body face: (point on it, unit normal).
pub fn planar_face(s: &Session, body: &str, face: usize) -> Option<(Vec3, Vec3)> {
let st = s.world_state();
+91
View File
@@ -333,6 +333,97 @@ fn profiles_then_extrude_takes_them_all() {
assert!(apply_commands(&app, &d).is_ok());
}
/// #684: with nothing selected, Extrude and Revolve take the last sketch's single profile only
/// while no feature uses it yet. A cube's sketch lies on its bottom face: taking that profile
/// again made the dialog default to Cut, so OK cut into the part nobody had picked.
#[test]
fn used_profile_of_the_last_sketch_is_not_pre_picked() {
let mut h = crate::scenario::Harness::new();
let base = h.app.run("sketch.create", json!({"plane":"XY"})).unwrap()["sketch"].as_u64().unwrap();
h.app.run("sketch.rectangle.two_point", json!({"p0":[0,0],"p1":[20,20]})).unwrap();
h.app.run("sketch.finish", json!({})).unwrap();
h.app.run("solid.extrude", json!({"distance":20})).unwrap();
h.app.run("select.clear", json!({})).unwrap();
let features = h.app.session.doc.features.len();
h.call("ui.start", json!({"command":"solid.extrude"}));
h.frames(2);
let ui = h.call("ui.selection", json!({}))["result"].clone();
assert_eq!(ui["selection"], json!([]));
assert_eq!(ui["dialog"], json!([{"label":"Profiles","items":[]}]), "the extruded sketch's profile is taken again");
let d = h.app.dialog.as_ref().unwrap();
assert!(matches!(d.kind, Kind::Extrude { operation: 1, auto_op: true, .. }), "Join, not Cut: {:?}", d.kind);
assert_eq!(d.active, 0, "picks go to Profiles");
// OK with nothing picked builds nothing: the cube stays whole.
h.step(&json!({"key":"Enter"})).unwrap();
assert!(h.app.dialog.is_some(), "OK waits for a pick");
assert_eq!(h.app.session.doc.features.len(), features);
h.step(&json!({"expect":{"bodies":1,"volume":8000}})).unwrap();
h.step(&json!({"key":"Escape"})).unwrap();
assert!(h.app.dialog.is_none());
// Revolve takes the same default.
h.app.start("solid.revolve");
h.frames(2);
assert!(h.app.dialog.as_ref().unwrap().inputs[0].items.is_empty(), "Revolve takes the used profile");
h.step(&json!({"key":"Escape"})).unwrap();
// A sketch being edited still offers its profile, used or not.
h.app.edit_sketch(base);
h.frames(2);
h.app.start("solid.extrude");
h.frames(2);
assert_eq!(h.app.dialog.as_ref().unwrap().inputs[0].items, vec![Sel::Profile { sketch: base, index: 0 }]);
h.step(&json!({"key":"Escape"})).unwrap();
h.app.finish_sketch();
// A new sketch on the top face, not used yet, is taken right after Finish Sketch.
let top = h.app.run("sketch.create", json!({"plane":{"origin":[0,0,20],"x_dir":[1,0,0],"y_dir":[0,1,0]}})).unwrap()["sketch"].as_u64().unwrap();
h.app.run("sketch.rectangle.two_point", json!({"p0":[5,5],"p1":[15,15]})).unwrap();
h.app.run("sketch.finish", json!({})).unwrap();
h.app.run("select.clear", json!({})).unwrap();
h.app.start("solid.extrude");
h.frames(2);
let d = h.app.dialog.as_ref().unwrap();
assert_eq!(d.inputs[0].items, vec![Sel::Profile { sketch: top, index: 0 }]);
assert!(matches!(d.kind, Kind::Extrude { operation: 1, .. }), "a boss on the top face joins: {:?}", d.kind);
h.step(&json!({"key":"Enter"})).unwrap();
assert!(h.app.dialog.is_none(), "{:?}", h.app.dialog);
h.step(&json!({"expect":{"bodies":1,"volume":9000}})).unwrap();
}
/// #684: what stops the pre-pick is a feature building from the sketch's profile (not just an
/// extrude), anywhere in the timeline; a feature that only follows its curves does not.
#[test]
fn profile_use_not_curve_use_stops_the_pre_pick() {
// A surface patch fills the sketch's region.
let mut app = app_with(json!([
{"command": "sketch.create", "params": {"plane": "XY", "name": "Region"}},
{"command": "sketch.rectangle.two_point", "params": {"p0": [0, 0], "p1": [20, 10]}},
{"command": "sketch.finish", "params": {}},
{"command": "surface.patch", "params": {"sketch": "Region"}},
]));
assert_eq!(app.session.doc.features.len(), 2);
assert!(start(&mut app, vec![], "solid.extrude").inputs[0].items.is_empty(), "the patched region is taken again");
// Rolled back to just after the sketch: the extrude after the marker still builds from it.
let mut app = app_with(json!([
{"command": "sketch.create", "params": {"plane": "XY", "name": "Base"}},
{"command": "sketch.rectangle.two_point", "params": {"p0": [0, 0], "p1": [20, 20]}},
{"command": "sketch.finish", "params": {}},
{"command": "solid.extrude", "params": {"distance": 20}},
{"command": "timeline.roll_to", "params": {"position": 1}},
]));
let base = app.session.doc.features.first().unwrap().id;
assert_eq!(app.session.world_state().sketch(base).map(|s| s.profiles.len()), Some(1));
assert!(start(&mut app, vec![], "solid.extrude").inputs[0].items.is_empty(), "the rolled-back extrude's profile is taken");
// A pipe follows the circle; the disc inside it is used by nothing.
let mut app = app_with(json!([
{"command": "sketch.create", "params": {"plane": "XY", "name": "Path"}},
{"command": "sketch.circle.center", "params": {"center": [0, 0], "radius": 20, "id": "c"}},
{"command": "sketch.finish", "params": {}},
{"command": "solid.pipe", "params": {"path_sketch": "Path", "path": ["c"], "diameter": 4}},
]));
let path = app.session.doc.features.first().unwrap().id;
assert_eq!(app.session.doc.features.len(), 2);
assert_eq!(start(&mut app, vec![], "solid.extrude").inputs[0].items, vec![Sel::Profile { sketch: path, index: 0 }]);
}
#[test]
fn edges_then_fillet_takes_them_with_chains() {
let mut app = boxed();