mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 02:19:56 -06:00
Fixes #5902 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
932 lines
39 KiB
Rust
932 lines
39 KiB
Rust
//! The document resources copied objects use, and adding them to the document they're pasted into.
|
|
//!
|
|
//! Objects refer to resources by name (image blobs, symbols, patterns, swatches, character and
|
|
//! paragraph styles, brushes) and to graphic styles by id. Copy follows those references, also
|
|
//! through the symbols, patterns, styles and brush art it finds, and keeps what they lead to.
|
|
//!
|
|
//! Pasting matches each resource by name: an identical one in the document is used, a missing one
|
|
//! is added, and a different one of the same name is added under a free name, which the pasted
|
|
//! objects follow. Global and spot swatches are the exception: for a name the document gives
|
|
//! another colour the caller picks Merge (the objects take the document's swatch) or Add (the
|
|
//! pasted swatch comes in renamed), the Swatch Conflict dialog's question. Colours linked to a
|
|
//! swatch then show it as the document does: its colour at their tint, and a Lab spot colour as
|
|
//! the document's Spot Colors option says. Pasting back into the document the objects came from
|
|
//! uses its resources as they are now.
|
|
|
|
use std::cell::OnceCell;
|
|
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
|
|
use std::sync::Arc;
|
|
|
|
use serde_json::{Map, Value, json};
|
|
use vectorcraft_brush::Brush;
|
|
use vectorcraft_color::{Color, Paint, Swatch};
|
|
use vectorcraft_doc::{
|
|
Appearance, AppearanceItem, Document, GraphicStyle, Node, NodeKind, PatternDef, Symbol, SymbolExportType, SymbolNineSlice, SymbolRegistration,
|
|
TextStyleDef,
|
|
};
|
|
|
|
use super::super::brushsym::{set_natural_size, stored_natural_size};
|
|
|
|
use super::super::patterncmds::add_pattern;
|
|
use super::super::{Result, bad, unique_name};
|
|
use super::Clipboard;
|
|
|
|
/// A resource kind objects refer to (graphic styles by id, the others by name).
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
|
enum Res {
|
|
Image,
|
|
Symbol,
|
|
Pattern,
|
|
Swatch,
|
|
CharStyle,
|
|
ParaStyle,
|
|
Brush,
|
|
GraphicStyle,
|
|
}
|
|
|
|
impl Res {
|
|
fn id(self) -> &'static str {
|
|
match self {
|
|
Res::Image => "image",
|
|
Res::Symbol => "symbol",
|
|
Res::Pattern => "pattern",
|
|
Res::Swatch => "swatch",
|
|
Res::CharStyle => "charStyle",
|
|
Res::ParaStyle => "paraStyle",
|
|
Res::Brush => "brush",
|
|
Res::GraphicStyle => "graphicStyle",
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A reference from art to a resource.
|
|
enum Ref<'a> {
|
|
Name(Res, &'a str),
|
|
/// A colour linked to a swatch, at a tint.
|
|
Swatch(&'a str, f32),
|
|
/// A link to the graphic style with this id.
|
|
Style(u32),
|
|
}
|
|
|
|
type Visit<'v> = &'v mut dyn FnMut(Ref<'_>);
|
|
|
|
fn paint_refs(p: &Paint, f: Visit) {
|
|
match p {
|
|
Paint::Solid { swatch: Some(s), tint, .. } => f(Ref::Swatch(s, *tint)),
|
|
Paint::Gradient(g) => {
|
|
// The gradient swatch it was applied from, and its stops' links.
|
|
if let Some(s) = &g.swatch {
|
|
f(Ref::Name(Res::Swatch, s));
|
|
}
|
|
g.gradient.stops.iter().filter_map(|st| Some((st.swatch.as_deref()?, st.tint))).for_each(|(s, t)| f(Ref::Swatch(s, t)));
|
|
}
|
|
Paint::Pattern { pattern, .. } => f(Ref::Name(Res::Pattern, pattern)),
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
fn appearance_refs(a: &Appearance, f: Visit) {
|
|
for it in &a.items {
|
|
paint_refs(it.paint(), f);
|
|
if let AppearanceItem::Stroke(l) = it
|
|
&& let Some(b) = &l.brush
|
|
{
|
|
f(Ref::Name(Res::Brush, b));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The paints a character or paragraph style can set (by their serialized names).
|
|
const STYLE_PAINTS: [&str; 2] = ["fill", "stroke"];
|
|
|
|
fn style_paint(attrs: &Map<String, Value>, key: &str) -> Option<Paint> {
|
|
serde_json::from_value(attrs.get(key)?.clone()).ok()
|
|
}
|
|
|
|
fn attrs_refs(attrs: &Map<String, Value>, f: Visit) {
|
|
for p in STYLE_PAINTS.iter().filter_map(|k| style_paint(attrs, k)) {
|
|
paint_refs(&p, f);
|
|
}
|
|
}
|
|
|
|
/// Every reference in the subtree of `n` (opacity-mask art included).
|
|
fn node_refs(n: &Node, f: Visit) {
|
|
n.walk(&mut |m| {
|
|
appearance_refs(&m.appearance, f);
|
|
if let Some(id) = m.extras().graphic_style {
|
|
f(Ref::Style(id));
|
|
}
|
|
if let Some(mask) = &m.extras().mask {
|
|
node_refs(&mask.art, f);
|
|
}
|
|
match &m.kind {
|
|
NodeKind::Text(t) => {
|
|
for r in &t.runs {
|
|
paint_refs(&r.style.fill, f);
|
|
paint_refs(&r.style.stroke, f);
|
|
if let Some(s) = &r.style.style_name {
|
|
f(Ref::Name(Res::CharStyle, s));
|
|
}
|
|
}
|
|
for s in t.para_styles().filter_map(|pa| pa.style_name.as_ref()) {
|
|
f(Ref::Name(Res::ParaStyle, s));
|
|
}
|
|
}
|
|
NodeKind::Image(im) => f(Ref::Name(Res::Image, &im.key)),
|
|
NodeKind::PlacedDocument(pl) => f(Ref::Name(Res::Image, &pl.key)),
|
|
NodeKind::SymbolInstance { symbol, .. } => f(Ref::Name(Res::Symbol, symbol)),
|
|
_ => {}
|
|
}
|
|
});
|
|
}
|
|
|
|
/// A global or spot colour of its own (not a tint of another swatch): what colours link to.
|
|
fn is_base(w: &Swatch) -> bool {
|
|
(w.global || w.spot) && matches!(w.paint, Paint::Solid { swatch: None, .. })
|
|
}
|
|
|
|
/// The definitions references lead to: a document's, or the clipboard's own.
|
|
struct Defs<'a> {
|
|
swatches: Vec<&'a Swatch>,
|
|
symbols: &'a [Symbol],
|
|
patterns: &'a [PatternDef],
|
|
styles: &'a [GraphicStyle],
|
|
char_styles: &'a [TextStyleDef],
|
|
para_styles: &'a [TextStyleDef],
|
|
brushes: Brushes<'a>,
|
|
/// Where each name is defined, built the first time a kind is followed.
|
|
index: DefIndex,
|
|
}
|
|
|
|
/// The positions of a kind's definitions by name.
|
|
type NameIndex = HashMap<String, Vec<usize>>;
|
|
|
|
/// [`Defs`]' definitions by name (graphic styles by id), each kind indexed the first time a
|
|
/// reference to it is followed: finding a name's definitions by scanning them all made a
|
|
/// document or library of N definitions that use each other cost N² comparisons.
|
|
#[derive(Default)]
|
|
struct DefIndex {
|
|
swatches: OnceCell<NameIndex>,
|
|
symbols: OnceCell<NameIndex>,
|
|
patterns: OnceCell<NameIndex>,
|
|
styles: OnceCell<HashMap<u32, Vec<usize>>>,
|
|
char_styles: OnceCell<NameIndex>,
|
|
para_styles: OnceCell<NameIndex>,
|
|
brushes: OnceCell<NameIndex>,
|
|
}
|
|
|
|
/// The items of `list` named `name`, in order, `index` holding their positions (built from
|
|
/// `list` the first time).
|
|
fn named<'l, T>(index: &OnceCell<NameIndex>, list: &'l [T], name_of: impl Fn(&T) -> &str, name: &str) -> impl Iterator<Item = &'l T> {
|
|
let index = index.get_or_init(|| {
|
|
let mut by: NameIndex = HashMap::new();
|
|
for (i, item) in list.iter().enumerate() {
|
|
by.entry(name_of(item).to_string()).or_default().push(i);
|
|
}
|
|
by
|
|
});
|
|
let at: &[usize] = index.get(name).map_or(&[], Vec::as_slice);
|
|
at.iter().filter_map(move |&i| list.get(i))
|
|
}
|
|
|
|
/// The brushes references lead to: a document's library, read the first time a brush is used
|
|
/// (`vectorcraft_brush::library` parses it), or the clipboard's own.
|
|
enum Brushes<'a> {
|
|
Lazy(&'a Document, &'a OnceCell<Vec<Brush>>),
|
|
Ready(&'a [Brush]),
|
|
}
|
|
|
|
impl<'a> Brushes<'a> {
|
|
fn get(&self) -> &'a [Brush] {
|
|
match *self {
|
|
Brushes::Lazy(d, cell) => cell.get_or_init(|| vectorcraft_brush::library(d)),
|
|
Brushes::Ready(list) => list,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'a> Defs<'a> {
|
|
/// `d`'s definitions; `brushes` holds its brush library once a brush is used.
|
|
fn of(d: &'a Document, brushes: &'a OnceCell<Vec<Brush>>) -> Self {
|
|
Self {
|
|
swatches: d.swatches_iter().collect(),
|
|
symbols: &d.symbols,
|
|
patterns: &d.patterns,
|
|
styles: &d.graphic_styles,
|
|
char_styles: &d.char_styles,
|
|
para_styles: &d.para_styles,
|
|
brushes: Brushes::Lazy(d, brushes),
|
|
index: DefIndex::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// What references lead out of.
|
|
enum Source<'a> {
|
|
Node(&'a Node),
|
|
Appearance(&'a Appearance),
|
|
Attrs(&'a Map<String, Value>),
|
|
Paint(&'a Paint),
|
|
}
|
|
|
|
/// The resources a set of objects uses, through the definitions they lead to.
|
|
#[derive(Default)]
|
|
pub(super) struct Used {
|
|
names: BTreeMap<Res, BTreeSet<String>>,
|
|
/// The tints (below 100 %) of the swatches used: (swatch, tint).
|
|
tints: Vec<(String, f32)>,
|
|
styles: BTreeSet<u32>,
|
|
}
|
|
|
|
impl Used {
|
|
fn of<'a>(defs: &Defs<'a>, nodes: impl IntoIterator<Item = &'a Node>) -> Self {
|
|
let mut used = Used::default();
|
|
let mut todo: Vec<Source<'a>> = nodes.into_iter().map(Source::Node).collect();
|
|
while let Some(src) = todo.pop() {
|
|
let mut found = |r: Ref<'_>| used.note(defs, r, &mut todo);
|
|
match src {
|
|
Source::Node(n) => node_refs(n, &mut found),
|
|
Source::Appearance(a) => appearance_refs(a, &mut found),
|
|
Source::Attrs(a) => attrs_refs(a, &mut found),
|
|
Source::Paint(p) => paint_refs(p, &mut found),
|
|
}
|
|
}
|
|
used
|
|
}
|
|
|
|
/// Note `r`; a definition seen for the first time joins `todo`.
|
|
fn note<'a>(&mut self, defs: &Defs<'a>, r: Ref<'_>, todo: &mut Vec<Source<'a>>) {
|
|
let (res, name) = match r {
|
|
Ref::Style(id) => {
|
|
if self.styles.insert(id) && id != 0 {
|
|
let index = defs.index.styles.get_or_init(|| {
|
|
let mut by: HashMap<u32, Vec<usize>> = HashMap::new();
|
|
for (i, g) in defs.styles.iter().enumerate() {
|
|
by.entry(g.id).or_default().push(i);
|
|
}
|
|
by
|
|
});
|
|
let at = index.get(&id).map_or(&[][..], Vec::as_slice);
|
|
todo.extend(at.iter().filter_map(|&i| defs.styles.get(i)).map(|g| Source::Appearance(&g.appearance)));
|
|
}
|
|
return;
|
|
}
|
|
Ref::Swatch(name, tint) => {
|
|
if tint < 1.0 && !self.has_tint(name, tint) {
|
|
self.tints.push((name.to_string(), tint));
|
|
}
|
|
(Res::Swatch, name)
|
|
}
|
|
Ref::Name(res, name) => (res, name),
|
|
};
|
|
let set = self.names.entry(res).or_default();
|
|
if set.contains(name) {
|
|
return;
|
|
}
|
|
set.insert(name.to_string());
|
|
let ix = &defs.index;
|
|
match res {
|
|
// A tint swatch's base, a gradient swatch's linked stops.
|
|
Res::Swatch => todo.extend(named(&ix.swatches, &defs.swatches, |w| &w.name, name).map(|w| Source::Paint(&w.paint))),
|
|
Res::Symbol => todo.extend(named(&ix.symbols, defs.symbols, |s| &s.name, name).map(|s| Source::Node(&s.art))),
|
|
Res::Pattern => todo.extend(named(&ix.patterns, defs.patterns, |p| &p.name, name).flat_map(|p| p.art.iter().map(|a| Source::Node(a)))),
|
|
Res::CharStyle => todo.extend(named(&ix.char_styles, defs.char_styles, |s| &s.name, name).map(|s| Source::Attrs(&s.attrs))),
|
|
Res::ParaStyle => todo.extend(named(&ix.para_styles, defs.para_styles, |s| &s.name, name).map(|s| Source::Attrs(&s.attrs))),
|
|
// A scatter or art brush's art, a pattern brush's tiles.
|
|
Res::Brush => todo.extend(named(&ix.brushes, defs.brushes.get(), |b| &b.name, name).flat_map(|b| b.art().into_iter().map(Source::Node))),
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
fn has(&self, r: Res, name: &str) -> bool {
|
|
self.names.get(&r).is_some_and(|s| s.contains(name))
|
|
}
|
|
fn has_tint(&self, name: &str, tint: f32) -> bool {
|
|
self.tints.iter().any(|(n, t)| n == name && *t == tint)
|
|
}
|
|
/// Does the clipboard carry swatch `w`: a used global or spot colour or gradient swatch, or a
|
|
/// tint swatch of a tint used? (Never the built-in Registration swatch.)
|
|
fn carries(&self, w: &Swatch) -> bool {
|
|
let named = is_base(w) || matches!(w.paint, Paint::Gradient(_));
|
|
!w.is_reserved() && (named && self.has(Res::Swatch, &w.name) || w.tint_of().is_some_and(|(b, t)| self.has_tint(b, t)))
|
|
}
|
|
}
|
|
|
|
/// What a preview of some art shows besides the art itself ([`art_resources`]).
|
|
pub struct ArtResources {
|
|
/// Changes when any resource the art uses changes, and only then.
|
|
pub key: u64,
|
|
/// The art and image bytes counted in `key` by their address, held weakly: their allocations
|
|
/// stay theirs (no other art can take an address) for as long as the key is kept.
|
|
_kept: (Vec<std::sync::Weak<Node>>, Vec<std::sync::Weak<Vec<u8>>>),
|
|
}
|
|
|
|
/// The resources `art` uses in `doc`, through the definitions they lead to as Copy follows them
|
|
/// ([`Used`]): images, symbols, patterns (tile type, overlap and spacing too), swatches (linked
|
|
/// colours, tints, gradient swatches; colour groups' included), character and paragraph styles,
|
|
/// brushes and graphic styles, and the document's Spot Colors option. Art and image bytes count
|
|
/// by identity, the other definitions by their saved form: working the key out walks `art` and
|
|
/// the definitions it leads to, and never reads an image's bytes.
|
|
pub fn art_resources<'a>(doc: &'a Document, art: impl IntoIterator<Item = &'a Node>) -> ArtResources {
|
|
use std::hash::{Hash, Hasher};
|
|
let library = OnceCell::new();
|
|
let defs = Defs::of(doc, &library);
|
|
// The art's references shortened to the brush library's life.
|
|
let mut nodes: Vec<&Node> = vec![];
|
|
for n in art {
|
|
nodes.push(n);
|
|
}
|
|
let used = Used::of(&defs, nodes);
|
|
let mut h = std::collections::hash_map::DefaultHasher::new();
|
|
let mut kept = (vec![], vec![]);
|
|
fn saved(h: &mut impl Hasher, v: &impl serde::Serialize) {
|
|
serde_json::to_vec(v).unwrap_or_default().hash(h);
|
|
}
|
|
fn node(h: &mut impl Hasher, kept: &mut Vec<std::sync::Weak<Node>>, a: &Arc<Node>) {
|
|
(Arc::as_ptr(a) as usize).hash(h);
|
|
kept.push(Arc::downgrade(a));
|
|
}
|
|
for (res, names) in &used.names {
|
|
for name in names {
|
|
(*res as u8, name).hash(&mut h);
|
|
match res {
|
|
Res::Image => {
|
|
if let Some(b) = doc.images.get(name) {
|
|
b.mime.hash(&mut h);
|
|
for bytes in std::iter::once(&b.bytes).chain(&b.proxy) {
|
|
(Arc::as_ptr(bytes) as usize).hash(&mut h);
|
|
kept.1.push(Arc::downgrade(bytes));
|
|
}
|
|
}
|
|
}
|
|
Res::Symbol => doc.symbols.iter().filter(|s| s.name == *name).for_each(|s| node(&mut h, &mut kept.0, &s.art)),
|
|
Res::Pattern => {
|
|
for p in doc.patterns.iter().filter(|p| p.name == *name) {
|
|
saved(&mut h, &PatternDef { art: vec![], ..p.clone() });
|
|
p.art.iter().for_each(|a| node(&mut h, &mut kept.0, a));
|
|
}
|
|
}
|
|
Res::CharStyle => doc.char_styles.iter().filter(|s| s.name == *name).for_each(|s| saved(&mut h, s)),
|
|
Res::ParaStyle => doc.para_styles.iter().filter(|s| s.name == *name).for_each(|s| saved(&mut h, s)),
|
|
Res::Brush => defs.brushes.get().iter().filter(|b| b.name == *name).for_each(|b| saved(&mut h, b)),
|
|
// Swatches below (tints count too); graphic styles go by id.
|
|
Res::Swatch | Res::GraphicStyle => {}
|
|
}
|
|
}
|
|
}
|
|
for w in doc.swatches_iter().filter(|w| used.carries(w) || used.has(Res::Swatch, &w.name)) {
|
|
saved(&mut h, w);
|
|
}
|
|
for g in doc.graphic_styles.iter().filter(|g| g.id != 0 && used.styles.contains(&g.id)) {
|
|
saved(&mut h, g);
|
|
}
|
|
if used.names.contains_key(&Res::Swatch) {
|
|
doc.spot_use_lab.hash(&mut h);
|
|
}
|
|
ArtResources { key: h.finish(), _kept: kept }
|
|
}
|
|
|
|
/// A resource list entry known by its name.
|
|
trait Named: Clone {
|
|
fn name(&self) -> &str;
|
|
fn set_name(&mut self, name: String);
|
|
}
|
|
|
|
macro_rules! named {
|
|
($($t:ty),+) => {$(
|
|
impl Named for $t {
|
|
fn name(&self) -> &str {
|
|
&self.name
|
|
}
|
|
fn set_name(&mut self, name: String) {
|
|
self.name = name;
|
|
}
|
|
}
|
|
)+};
|
|
}
|
|
named!(Symbol, TextStyleDef, Brush);
|
|
|
|
/// Where a pasted resource ended up in the document.
|
|
enum Placed {
|
|
/// The document's own of that name is used.
|
|
Existing,
|
|
/// The document's own numbered copy ("Name 2", from an earlier paste) is used.
|
|
Reused(String),
|
|
Added,
|
|
/// Added under a free name.
|
|
Renamed(String),
|
|
}
|
|
|
|
impl Placed {
|
|
/// Added as `name`, which `from` was free or not.
|
|
fn added(from: &str, name: String) -> Self {
|
|
if name == from { Placed::Added } else { Placed::Renamed(name) }
|
|
}
|
|
/// The name of the definition the paste added (`None` when the document's own is used).
|
|
fn new_name(&self, from: &str) -> Option<String> {
|
|
match self {
|
|
Placed::Existing | Placed::Reused(_) => None,
|
|
Placed::Added => Some(from.to_string()),
|
|
Placed::Renamed(n) => Some(n.clone()),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Is `name` `base` or a numbered copy of it ("base 2")?
|
|
fn copy_of(base: &str, name: &str) -> bool {
|
|
name == base || name.strip_prefix(base).and_then(|r| r.strip_prefix(' ')).is_some_and(|n| !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit()))
|
|
}
|
|
|
|
/// The document's own resource for pasted resource `name`, among the document's `names`: the
|
|
/// one of that name when `same` holds for it (or, pasting back into the source document, at all),
|
|
/// else a numbered copy of it (added by an earlier paste) that is the same. `None`: add it.
|
|
fn existing<'a>(name: &str, names: impl IntoIterator<Item = &'a str>, same_doc: bool, same: impl Fn(&str) -> bool) -> Option<Placed> {
|
|
let mut found: Vec<&str> = names.into_iter().filter(|n| copy_of(name, n)).collect();
|
|
// The name itself first.
|
|
found.sort_by_key(|n| *n != name);
|
|
if same_doc && found.first() == Some(&name) {
|
|
return Some(Placed::Existing);
|
|
}
|
|
found.into_iter().find(|n| same(n)).map(|n| if n == name { Placed::Existing } else { Placed::Reused(n.to_string()) })
|
|
}
|
|
|
|
/// Find `item` in `list` ([`existing`], `same` comparing it under the entry's name) or add it
|
|
/// under a free name.
|
|
fn place<T: Named>(list: &mut Vec<T>, item: &T, same_doc: bool, same: impl Fn(&T, &T) -> bool) -> Placed {
|
|
let same_as = |n: &str| {
|
|
let mut c = item.clone();
|
|
c.set_name(n.to_string());
|
|
list.iter().find(|x| x.name() == n).is_some_and(|x| same(x, &c))
|
|
};
|
|
if let Some(p) = existing(item.name(), list.iter().map(Named::name), same_doc, same_as) {
|
|
return p;
|
|
}
|
|
let name = unique_name(item.name(), |n| list.iter().any(|x| x.name() == n));
|
|
let mut c = item.clone();
|
|
c.set_name(name.clone());
|
|
list.push(c);
|
|
Placed::added(item.name(), name)
|
|
}
|
|
|
|
/// Merge or Add, for a swatch whose name the document gives another colour.
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub(crate) enum SwatchChoice {
|
|
#[default]
|
|
Merge,
|
|
Add,
|
|
}
|
|
|
|
/// The `swatchConflict` parameter: one choice for every conflict, or one per swatch name (the
|
|
/// others merge).
|
|
#[derive(Clone, Debug, Default)]
|
|
pub(crate) struct SwatchChoices {
|
|
all: SwatchChoice,
|
|
by_name: BTreeMap<String, SwatchChoice>,
|
|
}
|
|
|
|
impl SwatchChoices {
|
|
pub(crate) fn parse(cmd: &str, p: &Value) -> Result<Self> {
|
|
let one = |v: &Value| match v.as_str() {
|
|
Some("merge") => Ok(SwatchChoice::Merge),
|
|
Some("add") => Ok(SwatchChoice::Add),
|
|
_ => Err(bad(cmd, format!("swatchConflict must be \"merge\" or \"add\", not {v}"))),
|
|
};
|
|
match p.get("swatchConflict") {
|
|
None | Some(Value::Null) => Ok(Self::default()),
|
|
Some(Value::Object(m)) => {
|
|
Ok(Self { all: SwatchChoice::Merge, by_name: m.iter().map(|(k, v)| Ok((k.clone(), one(v)?))).collect::<Result<_>>()? })
|
|
}
|
|
Some(v) => Ok(Self { all: one(v)?, by_name: BTreeMap::new() }),
|
|
}
|
|
}
|
|
fn of(&self, name: &str) -> SwatchChoice {
|
|
self.by_name.get(name).copied().unwrap_or(self.all)
|
|
}
|
|
}
|
|
|
|
/// How pasted art is relinked to the document's resources.
|
|
#[derive(Debug, Default)]
|
|
struct Relink {
|
|
renames: BTreeMap<Res, BTreeMap<String, String>>,
|
|
/// Graphic style id on the clipboard → its id in the document. Links to other ids are dropped.
|
|
styles: BTreeMap<u32, u32>,
|
|
/// Swatch (its name in the document) → the colour linked colours show at 100 %.
|
|
colors: BTreeMap<String, Color>,
|
|
}
|
|
|
|
impl Relink {
|
|
fn name(&self, r: Res, s: &mut String) {
|
|
if let Some(n) = self.renames.get(&r).and_then(|m| m.get(s.as_str())) {
|
|
*s = n.clone();
|
|
}
|
|
}
|
|
/// `name` as the document knows it.
|
|
fn renamed(&self, r: Res, name: &str) -> String {
|
|
let mut n = name.to_string();
|
|
self.name(r, &mut n);
|
|
n
|
|
}
|
|
fn paint(&self, p: &mut Paint) {
|
|
match p {
|
|
Paint::Pattern { pattern, .. } => self.name(Res::Pattern, pattern),
|
|
Paint::Gradient(g) => {
|
|
if let Some(s) = &mut g.swatch {
|
|
self.name(Res::Swatch, s);
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
p.map_links(&mut |c, link, tint| {
|
|
if let Some(l) = link {
|
|
self.name(Res::Swatch, l);
|
|
if let Some(base) = self.colors.get(l.as_str()) {
|
|
*c = base.tinted(*tint);
|
|
}
|
|
}
|
|
false
|
|
});
|
|
}
|
|
fn appearance(&self, a: &mut Appearance) {
|
|
for it in &mut a.items {
|
|
self.paint(it.paint_mut());
|
|
if let AppearanceItem::Stroke(l) = it
|
|
&& let Some(b) = &mut l.brush
|
|
{
|
|
self.name(Res::Brush, b);
|
|
}
|
|
}
|
|
}
|
|
fn attrs(&self, a: &mut Map<String, Value>) {
|
|
for k in STYLE_PAINTS {
|
|
let Some(before) = style_paint(a, k) else { continue };
|
|
let mut p = before.clone();
|
|
self.paint(&mut p);
|
|
if p != before
|
|
&& let Ok(v) = serde_json::to_value(&p)
|
|
{
|
|
a.insert(k.to_string(), v);
|
|
}
|
|
}
|
|
}
|
|
/// Relink a node tree (the mirror of [`node_refs`]).
|
|
fn node(&self, n: &mut Node) {
|
|
self.appearance(&mut n.appearance);
|
|
n.extras_mut().graphic_style = n.extras().graphic_style.and_then(|id| self.styles.get(&id).copied());
|
|
if let Some(m) = n.extras_if_any().and_then(|e| e.mask.as_mut()) {
|
|
self.node(Arc::make_mut(&mut m.art));
|
|
}
|
|
match &mut n.kind {
|
|
NodeKind::Text(t) => {
|
|
for r in &mut t.runs {
|
|
self.paint(&mut r.style.fill);
|
|
self.paint(&mut r.style.stroke);
|
|
if let Some(s) = &mut r.style.style_name {
|
|
self.name(Res::CharStyle, s);
|
|
}
|
|
}
|
|
for pa in t.para_styles_mut() {
|
|
if let Some(s) = &mut pa.style_name {
|
|
self.name(Res::ParaStyle, s);
|
|
}
|
|
}
|
|
}
|
|
NodeKind::Image(im) => self.name(Res::Image, &mut im.key),
|
|
NodeKind::PlacedDocument(pl) => self.name(Res::Image, &mut pl.key),
|
|
NodeKind::SymbolInstance { symbol, .. } => self.name(Res::Symbol, symbol),
|
|
_ => {}
|
|
}
|
|
for c in n.children_mut().into_iter().flatten() {
|
|
self.node(Arc::make_mut(c));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Is `ours` (a document definition's art) the art of `theirs` (a pasted one's) once that is
|
|
/// relinked by `r`? Graphic style links don't count: they are relinked last.
|
|
fn same_art(r: &Relink, ours: &[Arc<Node>], theirs: &[Arc<Node>]) -> bool {
|
|
let unlinked = Relink::default();
|
|
let relinked = |r: &Relink, n: &Node| {
|
|
let mut n = n.clone();
|
|
r.node(&mut n);
|
|
n
|
|
};
|
|
ours.len() == theirs.len() && ours.iter().zip(theirs).all(|(a, b)| relinked(&unlinked, a) == relinked(r, b))
|
|
}
|
|
|
|
/// Is `ours` (a document brush) brush `theirs` (a pasted one) once the art of that is relinked by
|
|
/// `r`? Graphic style links don't count, as in [`same_art`].
|
|
fn same_brush(r: &Relink, ours: &Brush, theirs: &Brush) -> bool {
|
|
let relinked = |r: &Relink, b: &Brush| {
|
|
let mut b = b.clone();
|
|
b.art_mut().into_iter().for_each(|a| r.node(a));
|
|
b
|
|
};
|
|
relinked(&Relink::default(), ours) == relinked(r, theirs)
|
|
}
|
|
|
|
/// Does brush `b`'s art show symbols or patterns?
|
|
fn shows_definitions(b: &Brush) -> bool {
|
|
let mut found = false;
|
|
for a in b.art() {
|
|
node_refs(a, &mut |r| found |= matches!(r, Ref::Name(Res::Symbol | Res::Pattern, _)));
|
|
}
|
|
found
|
|
}
|
|
|
|
/// A symbol's Symbol Options, which its document keeps by symbol name: copied with the symbol and
|
|
/// given to it under the name pasting adds it as.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct SymbolOptions {
|
|
/// Natural size `(w, h)` (`None`: the document records none).
|
|
pub size: Option<(f64, f64)>,
|
|
pub registration: SymbolRegistration,
|
|
pub export_type: SymbolExportType,
|
|
/// 9-slice guides (`None`: 9-slice scaling off).
|
|
pub nine_slice: Option<SymbolNineSlice>,
|
|
}
|
|
|
|
impl SymbolOptions {
|
|
/// The options `d` keeps for symbol `name`.
|
|
fn of(d: &Document, name: &str) -> Self {
|
|
Self {
|
|
size: stored_natural_size(d, name),
|
|
registration: d.symbol_registration(name),
|
|
export_type: d.symbol_export_type(name),
|
|
nine_slice: d.symbol_nine_slice(name),
|
|
}
|
|
}
|
|
|
|
/// Make them the options of `d`'s symbol `name`.
|
|
fn give(&self, d: &mut Document, name: &str) {
|
|
set_natural_size(d, name, self.size);
|
|
d.set_symbol_registration(name, self.registration);
|
|
d.set_symbol_export_type(name, self.export_type);
|
|
d.set_symbol_nine_slice(name, self.nine_slice);
|
|
}
|
|
}
|
|
|
|
/// What pasting did to the document's resources (part of the paste result).
|
|
#[derive(Debug, Default)]
|
|
pub(crate) struct Imported {
|
|
relink: Relink,
|
|
/// Resources added to the document (renamed ones included).
|
|
pub added: usize,
|
|
/// Swatch conflicts resolved by merging.
|
|
pub merged: usize,
|
|
/// `{kind, from, to}` for every resource the pasted objects now know under another name: added
|
|
/// renamed, or a copy added renamed by an earlier paste.
|
|
pub renamed: Vec<Value>,
|
|
}
|
|
|
|
impl Imported {
|
|
fn placed(&mut self, r: Res, from: &str, p: &Placed) {
|
|
if matches!(p, Placed::Added | Placed::Renamed(_)) {
|
|
self.added += 1;
|
|
}
|
|
if let Placed::Reused(to) | Placed::Renamed(to) = p {
|
|
self.renamed.push(json!({"kind": r.id(), "from": from, "to": to}));
|
|
self.relink.renames.entry(r).or_default().insert(from.to_string(), to.clone());
|
|
}
|
|
}
|
|
/// Point a pasted copy at the document's resources.
|
|
pub(crate) fn apply(&self, n: &mut Node) {
|
|
self.relink.node(n);
|
|
}
|
|
}
|
|
|
|
impl Clipboard {
|
|
/// `nodes` (from `src`) and the resources of `src` they use.
|
|
pub(crate) fn gather(src: &Document, nodes: Vec<Node>) -> Self {
|
|
let library = OnceCell::new();
|
|
let used = Used::of(&Defs::of(src, &library), &nodes);
|
|
let pick = |r: Res, list: &[TextStyleDef]| list.iter().filter(|s| used.has(r, &s.name)).cloned().collect::<Vec<_>>();
|
|
// Read when a brush was used.
|
|
let brushes = match (used.names.get(&Res::Brush), library.get()) {
|
|
(Some(names), Some(library)) => library.iter().filter(|b| names.contains(&b.name)).cloned().collect(),
|
|
_ => vec![],
|
|
};
|
|
Self {
|
|
images: src.images.iter().filter(|(k, _)| used.has(Res::Image, k)).map(|(k, b)| (k.clone(), b.clone())).collect(),
|
|
symbols: src.symbols.iter().filter(|s| used.has(Res::Symbol, &s.name)).cloned().collect(),
|
|
symbol_options: src
|
|
.symbols
|
|
.iter()
|
|
.filter(|s| used.has(Res::Symbol, &s.name))
|
|
.map(|s| (s.name.clone(), SymbolOptions::of(src, &s.name)))
|
|
.collect(),
|
|
patterns: src.patterns.iter().filter(|p| used.has(Res::Pattern, &p.name)).cloned().collect(),
|
|
swatches: src.swatches_iter().filter(|w| used.carries(w)).cloned().collect(),
|
|
graphic_styles: src.graphic_styles.iter().filter(|g| g.id != 0 && used.styles.contains(&g.id)).cloned().collect(),
|
|
char_styles: pick(Res::CharStyle, &src.char_styles),
|
|
para_styles: pick(Res::ParaStyle, &src.para_styles),
|
|
brushes,
|
|
nodes,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
/// The resources the objects use now (Paste without Formatting leaves text styles behind).
|
|
pub(super) fn used(&self) -> Used {
|
|
let defs = Defs {
|
|
swatches: self.swatches.iter().collect(),
|
|
symbols: &self.symbols,
|
|
patterns: &self.patterns,
|
|
styles: &self.graphic_styles,
|
|
char_styles: &self.char_styles,
|
|
para_styles: &self.para_styles,
|
|
brushes: Brushes::Ready(&self.brushes),
|
|
index: DefIndex::default(),
|
|
};
|
|
Used::of(&defs, &self.nodes)
|
|
}
|
|
|
|
/// The used global and spot colours whose name `d` gives another one: (pasted, document).
|
|
pub(super) fn conflicts<'a, 'd>(&'a self, d: &'d Document, used: &Used) -> Vec<(&'a Swatch, &'d Swatch)> {
|
|
self.swatches
|
|
.iter()
|
|
.filter(|w| is_base(w) && used.has(Res::Swatch, &w.name))
|
|
.filter_map(|w| d.swatch(&w.name).filter(|t| is_base(t) && (t.paint != w.paint || t.spot != w.spot)).map(|t| (w, t)))
|
|
.collect()
|
|
}
|
|
|
|
/// Add the resources the objects use to `d` (inside the paste's undo step) and work out how
|
|
/// pasted copies relink to them.
|
|
pub(crate) fn import_into(&self, d: &mut Document, choices: &SwatchChoices, same_doc: bool) -> Imported {
|
|
let used = self.used();
|
|
let mut out = Imported::default();
|
|
self.import_images(d, &used, same_doc, &mut out);
|
|
self.import_swatches(d, &used, choices, same_doc, &mut out);
|
|
// Brushes whose art shows no symbols or patterns, before the symbols and patterns whose art
|
|
// they may brush; the others once those are in.
|
|
let mut added_brushes: HashSet<String> = self.import_brushes(d, &used, same_doc, false, &mut out).into_iter().collect();
|
|
// Character and paragraph styles (their colours relinked first).
|
|
for (r, mine, theirs) in [(Res::CharStyle, &self.char_styles, &mut d.char_styles), (Res::ParaStyle, &self.para_styles, &mut d.para_styles)] {
|
|
for def in mine.iter().filter(|s| used.has(r, &s.name)) {
|
|
let mut s = def.clone();
|
|
out.relink.attrs(&mut s.attrs);
|
|
let p = place(theirs, &s, same_doc, PartialEq::eq);
|
|
out.placed(r, &def.name, &p);
|
|
}
|
|
}
|
|
// Symbols and patterns, compared once relinked (symbols with their options too). Those
|
|
// added are relinked at the end, when every new name and graphic style id is known.
|
|
let mut added_symbols = HashSet::new();
|
|
for def in self.symbols.iter().filter(|s| used.has(Res::Symbol, &s.name)) {
|
|
let options = self.symbol_options.get(&def.name);
|
|
let theirs: HashMap<String, SymbolOptions> = match options {
|
|
Some(_) => {
|
|
d.symbols.iter().filter(|s| copy_of(&def.name, &s.name)).map(|s| (s.name.clone(), SymbolOptions::of(d, &s.name))).collect()
|
|
}
|
|
None => HashMap::new(),
|
|
};
|
|
let r = &out.relink;
|
|
let p = place(&mut d.symbols, def, same_doc, |a, b| {
|
|
options.is_none_or(|o| theirs.get(&a.name) == Some(o)) && same_art(r, std::slice::from_ref(&a.art), std::slice::from_ref(&b.art))
|
|
});
|
|
if let Some(name) = p.new_name(&def.name) {
|
|
if let Some(o) = options {
|
|
o.give(d, &name);
|
|
}
|
|
added_symbols.insert(name);
|
|
}
|
|
out.placed(Res::Symbol, &def.name, &p);
|
|
}
|
|
let mut added_patterns = HashSet::new();
|
|
for def in self.patterns.iter().filter(|p| used.has(Res::Pattern, &p.name)) {
|
|
// Tile options alike, and the art once relinked.
|
|
let bare = |p: &PatternDef| PatternDef { name: String::new(), art: vec![], ..p.clone() };
|
|
let same = |n: &str| d.pattern(n).is_some_and(|x| bare(x) == bare(def) && same_art(&out.relink, &x.art, &def.art));
|
|
let p = existing(&def.name, d.patterns.iter().map(|p| p.name.as_str()), same_doc, same).unwrap_or_else(|| {
|
|
// Its swatch comes along under the same name.
|
|
let name = unique_name(&def.name, |n| d.pattern(n).is_some() || d.swatch_name_taken(n));
|
|
add_pattern(d, PatternDef { name: name.clone(), ..def.clone() });
|
|
Placed::added(&def.name, name)
|
|
});
|
|
added_patterns.extend(p.new_name(&def.name));
|
|
out.placed(Res::Pattern, &def.name, &p);
|
|
}
|
|
added_brushes.extend(self.import_brushes(d, &used, same_doc, true, &mut out));
|
|
self.import_graphic_styles(d, &used, same_doc, &mut out);
|
|
for s in d.symbols.iter_mut().filter(|s| added_symbols.contains(&s.name)) {
|
|
out.relink.node(Arc::make_mut(&mut s.art));
|
|
}
|
|
for p in d.patterns.iter_mut().filter(|p| added_patterns.contains(&p.name)) {
|
|
for a in &mut p.art {
|
|
out.relink.node(Arc::make_mut(a));
|
|
}
|
|
}
|
|
if !added_brushes.is_empty() {
|
|
let mut lib = vectorcraft_brush::library(d);
|
|
for b in lib.iter_mut().filter(|b| added_brushes.contains(&b.name)) {
|
|
b.art_mut().into_iter().for_each(|a| out.relink.node(a));
|
|
}
|
|
vectorcraft_brush::store(d, &lib);
|
|
}
|
|
out
|
|
}
|
|
|
|
/// Image blobs: a different blob under a key the document uses gets a new key.
|
|
fn import_images(&self, d: &mut Document, used: &Used, same_doc: bool, out: &mut Imported) {
|
|
for (key, blob) in self.images.iter().filter(|(k, _)| used.has(Res::Image, k)) {
|
|
let p = existing(key, d.images.keys().map(String::as_str), same_doc, |k| d.images.get(k) == Some(blob)).unwrap_or_else(|| {
|
|
let k = unique_name(key, |n| d.images.contains_key(n));
|
|
d.images.insert(k.clone(), blob.clone());
|
|
Placed::added(key, k)
|
|
});
|
|
out.placed(Res::Image, key, &p);
|
|
}
|
|
}
|
|
|
|
/// The brushes whose art shows symbols or patterns (`late`), or the others: each one the
|
|
/// document has (its art compared once relinked) is used, the others are added. → the names of
|
|
/// those added, whose art is relinked at the end.
|
|
fn import_brushes(&self, d: &mut Document, used: &Used, same_doc: bool, late: bool, out: &mut Imported) -> Vec<String> {
|
|
let brushes: Vec<&Brush> = self.brushes.iter().filter(|b| used.has(Res::Brush, &b.name) && shows_definitions(b) == late).collect();
|
|
if brushes.is_empty() {
|
|
return vec![];
|
|
}
|
|
let mut lib = vectorcraft_brush::library(d);
|
|
let mut added = vec![];
|
|
for b in brushes {
|
|
let r = &out.relink;
|
|
let p = place(&mut lib, b, same_doc, |ours, theirs| same_brush(r, ours, theirs));
|
|
added.extend(p.new_name(&b.name));
|
|
out.placed(Res::Brush, &b.name, &p);
|
|
}
|
|
if !added.is_empty() {
|
|
vectorcraft_brush::store(d, &lib);
|
|
}
|
|
added
|
|
}
|
|
|
|
/// Global and spot colours (a name conflict merges or adds as `choices` say), then the tint
|
|
/// swatches of the tints used and the gradient swatches.
|
|
fn import_swatches(&self, d: &mut Document, used: &Used, choices: &SwatchChoices, same_doc: bool, out: &mut Imported) {
|
|
let conflicts: BTreeSet<String> =
|
|
if same_doc { BTreeSet::new() } else { self.conflicts(d, used).into_iter().map(|(w, _)| w.name.clone()).collect() };
|
|
let bases: Vec<&Swatch> = self.swatches.iter().filter(|w| is_base(w) && used.has(Res::Swatch, &w.name)).collect();
|
|
for w in &bases {
|
|
let p = if conflicts.contains(&w.name) && choices.of(&w.name) == SwatchChoice::Merge {
|
|
out.merged += 1;
|
|
Placed::Existing
|
|
} else {
|
|
add_swatch(d, w, same_doc)
|
|
};
|
|
out.placed(Res::Swatch, &w.name, &p);
|
|
}
|
|
// Linked colours show the document's swatch of their link.
|
|
for w in &bases {
|
|
let name = out.relink.renamed(Res::Swatch, &w.name);
|
|
if let Some(c) = d.global_color(&name) {
|
|
out.relink.colors.insert(name, c);
|
|
}
|
|
}
|
|
// Then the tint and gradient swatches, linked like the objects.
|
|
for def in self.swatches.iter().filter(|w| !is_base(w) && used.carries(w)) {
|
|
let mut w = def.clone();
|
|
out.relink.paint(&mut w.paint);
|
|
let p = add_swatch(d, &w, same_doc);
|
|
out.placed(Res::Swatch, &def.name, &p);
|
|
}
|
|
}
|
|
|
|
/// Graphic styles: in the source document by id, elsewhere by name and look.
|
|
fn import_graphic_styles(&self, d: &mut Document, used: &Used, same_doc: bool, out: &mut Imported) {
|
|
for g in self.graphic_styles.iter().filter(|g| used.styles.contains(&g.id)) {
|
|
let mut new = g.clone();
|
|
out.relink.appearance(&mut new.appearance);
|
|
let found = match d.graphic_styles.iter().position(|x| same_doc && x.id == g.id) {
|
|
Some(i) => Some(i),
|
|
None => {
|
|
let same = |n: &str| d.graphic_style(n).is_some_and(|x| x.same_look(&new));
|
|
let p = existing(&g.name, d.graphic_styles.iter().map(|x| x.name.as_str()), same_doc, same);
|
|
// Objects follow the style's id, not its name.
|
|
p.and_then(|p| d.graphic_style_index(if let Placed::Reused(n) = &p { n } else { &g.name }))
|
|
}
|
|
};
|
|
let i = match found {
|
|
Some(i) => i,
|
|
None => {
|
|
let name = unique_name(&g.name, |n| d.graphic_style(n).is_some());
|
|
out.placed(Res::GraphicStyle, &g.name, &Placed::added(&g.name, name.clone()));
|
|
let id = d.next_graphic_style_id();
|
|
d.graphic_styles.push(GraphicStyle { name, id, ..new });
|
|
d.graphic_styles.len() - 1
|
|
}
|
|
};
|
|
out.relink.styles.insert(g.id, d.graphic_style_id(i));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Swatch `w` in `d` ([`existing`]), else added under a free name (swatches and colour groups
|
|
/// share one namespace).
|
|
fn add_swatch(d: &mut Document, w: &Swatch, same_doc: bool) -> Placed {
|
|
let same = |n: &str| d.swatch(n).is_some_and(|t| *t == Swatch { name: n.to_string(), ..w.clone() });
|
|
existing(&w.name, d.swatches_iter().map(|t| t.name.as_str()), same_doc, same).unwrap_or_else(|| {
|
|
let name = d.free_swatch_name(&w.name);
|
|
d.swatches.push(Swatch { name: name.clone(), ..w.clone() });
|
|
Placed::added(&w.name, name)
|
|
})
|
|
}
|