Recolor: a nested pattern copy that only takes swatch links is not a color change

When a tile's inner pattern only took swatch links, the outer pattern's copy still named the
inner copy, so the trial pass counted a locked object filled with the outer pattern as left
unchanged. Copies are now compared with the link-only copies they use mapped back to their
originals, repeated until no more copies qualify.
This commit is contained in:
dotCooCoo
2026-10-29 12:59:21 -07:00
parent a90ae1e9a9
commit 8da340a7b9
2 changed files with 44 additions and 7 deletions
+19 -7
View File
@@ -228,19 +228,31 @@ impl<'a> Recolor<'a> {
}
/// The patterns this pass has copied whose copy differs from the original only by swatch
/// links: the copy's name → the original's.
/// links: the copy's name → the original's. A copy whose tile uses other copies counts when
/// those copies do.
fn link_only_copies(&self, d: &Document) -> HashMap<String, String> {
let none = HashMap::new();
let art = |v: &[Arc<Node>]| v.iter().map(|n| without_links((**n).clone(), &none)).collect::<Vec<_>>();
let art = |v: &[Arc<Node>], same: &HashMap<String, String>| v.iter().map(|n| without_links((**n).clone(), same)).collect::<Vec<_>>();
let copies: HashMap<&str, &PatternDef> = self.new_patterns.iter().map(|p| (p.name.as_str(), p)).collect();
self.patterns
let pairs: Vec<(&String, &String, &PatternDef, &PatternDef)> = self
.patterns
.iter()
.filter_map(|(old, new)| {
let new = new.as_ref()?;
let copy = copies.get(new.as_str())?;
(art(&d.pattern(old)?.art) == art(&copy.art)).then(|| (new.clone(), old.clone()))
Some((new, old, d.pattern(old)?, *copies.get(new.as_str())?))
})
.collect()
.collect();
let mut same = HashMap::new();
loop {
let found: Vec<(String, String)> = pairs
.iter()
.filter(|(new, _, original, copy)| !same.contains_key(*new) && art(&original.art, &same) == art(&copy.art, &same))
.map(|(new, old, _, _)| ((*new).clone(), (*old).clone()))
.collect();
if found.is_empty() {
return same;
}
same.extend(found);
}
}
/// Recolour the objects `ids` and everything inside them (each once), add the new images and
+25
View File
@@ -720,6 +720,31 @@ fn recolor_of_everything_does_not_count_locked_art_that_would_only_take_a_swatch
assert!(r.get("warning").is_none(), "{r}");
}
#[test]
fn recolor_of_everything_does_not_count_locked_art_whose_nested_pattern_would_only_take_a_swatch_link() {
let mut s = session();
run(&mut s, "swatch.new", json!({"name": "Red", "color": "#ff0000", "global": true}));
// "Dots": an unlinked red tile; "Rings": a tile filled with "Dots".
let tile = rect(&mut s, json!("#ff0000"));
run(&mut s, "select.set", json!({"ids": [tile.0]}));
run(&mut s, "object.pattern.make", json!({"name": "Dots", "width": 20, "height": 20}));
run(&mut s, "object.pattern.done", json!({}));
let outer = rect(&mut s, json!("#000000"));
run(&mut s, "paint.setFill", json!({"ids": [outer.0], "swatch": "Dots"}));
run(&mut s, "select.set", json!({"ids": [outer.0]}));
run(&mut s, "object.pattern.make", json!({"name": "Rings", "width": 40, "height": 40}));
run(&mut s, "object.pattern.done", json!({}));
rect(&mut s, json!("#ff0000"));
let rings = rect(&mut s, json!("#000000"));
run(&mut s, "paint.setFill", json!({"ids": [rings.0], "swatch": "Rings"}));
set_props(&mut s, &[rings], json!({"locked": true}));
select_all(&mut s);
// The inner tile would only take the link to the document's red swatch.
let r = run(&mut s, "recolor.apply", json!({"map": {"#ff0000": "#ff0000"}, "limitTo": "document"}));
assert_eq!(r["notChanged"], json!({"locked": 0, "hidden": 0}), "{r}");
assert!(r.get("warning").is_none(), "{r}");
}
#[test]
fn locked_and_hidden_members_of_a_selected_group_are_recolored_and_not_counted() {
let mut s = session();