Merge pull request #5007 from mockydevs/feat/symbols-select-unused-4918

Fix #4918: Symbols panel Select All Unused
This commit is contained in:
BFlat
2026-11-01 07:57:26 +07:00
committed by GitHub
33 changed files with 455 additions and 30 deletions
+11 -2
View File
@@ -38,7 +38,7 @@ use symbols::selected_instances;
pub(crate) use symbols::{instance_in_place, natural_size, normalize, place_xf, set_natural_size, store_symbol};
use symbols::{
symbol_adjust, symbol_break_link, symbol_delete, symbol_duplicate, symbol_edit, symbol_list, symbol_new, symbol_options, symbol_place,
symbol_replace, symbol_select_instances, symbol_set_current, symbol_spray, symbol_update,
symbol_replace, symbol_select_instances, symbol_set_current, symbol_spray, symbol_unused, symbol_update,
};
const SIZES_KEY: &str = vectorcraft_doc::SYMBOL_SIZES;
@@ -228,7 +228,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Delete Symbol",
[],
None,
"{name, expandInstances?: true} delete a symbol; its instances are expanded (or deleted with false)",
"{name | names: [name…], expandInstances?: true} delete a symbol (or several, in one undo step); its instances are expanded (or deleted with false)",
has_doc,
symbol_delete
),
@@ -269,6 +269,15 @@ pub fn specs() -> Vec<CommandSpec> {
has_doc,
symbol_set_current
),
cmd!(
query "symbol.unused",
"Select All Unused",
[],
None,
"{} → {names: [the symbols nothing places, in panel order]} (the Symbols panel selects them): instances in the art (opacity masks included), graph designs, pattern tiles (with the tiles that pattern editing's Cancel puts back) and brush art count, and so do instances in the art of a symbol that is used",
has_doc,
symbol_unused
),
cmd!(query "symbol.selectInstances", "Select All Instances", [], None, "{name} select every instance of a symbol → {count}", has_doc, symbol_select_instances),
cmd!(
"symbol.spray",
+93 -15
View File
@@ -107,6 +107,37 @@ fn instances_of(d: &Document, name: Option<&str>) -> Vec<NodeId> {
v
}
/// The symbols an instance in `n` (its opacity masks included) places, pushed onto `out`.
fn art_symbols(n: &Node, out: &mut Vec<String>) {
vectorcraft_doc::swatches::walk_with_masks(n, &mut |m| {
if let NodeKind::SymbolInstance { symbol, .. } = &m.kind {
out.push(symbol.clone());
}
});
}
/// Select All Unused (#4918): the symbols nothing places. Instances in the art (opacity masks
/// included), graph designs, pattern tiles and the tiles that pattern editing's Cancel puts back,
/// and the art of the document's brushes count, and so do the instances in the art of every symbol
/// a counted instance places. A symbol placed only inside an unused symbol is unused too.
pub(super) fn symbol_unused(s: &mut Session, _: &Value) -> Result<Value> {
let d = &s.doc()?.doc;
let mut todo = vec![];
let lib = brush::library(d);
let patterns = d.patterns.iter().chain(d.pattern_edit.iter().filter_map(|pe| pe.original.as_ref()));
let trees = d.layers.iter().map(|l| &**l).chain(d.graph_designs.iter().map(|g| &*g.art));
trees.chain(patterns.flat_map(|p| p.art.iter().map(|n| &**n))).chain(lib.iter().flat_map(|b| b.art())).for_each(|n| art_symbols(n, &mut todo));
let mut used = std::collections::BTreeSet::new();
while let Some(name) = todo.pop() {
if !used.contains(&name) {
d.symbols.iter().filter(|sym| sym.name == name).for_each(|sym| art_symbols(&sym.art, &mut todo));
used.insert(name);
}
}
let names: Vec<&str> = d.symbols.iter().map(|sym| sym.name.as_str()).filter(|n| !used.contains(*n)).collect();
Ok(json!({ "names": names }))
}
fn symbol_names(d: &Document) -> Vec<String> {
d.symbols.iter().map(|s| s.name.clone()).collect()
}
@@ -417,27 +448,74 @@ pub(super) fn symbol_update(s: &mut Session, p: &Value) -> Result<Value> {
Ok(json!({ "name": name, "id": id.0 }))
}
/// How many levels of nested instances Delete Symbol expands before it gives up.
const MAX_DELETE_ROUNDS: usize = 256;
pub(super) fn symbol_delete(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "symbol.delete";
let name = name_param(p, C)?.to_string();
if !s.doc()?.doc.symbols.iter().any(|x| x.name == name) {
// `names` deletes several symbols in one undo step (the Symbols panel's Select All Unused, #4918).
let names: Vec<String> = match p.get("names") {
Some(v) => {
let a = v.as_array().ok_or_else(|| bad(C, "names must be an array of symbol names"))?;
let mut names = Vec::with_capacity(a.len());
for n in a {
let n = n.as_str().ok_or_else(|| bad(C, "names must be an array of symbol names"))?;
if !names.iter().any(|x: &String| x == n) {
names.push(n.to_string());
}
}
if names.is_empty() {
return Err(bad(C, "names is empty"));
}
names
}
None => vec![name_param(p, C)?.to_string()],
};
let d = &s.doc()?.doc;
if let Some(name) = names.iter().find(|n| !d.symbols.iter().any(|x| &x.name == *n)) {
return Err(bad(C, format!("no symbol named `{name}`")));
}
let expand = switch_or(p, "expandInstances", true, C)?;
s.edit("Delete Symbol", |d, _| {
let inst = instances_of(d, Some(&name));
if expand {
break_links(d, &inst, true)?;
} else {
remove_each(d, &inst)?;
let label = if names.len() == 1 { "Delete Symbol" } else { "Delete Symbols" };
s.edit(label, |d, _| {
// Every instance goes before any definition, whatever the order of `names`: expanding an
// instance of one symbol can bring out instances of another being deleted (nested in its
// art), which expand in turn while their definitions are still there. Each round expands
// one level of nesting, so the rounds are capped.
let of_names = |d: &Document| {
let mut v = vec![];
d.walk(|n| {
if let NodeKind::SymbolInstance { symbol, .. } = &n.kind
&& names.contains(symbol)
{
v.push(n.id);
}
});
v
};
for _ in 0..=MAX_DELETE_ROUNDS {
let inst = of_names(d);
if inst.is_empty() {
break;
}
if expand {
break_links(d, &inst, true)?;
} else {
remove_each(d, &inst)?;
}
}
d.symbols.retain(|x| x.name != name);
set_natural_size(d, &name, None);
d.set_symbol_registration(&name, SymbolRegistration::CENTER);
d.clear_symbol_export_type(&name);
d.set_symbol_nine_slice(&name, None);
if d.unknown.get(CURRENT_SYMBOL).and_then(Value::as_str) == Some(name.as_str()) {
d.unknown.remove(CURRENT_SYMBOL);
if !of_names(d).is_empty() {
return Err(EngineError::Other("the symbols are nested too deeply to expand their instances".into()));
}
for name in &names {
d.symbols.retain(|x| &x.name != name);
set_natural_size(d, name, None);
d.set_symbol_registration(name, SymbolRegistration::CENTER);
d.clear_symbol_export_type(name);
d.set_symbol_nine_slice(name, None);
if d.unknown.get(CURRENT_SYMBOL).and_then(Value::as_str) == Some(name.as_str()) {
d.unknown.remove(CURRENT_SYMBOL);
}
}
Ok(())
})?;
+69
View File
@@ -1432,3 +1432,72 @@ fn symbol_nine_slice_guides_round_trip() {
s.execute("symbol.delete", &json!({"name": "Box copy"})).unwrap();
assert!(s.doc().unwrap().doc.unknown.get(vectorcraft_doc::SYMBOL_NINE_SLICE).is_none());
}
/// Symbols panel › Select All Unused (#4918): a symbol counts as used when an instance in the art,
/// a pattern tile, the art of a brush or a used symbol places it; one placed only inside an unused
/// symbol is unused too. Delete Symbol with `names` deletes several in one undo step.
#[test]
fn symbol_unused_lists_the_symbols_nothing_places_and_delete_takes_several() {
let mut s = session();
// A symbol of the selection (a new square at `x` when `x` is given), left placed.
let symbol = |s: &mut Session, name: &str, x: Option<f64>| {
if let Some(x) = x {
rect(s, x, 0.0, 10.0, 10.0);
}
NodeId(s.execute("symbol.new", &json!({"name": name})).unwrap()["id"].as_u64().unwrap())
};
let unused =
|s: &mut Session| -> Vec<String> { serde_json::from_value(s.execute("symbol.unused", &json!({})).unwrap()["names"].clone()).unwrap() };
// Placed: its instance stays on the artboard.
symbol(&mut s, "Placed", Some(0.0));
// Inner is placed only inside Outer, whose instance stays: both are used.
let inner = symbol(&mut s, "Inner", Some(20.0));
s.execute("select.set", &json!({"ids": [inner.0]})).unwrap();
symbol(&mut s, "Outer", None);
// Lonely's and Nested's instances go; Nested is placed only inside Lonely.
let nested = symbol(&mut s, "Nested", Some(40.0));
s.execute("select.set", &json!({"ids": [nested.0]})).unwrap();
let lonely = symbol(&mut s, "Lonely", None);
s.execute("edit.clear", &json!({"ids": [lonely.0]})).unwrap();
// In a brush's art only.
let in_brush = symbol(&mut s, "Brushed", Some(60.0));
s.execute("brush.new", &json!({"type": "art", "name": "B", "ids": [in_brush.0]})).unwrap();
s.execute("edit.clear", &json!({"ids": [in_brush.0]})).unwrap();
assert_eq!(unused(&mut s), ["Nested", "Lonely"], "in panel order");
// A placed one becomes unused when its last instance goes.
let placed = s.doc().unwrap().doc.layers[0]
.children()
.unwrap()
.iter()
.find(|n| matches!(&n.kind, NodeKind::SymbolInstance { symbol, .. } if symbol == "Placed"))
.unwrap()
.id;
s.execute("edit.clear", &json!({"ids": [placed.0]})).unwrap();
assert_eq!(unused(&mut s), ["Placed", "Nested", "Lonely"]);
// Delete Symbol takes them all in one undo step, and Undo brings them back.
let before = s.doc().unwrap().history.undo.len();
s.execute("symbol.delete", &json!({"names": ["Placed", "Nested", "Lonely"]})).unwrap();
let names = |s: &Session| s.doc().unwrap().doc.symbols.iter().map(|x| x.name.clone()).collect::<Vec<_>>();
assert_eq!(names(&s), ["Inner", "Outer", "Brushed"]);
assert_eq!(s.doc().unwrap().history.undo.len(), before + 1);
assert!(unused(&mut s).is_empty());
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(names(&s), ["Placed", "Inner", "Outer", "Nested", "Lonely", "Brushed"]);
// Deleting a symbol and the one placed inside it, in either order, expands every instance
// while both definitions are still there: no instance is left without its symbol.
for order in [["Inner", "Outer"], ["Outer", "Inner"]] {
s.execute("symbol.delete", &json!({"names": order})).unwrap();
let mut dangling = 0;
s.doc()
.unwrap()
.doc
.walk(|n| dangling += usize::from(matches!(&n.kind, NodeKind::SymbolInstance { symbol, .. } if symbol == "Inner" || symbol == "Outer")));
assert_eq!(dangling, 0, "{order:?}");
s.execute("edit.undo", &json!({})).unwrap();
}
// Bad `names` are refused and change nothing.
for bad in [json!({"names": []}), json!({"names": "Placed"}), json!({"names": [1]}), json!({"names": ["Placed", "Nope"]})] {
assert!(s.execute("symbol.delete", &bad).is_err(), "{bad}");
}
assert_eq!(names(&s).len(), 6);
}
+58 -1
View File
@@ -20,7 +20,14 @@ pub const KIND: &str = "deleteSymbol";
pub const USED: &str = "deleteUsedSymbol";
pub(super) const SPEC: DialogSpec = DialogSpec {
heading: |_| tl!("Delete the selected symbol?").into(),
heading: |d| {
if d.fields.get("names").and_then(Value::as_array).is_some_and(|n| n.len() > 1) {
tl!("Delete the selected symbols?")
} else {
tl!("Delete the selected symbol?")
}
.into()
},
body: |_, _, _| false,
confirm: confirm_unused,
ok: Some("Yes"),
@@ -55,6 +62,37 @@ pub(crate) fn ask(app: &mut VectorcraftApp, name: &str, now: bool) {
}
}
/// Delete Symbol for the unused symbols `names` that Select All Unused picked (#4918): asked once
/// ("Delete the selected symbols?", or the singular for one), or at once with `now`. Only those still
/// unused then (`symbol.unused`: brush art, patterns and nested instances count) go, in one undo step.
pub(crate) fn ask_many(app: &mut VectorcraftApp, names: &[String], now: bool) {
let Some(uid) = app.session.active().map(|st| st.uid) else { return };
if now {
// An error already went to the status bar.
let _ = delete_unused(app, uid, names);
} else {
app.ui.dialog = Some(Dialog::new(KIND, json!({ "names": names, "doc": uid })));
}
}
/// Deletes those of `names` that are still unused in document `uid` while it is the active one.
fn delete_unused(app: &mut VectorcraftApp, uid: u64, names: &[String]) -> DialogResult {
if app.session.active().map(|st| st.uid) != Some(uid) {
return Ok(Value::Null);
}
let unused: Vec<String> =
app.run("symbol.unused", json!({})).ok().and_then(|r| serde_json::from_value(r["names"].clone()).ok()).unwrap_or_default();
let names: Vec<&String> = names.iter().filter(|n| unused.contains(n)).collect();
if names.is_empty() {
return Ok(Value::Null);
}
let r = app.run("symbol.delete", json!({ "names": names }));
if let Err(e) = &r {
app.status(e.clone());
}
r
}
fn open(app: &mut VectorcraftApp, kind: &str, name: &str, uid: u64) {
app.ui.dialog = Some(Dialog::new(kind, json!({ "name": name, "doc": uid })));
}
@@ -78,6 +116,9 @@ fn target(d: &Dialog) -> (u64, String) {
fn confirm_unused(app: &mut VectorcraftApp, d: &Dialog) -> DialogResult {
app.ui.dialog = None;
let (uid, name) = target(d);
if let Some(names) = d.fields.get("names").and_then(|v| serde_json::from_value::<Vec<String>>(v.clone()).ok()) {
return delete_unused(app, uid, &names);
}
match instances(app, uid, &name) {
None => Ok(Value::Null),
Some(0) => app.run("symbol.delete", json!({ "name": name })),
@@ -98,3 +139,19 @@ fn confirm_used(app: &mut VectorcraftApp, d: &Dialog) -> DialogResult {
}
app.run("symbol.delete", json!({ "name": name, "expandInstances": !d.bool("discard") }))
}
#[cfg(test)]
mod tests {
use super::*;
/// The question for the symbols Select All Unused picked is plural (#4918).
#[test]
fn several_symbols_get_the_plural_question() {
let one = Dialog::new(KIND, json!({"name": "A", "doc": 1}));
let many = Dialog::new(KIND, json!({"names": ["A", "B"], "doc": 1}));
assert_eq!((SPEC.heading)(&one), "Delete the selected symbol?");
assert_eq!((SPEC.heading)(&many), "Delete the selected symbols?");
let picked_one = Dialog::new(KIND, json!({"names": ["A"], "doc": 1}));
assert_eq!((SPEC.heading)(&picked_one), "Delete the selected symbol?");
}
}
+3
View File
@@ -736,6 +736,9 @@ font-technology Variable Променливи
Delete Swatch Изтриване на образец
Delete Symbol Изтриване на символ
Delete the selected symbol? Да се изтрие ли избраният символ?
Delete the selected symbols? Да се изтрият ли избраните символи?
No unused symbols Няма неизползвани символи
Delete Symbols Изтриване на символи
One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. Един или повече от символите се използват и не могат да бъдат изтрити, докато екземплярите им не бъдат разширени или изтрити.
Expand Instances Разширяване на екземплярите
Delete Instances Изтриване на екземплярите
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -3068,6 +3068,9 @@ theme Appearance Izgled
Delete Brush Izbriši četkicu
Delete Symbol Izbriši simbol
Delete the selected symbol? Izbrisati odabrani simbol?
Delete the selected symbols? Izbrisati odabrane simbole?
No unused symbols Nema nekorištenih simbola
Delete Symbols Brisanje simbola
One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. Jedan ili više simbola se koriste i ne mogu se izbrisati dok se njihove instance ne prošire ili izbrišu.
Expand Instances Proširi instance
Delete Instances Izbriši instance
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -1053,6 +1053,9 @@
Delete Swatch Suprimeix la mostra
Delete Symbol Suprimeix el símbol
Delete the selected symbol? Voleu suprimir el símbol seleccionat?
Delete the selected symbols? Voleu suprimir els símbols seleccionats?
No unused symbols No hi ha símbols sense utilitzar
Delete Symbols Suprimeix els símbols
One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. Un o més dels símbols estan en ús i no es poden suprimir fins que se n’expandeixin o se’n suprimeixin les instàncies.
Expand Instances Expandeix les instàncies
Delete Instances Suprimeix les instàncies
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -3099,6 +3099,9 @@ theme Appearance Motiv
Delete Brush Odstranit štětec
Delete Symbol Odstranit symbol
Delete the selected symbol? Odstranit vybraný symbol?
Delete the selected symbols? Odstranit vybrané symboly?
No unused symbols Žádné nepoužité symboly
Delete Symbols Odstranit symboly
One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. Jeden nebo více symbolů se používá a nelze je odstranit, dokud jejich instance nerozložíte nebo neodstraníte.
Expand Instances Rozložit instance
Delete Instances Odstranit instance
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -2855,6 +2855,9 @@ theme Appearance Udseende
Delete Brush Slet pensel
Delete Symbol Slet symbol
Delete the selected symbol? Vil du slette det markerede symbol?
Delete the selected symbols? Vil du slette de markerede symboler?
No unused symbols Ingen ubrugte symboler
Delete Symbols Slet symboler
One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. Et eller flere af symbolerne er i brug og kan ikke slettes, før deres forekomster er udvidet eller slettet.
Expand Instances Udvid forekomster
Delete Instances Slet forekomster
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -730,6 +730,9 @@ font-technology Variable Variabel
Delete Swatch Farbfeld löschen
Delete Symbol Symbol löschen
Delete the selected symbol? Ausgewähltes Symbol löschen?
Delete the selected symbols? Ausgewählte Symbole löschen?
No unused symbols Keine nicht verwendeten Symbole
Delete Symbols Symbole löschen
One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. Mindestens eines der Symbole wird verwendet und kann erst gelöscht werden, wenn seine Instanzen umgewandelt oder gelöscht wurden.
Expand Instances Instanzen umwandeln
Delete Instances Instanzen löschen
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -710,6 +710,9 @@ font-technology Variable Μεταβλητές
Delete Swatch Διαγραφή δείγματος χρώματος
Delete Symbol Διαγραφή συμβόλου
Delete the selected symbol? Να διαγραφεί το επιλεγμένο σύμβολο;
Delete the selected symbols? Να διαγραφούν τα επιλεγμένα σύμβολα;
No unused symbols Δεν υπάρχουν αχρησιμοποίητα σύμβολα
Delete Symbols Διαγραφή συμβόλων
One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. Ένα ή περισσότερα από τα σύμβολα χρησιμοποιούνται και δεν μπορούν να διαγραφούν μέχρι να επεκταθούν ή να διαγραφούν τα στιγμιότυπά τους.
Expand Instances Επέκταση στιγμιοτύπων
Delete Instances Διαγραφή στιγμιοτύπων
Can't render this file because it contains an unexpected character in line 164 and column 5.
+3
View File
@@ -751,6 +751,9 @@ font-technology Variable Variables
Delete Swatch Eliminar muestra
Delete Symbol Eliminar símbolo
Delete the selected symbol? ¿Eliminar el símbolo seleccionado?
Delete the selected symbols? ¿Eliminar los símbolos seleccionados?
No unused symbols No hay símbolos sin usar
Delete Symbols Eliminar símbolos
One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. Uno o más de los símbolos están en uso y no se pueden eliminar hasta que se expandan o eliminen sus instancias.
Expand Instances Expandir instancias
Delete Instances Eliminar instancias
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -3038,6 +3038,9 @@ theme Appearance Ulkoasu
Delete Brush Poista sivellin
Delete Symbol Poista symboli
Delete the selected symbol? Poistetaanko valittu symboli?
Delete the selected symbols? Poistetaanko valitut symbolit?
No unused symbols Ei käyttämättömiä symboleja
Delete Symbols Poista symbolit
One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. Yksi tai useampi symboleista on käytössä, eikä niitä voi poistaa ennen kuin niiden ilmentymät on laajennettu tai poistettu.
Expand Instances Laajenna ilmentymät
Delete Instances Poista ilmentymät
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -756,6 +756,9 @@ font-technology Variable Variables
Delete Swatch Supprimer la nuance
Delete Symbol Supprimer le symbole
Delete the selected symbol? Supprimer le symbole sélectionné ?
Delete the selected symbols? Supprimer les symboles sélectionnés ?
No unused symbols Aucun symbole inutilisé
Delete Symbols Supprimer les symboles
One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. Un ou plusieurs symboles sont utilisés et ne peuvent pas être supprimés tant que leurs instances ne sont pas décomposées ou supprimées.
Expand Instances Décomposer les instances
Delete Instances Supprimer les instances
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -975,6 +975,9 @@ font-technology Variable Változó
Delete Swatch Színminta törlése
Delete Symbol Szimbólum törlése
Delete the selected symbol? Törli a kijelölt szimbólumot?
Delete the selected symbols? Törli a kijelölt szimbólumokat?
No unused symbols Nincsenek nem használt szimbólumok
Delete Symbols Szimbólumok törlése
One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. Egy vagy több szimbólum használatban van, és nem törölhető, amíg a példányait ki nem bontja vagy nem törli.
Expand Instances Példányok kibontása
Delete Instances Példányok törlése
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -2855,6 +2855,9 @@ theme Appearance Tampilan
Delete Brush Hapus kuas
Delete Symbol Hapus simbol
Delete the selected symbol? Hapus simbol yang dipilih?
Delete the selected symbols? Hapus simbol yang dipilih?
No unused symbols Tidak ada simbol yang tidak digunakan
Delete Symbols Hapus Simbol
One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. Satu atau beberapa simbol sedang digunakan dan tidak dapat dihapus sampai instansinya diperluas atau dihapus.
Expand Instances Perluas instansi
Delete Instances Hapus instansi
Can't render this file because it contains an unexpected character in line 2190 and column 5.
+3
View File
@@ -751,6 +751,9 @@ font-technology Variable Variabili
Delete Swatch Elimina campione
Delete Symbol Elimina simbolo
Delete the selected symbol? Eliminare il simbolo selezionato?
Delete the selected symbols? Eliminare i simboli selezionati?
No unused symbols Nessun simbolo inutilizzato
Delete Symbols Elimina simboli
One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. Uno o più simboli sono in uso e non possono essere eliminati finché le loro istanze non vengono espanse o eliminate.
Expand Instances Espandi istanze
Delete Instances Elimina istanze
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -1308,6 +1308,9 @@ digits Malayalam マラヤーラム数字
Delete Swatch スウォッチを削除
Delete Symbol シンボルを削除
Delete the selected symbol? 選択したシンボルを削除しますか?
Delete the selected symbols? 選択したシンボルを削除しますか?
No unused symbols 未使用のシンボルはありません
Delete Symbols シンボルを削除
One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. 1 つ以上のシンボルが使用中のため、インスタンスを拡張または削除するまで削除できません。
Expand Instances インスタンスを拡張
Delete Instances インスタンスを削除
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -2668,6 +2668,9 @@ theme Appearance იერსახე
Delete Brush ფუნჯის წაშლა
Delete Symbol სიმბოლოს წაშლა
Delete the selected symbol? წაიშალოს მონიშნული სიმბოლო?
Delete the selected symbols? წაიშალოს მონიშნული სიმბოლოები?
No unused symbols გამოუყენებელი სიმბოლოები არ არის
Delete Symbols სიმბოლოების წაშლა
One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. ერთი ან მეტი სიმბოლო გამოიყენება და მისი წაშლა შეუძლებელია, სანამ მისი ეგზემპლარები არ გაიშლება ან არ წაიშლება.
Expand Instances ეგზემპლარების გაშლა
Delete Instances ეგზემპლარების წაშლა
Can't render this file because it is too large.
+3
View File
@@ -1298,6 +1298,9 @@ digits Malayalam 말라얄람어
Delete Swatch 견본 삭제
Delete Symbol 심볼 삭제
Delete the selected symbol? 선택한 심볼을 삭제하시겠습니까?
Delete the selected symbols? 선택한 심볼을 모두 삭제하시겠습니까?
No unused symbols 사용하지 않는 심볼이 없습니다
Delete Symbols 심볼 삭제
One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. 하나 이상의 심볼이 사용 중이므로 인스턴스를 확장하거나 삭제하기 전에는 삭제할 수 없습니다.
Expand Instances 인스턴스 확장
Delete Instances 인스턴스 삭제
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -2869,6 +2869,9 @@ theme Appearance Vormgeving
Delete Brush Penseel verwijderen
Delete Symbol Symbool verwijderen
Delete the selected symbol? Het geselecteerde symbool verwijderen?
Delete the selected symbols? De geselecteerde symbolen verwijderen?
No unused symbols Geen ongebruikte symbolen
Delete Symbols Symbolen verwijderen
One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. Een of meer van de symbolen zijn in gebruik en kunnen pas worden verwijderd als hun exemplaren zijn uitgebreid of verwijderd.
Expand Instances Exemplaren uitbreiden
Delete Instances Exemplaren verwijderen
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -2978,6 +2978,9 @@ theme Appearance Wygląd
Delete Brush Usuń pędzel
Delete Symbol Usuń symbol
Delete the selected symbol? Usunąć zaznaczony symbol?
Delete the selected symbols? Usunąć zaznaczone symbole?
No unused symbols Brak nieużywanych symboli
Delete Symbols Usuń symbole
One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. Co najmniej jeden z symboli jest używany i nie można go usunąć, dopóki jego wystąpienia nie zostaną rozszerzone lub usunięte.
Expand Instances Rozszerz wystąpienia
Delete Instances Usuń wystąpienia
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -664,6 +664,9 @@
Delete Swatch Excluir Amostra
Delete Symbol Excluir Símbolo
Delete the selected symbol? Excluir o símbolo selecionado?
Delete the selected symbols? Excluir os símbolos selecionados?
No unused symbols Nenhum símbolo não utilizado
Delete Symbols Excluir símbolos
One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. Um ou mais símbolos estão em uso e não podem ser excluídos até que suas instâncias sejam expandidas ou excluídas.
Expand Instances Expandir Instâncias
Delete Instances Excluir Instâncias
Can't render this file because it contains an unexpected character in line 6 and column 47.
+3
View File
@@ -749,6 +749,9 @@ font-technology Variable Вариативные
Delete Swatch Удалить образец
Delete Symbol Удалить символ
Delete the selected symbol? Удалить выбранный символ?
Delete the selected symbols? Удалить выбранные символы?
No unused symbols Нет неиспользуемых символов
Delete Symbols Удалить символы
One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. Один или несколько символов используются, и их нельзя удалить, пока их экземпляры не будут разобраны или удалены.
Expand Instances Разобрать экземпляры
Delete Instances Удалить экземпляры
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -738,6 +738,9 @@ font-technology Variable Variabla
Delete Swatch Radera färgruta
Delete Symbol Radera symbol
Delete the selected symbol? Vill du radera den markerade symbolen?
Delete the selected symbols? Vill du radera de markerade symbolerna?
No unused symbols Inga oanvända symboler
Delete Symbols Radera symboler
One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. En eller flera av symbolerna används och kan inte raderas förrän deras instanser har expanderats eller raderats.
Expand Instances Expandera instanser
Delete Instances Radera instanser
Can't render this file because it contains an unexpected character in line 5 and column 25.
+3
View File
@@ -730,6 +730,9 @@ font-technology Variable Değişken
Delete Swatch Renk Örneğini Sil
Delete Symbol Sembolü Sil
Delete the selected symbol? Seçili sembol silinsin mi?
Delete the selected symbols? Seçili semboller silinsin mi?
No unused symbols Kullanılmayan sembol yok
Delete Symbols Sembolleri Sil
One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. Sembollerden biri veya birkaçı kullanımda ve örnekleri genişletilene ya da silinene kadar silinemez.
Expand Instances Örnekleri Genişlet
Delete Instances Örnekleri Sil
Can't render this file because it contains an unexpected character in line 5 and column 25.
+3
View File
@@ -745,6 +745,9 @@ font-technology Variable Варіативні
Delete Swatch Видалити зразок
Delete Symbol Видалити символ
Delete the selected symbol? Видалити вибраний символ?
Delete the selected symbols? Видалити вибрані символи?
No unused symbols Немає невикористаних символів
Delete Symbols Видалити символи
One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. Один або кілька символів використовуються, і їх не можна видалити, доки їхні екземпляри не буде розібрано або видалено.
Expand Instances Розібрати екземпляри
Delete Instances Видалити екземпляри
Can't render this file because it contains an unexpected character in line 178 and column 5.
+3
View File
@@ -871,6 +871,9 @@ font-technology Variable Biến thiên
Delete Swatch Xóa mẫu màu
Delete Symbol Xóa ký hiệu
Delete the selected symbol? Xóa ký hiệu đã chọn?
Delete the selected symbols? Xóa các ký hiệu đã chọn?
No unused symbols Không có ký hiệu nào chưa dùng
Delete Symbols Xóa các ký hiệu
One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. Một hoặc nhiều ký hiệu đang được sử dụng và không thể xóa cho đến khi các thực thể của chúng được mở rộng hoặc xóa.
Expand Instances Mở rộng thực thể
Delete Instances Xóa thực thể
Can't render this file because it contains an unexpected character in line 200 and column 5.
+3
View File
@@ -694,6 +694,9 @@
Delete Swatch 删除色板
Delete Symbol 删除符号
Delete the selected symbol? 要删除所选符号吗?
Delete the selected symbols? 要删除所选的多个符号吗?
No unused symbols 没有未使用的符号
Delete Symbols 删除符号
One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. 一个或多个符号正在使用中,在扩展或删除其实例之前无法删除。
Expand Instances 扩展实例
Delete Instances 删除实例
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -713,6 +713,9 @@ font-technology Variable 可變
Delete Swatch 刪除色票
Delete Symbol 刪除符號
Delete the selected symbol? 要刪除選取的符號嗎?
Delete the selected symbols? 要刪除選取的多個符號嗎?
No unused symbols 沒有未使用的符號
Delete Symbols 刪除符號
One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. 一個或多個符號正在使用中,在展開或刪除其範例之前無法刪除。
Expand Instances 展開範例
Delete Instances 刪除範例
Can't render this file because it contains an unexpected character in line 6 and column 47.
+136 -10
View File
@@ -201,6 +201,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
let names: Vec<String> = doc.symbols.iter().map(|s| s.name.clone()).collect();
let list: bool = pstate(ui.ctx(), "sym-list");
let sel = current(app);
let uid = app.session.active().map(|d| d.uid);
let marked = marked(ui.ctx(), uid, &names);
// The symbols drawn selected: the ones Select All Unused picked, else the current one.
let on = |n: &str| if marked.is_empty() { sel.as_deref() == Some(n) } else { marked.iter().any(|m| m == n) };
let mut clicked: Option<(String, bool)> = None;
let list_rect = widgets::list_box(ui, |ui| {
ui.set_min_height(110.0);
@@ -214,7 +218,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 26.0), Sense::click_and_drag());
// Dragged onto the canvas, the symbol is placed there.
widgets::drag_source(ui, &resp, || PanelDrag::Symbol(n.clone()));
if sel.as_deref() == Some(n.as_str()) {
if on(n) {
ui.painter().rect_filled(r, 0.0, t.row_selected);
} else if resp.hovered() {
ui.painter().rect_filled(r, 0.0, t.hover);
@@ -233,7 +237,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
let (r, resp) = ui.allocate_exact_size(vec2(44.0, 44.0), Sense::click_and_drag());
widgets::drag_source(ui, &resp, || PanelDrag::Symbol(n.clone()));
chip(ui, &doc, &keys, r.shrink(2.0), n);
let on = sel.as_deref() == Some(n.as_str());
let on = on(n);
ui.painter().rect_stroke(
r,
0.0,
@@ -258,6 +262,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
app.status(e);
}
if let Some((n, place)) = clicked {
// A click picks one symbol again.
set_pstate(ui.ctx(), MARKED, (uid, Vec::<String>::new()));
app.run("symbol.setCurrent", json!({"name": n})).ok();
if place {
app.run("symbol.place", json!({"name": n})).ok();
@@ -286,16 +292,17 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
app.run(cmd, p).ok();
}
// Alt deletes an unused symbol without asking; a symbol with instances still asks.
if widgets::icon_button_enabled(ui, "trash-2", tl!("Delete Symbol"), false, sel.is_some(), 24.0).clicked()
&& let Some(n) = &sel
{
crate::dialogs::symbol_delete::ask(app, n, super::alt_held(ui));
if widgets::icon_button_enabled(ui, "trash-2", tl!("Delete Symbol"), false, sel.is_some() || !marked.is_empty(), 24.0).clicked() {
delete(app, &marked, sel.as_deref(), super::alt_held(ui));
}
});
}
pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
let sel = current(app);
let names: Vec<String> = app.session.active().map(|d| d.doc.symbols.iter().map(|s| s.name.clone()).collect()).unwrap_or_default();
let uid = app.session.active().map(|d| d.uid);
let marked = marked(ui.ctx(), uid, &names);
let has_sel = app.session.active().is_some_and(|d| !d.selection.is_empty());
let inst = has_instances_selected(app);
let name = json!({"name": sel});
@@ -310,10 +317,8 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
}
}
// Asks first, as the panel's button does; the menu item has no Alt shortcut.
if menu_item(ui, tl!("Delete Symbol"), sel.is_some(), false)
&& let Some(n) = &sel
{
crate::dialogs::symbol_delete::ask(app, n, false);
if menu_item(ui, tl!("Delete Symbol"), sel.is_some() || !marked.is_empty(), false) {
delete(app, &marked, sel.as_deref(), false);
}
let items: [(&str, bool, &str, Value); 7] = [
(tl!("Symbol Options…"), sel.is_some(), "ui.symbolOptions", json!({"edit": true, "name": sel})),
@@ -330,6 +335,18 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
}
}
ui.separator();
// Select All Unused (#4918): the symbols nothing places show selected, and Delete Symbol
// deletes them all.
if menu_item(ui, tl!("Select All Unused"), !names.is_empty(), false)
&& let Ok(r) = app.run("symbol.unused", json!({}))
{
let unused: Vec<String> = serde_json::from_value(r["names"].clone()).unwrap_or_default();
if unused.is_empty() {
app.status(tl!("No unused symbols"));
}
set_pstate(ui.ctx(), MARKED, (uid, unused));
}
ui.separator();
let list: bool = pstate(ui.ctx(), "sym-list");
if menu_item(ui, tl!("Thumbnail View"), true, !list) {
set_pstate(ui.ctx(), "sym-list", false);
@@ -345,6 +362,31 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
}
}
/// The panel state key of the symbols Select All Unused picked.
const MARKED: &str = "sym-unused";
/// The symbols Select All Unused picked in the active document (`uid`) that it still has (`names`);
/// none once another document is active.
fn marked(ctx: &egui::Context, uid: Option<u64>, names: &[String]) -> Vec<String> {
let (of, mut marked): (Option<u64>, Vec<String>) = pstate(ctx, MARKED);
if of != uid {
return vec![];
}
marked.retain(|m| names.contains(m));
marked
}
/// Delete Symbol: the symbols Select All Unused picked (`marked`) when there are any, asked about
/// once unless `now` (Alt), else the current symbol `sel`.
fn delete(app: &mut VectorcraftApp, marked: &[String], sel: Option<&str>, now: bool) {
match (marked, sel) {
([], Some(n)) => crate::dialogs::symbol_delete::ask(app, n, now),
([], None) => {}
// Picked by Select All Unused, even one: deleted only while still unused.
(names, _) => crate::dialogs::symbol_delete::ask_many(app, names, now),
}
}
/// Save Symbol Library… for the document's symbols (`names`: the one selected in the panel).
fn save_library(app: &mut VectorcraftApp, names: Vec<String>) {
if let Err(e) = app.run("ui.saveSymbolLibrary", json!({ "names": names })) {
@@ -598,6 +640,90 @@ mod tests {
crate::tests_removeanchors::shapes_text(&out.shapes.iter().map(|c| c.shape.clone()).collect::<Vec<_>>())
}
/// Clicks the menu item labelled `label`.
fn click_menu(ctx: &egui::Context, app: &mut VectorcraftApp, label: &str) {
let labels = menu_frame(ctx, app, vec![]);
let item = labels.iter().find(|(t, _)| t.trim() == label).map(|(_, r)| r.center()).unwrap_or_else(|| panic!("{label} in the menu"));
menu_frame(ctx, app, vec![egui::Event::PointerMoved(item), button(item, true)]);
menu_frame(ctx, app, vec![button(item, false)]);
}
/// #4918: Select All Unused picks the symbols nothing places, Delete Symbol asks once and
/// deletes them all in one undo step, and a click on a symbol picks one again.
#[test]
fn select_all_unused_picks_the_unused_symbols_and_delete_deletes_them() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 200, "height": 100})).unwrap();
for (i, name) in ["A", "B", "C"].into_iter().enumerate() {
app.run("shape.rectangle", json!({"x": 10.0 + 40.0 * i as f64, "y": 10, "width": 30, "height": 20})).unwrap();
let id = app.run("symbol.new", json!({"name": name})).unwrap()["id"].as_u64().unwrap();
// A and C lose their instances; B keeps its own.
if name != "B" {
app.run("edit.clear", json!({"ids": [id]})).unwrap();
}
}
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
click_menu(&ctx, &mut app, "Select All Unused");
let names = symbols(&mut app);
assert_eq!(marked(&ctx, uid(&app), &names), ["A", "C"]);
let undo = app.session.active().unwrap().history.undo.len();
click_menu(&ctx, &mut app, "Delete Symbol");
assert_eq!(app.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some(crate::dialogs::symbol_delete::KIND));
assert_eq!(app.ui.dialog.as_ref().unwrap().fields["names"], json!(["A", "C"]), "asked once, about both");
crate::dialogs::confirm(&mut app).unwrap();
assert_eq!(symbols(&mut app), ["B"]);
assert_eq!(app.session.active().unwrap().history.undo.len(), undo + 1, "one undo step");
assert!(marked(&ctx, uid(&app), &symbols(&mut app)).is_empty(), "the deleted ones are no longer picked");
// Undone, they are back; Select All Unused again, then a click on a symbol picks just it.
app.run("edit.undo", json!({})).unwrap();
click_menu(&ctx, &mut app, "Select All Unused");
assert_eq!(marked(&ctx, uid(&app), &symbols(&mut app)).len(), 2);
frame(&ctx, &mut app, vec![]);
let tile = pos2(26.0, 26.0);
frame(&ctx, &mut app, vec![egui::Event::PointerMoved(tile), button(tile, true)]);
frame(&ctx, &mut app, vec![button(tile, false)]);
assert!(marked(&ctx, uid(&app), &symbols(&mut app)).is_empty());
// With nothing unused, the item reports it and picks nothing.
app.run("symbol.delete", json!({"names": ["A", "C"]})).unwrap();
click_menu(&ctx, &mut app, "Select All Unused");
assert!(marked(&ctx, uid(&app), &symbols(&mut app)).is_empty());
}
fn uid(app: &VectorcraftApp) -> Option<u64> {
app.session.active().map(|d| d.uid)
}
/// Picks made in one document don't carry to another with symbols of the same names, and a
/// picked symbol that gained a use since (here in a brush's art) isn't deleted (#4918 review).
#[test]
fn picked_symbols_stay_with_their_document_and_are_rechecked_before_deleting() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
let make = |app: &mut VectorcraftApp| {
app.run("file.new", json!({"width": 200, "height": 100})).unwrap();
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 30, "height": 20})).unwrap();
let id = app.run("symbol.new", json!({"name": "A"})).unwrap()["id"].as_u64().unwrap();
app.run("edit.clear", json!({"ids": [id]})).unwrap();
};
make(&mut app);
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
click_menu(&ctx, &mut app, "Select All Unused");
assert_eq!(marked(&ctx, uid(&app), &symbols(&mut app)), ["A"]);
make(&mut app);
assert!(marked(&ctx, uid(&app), &symbols(&mut app)).is_empty(), "not in the other document");
// Back in the first one: A gains a use in a brush's art, then Delete (Alt: no question).
click_menu(&ctx, &mut app, "Select All Unused");
let id = app.run("symbol.place", json!({"name": "A"})).unwrap()["id"].as_u64().unwrap();
app.run("brush.new", json!({"type": "art", "name": "B", "ids": [id]})).unwrap();
app.run("edit.clear", json!({"ids": [id]})).unwrap();
let names = symbols(&mut app);
let picked = marked(&ctx, uid(&app), &names);
assert_eq!(picked, ["A"]);
delete(&mut app, &picked, None, true);
assert_eq!(symbols(&mut app), ["A"], "still used by the brush, so kept");
}
#[test]
fn new_symbol_asks_for_symbol_options_before_making_the_symbol() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
+7 -1
View File
@@ -1561,7 +1561,8 @@ already walks Image Trace. Short agent entry points:
of `symbol.list` carry `nineSlice`: the guides or `null`; `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?}`, `symbol.replace`, `symbol.edit`, `symbol.duplicate`,
`symbol.selectInstances {name}`. `symbol.new` and `symbol.duplicate {name, newName?}` trim the white space around the
`symbol.selectInstances {name}`, `symbol.delete {name | names, expandInstances?}` (`names` deletes several in one
undo step). `symbol.new` and `symbol.duplicate {name, newName?}` trim the white space around the
name they are given, as `symbol.options` trims `newName`; a blank one gives "New Symbol" or "<name> copy".
- Dynamic symbols: an instance of a symbol whose `type` is `dynamic` changes the appearance of objects in the symbol's
art for itself only and stays linked. `symbol.instance.setAppearance {id?, path, fill?, stroke?, opacity?, blend?}`
@@ -1577,6 +1578,11 @@ already walks Image Trace. Short agent entry points:
```json
{"name":"run_command","arguments":{"command":"symbol.instance.setAppearance","params":{"id":42,"path":[1],"fill":"#d0021b","opacity":80}}}
```
- The Symbols panel menu's Select All Unused: `symbol.unused` (a query) lists the symbols nothing places, in panel
order, and the panel shows them selected; its Delete Symbol then deletes them all, asked once, in one undo step.
Instances in the art (opacity masks included), graph designs, pattern tiles (with the tiles that pattern editing's
Cancel puts back) and the art of the document's brushes count, and so do the instances in the art of a symbol that
is used; a symbol placed only inside an unused symbol is unused too.
- The Brushes panel menu's Select All Unused: `brush.unused` (a query) lists the brushes no stroke paints with, in
library order, and the panel selects them. Strokes in the art (opacity masks included), symbols, graph designs,
pattern tiles (with the tiles that pattern editing's Cancel puts back) and graphic styles count, and so do the
+3 -1
View File
@@ -1,6 +1,6 @@
# Parity with Adobe Illustrator
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Area Type Options › Text Flow, #4978; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Symbols panel: Select All Unused, #4918) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026)
## Feature submission evidence
@@ -607,6 +607,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- Swatch and graphic style libraries, Libraries panel files, presets and plug-ins open wherever they are dropped on the window, as File › Open opens them.
- **Symbol libraries** ([public source](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/create-or-import-symbol-libraries.html)): Window › Symbol Libraries and the Symbols panel's Symbol Libraries Menu open each library in a library panel of its own, as swatch and graphic style libraries do. The built-in library, Basic Shapes, is original art generated in code (`crates/engine/src/cmd/symbollib.rs`). A click on a symbol adds it to the document's Symbols as one undo step, with its Symbol Options (size, registration, export type, 9-slice guides) and every resource it uses (the symbols it nests, patterns, swatches, brushes, graphic styles, images), and makes it the current symbol; a symbol the document already has is not added again and another symbol of the same name keeps its name, the library's comes in numbered. Thumbnails, in the library panels and the Symbols panel, draw a symbol with the symbols it nests and the patterns and images it uses. Dragging a library symbol onto the canvas also places an instance there, in the same step ([placing from a library](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/create-and-place-symbols.html)). Save Symbols (the library menu) and Save Symbol Library… (the Symbols panel menu) write the document's symbols as a `.vectorcraft` document holding only them, into the user library folder (listed under User Defined) or a file; Other Library… reads the symbols of any document VectorCraft opens; a file it can't read is an error and opens no panel. Tests: `crates/engine/src/tests/symbollib.rs`, `crates/ui-egui/src/panels/tests_symbol_libraries.rs`, `panels::symbols::tests::thumbnails_show_the_symbols_a_symbol_nests`, `canvas::tests::a_library_symbol_dropped_on_the_canvas_is_added_and_placed_there_in_one_step`.
- **Delete Symbol asks first** ([Symbols panel](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/symbols-panel-options.html), [symbol libraries](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/create-or-import-symbol-libraries.html), [Alt-click Delete](https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html)): the Symbols panel's Delete Symbol button and menu item ask "Delete the selected symbol?" with Yes and No for a symbol no instance uses. For a symbol with placed instances they ask with Expand Instances (the instances become ordinary art), Delete Instances and Cancel; either Instances button deletes the symbol in one undo step. Alt-click on the button deletes an unused symbol without asking and still asks about one with instances. A question left open while another document became active deletes nothing. `symbol.delete` itself does not ask. Tests: `panels::symbols::tests::delete_symbol_asks_first_and_alt_click_deletes_an_unused_symbol_at_once`, `the_in_use_question_expands_or_deletes_the_instances`, `a_stale_delete_symbol_question_deletes_nothing`, `the_delete_symbol_questions_show_their_buttons`.
- **Symbols › Select All Unused** ([Symbols panel](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/symbols-panel-options.html), #4918): the Symbols panel menu's Select All Unused shows the symbols nothing places selected (`symbol.unused`): instances in the art (opacity masks included), graph designs, pattern tiles and brush art count, and so do instances in a used symbol's art. Delete Symbol, the button or the menu item, then asks "Delete the selected symbols?" once and deletes those still unused in one undo step (`symbol.delete {names}`); Alt-click on the button deletes them without asking, and a click on a symbol picks just it again. Tests: `tests::brushsym::symbol_unused_lists_the_symbols_nothing_places_and_delete_takes_several`, `panels::symbols::tests::select_all_unused_picks_the_unused_symbols_and_delete_deletes_them`, `dialogs::symbol_delete::tests::several_symbols_get_the_plural_question`.
- **Brush libraries** ([public source](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/apply-and-edit-strokes/create-new-brush-libraries.html), [import](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/apply-and-edit-strokes/import-brushes-from-brush-libraries-or-other-files.html), [Brushes panel](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/apply-and-edit-strokes/brushes-panel-overview.html)): Window › Brush Libraries, the Brushes panel's Brush Libraries Menu and its menu's Open Brush Library open each library in a library panel of its own, as swatch, graphic style and symbol libraries do. The built-in libraries (Art Ribbons, Calligraphic Nibs, Pattern Borders, Scatter Shapes) are original brushes generated in code (`crates/engine/src/cmd/brushlib.rs`); Striped Ribbon's art is filled with the library's own pattern and Sparkles scatters the library's own symbol. A click on a brush adds it to the document's Brushes as one undo step with every resource its art uses (patterns, symbols, swatches, graphic styles, images), makes it the current brush and, with paths selected, applies it to them in the same step; a brush the document already has is not added again and a different brush of the same name comes in numbered ("Wave 2"). Shift/Cmd-clicks select brushes for Add to Brushes; a brush dragged onto a path is added and applied to it, and one dragged onto the Brushes panel is added. Save Brushes… / the Brushes panel's Save Brush Library… write the document's brushes (or the selected ones) as a `.vectorcraft` library into the `Brushes` folder next to the preferences, listed under User Defined, or to a file; Other Library… reads the brushes of any document VectorCraft opens. Thumbnails, in the library panels, the Brushes panel and Brush Options, render brush art with the patterns and symbols it uses. Copying brushed art now carries the patterns and symbols its brush's art uses too. Tests: `crates/engine/src/tests/brushlib.rs`, `crates/ui-egui/src/panels/tests_brush_libraries.rs`, `panels::brushes::tests::library_brush_previews_show_the_patterns_their_art_uses`, `canvas::tests::a_library_brush_dropped_on_a_path_is_added_and_applied_in_one_step`.
- A canvas context menu: right-click selects the object under the pointer and lists what applies to the selection (Undo/Redo, clipboard, group, isolation, join, Remove Anchor Points when anchors are selected, masks, compound paths, guides, Transform, Arrange, Select, Export Selection), or the view commands on empty canvas.
- A right-click over locked objects adds an Unlock submenu to the canvas context menu. The submenu lists up to 20 locked objects under the pointer, topmost first, by the name the Layers panel shows, and choosing one unlocks it (`layer.setProps`). A locked group is listed as one item. Objects on hidden, locked or template layers are not listed.
@@ -1432,6 +1433,7 @@ Symbols area (#4938), from the public symbol documentation ([create and place sy
| Date | Change | Summary |
|---|---|---|
| 2026-11-01 | minor | Symbols panel: Select All Unused (`symbol.unused`) shows the symbols nothing places selected, and Delete Symbol deletes them all, asked once, in one undo step (`symbol.delete {names}`, #4918); scores and hours unchanged |
| 2026-11-01 | minor | Type core: Area Type Options › Text Flow (#4978): area type with several rows and columns fills them by columns (the default) or by rows, from the dialog's radio buttons or `text.areaOptions {flow}`; evidence updated; scores unchanged. |
| 2026-11-01 | minor | Symbols: dynamic symbols (#4938): Symbol Options › Symbol Type, per-instance appearance for objects of a dynamic symbol's art that stays linked through redefinition, Break Link baking, Reset and Static dropping it; evidence added. Score unchanged at 87% (estimated), hours 5–9 → 4–8. |
| 2026-11-01 | minor | Artboard background colors in output (#4937): raster exports (a transparent background too), PDF, SVG and EPS pages, `.ai` files' PDF pages, Export for Screens and print (and its preview) draw each artboard's color behind its art, inside the artboard and in the color's own model; Export for Screens has Include Artboard Fill beside Include Bleed (on by default, remembered; `includeArtboardFill`, also on `document.export` and `file.print`); editing data keeps the color as the artboard's, and Export for Screens' document rows keep it too; evidence updated; scores unchanged. |