mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-27 19:27:21 -06:00
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:
co-authored by
Claude Opus 5.5
parent
eb83efdb19
commit
321ab52aea
@@ -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(),
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user