mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-30 12:47:13 -06:00
Window › Symbol Libraries and the Symbols panel's Symbol Libraries Menu open each symbol library in a library panel of its own, on the shared library panel code the swatch and graphic style libraries use (https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/create-or-import-symbol-libraries.html, .../symbols-panel-options.html, .../create-and-place-symbols.html). - Basic Shapes: one original library generated in code (circle, squares, triangle, diamond, hexagon, star, burst, arrow); no other app's libraries. - A click adds a library symbol to the document's Symbols as one undo step, with its Symbol Options and the resources it uses (nested symbols, patterns, swatches, brushes, graphic styles, images), and makes it the current symbol; one already there isn't added again, a clash of names numbers the library's. Shift/Cmd-clicks select for Add to Symbols. Dragging a library symbol onto the canvas adds it and places an instance there, in the same step. - Save Symbols… (library menu, Window menu) and Save Symbol Library… (Symbols panel menu) write the document's symbols as a .vectorcraft document holding only them, into the user library folder (User Defined, window.userSymbolLibrary1-10) or a file; Other Library… reads the symbols of any document VectorCraft opens, and a file it can't read is an error. Commands: symbol.libraries, symbol.library, symbol.addFromLibrary (place, x, y), symbol.saveLibrary, symbol.loadLibrary; UI: window.symbolLibrary (.other), ui.saveSymbolLibrary (dialog saveSymbolLibrary). i18n rows in every complete catalog; docs updated.
228 lines
12 KiB
Rust
228 lines
12 KiB
Rust
//! Symbol libraries: the built-in library, saving a document's symbols as a library (User
|
|
//! Defined, or a file), reopening it and adding its symbols to another document (nested symbols,
|
|
//! patterns and Symbol Options along, one undo step, deduped, renamed on a clash, placed), other
|
|
//! documents' symbols, and files that can't be read.
|
|
|
|
use serde_json::{Value, json};
|
|
use vectorcraft_doc::{NodeId, NodeKind, SymbolRegistration};
|
|
|
|
use super::*;
|
|
|
|
fn session() -> Session {
|
|
let mut s = Session::new();
|
|
s.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
|
|
s
|
|
}
|
|
|
|
fn run(s: &mut Session, id: &str, p: Value) -> Value {
|
|
s.execute(id, &p).unwrap_or_else(|e| panic!("{id} {p}: {e}"))
|
|
}
|
|
|
|
fn doc(s: &Session) -> &vectorcraft_doc::Document {
|
|
&s.doc().unwrap().doc
|
|
}
|
|
|
|
fn undo_len(s: &Session) -> usize {
|
|
s.doc().unwrap().history.undo.len()
|
|
}
|
|
|
|
fn symbols(s: &Session) -> Vec<String> {
|
|
doc(s).symbols.iter().map(|x| x.name.clone()).collect()
|
|
}
|
|
|
|
fn strs(v: &Value) -> Vec<&str> {
|
|
v.as_array().unwrap().iter().map(|n| n.as_str().unwrap()).collect()
|
|
}
|
|
|
|
/// A folder of its own under the temp folder, emptied.
|
|
fn folder(name: &str) -> std::path::PathBuf {
|
|
let dir = std::env::temp_dir().join(format!("vc-symbollib-{name}-{}", std::process::id()));
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
dir
|
|
}
|
|
|
|
/// A document with "Star" (a red star), "Badge" (a circle with a Star instance in it, registration
|
|
/// top left) and "Dots" (a square painted with pattern "Dots").
|
|
fn kit() -> Session {
|
|
let mut s = session();
|
|
run(&mut s, "shape.star", json!({"cx": 50, "cy": 50, "radius1": 20, "radius2": 8}));
|
|
run(&mut s, "paint.setFill", json!({"color": "#ff0000"}));
|
|
run(&mut s, "symbol.new", json!({"name": "Star"}));
|
|
let star = s.doc().unwrap().selection.objects.clone();
|
|
let ring = run(&mut s, "shape.ellipse", json!({"x": 20, "y": 20, "width": 60, "height": 60}))["id"].as_u64().unwrap();
|
|
let mut ids: Vec<u64> = star.iter().map(|i| i.0).collect();
|
|
ids.push(ring);
|
|
run(&mut s, "symbol.new", json!({"name": "Badge", "ids": ids, "registration": 0}));
|
|
s.edit("Pattern", |d, _| {
|
|
let dot = vectorcraft_doc::Node::path(
|
|
d.alloc_id(),
|
|
vectorcraft_geom::shapes::ellipse(vectorcraft_geom::Rect::new(0.0, 0.0, 4.0, 4.0)),
|
|
vectorcraft_doc::Appearance::default_art(),
|
|
);
|
|
let def = vectorcraft_doc::PatternDef {
|
|
tile: vectorcraft_geom::Rect::new(0.0, 0.0, 10.0, 10.0),
|
|
..vectorcraft_doc::PatternDef::new("Dots", vec![std::sync::Arc::new(dot)])
|
|
};
|
|
d.patterns.push(def);
|
|
d.swatches.push(vectorcraft_color::Swatch {
|
|
name: "Dots".into(),
|
|
paint: vectorcraft_color::Paint::Pattern { pattern: "Dots".into(), xf: Default::default() },
|
|
global: false,
|
|
spot: false,
|
|
});
|
|
Ok(())
|
|
})
|
|
.unwrap();
|
|
run(&mut s, "shape.rectangle", json!({"x": 120, "y": 120, "width": 40, "height": 30}));
|
|
run(&mut s, "paint.setFill", json!({"swatch": "Dots"}));
|
|
run(&mut s, "symbol.new", json!({"name": "Dots"}));
|
|
s
|
|
}
|
|
|
|
#[test]
|
|
fn the_builtin_library_is_original_art_generated_in_code() {
|
|
let mut s = Session::new();
|
|
let list = run(&mut s, "symbol.libraries", json!({}));
|
|
let libs = list["libraries"].as_array().unwrap();
|
|
assert_eq!(libs.len(), 1, "{libs:?}");
|
|
assert_eq!((libs[0]["id"].as_str(), libs[0]["category"].as_str()), (Some("basic-shapes"), Some("builtIn")));
|
|
let lib = run(&mut s, "symbol.library", json!({"library": "basic shapes"}));
|
|
let names: Vec<&str> = lib["symbols"].as_array().unwrap().iter().map(|x| x["name"].as_str().unwrap()).collect();
|
|
assert_eq!(names, ["Circle", "Square", "Rounded Square", "Triangle", "Diamond", "Hexagon", "Star", "Burst", "Arrow"]);
|
|
assert_eq!(libs[0]["count"].as_u64(), Some(names.len() as u64));
|
|
let arrow = lib["symbols"].as_array().unwrap().iter().find(|x| x["name"] == "Arrow").unwrap();
|
|
assert_eq!(arrow["size"], json!([48.0, 32.0]));
|
|
assert!(s.execute("symbol.library", &json!({"library": "nope"})).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn a_saved_library_reopens_and_its_symbols_join_another_document_in_one_step() {
|
|
let dir = folder("user");
|
|
let mut s = kit();
|
|
s.symbol_libraries.set_user_dir(Some(dir.to_string_lossy().to_string()));
|
|
// Saving the Badge brings the Star it nests, not Dots.
|
|
let r = run(&mut s, "symbol.saveLibrary", json!({"user": true, "name": "Signs", "names": ["Badge"]}));
|
|
assert_eq!((r["library"].as_str(), r["count"].as_u64()), (Some("user/Signs.vectorcraft"), Some(2)));
|
|
let path = r["path"].as_str().unwrap().to_string();
|
|
let list = run(&mut s, "symbol.libraries", json!({}));
|
|
let user: Vec<&Value> = list["libraries"].as_array().unwrap().iter().filter(|l| l["category"] == "user").collect();
|
|
assert_eq!(user.len(), 1);
|
|
assert_eq!((user[0]["name"].as_str(), user[0]["count"].as_u64()), (Some("Signs"), Some(2)));
|
|
// The file is a document: its symbols, no art.
|
|
let bytes = std::fs::read(&path).unwrap();
|
|
let saved = vectorcraft_format::load(&bytes).unwrap();
|
|
assert_eq!(saved.symbols.iter().map(|x| x.name.as_str()).collect::<Vec<_>>(), ["Star", "Badge"]);
|
|
assert!(saved.layers.iter().all(|l| l.children().is_none_or(|c| c.is_empty())));
|
|
// Loading the file gives that User Defined library.
|
|
let r = run(&mut s, "symbol.loadLibrary", json!({ "path": path }));
|
|
assert_eq!((r["library"].as_str(), r["name"].as_str()), (Some("user/Signs.vectorcraft"), Some("Signs")));
|
|
|
|
// Into a new document: Badge and the Star in it, with Badge's registration, as one step.
|
|
run(&mut s, "file.new", json!({"width": 100, "height": 100}));
|
|
let before = undo_len(&s);
|
|
let r = run(&mut s, "symbol.addFromLibrary", json!({"library": "Signs", "name": "Badge"}));
|
|
assert_eq!((strs(&r["names"]), strs(&r["added"]), strs(&r["existing"])), (vec!["Badge"], vec!["Badge"], vec![]));
|
|
assert_eq!(undo_len(&s), before + 1);
|
|
assert_eq!(s.doc().unwrap().history.undo.last().unwrap().label, "Add to Symbols");
|
|
assert_eq!(symbols(&s), ["Star", "Badge"]);
|
|
assert_eq!(doc(&s).symbol_registration("Badge"), SymbolRegistration(0));
|
|
let (_, lib) = cmd::symbollib::library(&s, "Signs").unwrap();
|
|
assert_eq!(doc(&s).symbols, lib.doc.symbols);
|
|
assert_eq!(doc(&s).unknown.get("symbolSizes").and_then(|m| m.get("Badge")), lib.doc.unknown.get("symbolSizes").and_then(|m| m.get("Badge")));
|
|
assert_eq!(run(&mut s, "symbol.list", json!({}))["current"], "Badge");
|
|
// Undo takes it all back.
|
|
run(&mut s, "edit.undo", json!({}));
|
|
assert!(symbols(&s).is_empty());
|
|
run(&mut s, "edit.redo", json!({}));
|
|
// Again: already there, so no step.
|
|
let before = undo_len(&s);
|
|
let r = run(&mut s, "symbol.addFromLibrary", json!({"library": "user/Signs.vectorcraft", "names": ["Badge", "Star", "Badge"]}));
|
|
assert_eq!((strs(&r["names"]), strs(&r["existing"])), (vec!["Badge", "Star"], vec!["Badge", "Star"]));
|
|
assert_eq!((undo_len(&s), symbols(&s).len()), (before, 2));
|
|
assert_eq!(run(&mut s, "symbol.list", json!({}))["current"], "Badge");
|
|
let _ = std::fs::remove_dir_all(dir);
|
|
}
|
|
|
|
#[test]
|
|
fn a_library_file_brings_patterns_renames_clashes_and_places_an_instance() {
|
|
let dir = folder("file");
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
let file = dir.join("Kit.vectorcraft");
|
|
let mut s = kit();
|
|
let r = run(&mut s, "symbol.saveLibrary", json!({"path": file.to_string_lossy()}));
|
|
assert_eq!(r["count"].as_u64(), Some(3));
|
|
let lib = run(&mut s, "symbol.loadLibrary", json!({"path": file.to_string_lossy()}));
|
|
assert_eq!((lib["name"].as_str(), lib["count"].as_u64()), (Some("Kit"), Some(3)));
|
|
let id = lib["library"].as_str().unwrap().to_string();
|
|
assert!(id.starts_with("loaded/"));
|
|
|
|
// A document with another "Star" of its own.
|
|
run(&mut s, "file.new", json!({"width": 100, "height": 100}));
|
|
run(&mut s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 10, "height": 10}));
|
|
run(&mut s, "symbol.new", json!({"name": "Star"}));
|
|
let mine = doc(&s).symbols[0].clone();
|
|
let before = undo_len(&s);
|
|
let r = run(&mut s, "symbol.addFromLibrary", json!({"library": id, "names": ["Star", "Dots"], "place": true, "x": 30, "y": 40}));
|
|
assert_eq!(undo_len(&s), before + 1, "adding and placing are one step");
|
|
assert_eq!(strs(&r["names"]), ["Star 2", "Dots"]);
|
|
assert_eq!(doc(&s).symbols[0], mine, "the document's own Star stays");
|
|
// The pattern came along, with its swatch; the placed instance is the first one, selected.
|
|
assert!(doc(&s).pattern("Dots").is_some() && doc(&s).swatch("Dots").is_some());
|
|
let placed = NodeId(r["placed"].as_u64().unwrap());
|
|
assert_eq!(s.doc().unwrap().selection.objects, [placed]);
|
|
let NodeKind::SymbolInstance { symbol, .. } = &doc(&s).node(placed).unwrap().kind else { panic!("not an instance") };
|
|
assert_eq!(symbol, "Star 2");
|
|
let b = doc(&s).node(placed).unwrap().geometric_bounds().unwrap();
|
|
assert!((b.center().x - 30.0).abs() < 0.01 && (b.center().y - 40.0).abs() < 0.01, "{b:?}");
|
|
run(&mut s, "edit.undo", json!({}));
|
|
assert_eq!(symbols(&s), ["Star"]);
|
|
assert!(doc(&s).node(placed).is_none() && doc(&s).pattern("Dots").is_none());
|
|
// Placing a symbol the document has still places (one step), without adding it again.
|
|
run(&mut s, "edit.redo", json!({}));
|
|
let before = undo_len(&s);
|
|
let r = run(&mut s, "symbol.addFromLibrary", json!({"library": id, "name": "Dots", "place": true}));
|
|
assert_eq!((undo_len(&s), strs(&r["existing"])), (before + 1, vec!["Dots"]));
|
|
assert!(r["placed"].is_u64());
|
|
assert!(s.execute("symbol.addFromLibrary", &json!({"library": id, "name": "Nope"})).is_err());
|
|
let _ = std::fs::remove_dir_all(dir);
|
|
}
|
|
|
|
#[test]
|
|
fn other_documents_symbols_load_and_unreadable_files_are_errors() {
|
|
let mut s = kit();
|
|
// Another document's symbols load as a library, without its art.
|
|
let b64 = run(&mut s, "document.serialize", json!({}))["dataBase64"].as_str().unwrap().to_string();
|
|
let r = run(&mut s, "symbol.loadLibrary", json!({"dataBase64": b64, "name": "Poster.vectorcraft"}));
|
|
assert_eq!((r["name"].as_str(), r["count"].as_u64()), (Some("Poster"), Some(3)));
|
|
let (_, lib) = cmd::symbollib::library(&s, r["library"].as_str().unwrap()).unwrap();
|
|
assert!(lib.doc.layers.iter().all(|l| l.children().is_none_or(|c| c.is_empty())));
|
|
// So do an SVG's.
|
|
let svg = r##"<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100" height="100"><defs><symbol id="dot" viewBox="0 0 10 10"><circle cx="5" cy="5" r="5"/></symbol></defs><use xlink:href="#dot" width="10" height="10"/></svg>"##;
|
|
let r = run(&mut s, "symbol.loadLibrary", json!({"data": svg, "name": "Dots.svg"}));
|
|
assert_eq!(r["count"].as_u64(), Some(1), "{r}");
|
|
// Files that aren't documents, missing files and damaged native files are errors.
|
|
assert!(s.execute("symbol.loadLibrary", &json!({"data": "hello", "name": "x.txt"})).is_err());
|
|
assert!(s.execute("symbol.loadLibrary", &json!({"path": "/no/such/Library.vectorcraft"})).is_err());
|
|
let dir = folder("bad");
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
let bad = dir.join("Broken.vectorcraft");
|
|
std::fs::write(&bad, b"{\"format\": \"vectorcraft\", \"doc\": 7").unwrap();
|
|
assert!(s.execute("symbol.loadLibrary", &json!({"path": bad.to_string_lossy()})).is_err());
|
|
// A damaged file in the user folder isn't listed.
|
|
s.symbol_libraries.set_user_dir(Some(dir.to_string_lossy().to_string()));
|
|
let list = run(&mut s, "symbol.libraries", json!({}));
|
|
assert!(list["libraries"].as_array().unwrap().iter().all(|l| l["category"] != "user"));
|
|
s.symbol_libraries.set_user_dir(None);
|
|
let _ = std::fs::remove_dir_all(dir);
|
|
// Nothing to save, or no such symbol, is an error; a headless session has no user folder.
|
|
assert!(s.execute("symbol.saveLibrary", &json!({"names": ["Nope"]})).is_err());
|
|
assert!(s.execute("symbol.saveLibrary", &json!({"user": true})).is_err());
|
|
run(&mut s, "file.new", json!({"width": 100, "height": 100}));
|
|
assert!(s.execute("symbol.saveLibrary", &json!({})).is_err());
|
|
// Without a path the library comes back as the file's bytes.
|
|
let mut s = kit();
|
|
let r = run(&mut s, "symbol.saveLibrary", json!({"names": ["Star"]}));
|
|
let bytes = vectorcraft_format::base64_decode(r["dataBase64"].as_str().unwrap()).unwrap();
|
|
assert_eq!(vectorcraft_format::load(&bytes).unwrap().symbols.len(), 1);
|
|
}
|