mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 17:40:14 -06:00
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:
@@ -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 })
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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() };
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user