diff --git a/crates/engine/src/cmd/colorcmds.rs b/crates/engine/src/cmd/colorcmds.rs index 3b0c3f788..a7e42899a 100644 --- a/crates/engine/src/cmd/colorcmds.rs +++ b/crates/engine/src/cmd/colorcmds.rs @@ -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 { - let none = HashMap::new(); - let art = |v: &[Arc]| v.iter().map(|n| without_links((**n).clone(), &none)).collect::>(); + let art = |v: &[Arc], same: &HashMap| v.iter().map(|n| without_links((**n).clone(), same)).collect::>(); 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(©.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(©.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 diff --git a/crates/engine/src/tests/recolor.rs b/crates/engine/src/tests/recolor.rs index 72f8f2f64..b4b22d151 100644 --- a/crates/engine/src/tests/recolor.rs +++ b/crates/engine/src/tests/recolor.rs @@ -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();