mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-29 20:27:05 -06:00
Swatches: global swatch edits also update graph designs and the art of brushes
Squashed from #4489 onto main.
This commit is contained in:
@@ -108,9 +108,10 @@ impl Document {
|
||||
Some(list.remove(i))
|
||||
}
|
||||
|
||||
/// Visit every colour that can link to a swatch — solid paints and gradient stops of fills,
|
||||
/// strokes and text runs — in the art (opacity masks included), symbol definitions, pattern
|
||||
/// tiles, graphic styles and the swatches themselves (tint swatches, gradient swatches' stops).
|
||||
/// Visit every color that can link to a swatch (solid paints and gradient stops of fills,
|
||||
/// strokes and text runs) in the art (opacity masks included), symbol definitions, graph
|
||||
/// designs, pattern tiles and the tile that pattern editing's Cancel puts back, graphic styles
|
||||
/// and the swatches themselves (tint swatches, gradient swatches' stops).
|
||||
/// `f(colour, link, tint)` may change them and returns true when it did
|
||||
/// ([`Paint::map_links`]). Only nodes holding a changed paint (and the paths to them) are
|
||||
/// copied. Returns the number of changed paints.
|
||||
@@ -119,7 +120,10 @@ impl Document {
|
||||
for s in &mut self.symbols {
|
||||
n += map_tree(&mut s.art, f);
|
||||
}
|
||||
for p in &mut self.patterns {
|
||||
for g in &mut self.graph_designs {
|
||||
n += map_tree(&mut g.art, f);
|
||||
}
|
||||
for p in self.patterns.iter_mut().chain(self.pattern_edit.iter_mut().filter_map(|pe| pe.original.as_mut())) {
|
||||
n += map_trees(&mut p.art, f);
|
||||
}
|
||||
for gs in &mut self.graphic_styles {
|
||||
@@ -307,6 +311,19 @@ pub fn walk_with_masks<'a>(n: &'a Node, f: &mut dyn FnMut(&'a Node)) {
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Document::map_solid_paints`] for art a document keeps outside its trees, such as the art of a
|
||||
/// brush: `f` applied to the linkable colors of `n` and its subtree (opacity masks included). Only
|
||||
/// the nodes holding a changed paint are copied. Returns the number of changed paints.
|
||||
pub fn map_node_solid_paints(n: &mut Node, f: &mut LinkFn) -> usize {
|
||||
match map_node(n, f) {
|
||||
Some((new, count)) => {
|
||||
*n = new;
|
||||
count
|
||||
}
|
||||
None => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_tree(n: &mut Arc<Node>, f: &mut LinkFn) -> usize {
|
||||
match map_node(n, f) {
|
||||
Some((new, count)) => {
|
||||
@@ -400,6 +417,25 @@ mod tests {
|
||||
assert!(Arc::ptr_eq(&d.layers[0], &again.layers[0]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapping_reaches_graph_designs_and_the_tile_that_pattern_cancel_puts_back() {
|
||||
let (mut d, a, _) = doc();
|
||||
let art = d.node_arc(a).cloned().unwrap();
|
||||
d.graph_designs.push(crate::GraphDesign { name: "Bar".into(), art: art.clone() });
|
||||
d.pattern_edit =
|
||||
Some(crate::PatternEdit { pattern: "Tile".into(), layer: NodeId(0), original: Some(crate::PatternDef::new("Tile", vec![art])) });
|
||||
let mut loose = d.node(a).unwrap().clone();
|
||||
let blue = Color::rgb(0.0, 0.0, 1.0);
|
||||
let mut to_blue = |c: &mut Color, s: &mut Option<String>, _: &mut f32| s.as_deref() == Some("Brand") && std::mem::replace(c, blue) != blue;
|
||||
assert_eq!(d.map_solid_paints(&mut to_blue), 3, "the layer's rectangle, the design and the Cancel tile");
|
||||
assert_eq!(d.graph_designs[0].art.appearance.fill_paint(), linked(blue, "Brand"));
|
||||
let tile = &d.pattern_edit.as_ref().unwrap().original.as_ref().unwrap().art[0];
|
||||
assert_eq!(tile.appearance.fill_paint(), linked(blue, "Brand"));
|
||||
assert_eq!(map_node_solid_paints(&mut loose, &mut to_blue), 1);
|
||||
assert_eq!(loose.appearance.fill_paint(), linked(blue, "Brand"));
|
||||
assert_eq!(map_node_solid_paints(&mut loose, &mut to_blue), 0, "nothing left to change");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapping_within_ids_visits_each_paint_once() {
|
||||
let (mut d, a, b) = doc();
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_color::{Color, Paint, Swatch, SwatchGroup, tint_percent};
|
||||
use vectorcraft_doc::pattern::pattern_paint;
|
||||
use vectorcraft_doc::swatches::{LinkFn, color_name, node_colors};
|
||||
use vectorcraft_doc::swatches::{LinkFn, color_name, map_node_solid_paints, node_colors};
|
||||
use vectorcraft_doc::{Document, NodeId};
|
||||
|
||||
use super::paint::paint_from;
|
||||
@@ -257,7 +257,7 @@ fn swatch_delete(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
for n in &names {
|
||||
d.remove_swatch(n);
|
||||
}
|
||||
Ok(d.map_solid_paints(&mut unlink))
|
||||
Ok(map_swatch_links(d, &mut unlink))
|
||||
})?;
|
||||
map_default_paints(s, &mut unlink);
|
||||
Ok(json!({"deleted": names, "unlinked": unlinked}))
|
||||
@@ -529,6 +529,130 @@ pub(super) fn map_default_paints(s: &mut Session, f: &mut LinkFn) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply a swatch-link rewrite to the document ([`Document::map_solid_paints`]) and to the art of
|
||||
/// the brushes in its library, which is stored again only when a brush's paint changed. A Hue Shift
|
||||
/// key that was the color of a recolored paint takes the paint's new color. Returns the number of
|
||||
/// changed paints. Stored entries are stored again as they were read, except that a brush whose
|
||||
/// paint changed is merged into its entry ([`merge_stored`]), so fields the brush definition does
|
||||
/// not read are kept at every level.
|
||||
pub(super) fn map_swatch_links(d: &mut Document, f: &mut LinkFn) -> usize {
|
||||
use vectorcraft_brush::{Brush, BrushKind, Colorization};
|
||||
let mut n = d.map_solid_paints(f);
|
||||
// Each stored entry, its brush when it parses, the brush as written before the edit (only
|
||||
// when its paint changed), and whether its paint changed.
|
||||
let mut lib: Vec<(Value, Option<Brush>, Value, bool)> = match d.unknown.get(vectorcraft_brush::DOC_KEY) {
|
||||
Some(v) => v.as_array().into_iter().flatten().map(|e| (e.clone(), Brush::from_json(e.clone()).ok(), Value::Null, false)).collect(),
|
||||
None => {
|
||||
vectorcraft_brush::library(d).into_iter().map(|b| (serde_json::to_value(&b).unwrap_or_default(), Some(b), Value::Null, false)).collect()
|
||||
}
|
||||
};
|
||||
let mut in_brushes = 0;
|
||||
for (_, b, base, changed) in lib.iter_mut() {
|
||||
let Some(b) = b else { continue };
|
||||
let before_edit = serde_json::to_value(&*b).unwrap_or_default();
|
||||
let key = match b.kind.colorization() {
|
||||
Colorization::HueShift { key } => Some(key),
|
||||
_ => None,
|
||||
};
|
||||
let before = key.map(|_| art_paint_colors(b.art()));
|
||||
let in_brush = b.art_mut().into_iter().map(|art| map_node_solid_paints(art, f)).sum::<usize>();
|
||||
*changed = in_brush > 0;
|
||||
if *changed {
|
||||
*base = before_edit;
|
||||
}
|
||||
in_brushes += in_brush;
|
||||
let (Some(key), Some(before)) = (key, before) else { continue };
|
||||
// Colors are compared by hex value, as `art_colors` (Brush Options' key colors) lists them.
|
||||
let same = |a: &Color, b: &Color| a.to_hex() == b.to_hex();
|
||||
let after = art_paint_colors(b.art());
|
||||
// The key moves when the art's fills and strokes (not opacity masks) had the key color and
|
||||
// none has it now, to the first new color of a paint that had it.
|
||||
let moved = before.iter().any(|c| same(c, &key)) && !after.iter().any(|c| same(c, &key));
|
||||
let new_key = before.iter().zip(&after).find(|(o, _)| same(o, &key)).map(|(_, n)| *n);
|
||||
if let Some(key) = new_key.filter(|_| moved) {
|
||||
let c = match &mut b.kind {
|
||||
BrushKind::Art(a) => &mut a.colorization,
|
||||
BrushKind::Scatter(s) => &mut s.colorization,
|
||||
BrushKind::Pattern(p) => &mut p.colorization,
|
||||
BrushKind::Calligraphic(_) | BrushKind::Bristle(_) => continue,
|
||||
};
|
||||
*c = Colorization::HueShift { key };
|
||||
}
|
||||
}
|
||||
if in_brushes > 0 {
|
||||
let lib = lib
|
||||
.into_iter()
|
||||
.map(|(raw, b, base, changed)| match b.filter(|_| changed) {
|
||||
Some(b) => merge_stored(raw, &base, serde_json::to_value(b).unwrap_or_default()),
|
||||
None => raw,
|
||||
})
|
||||
.collect();
|
||||
d.unknown.insert(vectorcraft_brush::DOC_KEY.into(), Value::Array(lib));
|
||||
n += in_brushes;
|
||||
}
|
||||
n
|
||||
}
|
||||
|
||||
/// `new` (a definition as written after an edit) with the parts of `raw` (its stored entry) that
|
||||
/// `base` (the definition as read from `raw` and written before the edit) does not hold. An object
|
||||
/// keeps the fields of `raw` that `base` leaves out, and drops the fields that `base` holds and
|
||||
/// `new` leaves out. An array takes the length of `new`, each element merged with the element at
|
||||
/// the same index in `raw`.
|
||||
fn merge_stored(raw: Value, base: &Value, new: Value) -> Value {
|
||||
match (raw, new) {
|
||||
(Value::Object(mut raw), Value::Object(new)) => {
|
||||
let mut out = serde_json::Map::new();
|
||||
for (k, v) in new {
|
||||
let v = match raw.remove(&k) {
|
||||
Some(r) => merge_stored(r, base.get(k.as_str()).unwrap_or(&Value::Null), v),
|
||||
None => v,
|
||||
};
|
||||
out.insert(k, v);
|
||||
}
|
||||
out.extend(raw.into_iter().filter(|(k, _)| base.get(k.as_str()).is_none()));
|
||||
Value::Object(out)
|
||||
}
|
||||
(Value::Array(raw), Value::Array(new)) => {
|
||||
let mut raw = raw.into_iter();
|
||||
let merged = new.into_iter().enumerate().map(|(i, v)| match raw.next() {
|
||||
Some(r) => merge_stored(r, base.get(i).unwrap_or(&Value::Null), v),
|
||||
None => v,
|
||||
});
|
||||
Value::Array(merged.collect())
|
||||
}
|
||||
(_, new) => new,
|
||||
}
|
||||
}
|
||||
|
||||
/// The solid colors of the fills and strokes under `nodes` (type runs' fills and strokes included,
|
||||
/// opacity masks left out), in tree order and with repeats, the paints [`vectorcraft_brush::art_colors`]
|
||||
/// counts.
|
||||
fn art_paint_colors(nodes: Vec<&vectorcraft_doc::Node>) -> Vec<Color> {
|
||||
fn walk(n: &vectorcraft_doc::Node, out: &mut Vec<Color>) {
|
||||
for it in &n.appearance.items {
|
||||
let (visible, paint) = match it {
|
||||
vectorcraft_doc::AppearanceItem::Fill(f) => (f.visible, &f.paint),
|
||||
vectorcraft_doc::AppearanceItem::Stroke(s) => (s.visible, &s.paint),
|
||||
};
|
||||
out.extend(paint.color().filter(|_| visible));
|
||||
}
|
||||
if let vectorcraft_doc::NodeKind::Text(t) = &n.kind {
|
||||
for r in &t.runs {
|
||||
let stroke = (r.style.stroke_width > 0.0).then_some(&r.style.stroke);
|
||||
out.extend([Some(&r.style.fill), stroke].into_iter().flatten().filter_map(Paint::color));
|
||||
}
|
||||
}
|
||||
for c in n.children().into_iter().flatten() {
|
||||
walk(c, out);
|
||||
}
|
||||
}
|
||||
let mut out = vec![];
|
||||
for n in nodes {
|
||||
walk(n, &mut out);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn swatch_edit(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "swatch.edit";
|
||||
let name = str_param(p, "name").ok_or_else(|| bad(C, "missing `name`"))?.to_string();
|
||||
@@ -592,7 +716,7 @@ fn swatch_edit(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(d.map_solid_paints(&mut |c, l, t| relink.apply(c, l, t)))
|
||||
Ok(map_swatch_links(d, &mut |c, l, t| relink.apply(c, l, t)))
|
||||
})?;
|
||||
relink.defaults(s);
|
||||
Ok(json!({"name": to, "relinked": relinked}))
|
||||
@@ -747,7 +871,7 @@ fn swatch_merge(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
for n in merged {
|
||||
d.remove_swatch(n);
|
||||
}
|
||||
Ok(d.map_solid_paints(&mut relink))
|
||||
Ok(map_swatch_links(d, &mut relink))
|
||||
})?;
|
||||
map_default_paints(s, &mut relink);
|
||||
Ok(json!({"name": keep, "merged": merged, "relinked": relinked}))
|
||||
@@ -803,7 +927,7 @@ fn swatch_spot_options(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
.filter(|w| w.spot && matches!(w.paint, Paint::Solid { color: Color::Lab { .. }, swatch: None, .. }))
|
||||
.filter_map(|w| Some((w.name.clone(), d.global_color(&w.name)?)))
|
||||
.collect();
|
||||
let relinked = d.map_solid_paints(&mut follow_bases(&bases));
|
||||
let relinked = map_swatch_links(d, &mut follow_bases(&bases));
|
||||
Ok((relinked, bases))
|
||||
})?;
|
||||
map_default_paints(s, &mut follow_bases(&bases));
|
||||
@@ -880,7 +1004,7 @@ pub(super) fn rewrite_group(d: &mut Document, group: &str, colors: &[Color], ren
|
||||
let g = &mut d.swatch_groups[gi];
|
||||
(g.name, g.swatches) = (name.clone(), swatches);
|
||||
let names = g.swatches.iter().map(|w| w.name.clone()).collect();
|
||||
let relinked = if relinks.is_empty() { 0 } else { d.map_solid_paints(&mut |c, l, t| relinks.iter().any(|r| r.apply(c, l, t))) };
|
||||
let relinked = if relinks.is_empty() { 0 } else { map_swatch_links(d, &mut |c, l, t| relinks.iter().any(|r| r.apply(c, l, t))) };
|
||||
Ok(GroupEdit { name, swatches: names, relinks, relinked })
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ use vectorcraft_color::{Paint, Swatch, SwatchGroup, SwatchLibrary, default_swatc
|
||||
use super::fileio::{create_dir, extension, read_file, write_file, write_new_file};
|
||||
use super::menucmds::squash;
|
||||
use super::package::free_name;
|
||||
use super::swatch::{map_default_paints, str_list, swatch_json};
|
||||
use super::swatch::{map_default_paints, map_swatch_links, str_list, swatch_json};
|
||||
use super::{Color, CommandSpec, Result, Session, always, bad, bool_or, cmd, has_doc, str_param};
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
@@ -644,7 +644,7 @@ fn reset_defaults(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
(d.swatches, d.swatch_groups) = (defaults.into(), groups.into());
|
||||
added.extend(d.swatches_iter().filter(|w| !before.contains(&w.name)).map(|w| w.name.clone()));
|
||||
live.extend(d.swatches_iter().filter(|w| w.global).map(|w| w.name.clone()));
|
||||
d.map_solid_paints(&mut |_, l, _| unlink_dead(&live, l));
|
||||
map_swatch_links(d, &mut |_, l, _| unlink_dead(&live, l));
|
||||
return Ok(added);
|
||||
}
|
||||
for (i, w) in defaults.into_iter().enumerate() {
|
||||
|
||||
@@ -528,3 +528,203 @@ fn global_swatch_edits_reach_opacity_mask_art() {
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert_eq!(mask_fill(&s).color(), Some(Color::rgb(1.0, 1.0, 1.0)));
|
||||
}
|
||||
|
||||
/// The fills of graph design "Bar" and of the art of brush "Holder", as `place: color, link`.
|
||||
fn fills_outside_the_layers(s: &Session) -> Vec<String> {
|
||||
let d = doc(s);
|
||||
let mut out = vec![];
|
||||
let mut tree = |place: &str, n: &vectorcraft_doc::Node| {
|
||||
n.walk(&mut |m| {
|
||||
if let Paint::Solid { color, swatch, .. } = m.appearance.fill_paint() {
|
||||
out.push(format!("{place}: {}, {}", color.to_hex(), swatch.as_deref().unwrap_or("-")));
|
||||
}
|
||||
})
|
||||
};
|
||||
tree("design", &d.graph_designs.iter().find(|g| g.name == "Bar").unwrap().art);
|
||||
vectorcraft_brush::find(d, "Holder").unwrap().art().into_iter().for_each(|n| tree("brush art", n));
|
||||
out
|
||||
}
|
||||
|
||||
/// A global swatch edit that recolors a Hue Shift brush's key color in its art moves the key to the
|
||||
/// new color, so strokes still paint that art with the stroke color.
|
||||
#[test]
|
||||
fn global_swatch_edits_move_a_hue_shift_key() {
|
||||
let mut s = session();
|
||||
run(&mut s, "swatch.new", json!({"name": "Tone", "color": "#ff0000", "global": true}));
|
||||
let id = rect(&mut s);
|
||||
run(&mut s, "paint.setFill", json!({"swatch": "Tone", "ids": [id.0]}));
|
||||
run(&mut s, "paint.setStroke", json!({"none": true, "ids": [id.0]}));
|
||||
run(&mut s, "brush.new", json!({"type": "art", "name": "Key", "ids": [id.0], "params": {"colorization": {"method": "hueShift"}}}));
|
||||
let key = |s: &mut Session| run(s, "brush.get", json!({"name": "Key"}))["colorization"]["key"].clone();
|
||||
let (red, blue) = (json!(Color::rgb(1.0, 0.0, 0.0)), json!(Color::rgb(0.0, 0.0, 1.0)));
|
||||
assert_eq!(key(&mut s), red);
|
||||
run(&mut s, "swatch.edit", json!({"name": "Tone", "color": "#0000ff"}));
|
||||
assert_eq!(key(&mut s), blue);
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert_eq!(key(&mut s), red);
|
||||
|
||||
// A paint in the key color that only an opacity mask uses leaves the key where it is.
|
||||
run(&mut s, "swatch.new", json!({"name": "MaskTone", "color": "#ff0000", "global": true}));
|
||||
let obj = rect(&mut s);
|
||||
run(&mut s, "paint.setFill", json!({"color": "#ff0000", "ids": [obj.0]}));
|
||||
run(&mut s, "paint.setStroke", json!({"none": true, "ids": [obj.0]}));
|
||||
let m = rect(&mut s);
|
||||
run(&mut s, "paint.setFill", json!({"swatch": "MaskTone", "ids": [m.0]}));
|
||||
run(&mut s, "select.set", json!({"ids": [obj.0, m.0]}));
|
||||
run(&mut s, "transparency.makeOpacityMask", json!({"clip": true, "invert": false}));
|
||||
run(&mut s, "brush.new", json!({"type": "art", "name": "Masked", "ids": [obj.0], "params": {"colorization": {"method": "hueShift"}}}));
|
||||
let masked = |s: &mut Session| run(s, "brush.get", json!({"name": "Masked"}))["colorization"]["key"].clone();
|
||||
assert_eq!(masked(&mut s), red);
|
||||
run(&mut s, "swatch.edit", json!({"name": "MaskTone", "color": "#0000ff"}));
|
||||
assert_eq!(masked(&mut s), red);
|
||||
|
||||
// With the art and its mask on one swatch, the key takes the art's new color, not the mask's.
|
||||
run(&mut s, "swatch.new", json!({"name": "Both", "color": "#ffffff", "global": true}));
|
||||
let obj = rect(&mut s);
|
||||
run(&mut s, "paint.setFill", json!({"swatch": "Both", "ids": [obj.0]}));
|
||||
run(&mut s, "paint.setStroke", json!({"none": true, "ids": [obj.0]}));
|
||||
let m = rect(&mut s);
|
||||
run(&mut s, "paint.setFill", json!({"swatch": "Both", "tint": 50, "ids": [m.0]}));
|
||||
run(&mut s, "select.set", json!({"ids": [obj.0, m.0]}));
|
||||
run(&mut s, "transparency.makeOpacityMask", json!({"clip": true, "invert": false}));
|
||||
run(&mut s, "brush.new", json!({"type": "art", "name": "Both", "ids": [obj.0], "params": {"colorization": {"method": "hueShift"}}}));
|
||||
run(&mut s, "swatch.edit", json!({"name": "Both", "color": "#0000ff"}));
|
||||
assert_eq!(run(&mut s, "brush.get", json!({"name": "Both"}))["colorization"]["key"], blue);
|
||||
}
|
||||
|
||||
/// A global swatch edit that recolors brush art keeps the stored library entries that do not parse
|
||||
/// as brushes.
|
||||
#[test]
|
||||
fn global_swatch_edits_keep_unreadable_brush_entries() {
|
||||
let mut s = session();
|
||||
run(&mut s, "swatch.new", json!({"name": "Tone", "color": "#ff0000", "global": true}));
|
||||
let id = rect(&mut s);
|
||||
run(&mut s, "paint.setFill", json!({"swatch": "Tone", "ids": [id.0]}));
|
||||
run(&mut s, "brush.new", json!({"type": "art", "name": "Holder", "ids": [id.0]}));
|
||||
let junk = json!({"name": "Unreadable", "kind": 7});
|
||||
let doc = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc);
|
||||
let Some(Value::Array(entries)) = doc.unknown.get_mut(vectorcraft_brush::DOC_KEY) else { panic!("no stored brushes") };
|
||||
entries.push(junk.clone());
|
||||
run(&mut s, "swatch.edit", json!({"name": "Tone", "color": "#0000ff"}));
|
||||
let stored = &s.doc().unwrap().doc.unknown[vectorcraft_brush::DOC_KEY];
|
||||
assert!(stored.as_array().is_some_and(|a| a.contains(&junk)), "{stored}");
|
||||
assert!(vectorcraft_brush::find(&s.doc().unwrap().doc, "Holder").is_some());
|
||||
}
|
||||
|
||||
/// A global swatch edit that recolors brush art keeps the fields of stored brush entries that the
|
||||
/// brush definition does not read, both on the brush it recolors and on the brushes it leaves alone,
|
||||
/// and inside the art too. A swatch delete that unlinks the art drops the link and keeps those fields.
|
||||
#[test]
|
||||
fn global_swatch_edits_keep_unread_brush_fields() {
|
||||
// Adds `"extra": 8` to every object under `v` that links a swatch.
|
||||
fn tag_links(v: &mut Value) {
|
||||
match v {
|
||||
Value::Object(o) => {
|
||||
if o.contains_key("swatch") {
|
||||
o.insert("extra".into(), json!(8));
|
||||
}
|
||||
o.values_mut().for_each(tag_links);
|
||||
}
|
||||
Value::Array(a) => a.iter_mut().for_each(tag_links),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// The objects under `v` that carry `"extra": 8`.
|
||||
fn tagged(v: &Value) -> Vec<&Value> {
|
||||
match v {
|
||||
Value::Object(o) => o.get("extra").filter(|e| **e == json!(8)).map(|_| v).into_iter().chain(o.values().flat_map(tagged)).collect(),
|
||||
Value::Array(a) => a.iter().flat_map(tagged).collect(),
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
let mut s = session();
|
||||
run(&mut s, "swatch.new", json!({"name": "Tone", "color": "#ff0000", "global": true}));
|
||||
let id = rect(&mut s);
|
||||
run(&mut s, "paint.setFill", json!({"swatch": "Tone", "ids": [id.0]}));
|
||||
run(&mut s, "brush.new", json!({"type": "art", "name": "Holder", "ids": [id.0]}));
|
||||
let doc = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc);
|
||||
let Some(Value::Array(entries)) = doc.unknown.get_mut(vectorcraft_brush::DOC_KEY) else { panic!("no stored brushes") };
|
||||
for e in entries.iter_mut() {
|
||||
if let Some(o) = e.as_object_mut() {
|
||||
o.insert("extra".into(), json!(7));
|
||||
}
|
||||
tag_links(e);
|
||||
}
|
||||
let links = tagged(&doc.unknown[vectorcraft_brush::DOC_KEY]).len();
|
||||
assert!(links > 0, "{}", doc.unknown[vectorcraft_brush::DOC_KEY]);
|
||||
run(&mut s, "swatch.edit", json!({"name": "Tone", "color": "#0000ff"}));
|
||||
let stored = &s.doc().unwrap().doc.unknown[vectorcraft_brush::DOC_KEY];
|
||||
let entries = stored.as_array().map(Vec::as_slice).unwrap_or_default();
|
||||
assert!(entries.len() > 1, "{stored}");
|
||||
assert!(entries.iter().all(|e| e["extra"] == json!(7)), "{stored}");
|
||||
assert_eq!(tagged(stored).len(), links, "{stored}");
|
||||
let holder = vectorcraft_brush::find(&s.doc().unwrap().doc, "Holder").unwrap();
|
||||
let fill = holder.art().first().map(|n| n.appearance.fill_paint().color().map(|c| c.to_hex()));
|
||||
assert_eq!(fill, Some(Some("#0000ff".to_string())));
|
||||
|
||||
run(&mut s, "swatch.delete", json!({"name": "Tone"}));
|
||||
let stored = &s.doc().unwrap().doc.unknown[vectorcraft_brush::DOC_KEY];
|
||||
let unlinked = tagged(stored);
|
||||
assert_eq!(unlinked.len(), links, "{stored}");
|
||||
assert!(unlinked.iter().all(|o| o.get("swatch").is_none()), "{stored}");
|
||||
let holder = vectorcraft_brush::find(&s.doc().unwrap().doc, "Holder").unwrap();
|
||||
let fill = holder.art().first().map(|n| n.appearance.fill_paint().color().map(|c| c.to_hex()));
|
||||
assert_eq!(fill, Some(Some("#0000ff".to_string())));
|
||||
}
|
||||
|
||||
/// A global swatch edit, rename, delete and Default Swatches with replace change the art of graph
|
||||
/// designs and brushes: the art follows the swatch's color and name, or keeps its color unlinked,
|
||||
/// each in one undo step.
|
||||
#[test]
|
||||
fn global_swatch_edits_reach_graph_designs_and_brush_art() {
|
||||
let mut s = session();
|
||||
run(&mut s, "swatch.new", json!({"name": "Tone", "color": "#ff0000", "global": true}));
|
||||
for (cmd, p) in [("graph.design", json!({"new": "Bar"})), ("brush.new", json!({"type": "art", "name": "Holder"}))] {
|
||||
let id = rect(&mut s);
|
||||
run(&mut s, "paint.setFill", json!({"swatch": "Tone", "ids": [id.0]}));
|
||||
run(&mut s, "select.set", json!({"ids": [id.0]}));
|
||||
run(&mut s, cmd, p);
|
||||
run(&mut s, "edit.clear", json!({"ids": [id.0]}));
|
||||
}
|
||||
let both = |fill: &str| vec![format!("design: {fill}"), format!("brush art: {fill}")];
|
||||
assert_eq!(fills_outside_the_layers(&s), both("#ff0000, Tone"));
|
||||
let undo = s.doc().unwrap().history.undo.len();
|
||||
|
||||
let r = run(&mut s, "swatch.edit", json!({"name": "Tone", "color": "#0000ff"}));
|
||||
assert_eq!(r["relinked"], 2, "{r}");
|
||||
assert_eq!(fills_outside_the_layers(&s), both("#0000ff, Tone"));
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1);
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert_eq!(fills_outside_the_layers(&s), both("#ff0000, Tone"));
|
||||
|
||||
run(&mut s, "swatch.edit", json!({"name": "Tone", "newName": "Hue"}));
|
||||
assert_eq!(fills_outside_the_layers(&s), both("#ff0000, Hue"));
|
||||
// Default Swatches with replace drops the links to the swatches it removes.
|
||||
run(&mut s, "swatch.resetDefaults", json!({"replace": true}));
|
||||
assert_eq!(fills_outside_the_layers(&s), both("#ff0000, -"));
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
let r = run(&mut s, "swatch.delete", json!({"name": "Hue"}));
|
||||
assert_eq!(r["unlinked"], 2, "{r}");
|
||||
assert_eq!(fills_outside_the_layers(&s), both("#ff0000, -"));
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 2);
|
||||
}
|
||||
|
||||
/// A global swatch edited while a pattern is being edited also changes the tile that pattern
|
||||
/// editing's Cancel puts back.
|
||||
#[test]
|
||||
fn global_swatch_edits_reach_the_tile_that_pattern_cancel_puts_back() {
|
||||
let mut s = session();
|
||||
run(&mut s, "swatch.new", json!({"name": "Tone", "color": "#ff0000", "global": true}));
|
||||
let id = rect(&mut s);
|
||||
run(&mut s, "paint.setFill", json!({"swatch": "Tone", "ids": [id.0]}));
|
||||
run(&mut s, "select.set", json!({"ids": [id.0]}));
|
||||
let pattern = run(&mut s, "object.pattern.make", json!({"edit": false}))["name"].as_str().unwrap().to_string();
|
||||
run(&mut s, "object.pattern.edit", json!({"name": pattern}));
|
||||
run(&mut s, "swatch.edit", json!({"name": "Tone", "color": "#0000ff"}));
|
||||
run(&mut s, "object.pattern.cancel", json!({}));
|
||||
let mut fills = vec![];
|
||||
for n in &doc(&s).pattern(&pattern).unwrap().art {
|
||||
n.walk(&mut |m| fills.push(m.appearance.fill_paint()));
|
||||
}
|
||||
assert_eq!(fills, [Paint::Solid { color: Color::rgb(0.0, 0.0, 1.0), swatch: Some("Tone".into()), tint: 1.0 }]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user