M3.45: graphic style links: redefine, break link, options, unused, sort, Select > Same

The Appearance panel names the style a linked object uses and its menu offers Redefine
Graphic Style "name", which updates every object still linked (edited objects keep their
look and are unlinked). The Graphic Styles panel breaks links (button and menu), selects
all unused styles, sorts by name, deletes several styles, and opens Graphic Style Options
(rename; links follow) on double-click; New Graphic Style from the menu or with Alt asks
for a name. Select > Same > Graphic Style and Appearance Attribute work, and Document Info
lists the style names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bflatastic
2026-10-04 23:03:45 +07:00
co-authored by Claude Opus 5.5
parent eb83efdb19
commit 321ab52aea
11 changed files with 264 additions and 20 deletions
+12 -6
View File
@@ -16,7 +16,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Document Info",
["Window", "Document Info"],
None,
"{selectionOnly?} → {document, objects: {paths, compoundPaths, groups, …}, fonts, images, swatches, …}",
"{selectionOnly?} → {document, objects: {paths, compoundPaths, groups, …}, fonts, images, swatches, graphicStyleNames (with selectionOnly: the styles the selected objects are linked to), …}",
has_doc,
info
),
@@ -43,16 +43,15 @@ fn paint_kind(p: &Paint) -> Option<&'static str> {
fn info(s: &mut Session, p: &Value) -> Result<Value> {
let st = s.doc()?;
let d = &st.doc;
let roots: Vec<&Node> = if bool_or(p, "selectionOnly", false) {
st.selection.objects.iter().filter_map(|id| d.node(*id)).collect()
} else {
d.layers.iter().map(|l| &**l).collect()
};
let selection_only = bool_or(p, "selectionOnly", false);
let roots: Vec<&Node> =
if selection_only { st.selection.objects.iter().filter_map(|id| d.node(*id)).collect() } else { d.layers.iter().map(|l| &**l).collect() };
let mut counts: BTreeMap<&str, usize> = BTreeMap::new();
let mut fonts = BTreeSet::new();
let mut images: BTreeMap<String, Value> = BTreeMap::new();
let mut symbols = BTreeSet::new();
let mut spot = BTreeSet::new();
let mut styles: Vec<&str> = if selection_only { vec![] } else { d.graphic_styles.iter().map(|g| g.name.as_str()).collect() };
for r in roots {
r.walk(&mut |n| {
let k = match &n.kind {
@@ -104,6 +103,12 @@ fn info(s: &mut Session, p: &Value) -> Result<Value> {
if !n.appearance.effects.is_empty() {
*counts.entry("liveEffects").or_default() += 1;
}
if selection_only
&& let Some(g) = super::style::linked_style(d, n)
&& !styles.contains(&g.name.as_str())
{
styles.push(&g.name);
}
});
}
Ok(json!({
@@ -115,6 +120,7 @@ fn info(s: &mut Session, p: &Value) -> Result<Value> {
"spotColors": spot,
"swatches": d.swatches_iter().count(),
"graphicStyles": d.graphic_styles.len(),
"graphicStyleNames": styles,
"characterStyles": d.char_styles.len(),
"paragraphStyles": d.para_styles.len(),
"patterns": d.patterns.len(),
+73 -7
View File
@@ -75,6 +75,24 @@ pub fn specs() -> Vec<CommandSpec> {
cmd!("select.object.openPaths", "Open Paths", ["Select", "Object"], None, "{}", has_doc, |s, _| by_kind(s, |n| n
.path_data()
.is_some_and(|p| !p.is_closed()))),
cmd!(
"select.same.graphicStyle",
"Graphic Style",
["Select", "Same"],
None,
"{} the objects linked to the graphic style the first selected object is linked to",
has_selection,
same_graphic_style
),
cmd!(
"select.same.appearanceAttribute",
"Appearance Attribute",
["Select", "Same"],
None,
"{item?: appearance item index} the objects sharing an appearance attribute of the first selected object: its fill or stroke `item` (default: the Appearance panel's active item), else its first effect, else its topmost fill",
has_selection,
same_attribute
),
]
}
@@ -224,20 +242,68 @@ fn anchors_many(s: &mut Session, p: &Value) -> Result<Value> {
fn same(s: &mut Session, cmd: &str, eq: fn(&Node, &Node) -> bool) -> Result<Value> {
let st = s.doc()?;
let refn = st.selection.objects.first().and_then(|id| st.doc.node(*id)).cloned().ok_or_else(|| bad(cmd, "nothing selected"))?;
select_where(s, cmd, &json!({}), |_, n| !n.is_container() && eq(n, &refn))
}
/// Select the visible, unlocked objects in visible, unlocked layers that `f` accepts (not looking
/// inside accepted ones); Select > Reselect repeats `cmd` with `p`.
fn select_where(s: &mut Session, cmd: &str, p: &Value, f: impl Fn(&Document, &Node) -> bool) -> Result<Value> {
fn visit(d: &Document, n: &Node, f: &impl Fn(&Document, &Node) -> bool, ids: &mut Vec<NodeId>) {
if n.visible && !n.locked && !n.is_layer() && f(d, n) {
return ids.push(n.id);
}
for c in n.children().into_iter().flatten() {
visit(d, c, f, ids);
}
}
let st = s.doc()?;
let mut ids = vec![];
for l in st.doc.layers.iter().filter(|l| l.visible && !l.locked) {
l.walk(&mut |n| {
if !n.is_container() && n.visible && !n.locked && eq(n, &refn) {
ids.push(n.id);
}
});
visit(&st.doc, l, &f, &mut ids);
}
let c = cmd.to_string();
s.select(|_, sel| sel.set(ids.iter().copied()))?;
s.doc_mut()?.last_selection_cmd = Some((c, json!({})));
s.doc_mut()?.last_selection_cmd = Some((cmd.to_string(), p.clone()));
Ok(json!({ "count": ids.len() }))
}
fn same_graphic_style(s: &mut Session, _: &Value) -> Result<Value> {
let cmd = "select.same.graphicStyle";
let st = s.doc()?;
let id = st
.selection
.objects
.first()
.and_then(|id| super::style::linked_style(&st.doc, st.doc.node(*id)?))
.map(|g| g.id)
.ok_or_else(|| bad(cmd, "the selection has no graphic style"))?;
select_where(s, cmd, &json!({}), |d, n| n.graphic_style == Some(id) && super::style::linked_style(d, n).is_some())
}
/// An appearance attribute Select > Same > Appearance Attribute matches.
enum Attribute {
Item(vectorcraft_doc::AppearanceItem),
Effect(vectorcraft_doc::Effect),
}
fn same_attribute(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "select.same.appearanceAttribute";
let st = s.doc()?;
let refn = st.selection.objects.first().and_then(|id| st.doc.node(*id)).ok_or_else(|| bad(cmd, "nothing selected"))?;
let ap = &refn.appearance;
let item = p.get("item").and_then(Value::as_u64).map(|i| i as usize).or(s.appearance_item());
let attr = match (item.and_then(|i| ap.items.get(i)), ap.effects.first()) {
(Some(it), _) => Attribute::Item(it.clone()),
(None, Some(e)) => Attribute::Effect(e.clone()),
(None, None) => {
Attribute::Item(ap.items.iter().rev().find(|i| i.is_fill()).cloned().ok_or_else(|| bad(cmd, "the object has no fill, stroke or effect"))?)
}
};
select_where(s, cmd, p, |_, n| match &attr {
Attribute::Item(it) => n.appearance.items.contains(it),
Attribute::Effect(e) => n.appearance.effects.iter().any(|x| x.id == e.id && x.params == e.params),
})
}
fn same_layers(s: &mut Session, _: &Value) -> Result<Value> {
let st = s.doc()?;
let layers: BTreeSet<NodeId> = st.selection.objects.iter().filter_map(|id| st.doc.layer_of(*id)).collect();