mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-30 12:47:13 -06:00
M3.31: Swatches panel menu adds used colours, selects unused, merges, ungroups and sorts by kind
New commands: swatch.addUsedColors {selection?, global?} adds a swatch for
each colour the art uses that no solid swatch has (optionally global, the
art linking to them); swatch.unused lists the swatches nothing uses (links,
colours, gradients and patterns in art, symbols, pattern tiles and graphic
styles count); swatch.merge keeps the first colour and relinks the others'
art; swatch.ungroup and swatch.sortByKind (process, spot, gradients,
patterns). Each edit is one undo step; the panel menu items are enabled.
Document::visit_paints walks every paint of a document.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
fe258915e3
commit
bdf8f704af
@@ -76,3 +76,118 @@ fn move_reorders_groups_and_refuses_bad_moves() {
|
||||
assert_eq!(undo_len(&s), undo, "failed moves leave no undo step");
|
||||
assert!(doc(&s).swatch("Sunset").is_some_and(|w| matches!(w.paint, Paint::Gradient(_))));
|
||||
}
|
||||
|
||||
fn rect_filled(s: &mut Session, fill: Value) -> NodeId {
|
||||
let id = NodeId(run(s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 10, "height": 10}))["id"].as_u64().unwrap());
|
||||
let mut p = fill;
|
||||
p["ids"] = json!([id.0]);
|
||||
run(s, "paint.setFill", p);
|
||||
run(s, "paint.setStroke", json!({"ids": [id.0], "none": true}));
|
||||
id
|
||||
}
|
||||
|
||||
fn fill(s: &Session, id: NodeId) -> Paint {
|
||||
doc(s).node(id).unwrap().appearance.fill_paint()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_used_colors_skips_colours_that_have_swatches() {
|
||||
let mut s = session();
|
||||
rect_filled(&mut s, json!({"color": "#123456"}));
|
||||
rect_filled(&mut s, json!({"color": "#ed1c24"}));
|
||||
rect_filled(&mut s, json!({"swatch": "Orange"}));
|
||||
let undo = undo_len(&s);
|
||||
let r = run(&mut s, "swatch.addUsedColors", json!({}));
|
||||
assert_eq!(r, json!({"added": ["R=18 G=52 B=86"], "linked": 0}), "Red and Orange are swatches already");
|
||||
assert_eq!(undo_len(&s), undo + 1);
|
||||
assert_eq!(run(&mut s, "swatch.addUsedColors", json!({}))["added"], json!([]));
|
||||
// Only the selection's colours, as global swatches its paints link to.
|
||||
let d = rect_filled(&mut s, json!({"color": "#abcdef"}));
|
||||
rect_filled(&mut s, json!({"color": "#fedcba"}));
|
||||
run(&mut s, "select.set", json!({"ids": [d.0]}));
|
||||
let r = run(&mut s, "swatch.addUsedColors", json!({"selection": true, "global": true}));
|
||||
assert_eq!(r, json!({"added": ["R=171 G=205 B=239"], "linked": 1}));
|
||||
assert_eq!(fill(&s, d), Paint::Solid { color: vectorcraft_color::Color::from_hex("#abcdef").unwrap(), swatch: Some("R=171 G=205 B=239".into()) });
|
||||
assert!(doc(&s).swatch("R=171 G=205 B=239").unwrap().global);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unused_leaves_out_linked_colours_gradients_and_patterns_in_use() {
|
||||
let mut s = session();
|
||||
let all = run(&mut s, "swatch.unused", json!({}));
|
||||
assert!(!all["names"].as_array().unwrap().contains(&json!("[None]")));
|
||||
assert!(all["names"].as_array().unwrap().contains(&json!("Sunset")));
|
||||
run(&mut s, "swatch.edit", json!({"name": "Teal", "global": true}));
|
||||
rect_filled(&mut s, json!({"swatch": "Teal"}));
|
||||
run(&mut s, "swatch.edit", json!({"name": "Teal", "color": "#010203"}));
|
||||
rect_filled(&mut s, json!({"swatch": "Sunset"}));
|
||||
rect_filled(&mut s, json!({"color": "#ed1c24"}));
|
||||
let r = rect_filled(&mut s, json!({"color": "#ff0000"}));
|
||||
run(&mut s, "select.set", json!({"ids": [r.0]}));
|
||||
run(&mut s, "object.pattern.make", json!({"name": "Dots", "width": 20, "height": 20}));
|
||||
run(&mut s, "object.pattern.done", json!({}));
|
||||
rect_filled(&mut s, json!({"swatch": "Dots"}));
|
||||
let names = run(&mut s, "swatch.unused", json!({}))["names"].clone();
|
||||
let names: Vec<&str> = names.as_array().unwrap().iter().filter_map(Value::as_str).collect();
|
||||
for used in ["Teal", "Sunset", "Red", "Dots", "White"] {
|
||||
assert!(!names.contains(&used), "{used} is used: {names:?}");
|
||||
}
|
||||
for unused in ["Orange", "Ocean", "Bright Red", "K=50"] {
|
||||
assert!(names.contains(&unused), "{unused} is unused: {names:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_keeps_the_first_and_relinks_the_others_art() {
|
||||
let mut s = session();
|
||||
for n in ["Red", "Orange"] {
|
||||
run(&mut s, "swatch.edit", json!({"name": n, "global": true}));
|
||||
}
|
||||
let a = rect_filled(&mut s, json!({"swatch": "Red"}));
|
||||
let b = rect_filled(&mut s, json!({"swatch": "Orange"}));
|
||||
let red = doc(&s).swatch("Red").unwrap().paint.color().unwrap();
|
||||
let before = doc(&s).clone();
|
||||
let undo = undo_len(&s);
|
||||
let r = run(&mut s, "swatch.merge", json!({"names": ["Red", "Orange"]}));
|
||||
assert_eq!(r, json!({"name": "Red", "merged": ["Orange"], "relinked": 1}));
|
||||
assert!(doc(&s).swatch("Orange").is_none());
|
||||
assert_eq!(fill(&s, b), Paint::Solid { color: red, swatch: Some("Red".into()) });
|
||||
assert_eq!(undo_len(&s), undo + 1);
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert_eq!(*doc(&s), before);
|
||||
// A kept swatch that isn't global leaves the merged swatches' art unlinked, in its colour.
|
||||
run(&mut s, "swatch.merge", json!({"names": ["Lime", "Red", "Bright Red"]}));
|
||||
let lime = doc(&s).swatch("Lime").unwrap().paint.color().unwrap();
|
||||
assert_eq!(fill(&s, a), Paint::solid(lime));
|
||||
assert!(doc(&s).swatch("Red").is_none() && doc(&s).swatch("Bright Red").is_none());
|
||||
for bad in [json!({"names": ["Lime"]}), json!({"names": ["Lime", "Sunset"]}), json!({"names": ["Lime", "Nope"]})] {
|
||||
assert!(s.execute("swatch.merge", &bad).is_err(), "{bad}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ungroup_and_sort_by_kind_are_one_undo_step_each() {
|
||||
let mut s = session();
|
||||
let undo = undo_len(&s);
|
||||
assert_eq!(run(&mut s, "swatch.ungroup", json!({"name": "Brights"}))["swatches"].as_array().unwrap().len(), 5);
|
||||
assert!(doc(&s).swatch_groups.iter().all(|g| g.name != "Brights"));
|
||||
assert_eq!(names(&s, None).last().map(String::as_str), Some("Bright Violet"));
|
||||
assert!(s.execute("swatch.ungroup", &json!({"name": "Red"})).is_err());
|
||||
// A gradient first, a spot colour among the process ones.
|
||||
run(&mut s, "swatch.move", json!({"name": "Ocean", "to": 0}));
|
||||
run(&mut s, "swatch.edit", json!({"name": "Orange", "spot": true}));
|
||||
run(&mut s, "swatch.sortByKind", json!({}));
|
||||
assert_eq!(undo_len(&s), undo + 4);
|
||||
let rank = |w: &vectorcraft_color::Swatch| match &w.paint {
|
||||
Paint::None => 0,
|
||||
Paint::Solid { .. } if !w.spot => 1,
|
||||
Paint::Solid { .. } => 2,
|
||||
Paint::Gradient(_) => 3,
|
||||
Paint::Pattern { .. } => 4,
|
||||
};
|
||||
let ranks: Vec<u8> = doc(&s).swatches.iter().map(rank).collect();
|
||||
assert!(ranks.is_sorted(), "{ranks:?}");
|
||||
let n = names(&s, None);
|
||||
assert_eq!((n[0].as_str(), n[1].as_str()), ("[None]", "White"), "the order within a kind is kept");
|
||||
assert_eq!(n[n.iter().position(|x| x == "Orange").unwrap() + 1], "Ocean", "the spot colour comes after the process ones, then the gradients");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user