diff --git a/crates/engine/src/cmd/brushlib.rs b/crates/engine/src/cmd/brushlib.rs index 04498111c..8f36e16f6 100644 --- a/crates/engine/src/cmd/brushlib.rs +++ b/crates/engine/src/cmd/brushlib.rs @@ -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 { 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 = 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 = 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 }) diff --git a/crates/engine/src/cmd/clipboard.rs b/crates/engine/src/cmd/clipboard.rs index ec2722197..29933309e 100644 --- a/crates/engine/src/cmd/clipboard.rs +++ b/crates/engine/src/cmd/clipboard.rs @@ -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 = 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); diff --git a/crates/engine/src/cmd/clipboard/resources.rs b/crates/engine/src/cmd/clipboard/resources.rs index e6bd96371..6a436ab31 100644 --- a/crates/engine/src/cmd/clipboard/resources.rs +++ b/crates/engine/src/cmd/clipboard/resources.rs @@ -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>; + +/// [`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, + symbols: OnceCell, + patterns: OnceCell, + styles: OnceCell>>, + char_styles: OnceCell, + para_styles: OnceCell, + brushes: OnceCell, +} + +/// The items of `list` named `name`, in order, `index` holding their positions (built from +/// `list` the first time). +fn named<'l, T>(index: &OnceCell, list: &'l [T], name_of: impl Fn(&T) -> &str, name: &str) -> impl Iterator { + 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>) { 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> = 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 = 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() }; diff --git a/crates/engine/src/cmd/symbollib.rs b/crates/engine/src/cmd/symbollib.rs index 49e9f5a61..e5f2ccfe2 100644 --- a/crates/engine/src/cmd/symbollib.rs +++ b/crates/engine/src/cmd/symbollib.rs @@ -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 { - 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,28 @@ fn load(s: &mut Session, p: &Value) -> Result { })?; 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::>(); + 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()); + } +}