mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 09:59:47 -06:00
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:
@@ -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)])]);
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
//! Swatch libraries (Window → Swatch Libraries, the Swatches panel's library button): read-only
|
||||
//! sets of swatches the library panel shows. The built-in libraries are computed in
|
||||
//! [`vectorcraft_color::libraries`]; User Defined ones are the library files in the user library
|
||||
//! folder ([`Libraries`]), and Other Library… loads more from files. `swatch.library.add` copies
|
||||
//! swatches into the document; `swatch.library.save` writes the document's ([`palette_io`]).
|
||||
//! [`vectorcraft_color::libraries`] and the pattern libraries in [`super::patternlib`]; User
|
||||
//! Defined ones are the library files in the user library folder ([`Libraries`]), and Other
|
||||
//! Library… loads more from files. `swatch.library.add` copies swatches (and a pattern swatch's
|
||||
//! pattern) into the document; `swatch.library.save` writes the document's ([`palette_io`]).
|
||||
//! `swatch.library.copyToUser` copies a loaded library into the user library folder.
|
||||
|
||||
use std::collections::HashSet;
|
||||
@@ -13,10 +14,12 @@ use vectorcraft_color::libraries::{BuiltinLibrary, GRADIENT_LIBRARIES, SWATCH_LI
|
||||
use vectorcraft_color::palette_io::{self, PaletteFormat};
|
||||
use vectorcraft_color::recolor::Palette;
|
||||
use vectorcraft_color::{Paint, Swatch, SwatchGroup, SwatchLibrary, default_swatches};
|
||||
use vectorcraft_doc::pattern::pattern_paint;
|
||||
|
||||
use super::fileio::{create_dir, extension, read_file, write_file, write_new_file};
|
||||
use super::menucmds::squash;
|
||||
use super::package::free_name;
|
||||
use super::patternlib;
|
||||
use super::swatch::{map_default_paints, map_swatch_links, str_list, swatch_json};
|
||||
use super::{Color, CommandSpec, Result, Session, always, bad, cmd, has_doc, switch_or, text_param};
|
||||
|
||||
@@ -27,7 +30,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Swatch Libraries",
|
||||
[],
|
||||
None,
|
||||
"{} → {libraries: [{id, name, category: \"builtIn\"|\"gradients\"|\"user\" (User Defined: the library files in the user library folder, rescanned now)|\"loaded\" (swatch.library.load), count, path?}], userFolder} every library the library panel opens, in menu order",
|
||||
"{} → {libraries: [{id, name, category: \"builtIn\"|\"gradients\"|\"patterns\"|\"user\" (User Defined: the library files in the user library folder, rescanned now)|\"loaded\" (swatch.library.load), count, path?}], userFolder} every library the library panel opens, in menu order",
|
||||
always,
|
||||
list
|
||||
),
|
||||
@@ -36,7 +39,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Swatch Library",
|
||||
[],
|
||||
None,
|
||||
"{library: id or name} → {id, name, category, swatches: [{name, kind, group, global, spot, color?, hex?, gradient?}] (as swatch.list), groups: [{name, swatches: [names]}]}",
|
||||
"{library: id or name} → {id, name, category, swatches: [{name, kind, group, global, spot, color?, hex?, gradient?, pattern?}] (as swatch.list), groups: [{name, swatches: [names]}]}",
|
||||
always,
|
||||
get
|
||||
),
|
||||
@@ -45,7 +48,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Add to Swatches",
|
||||
["Window", "Swatch Libraries"],
|
||||
None,
|
||||
"{library: id or name, names?: [swatch or colour group names in the library] (default: all of it; a group comes as a colour group, a single swatch ungrouped), apply?: \"fill\"|\"stroke\" (also apply the first one to the selection and the defaults, as paint.setFill / paint.setStroke), focus?: true (with apply: false keeps the active proxy)} copy library swatches into the document as one undo step. A swatch the document already has (same name and paint) isn't added again; a name taken by another swatch gets a number (\"Clay 1 2\") → {library, added: [names in the document], existing: [names already there], applied?: name}",
|
||||
"{library: id or name, names?: [swatch or colour group names in the library] (default: all of it; a group comes as a colour group, a single swatch ungrouped), apply?: \"fill\"|\"stroke\" (also apply the first one to the selection and the defaults, as paint.setFill / paint.setStroke), focus?: true (with apply: false keeps the active proxy)} copy library swatches into the document as one undo step. A swatch the document already has (same name and paint) isn't added again; a name taken by another swatch gets a number (\"Clay 1 2\"). A pattern swatch brings its pattern, as pasting art painted with it does: the same pattern the document has is used, and a name another pattern or swatch has gets a number → {library, added: [names in the document], existing: [names already there], applied?: name}",
|
||||
has_doc,
|
||||
add
|
||||
),
|
||||
@@ -380,7 +383,7 @@ pub(crate) fn stem(name: &str) -> &str {
|
||||
pub struct LibraryInfo {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
/// "builtIn", "gradients", "user" or "loaded".
|
||||
/// "builtIn", "gradients", "patterns", "user" or "loaded".
|
||||
pub category: &'static str,
|
||||
}
|
||||
|
||||
@@ -393,9 +396,9 @@ fn builtin_info((category, b): (&'static str, &BuiltinLibrary)) -> LibraryInfo {
|
||||
LibraryInfo { id: b.id.into(), name: b.name.into(), category }
|
||||
}
|
||||
|
||||
/// Every library, in menu order: built-in, gradients, User Defined, loaded.
|
||||
/// Every library, in menu order: built-in, gradients, patterns, User Defined, loaded.
|
||||
pub fn libraries(s: &Session) -> Vec<LibraryInfo> {
|
||||
builtins().map(builtin_info).chain(s.swatch_libraries.infos().cloned()).collect()
|
||||
builtins().map(builtin_info).chain(patternlib::infos()).chain(s.swatch_libraries.infos().cloned()).collect()
|
||||
}
|
||||
|
||||
/// Library `key` (an id, or a name in any case) with its info.
|
||||
@@ -406,6 +409,9 @@ pub fn library(s: &Session, key: &str) -> Option<(LibraryInfo, Arc<SwatchLibrary
|
||||
if let Some(b) = builtins().find(|(_, b)| b.id == key) {
|
||||
return Some((builtin_info(b), builtin_library(key)?));
|
||||
}
|
||||
if let Some(found) = patternlib::library(key) {
|
||||
return Some(found);
|
||||
}
|
||||
let id = libraries(s).into_iter().find(|l| l.name.eq_ignore_ascii_case(key))?.id;
|
||||
library(s, &id)
|
||||
}
|
||||
@@ -611,10 +617,29 @@ fn add(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
};
|
||||
let focus = switch_or(p, "focus", true, C)?;
|
||||
let picked = picked(&lib, &names_param(p, C)?, C)?;
|
||||
// Swatches the document already has (same name and paint) are reused, not added again.
|
||||
// Swatches the document already has (same name and paint; for a pattern swatch, a swatch of the
|
||||
// same pattern) are reused, not added again.
|
||||
let patterns = patternlib::library_doc(&info.id);
|
||||
let d = &s.doc()?.doc;
|
||||
let (existing, fresh): (Vec<_>, Vec<_>) = picked.iter().partition(|(_, w)| d.swatch(&w.name).is_some_and(|x| x.paint == w.paint));
|
||||
let existing: Vec<String> = existing.iter().map(|(_, w)| w.name.clone()).collect();
|
||||
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,
|
||||
}
|
||||
}
|
||||
_ => d.swatch(&w.name).is_some_and(|x| x.paint == w.paint).then(|| w.name.clone()),
|
||||
};
|
||||
// Library name → the name of the document's swatch.
|
||||
let mut existing: Vec<(String, String)> = vec![];
|
||||
let mut fresh = vec![];
|
||||
for &(group, w) in &picked {
|
||||
match there(w) {
|
||||
Some(name) => existing.push((w.name.clone(), name)),
|
||||
None => fresh.push((group, w)),
|
||||
}
|
||||
}
|
||||
let undo_before = s.doc()?.history.undo.len();
|
||||
// Library name → the name it got in the document.
|
||||
let added: Vec<(String, String)> = if fresh.is_empty() {
|
||||
@@ -627,6 +652,21 @@ fn add(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let before = d.swatch_groups.len();
|
||||
let mut placed: Vec<(&str, usize)> = vec![];
|
||||
for (group, w) in fresh {
|
||||
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 name = match patternlib::pattern_swatch(d, &target) {
|
||||
Some(name) => name,
|
||||
None => {
|
||||
let name = d.free_swatch_name(&target);
|
||||
d.swatches.push(Swatch { name: name.clone(), paint: pattern_paint(&target), global: false, spot: false });
|
||||
name
|
||||
}
|
||||
};
|
||||
out.push((w.name.clone(), name));
|
||||
continue;
|
||||
}
|
||||
let name = d.free_swatch_name(&w.name);
|
||||
let sw = Swatch { name: name.clone(), ..(*w).clone() };
|
||||
match group {
|
||||
@@ -652,9 +692,10 @@ fn add(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(out)
|
||||
})?
|
||||
};
|
||||
let mut out = json!({"library": info.id, "added": added.iter().map(|(_, n)| n).collect::<Vec<_>>(), "existing": existing});
|
||||
let names = |list: &[(String, String)]| list.iter().map(|(_, n)| n.clone()).collect::<Vec<_>>();
|
||||
let mut out = json!({"library": info.id, "added": names(&added), "existing": names(&existing)});
|
||||
if let (Some(cmd), Some((_, first))) = (apply, picked.first()) {
|
||||
let name = added.iter().find(|(l, _)| *l == first.name).map_or(first.name.clone(), |(_, n)| n.clone());
|
||||
let name = added.iter().chain(&existing).find(|(l, _)| *l == first.name).map_or(first.name.clone(), |(_, n)| n.clone());
|
||||
s.execute(cmd, &json!({"swatch": name, "focus": focus}))?;
|
||||
// Adding and applying undo together.
|
||||
squash(s, undo_before, "Add to Swatches");
|
||||
|
||||
Reference in New Issue
Block a user