diff --git a/crates/engine/src/cmd/brushsym.rs b/crates/engine/src/cmd/brushsym.rs index ed9e0e639..158f3ddcf 100644 --- a/crates/engine/src/cmd/brushsym.rs +++ b/crates/engine/src/cmd/brushsym.rs @@ -31,6 +31,15 @@ pub fn specs() -> Vec { // ----- 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 { 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 { + 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 { const C: &str = "brush.apply"; let name = name_param(p, C)?.to_string(); diff --git a/crates/engine/src/tests_brushsym.rs b/crates/engine/src/tests_brushsym.rs index 5d16e01b0..46e893717 100644 --- a/crates/engine/src/tests_brushsym.rs +++ b/crates/engine/src/tests_brushsym.rs @@ -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(); diff --git a/crates/ui-egui/src/dialogs/brush_options.rs b/crates/ui-egui/src/dialogs/brush_options.rs index 4a17dec13..7bc618d7c 100644 --- a/crates/ui-egui/src/dialogs/brush_options.rs +++ b/crates/ui-egui/src/dialogs/brush_options.rs @@ -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 Result, 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 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 { 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 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) -> Result { + 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 { + 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 { + 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) diff --git a/crates/ui-egui/src/dialogs/mod.rs b/crates/ui-egui/src/dialogs/mod.rs index 8ecf07883..db49229fc 100644 --- a/crates/ui-egui/src/dialogs/mod.rs +++ b/crates/ui-egui/src/dialogs/mod.rs @@ -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, diff --git a/crates/ui-egui/src/dialogs/new_brush.rs b/crates/ui-egui/src/dialogs/new_brush.rs new file mode 100644 index 000000000..3746a5874 --- /dev/null +++ b/crates/ui-egui/src/dialogs/new_brush.rs @@ -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) -> Result { + 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 { + 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 { + 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 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}"); + } + } +} diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 068435e26..2a4fa3134 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -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 Нова цветова група diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 5766a2018..5b4fec064 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index d6de95b14..424639386 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index f379e6c18..4c0cde7c4 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index dedcf1a5d..87b09fbd4 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -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 Νέο παράθυρο diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index a153fd473..335514f2d 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index def49808d..24c07edb9 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -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ä diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 3b03e2c86..af87c848e 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 25dccc606..00ef75e72 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index f43186e39..384f25c47 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index cdc95fd44..40a76b6e0 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 207b1139b..e8f7173ff 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -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 新規カラーグループ diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index c1fad3c6f..caee93df9 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -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 სიმბოლოს ეგზემპლარის განთავსება diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 8257d6faf..e9f40c208 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -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 새 색상 그룹 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index fa5e55dd6..76fd108a2 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index f46b75f29..a89219ee3 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index fb65c0f5e..4b3539be6 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index d70bc2228..297708b36 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -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 Новая цветовая группа diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 24bea6a6a..fa08eadce 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 86f844da7..518fd3aa2 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -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 Нова колірна група diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index dae6c9400..8def49bdd 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index c0e84c8cb..83655bedc 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -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 新建颜色组 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index a0200aba9..e606a4a91 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -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 新增顏色群組 diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 0e68290ed..0818a587b 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -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 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 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()), diff --git a/crates/ui-egui/src/panels/brushes.rs b/crates/ui-egui/src/panels/brushes.rs index 7b43daf60..f02de35df 100644 --- a/crates/ui-egui/src/panels/brushes.rs +++ b/crates/ui-egui/src/panels/brushes.rs @@ -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 { 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(); diff --git a/docs/mcp.md b/docs/mcp.md index 03a3e5a04..5d93b0138 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -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 diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 4c3eaebce..c4cb7dc05 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -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.