Merge pull request #5300 from storytold/fix/sec-resource-name-index

Brush and symbol libraries: look definitions up by name
This commit is contained in:
Brandon Thomas
2026-11-03 00:19:22 -05:00
committed by GitHub
4 changed files with 113 additions and 22 deletions
+8 -3
View File
@@ -107,10 +107,14 @@ impl BrushLibrary {
/// `name`: the brushes and every resource their art uses.
pub fn from_document(src: &Document, names: &[String], name: String) -> std::result::Result<Self, String> {
let all = brush::library(src);
if let Some(n) = names.iter().find(|n| !all.iter().any(|b| b.name == **n)) {
// Names looked up in sets: scanning the lists for each brush made a library of N brushes
// cost N² comparisons.
let have: HashSet<&str> = all.iter().map(|b| b.name.as_str()).collect();
if let Some(n) = names.iter().find(|n| !have.contains(n.as_str())) {
return Err(format!("no brush `{n}`"));
}
let picked = all.iter().filter(|b| names.is_empty() || names.contains(&b.name));
let wanted: HashSet<&str> = names.iter().map(String::as_str).collect();
let picked = all.iter().filter(|b| names.is_empty() || wanted.contains(b.name.as_str()));
let clip = Clipboard::gather(src, picked.map(|b| stroke(&b.name)).collect());
let mut doc = clip.to_document();
for l in &mut doc.layers {
@@ -120,7 +124,8 @@ impl BrushLibrary {
}
// The brushes picked and those their art paints with, in the panel order of the document
// they came from.
let brushes: Vec<Brush> = all.into_iter().filter(|b| clip.brushes.iter().any(|c| c.name == b.name)).collect();
let kept: HashSet<&str> = clip.brushes.iter().map(|c| c.name.as_str()).collect();
let brushes: Vec<Brush> = all.iter().filter(|b| kept.contains(b.name.as_str())).cloned().collect();
brush::store(&mut doc, &brushes);
doc.title = name.clone();
Ok(Self { name, brushes, doc })
+11 -2
View File
@@ -161,10 +161,19 @@ impl Clipboard {
d.para_styles = self.para_styles.clone().into();
if !self.brushes.is_empty() {
let mut lib = vectorcraft_brush::library(&d);
// Each brush's place in the library by name: finding it by scanning the growing list
// made a library of N brushes cost N² comparisons.
let mut at: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
for (i, b) in lib.iter().enumerate() {
at.entry(b.name.clone()).or_insert(i);
}
for b in &self.brushes {
match lib.iter_mut().find(|x| x.name == b.name) {
match at.get(&b.name).and_then(|&i| lib.get_mut(i)) {
Some(x) => *x = b.clone(),
None => lib.push(b.clone()),
None => {
at.insert(b.name.clone(), lib.len());
lib.push(b.clone());
}
}
}
vectorcraft_brush::store(&mut d, &lib);
+56 -12
View File
@@ -14,7 +14,7 @@
//! uses its resources as they are now.
use std::cell::OnceCell;
use std::collections::{BTreeMap, BTreeSet};
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::sync::Arc;
use serde_json::{Map, Value, json};
@@ -149,6 +149,39 @@ struct Defs<'a> {
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
@@ -178,6 +211,7 @@ impl<'a> Defs<'a> {
char_styles: &d.char_styles,
para_styles: &d.para_styles,
brushes: Brushes::Lazy(d, brushes),
index: DefIndex::default(),
}
}
}
@@ -219,8 +253,16 @@ impl Used {
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) {
todo.extend(defs.styles.iter().filter(|g| g.id != 0 && g.id == id).map(|g| Source::Appearance(&g.appearance)));
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;
}
@@ -237,15 +279,16 @@ impl Used {
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(defs.swatches.iter().filter(|w| w.name == name).map(|w| Source::Paint(&w.paint))),
Res::Symbol => todo.extend(defs.symbols.iter().filter(|s| s.name == name).map(|s| Source::Node(&s.art))),
Res::Pattern => todo.extend(defs.patterns.iter().filter(|p| p.name == name).flat_map(|p| p.art.iter().map(|a| Source::Node(a)))),
Res::CharStyle => todo.extend(defs.char_styles.iter().filter(|s| s.name == name).map(|s| Source::Attrs(&s.attrs))),
Res::ParaStyle => todo.extend(defs.para_styles.iter().filter(|s| s.name == name).map(|s| Source::Attrs(&s.attrs))),
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(defs.brushes.get().iter().filter(|b| b.name == name).flat_map(|b| b.art().into_iter().map(Source::Node))),
Res::Brush => todo.extend(named(&ix.brushes, defs.brushes.get(), |b| &b.name, name).flat_map(|b| b.art().into_iter().map(Source::Node))),
_ => {}
}
}
@@ -649,6 +692,7 @@ impl Clipboard {
char_styles: &self.char_styles,
para_styles: &self.para_styles,
brushes: Brushes::Ready(&self.brushes),
index: DefIndex::default(),
};
Used::of(&defs, &self.nodes)
}
@@ -671,7 +715,7 @@ impl Clipboard {
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 = self.import_brushes(d, &used, same_doc, false, &mut out);
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)) {
@@ -683,14 +727,14 @@ impl Clipboard {
}
// Symbols and patterns, compared once relinked. Those added are relinked at the end, when
// every new name and graphic style id is known.
let mut added_symbols = vec![];
let mut added_symbols = HashSet::new();
for def in self.symbols.iter().filter(|s| used.has(Res::Symbol, &s.name)) {
let r = &out.relink;
let p = place(&mut d.symbols, def, same_doc, |a, b| same_art(r, std::slice::from_ref(&a.art), std::slice::from_ref(&b.art)));
added_symbols.extend(p.new_name(&def.name));
out.placed(Res::Symbol, &def.name, &p);
}
let mut added_patterns = vec![];
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() };
+38 -5
View File
@@ -7,7 +7,7 @@
//! document VectorCraft opens. `symbol.addFromLibrary` copies symbols into the document;
//! `symbol.saveLibrary` writes the document's.
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, LazyLock};
use serde_json::{Value, json};
@@ -104,10 +104,17 @@ impl SymbolLibrary {
/// `name`: the symbols, the symbols they nest and every other resource they use, with their
/// Symbol Options.
pub fn from_document(src: &Document, names: &[String], name: String) -> std::result::Result<Self, String> {
if let Some(n) = names.iter().find(|n| !src.symbols.iter().any(|s| s.name == **n)) {
// Each name's first place in the panel, looked up rather than scanned for: scanning the
// symbols for each one made a library of N symbols cost N² comparisons.
let mut order: HashMap<&str, usize> = HashMap::new();
for (i, s) in src.symbols.iter().enumerate() {
order.entry(s.name.as_str()).or_insert(i);
}
if let Some(n) = names.iter().find(|n| !order.contains_key(n.as_str())) {
return Err(format!("no symbol `{n}`"));
}
let picked = src.symbols.iter().filter(|s| names.is_empty() || names.contains(&s.name));
let wanted: HashSet<&str> = names.iter().map(String::as_str).collect();
let picked = src.symbols.iter().filter(|s| names.is_empty() || wanted.contains(s.name.as_str()));
let clip = Clipboard::gather(src, picked.map(|s| instance(&s.name)).collect());
let mut doc = clip.to_document();
for l in &mut doc.layers {
@@ -116,8 +123,7 @@ impl SymbolLibrary {
}
}
// The panel order of the document they came from.
let order = |n: &str| src.symbols.iter().position(|s| s.name == n);
doc.symbols.sort_by_key(|s| order(&s.name));
doc.symbols.sort_by_key(|s| order.get(s.name.as_str()).copied());
for s in &doc.symbols.clone() {
copy_options(src, &s.name, &mut doc, &s.name);
}
@@ -344,3 +350,30 @@ fn load(s: &mut Session, p: &Value) -> Result<Value> {
})?;
Ok(json!({"library": info.id, "name": info.name, "count": lib.len()}))
}
#[cfg(test)]
mod tests {
use super::*;
/// A library made from thousands of symbols holds them all, once each, in the panel order of
/// the document they came from, a name given twice kept at its first place.
#[test]
fn many_symbols_keep_their_panel_order() {
let mut src = Document::new(100.0, 100.0);
let n = 4000;
// Names out of alphabetical order, so the order kept is the panel's.
src.symbols = (0..n)
.map(|i| Symbol { name: format!("s{}", (i * 7919) % n), art: Arc::new(Node::group(NodeId(i as u64 + 1), vec![])), dynamic: false })
.collect();
let lib = SymbolLibrary::from_document(&src, &[], "Many".into()).unwrap();
let names = |d: &Document| d.symbols.iter().map(|s| s.name.clone()).collect::<Vec<_>>();
assert_eq!(names(&lib.doc), names(&src));
let picked = ["s3".to_string(), "s1".to_string()];
let lib = SymbolLibrary::from_document(&src, &picked, "Two".into()).unwrap();
let order = |name: &str| src.symbols.iter().position(|s| s.name == name);
let mut want = vec!["s3", "s1"];
want.sort_by_key(|n| order(n));
assert_eq!(names(&lib.doc), want);
assert!(SymbolLibrary::from_document(&src, &["nope".to_string()], "None".into()).is_err());
}
}