From d069e0e4be2ddd7bab82ef3a0f4c6a2f6daa9d0c Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Tue, 10 Nov 2026 21:08:57 -0800 Subject: [PATCH] Symbols: Break Link selects no art inside a hidden or locked group Break Link to Symbol on a selected group selected the art of every instance it expanded that was itself shown and unlocked, including art in a hidden or locked group inside the selection. The art now goes into the selection only when it and every group and layer holding it are shown and unlocked, the check Select All uses (select::is_selectable). --- crates/engine/src/cmd/brushsym/symbols.rs | 7 ++++-- crates/engine/src/tests/brushsym.rs | 28 +++++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/cmd/brushsym/symbols.rs b/crates/engine/src/cmd/brushsym/symbols.rs index ee4487ba7..395c07052 100644 --- a/crates/engine/src/cmd/brushsym/symbols.rs +++ b/crates/engine/src/cmd/brushsym/symbols.rs @@ -493,10 +493,13 @@ pub(super) fn symbol_break_link(s: &mut Session, p: &Value) -> Result { if ids.is_empty() { return Err(EngineError::Other("no symbol instances selected".into())); } + let iso = s.doc()?.isolation; let out = s.edit("Break Link to Symbol", |d, sel| { let out = break_links(d, &ids, true)?; - // Hidden or locked art is left out of the selection. - sel.set(out.iter().copied().filter(|id| d.node(*id).is_some_and(|n| n.visible && !n.locked))); + // Art that is hidden or locked, or in a hidden or locked group or layer, is left out of the + // selection. + let locks = d.locked_artboards(); + sel.set(out.iter().copied().filter(|id| crate::cmd::select::is_selectable(d, iso, &locks, *id))); Ok(out) })?; Ok(json!({ "ids": ids_json(&out) })) diff --git a/crates/engine/src/tests/brushsym.rs b/crates/engine/src/tests/brushsym.rs index 34f2cc4b0..ccf277585 100644 --- a/crates/engine/src/tests/brushsym.rs +++ b/crates/engine/src/tests/brushsym.rs @@ -1358,6 +1358,34 @@ fn break_link_keeps_hidden_and_locked_instances_hidden_and_locked() { assert!(selected.iter().all(|id| d.node(*id).is_some_and(|n| n.visible && !n.locked)), "{selected:?}"); } +/// Break Link to Symbol on a selected group does not select the art of a shown, unlocked instance +/// that sits in a hidden or a locked group inside it. +#[test] +fn break_link_leaves_art_in_hidden_or_locked_groups_unselected() { + let mut s = session(); + let (shown, name, _) = make_symbol(&mut s); + let subgroup = |s: &mut Session, y: i64| { + let id = NodeId(s.execute("symbol.place", &json!({"name": name, "x": 300, "y": y})).unwrap()["id"].as_u64().unwrap()); + select(s, &[id]); + NodeId(s.execute("object.group", &json!({})).unwrap()["id"].as_u64().unwrap()) + }; + let hidden = subgroup(&mut s, 200); + let locked = subgroup(&mut s, 100); + select(&mut s, &[shown, hidden, locked]); + let group = NodeId(s.execute("object.group", &json!({})).unwrap()["id"].as_u64().unwrap()); + let doc = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc); + doc.node_mut(hidden).unwrap().visible = false; + doc.node_mut(locked).unwrap().locked = true; + select(&mut s, &[group]); + let r = s.execute("symbol.breakLink", &json!({})).unwrap(); + assert_eq!(r["ids"].as_array().unwrap().len(), 3, "{r}"); + let d = &s.doc().unwrap().doc; + let selected: Vec = s.doc().unwrap().selection.objects.to_vec(); + assert_eq!(selected.len(), 1, "{selected:?}"); + let a = d.ancestry(selected[0]).unwrap(); + assert!(!a.contains(&hidden) && !a.contains(&locked), "{a:?}"); +} + /// Delete Symbol, expanding the instances, keeps a hidden instance's art hidden and a locked /// instance's art locked. #[test]