Brushes: Window › Brush Libraries opens brush libraries whose brushes click or drag into the document's Brushes

Squashed from #4020 onto main.
This commit is contained in:
Taha-Mahmoodi
2026-10-26 15:17:30 +07:00
committed by bflatastic
parent a1ef364191
commit 0a241a7fdd
56 changed files with 2961 additions and 176 deletions
+502
View File
@@ -0,0 +1,502 @@
//! Brush libraries (Window → Brush Libraries, the Brushes panel's library button): read-only sets
//! of brushes the library panel shows. A library is a document holding the brushes with the
//! resources their art uses (patterns, symbols, swatches, graphic styles, images); saved, it is a
//! native `.vectorcraft` file, which File › Open also opens. The built-in libraries are generated
//! here ([`BUILTIN`]); User Defined ones are the `.vectorcraft` files in the user library folder
//! ([`Libraries`]), and Other Library… loads the brushes of any document VectorCraft opens.
//! `brush.addFromLibrary` copies brushes into the document; `brush.saveLibrary` writes the
//! document's.
use std::collections::HashSet;
use std::sync::{Arc, LazyLock};
use serde_json::{Value, json};
use vectorcraft_brush::{self as brush, ArtBrush, Brush, BrushKind, Calligraphic, Colorization, PatternBrush, PatternFit, Scatter, Variation};
use vectorcraft_color::{Color, Paint};
use vectorcraft_doc::{Appearance, AppearanceItem, Document, FillLayer, Node, NodeId, NodeKind, PatternDef, Symbol};
use vectorcraft_geom::{Affine, FillRule, PathData, Point, Rect, SubPath, shapes};
use super::brushsym::{apply_brush, brush_targets, roots, stroke_paint};
use super::clipboard::Clipboard;
use super::clipboard::resources::SwatchChoices;
use super::patterncmds::add_pattern;
use super::swatch::str_list;
use super::swatchlib::{FileData, LibraryFile, LibraryInfo, stem};
use super::*;
/// The extension of saved brush libraries: native documents.
pub const BRUSHES_EXT: &str = vectorcraft_format::EXTENSION;
pub fn specs() -> Vec<CommandSpec> {
vec![
cmd!(
query "brush.libraries",
"Brush Libraries",
[],
None,
"{} → {libraries: [{id, name, category: \"builtIn\"|\"user\" (User Defined: the .vectorcraft files in the user library folder, rescanned now)|\"loaded\" (brush.loadLibrary), count, path?}], userFolder} every brush library the library panel opens, in menu order",
always,
list
),
cmd!(
query "brush.library",
"Brush Library",
[],
None,
"{library: id or name} → {id, name, category, brushes: [{name, type}] (as brush.list)}",
always,
get
),
cmd!(
"brush.addFromLibrary",
"Add to Brushes",
["Window", "Brush Libraries"],
None,
"{library: id or name, name? | names?: [brush names in the library] (default: all of it), apply?: false (also apply the first one to paths, as brush.apply does), ids?|id? (the paths apply takes; default the selected ones), swatchConflict? (as edit.paste)} copy library brushes into the document's Brushes as one undo step (with the apply; named Apply Brush when no brush is added and it only applies one), with the resources their art uses (patterns, symbols, swatches, graphic styles, images). A brush the document already has (same name and definition) isn't added again; a name another brush has gets a number (\"Ribbon 2\"). The first one becomes the current brush → {library, names: [each brush's name in the document], added: [names added], existing: [names already there], applied: [ids of the paths it was applied to]}",
has_doc,
add
),
cmd!(
"brush.saveLibrary",
"Save Brush Library…",
["Window", "Brushes"],
None,
"{path?, names?: [brush names] (default: all), name?: library name (default: the document's), user?: false (save into the user library folder, listed under User Defined)} save the document's brushes, with the resources their art uses, as a brush library: a .vectorcraft document holding only them → {path, count, library?: id when saved to the user folder}; without path or user → {dataBase64: the file's bytes, count}",
has_doc,
save
),
cmd!(
"brush.loadLibrary",
"Other Library…",
["Window", "Brush Libraries"],
None,
"{path? | data?: file text | dataBase64?, name?: file name (default: the path's)} load the brushes of any document VectorCraft opens (see document.formats; one without brushes of its own has the default ones), a saved brush library included, for the library panel (Window → Brush Libraries lists it until the app quits; a file of the user library folder, or a file with the same bytes as one there, opens as that User Defined library) → {library: id, name, count}",
always,
load
),
]
}
/// A brush library: its name, its brushes (in panel order) and a document holding them with the
/// resources their art uses; the document has no art of its own.
#[derive(Clone, Debug)]
pub struct BrushLibrary {
pub name: String,
pub brushes: Vec<Brush>,
pub doc: Document,
}
impl Default for BrushLibrary {
fn default() -> Self {
Self { name: String::new(), brushes: vec![], doc: Document::new(100.0, 100.0) }
}
}
impl BrushLibrary {
pub fn len(&self) -> usize {
self.brushes.len()
}
pub fn is_empty(&self) -> bool {
self.brushes.is_empty()
}
pub fn brush(&self, name: &str) -> Option<&Brush> {
self.brushes.iter().find(|b| b.name == name)
}
/// Document `src`'s brushes `names` (empty: all of them, in panel order) as a library named
/// `name`: the brushes and every resource their art uses.
pub fn from_document(src: &Document, names: &[String], name: String) -> std::result::Result<Self, String> {
let all = brush::library(src);
if let Some(n) = names.iter().find(|n| !all.iter().any(|b| b.name == **n)) {
return Err(format!("no brush `{n}`"));
}
let picked = all.iter().filter(|b| names.is_empty() || names.contains(&b.name));
let clip = Clipboard::gather(src, picked.map(|b| stroke(&b.name)).collect());
let mut doc = clip.to_document();
for l in &mut doc.layers {
if let Some(ch) = Arc::make_mut(l).children_mut() {
ch.clear();
}
}
// The brushes picked and those their art paints with, in the panel order of the document
// they came from.
let brushes: Vec<Brush> = all.into_iter().filter(|b| clip.brushes.iter().any(|c| c.name == b.name)).collect();
brush::store(&mut doc, &brushes);
doc.title = name.clone();
Ok(Self { name, brushes, doc })
}
}
impl LibraryFile for BrushLibrary {
const EXTS: &'static [&'static str] = &[BRUSHES_EXT];
fn read(bytes: &[u8], stem: &str) -> std::result::Result<Self, String> {
let doc = vectorcraft_format::load(bytes).map_err(|e| e.to_string())?;
BrushLibrary::from_document(&doc, &[], stem.to_string())
}
fn name(&self) -> &str {
&self.name
}
}
/// User Defined and loaded brush libraries.
pub type Libraries = super::swatchlib::Libraries<BrushLibrary>;
/// A path stroked with brush `name` (what the resource gathering follows).
fn stroke(name: &str) -> Node {
let mut ap = Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 1.0);
if let Some(st) = ap.stroke_mut() {
st.brush = Some(name.into());
}
let line = SubPath::polyline(&[Point::new(0.0, 0.0), Point::new(100.0, 0.0)], false);
Node::path(NodeId(0), PathData::single(line), ap)
}
// ---------- built-in libraries ----------
/// A library generated in code.
pub struct BuiltinBrushLibrary {
pub id: &'static str,
pub name: &'static str,
make: fn() -> BrushLibrary,
}
/// The built-in brush libraries, in menu order: original brushes generated in code, one library
/// per kind of brush.
pub const BUILTIN: &[BuiltinBrushLibrary] = &[
BuiltinBrushLibrary { id: "art-ribbons", name: "Art Ribbons", make: art_ribbons },
BuiltinBrushLibrary { id: "calligraphic-nibs", name: "Calligraphic Nibs", make: calligraphic_nibs },
BuiltinBrushLibrary { id: "pattern-borders", name: "Pattern Borders", make: pattern_borders },
BuiltinBrushLibrary { id: "scatter-shapes", name: "Scatter Shapes", make: scatter_shapes },
];
/// Built-in library `id`, made once.
fn builtin_library(id: &str) -> Option<Arc<BrushLibrary>> {
static MADE: LazyLock<Vec<Arc<BrushLibrary>>> = LazyLock::new(|| BUILTIN.iter().map(|b| Arc::new((b.make)())).collect());
BUILTIN.iter().position(|b| b.id == id).and_then(|i| MADE.get(i).cloned())
}
/// Library `name` of `brushes`, `doc` holding the resources their art uses.
fn built(name: &str, mut doc: Document, brushes: Vec<Brush>) -> BrushLibrary {
brush::store(&mut doc, &brushes);
doc.title = name.into();
BrushLibrary { name: name.into(), brushes, doc }
}
fn filled(p: PathData, paint: Paint) -> Node {
Node::path(NodeId(0), p, Appearance { items: vec![AppearanceItem::Fill(FillLayer::new(paint))], ..Default::default() })
}
fn black(p: PathData) -> Node {
filled(p, Paint::solid(Color::BLACK))
}
fn group(children: Vec<Node>) -> Node {
Node::group(NodeId(0), children.into_iter().map(Arc::new).collect())
}
fn polygon(pts: &[(f64, f64)]) -> PathData {
let pts: Vec<Point> = pts.iter().map(|&(x, y)| Point::new(x, y)).collect();
PathData::single(SubPath::polyline(&pts, true))
}
/// A band along y = `f(x)` for x in 0..`len`, `thick` pt thick, in `n` steps.
fn band(len: f64, thick: f64, n: usize, f: impl Fn(f64) -> f64) -> PathData {
let xs: Vec<f64> = (0..=n).map(|i| len * i as f64 / n as f64).collect();
let mut pts: Vec<Point> = xs.iter().map(|&x| Point::new(x, f(x) - thick / 2.0)).collect();
pts.extend(xs.iter().rev().map(|&x| Point::new(x, f(x) + thick / 2.0)));
PathData::single(SubPath::polyline(&pts, true))
}
/// An even-odd ring of radii `outer` and `inner` around `c`.
fn ring(c: Point, outer: f64, inner: f64) -> Node {
let disc = |r: f64| shapes::ellipse(Rect::new(c.x - r, c.y - r, c.x + r, c.y + r));
let children = [disc(outer), disc(inner)].into_iter().map(|p| Arc::new(Node::path(NodeId(0), p, Appearance::default()))).collect();
let mut n = Node::new(NodeId(0), NodeKind::Compound { children, rule: FillRule::EvenOdd });
n.appearance = Appearance { items: vec![AppearanceItem::Fill(FillLayer::new(Paint::solid(Color::BLACK)))], ..Default::default() }.into();
n
}
fn art(name: &str, art: Node, colorization: Colorization) -> Brush {
Brush { name: name.into(), kind: BrushKind::Art(ArtBrush { art, colorization, ..Default::default() }) }
}
/// Art Ribbons: art brushes, 100 pt long, drawn here; Striped Ribbon is filled with the library's
/// own pattern, Pinstripes.
fn art_ribbons() -> BrushLibrary {
let mut doc = Document::new(100.0, 100.0);
let stripe = Node::path(doc.alloc_id(), shapes::rectangle(Rect::new(0.0, 0.0, 6.0, 1.5)), {
let mut a = Appearance::default();
a.items.push(AppearanceItem::Fill(FillLayer::new(Paint::solid(Color::from_hex("#2f80c9").unwrap_or(Color::BLACK)))));
a
});
add_pattern(&mut doc, PatternDef { tile: Rect::new(0.0, 0.0, 6.0, 3.0), ..PatternDef::new("Pinstripes", vec![Arc::new(stripe)]) });
let notched = polygon(&[(0.0, -3.0), (100.0, -3.0), (95.0, 0.0), (100.0, 3.0), (0.0, 3.0), (5.0, 0.0)]);
let double = group(vec![black(shapes::rectangle(Rect::new(0.0, -2.5, 100.0, -1.5))), black(shapes::rectangle(Rect::new(0.0, 1.5, 100.0, 2.5)))]);
let wave = band(100.0, 1.6, 64, |x| 2.5 * (x / 100.0 * 4.0 * std::f64::consts::TAU).sin());
// Round and thick at the start, tapering to a point.
let teardrop = {
let n = 48;
let half = |x: f64| 3.5 * (1.0 - x / 100.0).powf(0.8) * (x / 4.0).min(1.0).sqrt();
let xs = |i: usize| 100.0 * i as f64 / n as f64;
let mut pts: Vec<Point> = (0..=n).map(xs).map(|x| Point::new(x, -half(x))).collect();
pts.extend((0..n).rev().map(xs).map(|x| Point::new(x, half(x))));
PathData::single(SubPath::polyline(&pts, true))
};
let striped = {
let mut n =
filled(shapes::rectangle(Rect::new(0.0, -4.0, 100.0, 4.0)), Paint::Pattern { pattern: "Pinstripes".into(), xf: Default::default() });
n.appearance.set_stroke(Paint::solid(Color::from_hex("#1d4f7c").unwrap_or(Color::BLACK)));
n
};
let brushes = vec![
art("Notched Ribbon", black(notched), Colorization::Tints),
art("Double Line", double, Colorization::Tints),
art("Wave", black(wave), Colorization::Tints),
art("Teardrop", black(teardrop), Colorization::Tints),
art("Striped Ribbon", striped, Colorization::None),
];
built("Art Ribbons", doc, brushes)
}
fn nib(name: &str, angle: f64, roundness: f64, size: f64) -> Brush {
Brush { name: name.into(), kind: BrushKind::Calligraphic(Calligraphic { angle, roundness, size, ..Default::default() }) }
}
/// Calligraphic Nibs: pen nibs of several shapes; Pressure Pen's size follows the pen pressure.
fn calligraphic_nibs() -> BrushLibrary {
let pressure = Brush {
name: "Pressure Pen".into(),
kind: BrushKind::Calligraphic(Calligraphic {
angle: 30.0,
roundness: 60.0,
size: 5.0,
variation: [0.0, 0.0, 4.0],
modes: Some([Variation::Fixed, Variation::Fixed, Variation::Pressure]),
}),
};
let brushes = vec![
nib("Fine Nib", 45.0, 25.0, 2.0),
nib("Broad Nib", 45.0, 15.0, 9.0),
nib("Chisel", 0.0, 8.0, 6.0),
nib("Round Pen", 0.0, 100.0, 4.0),
pressure,
];
built("Calligraphic Nibs", Document::new(100.0, 100.0), brushes)
}
fn border(name: &str, side: Node, corner: Option<Node>, fit: PatternFit) -> Brush {
let kind = BrushKind::Pattern(PatternBrush { side, outer_corner: corner.clone(), inner_corner: corner, fit, ..Default::default() });
Brush { name: name.into(), kind }
}
/// An invisible rectangle that fixes a tile's width.
fn spacer(r: Rect) -> Node {
Node::path(NodeId(0), shapes::rectangle(r), Appearance::default())
}
/// Pattern Borders: pattern brushes of tiles drawn here; Beads has corner tiles.
fn pattern_borders() -> BrushLibrary {
let zigzag = black(polygon(&[(0.0, 1.4), (5.0, -2.6), (10.0, 1.4), (10.0, 2.6), (5.0, -1.4), (0.0, 2.6)]));
let beads = group(vec![black(shapes::rectangle(Rect::new(0.0, -0.3, 8.0, 0.3))), black(shapes::ellipse(Rect::new(1.5, -2.5, 6.5, 2.5)))]);
let dash_dot = group(vec![
spacer(Rect::new(0.0, -1.0, 12.0, 1.0)),
black(shapes::rounded_rectangle(Rect::new(0.5, -0.75, 6.5, 0.75), 0.75)),
black(shapes::ellipse(Rect::new(8.5, -1.0, 10.5, 1.0))),
]);
let scallops = black(band(10.0, 1.0, 20, |x| -3.0 * (x / 10.0 * std::f64::consts::PI).sin()));
let brushes = vec![
border("Zigzag", zigzag, None, PatternFit::Stretch),
border("Beads", beads, Some(ring(Point::ZERO, 3.5, 1.5)), PatternFit::Stretch),
border("Dash Dot", dash_dot, None, PatternFit::AddSpace),
border("Scallops", scallops, None, PatternFit::Stretch),
];
built("Pattern Borders", Document::new(100.0, 100.0), brushes)
}
fn scatter(name: &str, art: Node, s: Scatter) -> Brush {
Brush { name: name.into(), kind: BrushKind::Scatter(Scatter { art, ..s }) }
}
/// Scatter Shapes: scatter brushes of shapes drawn here; Sparkles scatters the library's own
/// symbol, Sparkle.
fn scatter_shapes() -> BrushLibrary {
let mut doc = Document::new(100.0, 100.0);
// A four-pointed gold sparkle in the ±10 pt box symbol instances assume.
let points: Vec<Point> = (0..8)
.map(|i| {
let r = if i % 2 == 0 { 10.0 } else { 2.5 };
let a = std::f64::consts::FRAC_PI_4 * i as f64;
Point::new(r * a.cos(), r * a.sin())
})
.collect();
let gold = Color::from_hex("#e0a526").unwrap_or(Color::BLACK);
let sparkle = Node::path(doc.alloc_id(), PathData::single(SubPath::polyline(&points, true)), {
let mut a = Appearance::default();
a.items.push(AppearanceItem::Fill(FillLayer::new(Paint::solid(gold))));
a
});
doc.symbols.push(Symbol { name: "Sparkle".into(), art: Arc::new(sparkle) });
let instance = Node::new(NodeId(0), NodeKind::SymbolInstance { symbol: "Sparkle".into(), xf: Affine::scale(0.3) });
let random = |a: f64, b: f64| (a, b);
let base = Scatter { colorization: Colorization::Tints, ..Default::default() };
let brushes = vec![
scatter(
"Stars",
black(shapes::star(Point::ZERO, 4.0, 1.7, 5, 0.0)),
Scatter {
size: random(70.0, 130.0),
spacing: random(150.0, 220.0),
scatter: random(-80.0, 80.0),
rotation: random(-180.0, 180.0),
..base.clone()
},
),
scatter("Rings", ring(Point::ZERO, 2.5, 1.5), Scatter { spacing: (140.0, 140.0), ..base.clone() }),
scatter(
"Triangles",
black(shapes::polygon(Point::ZERO, 3.0, 3, 0.0)),
Scatter { spacing: (120.0, 120.0), rotation: (90.0, 90.0), rotation_relative_to_path: true, ..base.clone() },
),
scatter(
"Sparkles",
instance,
Scatter {
size: random(60.0, 140.0),
spacing: random(110.0, 180.0),
scatter: random(-60.0, 60.0),
rotation: random(-45.0, 45.0),
colorization: Colorization::None,
..base
},
),
];
built("Scatter Shapes", doc, brushes)
}
// ---------- commands ----------
fn builtin_info(b: &BuiltinBrushLibrary) -> LibraryInfo {
LibraryInfo { id: b.id.into(), name: b.name.into(), category: "builtIn" }
}
/// Every brush library, in menu order: built-in, User Defined, loaded.
pub fn libraries(s: &Session) -> Vec<LibraryInfo> {
BUILTIN.iter().map(builtin_info).chain(s.brush_libraries.infos().cloned()).collect()
}
/// Library `key` (an id, or a name in any case) with its info.
pub fn library(s: &Session, key: &str) -> Option<(LibraryInfo, Arc<BrushLibrary>)> {
if let Some(found) = s.brush_libraries.get(key) {
return Some(found);
}
if let Some(b) = BUILTIN.iter().find(|b| b.id == key) {
return Some((builtin_info(b), builtin_library(key)?));
}
let id = libraries(s).into_iter().find(|l| l.name.eq_ignore_ascii_case(key))?.id;
library(s, &id)
}
fn library_param(s: &Session, p: &Value, cmd: &str) -> Result<(LibraryInfo, Arc<BrushLibrary>)> {
let key = str_param(p, "library").ok_or_else(|| bad(cmd, "missing `library` (see brush.libraries)"))?;
library(s, key).ok_or_else(|| bad(cmd, format!("no brush library `{key}` (see brush.libraries)")))
}
fn list(s: &mut Session, _: &Value) -> Result<Value> {
s.brush_libraries.rescan();
let libs: Vec<Value> = libraries(s)
.into_iter()
.map(|l| {
let count = library(s, &l.id).map_or(0, |(_, lib)| lib.len());
json!({"id": l.id, "name": l.name, "category": l.category, "count": count, "path": s.brush_libraries.path(&l.id)})
})
.collect();
Ok(json!({ "libraries": libs, "userFolder": s.brush_libraries.user_dir() }))
}
fn get(s: &mut Session, p: &Value) -> Result<Value> {
let (info, lib) = library_param(s, p, "brush.library")?;
let brushes: Vec<Value> = lib.brushes.iter().map(|b| json!({"name": b.name, "type": b.kind.type_id()})).collect();
Ok(json!({"id": info.id, "name": info.name, "category": info.category, "brushes": brushes}))
}
fn add(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "brush.addFromLibrary";
let (info, lib) = library_param(s, p, C)?;
let mut names = str_list(p, "names");
names.extend(str_param(p, "name").map(str::to_string));
if let Some(n) = names.iter().find(|n| lib.brush(n).is_none()) {
return Err(bad(C, format!("no brush `{n}` in `{}`", lib.name)));
}
let mut picked: Vec<String> = if names.is_empty() { lib.brushes.iter().map(|b| b.name.clone()).collect() } else { names };
// Each once, in the order asked for.
let mut seen = HashSet::new();
picked.retain(|n| seen.insert(n.clone()));
let apply = bool_or(p, "apply", false);
if apply && picked.is_empty() {
return Err(bad(C, format!("`{}` has no brushes to apply", lib.name)));
}
let choices = SwatchChoices::parse(C, p)?;
let clip = Clipboard::gather(&lib.doc, picked.iter().map(|n| stroke(n)).collect());
// The typing `s.edit` would settle first is settled before the probe below, so the document
// the edit installs is the one it starts from.
if !s.previewing && super::typing_in_progress(s) {
super::settle_interaction(s)?;
}
let st = s.doc()?;
let before: HashSet<String> = brush::library(&st.doc).into_iter().map(|b| b.name).collect();
// A library brush's name in the document: its own, or the one the import gave it.
let target = |renamed: &[Value], n: &str| -> String {
let to = renamed.iter().find(|r| r["kind"] == "brush" && r["from"] == n).and_then(|r| r["to"].as_str());
to.unwrap_or(n).to_string()
};
// Where the brushes land, worked out on a copy that the edit then installs: nothing to add and
// nothing to apply is no edit, and a click that only applies brushes the document has is
// named for that.
let mut probe = (*st.doc).clone();
let imported = clip.import_into(&mut probe, &choices, false);
let doc_names: Vec<String> = picked.iter().map(|n| target(&imported.renamed, n)).collect();
let paths = if apply { brush_targets(&st.doc, &roots(s, p)?) } else { vec![] };
let paint = stroke_paint(s);
if imported.added > 0 || !paths.is_empty() {
let label = if imported.added > 0 { "Add to Brushes" } else { "Apply Brush" };
s.edit(label, |d, _| {
*d = probe;
if let Some(first) = doc_names.first() {
d.unknown.insert(brush::CURRENT_KEY.into(), json!(first));
apply_brush(d, &paths, first, &paint);
}
Ok(())
})?;
} else if let Some(first) = doc_names.first() {
s.execute("brush.setCurrent", &json!({ "name": first }))?;
}
let (added, existing): (Vec<&String>, Vec<&String>) = doc_names.iter().partition(|n| !before.contains(*n));
let applied: Vec<u64> = paths.iter().map(|id| id.0).collect();
Ok(json!({"library": info.id, "names": doc_names, "added": added, "existing": existing, "applied": applied}))
}
fn save(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "brush.saveLibrary";
let st = s.doc()?;
let name = str_param(p, "name").map(str::trim).filter(|n| !n.is_empty()).map_or_else(|| stem(&st.title()).to_string(), str::to_string);
let lib = BrushLibrary::from_document(&st.doc, &str_list(p, "names"), name).map_err(|e| bad(C, e))?;
if lib.is_empty() {
return Err(bad(C, "the document has no brushes to save"));
}
let mut out = json!({ "count": lib.len() });
let bytes = vectorcraft_format::save_file(&lib.doc);
s.brush_libraries.write(p, &lib.name, BRUSHES_EXT, FileData::Binary(bytes), &mut out, C)?;
Ok(out)
}
fn load(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "brush.loadLibrary";
let (info, lib) = s.brush_libraries.load(p, C, |bytes, file| {
let loaded = super::fileio::load(file, bytes).map_err(|e| bad(C, e.to_string()))?;
if loaded.preview_only {
return Err(bad(C, "only this Affinity file's embedded preview could be read; use File › Open to see it with its warning"));
}
BrushLibrary::from_document(&loaded.doc, &[], stem(file).to_string()).map_err(|e| bad(C, e))
})?;
Ok(json!({"library": info.id, "name": info.name, "count": lib.len()}))
}
+25 -15
View File
@@ -249,7 +249,7 @@ fn ids_json(ids: &[NodeId]) -> Value {
Value::Array(ids.iter().map(|i| Value::from(i.0)).collect())
}
fn roots(s: &Session, p: &Value) -> Result<Vec<NodeId>> {
pub(crate) fn roots(s: &Session, p: &Value) -> Result<Vec<NodeId>> {
match ids_param(p, "ids").or_else(|| id_param(p, "id").map(|i| vec![i])) {
Some(mut v) => {
// Explicit ids: only existing, non-layer objects, none inside another of them.
@@ -269,7 +269,7 @@ fn name_param<'a>(p: &'a Value, cmd: &str) -> Result<&'a str> {
// ---------- brushes ----------
/// Brushable nodes (paths and compound paths) under `ids`.
fn brush_targets(d: &Document, ids: &[NodeId]) -> Vec<NodeId> {
pub(crate) fn brush_targets(d: &Document, ids: &[NodeId]) -> Vec<NodeId> {
let mut out = vec![];
for id in ids {
let Some(n) = d.node(*id) else { continue };
@@ -302,6 +302,27 @@ fn brush_selection_art(s: &mut Session, p: &Value) -> Result<Value> {
Ok(json!({ "art": art.map(|n| serde_json::to_value(&n).unwrap_or_default()) }))
}
/// The stroke paint a brush gives paths without one: the current stroke colour, else black.
pub(crate) fn stroke_paint(s: &Session) -> Paint {
if s.paint.stroke.is_none() { Paint::solid(Color::BLACK) } else { s.paint.stroke.clone() }
}
/// Paint paths `ids` of `d` with brush `name` (a path without a stroke takes `stroke`).
pub(crate) fn apply_brush(d: &mut Document, ids: &[NodeId], name: &str, stroke: &Paint) {
for id in ids {
let Some(n) = d.node_mut(*id) else { continue };
if n.appearance.stroke().is_none_or(|st| st.paint.is_none()) {
n.appearance.set_stroke(stroke.clone());
}
if let Some(st) = n.appearance.stroke_mut() {
if st.width <= 0.0 {
st.width = 1.0;
}
st.brush = Some(name.to_string());
}
}
}
fn brush_apply(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "brush.apply";
let name = name_param(p, C)?.to_string();
@@ -309,21 +330,10 @@ fn brush_apply(s: &mut Session, p: &Value) -> Result<Value> {
return Err(bad(C, format!("no brush named `{name}`")));
}
let ids = brush_targets(&s.doc()?.doc, &roots(s, p)?);
let stroke = if s.paint.stroke.is_none() { Paint::solid(Color::BLACK) } else { s.paint.stroke.clone() };
let stroke = stroke_paint(s);
s.edit("Apply Brush", |d, _| {
d.unknown.insert(brush::CURRENT_KEY.into(), json!(name));
for id in &ids {
let Some(n) = d.node_mut(*id) else { continue };
if n.appearance.stroke().is_none_or(|st| st.paint.is_none()) {
n.appearance.set_stroke(stroke.clone());
}
if let Some(st) = n.appearance.stroke_mut() {
if st.width <= 0.0 {
st.width = 1.0;
}
st.brush = Some(name.clone());
}
}
apply_brush(d, &ids, &name, &stroke);
Ok(())
})?;
Ok(json!({ "ids": ids_json(&ids) }))
+1
View File
@@ -23,6 +23,7 @@ use vectorcraft_geom::Rect;
pub use flavours::{AI, BITMAP, EMF, EPS, FILE_HEAD, Flavour, PASTE_ORDER, PDF, PNG, SVG, TEXT, file_flavour, is_address, names_a_picture};
pub(crate) use resources::SwatchChoices;
pub use resources::{ArtResources, art_resources};
use super::*;
use crate::DocState;
+150 -15
View File
@@ -2,7 +2,7 @@
//!
//! Objects refer to resources by name (image blobs, symbols, patterns, swatches, character and
//! paragraph styles, brushes) and to graphic styles by id. Copy follows those references, also
//! through the symbols, patterns and styles it finds, and keeps what they lead to.
//! through the symbols, patterns, styles and brush art it finds, and keeps what they lead to.
//!
//! Pasting matches each resource by name: an identical one in the document is used, a missing one
//! is added, and a different one of the same name is added under a free name, which the pasted
@@ -13,6 +13,7 @@
//! the document's Spot Colors option says. Pasting back into the document the objects came from
//! uses its resources as they are now.
use std::cell::OnceCell;
use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc;
@@ -147,10 +148,28 @@ struct Defs<'a> {
styles: &'a [GraphicStyle],
char_styles: &'a [TextStyleDef],
para_styles: &'a [TextStyleDef],
brushes: Brushes<'a>,
}
/// The brushes references lead to: a document's library, read the first time a brush is used
/// (`vectorcraft_brush::library` parses it), or the clipboard's own.
enum Brushes<'a> {
Lazy(&'a Document, &'a OnceCell<Vec<Brush>>),
Ready(&'a [Brush]),
}
impl<'a> Brushes<'a> {
fn get(&self) -> &'a [Brush] {
match *self {
Brushes::Lazy(d, cell) => cell.get_or_init(|| vectorcraft_brush::library(d)),
Brushes::Ready(list) => list,
}
}
}
impl<'a> Defs<'a> {
fn of(d: &'a Document) -> Self {
/// `d`'s definitions; `brushes` holds its brush library once a brush is used.
fn of(d: &'a Document, brushes: &'a OnceCell<Vec<Brush>>) -> Self {
Self {
swatches: d.swatches_iter().collect(),
symbols: &d.symbols,
@@ -158,6 +177,7 @@ impl<'a> Defs<'a> {
styles: &d.graphic_styles,
char_styles: &d.char_styles,
para_styles: &d.para_styles,
brushes: Brushes::Lazy(d, brushes),
}
}
}
@@ -180,9 +200,9 @@ pub(super) struct Used {
}
impl Used {
fn of<'a>(defs: &Defs<'a>, nodes: &'a [Node]) -> Self {
fn of<'a>(defs: &Defs<'a>, nodes: impl IntoIterator<Item = &'a Node>) -> Self {
let mut used = Used::default();
let mut todo: Vec<Source<'a>> = nodes.iter().map(Source::Node).collect();
let mut todo: Vec<Source<'a>> = nodes.into_iter().map(Source::Node).collect();
while let Some(src) = todo.pop() {
let mut found = |r: Ref<'_>| used.note(defs, r, &mut todo);
match src {
@@ -224,6 +244,8 @@ impl Used {
Res::Pattern => todo.extend(defs.patterns.iter().filter(|p| p.name == name).flat_map(|p| p.art.iter().map(|a| Source::Node(a)))),
Res::CharStyle => todo.extend(defs.char_styles.iter().filter(|s| s.name == name).map(|s| Source::Attrs(&s.attrs))),
Res::ParaStyle => todo.extend(defs.para_styles.iter().filter(|s| s.name == name).map(|s| Source::Attrs(&s.attrs))),
// A scatter or art brush's art, a pattern brush's tiles.
Res::Brush => todo.extend(defs.brushes.get().iter().filter(|b| b.name == name).flat_map(|b| b.art().into_iter().map(Source::Node))),
_ => {}
}
}
@@ -242,6 +264,80 @@ impl Used {
}
}
/// What a preview of some art shows besides the art itself ([`art_resources`]).
pub struct ArtResources {
/// Changes when any resource the art uses changes, and only then.
pub key: u64,
/// The art and image bytes counted in `key` by their address, held weakly: their allocations
/// stay theirs (no other art can take an address) for as long as the key is kept.
_kept: (Vec<std::sync::Weak<Node>>, Vec<std::sync::Weak<Vec<u8>>>),
}
/// The resources `art` uses in `doc`, through the definitions they lead to as Copy follows them
/// ([`Used`]): images, symbols, patterns (tile type, overlap and spacing too), swatches (linked
/// colours, tints, gradient swatches; colour groups' included), character and paragraph styles,
/// brushes and graphic styles, and the document's Spot Colors option. Art and image bytes count
/// by identity, the other definitions by their saved form: working the key out walks `art` and
/// the definitions it leads to, and never reads an image's bytes.
pub fn art_resources<'a>(doc: &'a Document, art: impl IntoIterator<Item = &'a Node>) -> ArtResources {
use std::hash::{Hash, Hasher};
let library = OnceCell::new();
let defs = Defs::of(doc, &library);
// The art's references shortened to the brush library's life.
let mut nodes: Vec<&Node> = vec![];
for n in art {
nodes.push(n);
}
let used = Used::of(&defs, nodes);
let mut h = std::collections::hash_map::DefaultHasher::new();
let mut kept = (vec![], vec![]);
fn saved(h: &mut impl Hasher, v: &impl serde::Serialize) {
serde_json::to_vec(v).unwrap_or_default().hash(h);
}
fn node(h: &mut impl Hasher, kept: &mut Vec<std::sync::Weak<Node>>, a: &Arc<Node>) {
(Arc::as_ptr(a) as usize).hash(h);
kept.push(Arc::downgrade(a));
}
for (res, names) in &used.names {
for name in names {
(*res as u8, name).hash(&mut h);
match res {
Res::Image => {
if let Some(b) = doc.images.get(name) {
b.mime.hash(&mut h);
for bytes in std::iter::once(&b.bytes).chain(&b.proxy) {
(Arc::as_ptr(bytes) as usize).hash(&mut h);
kept.1.push(Arc::downgrade(bytes));
}
}
}
Res::Symbol => doc.symbols.iter().filter(|s| s.name == *name).for_each(|s| node(&mut h, &mut kept.0, &s.art)),
Res::Pattern => {
for p in doc.patterns.iter().filter(|p| p.name == *name) {
saved(&mut h, &PatternDef { art: vec![], ..p.clone() });
p.art.iter().for_each(|a| node(&mut h, &mut kept.0, a));
}
}
Res::CharStyle => doc.char_styles.iter().filter(|s| s.name == *name).for_each(|s| saved(&mut h, s)),
Res::ParaStyle => doc.para_styles.iter().filter(|s| s.name == *name).for_each(|s| saved(&mut h, s)),
Res::Brush => defs.brushes.get().iter().filter(|b| b.name == *name).for_each(|b| saved(&mut h, b)),
// Swatches below (tints count too); graphic styles go by id.
Res::Swatch | Res::GraphicStyle => {}
}
}
}
for w in doc.swatches_iter().filter(|w| used.carries(w) || used.has(Res::Swatch, &w.name)) {
saved(&mut h, w);
}
for g in doc.graphic_styles.iter().filter(|g| g.id != 0 && used.styles.contains(&g.id)) {
saved(&mut h, g);
}
if used.names.contains_key(&Res::Swatch) {
doc.spot_use_lab.hash(&mut h);
}
ArtResources { key: h.finish(), _kept: kept }
}
/// A resource list entry known by its name.
trait Named: Clone {
fn name(&self) -> &str;
@@ -469,6 +565,26 @@ fn same_art(r: &Relink, ours: &[Arc<Node>], theirs: &[Arc<Node>]) -> bool {
ours.len() == theirs.len() && ours.iter().zip(theirs).all(|(a, b)| relinked(&unlinked, a) == relinked(r, b))
}
/// Is `ours` (a document brush) brush `theirs` (a pasted one) once the art of that is relinked by
/// `r`? Graphic style links don't count, as in [`same_art`].
fn same_brush(r: &Relink, ours: &Brush, theirs: &Brush) -> bool {
let relinked = |r: &Relink, b: &Brush| {
let mut b = b.clone();
b.art_mut().into_iter().for_each(|a| r.node(a));
b
};
relinked(&Relink::default(), ours) == relinked(r, theirs)
}
/// Does brush `b`'s art show symbols or patterns?
fn shows_definitions(b: &Brush) -> bool {
let mut found = false;
for a in b.art() {
node_refs(a, &mut |r| found |= matches!(r, Ref::Name(Res::Symbol | Res::Pattern, _)));
}
found
}
/// What pasting did to the document's resources (part of the paste result).
#[derive(Debug, Default)]
pub(crate) struct Imported {
@@ -501,11 +617,13 @@ impl Imported {
impl Clipboard {
/// `nodes` (from `src`) and the resources of `src` they use.
pub(crate) fn gather(src: &Document, nodes: Vec<Node>) -> Self {
let used = Used::of(&Defs::of(src), &nodes);
let library = OnceCell::new();
let used = Used::of(&Defs::of(src, &library), &nodes);
let pick = |r: Res, list: &[TextStyleDef]| list.iter().filter(|s| used.has(r, &s.name)).cloned().collect::<Vec<_>>();
let brushes = match used.names.get(&Res::Brush) {
Some(names) => vectorcraft_brush::library(src).into_iter().filter(|b| names.contains(&b.name)).collect(),
None => vec![],
// Read when a brush was used.
let brushes = match (used.names.get(&Res::Brush), library.get()) {
(Some(names), Some(library)) => library.iter().filter(|b| names.contains(&b.name)).cloned().collect(),
_ => vec![],
};
Self {
images: src.images.iter().filter(|(k, _)| used.has(Res::Image, k)).map(|(k, b)| (k.clone(), b.clone())).collect(),
@@ -530,6 +648,7 @@ impl Clipboard {
styles: &self.graphic_styles,
char_styles: &self.char_styles,
para_styles: &self.para_styles,
brushes: Brushes::Ready(&self.brushes),
};
Used::of(&defs, &self.nodes)
}
@@ -549,8 +668,10 @@ impl Clipboard {
let used = self.used();
let mut out = Imported::default();
self.import_images(d, &used, same_doc, &mut out);
self.import_brushes(d, &used, same_doc, &mut out);
self.import_swatches(d, &used, choices, same_doc, &mut out);
// Brushes whose art shows no symbols or patterns, before the symbols and patterns whose art
// they may brush; the others once those are in.
let mut added_brushes = self.import_brushes(d, &used, same_doc, false, &mut out);
// Character and paragraph styles (their colours relinked first).
for (r, mine, theirs) in [(Res::CharStyle, &self.char_styles, &mut d.char_styles), (Res::ParaStyle, &self.para_styles, &mut d.para_styles)] {
for def in mine.iter().filter(|s| used.has(r, &s.name)) {
@@ -583,6 +704,7 @@ impl Clipboard {
added_patterns.extend(p.new_name(&def.name));
out.placed(Res::Pattern, &def.name, &p);
}
added_brushes.extend(self.import_brushes(d, &used, same_doc, true, &mut out));
self.import_graphic_styles(d, &used, same_doc, &mut out);
for s in d.symbols.iter_mut().filter(|s| added_symbols.contains(&s.name)) {
out.relink.node(Arc::make_mut(&mut s.art));
@@ -592,6 +714,13 @@ impl Clipboard {
out.relink.node(Arc::make_mut(a));
}
}
if !added_brushes.is_empty() {
let mut lib = vectorcraft_brush::library(d);
for b in lib.iter_mut().filter(|b| added_brushes.contains(&b.name)) {
b.art_mut().into_iter().for_each(|a| out.relink.node(a));
}
vectorcraft_brush::store(d, &lib);
}
out
}
@@ -607,20 +736,26 @@ impl Clipboard {
}
}
fn import_brushes(&self, d: &mut Document, used: &Used, same_doc: bool, out: &mut Imported) {
let brushes: Vec<&Brush> = self.brushes.iter().filter(|b| used.has(Res::Brush, &b.name)).collect();
/// The brushes whose art shows symbols or patterns (`late`), or the others: each one the
/// document has (its art compared once relinked) is used, the others are added. → the names of
/// those added, whose art is relinked at the end.
fn import_brushes(&self, d: &mut Document, used: &Used, same_doc: bool, late: bool, out: &mut Imported) -> Vec<String> {
let brushes: Vec<&Brush> = self.brushes.iter().filter(|b| used.has(Res::Brush, &b.name) && shows_definitions(b) == late).collect();
if brushes.is_empty() {
return;
return vec![];
}
let mut lib = vectorcraft_brush::library(d);
let before = lib.len();
let mut added = vec![];
for b in brushes {
let p = place(&mut lib, b, same_doc, PartialEq::eq);
let r = &out.relink;
let p = place(&mut lib, b, same_doc, |ours, theirs| same_brush(r, ours, theirs));
added.extend(p.new_name(&b.name));
out.placed(Res::Brush, &b.name, &p);
}
if lib.len() != before {
if !added.is_empty() {
vectorcraft_brush::store(d, &lib);
}
added
}
/// Global and spot colours (a name conflict merges or adds as `choices` say), then the tint
+2
View File
@@ -3,6 +3,7 @@
pub(crate) mod appearance;
mod assets;
mod attributes;
pub mod brushlib;
mod brushsym;
mod buildcmds;
pub mod clipboard;
@@ -271,6 +272,7 @@ pub fn command_specs() -> &'static [CommandSpec] {
v.extend(flatten::specs());
v.extend(stylelib::specs());
v.extend(symbollib::specs());
v.extend(brushlib::specs());
v.extend(library::specs());
v.extend(expand::specs());
v.extend(attributes::specs());