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.
This commit is contained in:
dotCooCoo
2026-11-02 12:04:02 +07:00
committed by bflatastic
parent cb7df83217
commit 6b7513c0a0
3 changed files with 39 additions and 11 deletions
+18 -5
View File
@@ -79,17 +79,30 @@ pub fn pattern_source(name: &str) -> Option<Arc<Document>> {
/// 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<String> {
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::<u32>().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<String> {
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`:
+2 -6
View File
@@ -623,11 +623,7 @@ fn add(s: &mut Session, p: &Value) -> Result<Value> {
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<Value> {
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 => {
+19
View File
@@ -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());
}