engine: an extrude Join, Cut or Intersect leaves hidden bodies alone (#2464)

With no targets picked, an extrude's Join, Cut or Intersect changed every body
of the active component it met, shown or hidden: a hidden coincident copy was
joined in and disappeared. Where some bodies of the active component are
hidden, the shown ones become the feature's participants, kept with it, so
showing or hiding bodies later doesn't change the model. Explicit targets and
participants, and a face's or a face sketch's own body, still win.

Test: the issue's plate with a hidden copy and a rim joined on.

Main reached the same fix for Cut and Intersect in #3961 (its targets-based
shown_scope in extrude); this one, which also covers Join and keeps the shown
bodies as participants, replaces it. Its test stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
This commit is contained in:
jyviko
2026-10-27 01:35:03 -04:00
parent 386b1d123f
commit 5df8a55657
2 changed files with 44 additions and 14 deletions
+20 -14
View File
@@ -693,6 +693,17 @@ fn add_feature_in(s: &mut Session, p: &Value, kind: FeatureKind, world: bool) ->
} else {
participants
};
// An extrude's Join, Cut or Intersect with no bodies picked changes the bodies of the active
// component that are shown, never a hidden one (#2464). The shown ones are kept as its
// participants, so showing or hiding bodies later doesn't change the model.
let participants = match &kind {
FeatureKind::Extrude { operation: Operation::Join | Operation::Cut | Operation::Intersect, targets, .. }
if participants.is_empty() && targets.is_empty() =>
{
shown_scope(s).unwrap_or_default()
}
_ => participants,
};
if matches!(kind, FeatureKind::ConstructionPlane { .. })
&& let Some(n) = name
{
@@ -828,9 +839,6 @@ fn extrude(s: &mut Session, p: &Value) -> Result<Value> {
} else if let Some(parts) = cut_scope(s, &p, operation, sketch, face_body)? {
p.to_mut()["participants"] = json!(parts);
}
if matches!(operation, Operation::Cut | Operation::Intersect) && targets.is_empty() && string_list(&p, "participants").is_empty() {
targets = shown_scope(s);
}
let kind = FeatureKind::Extrude {
sketch: SketchId(sketch),
profiles: match face_profile {
@@ -975,17 +983,6 @@ fn face_body_reached(s: &Session, sketch: u64) -> Result<Option<String>> {
Ok(Some(body.name.clone()))
}
/// A cut or intersect that names no bodies leaves hidden bodies alone, as hiding a body to keep
/// it out of a cut is the usual way to (#3961): while some of the active component's bodies
/// are hidden, its shown ones, stored as the feature's targets so showing the others again
/// later changes nothing. Empty (every body, as before) when none is hidden, or all are.
fn shown_scope(s: &Session) -> Vec<String> {
let st = s.model.state();
let scope = active_scope(s, &st);
let shown: Vec<String> = scope.iter().filter(|b| s.body_visible(&b.name)).map(|b| b.name.clone()).collect();
if shown.len() == scope.len() { Vec::new() } else { shown }
}
/// The body whose face `sketch` is attached to (on the face itself, not offset from it), when
/// it is one the active component works on ([`active_scope`]).
fn sketch_face_body(s: &Session, sketch: u64) -> Option<String> {
@@ -2961,6 +2958,15 @@ fn offset_face(s: &mut Session, p: &Value) -> Result<Value> {
)
}
/// The shown bodies of the active component, where some of its bodies are hidden and some shown;
/// `None` where all of them are shown (or none), and the feature's usual scope applies.
fn shown_scope(s: &Session) -> Option<Vec<String>> {
let st = s.model.state();
let (shown, hidden): (Vec<&solidcraft_doc::ModelBody>, Vec<_>) =
st.bodies.iter().filter(|b| s.doc.model_body_component(b) == s.active_component).partition(|b| s.body_visible(&b.name));
(!shown.is_empty() && !hidden.is_empty()).then(|| shown.iter().map(|b| b.name.clone()).collect())
}
#[cfg(test)]
#[path = "patterns_tests.rs"]
mod patterns_tests;
+24
View File
@@ -4493,3 +4493,27 @@ fn a_cut_leaves_hidden_bodies_alone() {
let shown = setup(false);
assert!(vol(&shown, "Below") < full - 1.0 && vol(&shown, "Above") < full - 1.0, "with nothing hidden, both are cut");
}
/// #2464: an extrude Join with no targets onto a plate with a hidden coincident copy changes the
/// shown plate only, and keeps the shown bodies as its participants.
#[test]
fn join_leaves_a_hidden_body_alone() {
let mut s = Session::default();
run(&mut s, "solid.box", json!({"length": 40, "width": 40, "height": 5, "body_name": "Plate"}));
run(&mut s, "solid.move", json!({"bodies": ["Plate"], "copy": true}));
run(&mut s, "browser.visibility", json!({"items": ["Plate (2)"], "visible": false}));
run(&mut s, "sketch.create", json!({"plane": "XY", "offset": 5, "name": "Rim"}));
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [2, 2], "p1": [38, 38]}));
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [4, 4], "p1": [36, 36]}));
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "solid.extrude", json!({"sketch": "Rim", "profiles": [{"point": [3, 3]}], "distance": 3, "operation": "join"}));
let st = s.model.state();
assert_eq!(st.bodies.len(), 2);
let v = |n: &str| solidcraft_kernel::measure(&st.body(n).unwrap().body).unwrap().volume;
assert!(rel(v("Plate"), 8816.0) < 1e-9 && rel(v("Plate (2)"), 8000.0) < 1e-9);
let f = s.doc.features.last().unwrap();
assert_eq!(f.participants, vec!["Plate".to_string()]);
// Showing the copy again doesn't change the model.
run(&mut s, "browser.visibility", json!({"items": ["Plate (2)"], "visible": true}));
assert_eq!(s.model.state().bodies.len(), 2);
}