Files
vectorcraft/crates/engine/src/cmd/patternlib.rs
T
NetterSilverandClaude Haiku 5.5 b1fd06090c Don't treat a pattern named "Name +N" as a numbered copy on import
Found while testing crates/engine/src/cmd/patternlib.rs:99 (existing_pattern)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-09 02:03:09 +07:00

171 lines
8.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Pattern libraries (Window → Swatch Libraries → Patterns, the Swatches panel's library button):
//! read-only sets of pattern swatches the swatch library panel shows. Each built-in library is a
//! document holding its patterns, drawn here from plain shapes ([`PATTERN_LIBRARIES`]).
//! `swatch.library.add` copies a pattern into the document with its swatch.
use std::sync::{Arc, LazyLock};
use vectorcraft_color::{Color, Paint, Swatch, SwatchLibrary};
use vectorcraft_doc::pattern::pattern_paint;
use vectorcraft_doc::{Appearance, Document, Node, NodeId, PatternDef, TileType};
use vectorcraft_geom::{PathData, Point, Rect, SubPath, shapes};
use super::clipboard::Clipboard;
use super::clipboard::resources::SwatchChoices;
use super::swatchlib::LibraryInfo;
/// A built-in pattern library: a stable id, its menu name and the function that adds its patterns.
pub struct BuiltinPatternLibrary {
pub id: &'static str,
pub name: &'static str,
make: fn(&mut Document),
}
/// The built-in pattern libraries (Window → Swatch Libraries → Patterns), in menu order.
pub const PATTERN_LIBRARIES: &[BuiltinPatternLibrary] =
&[BuiltinPatternLibrary { id: "geometric-patterns", name: "Geometric Patterns", make: geometric_patterns }];
/// The library category of the built-in pattern libraries.
pub const CATEGORY: &str = "patterns";
/// The built-in pattern libraries as the menus list them.
pub fn infos() -> impl Iterator<Item = LibraryInfo> {
PATTERN_LIBRARIES.iter().map(|b| LibraryInfo { id: b.id.into(), name: b.name.into(), category: CATEGORY })
}
/// Built-in pattern library `id`: the document holding its patterns, made once.
pub fn library_doc(id: &str) -> Option<Arc<Document>> {
static MADE: LazyLock<Vec<Arc<Document>>> = LazyLock::new(|| {
PATTERN_LIBRARIES
.iter()
.map(|b| {
let mut d = Document::new(100.0, 100.0);
d.title = b.name.into();
(b.make)(&mut d);
Arc::new(d)
})
.collect()
});
PATTERN_LIBRARIES.iter().position(|b| b.id == id).and_then(|i| MADE.get(i).cloned())
}
/// Built-in pattern library `id` as the library panel shows it: a swatch for each pattern.
pub fn library(id: &str) -> Option<(LibraryInfo, Arc<SwatchLibrary>)> {
static MADE: LazyLock<Vec<Arc<SwatchLibrary>>> = LazyLock::new(|| {
PATTERN_LIBRARIES
.iter()
.map(|b| {
let swatches = library_doc(b.id)
.map(|d| {
d.patterns
.iter()
.map(|p| Swatch { name: p.name.clone(), paint: pattern_paint(&p.name), global: false, spot: false })
.collect()
})
.unwrap_or_default();
Arc::new(SwatchLibrary { name: b.name.into(), swatches, groups: vec![] })
})
.collect()
});
let i = PATTERN_LIBRARIES.iter().position(|b| b.id == id)?;
Some((infos().nth(i)?, MADE.get(i)?.clone()))
}
/// The built-in library document that holds the pattern of swatch `w`, when `w` is one of a
/// built-in pattern library's own swatches (a library panel draws them from it). A swatch of
/// another library is not, even under the same name.
pub fn swatch_source(w: &Swatch) -> Option<Arc<Document>> {
let Paint::Pattern { pattern, .. } = &w.paint else { return None };
let own = |lib: &SwatchLibrary| lib.swatches.iter().any(|s| std::ptr::eq(s, w));
let b = PATTERN_LIBRARIES.iter().find(|b| library(b.id).is_some_and(|(_, lib)| own(&lib)))?;
library_doc(b.id).filter(|d| d.pattern(pattern).is_some())
}
/// 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`.
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()
}
/// 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.is_empty() && r.bytes().all(|b| b.is_ascii_digit()))
};
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> {
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`:
/// how the tiles are laid out).
fn add(d: &mut Document, name: &str, w: f64, h: f64, tile_type: TileType, shapes: Vec<PathData>) {
let art =
shapes.into_iter().map(|p| Arc::new(Node::path(d.alloc_id(), p, Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0)))).collect();
let def = PatternDef { tile: Rect::new(0.0, 0.0, w, h), tile_type, ..PatternDef::new(name, art) };
d.patterns.push(def);
}
fn polygon(points: &[(f64, f64)]) -> PathData {
let points: Vec<Point> = points.iter().map(|&(x, y)| Point::new(x, y)).collect();
PathData::single(SubPath::polyline(&points, true))
}
/// Geometric Patterns: black dots, stripes, checks, grids and shapes on a clear background.
fn geometric_patterns(d: &mut Document) {
let rect = |x0: f64, y0: f64, x1: f64, y1: f64| shapes::rectangle(Rect::new(x0, y0, x1, y1));
add(d, "Polka Dots", 12.0, 12.0, TileType::BrickByRow { offset: 0.5 }, vec![shapes::ellipse(Rect::new(3.0, 3.0, 9.0, 9.0))]);
add(d, "Fine Dots", 6.0, 6.0, TileType::Grid, vec![shapes::ellipse(Rect::new(2.0, 2.0, 4.0, 4.0))]);
add(d, "Vertical Stripes", 8.0, 8.0, TileType::Grid, vec![rect(0.0, 0.0, 3.0, 8.0)]);
add(d, "Horizontal Stripes", 8.0, 8.0, TileType::Grid, vec![rect(0.0, 0.0, 8.0, 3.0)]);
// A band along the tile's diagonal and the corners of the two bands beside it.
add(
d,
"Diagonal Stripes",
10.0,
10.0,
TileType::Grid,
vec![
polygon(&[(0.0, 0.0), (1.5, 0.0), (10.0, 8.5), (10.0, 10.0), (8.5, 10.0), (0.0, 1.5)]),
polygon(&[(8.5, 0.0), (10.0, 0.0), (10.0, 1.5)]),
polygon(&[(0.0, 8.5), (1.5, 10.0), (0.0, 10.0)]),
],
);
add(d, "Checkerboard", 10.0, 10.0, TileType::Grid, vec![rect(0.0, 0.0, 5.0, 5.0), rect(5.0, 5.0, 10.0, 10.0)]);
add(d, "Grid", 10.0, 10.0, TileType::Grid, vec![rect(0.0, 0.0, 10.0, 0.75), rect(0.0, 0.75, 0.75, 10.0)]);
add(d, "Diamonds", 10.0, 10.0, TileType::Grid, vec![polygon(&[(5.0, 1.0), (9.0, 5.0), (5.0, 9.0), (1.0, 5.0)])]);
add(d, "Triangles", 10.0, 10.0, TileType::BrickByRow { offset: 0.5 }, vec![polygon(&[(1.0, 9.0), (5.0, 1.0), (9.0, 9.0)])]);
add(d, "Crosses", 10.0, 10.0, TileType::Grid, vec![rect(4.0, 1.5, 6.0, 8.5), rect(1.5, 4.0, 8.5, 6.0)]);
add(d, "Zigzag", 10.0, 8.0, TileType::Grid, vec![polygon(&[(0.0, 5.0), (5.0, 1.0), (10.0, 5.0), (10.0, 7.0), (5.0, 3.0), (0.0, 7.0)])]);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn plus_signed_suffix_is_not_a_copy_number() {
let mut lib = Document::new(100.0, 100.0);
lib.patterns.push(PatternDef::new("Dots", vec![]));
for (other, expected) in [("Dots +2", None), ("Dots -2", None), ("Dots 2a", None), ("Dots ", None), ("Dots 2", Some("Dots 2"))] {
let mut d = Document::new(100.0, 100.0);
d.patterns.push(PatternDef::new(other, vec![]));
assert_eq!(existing_pattern(&d, &lib, "Dots").as_deref(), expected, "{other}");
}
}
}