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
+3 -1
View File
@@ -1070,13 +1070,15 @@ fn main() -> std::process::ExitCode {
}
// Linux may not map the window before the first frames, so a saved maximize is retried then (#3635).
let pending_maximize = cfg!(target_os = "linux") && app.ui.window.as_ref().is_some_and(|g| g.maximized);
// User Defined swatch, graphic style and symbol libraries live next to the preferences.
// User Defined swatch, graphic style, symbol and brush libraries live next to the preferences.
let swatches = prefs_path().and_then(|p| Some(p.parent()?.join("Swatches").to_string_lossy().to_string()));
app.session.swatch_libraries.set_user_dir(swatches);
let styles = prefs_path().and_then(|p| Some(p.parent()?.join("Graphic Styles").to_string_lossy().to_string()));
app.session.style_libraries.set_user_dir(styles);
let symbols = prefs_path().and_then(|p| Some(p.parent()?.join("Symbols").to_string_lossy().to_string()));
app.session.symbol_libraries.set_user_dir(symbols);
let brushes = prefs_path().and_then(|p| Some(p.parent()?.join("Brushes").to_string_lossy().to_string()));
app.session.brush_libraries.set_user_dir(brushes);
// With the User Defined libraries listed, the persistent library panels reopen.
vectorcraft_ui_egui::panels::library_panel::restore(&mut app);
// So do the Libraries panel's libraries; runs without preferences (agents' test
+12
View File
@@ -476,6 +476,18 @@ impl Brush {
}
}
/// [`Brush::art`], to change (relink the resources it uses).
pub fn art_mut(&mut self) -> Vec<&mut Node> {
match &mut self.kind {
BrushKind::Scatter(s) => vec![&mut s.art],
BrushKind::Art(a) => vec![&mut a.art],
BrushKind::Pattern(p) => std::iter::once(&mut p.side)
.chain([&mut p.outer_corner, &mut p.inner_corner, &mut p.start, &mut p.end].into_iter().flatten())
.collect(),
BrushKind::Calligraphic(_) | BrushKind::Bristle(_) => vec![],
}
}
/// A brush definition from JSON (a stored library entry or a command's params), its values
/// kept within their ranges ([`Brush::sanitize`]). A Hue Shift colorization without a `key`
/// takes the color the art's fills and strokes use most (the first of [`art_colors`]), or
+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());
+5
View File
@@ -1429,6 +1429,8 @@ pub struct Session {
pub style_libraries: cmd::stylelib::Libraries,
/// User Defined and loaded symbol libraries (Window → Symbol Libraries); not saved.
pub symbol_libraries: cmd::symbollib::Libraries,
/// User Defined and loaded brush libraries (Window → Brush Libraries); not saved.
pub brush_libraries: cmd::brushlib::Libraries,
/// The Libraries panel's libraries of graphics, colours and text styles (`library.*`).
pub libraries: cmd::library::Libraries,
/// Where the saved PDF presets are kept, one file each; `None`: only for the session
@@ -1588,6 +1590,7 @@ impl Session {
freeform_point: None,
style_libraries: Default::default(),
symbol_libraries: Default::default(),
brush_libraries: Default::default(),
libraries: Default::default(),
pdf_preset_dir: None,
profile_dir: None,
@@ -2605,6 +2608,8 @@ mod tests_blendopts;
#[cfg(test)]
mod tests_blendspine;
#[cfg(test)]
mod tests_brushlib;
#[cfg(test)]
mod tests_brushsym;
#[cfg(test)]
mod tests_build;
+327
View File
@@ -0,0 +1,327 @@
//! Brush libraries: the built-in libraries, saving a document's brushes as a library (User
//! Defined, or a file), reopening it and adding its brushes to another document (the patterns and
//! symbols their art uses along, one undo step, deduped, renamed on a clash, applied), other
//! documents' brushes, and files that can't be read.
use serde_json::{Value, json};
use vectorcraft_brush::BrushKind;
use vectorcraft_doc::{NodeId, NodeKind};
use super::*;
fn session() -> Session {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
s
}
fn run(s: &mut Session, id: &str, p: Value) -> Value {
s.execute(id, &p).unwrap_or_else(|e| panic!("{id} {p}: {e}"))
}
fn doc(s: &Session) -> &vectorcraft_doc::Document {
&s.doc().unwrap().doc
}
fn undo_len(s: &Session) -> usize {
s.doc().unwrap().history.undo.len()
}
fn brushes(s: &Session) -> Vec<String> {
vectorcraft_brush::library(doc(s)).into_iter().map(|b| b.name).collect()
}
fn brush(s: &Session, name: &str) -> vectorcraft_brush::Brush {
vectorcraft_brush::find(doc(s), name).unwrap_or_else(|| panic!("no brush {name}"))
}
fn strs(v: &Value) -> Vec<&str> {
v.as_array().unwrap().iter().map(|n| n.as_str().unwrap()).collect()
}
/// The patterns a brush's art fills with.
fn art_patterns(b: &vectorcraft_brush::Brush) -> Vec<String> {
let mut out = vec![];
for a in b.art() {
a.walk(&mut |n| {
for it in &n.appearance.items {
if let vectorcraft_color::Paint::Pattern { pattern, .. } = it.paint() {
out.push(pattern.clone());
}
}
});
}
out
}
/// A folder of its own under the temp folder, emptied.
fn folder(name: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!("vc-brushlib-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
dir
}
/// Pattern `name` (a 4 pt dot of `color` in a 10 pt tile) with its swatch, as one step.
fn add_pattern(s: &mut Session, name: &str, color: &str) {
let color = vectorcraft_color::Color::from_hex(color).unwrap();
s.edit("Pattern", |d, _| {
let dot = vectorcraft_doc::Node::path(
d.alloc_id(),
vectorcraft_geom::shapes::ellipse(vectorcraft_geom::Rect::new(0.0, 0.0, 4.0, 4.0)),
vectorcraft_doc::Appearance::basic(vectorcraft_color::Paint::solid(color), vectorcraft_color::Paint::None, 0.0),
);
let def = vectorcraft_doc::PatternDef {
tile: vectorcraft_geom::Rect::new(0.0, 0.0, 10.0, 10.0),
..vectorcraft_doc::PatternDef::new(name, vec![std::sync::Arc::new(dot)])
};
d.swatches.push(vectorcraft_color::Swatch {
name: name.into(),
paint: vectorcraft_color::Paint::Pattern { pattern: name.into(), xf: Default::default() },
global: false,
spot: false,
});
d.patterns.push(def);
Ok(())
})
.unwrap();
}
/// A document with brushes of its own: "My Nib" (calligraphic) and "Dotted Band" (an art brush
/// whose art is filled with pattern "Dots").
fn kit() -> Session {
let mut s = session();
run(&mut s, "brush.new", json!({"type": "calligraphic", "name": "My Nib", "params": {"angle": 20, "roundness": 40, "size": 7}}));
add_pattern(&mut s, "Dots", "#ff0000");
let band = run(&mut s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 100, "height": 8}))["id"].as_u64().unwrap();
run(&mut s, "paint.setFill", json!({"swatch": "Dots"}));
run(&mut s, "brush.new", json!({"type": "art", "name": "Dotted Band", "ids": [band]}));
s
}
#[test]
fn the_builtin_libraries_are_original_brushes_generated_in_code() {
let mut s = Session::new();
let list = run(&mut s, "brush.libraries", json!({}));
let libs = list["libraries"].as_array().unwrap();
let ids: Vec<&str> = libs.iter().map(|l| l["id"].as_str().unwrap()).collect();
assert_eq!(ids, ["art-ribbons", "calligraphic-nibs", "pattern-borders", "scatter-shapes"]);
assert!(libs.iter().all(|l| l["category"] == "builtIn"));
let lib = run(&mut s, "brush.library", json!({"library": "art ribbons"}));
assert_eq!(
lib["brushes"].as_array().unwrap().iter().map(|b| b["name"].as_str().unwrap()).collect::<Vec<_>>(),
["Notched Ribbon", "Double Line", "Wave", "Teardrop", "Striped Ribbon"]
);
assert_eq!(libs[0]["count"].as_u64(), Some(5));
// One kind of brush per library, every brush drawing something.
for (id, kind) in [("art-ribbons", "art"), ("calligraphic-nibs", "calligraphic"), ("pattern-borders", "pattern"), ("scatter-shapes", "scatter")] {
let (_, lib) = cmd::brushlib::library(&s, id).unwrap();
assert!(lib.len() >= 4, "{id}");
for b in &lib.brushes {
assert_eq!(b.kind.type_id(), kind, "{}", b.name);
let mut line = vectorcraft_geom::BezPath::new();
line.move_to((0.0, 0.0));
line.curve_to((60.0, -40.0), (140.0, 40.0), (200.0, 0.0));
let stroke = vectorcraft_doc::StrokeLayer::new(vectorcraft_color::Paint::solid(vectorcraft_color::Color::BLACK), 1.0);
assert!(!vectorcraft_brush::stroke_pieces(&lib.doc, b, &line, &stroke).is_empty(), "{} draws nothing", b.name);
}
}
// The resources their art uses come with them: Striped Ribbon's pattern, Sparkles' symbol.
let (_, ribbons) = cmd::brushlib::library(&s, "art-ribbons").unwrap();
assert_eq!(art_patterns(ribbons.brush("Striped Ribbon").unwrap()), ["Pinstripes"]);
assert!(ribbons.doc.pattern("Pinstripes").is_some());
let (_, shapes) = cmd::brushlib::library(&s, "scatter-shapes").unwrap();
let BrushKind::Scatter(sparkles) = &shapes.brush("Sparkles").unwrap().kind else { panic!("not a scatter brush") };
assert!(matches!(&sparkles.art.kind, NodeKind::SymbolInstance { symbol, .. } if symbol == "Sparkle"));
assert!(shapes.doc.symbols.iter().any(|x| x.name == "Sparkle"));
// None of them is a default brush.
let defaults: Vec<&str> = vectorcraft_brush::defaults().iter().map(|b| b.name.as_str()).collect();
for id in ids {
let (_, lib) = cmd::brushlib::library(&s, id).unwrap();
assert!(lib.brushes.iter().all(|b| !defaults.contains(&b.name.as_str())), "{id}");
}
assert!(s.execute("brush.library", &json!({"library": "nope"})).is_err());
}
#[test]
fn a_saved_library_reopens_and_its_brushes_join_another_document_in_one_step() {
let dir = folder("user");
let mut s = kit();
s.brush_libraries.set_user_dir(Some(dir.to_string_lossy().to_string()));
let r = run(&mut s, "brush.saveLibrary", json!({"user": true, "name": "Inks", "names": ["My Nib", "Dotted Band"]}));
assert_eq!((r["library"].as_str(), r["count"].as_u64()), (Some("user/Inks.vectorcraft"), Some(2)));
let path = r["path"].as_str().unwrap().to_string();
let list = run(&mut s, "brush.libraries", json!({}));
let user: Vec<&Value> = list["libraries"].as_array().unwrap().iter().filter(|l| l["category"] == "user").collect();
assert_eq!(user.len(), 1);
assert_eq!((user[0]["name"].as_str(), user[0]["count"].as_u64()), (Some("Inks"), Some(2)));
// The file is a document: the two brushes and the pattern their art uses, no art.
let saved = vectorcraft_format::load(&std::fs::read(&path).unwrap()).unwrap();
let names: Vec<String> = vectorcraft_brush::library(&saved).into_iter().map(|b| b.name).collect();
assert_eq!(names, ["My Nib", "Dotted Band"]);
assert_eq!(saved.patterns.iter().map(|p| p.name.as_str()).collect::<Vec<_>>(), ["Dots"]);
assert!(saved.layers.iter().all(|l| l.children().is_none_or(|c| c.is_empty())));
// Loading the file gives that User Defined library.
let r = run(&mut s, "brush.loadLibrary", json!({ "path": path }));
assert_eq!((r["library"].as_str(), r["name"].as_str()), (Some("user/Inks.vectorcraft"), Some("Inks")));
// Into a new document: Dotted Band and its pattern, as one step.
run(&mut s, "file.new", json!({"width": 100, "height": 100}));
let defaults = brushes(&s);
let before = undo_len(&s);
let r = run(&mut s, "brush.addFromLibrary", json!({"library": "Inks", "name": "Dotted Band"}));
assert_eq!((strs(&r["names"]), strs(&r["added"]), strs(&r["existing"])), (vec!["Dotted Band"], vec!["Dotted Band"], vec![]));
assert_eq!(undo_len(&s), before + 1);
assert_eq!(s.doc().unwrap().history.undo.last().unwrap().label, "Add to Brushes");
assert_eq!(brushes(&s), [defaults.clone(), vec!["Dotted Band".to_string()]].concat());
let (_, lib) = cmd::brushlib::library(&s, "Inks").unwrap();
assert_eq!(&brush(&s, "Dotted Band"), lib.brush("Dotted Band").unwrap());
assert!(doc(&s).pattern("Dots").is_some() && doc(&s).swatch("Dots").is_some(), "the pattern its art fills with");
assert_eq!(run(&mut s, "brush.list", json!({}))["current"], "Dotted Band");
// Undo takes it all back.
run(&mut s, "edit.undo", json!({}));
assert_eq!(brushes(&s), defaults);
assert!(doc(&s).pattern("Dots").is_none());
run(&mut s, "edit.redo", json!({}));
// Again: already there, so no step and no copy.
let before = undo_len(&s);
let r = run(&mut s, "brush.addFromLibrary", json!({"library": "user/Inks.vectorcraft", "names": ["Dotted Band", "My Nib", "Dotted Band"]}));
assert_eq!((strs(&r["names"]), strs(&r["existing"]), strs(&r["added"])), (vec!["Dotted Band", "My Nib"], vec!["Dotted Band"], vec!["My Nib"]));
assert_eq!(undo_len(&s), before + 1, "My Nib was new");
let before = undo_len(&s);
let r = run(&mut s, "brush.addFromLibrary", json!({"library": "Inks"}));
assert_eq!((strs(&r["existing"]).len(), undo_len(&s)), (2, before));
assert_eq!(brushes(&s).len(), defaults.len() + 2);
assert_eq!(doc(&s).patterns.len(), 1);
assert_eq!(run(&mut s, "brush.list", json!({}))["current"], "My Nib", "the library's first brush");
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn library_brushes_bring_their_patterns_and_symbols_rename_clashes_and_apply() {
let mut s = session();
// The document has a pattern "Pinstripes" and a brush "Wave" of its own.
add_pattern(&mut s, "Pinstripes", "#00ff00");
let mine = doc(&s).pattern("Pinstripes").unwrap().clone();
run(&mut s, "brush.new", json!({"type": "calligraphic", "name": "Wave"}));
let my_wave = brush(&s, "Wave");
let path = run(&mut s, "shape.line", json!({"x1": 10, "y1": 10, "x2": 150, "y2": 60}))["id"].as_u64().unwrap();
run(&mut s, "select.set", json!({"ids": [path]}));
let before = undo_len(&s);
let r = run(&mut s, "brush.addFromLibrary", json!({"library": "art-ribbons", "names": ["Striped Ribbon", "Wave"], "apply": true}));
assert_eq!(undo_len(&s), before + 1, "adding and applying are one step");
assert_eq!(strs(&r["names"]), ["Striped Ribbon", "Wave 2"]);
assert_eq!(r["applied"], json!([path]));
assert_eq!(brush(&s, "Wave"), my_wave, "the document's own Wave stays");
// The library's Pinstripes comes in under a free name, and the brush's art follows it.
assert_eq!(doc(&s).pattern("Pinstripes").unwrap(), &mine);
assert!(doc(&s).pattern("Pinstripes 2").is_some() && doc(&s).swatch("Pinstripes 2").is_some());
assert_eq!(art_patterns(&brush(&s, "Striped Ribbon")), ["Pinstripes 2"]);
// The first one is applied to the selected path and is the current brush.
let n = doc(&s).node(NodeId(path)).unwrap();
assert_eq!(n.appearance.stroke().and_then(|st| st.brush.clone()).as_deref(), Some("Striped Ribbon"));
assert_eq!(run(&mut s, "brush.list", json!({}))["current"], "Striped Ribbon");
// Adding it again finds it, its pattern renamed, and adds nothing.
let before = undo_len(&s);
let r = run(&mut s, "brush.addFromLibrary", json!({"library": "art-ribbons", "names": ["Striped Ribbon", "Wave"]}));
assert_eq!((strs(&r["names"]), strs(&r["existing"])), (vec!["Striped Ribbon", "Wave 2"], vec!["Striped Ribbon", "Wave 2"]));
assert_eq!(undo_len(&s), before);
assert!(doc(&s).pattern("Pinstripes 3").is_none() && vectorcraft_brush::find(doc(&s), "Striped Ribbon 2").is_none());
// Undo of the first add takes the brushes, the pattern and the brush stroke back.
run(&mut s, "edit.undo", json!({}));
assert!(vectorcraft_brush::find(doc(&s), "Striped Ribbon").is_none() && doc(&s).pattern("Pinstripes 2").is_none());
assert!(doc(&s).node(NodeId(path)).unwrap().appearance.stroke().is_none_or(|st| st.brush.is_none()));
// A scatter brush brings the symbol it scatters.
let r = run(&mut s, "brush.addFromLibrary", json!({"library": "Scatter Shapes", "name": "Sparkles"}));
assert_eq!(strs(&r["added"]), ["Sparkles"]);
assert_eq!(doc(&s).symbols.iter().map(|x| x.name.as_str()).collect::<Vec<_>>(), ["Sparkle"]);
// Applying a brush the document has still applies (one step) to the paths given, without
// adding it again.
run(&mut s, "select.none", json!({}));
let before = undo_len(&s);
let r = run(&mut s, "brush.addFromLibrary", json!({"library": "scatter-shapes", "name": "Sparkles", "apply": true, "ids": [path]}));
assert_eq!((undo_len(&s), strs(&r["existing"]), r["applied"].clone()), (before + 1, vec!["Sparkles"], json!([path])));
assert_eq!(doc(&s).node(NodeId(path)).unwrap().appearance.stroke().and_then(|st| st.brush.clone()).as_deref(), Some("Sparkles"));
assert_eq!(doc(&s).symbols.len(), 1);
assert!(s.execute("brush.addFromLibrary", &json!({"library": "art-ribbons", "name": "Nope"})).is_err());
assert!(s.execute("brush.addFromLibrary", &json!({"name": "Wave"})).is_err());
}
/// The undo step is Add to Brushes when a brush comes in (applied or not), and Apply Brush when
/// the brushes are all there and the click only applies one; undo and redo of either restore the
/// document as it was and as the step left it.
#[test]
fn adding_library_brushes_is_add_to_brushes_and_only_applying_one_is_apply_brush() {
let mut s = session();
let path = run(&mut s, "shape.line", json!({"x1": 10, "y1": 10, "x2": 150, "y2": 60}))["id"].as_u64().unwrap();
let label = |s: &Session| s.doc().unwrap().history.undo.last().unwrap().label.clone();
let stroke_brush = |s: &Session| doc(s).node(NodeId(path)).unwrap().appearance.stroke().and_then(|st| st.brush.clone());
run(&mut s, "brush.addFromLibrary", json!({"library": "art-ribbons", "name": "Wave", "apply": true, "ids": [path]}));
assert_eq!((label(&s), stroke_brush(&s).as_deref()), ("Add to Brushes".to_string(), Some("Wave")));
run(&mut s, "brush.addFromLibrary", json!({"library": "art-ribbons", "name": "Teardrop"}));
assert_eq!(label(&s), "Add to Brushes");
let (before, brushes_before) = (undo_len(&s), brushes(&s));
let r = run(&mut s, "brush.addFromLibrary", json!({"library": "art-ribbons", "name": "Teardrop", "apply": true, "ids": [path]}));
assert_eq!((strs(&r["added"]), strs(&r["existing"])), (vec![], vec!["Teardrop"]));
assert_eq!((undo_len(&s), label(&s), brushes(&s)), (before + 1, "Apply Brush".to_string(), brushes_before.clone()));
assert_eq!(stroke_brush(&s).as_deref(), Some("Teardrop"));
run(&mut s, "edit.undo", json!({}));
assert_eq!((stroke_brush(&s).as_deref(), brushes(&s)), (Some("Wave"), brushes_before.clone()));
run(&mut s, "edit.redo", json!({}));
assert_eq!((stroke_brush(&s).as_deref(), label(&s)), (Some("Teardrop"), "Apply Brush".to_string()));
}
#[test]
fn pasted_brushed_art_brings_the_patterns_its_brush_art_uses() {
let mut s = kit();
let line = run(&mut s, "shape.line", json!({"x1": 10, "y1": 100, "x2": 150, "y2": 120}))["id"].as_u64().unwrap();
run(&mut s, "brush.apply", json!({"name": "Dotted Band", "ids": [line]}));
run(&mut s, "select.set", json!({"ids": [line]}));
run(&mut s, "edit.copy", json!({}));
run(&mut s, "file.new", json!({"width": 200, "height": 200}));
run(&mut s, "edit.paste", json!({}));
assert!(vectorcraft_brush::find(doc(&s), "Dotted Band").is_some());
assert!(doc(&s).pattern("Dots").is_some(), "the brush's art is filled with it");
}
#[test]
fn other_documents_brushes_load_and_unreadable_files_are_errors() {
let mut s = kit();
// Another document's brushes load as a library, without its art.
let b64 = run(&mut s, "document.serialize", json!({}))["dataBase64"].as_str().unwrap().to_string();
let count = brushes(&s).len();
let r = run(&mut s, "brush.loadLibrary", json!({"dataBase64": b64, "name": "Poster.vectorcraft"}));
assert_eq!((r["name"].as_str(), r["count"].as_u64()), (Some("Poster"), Some(count as u64)));
let (_, lib) = cmd::brushlib::library(&s, r["library"].as_str().unwrap()).unwrap();
assert!(lib.doc.layers.iter().all(|l| l.children().is_none_or(|c| c.is_empty())));
assert!(lib.brush("Dotted Band").is_some() && lib.doc.pattern("Dots").is_some());
// An SVG has the default brushes, as it does opened.
let svg = r##"<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><rect width="10" height="10"/></svg>"##;
let r = run(&mut s, "brush.loadLibrary", json!({"data": svg, "name": "Box.svg"}));
assert_eq!(r["count"].as_u64(), Some(vectorcraft_brush::defaults().len() as u64), "{r}");
// Files that aren't documents, missing files and damaged native files are errors.
assert!(s.execute("brush.loadLibrary", &json!({"data": "hello", "name": "x.txt"})).is_err());
assert!(s.execute("brush.loadLibrary", &json!({"path": "/no/such/Library.vectorcraft"})).is_err());
let dir = folder("bad");
std::fs::create_dir_all(&dir).unwrap();
let bad = dir.join("Broken.vectorcraft");
std::fs::write(&bad, b"{\"format\": \"vectorcraft\", \"doc\": 7").unwrap();
assert!(s.execute("brush.loadLibrary", &json!({"path": bad.to_string_lossy()})).is_err());
// A damaged file in the user folder isn't listed.
s.brush_libraries.set_user_dir(Some(dir.to_string_lossy().to_string()));
let list = run(&mut s, "brush.libraries", json!({}));
assert!(list["libraries"].as_array().unwrap().iter().all(|l| l["category"] != "user"));
s.brush_libraries.set_user_dir(None);
let _ = std::fs::remove_dir_all(dir);
// No such brush, or a headless session's user folder, is an error.
assert!(s.execute("brush.saveLibrary", &json!({"names": ["Nope"]})).is_err());
assert!(s.execute("brush.saveLibrary", &json!({"user": true})).is_err());
// A document whose brushes were all deleted has none to save.
let all = brushes(&s);
run(&mut s, "brush.delete", json!({ "names": all }));
assert!(s.execute("brush.saveLibrary", &json!({})).is_err());
// Without a path the library comes back as the file's bytes.
let mut s = kit();
let r = run(&mut s, "brush.saveLibrary", json!({"names": ["My Nib"]}));
let bytes = vectorcraft_format::base64_decode(r["dataBase64"].as_str().unwrap()).unwrap();
assert_eq!(vectorcraft_brush::library(&vectorcraft_format::load(&bytes).unwrap()).len(), 1);
}
+36
View File
@@ -2494,6 +2494,12 @@ fn panel_drop(app: &mut VectorcraftApp, ui: &Ui, resp: &egui::Response, xf: &Xf)
widgets::PanelDrag::Brush { name, .. } => {
("brush.apply", json!({"name": name, "ids": [vectorcraft_tools::xform::paint_owner(&st.doc, hit.leaf).0]}))
}
// A brush from a brush library panel: added to the document's Brushes and applied to the
// path it lands on, in one step.
widgets::PanelDrag::LibraryBrush { library, name } => (
"brush.addFromLibrary",
json!({"library": library, "name": name, "apply": true, "ids": [vectorcraft_tools::xform::paint_owner(&st.doc, hit.leaf).0]}),
),
// Art dragged back onto the canvas: its move was already dropped. (A symbol was placed
// above.)
widgets::PanelDrag::Art(_)
@@ -8393,6 +8399,36 @@ mod tests {
assert_eq!(brush(&app).as_deref(), Some("Arrow"));
}
#[test]
fn a_library_brush_dropped_on_a_path_is_added_and_applied_in_one_step() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
let id =
app.session.execute("path.create", &json!({"anchors": [{"x": 100, "y": 150}, {"x": 300, "y": 150}]})).unwrap()["id"].as_u64().unwrap();
app.session.execute("select.none", &json!({})).unwrap();
let ctx = egui::Context::default();
frame(&mut app, &ctx, vec![]);
let xf = Xf::new(app.canvas_rect.unwrap(), app.view().unwrap());
let drop = |app: &mut VectorcraftApp, at: Point| {
let at = xf.to_screen(at);
egui::DragAndDrop::set_payload(&ctx, widgets::PanelDrag::LibraryBrush { library: "scatter-shapes".into(), name: "Sparkles".into() });
let up = egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed: false, modifiers: Default::default() };
frame(app, &ctx, vec![egui::Event::PointerMoved(at), up]);
};
let undo = app.session.active().unwrap().history.undo.len();
// Off the path: nothing.
drop(&mut app, Point::new(200.0, 250.0));
assert!(vectorcraft_brush::find(&app.session.active().unwrap().doc, "Sparkles").is_none());
// On it: added, with the symbol it scatters, and applied, as one step.
drop(&mut app, Point::new(200.0, 150.0));
let st = app.session.active().unwrap();
assert!(vectorcraft_brush::find(&st.doc, "Sparkles").is_some() && st.doc.symbols.iter().any(|s| s.name == "Sparkle"), "{}", app.ui.status);
let n = st.doc.node(vectorcraft_doc::NodeId(id)).unwrap();
assert_eq!(n.appearance.stroke().and_then(|s| s.brush.clone()).as_deref(), Some("Sparkles"));
assert_eq!(st.history.undo.len(), undo + 1);
assert_eq!(st.history.undo.last().unwrap().label, "Add to Brushes");
}
#[test]
fn middle_drag_pans_the_view_with_any_tool() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
@@ -225,7 +225,7 @@ pub(super) fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog)
ui.add_space(10.0);
// The brush as OK would make it, on a sample stroke.
let ty = d.str("__type");
stroke_preview(ui, &ty, edited_def(d), vec2(PREVIEW_W, 64.0));
stroke_preview(app, ui, &ty, edited_def(d), vec2(PREVIEW_W, 64.0));
}
/// The brush definition as OK would make it: `__def` with the fields' params.
+8 -7
View File
@@ -241,18 +241,18 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
ui.add_space(10.0);
match d.str("__type").as_str() {
"bristle" => bristle_body(app, ui, d),
"calligraphic" => calligraphic_body(ui, d),
"calligraphic" => calligraphic_body(app, ui, d),
_ => brush_art_options::body(app, ui, d),
}
false
}
fn calligraphic_body(ui: &mut egui::Ui, d: &mut Dialog) {
fn calligraphic_body(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
let c = calligraphic(d);
ui.horizontal(|ui| {
nib_preview(ui, &c, vec2(NIB_W, 84.0));
ui.add_space(8.0);
stroke_preview(ui, "calligraphic", serde_json::to_value(&c).unwrap_or_default(), vec2(STROKE_W - NIB_W - 8.0, 84.0));
stroke_preview(app, ui, "calligraphic", serde_json::to_value(&c).unwrap_or_default(), vec2(STROKE_W - NIB_W - 8.0, 84.0));
});
ui.add_space(12.0);
egui::Grid::new("brush-options-calli").num_columns(5).spacing([8.0, 8.0]).show(ui, |ui| {
@@ -290,11 +290,12 @@ fn bristle_body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
}
ui.add_space(8.0);
let p = params(d);
stroke_preview(ui, "bristle", p, vec2(STROKE_W, 64.0));
stroke_preview(app, ui, "bristle", p, vec2(STROKE_W, 64.0));
}
/// A stroke painted with the brush the fields describe (type `ty`, definition fields `def`).
pub(super) fn stroke_preview(ui: &mut egui::Ui, ty: &str, def: Value, size: egui::Vec2) {
/// A stroke painted with the brush the fields describe (type `ty`, definition fields `def`), with
/// the active document's resources (the patterns, symbols… its art uses).
pub(super) fn stroke_preview(app: &VectorcraftApp, ui: &mut egui::Ui, ty: &str, def: Value, size: egui::Vec2) {
let mut def = match def {
Value::Object(o) => o,
_ => Map::new(),
@@ -302,7 +303,7 @@ pub(super) fn stroke_preview(ui: &mut egui::Ui, ty: &str, def: Value, size: egui
def.insert("name".into(), json!("preview"));
def.insert("type".into(), json!(ty));
let (r, _) = ui.allocate_exact_size(size, Sense::hover());
crate::panels::brushes::chip(ui, r, &Value::Object(def));
crate::panels::brushes::definition_chip(ui, r, app.session.active(), &Value::Object(def));
ui.painter().rect_stroke(r, 0.0, Stroke::new(1.0, Tokens::get(ui.ctx()).input_border), egui::StrokeKind::Inside);
}
+2
View File
@@ -83,6 +83,7 @@ mod recovery;
mod revolve;
pub(crate) mod revolve_gizmo;
pub mod saturate;
pub mod save_brush_library;
mod save_changes;
pub mod save_for_web;
pub(crate) mod save_options;
@@ -299,6 +300,7 @@ registry! {
FlattenerPresets: [flattener_presets::KIND] => flattener_presets::SPEC,
SaveStyleLibrary: [save_style_library::KIND] => save_style_library::SPEC,
SaveSymbolLibrary: [save_symbol_library::KIND] => save_symbol_library::SPEC,
SaveBrushLibrary: [save_brush_library::KIND] => save_brush_library::SPEC,
Expand: [expand::KIND] => expand::SPEC,
SpotColors: [spot_colors::KIND] => spot_colors::SPEC,
TransformEach: [transform_each::KIND] => transform_each::SPEC,
@@ -0,0 +1,39 @@
//! Save Brush Library: a name and where it goes: the user library folder (listed under Window →
//! Brush Libraries → User Defined) or a `.vectorcraft` file picked in a save dialog (a download on
//! the web). OK runs `brush.saveLibrary`.
//!
//! Fields: `name`, `user` (save to the user library folder), `selectedOnly` and `names` (the
//! brushes selected in the Brushes panel), `__user` (there is a user library folder).
use serde_json::Value;
use vectorcraft_engine::cmd::brushlib::BRUSHES_EXT;
use super::DialogSpec;
use super::save_swatch_library::{destination, fields, name_row, params, save};
use super::swatch_options::grid;
use crate::VectorcraftApp;
use crate::state::Dialog;
/// The dialog kind of Save Brush Library.
pub const KIND: &str = "saveBrushLibrary";
pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| tl!("Save Brush Library").into(), body, confirm, min_width: 360.0, ..DialogSpec::FORM };
/// Open Save Brush Library for the document's brushes (`names`: those selected in the panel).
pub fn open(app: &mut VectorcraftApp, names: Vec<String>) -> Result<Value, String> {
let fields = fields(app, app.session.brush_libraries.user_dir().is_some(), names)?;
app.ui.dialog = Some(Dialog::new(KIND, fields));
Ok(Value::Null)
}
fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
grid(ui, |ui| {
name_row(ui, d);
destination(ui, d, tl!("Selected Brushes Only"));
});
false
}
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
save(app, d, "brush.saveLibrary", BRUSHES_EXT, params(d))
}
@@ -7,7 +7,8 @@
//! (there is a user library folder).
//!
//! The name and Save To rows and the saving are shared with Save Graphic Style Library
//! ([`super::save_style_library`]) and Save Symbol Library ([`super::save_symbol_library`]).
//! ([`super::save_style_library`]), Save Symbol Library ([`super::save_symbol_library`]) and Save
//! Brush Library ([`super::save_brush_library`]).
use serde_json::{Value, json};
use vectorcraft_color::palette_io::PaletteFormat;
+22 -1
View File
@@ -329,7 +329,6 @@ font-technology Variable Променливи
Brush Affects: Четката влияе върху:
Brush Definition Дефиниция на четка
Brush Libraries Библиотеки с четки
Brush Libraries (on the roadmap) Библиотеки с четки (планирани)
Brush Options… Опции за четката…
Brush Strokes Щрихи с четка
Brushes Четки
@@ -5482,6 +5481,28 @@ axis Y Axis Y ос
Rectangular Grid Tool Options Опции на инструмента „Правоъгълна мрежа“
Polar Grid Tool Options Опции на инструмента „Полярна мрежа“
Fill Line Запълване на линия
Add to Brushes Добавяне към четки
Brush Library Библиотека с четки
Save Brush Library Запази библиотека с четки
Save Brushes… Запази четки…
Open Brush Library Отваряне на библиотека с четки
Selected Brushes Only Само избрани четки
Brush Libraries Menu Меню с библиотеки с четки
User Brush Library 1 Потребителска библиотека с четки 1
User Brush Library 2 Потребителска библиотека с четки 2
User Brush Library 3 Потребителска библиотека с четки 3
User Brush Library 4 Потребителска библиотека с четки 4
User Brush Library 5 Потребителска библиотека с четки 5
User Brush Library 6 Потребителска библиотека с четки 6
User Brush Library 7 Потребителска библиотека с четки 7
User Brush Library 8 Потребителска библиотека с четки 8
User Brush Library 9 Потребителска библиотека с четки 9
User Brush Library 10 Потребителска библиотека с четки 10
Art Ribbons Арт ленти
Calligraphic Nibs Калиграфски писци
Pattern Borders Бордюри с мостри
Scatter Shapes Разсеяни фигури
Save Brush Library… Запази библиотека с четки…
@msg no empty artboards to delete няма празни работни области за изтриване
Scale Presets Пресети за мащаб
Live preview Преглед на живо
Can't render this file because it contains an unexpected character in line 2 and column 73.
+28 -1
View File
@@ -1384,7 +1384,6 @@ lighting Light Světlo
URL: URL:
Use Even-Odd Fill Rule Použít pravidlo výplně sudá-lichá
Use Non-Zero Winding Fill Rule Použít pravidlo výplně nenulového vinutí
Brush Libraries (on the roadmap) Knihovny štětců (v plánu)
List View Zobrazení seznamu
New Art Brush from Selection Nový obrazový štětec z výběru
New Bristle Brush Nový štětinový štětec
@@ -5558,6 +5557,34 @@ axis Y Axis osa Y
Rectangular Grid Tool Options Možnosti nástroje Obdélníková mřížka
Polar Grid Tool Options Možnosti nástroje Polární mřížka
Fill Line Výplň úsečky
Add to Brushes Přidat do štětců
Brush Library Knihovna štětců
Save Brush Library Uložit knihovnu štětců
Save Brushes… Uložit štětce…
Open Brush Library Otevřít knihovnu štětců
Selected Brushes Only Pouze vybrané štětce
Brush Libraries Menu Nabídka knihoven štětců
User Brush Library 1 Uživatelská knihovna štětců 1
User Brush Library 2 Uživatelská knihovna štětců 2
User Brush Library 3 Uživatelská knihovna štětců 3
User Brush Library 4 Uživatelská knihovna štětců 4
User Brush Library 5 Uživatelská knihovna štětců 5
User Brush Library 6 Uživatelská knihovna štětců 6
User Brush Library 7 Uživatelská knihovna štětců 7
User Brush Library 8 Uživatelská knihovna štětců 8
User Brush Library 9 Uživatelská knihovna štětců 9
User Brush Library 10 Uživatelská knihovna štětců 10
Art Ribbons Obrazové stuhy
Calligraphic Nibs Kaligrafická pera
Pattern Borders Vzorkové okraje
Scatter Shapes Rozptýlené tvary
Save Brush Library… Uložit knihovnu štětců…
@msg `{_1}` has no brushes to apply `{_1}` nemá žádné štětce k použití
@msg missing `library` (see brush.libraries) chybí `library` (viz brush.libraries)
@msg no brush `{n}` žádný štětec `{n}`
@msg no brush `{n}` in `{_1}` žádný štětec `{n}` v `{_1}`
@msg no brush library `{key}` (see brush.libraries) žádná knihovna štětců `{key}` (viz brush.libraries)
@msg the document has no brushes to save dokument nemá žádné štětce k uložení
@msg no empty artboards to delete žádné prázdné pracovní plochy k odstranění
Scale Presets Přednastavení měřítka
@msg a {width}×{height} screenshot's byte count overflows počet bajtů snímku {width}×{height} přeteče
Can't render this file because it contains an unexpected character in line 2 and column 73.
+22 -1
View File
@@ -1302,7 +1302,6 @@ lighting Light Lys
URL: URL:
Use Even-Odd Fill Rule Brug fyldreglen lige-ulige
Use Non-Zero Winding Fill Rule Brug fyldreglen ikke-nul-vikling
Brush Libraries (on the roadmap) Penselbiblioteker (på vejkortet)
List View Listevisning
New Art Brush from Selection Ny kunstpensel fra markering
New Bristle Brush Ny bustpensel
@@ -5498,6 +5497,28 @@ axis Y Axis Y-akse
Rectangular Grid Tool Options Indstillinger for rektangulært gitterværktøj
Polar Grid Tool Options Indstillinger for polært gitterværktøj
Fill Line Udfyld linje
Add to Brushes Føj til pensler
Brush Library Penselbibliotek
Save Brush Library Gem penselbibliotek
Save Brushes… Gem pensler…
Open Brush Library Åbn penselbibliotek
Selected Brushes Only Kun valgte pensler
Brush Libraries Menu Menu med penselbiblioteker
User Brush Library 1 Brugerpenselbibliotek 1
User Brush Library 2 Brugerpenselbibliotek 2
User Brush Library 3 Brugerpenselbibliotek 3
User Brush Library 4 Brugerpenselbibliotek 4
User Brush Library 5 Brugerpenselbibliotek 5
User Brush Library 6 Brugerpenselbibliotek 6
User Brush Library 7 Brugerpenselbibliotek 7
User Brush Library 8 Brugerpenselbibliotek 8
User Brush Library 9 Brugerpenselbibliotek 9
User Brush Library 10 Brugerpenselbibliotek 10
Art Ribbons Kunstbånd
Calligraphic Nibs Kalligrafiske penne
Pattern Borders Mønsterborter
Scatter Shapes Spredte former
Save Brush Library… Gem penselbibliotek…
@msg no empty artboards to delete ingen tomme tegneflader at slette
Scale Presets Skaleringsforudindstillinger
Live preview Direkte forhåndsvisning
Can't render this file because it contains an unexpected character in line 2 and column 73.
+28 -1
View File
@@ -317,7 +317,6 @@ font-technology Variable Variabel
Brush Affects: Pinsel wirkt auf:
Brush Definition Pinseldefinition
Brush Libraries Pinsel-Bibliotheken
Brush Libraries (on the roadmap) Pinsel-Bibliotheken (geplant)
Brush Options… Pinseloptionen…
Brush Strokes Pinselstriche
Brushes Pinsel
@@ -5140,6 +5139,34 @@ axis Y Axis Y-Achse
Rectangular Grid Tool Options Optionen für das Rechteckiges-Raster-Werkzeug
Polar Grid Tool Options Optionen für das Radiales-Raster-Werkzeug
Fill Line Linie füllen
Add to Brushes Zu Pinseln hinzufügen
Brush Library Pinsel-Bibliothek
Save Brush Library Pinsel-Bibliothek speichern
Save Brushes… Pinsel speichern…
Open Brush Library Pinsel-Bibliothek öffnen
Selected Brushes Only Nur ausgewählte Pinsel
Brush Libraries Menu Menü „Pinsel-Bibliotheken“
User Brush Library 1 Benutzer-Pinsel-Bibliothek 1
User Brush Library 2 Benutzer-Pinsel-Bibliothek 2
User Brush Library 3 Benutzer-Pinsel-Bibliothek 3
User Brush Library 4 Benutzer-Pinsel-Bibliothek 4
User Brush Library 5 Benutzer-Pinsel-Bibliothek 5
User Brush Library 6 Benutzer-Pinsel-Bibliothek 6
User Brush Library 7 Benutzer-Pinsel-Bibliothek 7
User Brush Library 8 Benutzer-Pinsel-Bibliothek 8
User Brush Library 9 Benutzer-Pinsel-Bibliothek 9
User Brush Library 10 Benutzer-Pinsel-Bibliothek 10
Art Ribbons Bildbänder
Calligraphic Nibs Kalligrafie-Federn
Pattern Borders Musterbordüren
Scatter Shapes Streuformen
Save Brush Library… Pinsel-Bibliothek speichern…
@msg `{_1}` has no brushes to apply `{_1}` hat keine Pinsel zum Anwenden
@msg missing `library` (see brush.libraries) `library` fehlt (siehe brush.libraries)
@msg no brush `{n}` kein Pinsel `{n}`
@msg no brush `{n}` in `{_1}` kein Pinsel `{n}` in `{_1}`
@msg no brush library `{key}` (see brush.libraries) keine Pinsel-Bibliothek `{key}` (siehe brush.libraries)
@msg the document has no brushes to save das Dokument hat keine Pinsel zum Speichern
@msg no empty artboards to delete keine leeren Zeichenflächen zum Löschen
Scale Presets Skalierungsvorgaben
@msg a {width}×{height} screenshot's byte count overflows die Byteanzahl eines {width}×{height}-Screenshots läuft über
Can't render this file because it contains an unexpected character in line 2 and column 73.
+28 -1
View File
@@ -320,7 +320,6 @@ font-technology Variable Μεταβλητές
Brush Affects: Το πινέλο επηρεάζει:
Brush Definition Ορισμός πινέλου
Brush Libraries Βιβλιοθήκες πινέλων
Brush Libraries (on the roadmap) Βιβλιοθήκες πινέλων (στον σχεδιασμό)
Brush Options… Επιλογές πινέλου…
Brush Strokes Πινελιές
Brushes Πινέλα
@@ -5510,6 +5509,34 @@ axis Y Axis Άξονας Y
Rectangular Grid Tool Options Επιλογές εργαλείου ορθογώνιου πλέγματος
Polar Grid Tool Options Επιλογές εργαλείου πολικού πλέγματος
Fill Line Γέμισμα γραμμής
Add to Brushes Προσθήκη στα πινέλα
Brush Library Βιβλιοθήκη πινέλων
Save Brush Library Αποθήκευση βιβλιοθήκης πινέλων
Save Brushes… Αποθήκευση πινέλων…
Open Brush Library Άνοιγμα βιβλιοθήκης πινέλων
Selected Brushes Only Μόνο επιλεγμένα πινέλα
Brush Libraries Menu Μενού βιβλιοθηκών πινέλων
User Brush Library 1 Βιβλιοθήκη πινέλων χρήστη 1
User Brush Library 2 Βιβλιοθήκη πινέλων χρήστη 2
User Brush Library 3 Βιβλιοθήκη πινέλων χρήστη 3
User Brush Library 4 Βιβλιοθήκη πινέλων χρήστη 4
User Brush Library 5 Βιβλιοθήκη πινέλων χρήστη 5
User Brush Library 6 Βιβλιοθήκη πινέλων χρήστη 6
User Brush Library 7 Βιβλιοθήκη πινέλων χρήστη 7
User Brush Library 8 Βιβλιοθήκη πινέλων χρήστη 8
User Brush Library 9 Βιβλιοθήκη πινέλων χρήστη 9
User Brush Library 10 Βιβλιοθήκη πινέλων χρήστη 10
Art Ribbons Γραφικές κορδέλες
Calligraphic Nibs Καλλιγραφικές πένες
Pattern Borders Περιθώρια μοτίβου
Scatter Shapes Σχήματα διασποράς
Save Brush Library… Αποθήκευση βιβλιοθήκης πινέλων…
@msg `{_1}` has no brushes to apply Η `{_1}` δεν έχει πινέλα για εφαρμογή
@msg missing `library` (see brush.libraries) λείπει το `library` (δείτε brush.libraries)
@msg no brush `{n}` δεν υπάρχει πινέλο `{n}`
@msg no brush `{n}` in `{_1}` δεν υπάρχει πινέλο `{n}` στη `{_1}`
@msg no brush library `{key}` (see brush.libraries) δεν υπάρχει βιβλιοθήκη πινέλων `{key}` (δείτε brush.libraries)
@msg the document has no brushes to save το έγγραφο δεν έχει πινέλα για αποθήκευση
@msg no empty artboards to delete δεν υπάρχουν κενοί πίνακες σχεδίασης για διαγραφή
Scale Presets Προεπιλογές κλίμακας
@msg a {width}×{height} screenshot's byte count overflows το πλήθος byte ενός στιγμιότυπου {width}×{height} υπερχειλίζει
Can't render this file because it contains an unexpected character in line 2069 and column 19.
+28 -1
View File
@@ -342,7 +342,6 @@ font-technology Variable Variables
Brush Affects: El pincel afecta a:
Brush Definition Definición del pincel
Brush Libraries Bibliotecas de pinceles
Brush Libraries (on the roadmap) Bibliotecas de pinceles (en la hoja de ruta)
Brush Options… Opciones de pincel…
Brush Strokes Trazos de pincel
Brushes Pinceles
@@ -5500,6 +5499,34 @@ axis Y Axis Eje Y
Rectangular Grid Tool Options Opciones de la herramienta Cuadrícula rectangular
Polar Grid Tool Options Opciones de la herramienta Cuadrícula polar
Fill Line Rellenar línea
Add to Brushes Agregar a pinceles
Brush Library Biblioteca de pinceles
Save Brush Library Guardar biblioteca de pinceles
Save Brushes… Guardar pinceles…
Open Brush Library Abrir biblioteca de pinceles
Selected Brushes Only Solo pinceles seleccionados
Brush Libraries Menu Menú Bibliotecas de pinceles
User Brush Library 1 Biblioteca de pinceles del usuario 1
User Brush Library 2 Biblioteca de pinceles del usuario 2
User Brush Library 3 Biblioteca de pinceles del usuario 3
User Brush Library 4 Biblioteca de pinceles del usuario 4
User Brush Library 5 Biblioteca de pinceles del usuario 5
User Brush Library 6 Biblioteca de pinceles del usuario 6
User Brush Library 7 Biblioteca de pinceles del usuario 7
User Brush Library 8 Biblioteca de pinceles del usuario 8
User Brush Library 9 Biblioteca de pinceles del usuario 9
User Brush Library 10 Biblioteca de pinceles del usuario 10
Art Ribbons Cintas de ilustración
Calligraphic Nibs Plumillas caligráficas
Pattern Borders Bordes de motivo
Scatter Shapes Formas de dispersión
Save Brush Library… Guardar biblioteca de pinceles…
@msg `{_1}` has no brushes to apply `{_1}` no tiene pinceles para aplicar
@msg missing `library` (see brush.libraries) falta `library` (consulte brush.libraries)
@msg no brush `{n}` no hay ningún pincel `{n}`
@msg no brush `{n}` in `{_1}` no hay ningún pincel `{n}` en `{_1}`
@msg no brush library `{key}` (see brush.libraries) no hay ninguna biblioteca de pinceles `{key}` (consulte brush.libraries)
@msg the document has no brushes to save el documento no tiene pinceles para guardar
@msg no empty artboards to delete no hay mesas de trabajo vacías que eliminar
Scale Presets Ajustes predefinidos de escala
@msg a {width}×{height} screenshot's byte count overflows el número de bytes de una captura de {width}×{height} se desborda
Can't render this file because it contains an unexpected character in line 2 and column 73.
+28 -1
View File
@@ -1359,7 +1359,6 @@ lighting Light Valo
URL: URL:
Use Even-Odd Fill Rule Käytä parillinen-pariton-täyttösääntöä
Use Non-Zero Winding Fill Rule Käytä nollasta poikkeavan kierrosluvun täyttösääntöä
Brush Libraries (on the roadmap) Sivellinkirjastot (suunnitteilla)
List View Luettelonäkymä
New Art Brush from Selection Uusi taidesivellin valinnasta
New Bristle Brush Uusi harjassivellin
@@ -5589,6 +5588,34 @@ axis Y Axis Y-akseli
Rectangular Grid Tool Options Suorakulmaisen ruudukon työkalun asetukset
Polar Grid Tool Options Napakoordinaattiruudukon työkalun asetukset
Fill Line Täytä viiva
Add to Brushes Lisää siveltimiin
Brush Library Sivellinkirjasto
Save Brush Library Tallenna sivellinkirjasto
Save Brushes… Tallenna siveltimet…
Open Brush Library Avaa sivellinkirjasto
Selected Brushes Only Vain valitut siveltimet
Brush Libraries Menu Sivellinkirjastojen valikko
User Brush Library 1 Käyttäjän sivellinkirjasto 1
User Brush Library 2 Käyttäjän sivellinkirjasto 2
User Brush Library 3 Käyttäjän sivellinkirjasto 3
User Brush Library 4 Käyttäjän sivellinkirjasto 4
User Brush Library 5 Käyttäjän sivellinkirjasto 5
User Brush Library 6 Käyttäjän sivellinkirjasto 6
User Brush Library 7 Käyttäjän sivellinkirjasto 7
User Brush Library 8 Käyttäjän sivellinkirjasto 8
User Brush Library 9 Käyttäjän sivellinkirjasto 9
User Brush Library 10 Käyttäjän sivellinkirjasto 10
Art Ribbons Kuvitusnauhat
Calligraphic Nibs Kalligrafiakärjet
Pattern Borders Kuvioreunukset
Scatter Shapes Sirontamuodot
Save Brush Library… Tallenna sivellinkirjasto…
@msg `{_1}` has no brushes to apply Kohteessa `{_1}` ei ole käytettäviä siveltimiä
@msg missing `library` (see brush.libraries) `library` puuttuu (katso brush.libraries)
@msg no brush `{n}` ei sivellintä `{n}`
@msg no brush `{n}` in `{_1}` ei sivellintä `{n}` kohteessa `{_1}`
@msg no brush library `{key}` (see brush.libraries) ei sivellinkirjastoa `{key}` (katso brush.libraries)
@msg the document has no brushes to save dokumentissa ei ole tallennettavia siveltimiä
@msg no empty artboards to delete ei poistettavia tyhjiä piirtoalustoja
Scale Presets Skaalauksen esiasetukset
@msg a {width}×{height} screenshot's byte count overflows {width}×{height} kuvakaappauksen tavumäärä ylittää rajan
Can't render this file because it contains an unexpected character in line 2 and column 73.
+28 -1
View File
@@ -343,7 +343,6 @@ font-technology Variable Variables
Brush Affects: Le pinceau affecte :
Brush Definition Définition de la forme
Brush Libraries Bibliothèques de formes
Brush Libraries (on the roadmap) Bibliothèques de formes (prévu dans la feuille de route)
Brush Options… Options de forme…
Brush Strokes Contours de forme
Brushes Formes
@@ -5557,6 +5556,34 @@ axis Y Axis Axe Y
Rectangular Grid Tool Options Options de l’outil Grille rectangulaire
Polar Grid Tool Options Options de l’outil Grille à coordonnées polaires
Fill Line Remplir le trait
Add to Brushes Ajouter aux formes
Brush Library Bibliothèque de formes
Save Brush Library Enregistrer la bibliothèque de formes
Save Brushes… Enregistrer les formes…
Open Brush Library Ouvrir une bibliothèque de formes
Selected Brushes Only Formes sélectionnées uniquement
Brush Libraries Menu Menu Bibliothèques de formes
User Brush Library 1 Bibliothèque de formes utilisateur 1
User Brush Library 2 Bibliothèque de formes utilisateur 2
User Brush Library 3 Bibliothèque de formes utilisateur 3
User Brush Library 4 Bibliothèque de formes utilisateur 4
User Brush Library 5 Bibliothèque de formes utilisateur 5
User Brush Library 6 Bibliothèque de formes utilisateur 6
User Brush Library 7 Bibliothèque de formes utilisateur 7
User Brush Library 8 Bibliothèque de formes utilisateur 8
User Brush Library 9 Bibliothèque de formes utilisateur 9
User Brush Library 10 Bibliothèque de formes utilisateur 10
Art Ribbons Rubans d'illustration
Calligraphic Nibs Plumes calligraphiques
Pattern Borders Bordures de motif
Scatter Shapes Formes de dispersion
Save Brush Library… Enregistrer la bibliothèque de formes…
@msg `{_1}` has no brushes to apply `{_1}` n'a aucune forme à appliquer
@msg missing `library` (see brush.libraries) `library` manquant (voir brush.libraries)
@msg no brush `{n}` aucune forme `{n}`
@msg no brush `{n}` in `{_1}` aucune forme `{n}` dans `{_1}`
@msg no brush library `{key}` (see brush.libraries) aucune bibliothèque de formes `{key}` (voir brush.libraries)
@msg the document has no brushes to save le document n'a aucune forme à enregistrer
@msg no empty artboards to delete aucun plan de travail vide à supprimer
Scale Presets Préréglages de mise à l'échelle
@msg a {width}×{height} screenshot's byte count overflows le nombre d'octets d'une capture {width}×{height} dépasse la limite
Can't render this file because it contains an unexpected character in line 2 and column 73.
+28 -1
View File
@@ -477,7 +477,6 @@ font-technology Variable Változó
Brush Art Ecsetgrafika
Brush Definition Ecsetdefiníció
Brush Libraries Ecsetkönyvtárak
Brush Libraries (on the roadmap) Ecsetkönyvtárak (tervezett funkció)
Brush Options… Ecset beállításai…
Brush Scale Options Ecsetméretezési beállítások
Brush Strokes Ecsetvonások
@@ -5544,6 +5543,34 @@ axis Y Axis Y tengely
Rectangular Grid Tool Options Téglalaprács beállításai
Polar Grid Tool Options Polárrács beállításai
Fill Line Szakasz kitöltése
Add to Brushes Hozzáadás az ecsetekhez
Brush Library Ecsetkönyvtár
Save Brush Library Ecsetkönyvtár mentése
Save Brushes… Ecsetek mentése…
Open Brush Library Ecsetkönyvtár megnyitása
Selected Brushes Only Csak a kijelölt ecsetek
Brush Libraries Menu Ecsetkönyvtárak menü
User Brush Library 1 Felhasználói ecsetkönyvtár 1
User Brush Library 2 Felhasználói ecsetkönyvtár 2
User Brush Library 3 Felhasználói ecsetkönyvtár 3
User Brush Library 4 Felhasználói ecsetkönyvtár 4
User Brush Library 5 Felhasználói ecsetkönyvtár 5
User Brush Library 6 Felhasználói ecsetkönyvtár 6
User Brush Library 7 Felhasználói ecsetkönyvtár 7
User Brush Library 8 Felhasználói ecsetkönyvtár 8
User Brush Library 9 Felhasználói ecsetkönyvtár 9
User Brush Library 10 Felhasználói ecsetkönyvtár 10
Art Ribbons Grafikus szalagok
Calligraphic Nibs Kalligrafikus tollhegyek
Pattern Borders Mintázatszegélyek
Scatter Shapes Szórt alakzatok
Save Brush Library… Ecsetkönyvtár mentése…
@msg `{_1}` has no brushes to apply A(z) `{_1}` nem tartalmaz alkalmazható ecsetet
@msg missing `library` (see brush.libraries) hiányzik a `library` (lásd: brush.libraries)
@msg no brush `{n}` nincs `{n}` ecset
@msg no brush `{n}` in `{_1}` nincs `{n}` ecset itt: `{_1}`
@msg no brush library `{key}` (see brush.libraries) nincs `{key}` ecsetkönyvtár (lásd: brush.libraries)
@msg the document has no brushes to save a dokumentumban nincs menthető ecset
@msg no empty artboards to delete nincs törölhető üres rajztábla
Scale Presets Méretezési beállítások
@msg a {width}×{height} screenshot's byte count overflows a(z) {width}×{height} képernyőkép bájtszáma túlcsordul
Can't render this file because it contains an unexpected character in line 2 and column 73.
+22 -1
View File
@@ -1302,7 +1302,6 @@ lighting Light Cahaya
URL: URL:
Use Even-Odd Fill Rule Gunakan aturan isian genap-ganjil
Use Non-Zero Winding Fill Rule Gunakan aturan isian lilitan tak nol
Brush Libraries (on the roadmap) Pustaka kuas (masih dalam roadmap)
List View Tampilan daftar
New Art Brush from Selection Kuas seni baru dari seleksi
New Bristle Brush Kuas bristle baru
@@ -5498,6 +5497,28 @@ axis Y Axis Sumbu Y
Rectangular Grid Tool Options Opsi alat kisi persegi panjang
Polar Grid Tool Options Opsi alat kisi polar
Fill Line Isi garis
Add to Brushes Tambahkan ke kuas
Brush Library Pustaka kuas
Save Brush Library Simpan pustaka kuas
Save Brushes… Simpan kuas…
Open Brush Library Buka pustaka kuas
Selected Brushes Only Hanya kuas terpilih
Brush Libraries Menu Menu pustaka kuas
User Brush Library 1 Pustaka kuas pengguna 1
User Brush Library 2 Pustaka kuas pengguna 2
User Brush Library 3 Pustaka kuas pengguna 3
User Brush Library 4 Pustaka kuas pengguna 4
User Brush Library 5 Pustaka kuas pengguna 5
User Brush Library 6 Pustaka kuas pengguna 6
User Brush Library 7 Pustaka kuas pengguna 7
User Brush Library 8 Pustaka kuas pengguna 8
User Brush Library 9 Pustaka kuas pengguna 9
User Brush Library 10 Pustaka kuas pengguna 10
Art Ribbons Pita seni
Calligraphic Nibs Pena kaligrafi
Pattern Borders Bingkai pola
Scatter Shapes Bentuk sebar
Save Brush Library… Simpan pustaka kuas…
@msg no empty artboards to delete tidak ada artboard kosong untuk dihapus
Scale Presets Preset skala
Live preview Pratinjau langsung
Can't render this file because it contains an unexpected character in line 3838 and column 19.
+28 -1
View File
@@ -342,7 +342,6 @@ font-technology Variable Variabili
Brush Affects: Il pennello influisce su:
Brush Definition Definizione pennello
Brush Libraries Librerie pennelli
Brush Libraries (on the roadmap) Librerie pennelli (in programma)
Brush Options… Opzioni pennello…
Brush Strokes Pennellate
Brushes Pennelli
@@ -5174,6 +5173,34 @@ axis Y Axis Asse Y
Rectangular Grid Tool Options Opzioni strumento Griglia rettangolare
Polar Grid Tool Options Opzioni strumento Griglia polare
Fill Line Riempi linea
Add to Brushes Aggiungi ai pennelli
Brush Library Libreria pennelli
Save Brush Library Salva libreria pennelli
Save Brushes… Salva pennelli…
Open Brush Library Apri libreria pennelli
Selected Brushes Only Solo pennelli selezionati
Brush Libraries Menu Menu Librerie pennelli
User Brush Library 1 Libreria pennelli utente 1
User Brush Library 2 Libreria pennelli utente 2
User Brush Library 3 Libreria pennelli utente 3
User Brush Library 4 Libreria pennelli utente 4
User Brush Library 5 Libreria pennelli utente 5
User Brush Library 6 Libreria pennelli utente 6
User Brush Library 7 Libreria pennelli utente 7
User Brush Library 8 Libreria pennelli utente 8
User Brush Library 9 Libreria pennelli utente 9
User Brush Library 10 Libreria pennelli utente 10
Art Ribbons Nastri grafici
Calligraphic Nibs Pennini calligrafici
Pattern Borders Bordi a motivo
Scatter Shapes Forme a dispersione
Save Brush Library… Salva libreria pennelli…
@msg `{_1}` has no brushes to apply `{_1}` non ha pennelli da applicare
@msg missing `library` (see brush.libraries) manca `library` (vedi brush.libraries)
@msg no brush `{n}` nessun pennello `{n}`
@msg no brush `{n}` in `{_1}` nessun pennello `{n}` in `{_1}`
@msg no brush library `{key}` (see brush.libraries) nessuna libreria pennelli `{key}` (vedi brush.libraries)
@msg the document has no brushes to save il documento non ha pennelli da salvare
@msg no empty artboards to delete nessuna tavola da disegno vuota da eliminare
Scale Presets Preimpostazioni di scala
@msg a {width}×{height} screenshot's byte count overflows il numero di byte di uno screenshot {width}×{height} va in overflow
Can't render this file because it contains an unexpected character in line 2 and column 73.
+28 -1
View File
@@ -982,7 +982,6 @@ font-technology Variable バリアブル
Browser ブラウザー
Brush Affects: ブラシの影響:
Brush Definition ブラシの定義
Brush Libraries (on the roadmap) ブラシライブラリ (今後対応予定)
Brush Options… ブラシオプション…
Built-in presets are read-only: New… starts an editable copy. 組み込みのプリセットは読み取り専用です。「新規…」で編集可能なコピーを作成できます。
Built-in presets don't change: New makes a copy you can edit. 組み込みのプリセットは変更できません。「新規」で編集可能なコピーを作成します。
@@ -5236,6 +5235,34 @@ axis Y Axis Y 軸
Rectangular Grid Tool Options 長方形グリッドツールオプション
Polar Grid Tool Options 同心円グリッドツールオプション
Fill Line 線を塗りつぶす
Add to Brushes ブラシに追加
Brush Library ブラシライブラリ
Save Brush Library ブラシライブラリを保存
Save Brushes… ブラシを保存…
Open Brush Library ブラシライブラリを開く
Selected Brushes Only 選択したブラシのみ
Brush Libraries Menu ブラシライブラリメニュー
User Brush Library 1 ユーザーブラシライブラリ 1
User Brush Library 2 ユーザーブラシライブラリ 2
User Brush Library 3 ユーザーブラシライブラリ 3
User Brush Library 4 ユーザーブラシライブラリ 4
User Brush Library 5 ユーザーブラシライブラリ 5
User Brush Library 6 ユーザーブラシライブラリ 6
User Brush Library 7 ユーザーブラシライブラリ 7
User Brush Library 8 ユーザーブラシライブラリ 8
User Brush Library 9 ユーザーブラシライブラリ 9
User Brush Library 10 ユーザーブラシライブラリ 10
Art Ribbons アートリボン
Calligraphic Nibs カリグラフィペン先
Pattern Borders パターンボーダー
Scatter Shapes 散布シェイプ
Save Brush Library… ブラシライブラリを保存…
@msg `{_1}` has no brushes to apply `{_1}` に適用できるブラシがありません
@msg missing `library` (see brush.libraries) `library` がありません (brush.libraries を参照)
@msg no brush `{n}` ブラシ `{n}` がありません
@msg no brush `{n}` in `{_1}` `{_1}` にブラシ `{n}` がありません
@msg no brush library `{key}` (see brush.libraries) ブラシライブラリ `{key}` がありません (brush.libraries を参照)
@msg the document has no brushes to save ドキュメントに保存するブラシがありません
@msg no empty artboards to delete 削除する空のアートボードはありません
Scale Presets 拡大縮小のプリセット
@msg a {width}×{height} screenshot's byte count overflows {width}×{height} のスクリーンショットのバイト数がオーバーフローします
Can't render this file because it contains an unexpected character in line 2 and column 73.
+28 -1
View File
@@ -1216,7 +1216,6 @@ lighting Light სინათლე
URL: URL:
Use Even-Odd Fill Rule ლუწ-კენტის შევსების წესის გამოყენება
Use Non-Zero Winding Fill Rule არანულოვანი შემოვლის შევსების წესის გამოყენება
Brush Libraries (on the roadmap) ფუნჯების ბიბლიოთეკები (სამომავლო გეგმაშია)
List View სიის ხედი
New Art Brush from Selection ახალი მხატვრული ფუნჯი მონიშნულიდან
New Bristle Brush ახალი ჯაგრის ფუნჯი
@@ -5474,6 +5473,34 @@ axis Y Axis Y ღერძი
Rectangular Grid Tool Options მართკუთხა ბადის ხელსაწყოს პარამეტრები
Polar Grid Tool Options პოლარული ბადის ხელსაწყოს პარამეტრები
Fill Line ხაზის შევსება
Add to Brushes ფუნჯებში დამატება
Brush Library ფუნჯების ბიბლიოთეკა
Save Brush Library ფუნჯების ბიბლიოთეკის შენახვა
Save Brushes… ფუნჯების შენახვა…
Open Brush Library ფუნჯების ბიბლიოთეკის გახსნა
Selected Brushes Only მხოლოდ მონიშნული ფუნჯები
Brush Libraries Menu ფუნჯების ბიბლიოთეკების მენიუ
User Brush Library 1 მომხმარებლის ფუნჯების ბიბლიოთეკა 1
User Brush Library 2 მომხმარებლის ფუნჯების ბიბლიოთეკა 2
User Brush Library 3 მომხმარებლის ფუნჯების ბიბლიოთეკა 3
User Brush Library 4 მომხმარებლის ფუნჯების ბიბლიოთეკა 4
User Brush Library 5 მომხმარებლის ფუნჯების ბიბლიოთეკა 5
User Brush Library 6 მომხმარებლის ფუნჯების ბიბლიოთეკა 6
User Brush Library 7 მომხმარებლის ფუნჯების ბიბლიოთეკა 7
User Brush Library 8 მომხმარებლის ფუნჯების ბიბლიოთეკა 8
User Brush Library 9 მომხმარებლის ფუნჯების ბიბლიოთეკა 9
User Brush Library 10 მომხმარებლის ფუნჯების ბიბლიოთეკა 10
Art Ribbons ნახატის ლენტები
Calligraphic Nibs კალიგრაფიული წვერები
Pattern Borders პატერნის ბორდიურები
Scatter Shapes გაბნეული ფიგურები
Save Brush Library… ფუნჯების ბიბლიოთეკის შენახვა…
@msg `{_1}` has no brushes to apply `{_1}`-ში გამოსაყენებელი ფუნჯები არ არის
@msg missing `library` (see brush.libraries) `library` აკლია (იხ. brush.libraries)
@msg no brush `{n}` ფუნჯი `{n}` არ არის
@msg no brush `{n}` in `{_1}` `{_1}`-ში ფუნჯი `{n}` არ არის
@msg no brush library `{key}` (see brush.libraries) ფუნჯების ბიბლიოთეკა `{key}` არ არის (იხ. brush.libraries)
@msg the document has no brushes to save დოკუმენტში შესანახი ფუნჯები არ არის
@msg no empty artboards to delete წასაშლელი ცარიელი სამუშაო დაფები არ არის
Scale Presets მასშტაბის წინასწარ დაყენებული მნიშვნელობები
@msg a {width}×{height} screenshot's byte count overflows {width}×{height} ეკრანის სურათის ბაიტების რაოდენობა გადაივსო
Can't render this file because it contains an unexpected character in line 2 and column 73.
+28 -1
View File
@@ -977,7 +977,6 @@ font-technology Variable 가변
Browser 브라우저
Brush Affects: 브러시 적용 대상:
Brush Definition 브러시 정의
Brush Libraries (on the roadmap) 브러시 라이브러리(개발 예정)
Brush Options… 브러시 옵션…
Built-in presets are read-only: New… starts an editable copy. 기본 제공 프리셋은 읽기 전용입니다. 새로 만들기…를 누르면 편집 가능한 복사본이 만들어집니다.
Built-in presets don't change: New makes a copy you can edit. 기본 제공 프리셋은 변경되지 않습니다. 새로 만들기를 누르면 편집 가능한 복사본이 만들어집니다.
@@ -5137,6 +5136,34 @@ axis Y Axis Y축
Rectangular Grid Tool Options 사각형 격자 도구 옵션
Polar Grid Tool Options 극좌표 격자 도구 옵션
Fill Line 선 채우기
Add to Brushes 브러시에 추가
Brush Library 브러시 라이브러리
Save Brush Library 브러시 라이브러리 저장
Save Brushes… 브러시 저장…
Open Brush Library 브러시 라이브러리 열기
Selected Brushes Only 선택한 브러시만
Brush Libraries Menu 브러시 라이브러리 메뉴
User Brush Library 1 사용자 브러시 라이브러리 1
User Brush Library 2 사용자 브러시 라이브러리 2
User Brush Library 3 사용자 브러시 라이브러리 3
User Brush Library 4 사용자 브러시 라이브러리 4
User Brush Library 5 사용자 브러시 라이브러리 5
User Brush Library 6 사용자 브러시 라이브러리 6
User Brush Library 7 사용자 브러시 라이브러리 7
User Brush Library 8 사용자 브러시 라이브러리 8
User Brush Library 9 사용자 브러시 라이브러리 9
User Brush Library 10 사용자 브러시 라이브러리 10
Art Ribbons 아트 리본
Calligraphic Nibs 서예 펜촉
Pattern Borders 패턴 테두리
Scatter Shapes 흩뿌리기 모양
Save Brush Library… 브러시 라이브러리 저장…
@msg `{_1}` has no brushes to apply `{_1}`에 적용할 브러시가 없습니다
@msg missing `library` (see brush.libraries) `library`가 없습니다(brush.libraries 참조)
@msg no brush `{n}` 브러시 `{n}`이(가) 없습니다
@msg no brush `{n}` in `{_1}` `{_1}`에 브러시 `{n}`이(가) 없습니다
@msg no brush library `{key}` (see brush.libraries) 브러시 라이브러리 `{key}`이(가) 없습니다(brush.libraries 참조)
@msg the document has no brushes to save 문서에 저장할 브러시가 없습니다
@msg no empty artboards to delete 삭제할 빈 아트보드가 없습니다
Scale Presets 크기 조절 사전 설정
@msg a {width}×{height} screenshot's byte count overflows {width}×{height} 스크린샷의 바이트 수가 넘칩니다
Can't render this file because it contains an unexpected character in line 2 and column 73.
+22 -1
View File
@@ -1316,7 +1316,6 @@ lighting Light Licht
URL: URL:
Use Even-Odd Fill Rule Even-odd-vulregel gebruiken
Use Non-Zero Winding Fill Rule Non-zero-winding-vulregel gebruiken
Brush Libraries (on the roadmap) Penseelbibliotheken (op de roadmap)
List View Lijstweergave
New Art Brush from Selection Nieuw kunstpenseel uit selectie
New Bristle Brush Nieuw borstelpenseel
@@ -5542,6 +5541,28 @@ axis Y Axis Y-as
Rectangular Grid Tool Options Opties rechthoekig-rastergereedschap
Polar Grid Tool Options Opties polair-rastergereedschap
Fill Line Lijn vullen
Add to Brushes Aan penselen toevoegen
Brush Library Penseelbibliotheek
Save Brush Library Penseelbibliotheek opslaan
Save Brushes… Penselen opslaan…
Open Brush Library Penseelbibliotheek openen
Selected Brushes Only Alleen geselecteerde penselen
Brush Libraries Menu Menu penseelbibliotheken
User Brush Library 1 Penseelbibliotheek van gebruiker 1
User Brush Library 2 Penseelbibliotheek van gebruiker 2
User Brush Library 3 Penseelbibliotheek van gebruiker 3
User Brush Library 4 Penseelbibliotheek van gebruiker 4
User Brush Library 5 Penseelbibliotheek van gebruiker 5
User Brush Library 6 Penseelbibliotheek van gebruiker 6
User Brush Library 7 Penseelbibliotheek van gebruiker 7
User Brush Library 8 Penseelbibliotheek van gebruiker 8
User Brush Library 9 Penseelbibliotheek van gebruiker 9
User Brush Library 10 Penseelbibliotheek van gebruiker 10
Art Ribbons Illustratielinten
Calligraphic Nibs Kalligrafische penpunten
Pattern Borders Patroonranden
Scatter Shapes Strooivormen
Save Brush Library… Penseelbibliotheek opslaan…
@msg no empty artboards to delete geen lege tekengebieden om te verwijderen
Scale Presets Schaalvoorinstellingen
Live preview Live voorbeeld
Can't render this file because it contains an unexpected character in line 2 and column 73.
+28 -1
View File
@@ -1298,7 +1298,6 @@ lighting Light Światło
URL: URL:
Use Even-Odd Fill Rule Użyj reguły wypełniania parzysty-nieparzysty
Use Non-Zero Winding Fill Rule Użyj reguły wypełniania niezerowego
Brush Libraries (on the roadmap) Biblioteki pędzli (w planach)
List View Widok listy
New Art Brush from Selection Nowy pędzel artystyczny z zaznaczenia
New Bristle Brush Nowy pędzel włosiankowy
@@ -5410,6 +5409,34 @@ axis Y Axis Oś Y
Rectangular Grid Tool Options Opcje narzędzia Siatka prostokątna
Polar Grid Tool Options Opcje narzędzia Siatka biegunowa
Fill Line Wypełnij odcinek
Add to Brushes Dodaj do pędzli
Brush Library Biblioteka pędzli
Save Brush Library Zapisz bibliotekę pędzli
Save Brushes… Zapisz pędzle…
Open Brush Library Otwórz bibliotekę pędzli
Selected Brushes Only Tylko zaznaczone pędzle
Brush Libraries Menu Menu bibliotek pędzli
User Brush Library 1 Biblioteka pędzli użytkownika 1
User Brush Library 2 Biblioteka pędzli użytkownika 2
User Brush Library 3 Biblioteka pędzli użytkownika 3
User Brush Library 4 Biblioteka pędzli użytkownika 4
User Brush Library 5 Biblioteka pędzli użytkownika 5
User Brush Library 6 Biblioteka pędzli użytkownika 6
User Brush Library 7 Biblioteka pędzli użytkownika 7
User Brush Library 8 Biblioteka pędzli użytkownika 8
User Brush Library 9 Biblioteka pędzli użytkownika 9
User Brush Library 10 Biblioteka pędzli użytkownika 10
Art Ribbons Wstęgi artystyczne
Calligraphic Nibs Stalówki kaligraficzne
Pattern Borders Obramowania wzorków
Scatter Shapes Kształty rozproszone
Save Brush Library… Zapisz bibliotekę pędzli…
@msg `{_1}` has no brushes to apply `{_1}` nie ma pędzli do zastosowania
@msg missing `library` (see brush.libraries) brak `library` (zobacz brush.libraries)
@msg no brush `{n}` brak pędzla `{n}`
@msg no brush `{n}` in `{_1}` brak pędzla `{n}` w `{_1}`
@msg no brush library `{key}` (see brush.libraries) brak biblioteki pędzli `{key}` (zobacz brush.libraries)
@msg the document has no brushes to save dokument nie ma pędzli do zapisania
@msg no empty artboards to delete brak pustych obszarów kompozycji do usunięcia
Scale Presets Ustawienia wstępne skali
@msg a {width}×{height} screenshot's byte count overflows liczba bajtów zrzutu ekranu {width}×{height} przekracza limit
Can't render this file because it contains an unexpected character in line 2 and column 73.
+28 -1
View File
@@ -312,7 +312,6 @@
Brush Affects: Pincel Afeta:
Brush Definition Definição do Pincel
Brush Libraries Bibliotecas de Pincéis
Brush Libraries (on the roadmap) Bibliotecas de Pincéis (no roadmap)
Brush Options… Opções de Pincel…
Brush Strokes Traços de Pincel
Brushes Pincéis
@@ -5475,6 +5474,34 @@ axis Y Axis Eixo Y
Rectangular Grid Tool Options Opções da Ferramenta Grade Retangular
Polar Grid Tool Options Opções da Ferramenta Grade Polar
Fill Line Preencher linha
Add to Brushes Adicionar aos Pincéis
Brush Library Biblioteca de Pincéis
Save Brush Library Salvar Biblioteca de Pincéis
Save Brushes… Salvar Pincéis…
Open Brush Library Abrir Biblioteca de Pincéis
Selected Brushes Only Somente Pincéis Selecionados
Brush Libraries Menu Menu de Bibliotecas de Pincéis
User Brush Library 1 Biblioteca de Pincéis do Usuário 1
User Brush Library 2 Biblioteca de Pincéis do Usuário 2
User Brush Library 3 Biblioteca de Pincéis do Usuário 3
User Brush Library 4 Biblioteca de Pincéis do Usuário 4
User Brush Library 5 Biblioteca de Pincéis do Usuário 5
User Brush Library 6 Biblioteca de Pincéis do Usuário 6
User Brush Library 7 Biblioteca de Pincéis do Usuário 7
User Brush Library 8 Biblioteca de Pincéis do Usuário 8
User Brush Library 9 Biblioteca de Pincéis do Usuário 9
User Brush Library 10 Biblioteca de Pincéis do Usuário 10
Art Ribbons Fitas de Arte
Calligraphic Nibs Penas Caligráficas
Pattern Borders Bordas de Padrão
Scatter Shapes Formas de Dispersão
Save Brush Library… Salvar Biblioteca de Pincéis…
@msg `{_1}` has no brushes to apply `{_1}` não tem pincéis para aplicar
@msg missing `library` (see brush.libraries) falta `library` (consulte brush.libraries)
@msg no brush `{n}` nenhum pincel `{n}`
@msg no brush `{n}` in `{_1}` nenhum pincel `{n}` em `{_1}`
@msg no brush library `{key}` (see brush.libraries) nenhuma biblioteca de pincéis `{key}` (consulte brush.libraries)
@msg the document has no brushes to save o documento não tem pincéis para salvar
@msg no empty artboards to delete não há mesas de trabalho vazias para excluir
Scale Presets Predefinições de escala
@msg a {width}×{height} screenshot's byte count overflows a contagem de bytes de uma captura {width}×{height} transborda
Can't render this file because it contains an unexpected character in line 6 and column 47.
+28 -1
View File
@@ -340,7 +340,6 @@ font-technology Variable Вариативные
Brush Affects: Кисть влияет на:
Brush Definition Определение кисти
Brush Libraries Библиотеки кистей
Brush Libraries (on the roadmap) Библиотеки кистей (в плане развития)
Brush Options… Параметры кисти…
Brush Strokes Мазки кисти
Brushes Кисти
@@ -5170,6 +5169,34 @@ axis Y Axis Ось Y
Rectangular Grid Tool Options Параметры инструмента «Прямоугольная сетка»
Polar Grid Tool Options Параметры инструмента «Полярная сетка»
Fill Line Заливка отрезка
Add to Brushes Добавить в кисти
Brush Library Библиотека кистей
Save Brush Library Сохранить библиотеку кистей
Save Brushes… Сохранить кисти…
Open Brush Library Открыть библиотеку кистей
Selected Brushes Only Только выбранные кисти
Brush Libraries Menu Меню «Библиотеки кистей»
User Brush Library 1 Пользовательская библиотека кистей 1
User Brush Library 2 Пользовательская библиотека кистей 2
User Brush Library 3 Пользовательская библиотека кистей 3
User Brush Library 4 Пользовательская библиотека кистей 4
User Brush Library 5 Пользовательская библиотека кистей 5
User Brush Library 6 Пользовательская библиотека кистей 6
User Brush Library 7 Пользовательская библиотека кистей 7
User Brush Library 8 Пользовательская библиотека кистей 8
User Brush Library 9 Пользовательская библиотека кистей 9
User Brush Library 10 Пользовательская библиотека кистей 10
Art Ribbons Художественные ленты
Calligraphic Nibs Каллиграфические перья
Pattern Borders Узорные каймы
Scatter Shapes Рассеянные фигуры
Save Brush Library… Сохранить библиотеку кистей…
@msg `{_1}` has no brushes to apply В `{_1}` нет кистей для применения
@msg missing `library` (see brush.libraries) не указан `library` (см. brush.libraries)
@msg no brush `{n}` нет кисти `{n}`
@msg no brush `{n}` in `{_1}` нет кисти `{n}` в `{_1}`
@msg no brush library `{key}` (see brush.libraries) нет библиотеки кистей `{key}` (см. brush.libraries)
@msg the document has no brushes to save в документе нет кистей для сохранения
@msg no empty artboards to delete нет пустых монтажных областей для удаления
Scale Presets Наборы масштаба
@msg a {width}×{height} screenshot's byte count overflows количество байтов снимка {width}×{height} переполняется
Can't render this file because it contains an unexpected character in line 2 and column 73.
+28 -1
View File
@@ -331,7 +331,6 @@ font-technology Variable Variabla
Brush Affects: Penseln påverkar:
Brush Definition Penseldefinition
Brush Libraries Penselbibliotek
Brush Libraries (on the roadmap) Penselbibliotek (planeras)
Brush Options… Penselalternativ…
Brush Strokes Penseldrag
Brushes Penslar
@@ -5582,6 +5581,34 @@ axis Y Axis Y-axel
Rectangular Grid Tool Options Alternativ för verktyget för rektangulärt rutnät
Polar Grid Tool Options Alternativ för verktyget för polärt rutnät
Fill Line Fyll linje
Add to Brushes Lägg till i penslar
Brush Library Penselbibliotek
Save Brush Library Spara penselbibliotek
Save Brushes… Spara penslar…
Open Brush Library Öppna penselbibliotek
Selected Brushes Only Endast markerade penslar
Brush Libraries Menu Meny för penselbibliotek
User Brush Library 1 Användarbibliotek för penslar 1
User Brush Library 2 Användarbibliotek för penslar 2
User Brush Library 3 Användarbibliotek för penslar 3
User Brush Library 4 Användarbibliotek för penslar 4
User Brush Library 5 Användarbibliotek för penslar 5
User Brush Library 6 Användarbibliotek för penslar 6
User Brush Library 7 Användarbibliotek för penslar 7
User Brush Library 8 Användarbibliotek för penslar 8
User Brush Library 9 Användarbibliotek för penslar 9
User Brush Library 10 Användarbibliotek för penslar 10
Art Ribbons Konstband
Calligraphic Nibs Kalligrafiska pennspetsar
Pattern Borders Mönsterbårder
Scatter Shapes Spridningsformer
Save Brush Library… Spara penselbibliotek…
@msg `{_1}` has no brushes to apply `{_1}` har inga penslar att använda
@msg missing `library` (see brush.libraries) `library` saknas (se brush.libraries)
@msg no brush `{n}` ingen pensel `{n}`
@msg no brush `{n}` in `{_1}` ingen pensel `{n}` i `{_1}`
@msg no brush library `{key}` (see brush.libraries) inget penselbibliotek `{key}` (se brush.libraries)
@msg the document has no brushes to save dokumentet har inga penslar att spara
@msg no empty artboards to delete inga tomma ritytor att radera
Scale Presets Skalningsförval
@msg a {width}×{height} screenshot's byte count overflows byteantalet för en skärmbild på {width}×{height} överstiger gränsen
Can't render this file because it contains an unexpected character in line 5 and column 25.
+28 -1
View File
@@ -326,7 +326,6 @@ font-technology Variable Değişken
Brush Affects: Fırça Etkileri:
Brush Definition Fırça Tanımı
Brush Libraries Fırça Kitaplıkları
Brush Libraries (on the roadmap) Fırça Kitaplıkları (yol haritasında)
Brush Options… Fırça Seçenekleri…
Brush Strokes Fırça Darbeleri
Brushes Fırçalar
@@ -5515,6 +5514,34 @@ axis Y Axis Y ekseni
Rectangular Grid Tool Options Dikdörtgen Izgara Aracı Seçenekleri
Polar Grid Tool Options Yuvarlak Izgara Aracı Seçenekleri
Fill Line Çizgiyi doldur
Add to Brushes Fırçalara Ekle
Brush Library Fırça Kitaplığı
Save Brush Library Fırça Kitaplığını Kaydet
Save Brushes… Fırçaları Kaydet…
Open Brush Library Fırça Kitaplığını Aç
Selected Brushes Only Yalnızca Seçilen Fırçalar
Brush Libraries Menu Fırça Kitaplıkları Menüsü
User Brush Library 1 Kullanıcı Fırça Kitaplığı 1
User Brush Library 2 Kullanıcı Fırça Kitaplığı 2
User Brush Library 3 Kullanıcı Fırça Kitaplığı 3
User Brush Library 4 Kullanıcı Fırça Kitaplığı 4
User Brush Library 5 Kullanıcı Fırça Kitaplığı 5
User Brush Library 6 Kullanıcı Fırça Kitaplığı 6
User Brush Library 7 Kullanıcı Fırça Kitaplığı 7
User Brush Library 8 Kullanıcı Fırça Kitaplığı 8
User Brush Library 9 Kullanıcı Fırça Kitaplığı 9
User Brush Library 10 Kullanıcı Fırça Kitaplığı 10
Art Ribbons Sanat Şeritleri
Calligraphic Nibs Kaligrafi Uçları
Pattern Borders Desen Kenarlıkları
Scatter Shapes Dağılım Şekilleri
Save Brush Library… Fırça Kitaplığını Kaydet…
@msg `{_1}` has no brushes to apply `{_1}` içinde uygulanacak fırça yok
@msg missing `library` (see brush.libraries) `library` eksik (bkz. brush.libraries)
@msg no brush `{n}` `{n}` fırçası yok
@msg no brush `{n}` in `{_1}` `{_1}` içinde `{n}` fırçası yok
@msg no brush library `{key}` (see brush.libraries) `{key}` fırça kitaplığı yok (bkz. brush.libraries)
@msg the document has no brushes to save belgede kaydedilecek fırça yok
@msg no empty artboards to delete silinecek boş çalışma yüzeyi yok
Scale Presets Ölçek ön ayarları
@msg a {width}×{height} screenshot's byte count overflows {width}×{height} ekran görüntüsünün bayt sayısı taşıyor
Can't render this file because it contains an unexpected character in line 5 and column 25.
+28 -1
View File
@@ -336,7 +336,6 @@ font-technology Variable Варіативні
Brush Affects: Пензель впливає на:
Brush Definition Визначення пензля
Brush Libraries Бібліотеки пензлів
Brush Libraries (on the roadmap) Бібліотеки пензлів (у плані розвитку)
Brush Options… Параметри пензля…
Brush Strokes Мазки пензля
Brushes Пензлі
@@ -5154,6 +5153,34 @@ axis Y Axis Вісь Y
Rectangular Grid Tool Options Параметри інструменту «Прямокутна сітка»
Polar Grid Tool Options Параметри інструменту «Полярна сітка»
Fill Line Заливка відрізка
Add to Brushes Додати до пензлів
Brush Library Бібліотека пензлів
Save Brush Library Зберегти бібліотеку пензлів
Save Brushes… Зберегти пензлі…
Open Brush Library Відкрити бібліотеку пензлів
Selected Brushes Only Тільки вибрані пензлі
Brush Libraries Menu Меню "Бібліотеки пензлів"
User Brush Library 1 Користувацька бібліотека пензлів 1
User Brush Library 2 Користувацька бібліотека пензлів 2
User Brush Library 3 Користувацька бібліотека пензлів 3
User Brush Library 4 Користувацька бібліотека пензлів 4
User Brush Library 5 Користувацька бібліотека пензлів 5
User Brush Library 6 Користувацька бібліотека пензлів 6
User Brush Library 7 Користувацька бібліотека пензлів 7
User Brush Library 8 Користувацька бібліотека пензлів 8
User Brush Library 9 Користувацька бібліотека пензлів 9
User Brush Library 10 Користувацька бібліотека пензлів 10
Art Ribbons Художні стрічки
Calligraphic Nibs Каліграфічні пера
Pattern Borders Візерункові облямівки
Scatter Shapes Розсіяні фігури
Save Brush Library… Зберегти бібліотеку пензлів…
@msg `{_1}` has no brushes to apply У `{_1}` немає пензлів для застосування
@msg missing `library` (see brush.libraries) не вказано `library` (див. brush.libraries)
@msg no brush `{n}` немає пензля `{n}`
@msg no brush `{n}` in `{_1}` немає пензля `{n}` у `{_1}`
@msg no brush library `{key}` (see brush.libraries) немає бібліотеки пензлів `{key}` (див. brush.libraries)
@msg the document has no brushes to save у документі немає пензлів для збереження
@msg no empty artboards to delete немає порожніх монтажних областей для видалення
Scale Presets Набори масштабу
@msg a {width}×{height} screenshot's byte count overflows кількість байтів знімка {width}×{height} переповнюється
Can't render this file because it contains an unexpected character in line 748 and column 40.
+28 -1
View File
@@ -395,7 +395,6 @@ font-technology Variable Biến thiên
Brush Art Đối tượng của cọ
Brush Definition Định nghĩa cọ
Brush Libraries Thư viện cọ
Brush Libraries (on the roadmap) Thư viện cọ (trong lộ trình)
Brush Options… Tùy chọn cọ…
Brush Scale Options Tùy chọn tỷ lệ cọ
Brush Strokes Nét cọ
@@ -5154,6 +5153,34 @@ axis Y Axis Trục Y
Rectangular Grid Tool Options Tùy chọn công cụ Lưới chữ nhật
Polar Grid Tool Options Tùy chọn công cụ Lưới cực
Fill Line Tô đoạn thẳng
Add to Brushes Thêm vào cọ
Brush Library Thư viện cọ
Save Brush Library Lưu thư viện cọ
Save Brushes… Lưu cọ…
Open Brush Library Mở thư viện cọ
Selected Brushes Only Chỉ cọ đã chọn
Brush Libraries Menu Menu thư viện cọ
User Brush Library 1 Thư viện cọ người dùng 1
User Brush Library 2 Thư viện cọ người dùng 2
User Brush Library 3 Thư viện cọ người dùng 3
User Brush Library 4 Thư viện cọ người dùng 4
User Brush Library 5 Thư viện cọ người dùng 5
User Brush Library 6 Thư viện cọ người dùng 6
User Brush Library 7 Thư viện cọ người dùng 7
User Brush Library 8 Thư viện cọ người dùng 8
User Brush Library 9 Thư viện cọ người dùng 9
User Brush Library 10 Thư viện cọ người dùng 10
Art Ribbons Dải băng nghệ thuật
Calligraphic Nibs Ngòi bút thư pháp
Pattern Borders Viền họa tiết
Scatter Shapes Hình phân tán
Save Brush Library… Lưu thư viện cọ…
@msg `{_1}` has no brushes to apply `{_1}` không có cọ nào để áp dụng
@msg missing `library` (see brush.libraries) thiếu `library` (xem brush.libraries)
@msg no brush `{n}` không có cọ `{n}`
@msg no brush `{n}` in `{_1}` không có cọ `{n}` trong `{_1}`
@msg no brush library `{key}` (see brush.libraries) không có thư viện cọ `{key}` (xem brush.libraries)
@msg the document has no brushes to save tài liệu không có cọ nào để lưu
@msg no empty artboards to delete không có bảng vẽ trống nào để xóa
Scale Presets Cài đặt sẵn tỉ lệ
@msg a {width}×{height} screenshot's byte count overflows số byte của ảnh chụp {width}×{height} bị tràn
Can't render this file because it contains an unexpected character in line 4052 and column 10.
+28 -1
View File
@@ -291,7 +291,6 @@
Brush Affects: 画笔影响:
Brush Definition 画笔定义
Brush Libraries 画笔库
Brush Libraries (on the roadmap) 画笔库 (规划中)
Brush Options… 画笔选项…
Brush Strokes 画笔描边
Brushes 画笔
@@ -5397,6 +5396,34 @@ axis Y Axis Y 轴
Rectangular Grid Tool Options 矩形网格工具选项
Polar Grid Tool Options 极坐标网格工具选项
Fill Line 填充线段
Add to Brushes 添加到画笔
Brush Library 画笔库
Save Brush Library 保存画笔库
Save Brushes… 保存画笔…
Open Brush Library 打开画笔库
Selected Brushes Only 仅限选择的画笔
Brush Libraries Menu 画笔库菜单
User Brush Library 1 用户画笔库 1
User Brush Library 2 用户画笔库 2
User Brush Library 3 用户画笔库 3
User Brush Library 4 用户画笔库 4
User Brush Library 5 用户画笔库 5
User Brush Library 6 用户画笔库 6
User Brush Library 7 用户画笔库 7
User Brush Library 8 用户画笔库 8
User Brush Library 9 用户画笔库 9
User Brush Library 10 用户画笔库 10
Art Ribbons 艺术丝带
Calligraphic Nibs 书法笔尖
Pattern Borders 图案边框
Scatter Shapes 散布形状
Save Brush Library… 保存画笔库…
@msg `{_1}` has no brushes to apply `{_1}` 中没有可应用的画笔
@msg missing `library` (see brush.libraries) 缺少 `library`(请参阅 brush.libraries)
@msg no brush `{n}` 没有画笔 `{n}`
@msg no brush `{n}` in `{_1}` `{_1}` 中没有画笔 `{n}`
@msg no brush library `{key}` (see brush.libraries) 没有画笔库 `{key}`(请参阅 brush.libraries)
@msg the document has no brushes to save 文档中没有可保存的画笔
@msg no empty artboards to delete 没有可删除的空白画板
Scale Presets 缩放预设
@msg a {width}×{height} screenshot's byte count overflows {width}×{height} 截图的字节数溢出
Can't render this file because it contains an unexpected character in line 2 and column 73.
+28 -1
View File
@@ -304,7 +304,6 @@ font-technology Variable 可變
Brush Affects: 筆刷影響:
Brush Definition 筆刷定義
Brush Libraries 筆刷資料庫
Brush Libraries (on the roadmap) 筆刷資料庫 (規劃中)
Brush Options… 筆刷選項…
Brush Strokes 筆刷筆畫
Brushes 筆刷
@@ -5193,6 +5192,34 @@ axis Y Axis Y 軸
Rectangular Grid Tool Options 矩形格線工具選項
Polar Grid Tool Options 極座標格點工具選項
Fill Line 填色線段
Add to Brushes 新增至筆刷
Brush Library 筆刷資料庫
Save Brush Library 儲存筆刷資料庫
Save Brushes… 儲存筆刷…
Open Brush Library 開啟筆刷資料庫
Selected Brushes Only 僅限選取的筆刷
Brush Libraries Menu 筆刷資料庫選單
User Brush Library 1 使用者筆刷資料庫 1
User Brush Library 2 使用者筆刷資料庫 2
User Brush Library 3 使用者筆刷資料庫 3
User Brush Library 4 使用者筆刷資料庫 4
User Brush Library 5 使用者筆刷資料庫 5
User Brush Library 6 使用者筆刷資料庫 6
User Brush Library 7 使用者筆刷資料庫 7
User Brush Library 8 使用者筆刷資料庫 8
User Brush Library 9 使用者筆刷資料庫 9
User Brush Library 10 使用者筆刷資料庫 10
Art Ribbons 藝術緞帶
Calligraphic Nibs 書法筆尖
Pattern Borders 圖樣邊框
Scatter Shapes 散佈形狀
Save Brush Library… 儲存筆刷資料庫…
@msg `{_1}` has no brushes to apply `{_1}` 中沒有可套用的筆刷
@msg missing `library` (see brush.libraries) 未提供 `library`(請參閱 brush.libraries)
@msg no brush `{n}` 沒有筆刷 `{n}`
@msg no brush `{n}` in `{_1}` `{_1}` 中沒有筆刷 `{n}`
@msg no brush library `{key}` (see brush.libraries) 沒有筆刷資料庫 `{key}`(請參閱 brush.libraries)
@msg the document has no brushes to save 文件中沒有可儲存的筆刷
@msg no empty artboards to delete 沒有可刪除的空白工作區域
Scale Presets 縮放預設集
@msg a {width}×{height} screenshot's byte count overflows {width}×{height} 螢幕擷取畫面的位元組數溢出
Can't render this file because it contains an unexpected character in line 6 and column 47.
+47 -7
View File
@@ -43,11 +43,6 @@ fn todos(label: &'static str, sc: &'static str) -> Item {
fn sub(label: &'static str, items: Vec<Item>) -> Item {
Item::Sub(label, items)
}
/// Window → … Libraries until the code-generated libraries land (disabled entries).
fn library_placeholders() -> Vec<Item> {
vec![todo("Built-in Libraries"), todo("User Defined"), Sep, todo("Other Library…")]
}
use Item::Sep;
/// UI-level commands: (id, label, shortcut, params doc).
@@ -808,6 +803,34 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
("window.userSymbolLibrary8", "User Symbol Library 8", "", "{} open the 8. User Defined symbol library"),
("window.userSymbolLibrary9", "User Symbol Library 9", "", "{} open the 9. User Defined symbol library"),
("window.userSymbolLibrary10", "User Symbol Library 10", "", "{} open the 10. User Defined symbol library"),
(
"window.brushLibrary",
"Brush Library",
"",
"{library: id or name (see brush.libraries) | null (close)} open a brush library in a library panel of its own (UI state `library_panels`, kind brushes); clicking a brush there runs brush.addFromLibrary (with apply when paths are selected), dragging one onto a path runs it with apply on that path, dragging one onto the Brushes panel adds it → {open, panel, name, count}",
),
(
"window.brushLibrary.other",
"Other Library…",
"",
"{path?} (default: pick a file) load the brushes of another document or a saved brush library (engine: brush.loadLibrary), and open them in a library panel",
),
(
"ui.saveBrushLibrary",
"Save Brushes…",
"",
"{names?: [the brushes selected in the Brushes panel]} open Save Brush Library (dialog `saveBrushLibrary`: name, user: save to the user library folder, selectedOnly); OK runs brush.saveLibrary",
),
("window.userBrushLibrary1", "User Brush Library 1", "", "{} open the 1. User Defined brush library (brush.libraries, category user)"),
("window.userBrushLibrary2", "User Brush Library 2", "", "{} open the 2. User Defined brush library"),
("window.userBrushLibrary3", "User Brush Library 3", "", "{} open the 3. User Defined brush library"),
("window.userBrushLibrary4", "User Brush Library 4", "", "{} open the 4. User Defined brush library"),
("window.userBrushLibrary5", "User Brush Library 5", "", "{} open the 5. User Defined brush library"),
("window.userBrushLibrary6", "User Brush Library 6", "", "{} open the 6. User Defined brush library"),
("window.userBrushLibrary7", "User Brush Library 7", "", "{} open the 7. User Defined brush library"),
("window.userBrushLibrary8", "User Brush Library 8", "", "{} open the 8. User Defined brush library"),
("window.userBrushLibrary9", "User Brush Library 9", "", "{} open the 9. User Defined brush library"),
("window.userBrushLibrary10", "User Brush Library 10", "", "{} open the 10. User Defined brush library"),
(
"ui.expandDialog",
"Expand…",
@@ -1795,6 +1818,16 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
Some(l) => crate::panels::symbols::open_library(app, &json!({ "library": l.id })),
None => Err("no such user library".into()),
},
"window.brushLibrary" => crate::panels::brushes::open_library(app, p),
"window.brushLibrary.other" => crate::panels::brushes::other_library(app, s("path")),
"ui.saveBrushLibrary" => {
let names = p.get("names").and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_str).map(str::to_string).collect());
crate::dialogs::save_brush_library::open(app, names.unwrap_or_default())
}
id if id.starts_with(crate::panels::brushes::USER_SLOT) => match crate::panels::brushes::user_library(app, id) {
Some(l) => crate::panels::brushes::open_library(app, &json!({ "library": l.id })),
None => Err("no such user library".into()),
},
"ui.expandDialog" => crate::dialogs::expand::open(app),
"ui.simplifyAuto" => crate::dialogs::simplify_slider::auto(app),
"ui.simplifyMoreOptions" => crate::dialogs::simplify_slider::more_options(app),
@@ -2110,6 +2143,9 @@ pub fn dynamic_label(app: &VectorcraftApp, id: &str, label: &str) -> String {
id if id.starts_with(crate::panels::symbols::USER_SLOT) => {
crate::panels::symbols::user_library(app, id).map_or_else(|| "—".into(), |l| l.name)
}
id if id.starts_with(crate::panels::brushes::USER_SLOT) => {
crate::panels::brushes::user_library(app, id).map_or_else(|| "—".into(), |l| l.name)
}
id if id.starts_with(crate::dialogs::perspective_presets::SLOT) => {
crate::dialogs::perspective_presets::slot_preset(app, id).unwrap_or_else(|| "—".into())
}
@@ -2125,13 +2161,14 @@ pub fn dynamic_label(app: &VectorcraftApp, id: &str, label: &str) -> String {
/// plug-in? Such names are shown as they are, never translated (a workspace the user calls
/// "Layers" stays "Layers").
fn shows_a_name(id: &str, label: &str) -> bool {
const SLOTS: [&str; 7] = [
const SLOTS: [&str; 8] = [
"file.openRecent",
"view.goto",
"type.recentFont",
crate::panels::swatches::USER_SLOT,
crate::panels::graphic_styles::USER_SLOT,
crate::panels::symbols::USER_SLOT,
crate::panels::brushes::USER_SLOT,
crate::dialogs::perspective_presets::SLOT,
];
SLOTS.iter().any(|s| id.starts_with(s))
@@ -2218,6 +2255,7 @@ fn hidden_when_disabled(id: &str) -> bool {
|| id.starts_with(crate::panels::swatches::USER_SLOT)
|| id.starts_with(crate::panels::graphic_styles::USER_SLOT)
|| id.starts_with(crate::panels::symbols::USER_SLOT)
|| id.starts_with(crate::panels::brushes::USER_SLOT)
|| id.starts_with(crate::dialogs::perspective_presets::SLOT)
}
@@ -2534,6 +2572,8 @@ pub fn enabled(app: &VectorcraftApp, id: &str) -> bool {
id if id.starts_with(crate::panels::graphic_styles::USER_SLOT) => crate::panels::graphic_styles::user_library(app, id).is_some(),
"ui.saveSymbolLibrary" => app.session.active().is_some_and(|d| !d.doc.symbols.is_empty()),
id if id.starts_with(crate::panels::symbols::USER_SLOT) => crate::panels::symbols::user_library(app, id).is_some(),
"ui.saveBrushLibrary" => crate::panels::brushes::has_brushes(app),
id if id.starts_with(crate::panels::brushes::USER_SLOT) => crate::panels::brushes::user_library(app, id).is_some(),
"ui.expandDialog" => app.session.active().is_some_and(|d| !d.selection.is_empty()),
"ui.spotColors" => app.session.active().is_some(),
"ui.menuDialog" => app.session.active().is_some_and(|d| !d.selection.is_empty()),
@@ -3232,7 +3272,7 @@ pub fn menu_tree_named(english_names: bool) -> Vec<(&'static str, Vec<Item>)> {
),
panel("Variables", crate::panels::variables::ID),
Sep,
sub("Brush Libraries", library_placeholders()),
sub("Brush Libraries", crate::panels::brushes::window_menu()),
sub("Graphic Style Libraries", crate::panels::graphic_styles::window_menu()),
sub("Swatch Libraries", crate::panels::swatches::window_menu()),
sub("Symbol Libraries", crate::panels::symbols::window_menu()),
+642 -43
View File
@@ -2,66 +2,155 @@
//! applies it to the selected paths (and makes it the Paintbrush's current brush); double-clicking
//! one opens its Brush Options. Shift-click selects a range of brushes, Cmd/Ctrl-click adds or
//! removes one, and the panel menu's Select All Unused selects the brushes no stroke paints with,
//! for Delete Brush and Duplicate Brush.
//! for Delete Brush and Duplicate Brush. Brush libraries open in library panels
//! ([`BrushLibraries`]).
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::Arc;
use egui::{Sense, Ui, vec2};
use serde_json::{Value, json};
use vectorcraft_brush::Brush;
use vectorcraft_doc::{Appearance, Document, Node, PressureProfile, color::Color, color::Paint};
use vectorcraft_engine::DocState;
use vectorcraft_engine::cmd::brushlib::{self, BrushLibrary};
use vectorcraft_engine::cmd::clipboard::{ArtResources, art_resources};
use vectorcraft_engine::cmd::swatchlib::LibraryInfo;
use super::library_panel::{self, LibraryKind, LibraryRef, Row};
use super::memo::Memo;
use super::{first_selected, pstate, set_pstate};
use crate::VectorcraftApp;
use crate::dialogs::brush_options;
use crate::menus::Item;
use crate::theme::Tokens;
use crate::widgets::{self, PanelDrag, menu_item};
const KINDS: [(&str, &str); 5] =
[("calligraphic", "Calligraphic"), ("scatter", "Scatter"), ("art", "Art"), ("bristle", "Bristle"), ("pattern", "Pattern")];
/// (name, type) of every brush in the active document's library.
/// (name, type) of every brush in the active document's library, and the current brush.
pub fn brushes(app: &mut VectorcraftApp) -> (Vec<(String, String)>, Option<String>) {
let Ok(v) = app.run("brush.list", json!({})) else { return (vec![], None) };
let list = v["brushes"]
.as_array()
.map(|a| a.iter().map(|b| (b["name"].as_str().unwrap_or("").to_string(), b["type"].as_str().unwrap_or("").to_string())).collect())
.unwrap_or_default();
(list, v["current"].as_str().map(str::to_string))
let Some(st) = app.session.active() else { return (vec![], None) };
let list = document_brushes(st).iter().map(|b| (b.name.clone(), b.ty.to_string())).collect();
(list, vectorcraft_brush::current(&st.doc))
}
/// A stroke preview for brush definition `def`, rendered at `size` and cached by the definition's
/// JSON. The stroke goes from light to heavy pen pressure and back, so pressure-sensitive brushes
/// show how they vary.
fn preview(ui: &Ui, def: &Value, size: egui::Vec2) -> Option<egui::TextureHandle> {
widgets::doc_preview(ui, &format!("brush:{def}"), size, |w, h| {
let mut doc = Document::new(w, h);
doc.unknown.insert("brushes".into(), json!([def]));
let name = def["name"].as_str()?.to_string();
let mut ap = Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 1.0);
let st = ap.stroke_mut()?;
st.brush = Some(name);
st.pressure = Some(PressureProfile::sample());
// A gentle S-curve across the swatch.
let mut bp = vectorcraft_geom::BezPath::new();
let (x0, x1) = (h * 0.5, w - h * 0.5);
bp.move_to((x0, h * 0.5));
bp.curve_to((x0 + (x1 - x0) * 0.35, h * 0.1), (x0 + (x1 - x0) * 0.65, h * 0.9), (x1, h * 0.5));
let id = doc.alloc_id();
let l = doc.layers[0].id;
doc.insert(Some(l), 0, Node::path(id, vectorcraft_geom::PathData::from_bezpath(&bp), ap)).ok()?;
Some(doc)
/// A brush as the panels show it: its definition (as `brush.get` gives it, which a drag carries)
/// and the key its stroke preview is cached by ([`preview_key`]), worked out once per document
/// version or library ([`document_brushes`], [`BrushLibraries`]).
pub(crate) struct Shown {
pub(crate) name: String,
pub(crate) ty: &'static str,
pub(crate) def: Value,
key: u64,
/// Keeps the art and images `key` counts by address from being reused.
_deps: ArtResources,
}
impl Shown {
fn of(doc: &Document, b: &Brush) -> Self {
let def = serde_json::to_value(b).unwrap_or_default();
let (key, deps) = preview_key(doc, &def, b.art());
Self { name: b.name.clone(), ty: b.kind.type_id(), def, key, _deps: deps }
}
}
/// The brushes of the document version `st` shows, each worked out once while that version is
/// active: a frame of the Brushes panel serialises and hashes none.
pub(crate) fn document_brushes(st: &DocState) -> Rc<Vec<Shown>> {
thread_local! {
static MEMO: RefCell<Memo<Document, Vec<Shown>>> = RefCell::new(Memo::default());
}
MEMO.with(|m| m.borrow_mut().get(&st.doc, st.revision, || vectorcraft_brush::library(&st.doc).iter().map(|b| Shown::of(&st.doc, b)).collect()))
}
/// The cache key of the stroke preview of brush definition `def` in `doc` (`art`: its art): the
/// definition and every resource of `doc` the art uses ([`art_resources`]: patterns with their
/// options, symbols, swatches and colour groups, gradients, images, styles…), so the preview
/// renders again when one of those changes and stays cached when anything else does. With what
/// keeps the key's addresses from being reused.
fn preview_key(doc: &Document, def: &Value, art: Vec<&Node>) -> (u64, ArtResources) {
use std::hash::{Hash, Hasher};
#[cfg(test)]
counters::KEYED.with(|k| k.set(k.get() + 1));
let deps = art_resources(doc, art);
let mut h = std::collections::hash_map::DefaultHasher::new();
def.to_string().hash(&mut h);
deps.key.hash(&mut h);
(h.finish(), deps)
}
/// A stroke preview for brush definition `def`, rendered at `size` with the resources of `doc`
/// ([`preview_doc`]) and cached by `key` ([`preview_key`]).
fn preview(ui: &Ui, doc: &Document, def: &Value, key: u64, size: egui::Vec2) -> Option<egui::TextureHandle> {
widgets::doc_preview(ui, &format!("brush:{key:016x}"), size, |w, h| {
#[cfg(test)]
tests::BUILDS.with(|b| b.set(b.get() + 1));
preview_doc(doc, def, w, h)
})
}
/// The stroke preview of brush definition `def` on white in `r` (the chip a dragged brush shows at
/// the pointer).
pub(crate) fn chip(ui: &Ui, r: egui::Rect, def: &Value) {
/// What a stroke preview of brush definition `def`, `w` × `h` pt, renders: a copy of `resources`
/// (a document whose patterns, symbols, swatches and images the brush art may use) without its art,
/// and a stroke painted with the brush across it. The stroke goes from light to heavy pen pressure
/// and back, so pressure-sensitive brushes show how they vary.
fn preview_doc(resources: &Document, def: &Value, w: f64, h: f64) -> Option<Document> {
let mut doc = resources.clone();
let layer = doc.alloc_id();
doc.layers = vec![Arc::new(Node::layer(layer, "Preview", vectorcraft_doc::LayerColor::Preset(0)))];
doc.unknown.insert(vectorcraft_brush::DOC_KEY.into(), json!([def]));
let name = def["name"].as_str()?.to_string();
let mut ap = Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 1.0);
let st = ap.stroke_mut()?;
st.brush = Some(name);
st.pressure = Some(PressureProfile::sample());
// A gentle S-curve across the swatch (square tiles keep a quarter of it either side).
let mut bp = vectorcraft_geom::BezPath::new();
let margin = (h * 0.5).min(w * 0.25);
let (x0, x1) = (margin, w - margin);
bp.move_to((x0, h * 0.5));
bp.curve_to((x0 + (x1 - x0) * 0.35, h * 0.1), (x0 + (x1 - x0) * 0.65, h * 0.9), (x1, h * 0.5));
let id = doc.alloc_id();
doc.insert(Some(layer), 0, Node::path(id, vectorcraft_geom::PathData::from_bezpath(&bp), ap)).ok()?;
Some(doc)
}
/// The stroke preview of brush definition `def` (with the resources of `doc`), cached by `key`
/// ([`preview_key`]), on white in `r`: the panels' brushes, Brush Options and the chip a dragged
/// brush shows at the pointer.
pub(crate) fn chip(ui: &Ui, r: egui::Rect, doc: &Document, def: &Value, key: u64) {
ui.painter().rect_filled(r, 0.0, egui::Color32::WHITE);
if let Some(tex) = preview(ui, def, r.size()) {
if let Some(tex) = preview(ui, doc, def, key, r.size()) {
#[cfg(test)]
tests::DRAWN.with(|d| d.set(d.get() + 1));
ui.painter().image(tex.id(), r, egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)), egui::Color32::WHITE);
}
}
/// [`chip`] of a brush the fields of Brush Options describe (`def`), with the resources of the
/// document version `st` (none without a document): its key is worked out again only when the
/// definition or the document changes, not every frame.
pub(crate) fn definition_chip(ui: &Ui, r: egui::Rect, st: Option<&DocState>, def: &Value) {
type Last = (std::sync::Weak<Document>, u64, Value, u64, ArtResources);
thread_local! {
static LAST: RefCell<Option<Last>> = const { RefCell::new(None) };
static BLANK: Arc<Document> = Arc::new(Document::new(1.0, 1.0));
}
let doc = st.map_or_else(|| BLANK.with(Arc::clone), |st| st.doc.clone());
let stamp = st.map_or(0, |st| st.revision);
let known = |l: &Last| l.0.as_ptr() == Arc::as_ptr(&doc) && l.1 == stamp && l.2 == *def;
let key = LAST.with(|l| l.borrow().as_ref().filter(|l| known(l)).map(|l| l.3)).unwrap_or_else(|| {
let brush = Brush::from_json(def.clone()).ok();
let (key, deps) = preview_key(&doc, def, brush.as_ref().map(Brush::art).unwrap_or_default());
LAST.with(|l| *l.borrow_mut() = Some((Arc::downgrade(&doc), stamp, def.clone(), key, deps)));
key
});
chip(ui, r, &doc, def, key);
}
fn selected_brush(app: &VectorcraftApp) -> Option<String> {
first_selected(app).and_then(|n| n.appearance.stroke().and_then(|s| s.brush.clone()))
}
@@ -166,8 +255,11 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
let mut multi = multi(app, ui.ctx(), &list, current.as_deref());
let list_view: bool = pstate(ui.ctx(), "br-list");
let hidden: Vec<String> = pstate(ui.ctx(), "br-hidden");
let defs: HashMap<String, Value> =
list.iter().filter_map(|(n, _)| app.run("brush.get", json!({"name": n})).ok().map(|v| (n.clone(), v))).collect();
// Previews show brush art with the document's patterns, symbols and other resources; the
// definitions and preview keys are the document version's, worked out once.
let doc = app.session.active().map(|d| d.doc.clone());
let memo = app.session.active().map(document_brushes).unwrap_or_default();
let defs: HashMap<&str, &Shown> = memo.iter().map(|b| (b.name.as_str(), b)).collect();
let mut clicked: Option<(String, egui::Modifiers)> = None;
let mut options: Option<String> = None;
let mut moved: Option<Value> = None;
@@ -189,7 +281,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
let row = |ui: &mut Ui, name: &str, size: egui::Vec2, label: bool| -> egui::Response {
let (r, resp) = ui.allocate_exact_size(size, Sense::click_and_drag());
// Dragged onto a path, the brush is applied to it.
widgets::drag_source(ui, &resp, || PanelDrag::Brush { name: name.to_string(), def: defs.get(name).cloned().unwrap_or_default() });
widgets::drag_source(ui, &resp, || PanelDrag::Brush {
name: name.to_string(),
def: defs.get(name).map(|b| b.def.clone()).unwrap_or_default(),
});
let on = match &multi {
Some(multi) => multi.iter().any(|m| m == name),
None => sel_brush.as_deref() == Some(name) || (sel_brush.is_none() && current.as_deref() == Some(name)),
@@ -201,11 +296,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
}
let pw = if label { 72.0 } else { size.x - 4.0 };
let pr = egui::Rect::from_min_size(r.left_top() + vec2(2.0, 2.0), vec2(pw, size.y - 4.0));
ui.painter().rect_filled(pr, 0.0, egui::Color32::WHITE);
if let Some(def) = defs.get(name)
&& let Some(tex) = preview(ui, def, pr.size())
{
ui.painter().image(tex.id(), pr, egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)), egui::Color32::WHITE);
if let (Some(b), Some(doc)) = (defs.get(name), doc.as_deref()) {
chip(ui, pr, doc, &b.def, b.key);
} else {
ui.painter().rect_filled(pr, 0.0, egui::Color32::WHITE);
}
if label {
ui.painter().text(
@@ -247,13 +341,16 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
ui.min_rect()
});
// Art dragged off the canvas and dropped on the list opens New Brush for that art, as the New
// Brush button opens it for the selection.
// Brush button opens it for the selection; a brush library's brush dropped there is added.
let zone = ui.interact(list_rect, ui.scope_id().with("brushes-drop"), Sense::hover());
if let Some(ids) = widgets::art_drop(ui, &zone)
&& let Err(e) = app.run("ui.newBrush", json!({ "ids": ids }))
{
app.status(e);
}
if let Some((library, name)) = library_brush_drop(ui, &zone) {
add(app, json!({"library": library, "name": name}));
}
// A brush dragged within the list moves among the brushes of its type.
if let Some(p) = moved
&& let Err(e) = app.run("brush.move", p)
@@ -300,7 +397,12 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
let targets = multi.unwrap_or_else(|| highlighted.into_iter().collect());
let has_doc = app.session.active().is_some();
widgets::bottom_bar(ui, |ui| {
widgets::icon_button_enabled(ui, "library", tl!("Brush Libraries (on the roadmap)"), false, false, 24.0);
let open = library_panel::any_open::<BrushLibraries>(&app.ui);
let lr = widgets::icon_button(ui, "library", BrushLibraries::MENU, open, 24.0);
egui::Popup::menu(&lr).show(|ui| {
ui.set_min_width(200.0);
library_panel::library_menu::<BrushLibraries>(app, ui);
});
if widgets::icon_button_enabled(ui, "dc-remove-brush", tl!("Remove Brush Stroke"), false, has_brush, 24.0).clicked() {
app.run("brush.remove", json!({})).ok();
}
@@ -325,6 +427,7 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
let (list, current) = brushes(app);
let targets = targets(app, ui.ctx(), &list, current.as_deref());
let has_sel = app.session.active().is_some_and(|d| !d.selection.is_empty());
let has_brushes = !list.is_empty();
if menu_item(ui, tl!("New Brush…"), app.session.active().is_some(), false)
&& let Err(e) = app.run("ui.newBrush", json!({}))
{
@@ -392,6 +495,221 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
if menu_item(ui, tl!("List View"), true, list) {
set_pstate(ui.ctx(), "br-list", true);
}
ui.separator();
ui.menu_button(tl!("Open Brush Library"), |ui| library_panel::library_menu::<BrushLibraries>(app, ui));
if menu_item(ui, tl!("Save Brush Library…"), has_brushes, false) {
save_library(app, targets);
}
}
/// Save Brush Library… for the document's brushes (`names`: those selected in the panel).
fn save_library(app: &mut VectorcraftApp, names: Vec<String>) {
if let Err(e) = app.run("ui.saveBrushLibrary", json!({ "names": names })) {
app.status(e);
}
}
/// A brush library's brush released over `zone` (outlined while one is held over it): → (its
/// library, its name).
fn library_brush_drop(ui: &Ui, zone: &egui::Response) -> Option<(String, String)> {
let drag = zone.dnd_hover_payload::<PanelDrag>()?;
let PanelDrag::LibraryBrush { library, name } = &*drag else { return None };
ui.painter().rect_stroke(zone.rect, 0.0, egui::Stroke::new(1.5, Tokens::get(ui.ctx()).accent), egui::StrokeKind::Inside);
zone.dnd_release_payload::<PanelDrag>().map(|_| (library.clone(), name.clone()))
}
// ---------- libraries ----------
/// A brush of a library as the library panel shows it.
pub(crate) struct LibBrush {
lib: Arc<BrushLibrary>,
index: usize,
/// Its definition and preview key, with the library's resources.
shown: Shown,
}
impl LibBrush {
fn name(&self) -> &str {
&self.shown.name
}
}
/// The brushes of library `lib` as its panel shows them, worked out once per library: a frame of
/// the panel serialises and hashes none.
pub(crate) fn library_brushes(lib: &Arc<BrushLibrary>) -> Rc<Vec<LibBrush>> {
thread_local! {
static MEMO: RefCell<Memo<BrushLibrary, Vec<LibBrush>>> = RefCell::new(Memo::default());
}
MEMO.with(|m| {
m.borrow_mut().get(lib, 0, || {
lib.brushes.iter().enumerate().map(|(index, b)| LibBrush { lib: lib.clone(), index, shown: Shown::of(&lib.doc, b) }).collect()
})
})
}
/// The chip brush `name` of the document version `st` shows at the pointer while dragged.
pub(crate) fn document_chip(ui: &Ui, r: egui::Rect, st: &DocState, name: &str) {
if let Some(b) = document_brushes(st).iter().find(|b| b.name == name) {
chip(ui, r, &st.doc, &b.def, b.key);
}
}
/// The chip brush `name` of library `lib` shows at the pointer while dragged.
pub(crate) fn library_chip(ui: &Ui, r: egui::Rect, lib: &Arc<BrushLibrary>, name: &str) {
if let Some(b) = library_brushes(lib).iter().find(|b| b.name() == name) {
chip(ui, r, &lib.doc, &b.shown.def, b.shown.key);
}
}
/// Brush libraries in library panels (engine: `brush.libraries`, `brush.addFromLibrary`…).
pub(crate) struct BrushLibraries;
impl LibraryKind for BrushLibraries {
const KIND: &'static str = "brushes";
const OPEN: &'static str = "window.brushLibrary";
const LIST: &'static str = "brush.libraries";
const LOAD: &'static str = "brush.loadLibrary";
const MENU: &'static str = "Brush Libraries Menu";
const ADD: &'static str = "Add to Brushes";
/// Stroke previews need room to show the brush.
const VIEW: super::swatches::View = super::swatches::View::LargeThumb;
type Lib = Rc<Vec<LibBrush>>;
type Item = LibBrush;
fn list(app: &VectorcraftApp) -> Vec<LibraryRef> {
let libs = brushlib::libraries(&app.session).into_iter();
libs.map(|l| LibraryRef { submenu: library_panel::submenu(l.category), id: l.id, name: l.name }).collect()
}
fn get(app: &VectorcraftApp, id: &str) -> Option<(String, Self::Lib)> {
let (info, lib) = brushlib::library(&app.session, id)?;
Some((info.name, library_brushes(&lib)))
}
fn find(app: &VectorcraftApp, key: &str) -> library_panel::Found {
brushlib::library(&app.session, key).map(|(info, lib)| (info.id, info.name, lib.len()))
}
fn path(app: &VectorcraftApp, id: &str) -> Option<String> {
app.session.brush_libraries.path(id).map(str::to_string)
}
fn rows<'a>(lib: &'a Self::Lib, query: &str) -> Vec<Row<'a, LibBrush>> {
let found = |n: &str| query.is_empty() || n.to_lowercase().contains(query);
lib.iter().filter(|b| found(b.name())).map(|b| Row { name: b.name(), item: Some(b) }).collect()
}
/// The brush's stroke preview, drawn with the library's resources.
fn draw(_: &VectorcraftApp, ui: &Ui, r: egui::Rect, b: &LibBrush, selected: bool, hovered: bool) {
chip(ui, r, &b.lib.doc, &b.shown.def, b.shown.key);
super::graphic_styles::outline(ui, r, selected, hovered);
}
fn row_icons(_: &Ui, _: egui::Rect, _: &LibBrush) {}
/// Its type.
fn describe(b: &LibBrush) -> String {
b.lib.brushes.get(b.index).map_or_else(String::new, |x| tl!(x.kind.label()).to_string())
}
/// Adds the brush to the document's Brushes as the current brush, and applies it to the
/// selected paths, as clicking a brush of the Brushes panel does.
fn click(app: &mut VectorcraftApp, _: &Ui, id: &str, name: &str) {
let apply = app.session.active().is_some_and(|d| !d.selection.is_empty());
add(app, json!({"library": id, "name": name, "apply": apply}));
}
fn add(app: &mut VectorcraftApp, id: &str, names: Vec<String>) {
add(app, json!({"library": id, "names": names}));
}
/// Dragged onto a path, the brush is added and applied to it; onto the Brushes panel, added.
fn drag(id: &str, name: &str) -> Option<PanelDrag> {
Some(PanelDrag::LibraryBrush { library: id.to_string(), name: name.to_string() })
}
fn menu_tail(app: &mut VectorcraftApp, ui: &mut Ui) {
ui.separator();
// As a command, so its file dialog, shown off the UI thread, loads the library when it
// answers.
if menu_item(ui, tl!("Other Library…"), true, false)
&& let Err(e) = app.run("window.brushLibrary.other", json!({}))
{
app.status(e);
}
let has_brushes = has_brushes(app);
if menu_item(ui, tl!("Save Brushes…"), has_brushes, false) {
let (list, current) = brushes(app);
let names = targets(app, ui.ctx(), &list, current.as_deref());
save_library(app, names);
}
}
}
/// Run `brush.addFromLibrary` with `p`; a failure goes to the status bar.
fn add(app: &mut VectorcraftApp, p: Value) {
if let Err(e) = app.run("brush.addFromLibrary", p) {
app.status(e);
}
}
/// Has the active document brushes to save? (Its stored library isn't parsed: a document without
/// one has the default brushes.)
pub(crate) fn has_brushes(app: &VectorcraftApp) -> bool {
let stored = |d: &Document| d.unknown.get(vectorcraft_brush::DOC_KEY).map(|v| v.as_array().is_some_and(|a| !a.is_empty()));
app.session.active().is_some_and(|st| stored(&st.doc).unwrap_or(true))
}
/// `window.brushLibrary {library}`: open a brush library in its own panel (`library` null closes
/// every brush library panel).
pub(crate) fn open_library(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
library_panel::open_command::<BrushLibraries>(app, p)
}
/// `brush.loadLibrary` params, then open the library in its panel.
pub(crate) fn load_library(app: &mut VectorcraftApp, params: Value) -> Result<Value, String> {
let r = app.run("brush.loadLibrary", params)?;
open_library(app, &json!({ "library": r["library"] }))
}
/// Other Library…: load the brushes of the document at `path`, else of one picked in an open
/// dialog.
pub(crate) fn other_library(app: &mut VectorcraftApp, path: Option<String>) -> Result<Value, String> {
match library_panel::pick_library_file(app, path, library_panel::library_pick_filters(BrushLibraries::KIND))? {
Some(path) => load_library(app, json!({ "path": path })),
None => Ok(Value::Null),
}
}
/// The id prefix of the Window → Brush Libraries → User Defined slots.
pub(crate) const USER_SLOT: &str = "window.userBrushLibrary";
/// The User Defined library slot `id` (`window.userBrushLibrary3`) stands for.
pub(crate) fn user_library(app: &VectorcraftApp, id: &str) -> Option<LibraryInfo> {
library_panel::user_slot(id, USER_SLOT, brushlib::libraries(&app.session))
}
/// Window → Brush Libraries.
pub(crate) fn window_menu() -> Vec<Item> {
const SLOTS: [&str; 10] = [
"window.userBrushLibrary1",
"window.userBrushLibrary2",
"window.userBrushLibrary3",
"window.userBrushLibrary4",
"window.userBrushLibrary5",
"window.userBrushLibrary6",
"window.userBrushLibrary7",
"window.userBrushLibrary8",
"window.userBrushLibrary9",
"window.userBrushLibrary10",
];
let open = |b: &'static brushlib::BuiltinBrushLibrary| Item::Cmd(b.name, BrushLibraries::OPEN, json!({ "library": b.id }));
let mut items: Vec<Item> = brushlib::BUILTIN.iter().map(open).collect();
items.extend([
Item::Sep,
Item::Sub("User Defined", SLOTS.iter().map(|id| Item::Cmd("User Library", id, Value::Null)).collect()),
Item::Sep,
Item::Cmd("Other Library…", "window.brushLibrary.other", Value::Null),
Item::Cmd("Save Brushes…", "ui.saveBrushLibrary", Value::Null),
]);
items
}
/// Brush definitions serialised and preview keys hashed on this test thread.
#[cfg(test)]
pub(crate) mod counters {
thread_local! {
pub(crate) static KEYED: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
}
#[cfg(test)]
@@ -400,6 +718,20 @@ mod tests {
use vectorcraft_doc::NodeId;
use vectorcraft_engine::Session;
thread_local! {
/// Stroke previews rendered (cache misses), and drawn, on this test thread.
pub(super) static BUILDS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
pub(super) static DRAWN: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
/// One frame of the panel → (previews rendered, previews drawn) in it.
fn counted(ctx: &egui::Context, app: &mut VectorcraftApp) -> (usize, usize) {
BUILDS.with(|b| b.set(0));
DRAWN.with(|d| d.set(0));
frame(ctx, app, vec![]);
(BUILDS.with(std::cell::Cell::get), DRAWN.with(std::cell::Cell::get))
}
use super::*;
/// One headless frame of the panel, 236 pt wide as in the dock.
@@ -770,4 +1102,271 @@ mod tests {
assert_eq!(undo(&app), steps);
assert!(app.ui.status.contains("brushes move only within their type"), "{}", app.ui.status);
}
/// Opaque pixels of brush definition `def`'s 80 × 40 pt preview with the resources of `doc`,
/// and those of them that are Pinstripes blue.
fn pixels(doc: &Document, def: &Value) -> (usize, usize) {
let preview = preview_doc(doc, def, 80.0, 40.0).expect("a preview");
let img = vectorcraft_render::Renderer::new().render_region(&preview, vectorcraft_geom::Rect::new(0.0, 0.0, 80.0, 40.0), 2.0, false);
let painted: Vec<&[u8; 4]> = img.pixels.as_chunks::<4>().0.iter().filter(|p| p[3] > 200).collect();
let blue = painted.iter().filter(|p| p[2] > 170 && p[0] < 90 && (100..160).contains(&p[1])).count();
(painted.len(), blue)
}
/// A library brush's preview shows the pattern its art is filled with: it renders with the
/// library's resources, which a bare document doesn't have.
#[test]
fn library_brush_previews_show_the_patterns_their_art_uses() {
let s = Session::new();
let (_, lib) = brushlib::library(&s, "art-ribbons").unwrap();
let def = serde_json::to_value(lib.brush("Striped Ribbon").unwrap()).unwrap();
let (painted, blue) = pixels(&lib.doc, &def);
assert!(painted > 0 && blue * 5 >= painted, "Pinstripes paints it: {painted} painted, {blue} blue");
let (painted, blue) = pixels(&Document::new(10.0, 10.0), &def);
assert_eq!(blue, 0, "without the pattern: {painted} painted");
// A square tile still shows a stroke.
let square = preview_doc(&lib.doc, &def, 40.0, 40.0).unwrap();
let img = vectorcraft_render::Renderer::new().render_region(&square, vectorcraft_geom::Rect::new(0.0, 0.0, 40.0, 40.0), 2.0, false);
assert!(img.pixels.as_chunks::<4>().0.iter().any(|p| p[3] > 200));
}
#[test]
fn a_library_brush_dropped_on_the_panel_is_added() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 200, "height": 200})).unwrap();
let ctx = egui::Context::default();
frame(&ctx, &mut app, vec![]);
let at = pos2(120.0, 40.0);
egui::DragAndDrop::set_payload(&ctx, PanelDrag::LibraryBrush { library: "pattern-borders".into(), name: "Beads".into() });
frame(&ctx, &mut app, vec![egui::Event::PointerMoved(at)]);
frame(&ctx, &mut app, vec![button(at, false)]);
assert!(vectorcraft_brush::find(&app.session.active().unwrap().doc, "Beads").is_some());
}
/// A preview is cached by its brush and the patterns and symbols its art shows: adding other
/// brushes (and the patterns and symbols they bring) renders only the new one, every preview
/// is drawn in the first frame, and changing a pattern a brush shows renders that brush again.
/// Symbol thumbnails are keyed the same way.
#[test]
fn previews_render_again_only_when_what_they_show_changes() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 200, "height": 200})).unwrap();
let ctx = egui::Context::default();
let count = |app: &mut VectorcraftApp| brushes(app).0.len();
let n = count(&mut app);
assert_eq!(counted(&ctx, &mut app), (n, n), "the first frame renders and draws every preview");
assert_eq!(counted(&ctx, &mut app), (0, n), "then they are cached");
// Striped Ribbon brings its pattern, Sparkles its symbol: only they render.
app.run("brush.addFromLibrary", json!({"library": "art-ribbons", "name": "Striped Ribbon"})).unwrap();
assert_eq!(counted(&ctx, &mut app), (1, n + 1));
let symbol_key =
|app: &VectorcraftApp, name: &str| super::super::symbols::document_symbols(app.session.active().unwrap()).key(name).unwrap_or(0);
let sparkle_before = symbol_key(&app, "Sparkle");
app.run("brush.addFromLibrary", json!({"library": "scatter-shapes", "name": "Sparkles"})).unwrap();
assert_eq!(counted(&ctx, &mut app), (1, n + 2));
// A symbol added beside Sparkle leaves its thumbnail key alone.
let sparkle = symbol_key(&app, "Sparkle");
assert_ne!(sparkle, sparkle_before, "0 before Sparkle came in");
app.run("symbol.addFromLibrary", json!({"library": "basic-shapes", "name": "Star"})).unwrap();
assert_eq!(symbol_key(&app, "Sparkle"), sparkle);
assert_eq!(counted(&ctx, &mut app), (0, n + 2));
// A new Pinstripes tile: Striped Ribbon renders again, the others don't.
app.session
.edit("Pattern", |d, _| {
let p = d.patterns.iter_mut().find(|p| p.name == "Pinstripes").unwrap();
let dot = Node::path(
NodeId(0),
vectorcraft_geom::shapes::ellipse(vectorcraft_geom::Rect::new(0.0, 0.0, 3.0, 3.0)),
Appearance::default_art(),
);
p.art = vec![std::sync::Arc::new(dot)];
Ok(())
})
.unwrap();
assert_eq!(counted(&ctx, &mut app), (1, n + 2));
}
fn keyed() -> usize {
counters::KEYED.with(|k| k.replace(0))
}
/// The Brushes panel serialises and hashes the brushes of a document version once: frames
/// after the first, of it or of it again after another document was active, do neither; an
/// edit, an undo and a redo each work them out once again and render no preview again. Brush
/// Options works its preview's key out again only when its definition or the document changes.
#[test]
fn frames_after_the_first_serialise_and_hash_no_brush() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 200, "height": 200})).unwrap();
let ctx = egui::Context::default();
let n = vectorcraft_brush::library(&app.session.active().unwrap().doc).len();
keyed();
assert_eq!(counted(&ctx, &mut app), (n, n));
assert_eq!(keyed(), n, "the first frame works each brush out");
for _ in 0..3 {
assert_eq!(counted(&ctx, &mut app), (0, n));
}
assert_eq!(keyed(), 0, "later frames serialise and hash none");
// Another document, then this one again.
app.run("file.new", json!({"width": 100, "height": 100})).unwrap();
frame(&ctx, &mut app, vec![]);
keyed();
app.run("document.activate", json!({"index": 0})).unwrap();
assert_eq!(counted(&ctx, &mut app), (0, n));
assert_eq!(keyed(), 0, "the document's brushes are still known");
// An edit to something else: the brushes are worked out again, no preview renders.
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 20, "height": 20})).unwrap();
assert_eq!(counted(&ctx, &mut app), (0, n));
assert_eq!(keyed(), n);
for cmd in ["edit.undo", "edit.redo"] {
app.run(cmd, json!({})).unwrap();
assert_eq!(counted(&ctx, &mut app), (0, n), "{cmd}");
frame(&ctx, &mut app, vec![]);
assert!(keyed() <= n, "{cmd}: once at most");
}
// Brush Options' preview of a definition.
let (_, lib) = brushlib::library(&app.session, "art-ribbons").unwrap();
let mut def = serde_json::to_value(lib.brush("Striped Ribbon").unwrap()).unwrap();
let options = |app: &VectorcraftApp, def: &Value| {
let raw = egui::RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(300.0, 100.0))), ..Default::default() };
let mut out = ctx.run_ui(raw, |ui| definition_chip(ui, Rect::from_min_size(Pos2::ZERO, vec2(200.0, 40.0)), app.session.active(), def));
out.textures_delta.clear();
};
options(&app, &def);
assert_eq!(keyed(), 1);
for _ in 0..3 {
options(&app, &def);
}
assert_eq!(keyed(), 0, "the same definition in the same document");
def["width"] = json!(150.0);
options(&app, &def);
options(&app, &def);
assert_eq!(keyed(), 1, "a field changed");
app.run("shape.rectangle", json!({"x": 40, "y": 10, "width": 20, "height": 20})).unwrap();
options(&app, &def);
options(&app, &def);
assert_eq!(keyed(), 1, "the document changed");
}
/// One headless frame of the Symbols panel → thumbnails rendered in it.
fn symbols_rendered(ctx: &egui::Context, app: &mut VectorcraftApp) -> usize {
use super::super::symbols::{self, counters::RENDERED};
RENDERED.with(|c| c.set(0));
let raw = egui::RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(236.0, 600.0))), ..Default::default() };
let mut out = ctx.run_ui(raw, |ui| symbols::show(app, ui));
out.textures_delta.clear();
RENDERED.with(std::cell::Cell::get)
}
/// A 4 × 4 PNG of colour `rgb`.
fn png(rgb: [u8; 3]) -> Vec<u8> {
let img = image::RgbaImage::from_pixel(4, 4, image::Rgba([rgb[0], rgb[1], rgb[2], 255]));
let mut out = vec![];
img.write_to(&mut std::io::Cursor::new(&mut out), image::ImageFormat::Png).unwrap();
out
}
/// Brush previews and symbol thumbnails render again when a swatch, an image, or a pattern's
/// tile type, overlap or spacing their art uses changes (only those that use it), and not when
/// one they don't use does.
#[test]
fn previews_follow_the_swatches_images_and_pattern_options_their_art_uses() {
use vectorcraft_doc::pattern::{Overlap, TileType};
use vectorcraft_doc::{ImageBlob, ImageObject, NodeKind, Symbol};
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 200, "height": 200})).unwrap();
// Striped Ribbon brings Pinstripes.
app.run("brush.addFromLibrary", json!({"library": "art-ribbons", "name": "Striped Ribbon"})).unwrap();
let (_, lib) = brushlib::library(&app.session, "art-ribbons").unwrap();
let wave = lib.brush("Wave").unwrap().clone();
let striped = lib.brush("Striped Ribbon").unwrap().art()[0].clone();
let ink = Paint::Solid { color: Color::BLACK, swatch: Some("Ink".into()), tint: 1.0 };
let inked = Node::path(
NodeId(0),
vectorcraft_geom::shapes::rectangle(vectorcraft_geom::Rect::new(0.0, 0.0, 20.0, 10.0)),
Appearance::basic(ink, Paint::None, 0.0),
);
let im = ImageObject {
key: "pic".into(),
width: 4,
height: 4,
xf: vectorcraft_geom::Affine::scale(5.0),
link: None,
placement: Default::default(),
pixels: None,
};
let pictured = Node::new(NodeId(0), NodeKind::Image(Box::new(im)));
// Filled with the gradient swatch Fade.
let fade = |c: Color| {
use vectorcraft_color::{Gradient, GradientKind, GradientPaint, GradientStop};
GradientPaint::new(Gradient::new(GradientKind::Linear, vec![GradientStop::new(0.0, Color::WHITE), GradientStop::new(1.0, c)]))
};
let mut faded_paint = fade(Color::BLACK);
faded_paint.swatch = Some("Fade".into());
let faded = Node::path(
NodeId(0),
vectorcraft_geom::shapes::rectangle(vectorcraft_geom::Rect::new(0.0, 0.0, 20.0, 10.0)),
Appearance::basic(Paint::Gradient(Box::new(faded_paint)), Paint::None, 0.0),
);
app.session
.edit("Resources", |d, _| {
for name in ["Ink", "Other"] {
d.swatches.push(vectorcraft_color::Swatch { name: name.into(), paint: Paint::solid(Color::BLACK), global: true, spot: false });
}
let paint = Paint::Gradient(Box::new(fade(Color::BLACK)));
d.swatches.push(vectorcraft_color::Swatch { name: "Fade".into(), paint, global: false, spot: false });
d.images.insert("pic".into(), ImageBlob::png(png([255, 0, 0])));
let mut brushes = vectorcraft_brush::library(d);
for (name, art) in [("Inked", &inked), ("Pictured", &pictured), ("Faded", &faded)] {
let mut b = wave.clone();
b.name = name.into();
if let vectorcraft_brush::BrushKind::Art(a) = &mut b.kind {
a.art = art.clone();
}
brushes.push(b);
}
vectorcraft_brush::store(d, &brushes);
for (name, art) in [("Ink Symbol", &inked), ("Picture Symbol", &pictured), ("Stripes Symbol", &striped), ("Fade Symbol", &faded)] {
d.symbols.push(Symbol { name: name.into(), art: Arc::new(art.clone()) });
}
Ok(())
})
.unwrap();
let ctx = egui::Context::default();
let n = brushes(&mut app).0.len();
assert_eq!((counted(&ctx, &mut app).0, symbols_rendered(&ctx, &mut app)), (n, 4));
assert_eq!((counted(&ctx, &mut app).0, symbols_rendered(&ctx, &mut app)), (0, 0));
let change = |app: &mut VectorcraftApp, what: &str, f: &dyn Fn(&mut Document)| {
app.session
.edit("Change", |d, _| {
f(d);
Ok(())
})
.unwrap();
(counted(&ctx, app).0, symbols_rendered(&ctx, app), what.to_string())
};
let swatch =
|name: &'static str| move |d: &mut Document| d.swatches.iter_mut().find(|w| w.name == name).unwrap().paint = Paint::solid(Color::WHITE);
fn pinstripes(d: &mut Document) -> &mut vectorcraft_doc::pattern::PatternDef {
d.patterns.iter_mut().find(|p| p.name == "Pinstripes").unwrap()
}
type Case<'a> = (&'a str, (usize, usize), &'a dyn Fn(&mut Document));
let cases: [Case; 8] = [
("Ink", (1, 1), &swatch("Ink")),
("Other", (0, 0), &swatch("Other")),
("Fade", (1, 1), &|d| d.swatches.iter_mut().find(|w| w.name == "Fade").unwrap().paint = Paint::Gradient(Box::new(fade(Color::WHITE)))),
("pic", (1, 1), &|d| {
d.images.insert("pic".into(), ImageBlob::png(png([0, 0, 255])));
}),
("another image", (0, 0), &|d| {
d.images.insert("pic2".into(), ImageBlob::png(png([0, 255, 0])));
}),
("tile type", (1, 1), &|d| pinstripes(d).tile_type = TileType::BrickByRow { offset: 0.5 }),
("overlap", (1, 1), &|d| pinstripes(d).overlap = Overlap { right_in_front: true, bottom_in_front: true }),
("spacing", (1, 1), &|d| pinstripes(d).h_spacing = 4.0),
];
for (what, (brushes, symbols), f) in cases {
assert_eq!(change(&mut app, what, f), (brushes, symbols, what.to_string()));
}
}
}
@@ -355,6 +355,7 @@ fn zone_input(ui: &Ui, zone: &Response, d: &Document, ev: &mut Events) {
| PanelDrag::Symbol(_)
| PanelDrag::LibrarySymbol { .. }
| PanelDrag::Brush { .. }
| PanelDrag::LibraryBrush { .. }
| PanelDrag::LibraryGraphic { .. }
| PanelDrag::LibraryItem { .. }
| PanelDrag::LibraryGroup { .. } => (None, None),
+4 -3
View File
@@ -1,4 +1,4 @@
//! Library panels: read-only views of one library each (swatch, graphic style and symbol
//! Library panels: read-only views of one library each (swatch, graphic style, symbol and brush
//! libraries; any kind of library implementing [`LibraryKind`]) with thumbnail and list views, a
//! find field, the libraries menu and previous / next arrows. A plain click on an item adds it to
//! the document and applies it; Shift and Cmd/Ctrl-clicks select items (and folders) for Add to ….
@@ -7,7 +7,7 @@
//! As in Illustrator (<https://helpx.adobe.com/illustrator/using/using-creating-swatches.html>,
//! "Swatch libraries"), each open library is a panel of its own, `library:<kind>:<library id>`
//! ([`panel_id`]), that floats, tabs, docks and closes as any panel of the shared docking system:
//! `window.swatchLibrary`, `window.graphicStyleLibrary` and `window.symbolLibrary` open one, or bring it forward when it is
//! `window.swatchLibrary`, `window.graphicStyleLibrary`, `window.symbolLibrary` and `window.brushLibrary` open one, or bring it forward when it is
//! open. `UiState::library_panels` keeps each one's view, Show Find Field and Persistent
//! (`ui.libraryPanel`); a persistent panel reopens when the app starts ([`restore`]). A persistent
//! panel whose library can't be found shows the library as missing, with its file and Locate
@@ -163,6 +163,7 @@ fn kind(kind: &str) -> Option<Kind> {
kind_of::<super::swatches::SwatchLibraries>(),
kind_of::<super::graphic_styles::GraphicStyleLibraries>(),
kind_of::<super::symbols::SymbolLibraries>(),
kind_of::<super::brushes::BrushLibraries>(),
]
.into_iter()
.find(|k| k.kind == kind)
@@ -559,7 +560,7 @@ fn set(app: &mut VectorcraftApp, panel: &str, mut params: Value) {
}
/// Open-dialog filters for Other Library… / Locate Library… of library panels of kind `kind`
/// (symbol libraries: any document).
/// (symbol and brush libraries: any document).
pub(crate) fn library_pick_filters(kind: &str) -> Vec<(&'static str, &'static [&'static str])> {
match kind {
super::swatches::SwatchLibraries::KIND => fileio::library_open_filters("Swatch libraries", swatchlib::LIBRARY_EXTS).collect(),
+41
View File
@@ -0,0 +1,41 @@
//! What panels work out once per document version or library and keep while it lasts: brush
//! definitions and stroke-preview keys, symbol thumbnail keys.
use std::rc::Rc;
use std::sync::{Arc, Weak};
/// How many documents and libraries a [`Memo`] remembers (the oldest goes first).
const KEPT: usize = 8;
/// Values worked out from an `Arc<A>` (a document version, a library), each kept while that
/// allocation lives: one is known by its address, which a held [`Weak`] keeps from being reused,
/// and by `stamp` (a document's revision, which also changes when it is edited in place).
pub(crate) struct Memo<A, T> {
entries: Vec<(Weak<A>, u64, Rc<T>)>,
}
impl<A, T> Default for Memo<A, T> {
fn default() -> Self {
Self { entries: vec![] }
}
}
impl<A, T> Memo<A, T> {
/// The value for `of` at `stamp`, built by `build` the first time it is asked for.
pub(crate) fn get(&mut self, of: &Arc<A>, stamp: u64, build: impl FnOnce() -> T) -> Rc<T> {
let at = Arc::as_ptr(of);
if let Some(i) = self.entries.iter().position(|(w, s, _)| w.as_ptr() == at && *s == stamp) {
let hit = self.entries.remove(i);
let value = hit.2.clone();
self.entries.push(hit);
return value;
}
self.entries.retain(|(w, ..)| w.strong_count() > 0 && w.as_ptr() != at);
if self.entries.len() >= KEPT {
self.entries.remove(0);
}
let value = Rc::new(build());
self.entries.push((Arc::downgrade(of), stamp, value.clone()));
value
}
}
+3
View File
@@ -29,6 +29,7 @@ pub mod libraries;
pub mod library_panel;
pub mod links;
pub mod magic_wand;
pub(crate) mod memo;
pub mod navigator;
pub mod opentype;
pub mod paragraph;
@@ -604,6 +605,8 @@ mod tests_asset_export;
#[cfg(test)]
mod tests_blend;
#[cfg(test)]
mod tests_brush_libraries;
#[cfg(test)]
mod tests_constrain;
#[cfg(test)]
mod tests_css_properties;
+10 -3
View File
@@ -609,14 +609,21 @@ pub(crate) fn drag_preview(app: &VectorcraftApp, ctx: &egui::Context) {
},
PanelDrag::GraphicStyle(name) => return pointer_chip(ctx, |ui, r| super::graphic_styles::paint_style(app, ui, r, name)),
PanelDrag::Symbol(name) => match app.session.active() {
Some(st) => return pointer_chip(ctx, |ui, r| super::symbols::chip(ui, &st.doc, super::symbols::resources_key(&st.doc), r, name)),
Some(st) => return pointer_chip(ctx, |ui, r| super::symbols::chip(ui, &st.doc, &super::symbols::document_symbols(st), r, name)),
None => return,
},
PanelDrag::LibrarySymbol { library, name } => match vectorcraft_engine::cmd::symbollib::library(&app.session, library) {
Some((_, lib)) => return pointer_chip(ctx, |ui, r| super::symbols::chip(ui, &lib.doc, super::symbols::resources_key(&lib.doc), r, name)),
Some((_, lib)) => return pointer_chip(ctx, |ui, r| super::symbols::chip(ui, &lib.doc, &super::symbols::library_symbols(&lib), r, name)),
None => return,
},
PanelDrag::Brush { name, .. } => match app.session.active() {
Some(st) => return pointer_chip(ctx, |ui, r| super::brushes::document_chip(ui, r, st, name)),
None => return,
},
PanelDrag::LibraryBrush { library, name } => match vectorcraft_engine::cmd::brushlib::library(&app.session, library) {
Some((_, lib)) => return pointer_chip(ctx, |ui, r| super::brushes::library_chip(ui, r, &lib, name)),
None => return,
},
PanelDrag::Brush { def, .. } => return pointer_chip(ctx, |ui, r| super::brushes::chip(ui, r, def)),
PanelDrag::LibraryGraphic { library, item } => return pointer_chip(ctx, |ui, r| super::libraries::chip(app, ui, r, library, item)),
PanelDrag::LibraryItem { .. } | PanelDrag::LibraryGroup { .. } => return,
};
+77 -30
View File
@@ -4,34 +4,39 @@
use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::Arc;
use std::rc::Rc;
use std::sync::{Arc, Weak};
use egui::{Sense, Stroke, StrokeKind, Ui, vec2};
use serde_json::{Value, json};
use vectorcraft_doc::{Document, Node, NodeKind};
use vectorcraft_engine::DocState;
use vectorcraft_engine::cmd::clipboard::{ArtResources, art_resources};
use vectorcraft_engine::cmd::swatchlib::LibraryInfo;
use vectorcraft_engine::cmd::symbollib::{self, SymbolLibrary};
use super::library_panel::{self, LibraryKind, LibraryRef, Row};
use super::memo::Memo;
use super::{pstate, set_pstate};
use crate::VectorcraftApp;
use crate::menus::Item;
use crate::theme::Tokens;
use crate::widgets::{self, PanelDrag, menu_item};
/// A thumbnail of symbol `name` (an instance at its natural size), cached by the identity of the
/// art it shows: its own and `resources`, [`resources_key`] of `doc`.
fn thumb(ui: &Ui, doc: &Document, resources: u64, name: &str, size: f32) -> Option<egui::TextureHandle> {
/// A thumbnail of symbol `name` (an instance at its natural size), cached by its key in `keys`
/// ([`Thumbs`]).
fn thumb(ui: &Ui, doc: &Document, keys: &Thumbs, name: &str, size: f32) -> Option<egui::TextureHandle> {
thread_local! {
static RENDERER: RefCell<vectorcraft_render::Renderer> = RefCell::new(vectorcraft_render::Renderer::new());
static CACHE: crate::graphics::TexCache<HashMap<(usize, u64, String, u32), egui::TextureHandle>> = crate::graphics::TexCache::default();
static CACHE: crate::graphics::TexCache<HashMap<(u64, u32), egui::TextureHandle>> = crate::graphics::TexCache::default();
}
let sym = doc.symbols.iter().find(|s| s.name == name)?;
let px = (size * ui.ctx().pixels_per_point()).round().max(8.0) as u32;
let key = (Arc::as_ptr(&sym.art) as usize, resources, name.to_string(), px);
let key = (*keys.keys.get(name)?, px);
if let Some(t) = CACHE.with(|c| c.borrow().get(&key).cloned()) {
return Some(t);
}
#[cfg(test)]
counters::RENDERED.with(|c| c.set(c.get() + 1));
let img = RENDERER.with(|r| render_thumb(&mut r.borrow_mut(), doc, name, px))?;
let color = crate::canvas::color_image(&img);
let tex = ui.ctx().load_texture(format!("sym-{name}-{px}"), color, egui::TextureOptions::LINEAR);
@@ -45,18 +50,64 @@ fn thumb(ui: &Ui, doc: &Document, resources: u64, name: &str, size: f32) -> Opti
Some(tex)
}
/// The identity of `doc`'s symbol and pattern art, which a symbol's thumbnail may show. It takes a
/// pass over them: work it out once per panel frame, not per thumbnail.
pub(crate) fn resources_key(doc: &Document) -> u64 {
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
for s in &doc.symbols {
(Arc::as_ptr(&s.art) as usize).hash(&mut h);
/// The thumbnail keys of a document version's or a library's symbols, worked out once for it
/// ([`document_symbols`], [`library_symbols`]): a symbol's key is its name, its art and every
/// resource the art uses ([`art_resources`]: the symbols it nests, patterns with their options,
/// swatches and colour groups, gradients, images, styles…), so a thumbnail renders again when one
/// of those changes and stays cached when anything else does.
pub(crate) struct Thumbs {
keys: HashMap<String, u64>,
/// Keeps the addresses the keys count from being reused.
_kept: Vec<(Weak<Node>, ArtResources)>,
}
impl Thumbs {
fn of(doc: &Document) -> Self {
use std::hash::{Hash, Hasher};
let mut keys = HashMap::new();
let mut kept = vec![];
for s in &doc.symbols {
#[cfg(test)]
counters::KEYED.with(|c| c.set(c.get() + 1));
let deps = art_resources(doc, [&*s.art]);
let mut h = std::collections::hash_map::DefaultHasher::new();
(&s.name, Arc::as_ptr(&s.art) as usize, deps.key).hash(&mut h);
keys.entry(s.name.clone()).or_insert(h.finish());
kept.push((Arc::downgrade(&s.art), deps));
}
Self { keys, _kept: kept }
}
for p in &doc.patterns {
p.art.iter().for_each(|a| (Arc::as_ptr(a) as usize).hash(&mut h));
/// The thumbnail key of symbol `name` (none when there is no such symbol).
#[cfg(test)]
pub(crate) fn key(&self, name: &str) -> Option<u64> {
self.keys.get(name).copied()
}
}
/// [`Thumbs`] of the document version `st` shows.
pub(crate) fn document_symbols(st: &DocState) -> Rc<Thumbs> {
thread_local! {
static MEMO: RefCell<Memo<Document, Thumbs>> = RefCell::new(Memo::default());
}
MEMO.with(|m| m.borrow_mut().get(&st.doc, st.revision, || Thumbs::of(&st.doc)))
}
/// [`Thumbs`] of symbol library `lib`.
pub(crate) fn library_symbols(lib: &Arc<SymbolLibrary>) -> Rc<Thumbs> {
thread_local! {
static MEMO: RefCell<Memo<SymbolLibrary, Thumbs>> = RefCell::new(Memo::default());
}
MEMO.with(|m| m.borrow_mut().get(lib, 0, || Thumbs::of(&lib.doc)))
}
/// Symbol thumbnails' keys worked out and thumbnails rendered on this test thread.
#[cfg(test)]
pub(crate) mod counters {
thread_local! {
pub(crate) static KEYED: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
pub(crate) static RENDERED: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
h.finish()
}
/// Symbol `name` of `doc` rendered `px` pixels wide: an instance at its natural size, alone on a
@@ -73,11 +124,11 @@ fn render_thumb(r: &mut vectorcraft_render::Renderer, doc: &Document, name: &str
r.render_thumbnail(&d, id, px)
}
/// Symbol `name`'s thumbnail on white in `r` (the panel's tiles and the chip a dragged symbol
/// shows at the pointer); `resources` is [`resources_key`] of `doc`.
pub(crate) fn chip(ui: &Ui, doc: &Document, resources: u64, r: egui::Rect, name: &str) {
/// Symbol `name`'s thumbnail (keyed by `keys`, [`document_symbols`] or [`library_symbols`] of
/// `doc`) on white in `r`: the panel's tiles and the chip a dragged symbol shows at the pointer.
pub(crate) fn chip(ui: &Ui, doc: &Document, keys: &Thumbs, r: egui::Rect, name: &str) {
ui.painter().rect_filled(r, 0.0, egui::Color32::WHITE);
if let Some(tex) = thumb(ui, doc, resources, name, r.width()) {
if let Some(tex) = thumb(ui, doc, keys, name, r.width()) {
ui.painter().image(tex.id(), r, egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)), egui::Color32::WHITE);
}
}
@@ -100,9 +151,8 @@ fn has_instances_selected(app: &VectorcraftApp) -> bool {
pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
let t = Tokens::get(ui.ctx());
let Some(doc) = app.session.active().map(|d| d.doc.clone()) else { return };
let Some((doc, keys)) = app.session.active().map(|d| (d.doc.clone(), document_symbols(d))) else { return };
let names: Vec<String> = doc.symbols.iter().map(|s| s.name.clone()).collect();
let resources = resources_key(&doc);
let list: bool = pstate(ui.ctx(), "sym-list");
let sel = current(app);
let mut clicked: Option<(String, bool)> = None;
@@ -123,7 +173,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
} else if resp.hovered() {
ui.painter().rect_filled(r, 0.0, t.hover);
}
chip(ui, &doc, resources, egui::Rect::from_center_size(r.left_center() + vec2(14.0, 0.0), vec2(22.0, 22.0)), n);
chip(ui, &doc, &keys, egui::Rect::from_center_size(r.left_center() + vec2(14.0, 0.0), vec2(22.0, 22.0)), n);
ui.painter().text(r.left_center() + vec2(32.0, 0.0), egui::Align2::LEFT_CENTER, n, egui::FontId::proportional(12.5), t.text);
if resp.double_clicked() {
clicked = Some((n.clone(), true));
@@ -136,7 +186,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
for n in &names {
let (r, resp) = ui.allocate_exact_size(vec2(44.0, 44.0), Sense::click_and_drag());
widgets::drag_source(ui, &resp, || PanelDrag::Symbol(n.clone()));
chip(ui, &doc, resources, r.shrink(2.0), n);
chip(ui, &doc, &keys, r.shrink(2.0), n);
let on = sel.as_deref() == Some(n.as_str());
ui.painter().rect_stroke(
r,
@@ -247,8 +297,6 @@ fn save_library(app: &mut VectorcraftApp, names: Vec<String>) {
pub(crate) struct LibSymbol {
lib: Arc<SymbolLibrary>,
index: usize,
/// [`resources_key`] of the library's document.
resources: u64,
}
impl LibSymbol {
@@ -278,8 +326,7 @@ impl LibraryKind for SymbolLibraries {
}
fn get(app: &VectorcraftApp, id: &str) -> Option<(String, Self::Lib)> {
let (info, lib) = symbollib::library(&app.session, id)?;
let resources = resources_key(&lib.doc);
let shown = (0..lib.len()).map(|index| LibSymbol { lib: lib.clone(), index, resources }).collect();
let shown = (0..lib.len()).map(|index| LibSymbol { lib: lib.clone(), index }).collect();
Some((info.name, Arc::new(shown)))
}
fn find(app: &VectorcraftApp, key: &str) -> library_panel::Found {
@@ -294,7 +341,7 @@ impl LibraryKind for SymbolLibraries {
}
/// The symbol's art on white, as the Symbols panel shows it.
fn draw(_: &VectorcraftApp, ui: &Ui, r: egui::Rect, s: &LibSymbol, selected: bool, hovered: bool) {
chip(ui, &s.lib.doc, s.resources, r, s.name());
chip(ui, &s.lib.doc, &library_symbols(&s.lib), r, s.name());
super::graphic_styles::outline(ui, r, selected, hovered);
}
fn row_icons(_: &Ui, _: egui::Rect, _: &LibSymbol) {}
@@ -0,0 +1,285 @@
//! Headless frames of brush libraries in library panels: clicking a brush adds it to the
//! document's Brushes (and applies it to the selected paths), dragging one carries it out, the
//! Window menu and the Brushes panel open libraries, Save Brushes writes User Defined libraries
//! that reopen, and Other Library… turns down files it can't read.
use egui::{Event, Modifiers, PointerButton, Pos2, Rect, vec2};
use serde_json::json;
use vectorcraft_engine::Session;
use super::brushes::BrushLibraries;
use super::library_panel::{self, LibraryKind, LibraryPanel, last_open};
use crate::VectorcraftApp;
use crate::widgets::PanelDrag;
fn app() -> VectorcraftApp {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 200, "height": 200})).unwrap();
app
}
fn context() -> egui::Context {
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
ctx
}
/// One frame of `draw` with `events`; the texts it painted.
fn frame(ctx: &egui::Context, events: Vec<Event>, time: f64, mut draw: impl FnMut(&mut egui::Ui)) -> Vec<String> {
fn texts(s: &egui::Shape, out: &mut Vec<String>) {
match s {
egui::Shape::Text(t) => out.push(t.galley.text().to_string()),
egui::Shape::Vec(v) => v.iter().for_each(|s| texts(s, out)),
_ => {}
}
}
let screen_rect = Some(Rect::from_min_size(Pos2::ZERO, vec2(1200.0, 800.0)));
let input = egui::RawInput { events, time: Some(time), screen_rect, ..Default::default() };
let mut out = ctx.run_ui(input, &mut draw);
out.textures_delta.clear();
let mut v = vec![];
out.shapes.iter().for_each(|c| texts(&c.shape, &mut v));
v
}
/// The centre of the open library panel's tile of brush `name` (a new window takes a sizing frame
/// first).
fn tile(app: &mut VectorcraftApp, ctx: &egui::Context, name: &str, time: f64) -> Pos2 {
for t in [time, time + 0.25] {
frame(ctx, vec![], t, |ui| library_panel::show_window(app, ui.ctx()));
}
let id = library_panel::tile_id(&last_open(&app.ui).map(LibraryPanel::panel).unwrap_or_default(), name);
ctx.read_response(id).unwrap_or_else(|| panic!("no tile for {name}")).rect.center()
}
fn click(app: &mut VectorcraftApp, ctx: &egui::Context, name: &str, modifiers: Modifiers, time: f64) {
let at = tile(app, ctx, name, time);
let button = |pressed| Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers };
let events = vec![Event::ModifiersChanged(modifiers), Event::PointerMoved(at), button(true), button(false)];
frame(ctx, events, time + 0.5, |ui| library_panel::show_window(app, ui.ctx()));
}
fn undo_len(app: &VectorcraftApp) -> usize {
app.session.doc().unwrap().history.undo.len()
}
fn has(app: &VectorcraftApp, name: &str) -> bool {
vectorcraft_brush::find(&app.session.doc().unwrap().doc, name).is_some()
}
fn count(app: &VectorcraftApp) -> usize {
vectorcraft_brush::library(&app.session.doc().unwrap().doc).len()
}
#[test]
fn clicking_a_library_brush_adds_it_to_the_documents_brushes_in_one_step() {
let mut app = app();
let ctx = context();
let r = app.run("window.brushLibrary", json!({"library": "Art Ribbons"})).unwrap();
assert_eq!((r["open"].as_str(), r["count"].as_u64()), (Some("art-ribbons"), Some(5)));
assert_eq!(last_open(&app.ui).map(|p| (p.kind.as_str(), p.id.as_str())), Some(("brushes", "art-ribbons")));
let (undo, brushes) = (undo_len(&app), count(&app));
click(&mut app, &ctx, "Wave", Modifiers::NONE, 0.0);
assert!(has(&app, "Wave"));
assert_eq!((count(&app), undo_len(&app)), (brushes + 1, undo + 1), "one brush, one undo step");
assert_eq!(app.run("brush.list", json!({})).unwrap()["current"], "Wave", "the Paintbrush's brush");
// Clicking it again adds nothing.
click(&mut app, &ctx, "Wave", Modifiers::NONE, 2.0);
assert_eq!((count(&app), undo_len(&app)), (brushes + 1, undo + 1));
// With a path selected, a click also applies the brush to it, in the same step.
let path = app.run("shape.line", json!({"x1": 10, "y1": 10, "x2": 150, "y2": 60})).unwrap()["id"].as_u64().unwrap();
let undo = undo_len(&app);
click(&mut app, &ctx, "Striped Ribbon", Modifiers::NONE, 4.0);
assert_eq!(undo_len(&app), undo + 1);
let st = app.session.doc().unwrap();
let n = st.doc.node(vectorcraft_doc::NodeId(path)).unwrap();
assert_eq!(n.appearance.stroke().and_then(|s| s.brush.clone()).as_deref(), Some("Striped Ribbon"));
assert!(st.doc.pattern("Pinstripes").is_some(), "with the pattern its art is filled with");
// Modifier clicks only select; Add to Brushes adds the selection.
app.run("select.none", json!({})).unwrap();
click(&mut app, &ctx, "Teardrop", Modifiers::COMMAND, 6.0);
assert!(!has(&app, "Teardrop"));
BrushLibraries::add(&mut app, "art-ribbons", vec!["Teardrop".into(), "Double Line".into()]);
assert!(has(&app, "Teardrop") && has(&app, "Double Line"));
// Find filters by name; list rows give the type.
let lib = BrushLibraries::get(&app, "art-ribbons").unwrap().1;
let rows = BrushLibraries::rows(&lib, "tear");
assert_eq!(rows.iter().map(|r| r.name).collect::<Vec<_>>(), ["Teardrop"]);
assert_eq!(BrushLibraries::describe(rows[0].item.unwrap()), "Art");
// Dragging a brush out of the panel carries it.
let at = tile(&mut app, &ctx, "Notched Ribbon", 8.0);
let press = Event::PointerButton { pos: at, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE };
frame(&ctx, vec![Event::PointerMoved(at), press], 8.5, |ui| library_panel::show_window(&mut app, ui.ctx()));
for (i, d) in [20.0, 60.0].into_iter().enumerate() {
frame(&ctx, vec![Event::PointerMoved(at + vec2(d, d))], 8.6 + 0.1 * i as f64, |ui| library_panel::show_window(&mut app, ui.ctx()));
}
let carried = egui::DragAndDrop::payload::<PanelDrag>(&ctx);
assert_eq!(carried.as_deref(), Some(&PanelDrag::LibraryBrush { library: "art-ribbons".into(), name: "Notched Ribbon".into() }));
assert!(app.run("window.brushLibrary", json!({"library": "nope"})).is_err());
app.run("window.brushLibrary", json!({"library": null})).unwrap();
assert!(last_open(&app.ui).is_none());
}
#[test]
fn the_window_menu_and_the_brushes_panel_open_brush_libraries() {
let mut app = app();
let entries = crate::menus::menu_entries(&app);
let under = |e: &crate::menus::MenuEntry| e.path == ["Window", "Brush Libraries"];
let opened: Vec<_> =
entries.iter().filter(|e| under(e) && e.command.as_deref() == Some("window.brushLibrary")).map(|e| e.params["library"].clone()).collect();
assert_eq!(opened, [json!("art-ribbons"), json!("calligraphic-nibs"), json!("pattern-borders"), json!("scatter-shapes")]);
let enabled = |cmd: &str| entries.iter().find(|e| under(e) && e.command.as_deref() == Some(cmd)).map(|e| e.enabled);
assert_eq!(enabled("window.brushLibrary.other"), Some(true));
assert_eq!(enabled("ui.saveBrushLibrary"), Some(true), "a new document has the default brushes");
assert!(!entries.iter().any(|e| e.path == ["Window", "Brush Libraries", "User Defined"]), "empty slots are hidden");
// The Brushes panel's library button and menu.
let ctx = context();
let texts = frame(&ctx, vec![], 0.0, |ui| {
super::brushes::show(&mut app, ui);
super::brushes::menu(&mut app, ui);
});
for t in ["Open Brush Library", "Save Brush Library…"] {
assert!(texts.iter().any(|x| x.ends_with(t)), "{t}: {texts:?}");
}
// Without brushes there is nothing to save.
let all: Vec<String> = vectorcraft_brush::library(&app.session.doc().unwrap().doc).into_iter().map(|b| b.name).collect();
app.run("brush.delete", json!({ "names": all })).unwrap();
let entries = crate::menus::menu_entries(&app);
assert!(entries.iter().any(|e| under(e) && e.command.as_deref() == Some("ui.saveBrushLibrary") && !e.enabled));
}
#[test]
fn save_brushes_writes_a_user_library_that_reopens_and_adds_to_another_document() {
let dir = std::env::temp_dir().join(format!("vc-brush-libraries-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let mut app = app();
app.session.brush_libraries.set_user_dir(Some(dir.to_string_lossy().to_string()));
app.run("brush.new", json!({"type": "calligraphic", "name": "Quill", "params": {"angle": 60, "roundness": 30, "size": 5}})).unwrap();
app.run("ui.saveBrushLibrary", json!({"names": ["Quill"]})).unwrap();
let d = app.ui.dialog.as_mut().expect("Save Brush Library opens");
assert_eq!((d.kind.as_str(), d.bool("user")), (crate::dialogs::save_brush_library::KIND, true));
d.fields.insert("name".into(), json!("Pens"));
d.fields.insert("selectedOnly".into(), json!(true));
let ctx = context();
frame(&ctx, vec![], 0.0, |ui| crate::dialogs::show(&mut app, ui.ctx()));
let texts = frame(&ctx, vec![], 0.5, |ui| crate::dialogs::show(&mut app, ui.ctx()));
assert!(texts.iter().any(|t| t == "Selected Brushes Only (1)"), "{texts:?}");
crate::dialogs::confirm(&mut app).unwrap();
assert!(app.ui.dialog.is_none());
let user: Vec<(String, String)> = crate::menus::menu_entries(&app)
.into_iter()
.filter(|e| e.path == ["Window", "Brush Libraries", "User Defined"])
.map(|e| (e.label, e.command.unwrap_or_default()))
.collect();
assert_eq!(user, [("Pens".to_string(), "window.userBrushLibrary1".to_string())]);
// Reopened in a library panel, its brush goes into another document.
app.run("file.new", json!({"width": 100, "height": 100})).unwrap();
app.run("window.userBrushLibrary1", json!({})).unwrap();
assert_eq!(last_open(&app.ui).map(|p| p.id.as_str()), Some("user/Pens.vectorcraft"));
let (name, lib) = BrushLibraries::get(&app, "user/Pens.vectorcraft").unwrap();
assert_eq!((name.as_str(), lib.len()), ("Pens", 1), "only the selected brush");
let undo = undo_len(&app);
click(&mut app, &ctx, "Quill", Modifiers::NONE, 1.0);
assert!(has(&app, "Quill"));
assert_eq!(undo_len(&app), undo + 1);
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn other_library_opens_a_documents_brushes_and_reports_unreadable_files() {
let dir = std::env::temp_dir().join(format!("vc-brush-other-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let mut app = app();
app.run("brush.new", json!({"type": "bristle", "name": "Mop"})).unwrap();
let brushes = count(&app);
let doc = dir.join("Studio.vectorcraft");
let b64 = app.run("document.serialize", json!({})).unwrap()["dataBase64"].as_str().unwrap().to_string();
std::fs::write(&doc, vectorcraft_format::base64_decode(&b64).unwrap()).unwrap();
app.run("file.new", json!({"width": 100, "height": 100})).unwrap();
let r = app.run("window.brushLibrary.other", json!({"path": doc.to_string_lossy()})).unwrap();
assert_eq!((r["name"].as_str(), r["count"].as_u64()), (Some("Studio"), Some(brushes as u64)));
let open = last_open(&app.ui).cloned().unwrap();
assert_eq!(open.kind, BrushLibraries::KIND);
let other = BrushLibraries::list(&app).into_iter().find(|l| l.id == open.id).unwrap();
assert_eq!(other.submenu, Some("Other Libraries"));
// A file that isn't a document is an error, and opens no panel.
let bad = dir.join("Broken.vectorcraft");
std::fs::write(&bad, b"not a document").unwrap();
app.run("window.brushLibrary", json!({"library": null})).unwrap();
assert!(app.run("window.brushLibrary.other", json!({"path": bad.to_string_lossy()})).is_err());
assert!(last_open(&app.ui).is_none());
let _ = std::fs::remove_dir_all(dir);
}
/// Brushes worked out (serialised and hashed) since the last call.
fn keyed() -> usize {
super::brushes::counters::KEYED.with(|k| k.replace(0))
}
/// A library panel works out a library's brushes once: frames after the first, and the panel
/// closed and opened again, serialise and hash none, however many art brushes the library has.
/// A library loaded from a document (Other Library…) keeps the brushes it was loaded with when
/// that document is edited. Symbol library panels key their thumbnails once too.
#[test]
fn library_panels_serialise_and_hash_their_brushes_once() {
const MANY: usize = 300;
let dir = std::env::temp_dir().join(format!("vc-brush-many-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let mut app = app();
let ctx = context();
// A document of MANY art brushes whose art is filled with a pattern, as a library.
app.run("brush.addFromLibrary", json!({"library": "art-ribbons", "name": "Striped Ribbon"})).unwrap();
let striped = vectorcraft_brush::find(&app.session.doc().unwrap().doc, "Striped Ribbon").unwrap();
app.session
.edit("Brushes", |d, _| {
let many: Vec<_> = (0..MANY).map(|i| vectorcraft_brush::Brush { name: format!("Ribbon {i}"), ..striped.clone() }).collect();
vectorcraft_brush::store(d, &many);
Ok(())
})
.unwrap();
let path = dir.join("Many.vectorcraft");
let b64 = app.run("document.serialize", json!({})).unwrap()["dataBase64"].as_str().unwrap().to_string();
std::fs::write(&path, vectorcraft_format::base64_decode(&b64).unwrap()).unwrap();
let r = app.run("window.brushLibrary.other", json!({"path": path.to_string_lossy()})).unwrap();
assert_eq!(r["count"].as_u64(), Some(MANY as u64));
let id = last_open(&app.ui).unwrap().id.clone();
let show = |app: &mut VectorcraftApp, t: f64| {
frame(&ctx, vec![], t, |ui| library_panel::show_window(app, ui.ctx()));
};
keyed();
show(&mut app, 0.0);
assert_eq!(keyed(), MANY, "the first frame works each brush out");
for i in 1..4 {
show(&mut app, i as f64);
}
assert_eq!(keyed(), 0, "later frames serialise and hash none");
// Closed and opened again.
app.run("window.brushLibrary", json!({"library": null})).unwrap();
show(&mut app, 5.0);
app.run("window.brushLibrary", json!({"library": id})).unwrap();
show(&mut app, 6.0);
show(&mut app, 7.0);
assert_eq!(keyed(), 0, "reopened, the library's brushes are still known");
// The document it was loaded from edited: the library doesn't change.
app.session
.edit("Brushes", |d, _| {
vectorcraft_brush::store(d, &[]);
Ok(())
})
.unwrap();
show(&mut app, 8.0);
assert_eq!((keyed(), BrushLibraries::get(&app, &id).unwrap().1.len()), (0, MANY));
app.run("window.brushLibrary", json!({"library": null})).unwrap();
// A symbol library.
use super::symbols::counters::KEYED;
app.run("window.symbolLibrary", json!({"library": "basic-shapes"})).unwrap();
KEYED.with(|k| k.set(0));
show(&mut app, 9.0);
assert!(KEYED.with(|k| k.replace(0)) > 0);
show(&mut app, 10.0);
show(&mut app, 11.0);
assert_eq!(KEYED.with(|k| k.get()), 0, "symbol thumbnails are keyed once");
let _ = std::fs::remove_dir_all(dir);
}
+9 -5
View File
@@ -292,13 +292,17 @@ fn effect_menu_keeps_glowing_edges_out_of_the_vector_stylize() {
}
#[test]
fn brush_libraries_submenu_is_a_disabled_placeholder() {
fn window_library_submenus_have_no_placeholders() {
let app = app();
let entries = menus::menu_entries(&app);
let items: Vec<_> = entries.iter().filter(|e| e.path == ["Window", "Brush Libraries"]).collect();
let labels: Vec<&str> = items.iter().map(|e| e.label.as_str()).collect();
assert_eq!(labels, ["Built-in Libraries", "User Defined", "Other Library…"]);
assert!(items.iter().all(|e| !e.enabled && e.command.is_none()));
for lib in ["Brush Libraries", "Graphic Style Libraries", "Swatch Libraries", "Symbol Libraries"] {
let items: Vec<_> = entries
.iter()
.filter(|e| e.path.first().map(String::as_str) == Some("Window") && e.path.get(1).map(String::as_str) == Some(lib))
.collect();
assert!(!items.is_empty(), "{lib}");
assert!(items.iter().all(|e| e.command.is_some()), "{lib}: {:?}", items.iter().map(|e| &e.label).collect::<Vec<_>>());
}
}
#[test]
+3
View File
@@ -2667,6 +2667,9 @@ pub enum PanelDrag {
/// A Brushes panel brush (`def`: its definition, for the chip at the pointer): the path it is
/// dropped on takes it (`brush.apply`).
Brush { name: String, def: serde_json::Value },
/// A brush of a brush library panel (`name`: its name in `library`): the path it is dropped on
/// takes it (`brush.addFromLibrary` with `apply`), and the Brushes panel adds it.
LibraryBrush { library: String, name: String },
/// A Libraries panel graphic (`item`: its id in `library`): the canvas places a copy centred
/// where it is dropped (`library.use`); the panel moves it to where it is dropped.
LibraryGraphic { library: String, item: String },
+14 -1
View File
@@ -1,6 +1,6 @@
# Control protocol
> **Last updated:** 2026-10-25 · **Change:** Transform Each opens on the values it was last confirmed with; its Copy button confirms with `copy: true` (#1732); previously: panel docking on craft-ui's layout: `ui.docking`, the parameters of `window.panel.float` and `window.panel.dock`; previously: `ui.screenshot`'s 64 MiB inline-PNG cap and its path-plus-data downgrade; symbol library panels (`window.symbolLibrary`, `window.symbolLibrary.other`, `ui.saveSymbolLibrary`); synthetic input budgets (#1709); bounded transport waits (#1726).
> **Last updated:** 2026-10-25 · **Change:** brush library panels (`window.brushLibrary`, `window.brushLibrary.other`, `ui.saveBrushLibrary`); previously: Transform Each opens on the values it was last confirmed with; its Copy button confirms with `copy: true` (#1732); previously: panel docking on craft-ui's layout: `ui.docking`, the parameters of `window.panel.float` and `window.panel.dock`; previously: `ui.screenshot`'s 64 MiB inline-PNG cap and its path-plus-data downgrade; symbol library panels (`window.symbolLibrary`, `window.symbolLibrary.other`, `ui.saveSymbolLibrary`); synthetic input budgets (#1709); bounded transport waits (#1726).
`vectorcraft --control <port>` listens on `127.0.0.1:<port>` (loopback only). The port must be a number
from 1 to 65535 (`--control=<port>` works too), given once; otherwise the app prints an error and exits
@@ -286,6 +286,18 @@ Defined libraries are `window.userSymbolLibrary1`…`10` in Window → Symbol Li
save to the user library folder, `selectedOnly` with `names`); `ui.dialog.confirm` runs `symbol.saveLibrary` (to a
file it asks for a path and suggests `name` with `.vectorcraft`).
Brush libraries open in the same panels: `window.brushLibrary {library}` (`library_panels` kind `brushes`; `library:
null` closes them). Clicking a brush there runs `brush.addFromLibrary {library, name}`, adding it to the document's
Brushes in one undo step, with `apply: true` when paths are selected (they take the brush in the same step; the step is
Apply Brush when the document has the brush already and the click only applies it);
Shift/Cmd-clicks select brushes for Add to Brushes. A brush dragged onto a path runs it with `apply: true` and that
path as `ids`, and one dragged onto the Brushes panel runs it without. `window.brushLibrary.other {path?}` loads the
brushes of any document VectorCraft opens (a saved brush library is a `.vectorcraft` document) and opens them there.
The User Defined libraries are `window.userBrushLibrary1`…`10` in Window → Brush Libraries. `ui.saveBrushLibrary
{names?}` (Save Brushes…, the Brushes panel's Save Brush Library…) opens the `saveBrushLibrary` dialog (fields
`name`, `user`: save to the user library folder, `selectedOnly` with `names`); `ui.dialog.confirm` runs
`brush.saveLibrary` (to a file it asks for a path and suggests `name` with `.vectorcraft`).
Tile Edge Color: Object → Pattern → Tile Edge Color… (`ui.tileEdgeColor`) opens the `tileEdgeColor` dialog (field
`color`: `#rrggbb` or a preset name such as "Light Blue"); `ui.dialog.confirm` sets the preference
`patternTileEdgeColor` (`prefs.set`), the colour pattern editing mode draws the tile edge and the swatch bounds in.
@@ -931,6 +943,7 @@ hidden tab (whose frames the browser pauses) answers when it is shown again.
| Date | Change | Summary |
|---|---|---|
| 2026-10-25 | minor | Panel docking on craft-ui's layout: `ui.docking`, the floating and dock parameters, native floating windows. |
| 2026-10-24 | minor | Documented the brush library panel commands (`window.brushLibrary`, `.other`, `ui.saveBrushLibrary`, `window.userBrushLibrary1`…`10`). |
| 2026-10-24 | minor | Documented `ui.screenshot`'s 64 MiB inline-PNG reply cap: with `path` the file is still written and `pngBase64` is omitted when over the cap; a path-less `data: true` request fails before encoding. |
| 2026-10-25 | minor | Documented that Transform Each opens on the values it was last confirmed with and that its Copy button confirms with `copy: true` (#1732). |
| 2026-10-23 | minor | Documented the symbol library panel commands (`window.symbolLibrary`, `.other`, `ui.saveSymbolLibrary`, `window.userSymbolLibrary1`…`10`). |
+15 -4
View File
File diff suppressed because one or more lines are too long
+32 -6
View File
@@ -105,7 +105,7 @@ The two flags (PhotoCraft and the other Craft apps take the same ones, so one cl
the suite) keep the agent to the folders you give it:
- Every file a command reads must lie inside a **read root**: `open_file`, `file.place`, relinking and
updating links, the images an SVG links to, swatch library, graphic style library, symbol library and colour-profile loads, plug-ins,
updating links, the images an SVG links to, swatch library, graphic style library, symbol library, brush library and colour-profile loads, plug-ins,
`file.newFromTemplate`, preset imports, shortcut sets (`shortcutSet.read`), the folders `text.findFontFiles` and `links.findFiles` search, the
scripts `script.check` reads and the files they include.
- Every file a command writes must lie inside a **write root**: `save_file`, `export`, the save and
@@ -1860,18 +1860,44 @@ document holding only them and what they use; without `path` it returns `{dataBa
the user library folder (category `user`, User Defined). `symbol.loadLibrary {path? | data? | dataBase64?, name?}`
loads the symbols of any document VectorCraft opens as a library to add from; a file it can't read is an error.
## Brush libraries
Brush libraries are read-only sets of brushes: four generated in code (Art Ribbons, Calligraphic Nibs, Pattern
Borders, Scatter Shapes), the `.vectorcraft` files in the user library folder of the desktop app (User Defined) and
the brushes of any document loaded with `brush.loadLibrary` (a document without brushes of its own has the default
ones). `brush.libraries` lists them (`id`, `name`, `category`: `builtIn`, `user` or `loaded`, `count`) and
`brush.library {library}` returns one's brushes (`name`, `type`; `library` is an id or a name).
`brush.addFromLibrary {library, name? | names?}` copies brushes into the document's Brushes as one undo step (no
names: the whole library), with the resources their art uses: patterns, symbols, swatches (`swatchConflict` as for
`edit.paste`), graphic styles and images. Brushes the document already has (same name and definition) are reported
under `existing`; a name another brush has gets a number, and `names` gives each one's name in the document. The
first one becomes the current brush. `apply: true` also applies it to the paths `ids` (default: the selected ones)
in the same step, as `brush.apply` does: what clicking a library brush with paths selected, or dragging one onto a
path, does.
```json
{"name":"run_command","arguments":{"command":"brush.addFromLibrary","params":{"library":"art-ribbons","name":"Striped Ribbon","apply":true}}}
```
`brush.saveLibrary {path?, names?, name?, user?}` writes the document's brushes as a brush library: a `.vectorcraft`
document holding only them and what their art uses; without `path` it returns `{dataBase64}`, and `user: true`
saves into the user library folder (category `user`, User Defined). `brush.loadLibrary {path? | data? |
dataBase64?, name?}` loads the brushes of any document VectorCraft opens as a library to add from; a file it can't
read is an error.
## Library panels
Each swatch, graphic style or symbol library opens in a read-only panel of its own (#2603), as in Illustrator, and
several can be open at once. `window.swatchLibrary {library}`, `window.graphicStyleLibrary {library}` and
`window.symbolLibrary {library}`
Each swatch, graphic style, symbol or brush library opens in a read-only panel of its own (#2603), as in Illustrator,
and several can be open at once. `window.swatchLibrary {library}`, `window.graphicStyleLibrary {library}`,
`window.symbolLibrary {library}` and `window.brushLibrary {library}`
(remote mode, `invoke_menu`) open one floating, or bring its panel forward when it is already open, and answer
`{open, panel, name, count}`; `library: null` closes every panel of that kind. The panel id is
`library:<kind>:<library id>` (`library:swatches:pastels`, `library:graphicStyles:hand-drawn`,
`library:swatches:loaded/C:/Palettes/Brand.ase`), and the panel commands take it like any other panel's:
`window.panel.float {panel, onto?, x?, y?}` groups or moves it, `window.panel.dock {panel}` docks it,
`window.panel.close {panel}` closes it. `window.swatchLibrary.other {path}` and `window.graphicStyleLibrary.other
{path}` load a file and open it the same way.
`window.panel.close {panel}` closes it. `window.swatchLibrary.other {path}`, `window.graphicStyleLibrary.other
{path}`, `window.symbolLibrary.other {path}` and `window.brushLibrary.other {path}` load a file and open it the same
way.
`ui.libraryPanel {panel, library?, view?, showFind?, persistent?}` sets one panel: `library` shows another library
of its kind in that panel (what Previous Library and Next Library do; a library open in a panel of its own is
+4 -3
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+3 -1
View File
File diff suppressed because one or more lines are too long