Files
vectorcraft/crates/engine/src/cmd/place/adopt.rs
T
arhammusheer 5beac4f122 #1244: smaller objects: Node 352 -> 152 bytes, NodeKind 200 -> 80, and a cheaper undo estimate
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
2026-10-12 11:02:32 +07:00

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);
}
}
}