mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 10:47:11 -06:00
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:
committed by
bflatastic
parent
a1ef364191
commit
0a241a7fdd
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()}))
|
||||
}
|
||||
@@ -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) }))
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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()),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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
File diff suppressed because one or more lines are too long
+32
-6
@@ -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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+3
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user