mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-27 03:06:57 -06:00
Window > Graphic Style Libraries and the Graphic Styles panel's library button open read-only libraries of original styles generated in code (Shadows and Glows, Outlines and Rules, Hand-Drawn, Gradient Finishes, Shape Effects, Blends and Transparency) in the shared library panel. Clicking a style adds it and applies it to the selection as one undo step (Alt: on top). Save Graphic Style Library writes .vcstyles JSON (styles unlinked from swatches, with the patterns they paint with) to the user library folder (User Defined) or a file; Other Library loads one, or another document's styles. Commands: graphicStyle.libraries, graphicStyle.library, graphicStyle.addFromLibrary, graphicStyle.saveLibrary, graphicStyle.loadLibrary; UI: window.graphicStyleLibrary (.other), ui.saveGraphicStyleLibrary, window.userGraphicStyleLibrary1-10. The user and loaded library bookkeeping is shared with swatch libraries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
561 lines
22 KiB
Rust
561 lines
22 KiB
Rust
//! The Graphic Styles panel: new, apply (replace or add), link, redefine, break link, rename,
|
|
//! delete, duplicate, sort and list.
|
|
//!
|
|
//! Applying a style links the targeted objects to it ([`Node::graphic_style`]). An object stays
|
|
//! linked while it keeps the style's look; editing its appearance or transparency breaks the link
|
|
//! ([`in_sync`]), and Redefine Graphic Style updates only the objects still linked.
|
|
//!
|
|
//! Styles keep placed gradients relative to the unit box ([`GraphicStyle::unit_box`]): a new or
|
|
//! redefined style stores them relative to its source's bounds, and each object a style is applied
|
|
//! to gets them at the same place relative to its own.
|
|
//!
|
|
//! With Override Character Color on ([`crate::Prefs::override_char_color`]), type a style is applied
|
|
//! to loses its characters' own fill (stroke) when the style has fills (strokes), so the style's
|
|
//! paint shows instead of the character colour under it.
|
|
|
|
use std::collections::HashMap;
|
|
|
|
use serde_json::{Value, json};
|
|
use vectorcraft_color::Paint;
|
|
use vectorcraft_doc::{Appearance, DEFAULT_GRAPHIC_STYLE, Document, GraphicStyle, Node, NodeId, NodeKind};
|
|
use vectorcraft_geom::Rect;
|
|
|
|
use super::*;
|
|
use crate::EngineError;
|
|
|
|
pub fn specs() -> Vec<CommandSpec> {
|
|
vec![
|
|
cmd!(
|
|
"graphicStyle.apply",
|
|
"Apply Graphic Style",
|
|
["Window", "Graphic Styles"],
|
|
None,
|
|
"{name, add?: bool, ids? (layers too), target?: \"object\"|\"contents\"} give the objects (default: the selection; a group or layer itself, its fills and strokes painting its members and its effects applying to them as one piece; target contents: the objects inside instead) the style's appearance, opacity, blend mode, isolate and knockout, and link them to it (placed gradients land at the same place relative to each object's bounds); add: true (Alt-click) adds its fills, strokes and effects on top of the existing appearance instead and unlinks",
|
|
has_doc,
|
|
style_apply
|
|
),
|
|
cmd!(
|
|
"graphicStyle.new",
|
|
"New Graphic Style",
|
|
["Window", "Graphic Styles"],
|
|
None,
|
|
"{name?, id?} a style from object `id` or the first selected object: its appearance, opacity, blend mode, isolate and knockout (a group without fills or strokes of its own lends its topmost object's, type its characters'; placed gradients are kept relative to its bounds); links the object. Names are made unique → {name}",
|
|
has_doc,
|
|
style_new
|
|
),
|
|
cmd!(
|
|
"graphicStyle.delete",
|
|
"Delete Graphic Style",
|
|
["Window", "Graphic Styles"],
|
|
None,
|
|
"{name} | {names: [name…]} delete styles; their objects keep their look but are unlinked",
|
|
has_doc,
|
|
style_delete
|
|
),
|
|
cmd!("graphicStyle.duplicate", "Duplicate Graphic Style", ["Window", "Graphic Styles"], None, "{name} → {name}", has_doc, style_duplicate),
|
|
cmd!(
|
|
query "graphicStyle.list",
|
|
"List Graphic Styles",
|
|
[],
|
|
None,
|
|
"{} → {styles: [{id, name, fill, stroke, strokeWidth, fills, strokes, effects: [effect id…], opacity: 0..100, blend, isolate, knockout: \"on\"|\"off\"|\"neutral\", linked: [object ids]}], selected: the style the first selected object is linked to, or null, overrideCharColor}",
|
|
has_doc,
|
|
style_list
|
|
),
|
|
cmd!(
|
|
"graphicStyle.redefine",
|
|
"Redefine Graphic Style",
|
|
["Window", "Appearance"],
|
|
None,
|
|
"{name?, id?} replace the style (default: the one the object was last linked to) with the look of object `id` or the first selected object; the objects still linked to it update, and that object becomes linked → {name}",
|
|
has_doc,
|
|
style_redefine
|
|
),
|
|
cmd!(
|
|
"graphicStyle.breakLink",
|
|
"Break Link to Graphic Style",
|
|
["Window", "Graphic Styles"],
|
|
None,
|
|
"{ids?} unlink the objects (default: the selection) from their graphic style; they keep their look",
|
|
has_doc,
|
|
style_break_link
|
|
),
|
|
cmd!(
|
|
"graphicStyle.rename",
|
|
"Rename Graphic Style",
|
|
["Window", "Graphic Styles"],
|
|
None,
|
|
"{name, to} (Graphic Style Options; names are unique and linked objects stay linked) → {name}",
|
|
has_doc,
|
|
style_rename
|
|
),
|
|
cmd!(
|
|
query "graphicStyle.unused",
|
|
"Select All Unused",
|
|
["Window", "Graphic Styles"],
|
|
None,
|
|
"{} → {names: [the styles no object is linked to]} (the panel selects them)",
|
|
has_doc,
|
|
style_unused
|
|
),
|
|
cmd!(
|
|
"graphicStyle.sortByName",
|
|
"Sort by Name",
|
|
["Window", "Graphic Styles"],
|
|
None,
|
|
"{} sort the styles by name (the Default Graphic Style stays first)",
|
|
has_doc,
|
|
style_sort
|
|
),
|
|
cmd!(
|
|
"graphicStyle.merge",
|
|
"Merge Graphic Styles",
|
|
["Window", "Graphic Styles"],
|
|
None,
|
|
"{names: [name…] (two or more), name?} add a style combining the styles' fills, strokes and effects (each style's on top of the ones before it) with the first one's opacity, blend mode, isolate and knockout → {name}",
|
|
has_doc,
|
|
style_merge
|
|
),
|
|
cmd!(
|
|
"graphicStyle.move",
|
|
"Move Graphic Style",
|
|
[],
|
|
None,
|
|
"{name, to: index} move the style to position `to` of the list (0 = first, clamped), as dragging it in the Graphic Styles panel does",
|
|
has_doc,
|
|
style_move
|
|
),
|
|
cmd!(
|
|
query "graphicStyle.setOptions",
|
|
"Graphic Styles Options",
|
|
[],
|
|
None,
|
|
"{overrideCharColor?: bool} Override Character Color (the preference `overrideCharColor`, on by default): applying a style to type replaces its characters' fill and stroke with the style's fills and strokes → {overrideCharColor}",
|
|
always,
|
|
style_set_options
|
|
),
|
|
]
|
|
}
|
|
|
|
// ---------- links ----------
|
|
|
|
/// Does a style applied to `n` (`top`: `n` is the target itself) descend into its children? Groups
|
|
/// and layers style their contents, and so do other containers (blends…) inside them, while a
|
|
/// compound path takes it as a whole, as with paint commands ([`leaf_targets`]).
|
|
fn descends(n: &Node, top: bool) -> bool {
|
|
match n.kind {
|
|
NodeKind::Group { .. } | NodeKind::Layer { .. } => true,
|
|
NodeKind::Compound { .. } => false,
|
|
_ => !top && n.is_container(),
|
|
}
|
|
}
|
|
|
|
/// The objects whose appearance a style applied to `n` sets: `n`, or its painted contents.
|
|
fn painted<'a>(n: &'a Node, top: bool, out: &mut Vec<&'a Node>) {
|
|
match n.children() {
|
|
Some(ch) if descends(n, top) => ch.iter().for_each(|c| painted(c, false, out)),
|
|
_ => out.push(n),
|
|
}
|
|
}
|
|
|
|
/// Does `n` still have style `g`'s look (its transparency, and the style's appearance as its own;
|
|
/// a group or layer with no appearance of its own: on every painted object inside it, the look a
|
|
/// style made from it captures)? Linked objects that don't are no longer linked.
|
|
pub(crate) fn in_sync(n: &Node, g: &GraphicStyle) -> bool {
|
|
if !g.transparency_matches(n) {
|
|
return false;
|
|
}
|
|
if g.appearance_on(n).approx_eq(&n.appearance) {
|
|
return true;
|
|
}
|
|
let mut leaves = vec![];
|
|
painted(n, true, &mut leaves);
|
|
n.appearance == Appearance::default() && leaves.iter().all(|l| l.id != n.id && g.appearance_on(l).approx_eq(&l.appearance))
|
|
}
|
|
|
|
/// The style `n` is linked to (and still looks like).
|
|
pub(crate) fn linked_style<'a>(d: &'a Document, n: &Node) -> Option<&'a GraphicStyle> {
|
|
d.graphic_style_by_id(n.graphic_style?).filter(|g| in_sync(n, g))
|
|
}
|
|
|
|
/// Objects anywhere in the art that `f` accepts.
|
|
fn objects_where(d: &Document, mut f: impl FnMut(&Node) -> bool) -> Vec<NodeId> {
|
|
let mut ids = vec![];
|
|
for l in &d.layers {
|
|
l.walk(&mut |n| {
|
|
if f(n) {
|
|
ids.push(n.id);
|
|
}
|
|
});
|
|
}
|
|
ids
|
|
}
|
|
|
|
/// The objects linked to each style id.
|
|
fn links(d: &Document) -> HashMap<u32, Vec<NodeId>> {
|
|
let mut out: HashMap<u32, Vec<NodeId>> = HashMap::new();
|
|
for l in &d.layers {
|
|
l.walk(&mut |n| {
|
|
if let Some(g) = linked_style(d, n) {
|
|
out.entry(g.id).or_default().push(n.id);
|
|
}
|
|
});
|
|
}
|
|
out
|
|
}
|
|
|
|
/// `ap` with its placed gradients moved from box `from` (an object's bounds) to the unit box.
|
|
fn unitized(mut ap: Appearance, from: Option<Rect>) -> Appearance {
|
|
if let Some(b) = from.filter(|_| ap.has_placed_gradient()) {
|
|
ap.rebase_gradients(b, GraphicStyle::UNIT_BOX);
|
|
}
|
|
ap
|
|
}
|
|
|
|
/// The appearance a new or redefined style takes from `n`: its own. A group without fills or
|
|
/// strokes of its own lends those of its topmost painted object, and type its characters' fill
|
|
/// and stroke. Placed gradients come relative to the unit box (from the bounds of the object
|
|
/// they're on).
|
|
fn captured(n: &Node) -> Appearance {
|
|
let mut ap = n.appearance.clone();
|
|
if !ap.items.is_empty() {
|
|
return unitized(ap, n.geometric_bounds());
|
|
}
|
|
match &n.kind {
|
|
NodeKind::Text(t) => {
|
|
if let Some(r) = t.runs.first() {
|
|
ap.items = r.style.basic_appearance().items;
|
|
// Type paints its characters in text space.
|
|
ap = unitized(ap, Some(t.local_bounds()));
|
|
}
|
|
}
|
|
_ => {
|
|
let mut leaves = vec![];
|
|
painted(n, true, &mut leaves);
|
|
if let Some(top) = leaves.last().filter(|l| l.id != n.id) {
|
|
// The topmost object's own effects apply before the group's.
|
|
let top = captured(top);
|
|
ap.items = top.items;
|
|
ap.effects.splice(0..0, top.effects);
|
|
}
|
|
}
|
|
}
|
|
ap
|
|
}
|
|
|
|
/// Override Character Color: type taking appearance `ap` loses its characters' fill when `ap` has
|
|
/// fills, and their stroke when it has strokes.
|
|
fn override_chars(n: &mut Node, ap: &Appearance) {
|
|
let NodeKind::Text(t) = &mut n.kind else { return };
|
|
let (fills, strokes) = (ap.items.iter().any(|i| i.is_fill()), ap.items.iter().any(|i| !i.is_fill()));
|
|
for r in &mut t.runs {
|
|
if fills {
|
|
r.style.fill = Paint::None;
|
|
}
|
|
if strokes {
|
|
r.style.stroke = Paint::None;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Give `n` style `g`: its appearance (a group or layer too: its own, above its contents; type its
|
|
/// characters' colour too with `chars`, Override Character Color) and transparency.
|
|
fn style_node(n: &mut Node, g: &GraphicStyle, chars: bool) {
|
|
n.appearance = g.appearance_on(n).into_owned();
|
|
if chars {
|
|
override_chars(n, &g.appearance);
|
|
}
|
|
g.apply_transparency(n);
|
|
}
|
|
|
|
/// Give `id` style `g` ([`style_node`]) and link it.
|
|
fn apply_style(d: &mut Document, id: NodeId, g: &GraphicStyle, chars: bool) {
|
|
if let Some(n) = d.node_mut(id) {
|
|
style_node(n, g, chars);
|
|
n.graphic_style = Some(g.id);
|
|
}
|
|
}
|
|
|
|
fn set_link(d: &mut Document, ids: &[NodeId], link: Option<u32>) {
|
|
for id in ids {
|
|
if let Some(n) = d.node_mut(*id) {
|
|
n.graphic_style = link;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn style_index(d: &Document, name: &str) -> Result<usize> {
|
|
d.graphic_style_index(name).ok_or_else(|| EngineError::Other(format!("no graphic style `{name}`")))
|
|
}
|
|
|
|
/// The object a style is made from: `id`, else the first selected object.
|
|
fn source<'a>(s: &'a Session, p: &Value, cmd: &str) -> Result<(NodeId, &'a Node)> {
|
|
let id = match id_param(p, "id") {
|
|
Some(id) => id,
|
|
None => super::edit::selected_roots(s)?.first().copied().ok_or_else(|| bad(cmd, "select an object (or pass `id`)"))?,
|
|
};
|
|
Ok((id, s.doc()?.doc.node(id).ok_or_else(|| bad(cmd, format!("no object {}", id.0)))?))
|
|
}
|
|
|
|
impl Session {
|
|
/// The graphic style of the first selected object: the one it was last linked to, and whether
|
|
/// it is still linked (still has that style's look).
|
|
pub fn selection_graphic_style(&self) -> Option<(&GraphicStyle, bool)> {
|
|
let st = self.active()?;
|
|
let n = st.doc.node(*st.selection.subjects().first()?)?;
|
|
let g = st.doc.graphic_style_by_id(n.graphic_style?)?;
|
|
Some((g, in_sync(n, g)))
|
|
}
|
|
|
|
/// A copy of `n` with style `g` applied as `graphicStyle.apply` applies it (Override Character
|
|
/// Color as set): the Graphic Styles panel's previews.
|
|
pub fn styled(&self, n: &Node, g: &GraphicStyle) -> Node {
|
|
let mut n = n.clone();
|
|
style_node(&mut n, g, self.prefs.override_char_color);
|
|
n
|
|
}
|
|
}
|
|
|
|
// ---------- commands ----------
|
|
|
|
fn style_apply(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let name = str_param(p, "name").ok_or_else(|| bad("graphicStyle.apply", "missing name"))?;
|
|
let i = style_index(&s.doc()?.doc, name)?;
|
|
let add = bool_or(p, "add", false);
|
|
let chars = s.prefs.override_char_color;
|
|
let ids = super::appearance::appearance_targets(s, p)?;
|
|
if ids.is_empty() {
|
|
return Err(bad("graphicStyle.apply", "nothing selected (or pass `ids`)"));
|
|
}
|
|
s.edit("Apply Graphic Style", |d, _| {
|
|
if add {
|
|
let g = d.graphic_styles[i].clone();
|
|
for id in &ids {
|
|
if let Some(n) = d.node_mut(*id) {
|
|
let ap = g.appearance_on(n).into_owned();
|
|
n.appearance.items.extend(ap.items);
|
|
n.appearance.effects.extend(ap.effects);
|
|
n.graphic_style = None;
|
|
}
|
|
}
|
|
} else {
|
|
d.graphic_style_id(i);
|
|
let g = d.graphic_styles[i].clone();
|
|
for id in &ids {
|
|
apply_style(d, *id, &g, chars);
|
|
}
|
|
}
|
|
Ok(())
|
|
})?;
|
|
ok()
|
|
}
|
|
|
|
fn style_new(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let (src, n) = source(s, p, "graphicStyle.new")?;
|
|
let d = &s.doc()?.doc;
|
|
let name = match str_param(p, "name").map(str::trim).filter(|n| !n.is_empty()) {
|
|
Some(n) => unique_name(n, |x| d.graphic_style(x).is_some()),
|
|
None => d.new_graphic_style_name(),
|
|
};
|
|
let g = GraphicStyle { id: d.next_graphic_style_id(), ..GraphicStyle::of(name.clone(), captured(n), n) };
|
|
s.edit("New Graphic Style", |d, _| {
|
|
set_link(d, &[src], Some(g.id));
|
|
d.graphic_styles.push(g);
|
|
Ok(())
|
|
})?;
|
|
Ok(json!({ "name": name }))
|
|
}
|
|
|
|
fn style_delete(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let names: Vec<String> = match (p.get("names").and_then(Value::as_array), str_param(p, "name")) {
|
|
(Some(a), _) => a.iter().filter_map(Value::as_str).map(str::to_string).collect(),
|
|
(None, Some(n)) => vec![n.to_string()],
|
|
(None, None) => return Err(bad("graphicStyle.delete", "missing name")),
|
|
};
|
|
s.edit(if names.len() > 1 { "Delete Graphic Styles" } else { "Delete Graphic Style" }, |d, _| {
|
|
for name in &names {
|
|
let i = style_index(d, name)?;
|
|
let id = d.graphic_styles.remove(i).id;
|
|
if id != 0 {
|
|
let ids = objects_where(d, |n| n.graphic_style == Some(id));
|
|
set_link(d, &ids, None);
|
|
}
|
|
}
|
|
Ok(())
|
|
})?;
|
|
ok()
|
|
}
|
|
|
|
fn style_duplicate(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let name = str_param(p, "name").ok_or_else(|| bad("graphicStyle.duplicate", "missing name"))?.to_string();
|
|
let new = s.edit("Duplicate Graphic Style", |d, _| {
|
|
let pos = style_index(d, &name)?;
|
|
let nm = unique_name(&format!("{name} copy"), |n| d.graphic_style(n).is_some());
|
|
let g = GraphicStyle { name: nm.clone(), id: d.next_graphic_style_id(), ..d.graphic_styles[pos].clone() };
|
|
d.graphic_styles.insert(pos + 1, g);
|
|
Ok(nm)
|
|
})?;
|
|
Ok(json!({"name": new}))
|
|
}
|
|
|
|
fn style_list(s: &mut Session, _: &Value) -> Result<Value> {
|
|
let d = &s.doc()?.doc;
|
|
let links = links(d);
|
|
let styles: Vec<Value> = d
|
|
.graphic_styles
|
|
.iter()
|
|
.map(|g| {
|
|
let mut v = style_json(g);
|
|
v["id"] = json!(g.id);
|
|
v["linked"] = json!(links.get(&g.id).map(|v| v.iter().map(|i| i.0).collect::<Vec<_>>()).unwrap_or_default());
|
|
v
|
|
})
|
|
.collect();
|
|
let selected = s.selection_graphic_style().filter(|(_, linked)| *linked).map(|(g, _)| g.name.clone());
|
|
Ok(json!({ "styles": styles, "selected": selected, "overrideCharColor": s.prefs.override_char_color }))
|
|
}
|
|
|
|
/// Style `g` described as `graphicStyle.list` lists it (without `id` and `linked`).
|
|
pub(crate) fn style_json(g: &GraphicStyle) -> Value {
|
|
let ap = &g.appearance;
|
|
let count = |fill: bool| ap.items.iter().filter(|i| i.is_fill() == fill).count();
|
|
json!({
|
|
"name": g.name,
|
|
"fill": ap.fill_paint().label(),
|
|
"stroke": ap.stroke_paint().label(),
|
|
"strokeWidth": ap.stroke_width(),
|
|
"fills": count(true),
|
|
"strokes": count(false),
|
|
"effects": ap.effects.iter().map(|e| e.id.as_str()).collect::<Vec<_>>(),
|
|
"opacity": (g.opacity * 100.0).round(),
|
|
"blend": g.blend.label(),
|
|
"isolate": g.isolate,
|
|
"knockout": g.knockout.label(),
|
|
})
|
|
}
|
|
|
|
fn style_redefine(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let cmd = "graphicStyle.redefine";
|
|
let (src, n) = source(s, p, cmd)?;
|
|
let d = &s.doc()?.doc;
|
|
let i = match str_param(p, "name") {
|
|
Some(name) => style_index(d, name)?,
|
|
None => n
|
|
.graphic_style
|
|
.and_then(|id| d.graphic_style_by_id(id))
|
|
.and_then(|g| d.graphic_style_index(&g.name))
|
|
.ok_or_else(|| bad(cmd, "the object has no graphic style (pass `name`)"))?,
|
|
};
|
|
let look = GraphicStyle::of(String::new(), captured(n), n);
|
|
let name = d.graphic_styles[i].name.clone();
|
|
let chars = s.prefs.override_char_color;
|
|
s.edit(&format!("Redefine Graphic Style \u{201c}{name}\u{201d}"), |d, _| {
|
|
let id = d.graphic_style_id(i);
|
|
let old = std::mem::replace(&mut d.graphic_styles[i], GraphicStyle { name: name.clone(), id, ..look });
|
|
// Objects still linked follow the new definition; edited ones are unlinked (they keep
|
|
// their look), and the source keeps its own.
|
|
let linked = objects_where(d, |n| n.graphic_style == Some(id) && n.id != src);
|
|
let (follow, edited): (Vec<NodeId>, Vec<NodeId>) = linked.into_iter().partition(|nid| d.node(*nid).is_some_and(|n| in_sync(n, &old)));
|
|
set_link(d, &edited, None);
|
|
let g = d.graphic_styles[i].clone();
|
|
for nid in follow {
|
|
apply_style(d, nid, &g, chars);
|
|
}
|
|
set_link(d, &[src], Some(id));
|
|
Ok(())
|
|
})?;
|
|
Ok(json!({ "name": name }))
|
|
}
|
|
|
|
fn style_break_link(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let ids = super::appearance::appearance_targets(s, p)?;
|
|
s.edit("Break Link to Graphic Style", |d, _| {
|
|
set_link(d, &ids, None);
|
|
Ok(())
|
|
})?;
|
|
ok()
|
|
}
|
|
|
|
fn style_rename(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let cmd = "graphicStyle.rename";
|
|
let name = str_param(p, "name").ok_or_else(|| bad(cmd, "missing name"))?.to_string();
|
|
let to = str_param(p, "to").map(str::trim).filter(|t| !t.is_empty()).ok_or_else(|| bad(cmd, "missing `to`"))?.to_string();
|
|
s.edit("Graphic Style Options", |d, _| {
|
|
let i = style_index(d, &name)?;
|
|
if to != name && d.graphic_style(&to).is_some() {
|
|
return Err(EngineError::Other(format!("a graphic style named `{to}` already exists")));
|
|
}
|
|
d.graphic_styles[i].name = to.clone();
|
|
Ok(())
|
|
})?;
|
|
Ok(json!({ "name": to }))
|
|
}
|
|
|
|
fn style_unused(s: &mut Session, _: &Value) -> Result<Value> {
|
|
let d = &s.doc()?.doc;
|
|
let links = links(d);
|
|
let names: Vec<&str> = d.graphic_styles.iter().filter(|g| !links.contains_key(&g.id)).map(|g| g.name.as_str()).collect();
|
|
Ok(json!({ "names": names }))
|
|
}
|
|
|
|
fn style_sort(s: &mut Session, _: &Value) -> Result<Value> {
|
|
s.edit("Sort by Name", |d, _| {
|
|
d.graphic_styles.sort_by_cached_key(|g| (g.name != DEFAULT_GRAPHIC_STYLE, g.name.to_lowercase()));
|
|
Ok(())
|
|
})?;
|
|
ok()
|
|
}
|
|
|
|
fn style_merge(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let cmd = "graphicStyle.merge";
|
|
let names: Vec<&str> = p.get("names").and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_str).collect()).unwrap_or_default();
|
|
if names.len() < 2 {
|
|
return Err(bad(cmd, "give two or more style `names`"));
|
|
}
|
|
let d = &s.doc()?.doc;
|
|
let styles = names.iter().map(|n| style_index(d, n).map(|i| &d.graphic_styles[i])).collect::<Result<Vec<_>>>()?;
|
|
let name = match str_param(p, "name").map(str::trim).filter(|n| !n.is_empty()) {
|
|
Some(n) => unique_name(n, |x| d.graphic_style(x).is_some()),
|
|
None => d.new_graphic_style_name(),
|
|
};
|
|
// The first style's contents slot (groups and type) stays where it was in its stack.
|
|
let mut ap = styles[0].appearance.clone();
|
|
for g in &styles[1..] {
|
|
ap.items.extend(g.appearance.items.iter().cloned());
|
|
ap.effects.extend(g.appearance.effects.iter().cloned());
|
|
}
|
|
// Styles saved before unit-box styles keep their placed gradients in document coordinates.
|
|
let unit_box = styles.iter().filter(|g| g.appearance.has_placed_gradient()).all(|g| g.unit_box);
|
|
let g = GraphicStyle { name: name.clone(), appearance: ap, id: d.next_graphic_style_id(), unit_box, ..styles[0].clone() };
|
|
s.edit("Merge Graphic Styles", |d, _| {
|
|
d.graphic_styles.push(g);
|
|
Ok(())
|
|
})?;
|
|
Ok(json!({ "name": name }))
|
|
}
|
|
|
|
fn style_move(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let cmd = "graphicStyle.move";
|
|
let name = str_param(p, "name").ok_or_else(|| bad(cmd, "missing name"))?;
|
|
let to = p.get("to").and_then(Value::as_u64).ok_or_else(|| bad(cmd, "missing `to` (an index)"))?;
|
|
let d = &s.doc()?.doc;
|
|
let from = style_index(d, name)?;
|
|
let to = (to as usize).min(d.graphic_styles.len() - 1);
|
|
if from == to {
|
|
return ok();
|
|
}
|
|
s.edit("Move Graphic Style", |d, _| {
|
|
let g = d.graphic_styles.remove(from);
|
|
d.graphic_styles.insert(to, g);
|
|
Ok(())
|
|
})?;
|
|
ok()
|
|
}
|
|
|
|
fn style_set_options(s: &mut Session, p: &Value) -> Result<Value> {
|
|
if let Some(on) = p.get("overrideCharColor").and_then(Value::as_bool) {
|
|
s.prefs.override_char_color = on;
|
|
}
|
|
Ok(json!({ "overrideCharColor": s.prefs.override_char_color }))
|
|
}
|