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:
parent
eb83efdb19
commit
321ab52aea
|
|
@ -16,7 +16,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||||
"Document Info",
|
"Document Info",
|
||||||
["Window", "Document Info"],
|
["Window", "Document Info"],
|
||||||
None,
|
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,
|
has_doc,
|
||||||
info
|
info
|
||||||
),
|
),
|
||||||
|
|
@ -43,16 +43,15 @@ fn paint_kind(p: &Paint) -> Option<&'static str> {
|
||||||
fn info(s: &mut Session, p: &Value) -> Result<Value> {
|
fn info(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
let st = s.doc()?;
|
let st = s.doc()?;
|
||||||
let d = &st.doc;
|
let d = &st.doc;
|
||||||
let roots: Vec<&Node> = if bool_or(p, "selectionOnly", false) {
|
let selection_only = bool_or(p, "selectionOnly", false);
|
||||||
st.selection.objects.iter().filter_map(|id| d.node(*id)).collect()
|
let roots: Vec<&Node> =
|
||||||
} else {
|
if selection_only { st.selection.objects.iter().filter_map(|id| d.node(*id)).collect() } else { d.layers.iter().map(|l| &**l).collect() };
|
||||||
d.layers.iter().map(|l| &**l).collect()
|
|
||||||
};
|
|
||||||
let mut counts: BTreeMap<&str, usize> = BTreeMap::new();
|
let mut counts: BTreeMap<&str, usize> = BTreeMap::new();
|
||||||
let mut fonts = BTreeSet::new();
|
let mut fonts = BTreeSet::new();
|
||||||
let mut images: BTreeMap<String, Value> = BTreeMap::new();
|
let mut images: BTreeMap<String, Value> = BTreeMap::new();
|
||||||
let mut symbols = BTreeSet::new();
|
let mut symbols = BTreeSet::new();
|
||||||
let mut spot = 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 {
|
for r in roots {
|
||||||
r.walk(&mut |n| {
|
r.walk(&mut |n| {
|
||||||
let k = match &n.kind {
|
let k = match &n.kind {
|
||||||
|
|
@ -104,6 +103,12 @@ fn info(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
if !n.appearance.effects.is_empty() {
|
if !n.appearance.effects.is_empty() {
|
||||||
*counts.entry("liveEffects").or_default() += 1;
|
*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!({
|
Ok(json!({
|
||||||
|
|
@ -115,6 +120,7 @@ fn info(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
"spotColors": spot,
|
"spotColors": spot,
|
||||||
"swatches": d.swatches_iter().count(),
|
"swatches": d.swatches_iter().count(),
|
||||||
"graphicStyles": d.graphic_styles.len(),
|
"graphicStyles": d.graphic_styles.len(),
|
||||||
|
"graphicStyleNames": styles,
|
||||||
"characterStyles": d.char_styles.len(),
|
"characterStyles": d.char_styles.len(),
|
||||||
"paragraphStyles": d.para_styles.len(),
|
"paragraphStyles": d.para_styles.len(),
|
||||||
"patterns": d.patterns.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
|
cmd!("select.object.openPaths", "Open Paths", ["Select", "Object"], None, "{}", has_doc, |s, _| by_kind(s, |n| n
|
||||||
.path_data()
|
.path_data()
|
||||||
.is_some_and(|p| !p.is_closed()))),
|
.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> {
|
fn same(s: &mut Session, cmd: &str, eq: fn(&Node, &Node) -> bool) -> Result<Value> {
|
||||||
let st = s.doc()?;
|
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"))?;
|
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![];
|
let mut ids = vec![];
|
||||||
for l in st.doc.layers.iter().filter(|l| l.visible && !l.locked) {
|
for l in st.doc.layers.iter().filter(|l| l.visible && !l.locked) {
|
||||||
l.walk(&mut |n| {
|
visit(&st.doc, l, &f, &mut ids);
|
||||||
if !n.is_container() && n.visible && !n.locked && eq(n, &refn) {
|
|
||||||
ids.push(n.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
let c = cmd.to_string();
|
|
||||||
s.select(|_, sel| sel.set(ids.iter().copied()))?;
|
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() }))
|
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> {
|
fn same_layers(s: &mut Session, _: &Value) -> Result<Value> {
|
||||||
let st = s.doc()?;
|
let st = s.doc()?;
|
||||||
let layers: BTreeSet<NodeId> = st.selection.objects.iter().filter_map(|id| st.doc.layer_of(*id)).collect();
|
let layers: BTreeSet<NodeId> = st.selection.objects.iter().filter_map(|id| st.doc.layer_of(*id)).collect();
|
||||||
|
|
|
||||||
|
|
@ -198,10 +198,14 @@ fn deleting_a_style_unlinks_and_renaming_keeps_links() {
|
||||||
let (_, b, c) = linked_three(&mut s);
|
let (_, b, c) = linked_three(&mut s);
|
||||||
run(&mut s, "graphicStyle.rename", json!({"name": "S", "to": "T"}));
|
run(&mut s, "graphicStyle.rename", json!({"name": "S", "to": "T"}));
|
||||||
assert_eq!(linked(&mut s, "T").len(), 3);
|
assert_eq!(linked(&mut s, "T").len(), 3);
|
||||||
|
run(&mut s, "select.set", json!({"ids": [b.0]}));
|
||||||
|
assert_eq!(run(&mut s, "select.same.graphicStyle", json!({}))["count"], 3);
|
||||||
let look = node(&s, c).appearance;
|
let look = node(&s, c).appearance;
|
||||||
run(&mut s, "graphicStyle.delete", json!({"name": "T"}));
|
run(&mut s, "graphicStyle.delete", json!({"name": "T"}));
|
||||||
assert_eq!((node(&s, b).graphic_style, node(&s, c).graphic_style), (None, None));
|
assert_eq!((node(&s, b).graphic_style, node(&s, c).graphic_style), (None, None));
|
||||||
assert_eq!(node(&s, c).appearance, look);
|
assert_eq!(node(&s, c).appearance, look);
|
||||||
|
run(&mut s, "select.set", json!({"ids": [b.0]}));
|
||||||
|
assert!(s.execute("select.same.graphicStyle", &json!({})).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -218,3 +222,32 @@ fn unused_styles_and_sort_by_name() {
|
||||||
let names: Vec<String> = s.doc().unwrap().doc.graphic_styles.iter().map(|g| g.name.clone()).collect();
|
let names: Vec<String> = s.doc().unwrap().doc.graphic_styles.iter().map(|g| g.name.clone()).collect();
|
||||||
assert_eq!(names, ["Default Graphic Style", "Black Outline", "Heavy Ink", "S", "Sunshine"]);
|
assert_eq!(names, ["Default Graphic Style", "Black Outline", "Heavy Ink", "S", "Sunshine"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn select_same_appearance_attribute() {
|
||||||
|
let mut s = session();
|
||||||
|
let a = rect(&mut s, 10.0);
|
||||||
|
run(&mut s, "effect.apply", json!({"effect": "distort.roughen"}));
|
||||||
|
let b = rect(&mut s, 100.0);
|
||||||
|
set_fill(&mut s, b, "#ff0000");
|
||||||
|
run(&mut s, "effect.apply", json!({"effect": "distort.roughen"}));
|
||||||
|
let c = rect(&mut s, 200.0);
|
||||||
|
// The shared effect.
|
||||||
|
run(&mut s, "select.set", json!({"ids": [a.0]}));
|
||||||
|
run(&mut s, "select.same.appearanceAttribute", json!({}));
|
||||||
|
assert_eq!(s.doc().unwrap().selection.objects, vec![a, b]);
|
||||||
|
// The shared stroke (item 1).
|
||||||
|
run(&mut s, "select.set", json!({"ids": [a.0]}));
|
||||||
|
run(&mut s, "select.same.appearanceAttribute", json!({"item": 1}));
|
||||||
|
assert_eq!(s.doc().unwrap().selection.objects, vec![a, b, c]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn document_info_lists_style_names() {
|
||||||
|
let mut s = session();
|
||||||
|
let (a, ..) = linked_three(&mut s);
|
||||||
|
let i = run(&mut s, "document.info", json!({}));
|
||||||
|
assert_eq!(i["graphicStyleNames"].as_array().unwrap().len(), 5);
|
||||||
|
run(&mut s, "select.set", json!({"ids": [a.0]}));
|
||||||
|
assert_eq!(run(&mut s, "document.info", json!({"selectionOnly": true}))["graphicStyleNames"], json!(["S"]));
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
//! Graphic Style Options: the style's name. Opened on a style it renames it (`graphicStyle.rename`,
|
||||||
|
//! linked objects stay linked); opened for New Graphic Style it names the style made from the
|
||||||
|
//! selection (`graphicStyle.new`).
|
||||||
|
//!
|
||||||
|
//! Fields: `name` and `__style` (the style being renamed; empty for a new style).
|
||||||
|
|
||||||
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
|
use super::swatch_options::{grid, label};
|
||||||
|
use super::{DialogSpec, form};
|
||||||
|
use crate::VectorcraftApp;
|
||||||
|
use crate::state::Dialog;
|
||||||
|
|
||||||
|
/// The dialog kind of Graphic Style Options.
|
||||||
|
pub const KIND: &str = "graphicStyleOptions";
|
||||||
|
|
||||||
|
pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| "Graphic Style Options".into(), body, confirm, min_width: 320.0, ..DialogSpec::FORM };
|
||||||
|
|
||||||
|
/// Open Graphic Style Options for style `name`, or (`None`) to name a new style from the selection.
|
||||||
|
pub fn open(app: &mut VectorcraftApp, name: Option<&str>) -> Result<Value, String> {
|
||||||
|
let d = &app.session.active().ok_or("no document open")?.doc;
|
||||||
|
let name = match name {
|
||||||
|
Some(n) => d.graphic_style(n).ok_or_else(|| format!("no graphic style `{n}`"))?.name.clone(),
|
||||||
|
None => d.new_graphic_style_name(),
|
||||||
|
};
|
||||||
|
let style = if d.graphic_style(&name).is_some() { name.clone() } else { String::new() };
|
||||||
|
app.ui.dialog = Some(Dialog::new(KIND, json!({ "name": name, "__style": style })));
|
||||||
|
Ok(Value::Null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||||
|
grid(ui, |ui| {
|
||||||
|
label(ui, "Style Name:");
|
||||||
|
form::text(ui, d, "name", 190.0);
|
||||||
|
ui.end_row();
|
||||||
|
});
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// OK: name the new style or rename the style. A name another style has keeps the dialog open.
|
||||||
|
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||||
|
let (name, style) = (d.str("name"), d.str("__style"));
|
||||||
|
let r = match style.as_str() {
|
||||||
|
"" => app.run("graphicStyle.new", json!({ "name": name })),
|
||||||
|
s if s == name => Ok(Value::Null),
|
||||||
|
s => app.run("graphicStyle.rename", json!({ "name": s, "to": name })),
|
||||||
|
};
|
||||||
|
if r.is_ok() {
|
||||||
|
app.ui.dialog = None;
|
||||||
|
}
|
||||||
|
r
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use vectorcraft_engine::Session;
|
||||||
|
|
||||||
|
fn app() -> VectorcraftApp {
|
||||||
|
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||||
|
app.run("file.new", json!({"width": 200, "height": 200})).unwrap();
|
||||||
|
app.run("shape.rectangle", json!({"x": 0, "y": 0, "width": 50, "height": 50})).unwrap();
|
||||||
|
app
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set(app: &mut VectorcraftApp, v: &str) {
|
||||||
|
app.ui.dialog.as_mut().unwrap().fields.insert("name".into(), json!(v));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn names(app: &VectorcraftApp) -> Vec<String> {
|
||||||
|
app.session.active().unwrap().doc.graphic_styles.iter().map(|g| g.name.clone()).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn names_a_new_style_then_renames_it() {
|
||||||
|
let mut app = app();
|
||||||
|
app.run("ui.graphicStyleOptions", json!({})).unwrap();
|
||||||
|
assert_eq!(app.ui.dialog.as_ref().unwrap().str("name"), "Graphic Style 5");
|
||||||
|
// Drawn headlessly in the shared frame.
|
||||||
|
let ctx = egui::Context::default();
|
||||||
|
crate::theme::install_fonts(&ctx);
|
||||||
|
let mut out = ctx.run_ui(Default::default(), |ui| super::super::show(&mut app, ui.ctx()));
|
||||||
|
out.textures_delta.clear();
|
||||||
|
set(&mut app, "Glow");
|
||||||
|
super::super::confirm(&mut app).unwrap();
|
||||||
|
assert!(app.ui.dialog.is_none() && names(&app).last().unwrap() == "Glow");
|
||||||
|
|
||||||
|
app.run("ui.graphicStyleOptions", json!({"name": "Glow"})).unwrap();
|
||||||
|
set(&mut app, "Sunshine");
|
||||||
|
assert!(super::super::confirm(&mut app).is_err(), "names are unique");
|
||||||
|
assert!(app.ui.dialog.is_some(), "a taken name keeps the dialog open");
|
||||||
|
set(&mut app, "Halo");
|
||||||
|
super::super::confirm(&mut app).unwrap();
|
||||||
|
assert_eq!(names(&app).last().unwrap(), "Halo");
|
||||||
|
assert!(app.run("ui.graphicStyleOptions", json!({"name": "nope"})).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,6 +17,7 @@ mod effect;
|
||||||
mod export_for_screens;
|
mod export_for_screens;
|
||||||
mod form;
|
mod form;
|
||||||
mod gradient_stop;
|
mod gradient_stop;
|
||||||
|
pub mod graphic_style_options;
|
||||||
pub mod new_color_group;
|
pub mod new_color_group;
|
||||||
mod new_document;
|
mod new_document;
|
||||||
pub mod new_swatch;
|
pub mod new_swatch;
|
||||||
|
|
@ -140,6 +141,7 @@ registry! {
|
||||||
NewColorGroup: [new_color_group::KIND] => new_color_group::SPEC,
|
NewColorGroup: [new_color_group::KIND] => new_color_group::SPEC,
|
||||||
GradientStop: ["gradientStop"] => gradient_stop::SPEC,
|
GradientStop: ["gradientStop"] => gradient_stop::SPEC,
|
||||||
ColorPicker: [color_picker::KIND] => color_picker::SPEC,
|
ColorPicker: [color_picker::KIND] => color_picker::SPEC,
|
||||||
|
GraphicStyleOptions: [graphic_style_options::KIND] => graphic_style_options::SPEC,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The spec for a `Dialog::kind` ([`DialogSpec::FORM`] when unregistered).
|
/// The spec for a `Dialog::kind` ([`DialogSpec::FORM`] when unregistered).
|
||||||
|
|
|
||||||
|
|
@ -189,6 +189,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||||
"",
|
"",
|
||||||
"{stroke?: bool (default: the active proxy), color?: \"#rrggbb\"|[r,g,b]|{c,m,y,k}|{gray} (default: the proxy's colour)} open the Color Picker (fields: hex or color, channel, webOnly, swatches); OK runs paint.setFill / paint.setStroke",
|
"{stroke?: bool (default: the active proxy), color?: \"#rrggbb\"|[r,g,b]|{c,m,y,k}|{gray} (default: the proxy's colour)} open the Color Picker (fields: hex or color, channel, webOnly, swatches); OK runs paint.setFill / paint.setStroke",
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"ui.graphicStyleOptions",
|
||||||
|
"Graphic Style Options…",
|
||||||
|
"",
|
||||||
|
"{name?} open Graphic Style Options (dialog `graphicStyleOptions`, field `name`): for style `name` OK renames it (graphicStyle.rename); without, OK makes a new style of that name from the selection (graphicStyle.new)",
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Handle a UI command. `None` = not a UI command (the engine handles it).
|
/// Handle a UI command. `None` = not a UI command (the engine handles it).
|
||||||
|
|
@ -524,6 +530,7 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
|
||||||
crate::dialogs::new_color_group::open(app, names.unwrap_or_default())
|
crate::dialogs::new_color_group::open(app, names.unwrap_or_default())
|
||||||
}
|
}
|
||||||
"ui.colorPicker" => crate::dialogs::open_color_picker(app, p),
|
"ui.colorPicker" => crate::dialogs::open_color_picker(app, p),
|
||||||
|
"ui.graphicStyleOptions" => crate::dialogs::graphic_style_options::open(app, s("name").as_deref()),
|
||||||
_ => return None,
|
_ => return None,
|
||||||
};
|
};
|
||||||
Some(r)
|
Some(r)
|
||||||
|
|
@ -687,6 +694,7 @@ pub fn enabled(app: &VectorcraftApp, id: &str) -> bool {
|
||||||
"effect.applyLast" | "effect.last" => app.last_effect.is_some() && app.session.active().is_some_and(|d| !d.selection.is_empty()),
|
"effect.applyLast" | "effect.last" => app.last_effect.is_some() && app.session.active().is_some_and(|d| !d.selection.is_empty()),
|
||||||
"file.export.pdf" => app.session.active().is_some(),
|
"file.export.pdf" => app.session.active().is_some(),
|
||||||
"ui.swatchOptions" | "ui.newSwatch" | "ui.newColorGroup" => app.session.active().is_some(),
|
"ui.swatchOptions" | "ui.newSwatch" | "ui.newColorGroup" => app.session.active().is_some(),
|
||||||
|
"ui.graphicStyleOptions" => app.session.active().is_some(),
|
||||||
_ => true,
|
_ => true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1077,14 +1085,14 @@ pub fn menu_tree() -> Vec<(&'static str, Vec<Item>)> {
|
||||||
"Same",
|
"Same",
|
||||||
vec![
|
vec![
|
||||||
c("Appearance", "select.same.appearance"),
|
c("Appearance", "select.same.appearance"),
|
||||||
todo("Appearance Attribute"),
|
c("Appearance Attribute", "select.same.appearanceAttribute"),
|
||||||
c("Blending Mode", "select.same.blendingMode"),
|
c("Blending Mode", "select.same.blendingMode"),
|
||||||
c("Fill & Stroke", "select.same.fillAndStroke"),
|
c("Fill & Stroke", "select.same.fillAndStroke"),
|
||||||
c("Fill Color", "select.same.fillColor"),
|
c("Fill Color", "select.same.fillColor"),
|
||||||
c("Opacity", "select.same.opacity"),
|
c("Opacity", "select.same.opacity"),
|
||||||
c("Stroke Color", "select.same.strokeColor"),
|
c("Stroke Color", "select.same.strokeColor"),
|
||||||
c("Stroke Weight", "select.same.strokeWeight"),
|
c("Stroke Weight", "select.same.strokeWeight"),
|
||||||
todo("Graphic Style"),
|
c("Graphic Style", "select.same.graphicStyle"),
|
||||||
c("Shape", "select.same.shapeType"),
|
c("Shape", "select.same.shapeType"),
|
||||||
c("Symbol Instance", "select.same.symbolInstance"),
|
c("Symbol Instance", "select.same.symbolInstance"),
|
||||||
Sep,
|
Sep,
|
||||||
|
|
|
||||||
|
|
@ -175,12 +175,17 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||||
ui.painter().line_segment([r.left_bottom(), r.right_bottom()], Stroke::new(1.0, t.input_border));
|
ui.painter().line_segment([r.left_bottom(), r.right_bottom()], Stroke::new(1.0, t.input_border));
|
||||||
let mixed = mixed_appearances(app);
|
let mixed = mixed_appearances(app);
|
||||||
let label = if mixed { "Mixed Appearances" } else { object_label(app) };
|
let label = if mixed { "Mixed Appearances" } else { object_label(app) };
|
||||||
|
// A linked object names its graphic style ("Rectangle: Sunshine").
|
||||||
|
let label = match app.session.selection_graphic_style() {
|
||||||
|
Some((g, true)) if !mixed => std::borrow::Cow::Owned(format!("{label}: {}", g.name)),
|
||||||
|
_ => std::borrow::Cow::Borrowed(label),
|
||||||
|
};
|
||||||
if !hide_thumb {
|
if !hide_thumb {
|
||||||
let th = Rect::from_min_size(r.left_center() + vec2(6.0, -12.0), vec2(24.0, 24.0));
|
let th = Rect::from_min_size(r.left_center() + vec2(6.0, -12.0), vec2(24.0, 24.0));
|
||||||
let fill = node.as_ref().map(|n| n.appearance.fill_paint()).unwrap_or_else(|| app.session.paint.fill.clone());
|
let fill = node.as_ref().map(|n| n.appearance.fill_paint()).unwrap_or_else(|| app.session.paint.fill.clone());
|
||||||
chip(ui, th, &fill);
|
chip(ui, th, &fill);
|
||||||
}
|
}
|
||||||
text(ui, r.left_center() + vec2(if hide_thumb { 8.0 } else { 46.0 }, 0.0), label, true);
|
text(ui, r.left_center() + vec2(if hide_thumb { 8.0 } else { 46.0 }, 0.0), &label, true);
|
||||||
match &node {
|
match &node {
|
||||||
// Objects that differ have no common stack to list.
|
// Objects that differ have no common stack to list.
|
||||||
Some(_) if mixed => {}
|
Some(_) if mixed => {}
|
||||||
|
|
@ -582,7 +587,11 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||||
set_pstate(ui.ctx(), "ap-hide-thumb", !hide);
|
set_pstate(ui.ctx(), "ap-hide-thumb", !hide);
|
||||||
}
|
}
|
||||||
ui.separator();
|
ui.separator();
|
||||||
menu_item(ui, "Redefine Graphic Style", false, false);
|
let style = app.session.selection_graphic_style().map(|(g, _)| g.name.clone());
|
||||||
|
let redefine = style.as_ref().map_or_else(|| "Redefine Graphic Style".into(), |n| format!("Redefine Graphic Style \u{201c}{n}\u{201d}"));
|
||||||
|
if menu_item(ui, &redefine, style.is_some(), false) {
|
||||||
|
app.run("graphicStyle.redefine", json!({})).ok();
|
||||||
|
}
|
||||||
menu_item(ui, "Show All Hidden Attributes", false, false);
|
menu_item(ui, "Show All Hidden Attributes", false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
//! Document Info panel: the document's setup and what it contains (objects by kind, fonts,
|
//! Document Info panel: the document's setup and what it contains (objects by kind, fonts,
|
||||||
//! images, symbols, spot colours…), for the whole document or the selection only.
|
//! images, symbols, spot colours, graphic styles…), for the whole document or the selection only.
|
||||||
|
|
||||||
use egui::Ui;
|
use egui::Ui;
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
|
@ -71,7 +71,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||||
widgets::dim_label(ui, "None");
|
widgets::dim_label(ui, "None");
|
||||||
}
|
}
|
||||||
widgets::divider(ui);
|
widgets::divider(ui);
|
||||||
for (key, title) in [("fonts", "Fonts"), ("symbols", "Symbols"), ("spotColors", "Spot Colors")] {
|
for (key, title) in [("fonts", "Fonts"), ("symbols", "Symbols"), ("spotColors", "Spot Colors"), ("graphicStyleNames", "Graphic Styles")] {
|
||||||
let list: Vec<&str> = i[key].as_array().map(|a| a.iter().filter_map(Value::as_str).collect()).unwrap_or_default();
|
let list: Vec<&str> = i[key].as_array().map(|a| a.iter().filter_map(Value::as_str).collect()).unwrap_or_default();
|
||||||
widgets::subheader(ui, &format!("{title} ({})", list.len()));
|
widgets::subheader(ui, &format!("{title} ({})", list.len()));
|
||||||
for f in list {
|
for f in list {
|
||||||
|
|
@ -87,7 +87,6 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||||
widgets::divider(ui);
|
widgets::divider(ui);
|
||||||
for (key, label) in [
|
for (key, label) in [
|
||||||
("swatches", "Swatches"),
|
("swatches", "Swatches"),
|
||||||
("graphicStyles", "Graphic Styles"),
|
|
||||||
("characterStyles", "Character Styles"),
|
("characterStyles", "Character Styles"),
|
||||||
("paragraphStyles", "Paragraph Styles"),
|
("paragraphStyles", "Paragraph Styles"),
|
||||||
("patterns", "Pattern Swatches"),
|
("patterns", "Pattern Swatches"),
|
||||||
|
|
|
||||||
|
|
@ -241,6 +241,9 @@ mod tests {
|
||||||
show(&mut app, ui);
|
show(&mut app, ui);
|
||||||
menu(&mut app, ui);
|
menu(&mut app, ui);
|
||||||
});
|
});
|
||||||
|
// The Appearance panel names the style the object is linked to.
|
||||||
|
let shown = frame(&ctx, |ui| super::super::appearance::show(&mut app, ui));
|
||||||
|
assert!(shown.iter().any(|t| t == "Rectangle: Mine"), "{shown:?}");
|
||||||
assert!(texts.iter().any(|t| t == "Mine") && texts.iter().any(|t| t == "Sunshine"), "{texts:?}");
|
assert!(texts.iter().any(|t| t == "Mine") && texts.iter().any(|t| t == "Sunshine"), "{texts:?}");
|
||||||
// Select All Unused picks every style but the one the rectangle is linked to.
|
// Select All Unused picks every style but the one the rectangle is linked to.
|
||||||
let unused: Vec<String> = serde_json::from_value(app.session.execute("graphicStyle.unused", &json!({})).unwrap()["names"].clone()).unwrap();
|
let unused: Vec<String> = serde_json::from_value(app.session.execute("graphicStyle.unused", &json!({})).unwrap()["names"].clone()).unwrap();
|
||||||
|
|
|
||||||
|
|
@ -36,3 +36,7 @@ fill or stroke (fields as in `swatchOptions` plus `group`; a gradient or pattern
|
||||||
`swatches`); `ui.dialog.confirm` runs `swatch.new` / `swatch.newGroup`.
|
`swatches`); `ui.dialog.confirm` runs `swatch.new` / `swatch.newGroup`.
|
||||||
|
|
||||||
The Color Picker is a dialog too: `engine.execute {command: "ui.colorPicker", params: {stroke?, color?}}` opens it for the fill (or stroke) proxy; `ui.dialog.set {field: "hex", value: "00FF00"}` (or `color`, `channel`, `webOnly`, `swatches`) then `ui.dialog.confirm` applies the colour through `paint.setFill` / `paint.setStroke`.
|
The Color Picker is a dialog too: `engine.execute {command: "ui.colorPicker", params: {stroke?, color?}}` opens it for the fill (or stroke) proxy; `ui.dialog.set {field: "hex", value: "00FF00"}` (or `color`, `channel`, `webOnly`, `swatches`) then `ui.dialog.confirm` applies the colour through `paint.setFill` / `paint.setStroke`.
|
||||||
|
|
||||||
|
Graphic styles: `ui.graphicStyleOptions {name}` opens the `graphicStyleOptions` dialog (field `name`) for a style and
|
||||||
|
`ui.dialog.confirm` renames it with `graphicStyle.rename` (a name another style has returns an error and keeps the
|
||||||
|
dialog open); without `name` it names a new style made from the selection (`graphicStyle.new`).
|
||||||
|
|
|
||||||
17
docs/mcp.md
17
docs/mcp.md
|
|
@ -196,3 +196,20 @@ plain group and keeps the clipping path, unpainted.
|
||||||
{"name":"run_command","arguments":{"command":"select.set","params":{"ids":[12,15]}}}
|
{"name":"run_command","arguments":{"command":"select.set","params":{"ids":[12,15]}}}
|
||||||
{"name":"run_command","arguments":{"command":"object.clippingMask.make","params":{}}}
|
{"name":"run_command","arguments":{"command":"object.clippingMask.make","params":{}}}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Graphic styles
|
||||||
|
|
||||||
|
A graphic style holds an appearance (fills, strokes, effects) plus opacity, blend mode, isolate and knockout.
|
||||||
|
`graphicStyle.new {name?, id?}` captures an object (a group without its own fills or strokes lends its topmost
|
||||||
|
object's, type its characters'). `graphicStyle.apply {name, ids?}` gives the objects the style and links them;
|
||||||
|
`add: true` adds the style on top of the existing appearance instead. Linked objects stay linked while they keep the
|
||||||
|
style's look: editing their appearance or transparency breaks the link, and `graphicStyle.redefine {name?, id?}`
|
||||||
|
updates only the objects still linked. `graphicStyle.list` returns each style with the ids linked to it and the
|
||||||
|
style of the first selected object; `select.same.graphicStyle` selects an object's fellow users.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"name":"run_command","arguments":{"command":"graphicStyle.new","params":{"id":12,"name":"Glow"}}}
|
||||||
|
{"name":"run_command","arguments":{"command":"graphicStyle.apply","params":{"name":"Glow","ids":[15,20]}}}
|
||||||
|
{"name":"run_command","arguments":{"command":"graphicStyle.redefine","params":{"id":12}}}
|
||||||
|
{"name":"run_command","arguments":{"command":"graphicStyle.list","params":{}}}
|
||||||
|
```
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue