mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
swatch.library.save replied with the number of solid colors and gradients it was asked to save, including the gradients the format leaves out. For a new document saved as .ase, count was 50 and the file held 46 colors. The .gpl and .ase writers keep solid colors only, and the CSS writer leaves freeform gradients out. count is now the number of swatches written. The new PaletteFormat::holds returns whether a file of a format keeps a swatch with a given paint, and swatch.library.save counts the swatches for which it returns true. The reply has the same fields as before. Tests: after a freeform gradient is added to a new document's swatches, the test checks that count has these values: 51 for .vcswatches, 46 for .gpl and .ase, and 50 for CSS. Each file holds that many swatches. Docs: docs/mcp.md.
449 lines
27 KiB
Rust
449 lines
27 KiB
Rust
//! Swatch libraries: listing, reading, adding to the document (deduped, one undo step, applied)
|
|
//! and Default Swatches.
|
|
|
|
use serde_json::{Value, json};
|
|
use vectorcraft_color::{Color, Paint};
|
|
|
|
use super::*;
|
|
|
|
fn session() -> Session {
|
|
let mut s = Session::new();
|
|
s.execute("file.new", &json!({"width": 100, "height": 100})).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()
|
|
}
|
|
|
|
#[test]
|
|
fn list_and_get_every_builtin_library() {
|
|
let mut s = Session::new();
|
|
let libs = run(&mut s, "swatch.library.list", json!({}));
|
|
let libs = libs["libraries"].as_array().unwrap();
|
|
let ids: Vec<&str> = libs.iter().map(|l| l["id"].as_str().unwrap()).collect();
|
|
for id in [
|
|
"web-safe-216",
|
|
"grays-neutrals",
|
|
"earth-tones",
|
|
"skin-tone-ramps",
|
|
"pastels",
|
|
"brights",
|
|
"metallic-gradients",
|
|
"perceptual-scales",
|
|
"harmony-sets",
|
|
] {
|
|
assert!(ids.contains(&id), "{id} missing from {ids:?}");
|
|
}
|
|
let web = libs.iter().find(|l| l["id"] == "web-safe-216").unwrap();
|
|
assert_eq!((web["name"].as_str(), web["count"].as_u64()), (Some("Web Safe 216"), Some(216)));
|
|
// By id or by name in any case; groups list their swatches.
|
|
let g = run(&mut s, "swatch.library.get", json!({"library": "earth tones"}));
|
|
assert_eq!(g["id"], "earth-tones");
|
|
let clay = g["groups"].as_array().unwrap().iter().find(|x| x["name"] == "Clay").unwrap();
|
|
assert_eq!(clay["swatches"].as_array().unwrap().len(), 6);
|
|
assert!(g["swatches"].as_array().unwrap().iter().all(|w| w["kind"] == "color" && w["group"].is_string()));
|
|
let m = run(&mut s, "swatch.library.get", json!({"library": "metallic-gradients"}));
|
|
assert!(m["swatches"].as_array().unwrap().iter().all(|w| w["kind"] == "gradient"));
|
|
assert!(s.execute("swatch.library.get", &json!({"library": "nope"})).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn add_dedupes_as_one_undo_step() {
|
|
let mut s = session();
|
|
let before = undo_len(&s);
|
|
let r = run(&mut s, "swatch.library.add", json!({"library": "earth-tones", "names": ["Clay", "Ochre 2", "Clay 3"]}));
|
|
// The Clay group comes whole (its third swatch once); Ochre 2 comes ungrouped.
|
|
let added: Vec<&str> = r["added"].as_array().unwrap().iter().map(|v| v.as_str().unwrap()).collect();
|
|
assert_eq!(added.len(), 7, "{added:?}");
|
|
assert_eq!(undo_len(&s), before + 1, "one undo step");
|
|
let d = doc(&s);
|
|
assert_eq!(d.swatch_groups.iter().find(|g| g.name == "Clay").map(|g| g.swatches.len()), Some(6));
|
|
assert!(d.swatches.iter().any(|w| w.name == "Ochre 2"));
|
|
// Adding again finds them all and changes nothing (no undo step).
|
|
let r = run(&mut s, "swatch.library.add", json!({"library": "earth-tones", "names": ["Clay", "Ochre 2"]}));
|
|
assert_eq!((r["added"].as_array().unwrap().len(), r["existing"].as_array().unwrap().len()), (0, 7));
|
|
assert_eq!(undo_len(&s), before + 1);
|
|
// A name taken by a different colour gets a number.
|
|
run(&mut s, "swatch.new", json!({"name": "Ochre 3", "color": "#123456"}));
|
|
let r = run(&mut s, "swatch.library.add", json!({"library": "earth-tones", "names": ["Ochre 3"]}));
|
|
assert_eq!(r["added"], json!(["Ochre 3 2"]));
|
|
// Undo removes the whole first add.
|
|
for _ in 0..3 {
|
|
run(&mut s, "edit.undo", json!({}));
|
|
}
|
|
assert!(doc(&s).swatch_groups.iter().all(|g| g.name != "Clay") && doc(&s).swatch("Ochre 2").is_none());
|
|
assert!(s.execute("swatch.library.add", &json!({"library": "earth-tones", "names": ["Nope"]})).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn add_with_apply_paints_the_selection_in_the_same_undo_step() {
|
|
let mut s = session();
|
|
let id = run(&mut s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 50, "height": 50}))["id"].clone();
|
|
let before = undo_len(&s);
|
|
let r = run(&mut s, "swatch.library.add", json!({"library": "web-safe-216", "names": ["#FF6600"], "apply": "fill"}));
|
|
assert_eq!((r["added"].clone(), r["applied"].clone()), (json!(["#FF6600"]), json!("#FF6600")));
|
|
let fill = doc(&s).node(vectorcraft_doc::NodeId(id.as_u64().unwrap())).unwrap().appearance.fill_paint();
|
|
assert_eq!(fill.color().map(|c| c.to_hex()), Some("#ff6600".into()));
|
|
assert_eq!(undo_len(&s), before + 1, "added and applied as one step");
|
|
run(&mut s, "edit.undo", json!({}));
|
|
assert!(doc(&s).swatch("#FF6600").is_none());
|
|
// A gradient swatch applied to the stroke; an existing swatch is applied without an add.
|
|
run(&mut s, "swatch.library.add", json!({"library": "metallic-gradients", "names": ["Gold"], "apply": "stroke"}));
|
|
let n = doc(&s).node(vectorcraft_doc::NodeId(id.as_u64().unwrap())).unwrap();
|
|
assert!(matches!(n.appearance.stroke_paint(), Paint::Gradient(_)));
|
|
let r = run(&mut s, "swatch.library.add", json!({"library": "metallic-gradients", "names": ["Gold"], "apply": "fill"}));
|
|
assert_eq!((r["added"].clone(), r["applied"].clone()), (json!([]), json!("Gold")));
|
|
}
|
|
|
|
#[test]
|
|
fn reset_defaults_restores_missing_swatches_or_replaces_them() {
|
|
let mut s = session();
|
|
let defaults: Vec<String> = doc(&s).swatches_iter().map(|w| w.name.clone()).collect();
|
|
run(&mut s, "swatch.delete", json!({"names": ["Red", "Grays"]}));
|
|
run(&mut s, "swatch.new", json!({"name": "Mine", "color": "#abcdef"}));
|
|
let r = run(&mut s, "swatch.resetDefaults", json!({}));
|
|
assert!(r["added"].as_array().unwrap().iter().any(|n| n == "Red"));
|
|
let d = doc(&s);
|
|
assert!(d.swatch("Mine").is_some(), "the user's swatches stay");
|
|
assert_eq!(d.swatch_groups.iter().find(|g| g.name == "Grays").map(|g| g.swatches.len()), Some(9));
|
|
assert!(defaults.iter().all(|n| d.swatch(n).is_some()));
|
|
// Replace: exactly the defaults; art linked to a removed global swatch keeps its colour.
|
|
run(&mut s, "swatch.edit", json!({"name": "Mine", "global": true}));
|
|
let id = run(&mut s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 5, "height": 5}))["id"].clone();
|
|
run(&mut s, "select.set", json!({"ids": [id]}));
|
|
run(&mut s, "paint.setFill", json!({"swatch": "Mine"}));
|
|
assert_eq!(s.paint.fill, Paint::Solid { color: Color::from_hex("#abcdef").unwrap(), swatch: Some("Mine".into()), tint: 1.0 });
|
|
run(&mut s, "swatch.resetDefaults", json!({"replace": true}));
|
|
let d = doc(&s);
|
|
assert_eq!(d.swatches_iter().map(|w| w.name.clone()).collect::<Vec<_>>(), defaults);
|
|
let fill = d.node(vectorcraft_doc::NodeId(id.as_u64().unwrap())).unwrap().appearance.fill_paint();
|
|
assert_eq!(fill, Paint::solid(Color::from_hex("#abcdef").unwrap()));
|
|
assert_eq!(s.paint.fill, Paint::solid(Color::from_hex("#abcdef").unwrap()), "the default fill is unlinked too");
|
|
}
|
|
|
|
/// A fresh scratch folder for one test.
|
|
fn temp_dir(name: &str) -> std::path::PathBuf {
|
|
let d = std::env::temp_dir().join(format!("vc-swatchlib-{name}-{}", std::process::id()));
|
|
let _ = std::fs::remove_dir_all(&d);
|
|
d
|
|
}
|
|
|
|
#[test]
|
|
fn save_and_load_round_trip_keeps_cmyk_spot_and_groups() {
|
|
let mut s = session();
|
|
run(&mut s, "swatch.new", json!({"name": "Ink", "color": {"c": 1.0, "m": 0.5, "y": 0.0, "k": 0.2}, "spot": true}));
|
|
run(&mut s, "swatch.newGroup", json!({"name": "Brand", "colors": ["#123456", "#abcdef"]}));
|
|
let r = run(&mut s, "swatch.library.save", json!({"names": ["Ink", "Brand", "Sunset"], "name": "Brand Kit"}));
|
|
assert_eq!((r["format"].as_str(), r["count"].as_u64()), (Some("vcswatches"), Some(4)));
|
|
let data = r["data"].as_str().unwrap().to_string();
|
|
let r = run(&mut s, "swatch.library.load", json!({"data": data, "name": "kit.vcswatches"}));
|
|
assert_eq!((r["name"].as_str(), r["count"].as_u64()), (Some("Brand Kit"), Some(4)));
|
|
let id = r["library"].as_str().unwrap().to_string();
|
|
let (_, lib) = cmd::swatchlib::library(&s, &id).unwrap();
|
|
let ink = lib.swatch("Ink").unwrap();
|
|
assert!(ink.spot && ink.global);
|
|
assert_eq!(ink.paint.color(), Some(Color::cmyk(1.0, 0.5, 0.0, 0.2)));
|
|
assert_eq!(lib.groups[0].name, "Brand");
|
|
assert_eq!(lib.groups[0].swatches.len(), 2);
|
|
assert!(matches!(lib.swatch("Sunset").unwrap().paint, Paint::Gradient(_)));
|
|
// Listed as loaded; adding from it works like any library.
|
|
let list = run(&mut s, "swatch.library.list", json!({}));
|
|
assert!(list["libraries"].as_array().unwrap().iter().any(|l| l["id"] == id.as_str() && l["category"] == "loaded"));
|
|
assert!(s.execute("swatch.library.save", &json!({"names": ["Nope"]})).is_err());
|
|
assert!(s.execute("swatch.library.save", &json!({"user": true})).is_err(), "no user folder in a headless session");
|
|
}
|
|
|
|
#[test]
|
|
fn libraries_saved_in_the_user_folder_are_user_defined() {
|
|
let dir = temp_dir("user");
|
|
let mut s = session();
|
|
s.swatch_libraries.set_user_dir(Some(dir.to_string_lossy().to_string()));
|
|
let r = run(&mut s, "swatch.library.save", json!({"user": true, "name": "My: Greys", "format": "gpl", "names": ["Grays"]}));
|
|
assert_eq!(r["library"], "user/My- Greys.gpl");
|
|
let path = r["path"].as_str().unwrap().to_string();
|
|
assert!(std::fs::read_to_string(&path).unwrap().starts_with("GIMP Palette\nName: My: Greys\n"));
|
|
let list = run(&mut s, "swatch.library.list", 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("My: Greys"), Some(9)));
|
|
// Opening a file of the user folder gives its User Defined library.
|
|
let r = run(&mut s, "swatch.library.load", json!({ "path": path }));
|
|
assert_eq!(r["library"], "user/My- Greys.gpl");
|
|
// CSS by extension.
|
|
let css = dir.join("out.css").to_string_lossy().to_string();
|
|
run(&mut s, "swatch.library.save", json!({ "path": css }));
|
|
assert!(std::fs::read_to_string(&css).unwrap().contains(" --white: #ffffff;"));
|
|
// A name with nothing a file name can keep is saved as Library.vcswatches, and User Defined lists it.
|
|
let r = run(&mut s, "swatch.library.save", json!({"user": true, "name": "..."}));
|
|
assert_eq!(r["library"], "user/Library.vcswatches");
|
|
assert_eq!(cmd::swatchlib::library(&s, "user/Library.vcswatches").map(|(info, _)| info.name), Some("...".into()));
|
|
let _ = std::fs::remove_dir_all(dir);
|
|
}
|
|
|
|
#[test]
|
|
fn another_documents_swatches_load_as_a_library() {
|
|
let mut s = session();
|
|
run(&mut s, "swatch.new", json!({"name": "Signal", "color": "#ff3300"}));
|
|
let b64 = run(&mut s, "document.serialize", json!({}))["dataBase64"].as_str().unwrap().to_string();
|
|
let r = run(&mut s, "swatch.library.load", json!({"dataBase64": b64, "name": "Poster.vectorcraft"}));
|
|
assert_eq!(r["name"], "Poster");
|
|
let (_, lib) = cmd::swatchlib::library(&s, r["library"].as_str().unwrap()).unwrap();
|
|
assert!(lib.swatch("Signal").is_some() && lib.swatch("[None]").is_none());
|
|
assert!(s.execute("swatch.library.load", &json!({"data": "hello", "name": "x.txt"})).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn swatch_exchange_files_load_add_and_list_as_user_libraries() {
|
|
let bytes = vectorcraft_testkit::ase::sample();
|
|
let mut s = session();
|
|
let r = run(&mut s, "swatch.library.load", json!({"dataBase64": vectorcraft_format::base64_encode(&bytes), "name": "Brand.ase"}));
|
|
assert_eq!((r["library"].as_str(), r["name"].as_str(), r["count"].as_u64()), (Some("loaded/Brand.ase"), Some("Brand"), Some(4)));
|
|
let g = run(&mut s, "swatch.library.get", json!({"library": r["library"]}));
|
|
let swatches = g["swatches"].as_array().unwrap();
|
|
let kinds: Vec<(&str, bool, bool)> =
|
|
swatches.iter().map(|w| (w["name"].as_str().unwrap(), w["global"].as_bool().unwrap(), w["spot"].as_bool().unwrap())).collect();
|
|
assert_eq!(kinds, [("Sky", true, false), ("Ink", true, true), ("Mist", false, false), ("Clay", false, false)]);
|
|
assert_eq!(swatches[3]["color"], json!({"model": "lab", "l": 50.0, "a": 20.0, "b": -30.0}));
|
|
// Added to the document, the spot color stays a spot color and the group stays a color group.
|
|
run(&mut s, "swatch.library.add", json!({"library": r["library"]}));
|
|
let d = doc(&s);
|
|
assert!(d.swatch("Ink").is_some_and(|w| w.spot && w.global));
|
|
assert_eq!(d.swatch_groups.iter().find(|g| g.name == "Neutrals").map(|g| g.swatches.len()), Some(2));
|
|
// A damaged file is an error from the swatch exchange reader, not from the document loader.
|
|
let cut = vectorcraft_format::base64_encode(&bytes[..bytes.len() - 1]);
|
|
let e = s.execute("swatch.library.load", &json!({"dataBase64": cut, "name": "Cut.ase"})).unwrap_err().to_string();
|
|
assert!(e.contains("cut short"), "{e}");
|
|
// `.ase` files in the user library folder are User Defined libraries.
|
|
let dir = temp_dir("ase");
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
std::fs::write(dir.join("Brand.ase"), &bytes).unwrap();
|
|
s.swatch_libraries.set_user_dir(Some(dir.to_string_lossy().to_string()));
|
|
let list = run(&mut s, "swatch.library.list", json!({}));
|
|
let user: Vec<(&str, u64)> = list["libraries"]
|
|
.as_array()
|
|
.unwrap()
|
|
.iter()
|
|
.filter(|l| l["category"] == "user")
|
|
.map(|l| (l["id"].as_str().unwrap(), l["count"].as_u64().unwrap()))
|
|
.collect();
|
|
assert_eq!(user, [("user/Brand.ase", 4)]);
|
|
let _ = std::fs::remove_dir_all(dir);
|
|
}
|
|
|
|
#[test]
|
|
fn swatch_exchange_files_save_with_models_kinds_and_groups() {
|
|
let mut s = session();
|
|
run(&mut s, "swatch.new", json!({"name": "Ink", "color": {"c": 1.0, "m": 0.5, "y": 0.0, "k": 0.2}, "spot": true}));
|
|
run(&mut s, "swatch.new", json!({"name": "Clay", "color": {"l": 50, "a": 20, "b": -30}}));
|
|
run(&mut s, "swatch.newGroup", json!({"name": "Brand", "colors": ["#123456", "#abcdef"]}));
|
|
let r = run(&mut s, "swatch.library.save", json!({"format": "ase", "names": ["Ink", "Clay", "Brand", "Grays", "Sunset"], "name": "Brand Kit"}));
|
|
assert_eq!(r["format"], "ase");
|
|
assert!(r.get("data").is_none(), "a binary file comes back as dataBase64");
|
|
let b64 = r["dataBase64"].as_str().unwrap().to_string();
|
|
assert!(vectorcraft_format::base64_decode(&b64).unwrap().starts_with(b"ASEF"));
|
|
let r = run(&mut s, "swatch.library.load", json!({"dataBase64": b64, "name": "Brand Kit.ase"}));
|
|
let (_, lib) = cmd::swatchlib::library(&s, r["library"].as_str().unwrap()).unwrap();
|
|
assert_eq!(lib.name, "Brand Kit", "named after the file");
|
|
let ink = lib.swatch("Ink").unwrap();
|
|
assert!(ink.spot && ink.global);
|
|
assert_eq!(ink.paint.color(), Some(Color::cmyk(1.0, 0.5, 0.0, 0.2)));
|
|
assert_eq!(lib.swatch("Clay").unwrap().paint.color(), Some(Color::lab(50.0, 20.0, -30.0)));
|
|
assert!(lib.swatch("Sunset").is_none(), "a gradient is left out");
|
|
let groups: Vec<(&str, usize)> = lib.groups.iter().map(|g| (g.name.as_str(), g.swatches.len())).collect();
|
|
assert_eq!(groups, [("Grays", 9), ("Brand", 2)], "in document order");
|
|
// Gray colors come back within float rounding.
|
|
let grays = &doc(&s).swatch_groups.iter().find(|g| g.name == "Grays").unwrap().swatches;
|
|
for (w, want) in lib.groups[0].swatches.iter().zip(grays) {
|
|
let (Some(Color::Gray { k }), Some(Color::Gray { k: want_k })) = (w.paint.color(), want.paint.color()) else {
|
|
panic!("{} isn't gray", w.name)
|
|
};
|
|
assert!(w.name == want.name && (k - want_k).abs() < 1e-4, "{}: {k} for {want_k}", w.name);
|
|
}
|
|
// Into the user library folder, and to a path ending in .ase.
|
|
let dir = temp_dir("ase-save");
|
|
s.swatch_libraries.set_user_dir(Some(dir.to_string_lossy().to_string()));
|
|
let r = run(&mut s, "swatch.library.save", json!({"format": "ase", "user": true, "name": "Brand Kit"}));
|
|
assert_eq!(r["library"], "user/Brand Kit.ase");
|
|
assert!(std::fs::read(r["path"].as_str().unwrap()).unwrap().starts_with(b"ASEF"));
|
|
assert!(cmd::swatchlib::library(&s, "user/Brand Kit.ase").is_some(), "the user folder lists it");
|
|
let path = dir.join("out.ase").to_string_lossy().to_string();
|
|
assert_eq!(run(&mut s, "swatch.library.save", json!({ "path": path }))["format"], "ase");
|
|
assert!(std::fs::read(&path).unwrap().starts_with(b"ASEF"));
|
|
let e = s.execute("swatch.library.save", &json!({"format": "aco"})).unwrap_err().to_string();
|
|
assert!(e.contains("vcswatches, gpl, ase or css"), "{e}");
|
|
let _ = std::fs::remove_dir_all(dir);
|
|
}
|
|
|
|
#[test]
|
|
fn save_counts_the_swatches_the_file_holds() {
|
|
let mut s = session();
|
|
// A new document has 46 solid colors and 4 linear or radial gradients besides None. Mesh is a freeform gradient.
|
|
run(&mut s, "swatch.new", json!({"name": "Mesh", "gradient": {"kind": "freeform"}}));
|
|
let mut counts = vec![];
|
|
for format in ["vcswatches", "gpl", "ase", "css"] {
|
|
let r = run(&mut s, "swatch.library.save", json!({ "format": format }));
|
|
// The libraries load back with that many swatches, and the CSS file has one property for each.
|
|
let held = if format == "css" {
|
|
r["data"].as_str().unwrap().lines().filter(|l| l.starts_with(" --")).count() as u64
|
|
} else {
|
|
let load = json!({"data": r["data"], "dataBase64": r["dataBase64"], "name": format!("Saved.{format}")});
|
|
run(&mut s, "swatch.library.load", load)["count"].as_u64().unwrap()
|
|
};
|
|
counts.push((format, r["count"].as_u64().unwrap(), held));
|
|
}
|
|
assert_eq!(counts, [("vcswatches", 51, 51), ("gpl", 46, 46), ("ase", 46, 46), ("css", 50, 50)]);
|
|
}
|
|
|
|
#[test]
|
|
fn loaded_libraries_copy_into_the_user_folder_without_replacing_files() {
|
|
use vectorcraft_testkit::ase::{Block, ase, sample};
|
|
let dir = temp_dir("copy");
|
|
let (user, elsewhere) = (dir.join("user"), dir.join("elsewhere"));
|
|
std::fs::create_dir_all(&user).unwrap();
|
|
std::fs::create_dir_all(&elsewhere).unwrap();
|
|
let source = elsewhere.join("Brand.ase");
|
|
std::fs::write(&source, sample()).unwrap();
|
|
let source = source.to_string_lossy().to_string();
|
|
// Another library whose file name differs only in case.
|
|
let other = ase(&[Block::Color("Only", b"RGB ", &[1.0, 0.0, 0.0], 2)]);
|
|
std::fs::write(user.join("brand.ase"), &other).unwrap();
|
|
let mut s = session();
|
|
s.swatch_libraries.set_user_dir(Some(user.to_string_lossy().to_string()));
|
|
let loaded = run(&mut s, "swatch.library.load", json!({ "path": source }))["library"].clone();
|
|
assert_eq!(loaded, format!("loaded/{source}"));
|
|
let r = run(&mut s, "swatch.library.copyToUser", json!({ "library": loaded }));
|
|
// A swatch exchange library is named after its file.
|
|
assert_eq!(
|
|
(r["library"].as_str(), r["name"].as_str(), r["count"].as_u64(), r["copied"].as_bool()),
|
|
(Some("user/Brand 2.ase"), Some("Brand 2"), Some(4), Some(true))
|
|
);
|
|
assert_eq!(r["path"], user.join("Brand 2.ase").to_string_lossy().to_string());
|
|
assert_eq!(std::fs::read(user.join("Brand 2.ase")).unwrap(), sample(), "the file as it is");
|
|
assert_eq!(std::fs::read(user.join("brand.ase")).unwrap(), other, "never replaced");
|
|
let listed = |s: &mut Session| -> Vec<String> {
|
|
let list = run(s, "swatch.library.list", json!({}));
|
|
let libs = list["libraries"].as_array().unwrap().iter().filter(|l| matches!(l["category"].as_str(), Some("user" | "loaded")));
|
|
libs.map(|l| l["id"].as_str().unwrap().to_string()).collect()
|
|
};
|
|
assert_eq!(listed(&mut s), ["user/Brand 2.ase", "user/brand.ase"], "listed as User Defined only");
|
|
// The loaded id finds the copy, so choices made with it (Limit to Library) keep their colors.
|
|
let found = cmd::swatchlib::library(&s, loaded.as_str().unwrap()).map(|(info, _)| info.id);
|
|
assert_eq!(found.as_deref(), Some("user/Brand 2.ase"));
|
|
assert!(cmd::swatchlib::limit_palette(&s, loaded.as_str().unwrap()).is_some());
|
|
// The same file again opens the copy, which is listed once; copying it writes nothing.
|
|
let again = run(&mut s, "swatch.library.load", json!({ "path": source }))["library"].clone();
|
|
assert_eq!(again, "user/Brand 2.ase");
|
|
assert_eq!(listed(&mut s), ["user/Brand 2.ase", "user/brand.ase"]);
|
|
let r = run(&mut s, "swatch.library.copyToUser", json!({ "library": again }));
|
|
assert_eq!((r["library"].as_str(), r["copied"].as_bool()), (Some("user/Brand 2.ase"), Some(false)));
|
|
assert_eq!(std::fs::read_dir(&user).unwrap().count(), 2);
|
|
// A document's swatches and a library loaded from data are written as .vcswatches.
|
|
run(&mut s, "swatch.new", json!({"name": "Signal", "color": "#ff3300"}));
|
|
let b64 = run(&mut s, "document.serialize", json!({}))["dataBase64"].as_str().unwrap().to_string();
|
|
let poster = elsewhere.join("Poster.vectorcraft");
|
|
std::fs::write(&poster, vectorcraft_format::base64_decode(&b64).unwrap()).unwrap();
|
|
let loaded = run(&mut s, "swatch.library.load", json!({ "path": poster.to_string_lossy() }))["library"].clone();
|
|
let r = run(&mut s, "swatch.library.copyToUser", json!({ "library": loaded }));
|
|
assert_eq!((r["library"].as_str(), r["copied"].as_bool()), (Some("user/Poster.vcswatches"), Some(true)));
|
|
assert!(cmd::swatchlib::library(&s, "user/Poster.vcswatches").unwrap().1.swatch("Signal").is_some());
|
|
// Copied again, the document's swatches are written as the same bytes: the copy there is used.
|
|
let loaded = run(&mut s, "swatch.library.load", json!({ "path": poster.to_string_lossy() }))["library"].clone();
|
|
let r = run(&mut s, "swatch.library.copyToUser", json!({ "library": loaded }));
|
|
assert_eq!((r["library"].as_str(), r["copied"].as_bool()), (Some("user/Poster.vcswatches"), Some(false)));
|
|
let kit = vectorcraft_format::base64_encode(&sample());
|
|
let loaded = run(&mut s, "swatch.library.load", json!({"dataBase64": kit, "name": "Kit.ase"}))["library"].clone();
|
|
assert_eq!(run(&mut s, "swatch.library.copyToUser", json!({ "library": loaded }))["library"], "user/Kit.vcswatches");
|
|
// A library without a name is copied as Library.
|
|
let loaded = run(&mut s, "swatch.library.load", json!({"data": "GIMP Palette\nName:\n0 0 0 Black\n"}))["library"].clone();
|
|
let r = run(&mut s, "swatch.library.copyToUser", json!({ "library": loaded }));
|
|
assert_eq!((r["library"].as_str(), r["name"].as_str(), r["copied"].as_bool()), (Some("user/Library.vcswatches"), Some("Library"), Some(true)));
|
|
// When the source no longer reads as a library, the copy fails and no new file is left in the folder.
|
|
let junk = elsewhere.join("Junk.ase");
|
|
std::fs::write(&junk, ase(&[Block::Color("Junk", b"RGB ", &[0.0, 1.0, 0.0], 2)])).unwrap();
|
|
let loaded = run(&mut s, "swatch.library.load", json!({ "path": junk.to_string_lossy() }))["library"].clone();
|
|
std::fs::write(&junk, b"not a library").unwrap();
|
|
let files = std::fs::read_dir(&user).unwrap().count();
|
|
let e = s.execute("swatch.library.copyToUser", &json!({ "library": loaded })).unwrap_err().to_string();
|
|
assert!(e.contains("the copy `user/Junk.ase` can't be read as a library"), "{e}");
|
|
assert_eq!(std::fs::read_dir(&user).unwrap().count(), files);
|
|
// A User Defined library comes back as it is; a built-in one is an error.
|
|
let r = run(&mut s, "swatch.library.copyToUser", json!({"library": "user/brand.ase"}));
|
|
assert_eq!((r["library"].as_str(), r["copied"].as_bool()), (Some("user/brand.ase"), Some(false)));
|
|
assert!(s.execute("swatch.library.copyToUser", &json!({"library": "pastels"})).unwrap_err().to_string().contains("built in"));
|
|
assert!(s.execute("swatch.library.copyToUser", &json!({})).is_err());
|
|
// Without a user library folder (the web, headless sessions) there is nowhere to copy to, and
|
|
// the command is off.
|
|
let mut headless = session();
|
|
let info = find_command("swatch.library.copyToUser").unwrap().info(&headless);
|
|
assert!(!info.enabled && info.disabled_reason.as_deref() == Some("no user library folder here"), "{info:?}");
|
|
assert!(find_command("swatch.library.copyToUser").unwrap().info(&s).enabled);
|
|
let loaded = run(&mut headless, "swatch.library.load", json!({"dataBase64": kit, "name": "Kit.ase"}))["library"].clone();
|
|
let e = headless.execute("swatch.library.copyToUser", &json!({ "library": loaded })).unwrap_err().to_string();
|
|
assert!(e.contains("no user library folder"), "{e}");
|
|
let _ = std::fs::remove_dir_all(dir);
|
|
}
|
|
|
|
#[test]
|
|
fn a_library_group_stays_one_group_when_a_swatch_has_its_name() {
|
|
use vectorcraft_testkit::ase::{Block, ase};
|
|
let mut s = session();
|
|
assert!(doc(&s).swatch("Blue").is_some(), "a new document has a swatch named Blue");
|
|
let bytes = ase(&[
|
|
Block::GroupStart("Blue"),
|
|
Block::Color("Deep", b"RGB ", &[0.0, 0.0, 0.4], 2),
|
|
Block::Color("Pale", b"RGB ", &[0.6, 0.7, 1.0], 2),
|
|
Block::GroupEnd,
|
|
Block::GroupStart("Blue 2"),
|
|
Block::Color("Frost", b"RGB ", &[0.9, 0.95, 1.0], 2),
|
|
Block::GroupEnd,
|
|
]);
|
|
let r = run(&mut s, "swatch.library.load", json!({"dataBase64": vectorcraft_format::base64_encode(&bytes), "name": "Blues.ase"}));
|
|
run(&mut s, "swatch.library.add", json!({"library": r["library"]}));
|
|
let groups: Vec<(&str, Vec<&str>)> =
|
|
doc(&s).swatch_groups.iter().map(|g| (g.name.as_str(), g.swatches.iter().map(|w| w.name.as_str()).collect())).collect();
|
|
assert!(groups.contains(&("Blue 2", vec!["Deep", "Pale"])), "{groups:?}");
|
|
assert!(groups.contains(&("Blue 2 2", vec!["Frost"])), "the library's own Blue 2 stays apart: {groups:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn text_libraries_sent_as_bytes_load_when_they_are_not_valid_utf8() {
|
|
let mut s = session();
|
|
let latin1 = vectorcraft_format::base64_encode(b"GIMP Palette\n237 28 36 Rouge fonc\xe9\n");
|
|
let r = run(&mut s, "swatch.library.load", json!({"dataBase64": latin1, "name": "Rouge.gpl"}));
|
|
assert_eq!(r["count"], 1);
|
|
}
|
|
|
|
#[test]
|
|
fn gradient_libraries_are_listed_and_gradient_swatches_rename_and_take_new_gradients() {
|
|
let mut s = session();
|
|
let list = run(&mut s, "swatch.library.list", json!({}));
|
|
let grads: Vec<&Value> = list["libraries"].as_array().unwrap().iter().filter(|l| l["category"] == "gradients").collect();
|
|
assert!(grads.len() >= 5 && grads.iter().any(|l| l["id"] == "sky-gradients"));
|
|
run(&mut s, "swatch.library.add", json!({"library": "sky-gradients", "names": ["Dawn"]}));
|
|
// Rename keeps the gradient.
|
|
let before = doc(&s).swatch("Dawn").unwrap().paint.clone();
|
|
run(&mut s, "swatch.edit", json!({"name": "Dawn", "newName": "Early"}));
|
|
assert_eq!(doc(&s).swatch("Early").unwrap().paint, before);
|
|
// An edited gradient replaces it, unplaced and unlinked; one undo step.
|
|
let undo = undo_len(&s);
|
|
let g =
|
|
json!({"gradient": {"kind": "radial", "stops": [{"offset": 0, "color": "#ff0000"}, {"offset": 1, "color": "#0000ff"}], "swatch": "Early"}});
|
|
run(&mut s, "swatch.edit", json!({"name": "Early", "paint": g}));
|
|
let Paint::Gradient(gp) = &doc(&s).swatch("Early").unwrap().paint else { panic!("a gradient") };
|
|
assert_eq!((gp.gradient.kind, gp.gradient.stops.len(), gp.swatch.clone(), gp.geom), (vectorcraft_color::GradientKind::Radial, 2, None, None));
|
|
assert_eq!(undo_len(&s), undo + 1);
|
|
// Kinds don't mix; a colour replaces a colour.
|
|
assert!(s.execute("swatch.edit", &json!({"name": "Early", "paint": {"color": "#00ff00"}})).is_err());
|
|
run(&mut s, "swatch.edit", json!({"name": "Red", "paint": {"color": "#00ff00"}}));
|
|
assert_eq!(doc(&s).swatch("Red").unwrap().paint.color().map(|c| c.to_hex()), Some("#00ff00".into()));
|
|
}
|