Swatch libraries: Patterns › Geometric Patterns adds a pattern with its swatch

Window › Swatch Libraries and the Swatches panel's library menu get a
Patterns submenu with Geometric Patterns, a built-in library of eleven
black pattern swatches drawn in code (dots, stripes, a checkerboard, a
grid, diamonds, triangles, crosses and a zigzag).

- Clicking a pattern in its library panel adds the pattern and its
  swatch to the document and applies it to the active fill or stroke
  (Alt: the other one), as one undo step. A second click reuses the
  swatch; a different pattern under the same name keeps its name and
  the library's pattern comes in numbered.
- swatch.library.list lists the pattern libraries after the gradient
  libraries (category "patterns"), and swatch.library.add brings a
  pattern swatch's pattern as pasting art painted with it does.
- The library panel draws each pattern tile from the library's pattern.
- "Geometric Patterns" is translated in every catalog.
This commit is contained in:
dotCooCoo
2026-11-02 12:04:02 +07:00
committed by bflatastic
parent 65dcc83bf1
commit cb7df83217
38 changed files with 355 additions and 24 deletions
+135
View File
@@ -0,0 +1,135 @@
//! 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 pattern `name` (a library panel draws its pattern
/// swatches from it). Pattern names are unique across the built-in libraries.
pub fn pattern_source(name: &str) -> Option<Arc<Document>> {
PATTERN_LIBRARIES.iter().filter_map(|b| library_doc(b.id)).find(|d| d.pattern(name).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`, and whether anything was added.
pub(crate) fn import(d: &mut Document, lib: &Document, name: &str) -> (String, bool) {
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)
}
/// The name of the first swatch of `d` that paints with pattern `pattern`.
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())
}
/// 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)])]);
}