Brushes: New Brush asks for the type and opens its Brush Options

Squashed from #3390 onto main.
This commit is contained in:
bflatastic
2026-10-21 23:15:05 +07:00
parent 79c4647665
commit b008e83658
32 changed files with 547 additions and 10 deletions
+15
View File
@@ -31,6 +31,15 @@ pub fn specs() -> Vec<CommandSpec> {
// ----- brushes -----
cmd!(query "brush.list", "Brushes", [], None, "{} → {brushes: [{name, type}], current}", has_doc, brush_list),
cmd!(query "brush.get", "Brush Definition", [], None, "{name} → the brush definition (JSON)", has_doc, brush_get),
cmd!(
query "brush.selectionArt",
"Brush Art",
[],
None,
"{ids?|id?} → {art: the object brush.new takes as a new scatter or art brush's art and as a new pattern brush's side tile (several objects become one group), or null when nothing is selected or ids name no object}",
has_doc,
brush_selection_art
),
cmd!(
"brush.apply",
"Apply Brush",
@@ -287,6 +296,12 @@ fn brush_get(s: &mut Session, p: &Value) -> Result<Value> {
Ok(serde_json::to_value(b).unwrap_or_default())
}
/// The art `brush.new` takes from `ids` or `id`, else from the selection ([`selection_art`]).
fn brush_selection_art(s: &mut Session, p: &Value) -> Result<Value> {
let art = selection_art(s, p)?;
Ok(json!({ "art": art.map(|n| serde_json::to_value(&n).unwrap_or_default()) }))
}
fn brush_apply(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "brush.apply";
let name = name_param(p, C)?.to_string();
+23
View File
@@ -230,6 +230,29 @@ fn brush_rename_reaches_the_tile_that_pattern_cancel_puts_back() {
assert_eq!(brushes, ["Chalk"]);
}
/// `brush.selectionArt` gives the art `brush.new` takes: the selected object, a group of the selected
/// objects, the object of `ids`, or null when nothing is selected. It changes nothing.
#[test]
fn selection_art_is_the_art_new_brush_takes() {
let mut s = session();
let art = |s: &mut Session, p: Value| s.execute("brush.selectionArt", &p).unwrap()["art"].clone();
s.select(|_, sel| sel.clear()).unwrap();
assert_eq!(art(&mut s, json!({})), Value::Null);
let a = rect(&mut s, 0.0, 0.0, 40.0, 8.0);
let b = rect(&mut s, 0.0, 20.0, 40.0, 8.0);
let steps = s.doc().unwrap().history.undo.len();
select(&mut s, &[a, b]);
let group: Node = serde_json::from_value(art(&mut s, json!({}))).unwrap();
assert!(matches!(&group.kind, NodeKind::Group { children, .. } if children.len() == 2), "{group:?}");
let one: Node = serde_json::from_value(art(&mut s, json!({"ids": [a.0]}))).unwrap();
assert_eq!(one.geometric_bounds(), Some(bounds(&s, a)));
// The art brush `brush.new` makes from the same selection holds the same art.
s.execute("brush.new", &json!({"type": "art", "name": "Two"})).unwrap();
let made: Node = serde_json::from_value(s.execute("brush.get", &json!({"name": "Two"})).unwrap()["art"].clone()).unwrap();
assert_eq!(made.geometric_bounds(), group.geometric_bounds());
assert_eq!(s.doc().unwrap().history.undo.len(), steps + 1, "only brush.new is an undo step");
}
#[test]
fn brush_options_rename_duplicate_and_delete_update_strokes() {
let mut s = session();
+76 -5
View File
@@ -11,6 +11,10 @@
//! `angleMode`, `roundnessMode`, `sizeMode` (`fixed` | `random` | `pressure`) and
//! `angleVariation`, `roundnessVariation`, `sizeVariation`. Bristle: `shape` (`roundPoint`…),
//! `size` (pt), `length` (25..300 %), `density`, `thickness`, `opacity`, `stiffness` (1..100 %).
//!
//! New Brush opens the dialog for a brush that does not exist yet ([`open_new`]), with `__create`
//! and, for dropped art, `__ids`: OK runs `brush.new` instead, which gives a name another brush has
//! a number, and Cancel adds nothing.
use egui::{Color32, Sense, Stroke, pos2, vec2};
use serde_json::{Map, Value, json};
@@ -25,7 +29,13 @@ use crate::{VectorcraftApp, widgets};
/// The dialog kind of Brush Options.
pub const KIND: &str = "brushOptions";
pub(super) const SPEC: DialogSpec = DialogSpec { heading, body, confirm, min_width: 440.0, ..DialogSpec::FORM };
pub(super) const SPEC: DialogSpec = DialogSpec { heading, body, confirm: ok, min_width: 440.0, ..DialogSpec::FORM };
/// The field of a Brush Options dialog that New Brush opened.
const CREATE: &str = "__create";
/// The field of such a dialog holding the ids of the art dropped on the Brushes panel.
const IDS: &str = "__ids";
/// Calligraphic values: (field, label, suffix, decimals), in [`Calligraphic::RANGES`] order.
const CALLI: [(&str, &str, &str, usize); 3] = [("angle", "Angle:", "°", 0), ("roundness", "Roundness:", "%", 0), ("size", "Size:", " pt", 1)];
@@ -84,8 +94,15 @@ pub fn open(app: &mut VectorcraftApp, name: Option<&str>) -> Result<Value, Strin
};
let def = app.run("brush.get", json!({ "name": name }))?;
let b: Brush = serde_json::from_value(def.clone()).map_err(|e| e.to_string())?;
let mut f = Map::new();
let mut f = fields(&b, def)?;
f.insert("__brush".into(), json!(b.name));
app.ui.dialog = Some(Dialog { kind: KIND.into(), fields: f });
Ok(Value::Null)
}
/// The fields of brush `b`, whose definition is `def`.
fn fields(b: &Brush, def: Value) -> Result<Map<String, Value>, String> {
let mut f = Map::new();
f.insert("name".into(), json!(b.name));
f.insert("__type".into(), json!(b.kind.type_id()));
match &b.kind {
@@ -105,10 +122,9 @@ pub fn open(app: &mut VectorcraftApp, name: Option<&str>) -> Result<Value, Strin
}
}
// The definition goes along, for the preview (and the art it carries).
BrushKind::Scatter(_) | BrushKind::Art(_) | BrushKind::Pattern(_) => brush_art_options::fields(&b, def, &mut f),
BrushKind::Scatter(_) | BrushKind::Art(_) | BrushKind::Pattern(_) => brush_art_options::fields(b, def, &mut f),
}
app.ui.dialog = Some(Dialog { kind: KIND.into(), fields: f });
Ok(Value::Null)
Ok(f)
}
/// Can Brush Options open without a name (the selected path's brush, else the current one, exists)?
@@ -124,6 +140,61 @@ fn target(app: &VectorcraftApp) -> Option<String> {
selected.or_else(|| vectorcraft_brush::current(&st.doc))
}
/// Open the Brush Options of a new brush of type `ty` (`calligraphic` | `scatter` | `art` |
/// `bristle` | `pattern`), the second step of New Brush. The fields start from the type's default
/// values and the name "New <Type> Brush", numbered when another brush has it. A scatter or art
/// brush takes the art `brush.selectionArt` gives for `ids` (else for the selection) as its art,
/// and a pattern brush takes it as its side tile; the preview draws it. Without that art nothing
/// opens.
pub(super) fn open_new(app: &mut VectorcraftApp, ty: &str, ids: Option<Value>) -> Result<Value, String> {
let st = app.session.active().ok_or("no document open")?;
let title: String = ty.chars().take(1).flat_map(char::to_uppercase).chain(ty.chars().skip(1)).collect();
let name = vectorcraft_brush::unique_name(&vectorcraft_brush::library(&st.doc), &format!("New {title} Brush"));
let mut def = json!({ "name": name, "type": ty });
let art_key = match ty {
"scatter" | "art" => Some("art"),
"pattern" => Some("side"),
_ => None,
};
if let Some(key) = art_key {
let mut r = app.run("brush.selectionArt", ids.clone().map_or_else(|| json!({}), |ids| json!({ "ids": ids })))?;
let art = r["art"].take();
if art.is_null() {
return Err("nothing selected".into());
}
def[key] = art;
}
let mut b: Brush = serde_json::from_value(def).map_err(|e| e.to_string())?;
b.sanitize();
let def = serde_json::to_value(&b).map_err(|e| e.to_string())?;
let mut f = fields(&b, def)?;
f.insert(CREATE.into(), json!(true));
if let Some(ids) = ids {
f.insert(IDS.into(), ids);
}
app.ui.dialog = Some(Dialog { kind: KIND.into(), fields: f });
Ok(Value::Null)
}
/// OK of a new brush's Brush Options: `brush.new` with the type, the name, the values set and the
/// dropped art's ids. The dialog closes once the brush is made.
fn create(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
let mut p = json!({ "type": d.str("__type"), "name": d.str("name").trim(), "params": params(d) });
if let Some(ids) = d.fields.get(IDS) {
p["ids"] = ids.clone();
}
let r = app.run("brush.new", p);
if r.is_ok() {
app.ui.dialog = None;
}
r
}
/// OK: make the new brush when New Brush opened the dialog, else edit the brush.
fn ok(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
if d.bool(CREATE) { create(app, d) } else { confirm(app, d) }
}
/// The value of field `key` within `range`.
pub(super) fn clamped(d: &Dialog, key: &str, (lo, hi): (f64, f64)) -> f64 {
d.f64(key, lo).clamp(lo, hi)
+2
View File
@@ -55,6 +55,7 @@ pub mod live_paint_selection;
pub mod missing_fonts;
pub mod missing_links;
pub(crate) mod modal;
pub mod new_brush;
pub mod new_color_group;
mod new_document;
pub mod new_swatch;
@@ -343,6 +344,7 @@ registry! {
LivePaintGaps: [live_paint_gaps::KIND] => live_paint_gaps::SPEC,
LivePaintSelectionOptions: [live_paint_selection::KIND] => live_paint_selection::SPEC,
BrushOptions: [brush_options::KIND] => brush_options::SPEC,
NewBrush: [new_brush::KIND] => new_brush::SPEC,
EditSelection: [edit_selection::KIND] => edit_selection::SPEC,
PerspectivePlane: [perspective_plane::KIND] => perspective_plane::SPEC,
LayerOptions: [layer_options::KIND] => layer_options::SPEC,
+188
View File
@@ -0,0 +1,188 @@
//! New Brush (the Brushes panel's New Brush button, its menu's New Brush…, and art dropped on the
//! panel): the type of brush to make. Scatter, Art and Pattern are off when no art is selected or
//! dropped. OK opens that type's Brush Options for the new brush
//! ([`super::brush_options::open_new`]), whose OK makes it; Cancel makes nothing.
//!
//! Fields: `type` (`calligraphic` | `scatter` | `art` | `bristle` | `pattern`), `__art` (art is
//! selected or was dropped) and `__ids` (the ids of the dropped art; without it, the selection's).
use serde_json::{Value, json};
use super::{DialogSpec, brush_options};
use crate::state::Dialog;
use crate::{VectorcraftApp, widgets};
/// The dialog kind of New Brush.
pub const KIND: &str = "newBrush";
pub(super) const SPEC: DialogSpec =
DialogSpec { heading: |_| tl!("New Brush").into(), body, confirm, ok_enabled: Some(ok_enabled), min_width: 300.0, ..DialogSpec::FORM };
/// The brush types, in the Brushes panel's order.
const TYPES: [&str; 5] = ["calligraphic", "scatter", "art", "bristle", "pattern"];
/// Open New Brush for the art of `ids` (art dropped on the Brushes panel), else for the selection.
/// It starts on Art when there is art, and it starts on Calligraphic otherwise.
pub fn open(app: &mut VectorcraftApp, ids: Option<Value>) -> Result<Value, String> {
let st = app.session.active().ok_or("no document open")?;
let art = match &ids {
Some(ids) => ids.as_array().is_some_and(|a| !a.is_empty()),
None => !st.selection.is_empty(),
};
let mut f = json!({ "type": if art { "art" } else { "calligraphic" }, "__art": art });
if let Some(ids) = ids {
f["__ids"] = ids;
}
app.ui.dialog = Some(Dialog::new(KIND, f));
Ok(Value::Null)
}
/// Does a brush of type `ty` need art?
fn needs_art(ty: &str) -> bool {
matches!(ty, "scatter" | "art" | "pattern")
}
/// The type the fields choose: `type`, or Calligraphic when it names none.
fn chosen(d: &Dialog) -> &'static str {
let ty = d.str("type");
TYPES.into_iter().find(|t| *t == ty).unwrap_or("calligraphic")
}
fn ok_enabled(_: &VectorcraftApp, d: &Dialog) -> bool {
d.bool("__art") || !needs_art(chosen(d))
}
fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
let labels = [tl!("Calligraphic Brush"), tl!("Scatter Brush"), tl!("Art Brush"), tl!("Bristle Brush"), tl!("Pattern Brush")];
let (art, cur) = (d.bool("__art"), chosen(d));
for (ty, label) in TYPES.into_iter().zip(labels) {
if widgets::radio(ui, label, cur == ty, art || !needs_art(ty)) {
d.fields.insert("type".into(), json!(ty));
}
}
if !art {
ui.add_space(6.0);
widgets::dim_label(ui, tl!("Select art to make a scatter, art or pattern brush."));
}
false
}
/// OK: the chosen type's Brush Options for the new brush. A type that needs art, without art,
/// keeps the dialog open.
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
let ty = chosen(d);
if needs_art(ty) && !d.bool("__art") {
return Err("nothing selected".into());
}
brush_options::open_new(app, ty, d.fields.get("__ids").cloned())
}
#[cfg(test)]
mod tests {
use vectorcraft_engine::Session;
use super::*;
use crate::widgets::accessibility_tests::{named, tree};
fn app() -> VectorcraftApp {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
app
}
fn set(app: &mut VectorcraftApp, fields: Value) {
let d = app.ui.dialog.as_mut().unwrap();
for (k, v) in fields.as_object().unwrap() {
d.fields.insert(k.clone(), v.clone());
}
}
fn library(app: &mut VectorcraftApp) -> Vec<(String, String)> {
crate::panels::brushes::brushes(app).0
}
fn undo_steps(app: &VectorcraftApp) -> usize {
app.session.active().unwrap().history.undo.len()
}
/// Which of the five types screen readers find on, in the dialog's order.
fn types_on(app: &mut VectorcraftApp) -> Vec<bool> {
let t = tree(|ui| super::super::show(app, ui.ctx()));
["Calligraphic Brush", "Scatter Brush", "Art Brush", "Bristle Brush", "Pattern Brush"].map(|l| !named(&t, l).is_disabled()).to_vec()
}
/// With nothing selected, Scatter, Art and Pattern are off, New Brush starts on Calligraphic,
/// and OK on one of them keeps the dialog open. With art selected, every type is on and New
/// Brush starts on Art.
#[test]
fn scatter_art_and_pattern_brushes_need_art() {
let mut app = app();
app.run("ui.newBrush", json!({})).unwrap();
assert_eq!(types_on(&mut app), [true, false, false, true, false]);
assert_eq!(app.ui.dialog.as_ref().unwrap().str("type"), "calligraphic");
set(&mut app, json!({"type": "art"}));
assert_eq!(super::super::confirm(&mut app), Err("nothing selected".into()));
assert_eq!(app.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some(KIND));
app.ui.dialog = None;
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 20})).unwrap();
app.run("ui.newBrush", json!({})).unwrap();
assert_eq!(types_on(&mut app), [true; 5]);
assert_eq!(app.ui.dialog.as_ref().unwrap().str("type"), "art");
}
/// OK opens the chosen type's Brush Options for a new brush, named New <Type> Brush; their OK
/// makes the brush with the name and values set, from the selected art, in one undo step.
#[test]
fn ok_opens_the_types_brush_options_and_their_ok_makes_the_brush() {
let mut app = app();
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 20})).unwrap();
let (before, steps) = (library(&mut app), undo_steps(&app));
app.run("ui.newBrush", json!({})).unwrap();
set(&mut app, json!({"type": "scatter"}));
super::super::confirm(&mut app).unwrap();
let d = app.ui.dialog.as_ref().expect("Scatter Brush Options");
assert_eq!((d.kind.as_str(), d.str("__type"), d.str("name")), (super::brush_options::KIND, "scatter".into(), "New Scatter Brush".into()));
assert!(d.fields["__def"]["art"].is_object(), "the selected art, for the preview");
let text = crate::tests_labels::painted_text(&mut app, |app, ui| super::super::show(app, ui.ctx()));
assert!(text.contains("Scatter Brush Options") && text.contains("Spacing:"), "{text}");
assert_eq!(library(&mut app), before, "no brush until OK");
// The name is trimmed, and a name another brush has gets a number, as `brush.new` gives it.
set(&mut app, json!({"name": " Dots ", "sizeMin": 40, "spacingMin": 150}));
super::super::confirm(&mut app).unwrap();
assert!(app.ui.dialog.is_none());
assert_eq!(library(&mut app)[before.len()..], [("Dots 2".to_string(), "scatter".to_string())]);
let def = app.run("brush.get", json!({"name": "Dots 2"})).unwrap();
assert_eq!((&def["size"][0], &def["spacing"][0]), (&json!(40.0), &json!(150.0)));
assert!(def["art"].is_object(), "{def}");
assert_eq!(undo_steps(&app), steps + 1);
// A Calligraphic brush, with no art.
app.run("ui.newBrush", json!({})).unwrap();
set(&mut app, json!({"type": "calligraphic"}));
super::super::confirm(&mut app).unwrap();
assert_eq!(app.ui.dialog.as_ref().unwrap().str("name"), "New Calligraphic Brush");
set(&mut app, json!({"size": 9}));
super::super::confirm(&mut app).unwrap();
let def = app.run("brush.get", json!({"name": "New Calligraphic Brush"})).unwrap();
assert_eq!((&def["type"], &def["size"]), (&json!("calligraphic"), &json!(9.0)));
}
/// Cancel in New Brush, or in the Brush Options it opens, adds no brush and no undo step.
#[test]
fn cancel_adds_no_brush() {
let mut app = app();
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 20})).unwrap();
let before = (library(&mut app), undo_steps(&app));
app.run("ui.newBrush", json!({})).unwrap();
super::super::cancel(&mut app);
assert!(app.ui.dialog.is_none());
for ty in TYPES {
app.run("ui.newBrush", json!({})).unwrap();
set(&mut app, json!({"type": ty}));
super::super::confirm(&mut app).unwrap();
assert_eq!(app.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some(super::brush_options::KIND), "{ty}");
super::super::cancel(&mut app);
assert!(app.ui.dialog.is_none(), "{ty}");
assert_eq!((library(&mut app), undo_steps(&app)), before, "{ty}");
}
}
}
+8
View File
@@ -1902,6 +1902,14 @@
Round Fan Кръгъл ветрилообразен
Flat Fan Плосък ветрилообразен
New Brush… Нова четка…
New Brush Нова четка
Calligraphic Brush Калиграфска четка
Scatter Brush Разпръсната четка
Art Brush Артистична четка
Bristle Brush Четка с косъм
Pattern Brush Четка с мостра
Select art to make a scatter, art or pattern brush. Изберете графика, за да създадете разпръсната четка, артистична четка или четка с мостра.
Brush Art Графика на четка
New Calligraphic Brush Нова калиграфска четка
New Character Style Нов стил на знак
New Color Group Нова цветова група
Can't render this file because it contains an unexpected character in line 2 and column 73.
+8
View File
@@ -2982,6 +2982,14 @@ theme Appearance Motiv
Edit Symbol Upravit symbol
Expand Brush Strokes Rozložit tahy štětcem
New Brush… Nový štětec…
New Brush Nový štětec
Calligraphic Brush Kaligrafický štětec
Scatter Brush Rozptylový štětec
Art Brush Obrazový štětec
Bristle Brush Štětinový štětec
Pattern Brush Vzorkový štětec
Select art to make a scatter, art or pattern brush. Vyberte kresbu, abyste mohli vytvořit rozptylový, obrazový nebo vzorkový štětec.
Brush Art Kresba štětce
New Symbol Nový symbol
New Symbol… Nový symbol…
Place Symbol Instance Umístit instanci symbolu
Can't render this file because it contains an unexpected character in line 2 and column 73.
+8
View File
@@ -2749,6 +2749,14 @@ theme Appearance Udseende
Edit Symbol Redigér symbol
Expand Brush Strokes Udvid penselstrøg
New Brush… Ny pensel…
New Brush Ny pensel
Calligraphic Brush Kalligrafipensel
Scatter Brush Spredningspensel
Art Brush Grafikpensel
Bristle Brush Børstepensel
Pattern Brush Mønsterpensel
Select art to make a scatter, art or pattern brush. Markér grafik for at oprette en spredningspensel, grafikpensel eller mønsterpensel.
Brush Art Penselgrafik
New Symbol Nyt symbol
New Symbol… Nyt symbol…
Place Symbol Instance Placér symbolforekomst
Can't render this file because it contains an unexpected character in line 2 and column 73.
+8
View File
@@ -1928,6 +1928,14 @@ file-filter Templates Vorlagen
Round Fan Rund, Fächer
Flat Fan Flach, Fächer
New Brush… Neuer Pinsel…
New Brush Neuer Pinsel
Calligraphic Brush Kalligrafiepinsel
Scatter Brush Spezialpinsel
Art Brush Bildpinsel
Bristle Brush Borstenpinsel
Pattern Brush Musterpinsel
Select art to make a scatter, art or pattern brush. Wählen Sie eine Grafik aus, um einen Spezial-, Bild- oder Musterpinsel zu erstellen.
Brush Art Pinselgrafik
New Calligraphic Brush Neuer Kalligrafiepinsel
New Character Style Neues Zeichenformat
New Color Group Neue Farbgruppe
Can't render this file because it contains an unexpected character in line 2 and column 73.
+8
View File
@@ -1624,6 +1624,14 @@
Pressure Πίεση
Shape: Σχήμα:
New Brush… Νέο πινέλο…
New Brush Νέο πινέλο
Calligraphic Brush Καλλιγραφικό πινέλο
Scatter Brush Πινέλο διασποράς
Art Brush Πινέλο σχεδίου
Bristle Brush Πινέλο με τρίχες
Pattern Brush Πινέλο μοτίβου
Select art to make a scatter, art or pattern brush. Επιλέξτε σχέδιο για να δημιουργήσετε πινέλο διασποράς, σχεδίου ή μοτίβου.
Brush Art Σχέδιο πινέλου
New Layer… Νέο επίπεδο…
New Swatch… Νέο δείγμα χρώματος…
New Window Νέο παράθυρο
1 # Ελληνικά (el) — VectorCraft
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
+8
View File
@@ -1952,6 +1952,14 @@
Round Fan Abanico redondo
Flat Fan Abanico plano
New Brush… Nuevo pincel…
New Brush Nuevo pincel
Calligraphic Brush Pincel caligráfico
Scatter Brush Pincel de dispersión
Art Brush Pincel de arte
Bristle Brush Pincel de cerdas
Pattern Brush Pincel de motivo
Select art to make a scatter, art or pattern brush. Selecciona una ilustración para crear un pincel de dispersión, de arte o de motivo.
Brush Art Ilustración del pincel
New Calligraphic Brush Nuevo pincel caligráfico
New Character Style Nuevo estilo de carácter
New Color Group Nuevo grupo de colores
Can't render this file because it contains an unexpected character in line 2 and column 73.
+8
View File
@@ -2925,6 +2925,14 @@ theme Appearance Ulkoasu
Edit Symbol Muokkaa symbolia
Expand Brush Strokes Laajenna sivellinviivat
New Brush… Uusi sivellin…
New Brush Uusi sivellin
Calligraphic Brush Kalligrafinen sivellin
Scatter Brush Sirotesivellin
Art Brush Taidesivellin
Bristle Brush Harjassivellin
Pattern Brush Kuviosivellin
Select art to make a scatter, art or pattern brush. Valitse kuvitus, jotta voit luoda sirote-, taide- tai kuviosiveltimen.
Brush Art Siveltimen kuvitus
New Symbol Uusi symboli
New Symbol… Uusi symboli…
Place Symbol Instance Sijoita symbolin ilmentymä
Can't render this file because it contains an unexpected character in line 2 and column 73.
+8
View File
@@ -1949,6 +1949,14 @@
Round Fan Éventail rond
Flat Fan Éventail plat
New Brush… Nouvelle forme…
New Brush Nouvelle forme
Calligraphic Brush Forme calligraphique
Scatter Brush Forme diffuse
Art Brush Forme artistique
Bristle Brush Forme Soies
Pattern Brush Forme de motif
Select art to make a scatter, art or pattern brush. Sélectionnez une illustration pour créer une forme diffuse, artistique ou de motif.
Brush Art Illustration de la forme
New Calligraphic Brush Nouvelle forme calligraphique
New Character Style Nouveau style de caractère
New Color Group Nouveau groupe de couleurs
Can't render this file because it contains an unexpected character in line 2 and column 73.
+8
View File
@@ -323,6 +323,7 @@
Art Grafikus
Art & Illustration Grafika és illusztráció
Art and Bright Colors Grafika és élénk színek
Art Brush Grafikus ecset
Art Brush Options Grafikus ecset beállításai
Art Optimized Grafikára optimalizált
Art using a deleted global swatch keeps its colour. A törölt globális színmintát használó grafika megtartja a színét.
@@ -457,6 +458,7 @@
Bring Forward Eggyel előre
Bring to Front Előre hozás
Bristle Sörte
Bristle Brush Sörteecset
Bristle Brush Options Sörteecset beállításai
Bristle Brush Strokes Sörteecset-vonások
Bristle Density: Sörtesűrűség:
@@ -465,6 +467,7 @@
Brown Barna
Browser Böngésző
Brush Affects: Az ecset ezekre hat:
Brush Art Ecsetgrafika
Brush Definition Ecsetdefiníció
Brush Libraries Ecsetkönyvtárak
Brush Libraries (on the roadmap) Ecsetkönyvtárak (tervezett funkció)
@@ -485,6 +488,7 @@
Butt Cap Lapos vonalvég
Byte Order: Bájtsorrend:
Calligraphic Kalligrafikus
Calligraphic Brush Kalligrafikus ecset
Calligraphic Brush Options Kalligrafikus ecset beállításai
Can't open “{name}”. Nem nyitható meg: „{name}”.
Can't save “{name}”. Nem menthető: „{name}”.
@@ -2394,6 +2398,7 @@ file-filter Templates Sablonok
New Art Has Basic Appearance Új grafika alapmegjelenéssel
New Artboard Új rajztábla
New Bristle Brush Új sörteecset
New Brush Új ecset
New Brush… Új ecset…
New Calligraphic Brush Új kalligrafikus ecset
New Character Style Új karakterstílus
@@ -2724,6 +2729,7 @@ file-filter Templates Sablonok
Path Length Útvonalak hossza
Paths: {paths} Anchors: {anchors} Colors: {colors} Útvonalak: {paths} Horgonypontok: {anchors} Színek: {colors}
Pattern Mintázat
Pattern Brush Mintázatecset
Pattern Brush Options Mintázatecset beállításai
Pattern Editing Mintázatszerkesztés
Pattern Options Mintázat beállításai
@@ -3367,6 +3373,7 @@ mark-style Japanese Japán
Scallop Tool Csipkézés
Scans: Letapogatások:
Scatter Szóró
Scatter Brush Szóróecset
Scatter Brush Options Szóróecset beállításai
Scatter Graph Tool Pontdiagram
Scatter: Szóródás:
@@ -3423,6 +3430,7 @@ mark-style Japanese Japán
Select art and choose Object › Pattern › Make. Jelölj ki egy grafikát, és válaszd az Objektum › Mintázat › Létrehozás parancsot.
Select art and click New Symbol to make one. Jelölj ki egy grafikát, és kattints az Új szimbólum gombra.
Select art and use New Brush to make one. Jelölj ki egy grafikát, és hozz létre belőle ecsetet az Új ecset paranccsal.
Select art to make a scatter, art or pattern brush. Jelölj ki egy grafikát szóró-, grafikus vagy mintázatecset létrehozásához.
Select Bound Object Kötött objektum kijelölése
Select Data Set Adatkészlet kiválasztása
Select Freeform Point Szabad formájú pont kijelölése
Can't render this file because it contains an unexpected character in line 2 and column 73.
+8
View File
@@ -2749,6 +2749,14 @@ theme Appearance Tampilan
Edit Symbol Edit simbol
Expand Brush Strokes Perluas guratan kuas
New Brush… Kuas baru…
New Brush Kuas baru
Calligraphic Brush Kuas kaligrafi
Scatter Brush Kuas sebar
Art Brush Kuas seni
Bristle Brush Kuas bristle
Pattern Brush Kuas pola
Select art to make a scatter, art or pattern brush. Pilih karya seni untuk membuat kuas sebar, seni, atau pola.
Brush Art Karya seni kuas
New Symbol Simbol baru
New Symbol… Simbol baru…
Place Symbol Instance Tempatkan instansi simbol
1 # Indonesian (id) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
+8
View File
@@ -1979,6 +1979,14 @@ file-filter Templates Modelli
Round Fan Ventaglio tondo
Flat Fan Ventaglio piatto
New Brush… Nuovo pennello…
New Brush Nuovo pennello
Calligraphic Brush Pennello calligrafico
Scatter Brush Pennello a dispersione
Art Brush Pennello artistico
Bristle Brush Pennello a setole
Pattern Brush Pennello motivo
Select art to make a scatter, art or pattern brush. Seleziona la grafica per creare un pennello a dispersione, artistico o motivo.
Brush Art Grafica del pennello
New Calligraphic Brush Nuovo pennello calligrafico
New Character Style Nuovo stile carattere
New Color Group Nuovo gruppo colori
Can't render this file because it contains an unexpected character in line 2 and column 73.
+8
View File
@@ -2094,6 +2094,14 @@
Round Fan 丸・扇形
Flat Fan 平・扇形
New Brush… 新規ブラシ…
New Brush 新規ブラシ
Calligraphic Brush 絵筆ブラシ
Scatter Brush 散布ブラシ
Art Brush アートブラシ
Bristle Brush 剛毛ブラシ
Pattern Brush パターンブラシ
Select art to make a scatter, art or pattern brush. 散布ブラシ、アートブラシ、パターンブラシを作成するには、アートを選択します。
Brush Art ブラシのアート
New Calligraphic Brush 新規絵筆ブラシ
New Character Style 新規文字スタイル
New Color Group 新規カラーグループ
Can't render this file because it contains an unexpected character in line 2 and column 73.
+8
View File
@@ -2561,6 +2561,14 @@ theme Appearance იერსახე
Edit Symbol სიმბოლოს რედაქტირება
Expand Brush Strokes ფუნჯის შტრიხების გაშლა
New Brush… ახალი ფუნჯი…
New Brush ახალი ფუნჯი
Calligraphic Brush კალიგრაფიული ფუნჯი
Scatter Brush გაფანტვის ფუნჯი
Art Brush მხატვრული ფუნჯი
Bristle Brush ჯაგრის ფუნჯი
Pattern Brush პატერნის ფუნჯი
Select art to make a scatter, art or pattern brush. მონიშნეთ ნახატი გაფანტვის, მხატვრული ან პატერნის ფუნჯის შესაქმნელად.
Brush Art ფუნჯის ნახატი
New Symbol ახალი სიმბოლო
New Symbol… ახალი სიმბოლო…
Place Symbol Instance სიმბოლოს ეგზემპლარის განთავსება
Can't render this file because it contains an unexpected character in line 2 and column 73.
+8
View File
@@ -2089,6 +2089,14 @@
Round Fan 둥근 부채형
Flat Fan 평평한 부채형
New Brush… 새 브러시…
New Brush 새 브러시
Calligraphic Brush 캘리그래피 브러시
Scatter Brush 흩뿌리기 브러시
Art Brush 아트 브러시
Bristle Brush 강모 브러시
Pattern Brush 패턴 브러시
Select art to make a scatter, art or pattern brush. 흩뿌리기, 아트 또는 패턴 브러시를 만들려면 아트웍을 선택하세요.
Brush Art 브러시 아트
New Calligraphic Brush 새 캘리그래피 브러시
New Character Style 새 문자 스타일
New Color Group 새 색상 그룹
Can't render this file because it contains an unexpected character in line 2 and column 73.
+8
View File
@@ -2763,6 +2763,14 @@ theme Appearance Vormgeving
Edit Symbol Symbool bewerken
Expand Brush Strokes Kwaststreken omzetten
New Brush… Nieuwe kwast…
New Brush Nieuwe kwast
Calligraphic Brush Kalligrafisch penseel
Scatter Brush Verspreidingspenseel
Art Brush Artpenseel
Bristle Brush Borstelpenseel
Pattern Brush Patroonpenseel
Select art to make a scatter, art or pattern brush. Selecteer illustratie om een verspreidings-, art- of patroonpenseel te maken.
Brush Art Kwastillustratie
New Symbol Nieuw symbool
New Symbol… Nieuw symbool…
Place Symbol Instance Symboolexemplaar plaatsen
Can't render this file because it contains an unexpected character in line 2 and column 73.
+8
View File
@@ -2860,6 +2860,14 @@ theme Appearance Wygląd
Edit Symbol Edytuj symbol
Expand Brush Strokes Rozszerz pociągnięcia pędzla
New Brush… Nowy pędzel…
New Brush Nowy pędzel
Calligraphic Brush Pędzel kaligraficzny
Scatter Brush Pędzel rozproszony
Art Brush Pędzel artystyczny
Bristle Brush Pędzel włosiankowy
Pattern Brush Pędzel wzorka
Select art to make a scatter, art or pattern brush. Zaznacz kompozycję, aby utworzyć pędzel rozproszony, artystyczny lub wzorka.
Brush Art Kompozycja pędzla
New Symbol Nowy symbol
New Symbol… Nowy symbol…
Place Symbol Instance Umieść wystąpienie symbolu
Can't render this file because it contains an unexpected character in line 2 and column 73.
+8
View File
@@ -1592,6 +1592,14 @@
New Artboard Nova Mesa de Trabalho
New Bristle Brush Novo Pincel de Cerdas
New Brush… Novo Pincel…
New Brush Novo Pincel
Calligraphic Brush Pincel Caligráfico
Scatter Brush Pincel de Dispersão
Art Brush Pincel de Arte
Bristle Brush Pincel de Cerdas
Pattern Brush Pincel de Padrão
Select art to make a scatter, art or pattern brush. Selecione arte para criar um pincel de dispersão, de arte ou de padrão.
Brush Art Arte do Pincel
New Calligraphic Brush Novo Pincel Caligráfico
New Character Style Novo Estilo de Caractere
New Color Group Novo Grupo de Cores
Can't render this file because it contains an unexpected character in line 6 and column 47.
+8
View File
@@ -1975,6 +1975,14 @@ file-filter Templates Шаблоны
Round Fan Круглая веерная
Flat Fan Плоская веерная
New Brush… Новая кисть…
New Brush Новая кисть
Calligraphic Brush Каллиграфическая кисть
Scatter Brush Кисть-распыление
Art Brush Художественная кисть
Bristle Brush Щетинная кисть
Pattern Brush Узорная кисть
Select art to make a scatter, art or pattern brush. Выделите рисунок, чтобы создать кисть-распыление, художественную или узорную кисть.
Brush Art Рисунок кисти
New Calligraphic Brush Новая каллиграфическая кисть
New Character Style Новый стиль символа
New Color Group Новая цветовая группа
Can't render this file because it contains an unexpected character in line 2 and column 73.
+8
View File
@@ -1902,6 +1902,14 @@
Round Fan Rund solfjäder
Flat Fan Platt solfjäder
New Brush… Ny pensel…
New Brush Ny pensel
Calligraphic Brush Kalligrafisk pensel
Scatter Brush Spridningspensel
Art Brush Konstpensel
Bristle Brush Borstpensel
Pattern Brush Mönsterpensel
Select art to make a scatter, art or pattern brush. Markera grafik för att skapa en spridnings-, konst- eller mönsterpensel.
Brush Art Penselgrafik
New Calligraphic Brush Ny kalligrafisk pensel
New Character Style Ny teckenstil
New Color Group Ny färggrupp
Can't render this file because it contains an unexpected character in line 5 and column 25.
+8
View File
@@ -1971,6 +1971,14 @@ file-filter Templates Шаблони
Round Fan Кругла віялова
Flat Fan Пласка віялова
New Brush… Новий пензель…
New Brush Новий пензель
Calligraphic Brush Каліграфічний пензель
Scatter Brush Пензель-розпорошення
Art Brush Художній пензель
Bristle Brush Щетинний пензель
Pattern Brush Візерунковий пензель
Select art to make a scatter, art or pattern brush. Виділіть малюнок, щоб створити пензель-розпорошення, художній або візерунковий пензель.
Brush Art Малюнок пензля
New Calligraphic Brush Новий каліграфічний пензель
New Character Style Новий стиль символу
New Color Group Нова колірна група
Can't render this file because it contains an unexpected character in line 726 and column 40.
+8
View File
@@ -253,6 +253,7 @@
Arrange: Bring to Front Sắp xếp: Đưa lên trên cùng
Arrowheads: Đầu mũi tên:
Art Tác phẩm
Art Brush Cọ bản vẽ
Art Brush Options Tùy chọn cọ bản vẽ
Art using a deleted global swatch keeps its colour. Tác phẩm dùng mẫu màu toàn cục đã xóa vẫn giữ màu của nó.
Artboard Bảng vẽ
@@ -376,6 +377,7 @@
Bring Forward Đưa lên trước
Bring to Front Đưa lên trên cùng
Bristle Lông cứng
Bristle Brush Cọ lông
Bristle Brush Options Tùy chọn cọ lông
Bristle Brush Strokes Nét cọ lông cứng
Bristle Density: Mật độ lông cọ:
@@ -383,6 +385,7 @@
Bristle Thickness: Độ dày lông cọ:
Browser Trình duyệt
Brush Affects: Cọ tác động:
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)
@@ -401,6 +404,7 @@
Butt Cap Đầu nét vuông
Byte Order: Thứ tự byte:
Calligraphic Thư pháp
Calligraphic Brush Cọ thư pháp
Calligraphic Brush Options Tùy chọn cọ thư pháp
Can't open “{name}”. Không thể mở “{name}”.
Can't save “{name}”. Không thể lưu “{name}”.
@@ -2202,6 +2206,7 @@ file-filter Templates Mẫu
New Art Has Basic Appearance Bản vẽ mới có diện mạo cơ bản
New Artboard Bảng vẽ mới
New Bristle Brush Cọ lông mới
New Brush Cọ mới
New Brush… Cọ mới…
New Calligraphic Brush Cọ thư pháp mới
New Character Style Kiểu ký tự mới
@@ -2532,6 +2537,7 @@ file-filter Templates Mẫu
Path Length Độ dài đường dẫn
Paths: {paths} Anchors: {anchors} Colors: {colors} Đường dẫn: {paths} Điểm neo: {anchors} Màu: {colors}
Pattern Họa tiết
Pattern Brush Cọ họa tiết
Pattern Brush Options Tùy chọn cọ họa tiết
Pattern Editing Chỉnh sửa họa tiết
Pattern Options Tùy chọn họa tiết
@@ -3118,6 +3124,7 @@ mark-style Japanese Kiểu Nhật
Scallop Tool Công cụ Vỏ sò
Scans: Lần quét:
Scatter Phân tán
Scatter Brush Cọ phân tán
Scatter Brush Options Tùy chọn cọ phân tán
Scatter Graph Tool Công cụ Biểu đồ phân tán
Scatter: Phân tán:
@@ -3173,6 +3180,7 @@ mark-style Japanese Kiểu Nhật
Select art and choose Object › Pattern › Make. Chọn đối tượng và chọn Đối tượng › Họa tiết › Tạo.
Select art and click New Symbol to make one. Chọn đối tượng và bấm Ký hiệu mới để tạo.
Select art and use New Brush to make one. Chọn đối tượng và dùng Cọ mới để tạo.
Select art to make a scatter, art or pattern brush. Chọn đối tượng để tạo cọ phân tán, cọ bản vẽ hoặc cọ họa tiết.
Select Bound Object Chọn đối tượng gắn
Select Data Set Chọn tập dữ liệu
Select Freeform Point Chọn điểm tự do
1 # Vietnamese (vi) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
253
254
255
256
257
258
259
377
378
379
380
381
382
383
385
386
387
388
389
390
391
404
405
406
407
408
409
410
2206
2207
2208
2209
2210
2211
2212
2537
2538
2539
2540
2541
2542
2543
3124
3125
3126
3127
3128
3129
3130
3180
3181
3182
3183
3184
3185
3186
+8
View File
@@ -1868,6 +1868,14 @@ file-filter Templates 模板
Round Fan 圆扇头
Flat Fan 扁扇头
New Brush… 新建画笔…
New Brush 新建画笔
Calligraphic Brush 书法画笔
Scatter Brush 散布画笔
Art Brush 艺术画笔
Bristle Brush 鬃毛画笔
Pattern Brush 图案画笔
Select art to make a scatter, art or pattern brush. 选择图稿以创建散布画笔、艺术画笔或图案画笔。
Brush Art 画笔图稿
New Calligraphic Brush 新建书法画笔
New Character Style 新建字符样式
New Color Group 新建颜色组
Can't render this file because it contains an unexpected character in line 2 and column 73.
+8
View File
@@ -2720,6 +2720,14 @@ file-filter Templates 範本
Round Fan 圓扇頭
Flat Fan 扁扇頭
New Brush… 新增筆刷…
New Brush 新增筆刷
Calligraphic Brush 書法筆刷
Scatter Brush 散佈筆刷
Art Brush 藝術筆刷
Bristle Brush 鬃毛筆刷
Pattern Brush 圖樣筆刷
Select art to make a scatter, art or pattern brush. 選取圖稿以製作散佈筆刷、藝術筆刷或圖樣筆刷。
Brush Art 筆刷圖稿
New Calligraphic Brush 新增書法筆刷
New Character Style 新增字元樣式
New Color Group 新增顏色群組
Can't render this file because it contains an unexpected character in line 6 and column 47.
+8
View File
@@ -462,6 +462,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
"",
"{} or {stroke?: bool (default: the active proxy), picker?: bool (the open Color Picker instead)} show the system's color sampler (macOS; browsers with an EyeDropper) and answer at once; the color the person picks anywhere on the screen then runs paint.setFill / paint.setStroke (one undo step) or becomes the Color Picker's color (field `color`), converted from display sRGB into the working RGB space; Escape cancels it and the colors stay as they were. ui.inspect reports `screenSampler: true` until it answers. An error without a sampler, while one is open, or with `picker` and no Color Picker open",
),
(
"ui.newBrush",
"New Brush…",
"",
"{ids?} (default: the selected art) open New Brush, what the Brushes panel's New Brush button and art dropped on the panel open (dialog `newBrush`, field `type`: calligraphic|scatter|art|bristle|pattern, Art when there is art, else Calligraphic; scatter, art and pattern need art, else OK gives the error `nothing selected`); OK opens that type's Brush Options for the new brush (dialog `brushOptions` with the fields of ui.brushOptions and `__create`; name: New <Type> Brush, numbered when taken; the `ids` given in `__ids`), whose OK runs brush.new {type, name, params, ids?} (a name another brush has gets a number) and whose Cancel adds no brush",
),
(
"ui.brushOptions",
"Brush Options…",
@@ -1570,6 +1576,7 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
crate::dialogs::symbol_options::open(app)
}
}
"ui.newBrush" => crate::dialogs::new_brush::open(app, p.get("ids").cloned()),
"ui.brushOptions" => crate::dialogs::brush_options::open(app, s("name").as_deref()),
"tool.options" => crate::toolbar::open_options(app, &s("tool").unwrap_or_default()),
"ui.colorGuideOptions" => {
@@ -2331,6 +2338,7 @@ pub fn enabled(app: &VectorcraftApp, id: &str) -> bool {
"ui.graphicStyleOptions" => app.session.active().is_some(),
"ui.newArtboard" => app.session.active().is_some(),
"ui.sampleScreenColor" => crate::screen_color::available(app),
"ui.newBrush" => app.session.active().is_some(),
"ui.brushOptions" => crate::dialogs::brush_options::available(app),
"ui.hyphenation" | "ui.justification" => crate::panels::character::text_style(app).is_some(),
"ui.colorBalanceDialog" | "ui.saturateDialog" => app.session.active().is_some_and(|d| !d.selection.is_empty()),
+41 -5
View File
@@ -246,10 +246,11 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
}
ui.min_rect()
});
// Art dragged off the canvas and dropped on the list becomes an Art brush, as New Brush makes.
// 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.
let zone = ui.interact(list_rect, ui.id().with("brushes-drop"), Sense::hover());
if let Some(ids) = widgets::art_drop(ui, &zone)
&& let Err(e) = app.run("brush.new", json!({ "type": "art", "ids": ids }))
&& let Err(e) = app.run("ui.newBrush", json!({ "ids": ids }))
{
app.status(e);
}
@@ -297,7 +298,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
}
let has_brush = sel_brush.is_some();
let targets = multi.unwrap_or_else(|| highlighted.into_iter().collect());
let has_sel = app.session.active().is_some_and(|d| !d.selection.is_empty());
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);
if widgets::icon_button_enabled(ui, "dc-remove-brush", tl!("Remove Brush Stroke"), false, has_brush, 24.0).clicked() {
@@ -307,8 +308,11 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
app.run("object.expandBrush", json!({})).ok();
}
ui.add_space((ui.available_width() - 2.0 * 28.0).max(0.0));
if widgets::icon_button_enabled(ui, "dc-new-item", tl!("New Art Brush from Selection"), false, has_sel, 24.0).clicked() {
app.run("brush.new", json!({"type": "art"})).ok();
// New Brush asks for the type, then opens that type's Brush Options.
if widgets::icon_button_enabled(ui, "dc-new-item", tl!("New Brush"), false, has_doc, 24.0).clicked()
&& let Err(e) = app.run("ui.newBrush", json!({}))
{
app.status(e);
}
if widgets::icon_button_enabled(ui, "trash-2", tl!("Delete Brush"), false, !targets.is_empty(), 24.0).clicked() {
run_on(app, "brush.delete", &targets);
@@ -321,6 +325,12 @@ 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());
if menu_item(ui, tl!("New Brush…"), app.session.active().is_some(), false)
&& let Err(e) = app.run("ui.newBrush", json!({}))
{
app.status(e);
}
// The types' own items make a brush at once, without its Brush Options.
for (label, ty) in [(tl!("New Calligraphic Brush"), "calligraphic"), (tl!("New Bristle Brush"), "bristle")] {
if menu_item(ui, label, true, false) {
app.run("brush.new", json!({"type": ty})).ok();
@@ -484,6 +494,26 @@ mod tests {
assert_eq!(brushes(&mut app).1.as_deref(), Some("Pressure Round"), "renamed, still current");
}
/// The New Brush button and the panel menu's New Brush… open New Brush, with nothing selected
/// too, and make no brush.
#[test]
fn the_new_brush_button_and_menu_item_open_new_brush() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 200, "height": 100})).unwrap();
let ctx = egui::Context::default();
ctx.enable_accesskit();
let before = names(&mut app);
let out = frame_with(&ctx, &mut app, vec![], egui::Modifiers::NONE, 0.0, show);
let new = clickable(&out).into_iter().find(|(l, _)| l.as_deref() == Some("New Brush")).expect("a New Brush button").1.center();
click(&ctx, &mut app, new, egui::Modifiers::NONE, 1.0, show);
let kind = |app: &VectorcraftApp| app.ui.dialog.as_ref().map(|d| d.kind.clone());
assert_eq!(kind(&app).as_deref(), Some(crate::dialogs::new_brush::KIND));
app.ui.dialog = None;
menu_click(&ctx, &mut app, "New Brush…", 2.0);
assert_eq!(kind(&app).as_deref(), Some(crate::dialogs::new_brush::KIND));
assert_eq!(names(&mut app), before);
}
/// The rectangles `out` fills with the selected-row color.
fn lit(ctx: &egui::Context, out: &egui::FullOutput) -> Vec<Rect> {
let t = Tokens::get(ctx);
@@ -671,6 +701,12 @@ mod tests {
let over = pos2(100.0, 60.0);
egui::DragAndDrop::set_payload(&ctx, PanelDrag::Art(vec![NodeId(id)]));
frame(&ctx, &mut app, vec![egui::Event::PointerMoved(over), button(over, false)]);
// New Brush opens on Art for the dropped art, and OK twice makes the brush.
let d = app.ui.dialog.as_ref().expect("New Brush opened");
assert_eq!((d.kind.as_str(), d.str("type"), d.fields.get("__ids")), (crate::dialogs::new_brush::KIND, "art".into(), Some(&json!([id]))));
assert_eq!(brushes(&mut app).0, before);
crate::dialogs::confirm(&mut app).unwrap();
crate::dialogs::confirm(&mut app).unwrap();
let (after, _) = brushes(&mut app);
assert_eq!(after.len(), before.len() + 1);
let new: Vec<_> = after.iter().filter(|b| !before.contains(b)).collect();
+8
View File
@@ -1295,6 +1295,14 @@ already walks Image Trace. Short agent entry points:
mask and graphic style link. With a Live Paint group among the objects, `livePaint.make` adds the others to it as
`livePaint.merge` does, and so does `livePaint.fill {ids}` when `point` is outside that group's faces.
`brush.selectionArt {ids?|id?}` (a query) replies `{art}`: the object that `brush.new` given the same `ids` or `id`,
or the same selection, takes as a new scatter or art brush's art and as a new pattern brush's side tile. Several
objects become one group, and `art` is `null` when nothing is selected or `ids` name no object. In the app, the
Brushes panel's New Brush button, its menu's New Brush… and art dropped on the panel open New Brush
(`ui.newBrush {ids?}`, dialog `newBrush`, field `type`). Scatter, art and pattern need art. OK opens that type's
Brush Options for the new brush; for a scatter, art or pattern brush they preview the art from `brush.selectionArt`.
Their OK runs `brush.new {type, name, params, ids?}`, and Cancel adds no brush.
`dataset.select`, `dataset.next` and `dataset.prev` reply `{name, applied, skipped}`. They add `overflow: [id…]`
when area type or type on a path whose text they rewrote doesn't fit, as `text.setText` does (see
[Fitting area type](#fitting-area-type-auto-size-shrink-text-to-fit-overflow)). They skip an object that is
+2
View File
@@ -588,6 +588,8 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- **Temporary tools:** Cmd with any other tool drags with the selection tool used last (Selection, Direct Selection or Group Selection). Held Space pans and Cmd+Space zooms (Cmd+Alt+Space out) whatever the tool.
A Space+Shift drag rotates the view whatever the tool, as a Rotate View tool drag does, and Shift does not round the angle to 15°.
- **Swatches panel clicks:** Ctrl/Cmd-clicking New Swatch makes a spot color, and Ctrl/Cmd+Shift-clicking it makes a global process color.
- **Panel drag and drop:** art dragged off the canvas onto the Symbols panel becomes a symbol, art dragged onto the Swatches panel becomes a pattern swatch, and art dropped on the Brushes panel opens New Brush for that art; a symbol dragged onto the canvas places an instance where it is dropped, and a brush dragged onto a path is applied to it.
New Brush asks for the type of brush first, as the [Brushes](https://web.archive.org/web/20241230205225/https://helpx.adobe.com/illustrator/using/brushes.html) page (archived copy) describes. The Brushes panel's New Brush button, its menu's New Brush… and art dropped on the panel open the New Brush dialog, where Scatter Brush, Art Brush and Pattern Brush need selected or dropped art. OK opens that type's Brush Options for the new brush, and their OK makes the brush with the name and options set; Cancel in either dialog adds no brush (`ui.newBrush`). The menu's New Calligraphic Brush, New Bristle Brush and New Art, Scatter and Pattern Brush from Selection items still make a brush at once.
- **Panel drag and drop:** art dragged off the canvas onto the Symbols panel becomes a symbol, onto the Swatches panel a pattern swatch and onto the Brushes panel an Art brush; a symbol dragged onto the canvas places an instance where it is dropped, and a brush dragged onto a path is applied to it.
In the Brushes panel, a brush dragged onto another brush of its type goes before or after it, by the half of the tile or row the pointer is over, and a bar marks the slot while the brush is held there (`brush.move`); the move is one undo step. Brushes move only within their type, as the [Brushes](https://web.archive.org/web/20241230205225/https://helpx.adobe.com/illustrator/using/brushes.html) page states ("You can move brushes only within their type"): a brush dropped on a brush of another type stays where it is, and the status bar shows "brushes move only within their type".
In the Swatches panel, Alt-dragging a swatch onto another swatch of its kind (not a tint swatch) replaces that swatch's color, gradient or pattern, and the swatch keeps its name and place.