Symbols: Break Link keeps a hidden or locked instance's art hidden or locked

Break Link to Symbol on a selected group, and Delete Symbol with its
instances expanded, turned a hidden or locked instance into visible,
unlocked art. The art now keeps the instance's hidden and locked state,
as Object > Expand does, and Break Link leaves hidden or locked art out of
the selection.
This commit is contained in:
dotCooCoo
2026-11-10 20:09:28 -08:00
parent add0fcbe03
commit 2faadd28f4
4 changed files with 58 additions and 7 deletions
+1 -1
View File
@@ -208,7 +208,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Break Link to Symbol",
[],
None,
"{ids?|id?} expand selected instances into plain art (with their appearance changes to a dynamic symbol), placed through the symbol's 9-slice guides when it has them, with the instance's opacity mask → {ids}",
"{ids?|id?} expand selected instances into plain art (with their appearance changes to a dynamic symbol), placed through the symbol's 9-slice guides when it has them, with the instance's opacity mask; a hidden or locked instance's art stays hidden or locked and is not selected → {ids}",
has_selection,
symbol_break_link
),
+8 -5
View File
@@ -390,9 +390,9 @@ fn expanded_bound(d: &Document, inst: &Node, own: bool) -> u64 {
most.saturating_add(1).saturating_add(mask)
}
/// Replace instances `ids` with their art ([`expanded_instance`], `own` as there). Returns the
/// art's ids. Fails before any edit when the instances together would make more than
/// [`MAX_EXPAND_NODES`] objects.
/// Replace instances `ids` with their art ([`expanded_instance`], `own` as there). A hidden or
/// locked instance's art stays hidden or locked. Returns the art's ids. Fails before any edit when
/// the instances together would make more than [`MAX_EXPAND_NODES`] objects.
fn break_links(d: &mut Document, ids: &[NodeId], own: bool) -> Result<Vec<NodeId>> {
let total = ids.iter().filter_map(|id| d.node(*id)).fold(0u64, |sum, n| sum.saturating_add(expanded_bound(d, n, own)));
if total > MAX_EXPAND_NODES as u64 {
@@ -401,8 +401,10 @@ fn break_links(d: &mut Document, ids: &[NodeId], own: bool) -> Result<Vec<NodeId
let mut out = vec![];
for id in ids {
let Some(inst) = d.node(*id).cloned() else { continue };
let Some(art) = expanded_instance(d, &inst, own) else { continue };
let Some(mut art) = expanded_instance(d, &inst, own) else { continue };
let (par, idx, _) = d.position(*id).ok_or(EngineError::NoNode(*id))?;
art.visible &= inst.visible;
art.locked |= inst.locked;
let aid = art.id;
d.remove(*id)?;
d.insert(par, idx, art)?;
@@ -493,7 +495,8 @@ pub(super) fn symbol_break_link(s: &mut Session, p: &Value) -> Result<Value> {
}
let out = s.edit("Break Link to Symbol", |d, sel| {
let out = break_links(d, &ids, true)?;
sel.set(out.iter().copied());
// 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)));
Ok(out)
})?;
Ok(json!({ "ids": ids_json(&out) }))
+47
View File
@@ -1321,6 +1321,53 @@ fn break_link_counts_the_instances_mask_art() {
assert!(matches!(node(&s, inst).kind, NodeKind::SymbolInstance { .. }), "it stays an instance");
}
/// A hidden, a locked and a plain instance of one symbol in a group, the group selected.
fn group_with_hidden_and_locked_instances(s: &mut Session) -> (NodeId, String) {
let (shown, name, _) = make_symbol(s);
let hidden = NodeId(s.execute("symbol.place", &json!({"name": name, "x": 300, "y": 200})).unwrap()["id"].as_u64().unwrap());
let locked = NodeId(s.execute("symbol.place", &json!({"name": name, "x": 300, "y": 100})).unwrap()["id"].as_u64().unwrap());
select(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(s, &[group]);
(group, name)
}
/// The visibility and lock state of the children of `group`, in paint order.
fn shown_and_locked(s: &Session, group: NodeId) -> Vec<(bool, bool)> {
let g = node(s, group);
let NodeKind::Group { children, .. } = &g.kind else { panic!("a group: {}", g.kind_label()) };
assert!(children.iter().all(|c| !matches!(c.kind, NodeKind::SymbolInstance { .. })), "every instance is expanded");
children.iter().map(|c| (c.visible, c.locked)).collect()
}
/// Break Link to Symbol on a selected group keeps a hidden instance's art hidden and a locked
/// instance's art locked, and selects only the art that can be selected.
#[test]
fn break_link_keeps_hidden_and_locked_instances_hidden_and_locked() {
let mut s = session();
let (group, _) = group_with_hidden_and_locked_instances(&mut s);
let r = s.execute("symbol.breakLink", &json!({})).unwrap();
assert_eq!(r["ids"].as_array().unwrap().len(), 3, "{r}");
assert_eq!(shown_and_locked(&s, group), vec![(true, false), (false, false), (true, true)]);
let d = &s.doc().unwrap().doc;
let selected: Vec<NodeId> = s.doc().unwrap().selection.objects.to_vec();
assert_eq!(selected.len(), 1, "{selected:?}");
assert!(selected.iter().all(|id| d.node(*id).is_some_and(|n| n.visible && !n.locked)), "{selected:?}");
}
/// Delete Symbol, expanding the instances, keeps a hidden instance's art hidden and a locked
/// instance's art locked.
#[test]
fn delete_symbol_keeps_hidden_and_locked_instances_hidden_and_locked() {
let mut s = session();
let (group, name) = group_with_hidden_and_locked_instances(&mut s);
s.execute("symbol.delete", &json!({"name": name})).unwrap();
assert_eq!(shown_and_locked(&s, group), vec![(true, false), (false, false), (true, true)]);
}
#[test]
fn delete_duplicate_and_replace_symbols() {
let mut s = session();
+2 -1
View File
@@ -2014,7 +2014,8 @@ already walks Image Trace. Short agent entry points:
a symbol with guides keeps its corners at their size, and `symbol.breakLink` gives its art as it draws; dragging a
guide on the canvas in symbol editing mode sends `nineSliceGuides` in one undo step; `type` is
Symbol Type, `static` by default for the command and `dynamic` by default in the Symbol Options dialog),
`symbol.place {name?, x?, y?}`, `symbol.breakLink {ids?}` (the art keeps the instance's opacity mask), `symbol.replace`,
`symbol.place {name?, x?, y?}`, `symbol.breakLink {ids?}` (the art keeps the instance's opacity mask; a hidden or locked instance's
art stays hidden or locked and is not selected), `symbol.replace`,
`symbol.edit {id?, ids?, name?, x?, y?, artboard?, viewCenter?}` (symbol editing mode: the symbol's art on a temporary
isolated layer, where the instance `id` shows it or centered on (x, y), by default on the center of artboard
`artboard`, the active one the app passes, or in a document without artboards on `viewCenter`, the view's center the