mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Brushes: New Brush and Bristle Brush Options take a bristle brush name of at most 31 characters
Squashed from #3312 onto main.
This commit is contained in:
@@ -413,6 +413,8 @@ impl Bristle {
|
||||
pub const SIZE_RANGE: (f64, f64) = (0.1, 1296.0);
|
||||
pub const LENGTH_RANGE: (f64, f64) = (25.0, 300.0);
|
||||
pub const PERCENT_RANGE: (f64, f64) = (1.0, 100.0);
|
||||
/// The most characters `brush.new` and `brush.options` take for a new bristle brush name.
|
||||
pub const MAX_NAME_CHARS: usize = 31;
|
||||
}
|
||||
|
||||
impl Default for Bristle {
|
||||
|
||||
@@ -10,7 +10,7 @@ use std::collections::BTreeSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_brush::{self as brush, Brush};
|
||||
use vectorcraft_brush::{self as brush, Bristle, Brush, BrushKind};
|
||||
use vectorcraft_color::{Color, Paint};
|
||||
use vectorcraft_doc::{AppearanceItem, Document, FillLayer, Node, NodeId, NodeKind, Symbol, SymbolExportType, SymbolNineSlice, SymbolRegistration};
|
||||
use vectorcraft_geom::{Affine, Point, Vec2};
|
||||
@@ -72,7 +72,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"New Brush…",
|
||||
[],
|
||||
None,
|
||||
"{type: calligraphic|scatter|art|pattern|bristle, name? (trimmed; blank: New <Type> Brush), params?: {…definition fields}, ids?|id?} new brush; art/scatter/pattern brushes take the selected art (pattern: side tile) unless params give it → {name}",
|
||||
"{type: calligraphic|scatter|art|pattern|bristle, name? (trimmed; blank: New <Type> Brush; a bristle brush name, with the number a taken name gets, has at most 31 characters), params?: {…definition fields}, ids?|id?} new brush; art/scatter/pattern brushes take the selected art (pattern: side tile) unless params give it → {name}",
|
||||
has_doc,
|
||||
brush_new
|
||||
),
|
||||
@@ -117,7 +117,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Brush Options…",
|
||||
[],
|
||||
None,
|
||||
"{name, params?: {…fields to change (brush.get shows them; values outside their ranges are clamped). calligraphic: angle −180..180°, roundness 0..100%, size 0..1296 pt, variation: [angle°, roundness%, size pt], modes: [fixed|random|pressure for angle, roundness, size] (pressure goes from value − variation at the lightest pen pressure to value + variation at the heaviest). scatter: size [a, b] 1..10000%, spacing [a, b] 1..10000%, scatter [a, b] −1000..1000%, rotation [a, b] −180..180°, modes: [fixed|random|pressure for size, spacing, scatter, rotation] (fixed: a; random: between a and b; pressure: a at the lightest pen pressure to b at the heaviest), rotation_relative_to_path. art: width 1..1000%, scale: {mode: proportional|stretch|betweenGuides, start, end: 0..1 of the art length}, direction: leftToRight|rightToLeft|topToBottom|bottomToTop, flip_along, flip_across, overlap. pattern: scale 1..10000%, spacing 0..10000%, fit: stretch|addSpace|approximate, flip_along, flip_across. scatter, art and pattern: colorization: {method: none|tints|tintsAndShades|hueShift, key: colour (hueShift: the art colour that becomes the stroke colour; without a key: the solid color the art's fills and strokes use most, black when they use none)}; colorization and the art brush's scale change only the fields given}, newName? (trimmed; blank: the name is kept)} edit a brush definition (strokes using it update) → {name}",
|
||||
"{name, params?: {…fields to change (brush.get shows them; values outside their ranges are clamped). calligraphic: angle −180..180°, roundness 0..100%, size 0..1296 pt, variation: [angle°, roundness%, size pt], modes: [fixed|random|pressure for angle, roundness, size] (pressure goes from value − variation at the lightest pen pressure to value + variation at the heaviest). scatter: size [a, b] 1..10000%, spacing [a, b] 1..10000%, scatter [a, b] −1000..1000%, rotation [a, b] −180..180°, modes: [fixed|random|pressure for size, spacing, scatter, rotation] (fixed: a; random: between a and b; pressure: a at the lightest pen pressure to b at the heaviest), rotation_relative_to_path. art: width 1..1000%, scale: {mode: proportional|stretch|betweenGuides, start, end: 0..1 of the art length}, direction: leftToRight|rightToLeft|topToBottom|bottomToTop, flip_along, flip_across, overlap. pattern: scale 1..10000%, spacing 0..10000%, fit: stretch|addSpace|approximate, flip_along, flip_across. scatter, art and pattern: colorization: {method: none|tints|tintsAndShades|hueShift, key: colour (hueShift: the art colour that becomes the stroke colour; without a key: the solid color the art's fills and strokes use most, black when they use none)}; colorization and the art brush's scale change only the fields given}, newName? (trimmed; blank: the name is kept; a new bristle brush name has at most 31 characters, and a longer name the brush has is kept)} edit a brush definition (strokes using it update) → {name}",
|
||||
has_doc,
|
||||
brush_options
|
||||
),
|
||||
@@ -501,6 +501,9 @@ fn brush_new(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let given = str_param(p, "name").map(str::trim).filter(|n| !n.is_empty());
|
||||
let base_name = given.map_or_else(|| format!("New {} Brush", ty[..1].to_uppercase() + &ty[1..]), str::to_string);
|
||||
let name = brush::unique_name(&lib, &base_name);
|
||||
if ty == "bristle" {
|
||||
bristle_name_fits(C, &name)?;
|
||||
}
|
||||
let mut v = json!({"name": name, "type": ty});
|
||||
if let Some(params) = p.get("params") {
|
||||
merge(&mut v, params);
|
||||
@@ -521,6 +524,15 @@ fn brush_new(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(json!({ "name": name }))
|
||||
}
|
||||
|
||||
/// Refuses `name` for a bristle brush when it has more than [`Bristle::MAX_NAME_CHARS`] characters.
|
||||
fn bristle_name_fits(cmd: &str, name: &str) -> Result<()> {
|
||||
let (count, max) = (name.chars().count(), Bristle::MAX_NAME_CHARS);
|
||||
if count > max {
|
||||
return Err(bad(cmd, format!("a bristle brush name can have at most {max} characters: `{name}` has {count}")));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A brush definition from command params, its values kept within their ranges ([`Brush::from_json`]).
|
||||
fn parse_brush(cmd: &str, v: Value) -> Result<Brush> {
|
||||
Brush::from_json(v).map_err(|e| bad(cmd, format!("invalid brush: {e}")))
|
||||
@@ -659,6 +671,10 @@ fn brush_options(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if lib.iter().any(|b| &b.name == nn) {
|
||||
return Err(bad(C, format!("a brush named `{nn}` exists")));
|
||||
}
|
||||
// A `newName` that only trims the white space around the saved name is taken at any length.
|
||||
if matches!(lib[i].kind, BrushKind::Bristle(_)) && nn != name.trim() {
|
||||
bristle_name_fits(C, nn)?;
|
||||
}
|
||||
v["name"] = json!(nn);
|
||||
} else {
|
||||
v["name"] = json!(name);
|
||||
|
||||
@@ -352,6 +352,54 @@ fn new_duplicate_and_renamed_brushes_trim_their_names() {
|
||||
assert_eq!(names[names.len() - 6..], ["Nib", " Old ", "New Calligraphic Brush", "Quill", "Nib copy", "Old copy"]);
|
||||
}
|
||||
|
||||
/// New Brush and Bristle Brush Options take a bristle brush name of at most 31 characters, counted
|
||||
/// in Unicode characters (31 é's fit). A longer name that a document already holds is kept, and the
|
||||
/// other brush types take a name of any length.
|
||||
#[test]
|
||||
fn bristle_brush_names_have_at_most_31_characters() {
|
||||
let mut s = session();
|
||||
let library = |s: &Session| vectorcraft_brush::library(&s.doc().unwrap().doc);
|
||||
let refused = |s: &mut Session, cmd: &str, p: Value| s.execute(cmd, &p).unwrap_err().to_string();
|
||||
let (long, fits) = ("b".repeat(32), "é".repeat(31));
|
||||
let e = refused(&mut s, "brush.new", json!({"type": "bristle", "name": long}));
|
||||
assert!(e.contains("a bristle brush name can have at most 31 characters: `bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb` has 32"), "{e}");
|
||||
assert!(!library(&s).iter().any(|b| b.name == long));
|
||||
assert_eq!(s.execute("brush.new", &json!({"type": "bristle", "name": fits})).unwrap()["name"], fits.as_str());
|
||||
// A taken name is numbered, and the numbered name is the one counted.
|
||||
let e = refused(&mut s, "brush.new", json!({"type": "bristle", "name": fits}));
|
||||
assert!(e.contains(&format!("`{fits} 2` has 33")), "{e}");
|
||||
let e = refused(&mut s, "brush.options", json!({"name": "Bristle Round", "newName": long}));
|
||||
assert!(e.contains("at most 31 characters"), "{e}");
|
||||
let short = "c".repeat(31);
|
||||
assert_eq!(s.execute("brush.options", &json!({"name": "Bristle Round", "newName": short})).unwrap()["name"], short.as_str());
|
||||
// Calligraphic brushes take a longer name.
|
||||
assert_eq!(s.execute("brush.new", &json!({"type": "calligraphic", "name": long})).unwrap()["name"], long.as_str());
|
||||
let longer = format!("{long}x");
|
||||
assert_eq!(s.execute("brush.options", &json!({"name": long, "newName": longer})).unwrap()["name"], longer.as_str());
|
||||
// Bristle brushes that already have longer names, one of them with white space around it, as a
|
||||
// saved file can hold.
|
||||
let (old, spaced) = ("d".repeat(40), format!(" {} ", "e".repeat(40)));
|
||||
let mut lib = library(&s);
|
||||
lib.iter_mut().filter(|b| b.name == short).for_each(|b| b.name = old.clone());
|
||||
lib.iter_mut().filter(|b| b.name == fits).for_each(|b| b.name = spaced.clone());
|
||||
vectorcraft_brush::store(std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc), &lib);
|
||||
let l = line(&mut s);
|
||||
s.execute("brush.apply", &json!({"name": old, "ids": [l.0]})).unwrap();
|
||||
// Brush Options without a new name, with the same name, or with the name only trimmed keep them.
|
||||
s.execute("brush.options", &json!({"name": old, "params": {"size": 9}})).unwrap();
|
||||
assert_eq!(s.execute("brush.options", &json!({"name": old, "newName": old, "params": {"density": 20}})).unwrap()["name"], old.as_str());
|
||||
assert_eq!(s.execute("brush.options", &json!({"name": spaced, "newName": spaced})).unwrap()["name"], spaced.trim());
|
||||
assert_eq!(s.execute("brush.get", &json!({"name": old})).unwrap()["size"], json!(9.0));
|
||||
assert_eq!(s.execute("brush.duplicate", &json!({"name": old})).unwrap()["name"], format!("{old} copy"));
|
||||
// Saved and opened again, the brush and the stroke painted with it keep the name.
|
||||
let back = vectorcraft_format::load(&vectorcraft_format::save(&s.doc().unwrap().doc, false)).unwrap();
|
||||
assert!(vectorcraft_brush::find(&back, &old).is_some_and(|b| b.kind.type_id() == "bristle"));
|
||||
assert_eq!(back.node(l).unwrap().appearance.stroke().unwrap().brush.as_deref(), Some(old.as_str()));
|
||||
let mut s2 = Session::new();
|
||||
s2.add_document(back, None);
|
||||
assert_eq!(s2.execute("brush.options", &json!({"name": old, "params": {"size": 4}})).unwrap()["name"], old.as_str());
|
||||
}
|
||||
|
||||
/// The Brushes panel's multi-selection: `names` deletes or duplicates several brushes in one undo
|
||||
/// step, and an unknown name changes nothing.
|
||||
#[test]
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
//! Brush Options (double-click a brush in the Brushes panel, or the panel menu's Brush Options…).
|
||||
//! Calligraphic Brush Options: Name, then Angle, Roundness and Size, each Fixed, Random or
|
||||
//! Pressure with its Variation, over live previews of the nib and of a stroke (light, heavy, light
|
||||
//! pressure). Bristle Brush Options: Name, Shape and the bristle sliders. Scatter, Art and Pattern
|
||||
//! Brush Options: see [`super::brush_art_options`]. OK runs `brush.options` (strokes painted with
|
||||
//! the brush update); a name another brush has keeps the dialog open.
|
||||
//! pressure). Bristle Brush Options: Name (typing stops at [`Bristle::MAX_NAME_CHARS`]
|
||||
//! characters), Shape and the bristle sliders. Scatter, Art and Pattern Brush Options: see
|
||||
//! [`super::brush_art_options`]. OK runs `brush.options` (strokes painted with the brush update);
|
||||
//! a name another brush has, or a new bristle brush name that is too long, keeps the dialog open.
|
||||
//!
|
||||
//! Fields: `__brush` (the brush's name), `__type` (`calligraphic` | `bristle` | `scatter` | `art` |
|
||||
//! `pattern`), `name`.
|
||||
@@ -233,7 +234,8 @@ fn params(d: &Dialog) -> Value {
|
||||
fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
grid(ui, |ui| {
|
||||
label(ui, tl!("Name:"));
|
||||
form::text(ui, d, "name", 240.0);
|
||||
let max = if d.str("__type") == "bristle" { Bristle::MAX_NAME_CHARS } else { usize::MAX };
|
||||
form::text_edit_limited(ui, d, "name", 240.0, max);
|
||||
ui.end_row();
|
||||
});
|
||||
ui.add_space(10.0);
|
||||
@@ -344,7 +346,8 @@ fn nib_preview(ui: &mut egui::Ui, c: &Calligraphic, size: egui::Vec2) {
|
||||
p.rect_stroke(r, 0.0, Stroke::new(1.0, t.input_border), egui::StrokeKind::Inside);
|
||||
}
|
||||
|
||||
/// OK: edit the brush (and rename it). A name another brush has keeps the dialog open.
|
||||
/// OK: edit the brush (and rename it). A name another brush has, or a new bristle brush name that
|
||||
/// is too long, keeps the dialog open.
|
||||
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
let name = d.str("name").trim().to_string();
|
||||
let r = app.run("brush.options", json!({ "name": d.str("__brush"), "newName": name, "params": params(d) }));
|
||||
@@ -426,6 +429,36 @@ mod tests {
|
||||
assert!(app.ui.dialog.is_none());
|
||||
}
|
||||
|
||||
/// Bristle Brush Options' Name field takes at most 31 characters as they are typed; the
|
||||
/// Calligraphic Brush Options' Name field takes more.
|
||||
#[test]
|
||||
fn bristle_brush_options_name_field_takes_at_most_31_characters() {
|
||||
let mut app = app();
|
||||
let typed = "é".repeat(40);
|
||||
for (brush, kept) in [("Bristle Round", 31), ("6 pt. Flat", 40)] {
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
let frame = |app: &mut VectorcraftApp, events: Vec<egui::Event>| {
|
||||
let mut out = ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| super::super::show(app, ui.ctx()));
|
||||
out.textures_delta.clear();
|
||||
};
|
||||
app.run("ui.brushOptions", json!({ "name": brush })).unwrap();
|
||||
// The Name field takes the focus with its text selected; typing replaces it.
|
||||
frame(&mut app, vec![]);
|
||||
frame(&mut app, vec![]);
|
||||
frame(&mut app, vec![egui::Event::Text(typed.clone())]);
|
||||
assert_eq!(app.ui.dialog.as_ref().unwrap().str("name"), "é".repeat(kept), "{brush}");
|
||||
super::super::confirm(&mut app).unwrap();
|
||||
assert!(app.run("brush.get", json!({ "name": "é".repeat(kept) })).is_ok(), "{brush}");
|
||||
}
|
||||
// A new name of more than 31 characters in the field (a longer saved name with some of its
|
||||
// characters deleted) is refused, and the dialog stays open.
|
||||
app.run("ui.brushOptions", json!({ "name": "é".repeat(31) })).unwrap();
|
||||
set(&mut app, json!({ "name": "é".repeat(35) }));
|
||||
assert!(super::super::confirm(&mut app).is_err());
|
||||
assert!(app.ui.dialog.is_some());
|
||||
}
|
||||
|
||||
/// A path painted with brush `name`, and the height of the art its stroke draws.
|
||||
fn brushed(app: &mut VectorcraftApp, name: &str) -> (vectorcraft_doc::NodeId, f64) {
|
||||
let id = app.run("shape.line", json!({"x1": 20, "y1": 100, "x2": 280, "y2": 100})).unwrap()["id"].as_u64().unwrap();
|
||||
|
||||
@@ -134,6 +134,13 @@ pub(super) fn text(ui: &mut egui::Ui, d: &mut Dialog, key: &str, width: f32) ->
|
||||
|
||||
/// [`text`] returning the field's response (focus, Enter).
|
||||
pub(super) fn text_edit(ui: &mut egui::Ui, d: &mut Dialog, key: &str, width: f32) -> egui::Response {
|
||||
text_edit_limited(ui, d, key, width, usize::MAX)
|
||||
}
|
||||
|
||||
/// [`text_edit`] that takes at most `max_chars` characters: typing and pasting stop at the limit,
|
||||
/// and a longer value already in the field is kept, with nothing added to it until it has fewer
|
||||
/// than `max_chars` characters.
|
||||
pub(super) fn text_edit_limited(ui: &mut egui::Ui, d: &mut Dialog, key: &str, width: f32, max_chars: usize) -> egui::Response {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let mut s = d.str(key);
|
||||
let id = ui.id().with(("dlg-text", key));
|
||||
@@ -145,7 +152,7 @@ pub(super) fn text_edit(ui: &mut egui::Ui, d: &mut Dialog, key: &str, width: f32
|
||||
.inner_margin(egui::Margin::symmetric(6, 3))
|
||||
.show(ui, |ui| {
|
||||
crate::theme::strong_selected_text(ui);
|
||||
ui.add(egui::TextEdit::singleline(&mut s).id(id).frame(egui::Frame::NONE).desired_width(width))
|
||||
ui.add(egui::TextEdit::singleline(&mut s).id(id).frame(egui::Frame::NONE).desired_width(width).char_limit(max_chars))
|
||||
});
|
||||
if r.inner.changed() {
|
||||
d.fields.insert(key.into(), Value::String(s));
|
||||
|
||||
@@ -3933,6 +3933,7 @@ axis Both И двете
|
||||
@msg no blend options няма опции за преливане
|
||||
@msg no bounds няма граници
|
||||
@msg no brush named `{name}` няма четка с име `{name}`
|
||||
@msg a bristle brush name can have at most {max} characters: `{name}` has {count} най-голяма дължина на името на четка с косъм (знаци): {max}; в `{name}` знаците са {count}
|
||||
@msg no brush selected няма избрана четка
|
||||
@msg brushes move only within their type четките се преместват само в рамките на своя тип
|
||||
@msg no catalog няма каталог
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4323,6 +4323,7 @@ imageTrace Outline Obrys
|
||||
@msg no blend options žádné možnosti prolnutí
|
||||
@msg no bounds žádné hranice
|
||||
@msg no brush named `{name}` štětec s názvem `{name}` neexistuje
|
||||
@msg a bristle brush name can have at most {max} characters: `{name}` has {count} název štětinového štětce může mít nejvýše {max} znaků: `{name}` jich má {count}
|
||||
@msg no brush selected není vybrán žádný štětec
|
||||
@msg brushes move only within their type štětce lze přesouvat jen v rámci jejich typu
|
||||
@msg no catalog chybí katalog
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3952,6 +3952,7 @@ theme Appearance Erscheinungsbild
|
||||
@msg no blend options keine Angleichungsoptionen
|
||||
@msg no bounds keine Begrenzung
|
||||
@msg no brush named `{name}` kein Pinsel namens `{name}`
|
||||
@msg a bristle brush name can have at most {max} characters: `{name}` has {count} der Name eines Borstenpinsels darf höchstens {max} Zeichen lang sein: `{name}` hat {count}
|
||||
@msg no brush selected kein Pinsel ausgewählt
|
||||
@msg brushes move only within their type Pinsel lassen sich nur innerhalb ihrer Art verschieben
|
||||
@msg this kind of brush has no options dialog yet für diese Pinselart gibt es noch kein Optionsdialogfeld
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3989,6 +3989,7 @@ theme Appearance Εμφάνιση
|
||||
@msg no blend options δεν υπάρχουν επιλογές ανάμειξης
|
||||
@msg no bounds δεν υπάρχουν όρια
|
||||
@msg no brush named `{name}` δεν υπάρχει πινέλο με όνομα `{name}`
|
||||
@msg a bristle brush name can have at most {max} characters: `{name}` has {count} το όνομα ενός πινέλου με τρίχες μπορεί να έχει έως {max} χαρακτήρες: το `{name}` έχει {count}
|
||||
@msg no brush selected δεν έχει επιλεγεί πινέλο
|
||||
@msg brushes move only within their type τα πινέλα μετακινούνται μόνο μέσα στον τύπο τους
|
||||
@msg no catalog δεν υπάρχει κατάλογος
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2029 and column 19.
|
@@ -4021,6 +4021,7 @@ axis Both Ambos
|
||||
@msg no blend options no hay opciones de fusión
|
||||
@msg no bounds sin límites
|
||||
@msg no brush named `{name}` no hay ningún pincel llamado `{name}`
|
||||
@msg a bristle brush name can have at most {max} characters: `{name}` has {count} el nombre de un pincel de cerdas puede tener como máximo {max} caracteres: `{name}` tiene {count}
|
||||
@msg no brush selected no hay ningún pincel seleccionado
|
||||
@msg brushes move only within their type los pinceles solo se pueden mover dentro de su tipo
|
||||
@msg no catalog no hay catálogo
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4238,6 +4238,7 @@ imageTrace Outline Ääriviiva
|
||||
@msg no blend options ei sulautusasetuksia
|
||||
@msg no bounds ei rajoja
|
||||
@msg no brush named `{name}` ei sivellintä nimeltä `{name}`
|
||||
@msg a bristle brush name can have at most {max} characters: `{name}` has {count} harjassiveltimen nimessä voi olla enintään {max} merkkiä: nimessä `{name}` on {count} merkkiä
|
||||
@msg no brush selected sivellintä ei ole valittu
|
||||
@msg brushes move only within their type siveltimiä voi siirtää vain oman tyyppinsä sisällä
|
||||
@msg no catalog ei luetteloa
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4018,6 +4018,7 @@ axis Both Les deux
|
||||
@msg no blend options aucune option de dégradé de formes
|
||||
@msg no bounds aucune limite
|
||||
@msg no brush named `{name}` aucune forme nommée `{name}`
|
||||
@msg a bristle brush name can have at most {max} characters: `{name}` has {count} le nom d’une forme Soies peut compter au plus {max} caractères, et `{name}` en compte {count}
|
||||
@msg no brush selected aucune forme sélectionnée
|
||||
@msg brushes move only within their type les formes ne se déplacent qu’à l’intérieur de leur type
|
||||
@msg no catalog aucun catalogue
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4658,6 +4658,7 @@ imageTrace Outline Körvonal
|
||||
@msg no blend options nincsenek összemosási beállítások
|
||||
@msg no bounds nincsenek határok
|
||||
@msg no brush named `{name}` nincs ilyen nevű ecset: `{name}`
|
||||
@msg a bristle brush name can have at most {max} characters: `{name}` has {count} a sörteecset neve legfeljebb {max} karakter hosszú lehet: `{name}` hossza {count} karakter
|
||||
@msg no brush selected nincs kijelölt ecset
|
||||
@msg brushes move only within their type az ecsetek csak a saját típusukon belül helyezhetők át
|
||||
@msg no catalog nincs katalógus
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4061,6 +4061,7 @@ axis Both Entrambi
|
||||
@msg no blend options nessuna opzione di fusione
|
||||
@msg no bounds nessun limite
|
||||
@msg no brush named `{name}` nessun pennello denominato `{name}`
|
||||
@msg a bristle brush name can have at most {max} characters: `{name}` has {count} il nome di un pennello a setole può avere al massimo {max} caratteri: `{name}` ne ha {count}
|
||||
@msg no brush selected nessun pennello selezionato
|
||||
@msg brushes move only within their type i pennelli si possono spostare solo all'interno del loro tipo
|
||||
@msg no catalog nessun catalogo
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3877,6 +3877,7 @@ swatch Bright Violet ビビッドバイオレット
|
||||
@msg Show All Hidden Attributes: nothing is hidden 非表示の属性をすべて表示: 非表示の属性はありません
|
||||
@msg Expand: no brush strokes selected 拡張: ブラシストロークが選択されていません
|
||||
@msg no brush named `{name}` `{name}` という名前のブラシが見つかりません
|
||||
@msg a bristle brush name can have at most {max} characters: `{name}` has {count} 剛毛ブラシの名前は最大 {max} 文字です: `{name}` は {count} 文字あります
|
||||
@msg no brush selected ブラシが選択されていません
|
||||
@msg brushes move only within their type ブラシは同じ種類の中でのみ移動できます
|
||||
@msg no symbol instances selected シンボルインスタンスが選択されていません
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3802,6 +3802,7 @@ effect Regular ჩვეულებრივი
|
||||
@msg no blend options გადასვლის პარამეტრები არ არის
|
||||
@msg no bounds საზღვრები არ არის
|
||||
@msg no brush named `{name}` ფუნჯი სახელით `{name}` არ არის
|
||||
@msg a bristle brush name can have at most {max} characters: `{name}` has {count} ჯაგრის ფუნჯის სახელი შეიძლება შეიცავდეს არაუმეტეს {max} სიმბოლოს: `{name}` შეიცავს {count} სიმბოლოს
|
||||
@msg no brush selected ფუნჯი მონიშნული არ არის
|
||||
@msg brushes move only within their type ფუნჯების გადატანა მხოლოდ საკუთარი ტიპის ფარგლებში შეიძლება
|
||||
@msg no catalog კატალოგი არ არის
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3809,6 +3809,7 @@ theme Appearance 외관
|
||||
@msg Show All Hidden Attributes: nothing is hidden 모든 숨겨진 속성 표시: 숨겨진 항목이 없습니다
|
||||
@msg Expand: no brush strokes selected 확장: 선택한 브러시 선이 없습니다
|
||||
@msg no brush named `{name}` `{name}` 브러시가 없습니다
|
||||
@msg a bristle brush name can have at most {max} characters: `{name}` has {count} 강모 브러시 이름은 최대 {max}자까지 쓸 수 있습니다: `{name}`은(는) {count}자입니다
|
||||
@msg no brush selected 선택한 브러시가 없습니다
|
||||
@msg brushes move only within their type 브러시는 같은 유형 안에서만 이동할 수 있습니다
|
||||
@msg no symbol instances selected 선택한 심볼 인스턴스가 없습니다
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4145,6 +4145,7 @@ imageTrace Outline Kontur
|
||||
@msg no blend options brak opcji przejścia
|
||||
@msg no bounds brak granic
|
||||
@msg no brush named `{name}` brak pędzla o nazwie `{name}`
|
||||
@msg a bristle brush name can have at most {max} characters: `{name}` has {count} nazwa pędzla włosiankowego może mieć najwyżej {max} znaków (liczba znaków w `{name}`: {count})
|
||||
@msg no brush selected nie zaznaczono pędzla
|
||||
@msg brushes move only within their type pędzle można przesuwać tylko w obrębie ich typu
|
||||
@msg no catalog brak katalogu
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3601,6 +3601,7 @@ axis Both Ambos
|
||||
@msg no blend options sem opções de mesclagem
|
||||
@msg no bounds sem limites
|
||||
@msg no brush named `{name}` nenhum pincel chamado `{name}`
|
||||
@msg a bristle brush name can have at most {max} characters: `{name}` has {count} o nome de um pincel de cerdas pode ter no máximo {max} caracteres: `{name}` tem {count}
|
||||
@msg no brush selected nenhum pincel selecionado
|
||||
@msg brushes move only within their type os pincéis só podem ser movidos dentro do seu tipo
|
||||
@msg no catalog sem catálogo
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -4057,6 +4057,7 @@ axis Both Оба
|
||||
@msg no blend options нет параметров перехода
|
||||
@msg no bounds нет границ
|
||||
@msg no brush named `{name}` нет кисти с именем `{name}`
|
||||
@msg a bristle brush name can have at most {max} characters: `{name}` has {count} наибольшая длина имени щетинной кисти (символов): {max}; в `{name}` символов: {count}
|
||||
@msg no brush selected кисть не выбрана
|
||||
@msg brushes move only within their type кисти можно перемещать только в пределах их типа
|
||||
@msg no catalog нет каталога
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3938,6 +3938,7 @@ axis Both Båda
|
||||
@msg no blend options inga blandningsalternativ
|
||||
@msg no bounds inga gränser
|
||||
@msg no brush named `{name}` ingen pensel med namnet `{name}`
|
||||
@msg a bristle brush name can have at most {max} characters: `{name}` has {count} namnet på en borstpensel får ha högst {max} tecken: `{name}` har {count}
|
||||
@msg no brush selected ingen pensel är markerad
|
||||
@msg brushes move only within their type penslar kan bara flyttas inom sin egen typ
|
||||
@msg no catalog ingen katalog
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -1844,6 +1844,24 @@ fn legacy_ait_param_error_is_translated() {
|
||||
}
|
||||
}
|
||||
|
||||
/// The error of New Brush and Bristle Brush Options for a bristle brush name of more than 31
|
||||
/// characters, whose text the scan above doesn't read, is translated whole in each language of
|
||||
/// [`COMPLETE_MESSAGES`] and in Bulgarian, with the name and both counts kept.
|
||||
#[test]
|
||||
fn bristle_brush_name_limit_error_is_translated() {
|
||||
use serde_json::json;
|
||||
let mut s = vectorcraft_engine::Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let name = "b".repeat(32);
|
||||
let e = s.execute("brush.new", &json!({"type": "bristle", "name": name})).unwrap_err();
|
||||
let vectorcraft_engine::EngineError::BadParams { msg, .. } = &e else { panic!("{e}") };
|
||||
for code in COMPLETE_MESSAGES.iter().chain(&["bg"]) {
|
||||
let shown = message(Lang::from_code(code).unwrap(), &e.to_string());
|
||||
let kept = shown.contains(&format!("`{name}`")) && shown.contains("31") && shown.contains("32");
|
||||
assert!(!shown.contains(msg.as_str()) && kept, "{code}: {shown}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The errors for a parameter key a command doesn't take (#1442), whose text the scan above
|
||||
/// doesn't read, are translated whole in each language of [`COMPLETE_MESSAGES`].
|
||||
#[test]
|
||||
|
||||
@@ -4045,6 +4045,7 @@ axis Both Обидві
|
||||
@msg no blend options немає параметрів переходу
|
||||
@msg no bounds немає кордонів
|
||||
@msg no brush named `{name}` немає пензля з ім’ям `{name}`
|
||||
@msg a bristle brush name can have at most {max} characters: `{name}` has {count} найбільша довжина імені щетинного пензля (символів): {max}; у `{name}` символів: {count}
|
||||
@msg no brush selected пензель не вибрано
|
||||
@msg brushes move only within their type пензлі можна переміщувати лише в межах їхнього типу
|
||||
@msg no catalog немає каталогу
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 726 and column 40.
|
@@ -4305,6 +4305,7 @@ theme Appearance Giao diện
|
||||
@msg no blend options không có tùy chọn hòa trộn
|
||||
@msg no bounds không có giới hạn
|
||||
@msg no brush named `{name}` không có cọ tên `{name}`
|
||||
@msg a bristle brush name can have at most {max} characters: `{name}` has {count} tên cọ lông có tối đa {max} ký tự: `{name}` có {count} ký tự
|
||||
@msg no brush selected chưa chọn cọ
|
||||
@msg brushes move only within their type chỉ có thể di chuyển cọ trong cùng loại của nó
|
||||
@msg no catalog không có danh mục
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 3997 and column 10.
|
@@ -3627,6 +3627,7 @@ imageTrace Outline 轮廓
|
||||
@msg Show All Hidden Attributes: nothing is hidden 显示所有隐藏属性:没有隐藏任何内容
|
||||
@msg Expand: no brush strokes selected 扩展:未选择画笔描边
|
||||
@msg no brush named `{name}` 没有名为 `{name}` 的画笔
|
||||
@msg a bristle brush name can have at most {max} characters: `{name}` has {count} 鬃毛画笔的名称最多 {max} 个字符:`{name}` 有 {count} 个字符
|
||||
@msg no brush selected 未选择画笔
|
||||
@msg brushes move only within their type 画笔只能在同一类型内移动
|
||||
@msg no symbol instances selected 未选择符号实例
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1621,6 +1621,7 @@ file-filter Templates 範本
|
||||
@msg Show All Hidden Attributes: nothing is hidden 顯示所有隱藏屬性:沒有隱藏的項目
|
||||
@msg Expand: no brush strokes selected 展開:未選取筆刷筆畫
|
||||
@msg no brush named `{name}` 沒有名為 `{name}` 的筆刷
|
||||
@msg a bristle brush name can have at most {max} characters: `{name}` has {count} 鬃毛筆刷的名稱最多 {max} 個字元:`{name}` 有 {count} 個字元
|
||||
@msg no brush selected 未選取筆刷
|
||||
@msg brushes move only within their type 筆刷只能在同一類型內移動
|
||||
@msg no symbol instances selected 未選取符號實體
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
+6
-2
@@ -1236,7 +1236,7 @@ already walks Image Trace. Short agent entry points:
|
||||
pattern tiles and graphic styles count, and so do the strokes in the art of a brush that is used. The art of a brush
|
||||
that is not used does not count, so a brush whose own art is the only art that paints with it is listed.
|
||||
- Brushes: `brush.list`, `brush.get {name}`, `brush.apply {name, ids?}`, `brush.new {type, name?, params?, ids?}`,
|
||||
`brush.setCurrent {name}`, `brush.options {name, params?}`, `brush.delete {name} | {names}`,
|
||||
`brush.setCurrent {name}`, `brush.options {name, params?, newName?}`, `brush.delete {name} | {names}`,
|
||||
`brush.duplicate {name, newName?} | {names}`, `brush.freehand`, `object.expandBrush {ids?}`. `brush.new`,
|
||||
`brush.duplicate` and `brush.options` trim the white space around the name they are given. A blank `name` gives
|
||||
the type's default name ("New Calligraphic Brush" for a calligraphic brush), and a blank `newName` gives
|
||||
@@ -1244,7 +1244,11 @@ already walks Image Trace. Short agent entry points:
|
||||
`brush.new` or `brush.options` without a `key` takes the color that the art's fills and strokes use most (on a tie,
|
||||
the one found first), or black when they use no solid color. `brush.options` changes only the fields of
|
||||
`colorization` and of an art brush's `scale` that `params` gives, so `{colorization: {method: "hueShift"}}` keeps a
|
||||
Hue Shift brush's key.
|
||||
Hue Shift brush's key. `brush.new` and `brush.options` take a bristle brush name of at most 31 characters:
|
||||
`brush.new` refuses a longer one, counting the number it adds to a taken name, and `brush.options` refuses a
|
||||
longer `newName`. `brush.duplicate` does not check the length. A bristle brush that already has a longer name
|
||||
keeps it, and `brush.options` without a new name, or with a `newName` that only trims the white space around the
|
||||
name, does not refuse. Other brush types take a name of any length.
|
||||
- Variables: `variable.list`, `variable.define {name, kind: text|visibility|image|graph}`, `variable.bind {variable, ids?}`,
|
||||
`variable.makeTextDynamic`, `variable.makeVisibilityDynamic`, `variable.makeImageDynamic`, `variable.makeGraphDynamic`,
|
||||
`variable.unbind`, `variable.rename`, `variable.delete`, `variable.highlight` (image values are file paths, graph
|
||||
|
||||
@@ -727,6 +727,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
Art Brush Options' Width (1% to 1000%) and Pattern Brush Options' Scale (1% to 10000%) each have a slider beside the field, over the same range in whole percents; screen readers name the slider as its row and read the field's value.
|
||||
Scatter, Art and Pattern Brush Options have the Tips button for Colorization that the [Brushes](https://web.archive.org/web/20241230205225/https://helpx.adobe.com/illustrator/using/brushes.html) page (archived copy) describes, beside Method. It opens Colorization Tips, which shows each method's name and what it does beside a sample of the brush, as OK would make it, painted with that method on a blue stroke; Hue Shift's sample uses the dialog's Key Color. `ui.dialog.set {field: "tips", value: true}` opens it. Screen readers name the button, the popover and each sample.
|
||||
Hue Shift set by `brush.new` or `brush.options` without a key color takes the color the art's fills and strokes use most, as Brush Options already offers it, following the Brushes page: "By default, the key color is the most prominent color in the art" (<https://web.archive.org/web/20241230205225/https://helpx.adobe.com/illustrator/using/brushes.html>). The page does not state how prominence is measured. Counting the fills and strokes that use each solid color, with the first one found winning a tie and black for art with no solid color, is VectorCraft's own rule. A `brush.options` edit that gives only the method keeps the brush's key color, and one that gives only an art brush's start guide keeps its end guide. Evidence: `tests_brushsym::hue_shift_without_a_key_takes_the_most_used_art_color`, `tests_brushsym::art_brush_options_keep_the_scale_fields_an_edit_leaves_out`.
|
||||
Bristle Brush Options and `brush.new` take a bristle brush name of at most 31 characters, as the Brushes page states for the Bristle Brush Options. The Name field stops taking characters at 31, and `brush.new` and `brush.options` refuse a longer new name with an error that names the limit. The page does not say how the limit counts characters or what happens to longer names already saved; counting characters and the number added to a taken name, keeping longer saved names, and leaving `brush.duplicate` unlimited are VectorCraft's own rules. A bristle brush that a document or a saved file already holds under a longer name keeps it, and the other brush types take a name of any length. Test: `bristle_brush_names_have_at_most_31_characters` in `tests_brushsym.rs`. Public reference: [Brushes](https://web.archive.org/web/20241230205225/https://helpx.adobe.com/illustrator/using/brushes.html) (archived copy).
|
||||
In the Brushes panel, Shift-click selects a range of brushes and Ctrl/Cmd-click adds or removes one. Delete Brush and Duplicate Brush then act on all of them (`brush.delete {names}`, `brush.duplicate {names}`).
|
||||
Document Info has a Brushes category after Graphic Styles. It lists the Brushes panel's brushes, or with Selection Only it lists the brushes that the selected objects' strokes use. The Document Info report and Package's report list them too (`document.info {category: "brushes"}`, `brushNames`).
|
||||
The Brushes panel menu's Select All Unused selects the brushes that are not used in the document, as the [Brushes](https://web.archive.org/web/20241230205225/https://helpx.adobe.com/illustrator/using/brushes.html) page (archived copy) describes. A brush counts as used when a stroke paints with it in the art (opacity masks included), in symbols, graph designs, pattern tiles or graphic styles, or in the art of a used brush (`brush.unused`).
|
||||
|
||||
Reference in New Issue
Block a user