mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-27 03:06:57 -06:00
Rarely set fields move into one optional boxed NodeExtras, and large NodeKind payloads (images, live shapes, blends, envelopes) are boxed. Every object, and every undo step that copies it, shrinks: a 50k-path move keeps ~24.5 MB per step instead of ~40 MB. Files stay byte-identical (Node keeps its flat JSON through a borrowed Serialize and a from-wire Deserialize, no flatten), and loading is slightly faster. Undo and redo reuse their step's size estimate instead of walking the document again (8.3 -> 5.6 ms for a 50k-path move). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmMmc1J8XeNLxhRihWv9cc # Conflicts: # crates/engine/src/cmd/graph.rs # crates/svg/src/export.rs
233 lines
8.2 KiB
Rust
233 lines
8.2 KiB
Rust
//! Resources placed art brings along: the images, symbols, pattern swatches and colour swatches it
|
|
//! uses (also through symbol and pattern art and through tint swatches) join the document. A name
|
|
//! the document already uses for something different gets a free variant (`Star 2`,
|
|
//! `pdf-image-1 2`…) and the art's references follow; a colour linked to a swatch the document
|
|
//! defines differently keeps its colour, unlinked. Text styles are left as they are: the art carries
|
|
//! its own formatting.
|
|
|
|
use std::collections::BTreeMap;
|
|
use std::sync::Arc;
|
|
|
|
use vectorcraft_color::Paint;
|
|
use vectorcraft_doc::{AppearanceItem, Document, Node, NodeKind};
|
|
|
|
use super::unique_name;
|
|
|
|
/// Visit `n` and every descendant (children and opacity-mask art) mutably.
|
|
pub(super) fn each_mut(n: &mut Node, f: &mut impl FnMut(&mut Node)) {
|
|
f(n);
|
|
if let Some(m) = n.extras_if_any().and_then(|e| e.mask.as_mut()) {
|
|
each_mut(Arc::make_mut(&mut m.art), f);
|
|
}
|
|
if let Some(ch) = n.children_mut() {
|
|
for c in ch {
|
|
each_mut(Arc::make_mut(c), f);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A reference from art to a document resource.
|
|
enum Ref<'a> {
|
|
Image(&'a mut String),
|
|
Symbol(&'a mut String),
|
|
Pattern(&'a mut String),
|
|
/// A colour's (or gradient's) swatch link; renaming it to `None` unlinks it.
|
|
Swatch(&'a mut Option<String>),
|
|
}
|
|
|
|
/// Every resource reference held by paint `p`.
|
|
fn paint_refs(p: &mut Paint, f: &mut impl FnMut(Ref)) {
|
|
match p {
|
|
Paint::Pattern { pattern, .. } => f(Ref::Pattern(pattern)),
|
|
Paint::Gradient(g) if g.swatch.is_some() => f(Ref::Swatch(&mut g.swatch)),
|
|
_ => {}
|
|
}
|
|
p.map_links(&mut |_, link, _| {
|
|
if link.is_some() {
|
|
f(Ref::Swatch(link));
|
|
}
|
|
false
|
|
});
|
|
}
|
|
|
|
/// Every resource reference in `n` and its descendants.
|
|
fn refs(n: &mut Node, f: &mut impl FnMut(Ref)) {
|
|
each_mut(n, &mut |n| {
|
|
for it in &mut n.appearance.items {
|
|
match it {
|
|
AppearanceItem::Fill(fl) => paint_refs(&mut fl.paint, f),
|
|
AppearanceItem::Stroke(st) => paint_refs(&mut st.paint, f),
|
|
}
|
|
}
|
|
match &mut n.kind {
|
|
NodeKind::Text(t) => {
|
|
for r in &mut t.runs {
|
|
paint_refs(&mut r.style.fill, f);
|
|
paint_refs(&mut r.style.stroke, f);
|
|
}
|
|
}
|
|
NodeKind::Image(im) => f(Ref::Image(&mut im.key)),
|
|
NodeKind::PlacedDocument(pl) => f(Ref::Image(&mut pl.key)),
|
|
NodeKind::SymbolInstance { symbol, .. } => f(Ref::Symbol(symbol)),
|
|
_ => {}
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Resource names in first-use order.
|
|
#[derive(Default)]
|
|
struct Used {
|
|
images: Vec<String>,
|
|
symbols: Vec<String>,
|
|
patterns: Vec<String>,
|
|
swatches: Vec<String>,
|
|
}
|
|
|
|
impl Used {
|
|
fn note(&mut self, r: Ref) {
|
|
let (list, name) = match r {
|
|
Ref::Image(k) => (&mut self.images, &*k),
|
|
Ref::Symbol(k) => (&mut self.symbols, &*k),
|
|
Ref::Pattern(k) => (&mut self.patterns, &*k),
|
|
Ref::Swatch(Some(k)) => (&mut self.swatches, &*k),
|
|
Ref::Swatch(None) => return,
|
|
};
|
|
if !list.contains(name) {
|
|
list.push(name.clone());
|
|
}
|
|
}
|
|
}
|
|
|
|
/// What each resource the art uses is called in the document (`None`: a swatch link to drop) and
|
|
/// whether its definition is copied in.
|
|
type Renames = BTreeMap<String, (Option<String>, bool)>;
|
|
|
|
/// Names in `dst` for the `used` definitions of `src` (`def`): the same name when `dst` has none
|
|
/// (copied) or an equal one (shared), else a free variant (`taken`, copied).
|
|
fn names<T: PartialEq>(
|
|
used: &[String],
|
|
def: impl Fn(&str) -> Option<T>,
|
|
existing: impl Fn(&str) -> Option<T>,
|
|
taken: impl Fn(&str) -> bool,
|
|
) -> Renames {
|
|
let mut out = Renames::new();
|
|
for name in used {
|
|
let Some(d) = def(name) else { continue };
|
|
let entry = match existing(name) {
|
|
None => (Some(name.clone()), true),
|
|
Some(e) if e == d => (Some(name.clone()), false),
|
|
Some(_) => (Some(unique_name(name, |c| taken(c) || out.values().any(|(n, _)| n.as_deref() == Some(c)))), true),
|
|
};
|
|
out.insert(name.clone(), entry);
|
|
}
|
|
out
|
|
}
|
|
|
|
/// The renamed resources whose definitions are copied: (name in `src`, name in `dst`).
|
|
fn copies(m: &Renames) -> Vec<(&str, &str)> {
|
|
m.iter().filter(|(_, (_, copy))| *copy).filter_map(|(old, (new, _))| Some((old.as_str(), new.as_deref()?))).collect()
|
|
}
|
|
|
|
/// Bring the resources `nodes` (art from `src`) use into `dst`, renaming on conflict, and point
|
|
/// `nodes` at the names they got.
|
|
pub(crate) fn adopt(dst: &mut Document, src: &Document, nodes: &mut [Node]) {
|
|
let mut used = Used::default();
|
|
for n in nodes.iter_mut() {
|
|
refs(n, &mut |r| used.note(r));
|
|
}
|
|
// Symbol and pattern art and tint swatches can use more resources (the lists grow while they
|
|
// are walked).
|
|
let (mut si, mut pi, mut wi) = (0, 0, 0);
|
|
loop {
|
|
let mut art: Vec<Node> = vec![];
|
|
if let Some(name) = used.symbols.get(si) {
|
|
si += 1;
|
|
art.extend(src.symbols.iter().find(|s| s.name == *name).map(|s| (*s.art).clone()));
|
|
} else if let Some(name) = used.patterns.get(pi) {
|
|
pi += 1;
|
|
art.extend(src.pattern(name).into_iter().flat_map(|p| p.art.iter().map(|a| (**a).clone())));
|
|
} else if let Some(name) = used.swatches.get(wi) {
|
|
wi += 1;
|
|
if let Some(mut paint) = src.swatch(name).map(|w| w.paint.clone()) {
|
|
paint_refs(&mut paint, &mut |r| used.note(r));
|
|
}
|
|
} else {
|
|
break;
|
|
}
|
|
for mut a in art {
|
|
refs(&mut a, &mut |r| used.note(r));
|
|
}
|
|
}
|
|
|
|
let images = names(&used.images, |k| src.images.get(k), |k| dst.images.get(k), |c| dst.images.contains_key(c));
|
|
let symbols = names(
|
|
&used.symbols,
|
|
|n| src.symbols.iter().find(|s| s.name == n),
|
|
|n| dst.symbols.iter().find(|s| s.name == n),
|
|
|c| dst.symbols.iter().any(|s| s.name == c),
|
|
);
|
|
let patterns = names(&used.patterns, |n| src.pattern(n), |n| dst.pattern(n), |c| dst.pattern(c).is_some());
|
|
// A swatch the document defines differently isn't renamed (that would add look-alike swatches):
|
|
// the colours linked to it keep their look, unlinked.
|
|
let swatches: Renames = used
|
|
.swatches
|
|
.iter()
|
|
.filter_map(|n| {
|
|
let w = src.swatch(n)?;
|
|
let entry = match dst.swatch(n) {
|
|
None => (Some(n.clone()), true),
|
|
Some(e) if e == w => (Some(n.clone()), false),
|
|
Some(_) => (None, false),
|
|
};
|
|
Some((n.clone(), entry))
|
|
})
|
|
.collect();
|
|
|
|
let mut rename = |r: Ref| {
|
|
let (map, k) = match r {
|
|
Ref::Image(k) => (&images, k),
|
|
Ref::Symbol(k) => (&symbols, k),
|
|
Ref::Pattern(k) => (&patterns, k),
|
|
Ref::Swatch(link) => {
|
|
if let Some((new, _)) = link.as_deref().and_then(|k| swatches.get(k)) {
|
|
*link = new.clone();
|
|
}
|
|
return;
|
|
}
|
|
};
|
|
if let Some((Some(new), _)) = map.get(k.as_str()) {
|
|
*k = new.clone();
|
|
}
|
|
};
|
|
for n in nodes.iter_mut() {
|
|
refs(n, &mut rename);
|
|
}
|
|
for (old, new) in copies(&images) {
|
|
if let Some(blob) = src.images.get(old) {
|
|
dst.images.insert(new.to_string(), blob.clone());
|
|
}
|
|
}
|
|
for (old, new) in copies(&symbols) {
|
|
if let Some(mut def) = src.symbols.iter().find(|s| s.name == old).cloned() {
|
|
def.name = new.to_string();
|
|
refs(Arc::make_mut(&mut def.art), &mut rename);
|
|
dst.symbols.push(def);
|
|
}
|
|
}
|
|
for (old, new) in copies(&patterns) {
|
|
if let Some(mut def) = src.pattern(old).cloned() {
|
|
def.name = new.to_string();
|
|
for a in &mut def.art {
|
|
refs(Arc::make_mut(a), &mut rename);
|
|
}
|
|
dst.patterns.push(def);
|
|
}
|
|
}
|
|
for (old, _) in copies(&swatches) {
|
|
if let Some(mut w) = src.swatch(old).cloned() {
|
|
paint_refs(&mut w.paint, &mut rename);
|
|
dst.swatches.push(w);
|
|
}
|
|
}
|
|
}
|