From 6b7513c0a0069b9e553f9887689c536615dbd0f1 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 31 Oct 2026 12:46:10 -0700 Subject: [PATCH] Swatch libraries: a pattern swatch is found without copying the document swatch.library.add looks for the library's pattern among the document's patterns of that name or a numbered copy of it, instead of importing it into a copy of the document. Only a swatch that paints with the pattern unmoved counts as the pattern's swatch, so a swatch whose pattern was moved in its space is not reused and applied in its place. --- crates/engine/src/cmd/patternlib.rs | 23 ++++++++++++++++++----- crates/engine/src/cmd/swatchlib.rs | 8 ++------ crates/engine/src/tests/swatchlib.rs | 19 +++++++++++++++++++ 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/crates/engine/src/cmd/patternlib.rs b/crates/engine/src/cmd/patternlib.rs index f7979b10d..09afda500 100644 --- a/crates/engine/src/cmd/patternlib.rs +++ b/crates/engine/src/cmd/patternlib.rs @@ -79,17 +79,30 @@ pub fn pattern_source(name: &str) -> Option> { /// Add pattern `name` of library `lib` to `d` with the resources its art uses, as pasting art /// painted with it does: the same pattern there already is used, and a name another pattern or -/// swatch has gets a number → the pattern's name in `d`, and whether anything was added. -pub(crate) fn import(d: &mut Document, lib: &Document, name: &str) -> (String, bool) { +/// swatch has gets a number → the pattern's name in `d`. +pub(crate) fn import(d: &mut Document, lib: &Document, name: &str) -> String { let user = Node::path(NodeId(0), shapes::rectangle(Rect::new(0.0, 0.0, 1.0, 1.0)), Appearance::basic(pattern_paint(name), Paint::None, 0.0)); let imported = Clipboard::gather(lib, vec![user]).import_into(d, &SwatchChoices::default(), false); let renamed = imported.renamed.iter().find(|r| r["kind"] == "pattern" && r["from"] == name).and_then(|r| r["to"].as_str()); - (renamed.unwrap_or(name).to_string(), imported.added > 0) + renamed.unwrap_or(name).to_string() } -/// The name of the first swatch of `d` that paints with pattern `pattern`. +/// The pattern of `d` that is pattern `name` of library `lib`: the same tile options and art under +/// that name or a numbered copy of it ("Polka Dots 2"), the name itself first. +pub(crate) fn existing_pattern(d: &Document, lib: &Document, name: &str) -> Option { + let def = lib.pattern(name)?; + let bare = |p: &PatternDef| PatternDef { name: String::new(), art: vec![], ..p.clone() }; + let copy = |n: &str| n == name || n.strip_prefix(name).and_then(|r| r.strip_prefix(' ')).is_some_and(|r| r.parse::().is_ok()); + let mut found: Vec<&PatternDef> = d.patterns.iter().filter(|p| copy(&p.name) && bare(p) == bare(def) && p.art == def.art).collect(); + found.sort_by_key(|p| p.name != name); + found.first().map(|p| p.name.clone()) +} + +/// The name of the first swatch of `d` that paints with pattern `pattern` as placed in its own +/// space, as a pattern's own swatch does. pub(crate) fn pattern_swatch(d: &Document, pattern: &str) -> Option { - d.swatches_iter().find(|w| matches!(&w.paint, Paint::Pattern { pattern: p, .. } if p == pattern)).map(|w| w.name.clone()) + let paint = pattern_paint(pattern); + d.swatches_iter().find(|w| w.paint == paint).map(|w| w.name.clone()) } /// A pattern of `shapes` filled with black on the tile `w` × `h` from the origin (`tile_type`: diff --git a/crates/engine/src/cmd/swatchlib.rs b/crates/engine/src/cmd/swatchlib.rs index d0d3c2839..ed2671eaf 100644 --- a/crates/engine/src/cmd/swatchlib.rs +++ b/crates/engine/src/cmd/swatchlib.rs @@ -623,11 +623,7 @@ fn add(s: &mut Session, p: &Value) -> Result { let d = &s.doc()?.doc; let there = |w: &Swatch| match (&w.paint, &patterns) { (Paint::Pattern { pattern, .. }, Some(src)) => { - let mut probe = (**d).clone(); - match patternlib::import(&mut probe, src, pattern) { - (target, false) => patternlib::pattern_swatch(d, &target), - _ => None, - } + patternlib::existing_pattern(d, src, pattern).and_then(|target| patternlib::pattern_swatch(d, &target)) } _ => d.swatch(&w.name).is_some_and(|x| x.paint == w.paint).then(|| w.name.clone()), }; @@ -655,7 +651,7 @@ fn add(s: &mut Session, p: &Value) -> Result { if let (Paint::Pattern { pattern, .. }, Some(src)) = (&w.paint, &patterns) { // The pattern comes with its swatch; a pattern the document has without one // gets one. - let (target, _) = patternlib::import(d, src, pattern); + let target = patternlib::import(d, src, pattern); let name = match patternlib::pattern_swatch(d, &target) { Some(name) => name, None => { diff --git a/crates/engine/src/tests/swatchlib.rs b/crates/engine/src/tests/swatchlib.rs index f1bdf0139..6c73cc6cc 100644 --- a/crates/engine/src/tests/swatchlib.rs +++ b/crates/engine/src/tests/swatchlib.rs @@ -695,6 +695,25 @@ fn pattern_libraries_bring_their_patterns_and_add_each_once() { let d = doc(&s); assert!(added.iter().all(|n| d.pattern(n).is_some() && d.swatch(n).is_some()), "{added:?}"); assert!(matches!(d.node(id).unwrap().appearance.fill_paint(), Paint::Pattern { pattern, .. } if pattern == "Polka Dots 2")); + // A swatch of the pattern moved in its space is not the pattern's own swatch: a new one is added + // and applied, and no second pattern. + s.edit("Move", |d, _| { + if let Some(w) = d.swatches_iter_mut().find(|w| w.name == "Polka Dots 2") { + w.paint = Paint::Pattern { pattern: "Polka Dots 2".into(), xf: kurbo::Affine::translate((3.0, 0.0)) }; + } + Ok(()) + }) + .unwrap(); + let patterns = doc(&s).patterns.len(); + let r = run(&mut s, "swatch.library.add", json!({"library": "geometric-patterns", "names": ["Polka Dots"], "apply": "fill"})); + let applied = r["applied"].as_str().unwrap().to_string(); + assert_ne!(applied, "Polka Dots 2"); + let d = doc(&s); + assert_eq!( + (d.patterns.len(), d.swatch(&applied).map(|w| w.paint.clone())), + (patterns, Some(vectorcraft_doc::pattern::pattern_paint("Polka Dots 2"))) + ); + assert_eq!(d.node(id).unwrap().appearance.fill_paint(), vectorcraft_doc::pattern::pattern_paint("Polka Dots 2")); // A pattern library has no colours to limit to, and is built in. assert!(s.execute("swatch.library.copyToUser", &json!({"library": "geometric-patterns"})).is_err()); }