From 02582f318dab61bcb3c47c9a680bdbfd6af718ad Mon Sep 17 00:00:00 2001 From: theironchef <6278036+theironchef@users.noreply.github.com> Date: Wed, 7 Oct 2026 15:39:00 -0700 Subject: [PATCH] Fix #388: Pencil, Paintbrush, Smooth, Blob Brush and Eraser Tool Options on double-click The five freehand tools keep options (Fidelity, fill, the two tolerances, the brush size) that last across tool switches and are saved with the preferences, but nothing in the window showed or changed them: a double-click on the tool's button answered "the pencil tool has no options", which the toolbar drops. It now opens a Tool Options dialog with the options the tool has, and OK sets them with tool.setOption. The dialog's strings are in the Japanese, Chinese (both scripts) and Brazilian Portuguese catalogs. Co-Authored-By: Claude Sonnet 5.5 --- ROADMAP.md | 2 +- crates/ui-egui/src/dialogs/freehand.rs | 208 +++++++++++++++++++++++++ crates/ui-egui/src/dialogs/mod.rs | 2 + crates/ui-egui/src/i18n/ja.tsv | 4 + crates/ui-egui/src/i18n/pt-br.tsv | 4 + crates/ui-egui/src/i18n/zh-hans.tsv | 4 + crates/ui-egui/src/i18n/zh-hant.tsv | 4 + crates/ui-egui/src/menus.rs | 2 +- crates/ui-egui/src/toolbar.rs | 7 +- docs/control-protocol.md | 6 + 10 files changed, 239 insertions(+), 4 deletions(-) create mode 100644 crates/ui-egui/src/dialogs/freehand.rs diff --git a/ROADMAP.md b/ROADMAP.md index 769d4330..a214ce38 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -121,7 +121,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist - **Editing type:** double-clicking type with a selection tool switches to the Type tool with the caret where it was clicked. - **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. - **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. - - **Tool double-clicks:** double-clicking a tool's button opens its options: Hand fits the artboard in the window, Zoom shows 100 %, Rotate, Scale, Reflect and Shear open their Object › Transform dialogs, the Selection, Direct Selection and Group Selection tools the Move dialog (Enter opens these seven tools' dialogs too), Gradient, Eyedropper, Blend and the Liquify tools their panels or options. + - **Tool double-clicks:** double-clicking a tool's button opens its options: Hand fits the artboard in the window, Zoom shows 100 %, Rotate, Scale, Reflect and Shear open their Object › Transform dialogs, the Selection, Direct Selection and Group Selection tools the Move dialog (Enter opens these seven tools' dialogs too), Gradient, Eyedropper, Blend and the Liquify tools their panels or options, and the Pencil, Paintbrush, Smooth, Blob Brush and Eraser tools their Tool Options (Fidelity, fill, the tolerances, the brush size). - **Layers panel:** every layer, sublayer, group and object is a clickable row at any depth: a click highlights it and makes its layer current (new art goes there), Shift-click a range, Ctrl/Cmd-click toggles. Sublayers are no longer treated as objects: clicks, marquees, Select All and the Select menu take the art inside them, and each sublayer selects in its own colour. Selection squares on rows with selected art (click selects a row's art, Shift adds or removes; dragging the square moves the selection to another row, Alt copies), the eye and lock on every row with drag-down-the-column (one undo step), Ctrl/Cmd-click an eye for per-layer Outline, Alt-click an eye or lock to hide or lock the other layers (again: show or unlock them all), triangles with Alt to open or close everything inside, drag and drop rows above, below or into layers and groups with a drop line (Alt copies, the trash deletes), inline rename, Delete key on highlighted rows. Buttons and the full panel menu: New Layer/Sublayer (Alt: options, Ctrl: on top), Duplicate, Delete, Options for Selection, clipping mask, isolation, Locate Object, Merge Selected, Flatten Artwork, Collect in New Layer, Release to Layers (Sequence and Build), Reverse Order, Template, Hide/Outline/Lock Others and Show/Preview/Unlock All, Paste Remembers Layers, Panel Options (row size, thumbnails per kind, Show Layers Only). Layer Options: name, colour (presets or custom), Template, Lock, Show, Print, Preview (outline drawing and clicking) and Dim Images to %. Every operation is a command (`layer.move`, `layer.highlight`, `layer.merge`…) with validation against cycles and invalid nesting. - **Drawing aids:** Smart Guides and snapping (moving, and resizing by the bounding-box handles: the dragged edges align with the other objects; the Artboard tool snaps artboards, and their bleed, to other artboards, bleeds and art, the grid or whole pixels while moving, copying and resizing them, and the Transform panel shows and edits the active artboard's X/Y/W/H), guides dragged out of the rulers, and Draw Normal / Behind / Inside modes. - **Views:** the status bar's artboard navigator (first, previous, next, last; `view.goToArtboard`) goes to an artboard and fits it, and Fit Artboard in Window and Actual Size show the navigator's artboard. The Artboards panel highlights that active artboard: clicking a row makes it active (the Artboard tool's too, while it is chosen) as does clicking an artboard with the Artboard tool, double-clicking its number also fits it in the window and double-clicking its name renames it. diff --git a/crates/ui-egui/src/dialogs/freehand.rs b/crates/ui-egui/src/dialogs/freehand.rs new file mode 100644 index 00000000..3f8628b8 --- /dev/null +++ b/crates/ui-egui/src/dialogs/freehand.rs @@ -0,0 +1,208 @@ +//! Tool Options of the freehand tools (double-click Pencil, Paintbrush, Smooth, Blob Brush or +//! Eraser): the options each tool keeps, which last across tool switches and are saved with the +//! preferences. OK sets them with `tool.setOption {tool, values}`. +//! +//! Fields: `tool`; `fidelity` (points; Pencil, Paintbrush and Smooth), `fill` (Pencil and +//! Paintbrush), `closeWithin` and `editWithin` (screen pixels, 0 turns it off; Pencil and +//! Paintbrush) and `size` (points; Blob Brush and Eraser). + +use serde_json::{Map, Value, json}; + +use super::document_setup::check; +use super::{DialogSpec, form, run_and_close}; +use crate::state::Dialog; +use crate::theme::Tokens; +use crate::{VectorcraftApp, widgets}; + +/// The dialog kind of the freehand tools' Tool Options. +pub const KIND: &str = "freehandOptions"; + +pub(super) const SPEC: DialogSpec = DialogSpec { heading, body, confirm, min_width: 420.0, ..DialogSpec::FORM }; + +/// The tools the dialog serves. +pub const TOOLS: [&str; 5] = ["pencil", "paintbrush", "smooth", "blobBrush", "eraser"]; + +/// The options the dialog edits, in the order it shows them (a tool has those it reports). +const KEYS: [&str; 5] = ["fidelity", "size", "fill", "closeWithin", "editWithin"]; + +/// Width of the label column: wide enough for the tolerances' labels when there are any. +fn label_w(d: &Dialog) -> f32 { + if d.fields.contains_key("closeWithin") { 230.0 } else { 90.0 } +} + +/// The range of the Fidelity slider (points); the field takes what the tool allows. +const FIDELITY: (f64, f64) = (0.5, 20.0); + +fn heading(d: &Dialog) -> String { + let label = vectorcraft_tools::tool_info(&d.str("tool")).map_or("", |t| t.label); + crate::i18n::fmt(tl!("{tool} Options"), &[("tool", tl!(label))]) +} + +/// Open the options of freehand tool `tool` with their current values. +pub fn open(app: &mut VectorcraftApp, tool: &str) -> Result { + if !TOOLS.contains(&tool) { + return Err(format!("`{tool}` isn't a freehand tool")); + } + let o = app.session.tool_options_of(tool); + let mut fields: Map = KEYS.iter().filter_map(|key| Some((key.to_string(), o.get(*key)?.clone()))).collect(); + fields.insert("tool".into(), json!(tool)); + app.ui.dialog = Some(Dialog { kind: KIND.into(), fields }); + Ok(json!({ "dialog": KIND })) +} + +/// A row with a number field for `d.fields[key]` (`suffix` is its unit), kept within `range`. +fn number(ui: &mut egui::Ui, d: &mut Dialog, (key, label): (&str, &str), suffix: &str, decimals: usize, range: (f64, f64)) { + let label_w = label_w(d); + let v = d.f64(key, 0.0); + widgets::label_row(ui, label, label_w, |ui| { + if let Some(x) = widgets::plain_field(ui, ("freehand-field", key), v, suffix, decimals, 72.0) { + d.fields.insert(key.into(), json!(x.clamp(range.0, range.1))); + } + }); +} + +/// The Fidelity row: a slider over [`FIDELITY`] in tenths, and a field. +fn fidelity(ui: &mut egui::Ui, d: &mut Dialog) { + let label_w = label_w(d); + let t = Tokens::get(ui.ctx()); + let v = d.f64("fidelity", 1.5); + let (min, max) = FIDELITY; + let mut new = None; + widgets::label_row(ui, tl!("Fidelity:"), label_w, |ui| { + let at = ((v.clamp(min, max) - min) / (max - min)) as f32; + if let (Some(x), _) = widgets::color_slider(ui, "freehand-fidelity", at, form::SLIDER_WIDTH, &|_| t.input_border) { + new = Some(((min + x as f64 * (max - min)) * 10.0).round() / 10.0); + } + if let Some(x) = widgets::plain_field(ui, "freehand-fidelity-field", v, "pt", 1, 64.0) { + new = Some(x.clamp(0.05, 100.0)); + } + }); + if let Some(n) = new.filter(|n| *n != v) { + d.fields.insert("fidelity".into(), json!(n)); + } + ui.label( + egui::RichText::new(tl!("A lower value follows the stroke closely; a higher one gives fewer points and smoother curves.")) + .size(11.5) + .color(t.text_dim), + ); +} + +fn body(_app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { + if d.fields.contains_key("fidelity") { + fidelity(ui, d); + ui.add_space(10.0); + } + if d.fields.contains_key("size") { + number(ui, d, ("size", tl!("Size:")), "pt", 1, (0.1, 1000.0)); + } + if d.fields.contains_key("fill") { + check(ui, d, "fill", tl!("Fill new strokes")); + ui.add_space(6.0); + } + if d.fields.contains_key("closeWithin") { + number(ui, d, ("closeWithin", tl!("Close paths when ends are within:")), "px", 0, (0.0, 100.0)); + } + if d.fields.contains_key("editWithin") { + number(ui, d, ("editWithin", tl!("Edit selected paths within:")), "px", 0, (0.0, 100.0)); + } + false +} + +/// OK: set the tool's options. +fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { + let values: Map = KEYS.iter().filter_map(|k| Some((k.to_string(), d.fields.get(*k)?.clone()))).collect(); + run_and_close(app, "tool.setOption", json!({ "tool": d.str("tool"), "values": values })) +} + +#[cfg(test)] +mod tests { + use super::*; + use vectorcraft_engine::Session; + + fn app() -> VectorcraftApp { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 400, "height": 300})).unwrap(); + app + } + + /// The labels the open dialog paints. + fn painted(app: &mut VectorcraftApp) -> String { + crate::tests_labels::painted_text(app, |app, ui| super::super::show(app, ui.ctx())) + } + + #[test] + fn each_tool_shows_its_options_and_ok_sets_them() { + let mut app = app(); + // Pencil: Fidelity, fill, the two tolerances; no Size. + app.run("tool.options", json!({"tool": "pencil"})).unwrap(); + let text = painted(&mut app); + for label in ["Pencil Tool Options", "Fidelity:", "Fill new strokes", "Close paths when ends are within:", "Edit selected paths within:"] { + assert!(text.contains(label), "{label} in {text}"); + } + assert!(!text.contains("Size:"), "{text}"); + let d = app.ui.dialog.as_mut().unwrap(); + assert_eq!((d.f64("fidelity", 0.0), d.bool("fill"), d.f64("closeWithin", 0.0), d.f64("editWithin", 0.0)), (1.5, false, 15.0, 12.0)); + for (k, v) in [("fidelity", json!(6.5)), ("fill", json!(true)), ("closeWithin", json!(0)), ("editWithin", json!(20))] { + d.fields.insert(k.into(), v); + } + super::super::confirm(&mut app).unwrap(); + assert!(app.ui.dialog.is_none()); + let o = app.session.tool_options_of("pencil"); + assert_eq!( + (o["fidelity"].as_f64(), o["fill"].as_bool(), o["closeWithin"].as_f64(), o["editWithin"].as_f64()), + (Some(6.5), Some(true), Some(0.0), Some(20.0)) + ); + // The Paintbrush keeps its own: the Pencil's change left it alone. + app.run("tool.options", json!({"tool": "paintbrush"})).unwrap(); + let d = app.ui.dialog.as_ref().unwrap(); + assert_eq!((d.f64("fidelity", 0.0), d.bool("fill")), (1.5, false)); + assert!(painted(&mut app).contains("Paintbrush Tool Options")); + // Smooth has Fidelity only, the Blob Brush and the Eraser Size only. + app.run("tool.options", json!({"tool": "smooth"})).unwrap(); + let text = painted(&mut app); + assert!(text.contains("Smooth Tool Options") && text.contains("Fidelity:"), "{text}"); + assert!(!text.contains("Fill new strokes") && !text.contains("Size:"), "{text}"); + for tool in ["blobBrush", "eraser"] { + app.run("tool.options", json!({"tool": tool})).unwrap(); + let text = painted(&mut app); + assert!(text.contains("Size:") && !text.contains("Fidelity:"), "{tool}: {text}"); + app.ui.dialog.as_mut().unwrap().fields.insert("size".into(), json!(24)); + super::super::confirm(&mut app).unwrap(); + assert_eq!(app.session.tool_options_of(tool)["size"].as_f64(), Some(24.0), "{tool}"); + } + } + + #[test] + fn a_new_fidelity_changes_what_the_pencil_draws() { + let mut app = app(); + // The anchors of a wave drawn around height `y0`, with nothing selected (a stroke starting + // at the end of a selected path would continue it). + let anchors = |app: &mut VectorcraftApp, y0: f64| { + app.run("select.set", json!({"ids": []})).unwrap(); + app.select_tool("pencil"); + let view = app.view_info(); + let pts: Vec<_> = (0..=40).map(|i| (20.0 + i as f64 * 8.0, y0 + 30.0 * (i as f64 * 0.25).sin())).collect(); + let pointer = |kind, (x, y)| vectorcraft_tools::PointerEvent::new(kind, x, y); + crate::canvas::dispatch(app, &pointer(vectorcraft_tools::PointerKind::Down, pts[0]), view); + for p in &pts[1..] { + crate::canvas::dispatch(app, &pointer(vectorcraft_tools::PointerKind::Drag, *p), view); + } + crate::canvas::dispatch(app, &pointer(vectorcraft_tools::PointerKind::Up, pts[40]), view); + let doc = &app.session.doc().unwrap().doc; + doc.layers[0].children().unwrap().last().unwrap().path_data().map_or(0, |p| p.subpaths.iter().map(|s| s.anchors.len()).sum::()) + }; + let precise = anchors(&mut app, 100.0); + app.run("tool.options", json!({"tool": "pencil"})).unwrap(); + app.ui.dialog.as_mut().unwrap().fields.insert("fidelity".into(), json!(12)); + super::super::confirm(&mut app).unwrap(); + let smooth = anchors(&mut app, 200.0); + assert!(smooth < precise, "{smooth} anchors at fidelity 12, {precise} at the default"); + } + + #[test] + fn the_other_tools_have_no_freehand_options() { + let mut app = app(); + assert!(open(&mut app, "pen").is_err()); + assert!(app.ui.dialog.is_none()); + } +} diff --git a/crates/ui-egui/src/dialogs/mod.rs b/crates/ui-egui/src/dialogs/mod.rs index 8f2605ea..e437e902 100644 --- a/crates/ui-egui/src/dialogs/mod.rs +++ b/crates/ui-egui/src/dialogs/mod.rs @@ -31,6 +31,7 @@ pub mod file_info; pub mod flatten; pub mod flattener_presets; mod form; +pub mod freehand; mod gradient_stop; pub mod graphic_style_options; pub mod halftone; @@ -268,6 +269,7 @@ registry! { PerspectiveGrid: [perspective_grid::KIND] => perspective_grid::SPEC, Envelope: [envelope::WARP, envelope::MESH, envelope::OPTIONS] => envelope::SPEC, LiquifyOptions: [liquify::KIND] => liquify::SPEC, + FreehandOptions: [freehand::KIND] => freehand::SPEC, PerspectiveGridPresets: [perspective_presets::KIND] => perspective_presets::SPEC, PerspectiveGridOptions: [perspective_options::KIND] => perspective_options::SPEC, BlendOptions: [blend_options::KIND] => blend_options::SPEC, diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 56a46ea4..156f0108 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -660,6 +660,7 @@ < 1 sec 1 秒未満 <不透明マスク> A File… ファイル… + A lower value follows the stroke closely; a higher one gives fewer points and smoother curves. 値を小さくするとストロークに忠実に、大きくするとポイントが少なく滑らかな曲線になります。 A new preset starting from the selected one 選択したプリセットをもとにした新しいプリセット A saved copy of the selected preset 選択したプリセットの保存済みの複製 Absolute Position 絶対位置 @@ -869,6 +870,7 @@ Clipboard as Text クリップボードをテキストとして取得 Close Library ライブラリを閉じる Close Path パスを閉じる + Close paths when ends are within: 端点が次の距離以内のとき、パスを閉じる: Collate 部単位で印刷 Collect in New Layer 新規レイヤーに集める Color Burn 焼き込みカラー @@ -1123,6 +1125,7 @@ Edit Print Preset プリントプリセットを編集 Edit Symbol シンボルを編集 Edit Theme テーマを編集 + Edit selected paths within: 選択したパスを次の距離以内で編集: Edit or Apply Color Group カラーグループを編集または適用 Edit or Apply Color Group… カラーグループを編集または適用… Edit or Apply Colors カラーを編集/適用 @@ -1241,6 +1244,7 @@ Fill New Type Objects With Placeholder Text 新規テキストオブジェクトをダミーテキストで埋める Fill Rule 塗りのルール Fill and Stroke 塗りと線 + Fill new strokes 新しいストロークに塗りを適用 Fill: 塗り: Find 検索 Find Font フォントを検索 diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index fdbf1147..59e4b424 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -99,6 +99,7 @@ < 1 sec < 1 segundo A File… Um arquivo… + A lower value follows the stroke closely; a higher one gives fewer points and smoother curves. Um valor menor segue o traço de perto; um valor maior gera menos pontos e curvas mais suaves. A new preset starting from the selected one Um novo predefinido a partir do selecionado A saved copy of the selected preset Uma cópia salva do predefinido selecionado About VectorCraft Sobre o VectorCraft @@ -381,6 +382,7 @@ Close All Fechar Tudo Close Library Fechar Biblioteca Close Path Fechar Caminho + Close paths when ends are within: Fechar caminhos quando as extremidades estiverem a menos de: Collapse Panels to Icons Recolher Painéis em Ícones Collapse to Icons Recolher em Ícones Collate Agrupar @@ -695,6 +697,7 @@ Edit PDF Preset Editar Predefinido de PDF Edit Pattern Editar Padrão Edit Print Preset Editar Predefinido de Impressão + Edit selected paths within: Editar caminhos selecionados a menos de: Edit Selection… Editar Seleção… Edit Symbol Editar Símbolo Edit Text Editar Texto @@ -848,6 +851,7 @@ Fill New Type Objects With Placeholder Text Preencher Novos Objetos de Texto com Texto de Placeholder Fill Rule Regra de Preenchimento Fill and Stroke Preenchimento e Traçado + Fill new strokes Preencher novos traçados Fill with Placeholder Text Preencher com Texto de Placeholder Fill: Preenchimento: Find Localizar diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index f29041e4..2335e05e 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -40,6 +40,7 @@ [Default] is protected: New… starts an editable copy. [默认] 受保护:“新建…”会创建可编辑的副本。 [Unnamed] [未命名] A File… 文件… + A lower value follows the stroke closely; a higher one gives fewer points and smoother curves. 数值较小时紧贴所绘线条,较大时锚点较少、曲线更平滑。 A new preset starting from the selected one 以所选预设为基础的新预设 A saved copy of the selected preset 所选预设的副本 About VectorCraft 关于 VectorCraft @@ -350,6 +351,7 @@ Close All 全部关闭 Close Library 关闭库 Close Path 闭合路径 + Close paths when ends are within: 端点相距在以下范围内时闭合路径: Cmd Cmd CMYK CMYK CMYK Color CMYK 颜色 @@ -722,6 +724,7 @@ Edit Pattern 编辑图案 Edit PDF Preset 编辑 PDF 预设 Edit Print Preset 编辑打印预设 + Edit selected paths within: 在以下范围内编辑所选路径: Edit Selection… 编辑所选对象… Edit Symbol 编辑符号 Edit Text 编辑文字 @@ -886,6 +889,7 @@ Fill & Stroke 填色和描边 Fill (X), double-click for the Color Picker 填色 (X),双击打开拾色器 Fill and Stroke 填色和描边 + Fill new strokes 为新描边应用填色 Fill Color 填色 Fill New Type Objects With Placeholder Text 用占位文本填充新文字对象 Fill Rule 填充规则 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 6655bd03..6ad02831 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -48,6 +48,7 @@ [Default] is protected: New… starts an editable copy. [預設] 受到保護:「新增…」會開始一份可編輯的複本。 [Unnamed] [未命名] A File… 一個檔案… + A lower value follows the stroke closely; a higher one gives fewer points and smoother curves. 數值較小時會貼近筆畫,較大時則錨點較少、曲線較平滑。 A new preset starting from the selected one 以所選預設集為基礎建立新預設集 A saved copy of the selected preset 所選預設集的已儲存拷貝 About VectorCraft 關於 VectorCraft @@ -361,6 +362,7 @@ Close All 全部關閉 Close Library 關閉資料庫 Close Path 封閉路徑 + Close paths when ends are within: 端點相距在以下範圍內時封閉路徑: Cmd Cmd CMYK CMYK CMYK Color CMYK 色彩 @@ -740,6 +742,7 @@ Edit Pattern 編輯圖樣 Edit PDF Preset 編輯 PDF 預設集 Edit Print Preset 編輯列印預設集 + Edit selected paths within: 在以下範圍內編輯選取的路徑: Edit Selection 編輯選取範圍 Edit Selection… 編輯選取範圍… Edit Symbol 編輯符號 @@ -909,6 +912,7 @@ Fill (X), double-click for the Color Picker 填色 (X),按兩下開啟檢色器 Fill and Stroke 填色與筆畫 Fill Color 填色顏色 + Fill new strokes 為新筆畫套用填色 Fill New Type Objects With Placeholder Text 以預留位置文字填滿新文字物件 Fill Rule 填色規則 Fill with Placeholder Text 以預留位置文字填滿 diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 42668561..53f92218 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -276,7 +276,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "tool.options", "Tool Options…", "", - "{tool: id} what double-clicking a tool button opens: hand → fits the artboard in the window (view.fitArtboard), zoom → 100% (view.actualSize), rotate|scale|reflect|shear → that Object › Transform dialog (dialog `rotate`, `scale`, `reflect` or `shear`; error `nothing selected` without a selection), selection|directSelection|groupSelection → the Move dialog (dialog `move`; the same error), gradient → the Gradient panel (window.panel), eyedropper → Eyedropper Options (dialog `eyedropperOptions`, fields sampleSize, pickUp, apply; OK runs eyedropper.setOptions), printTiling → resets the print tiling (print.tiling.set {reset: true}), warp|twirl|pucker|bloat|scallop|crystallize|wrinkle → that tool's Tool Options (dialog `liquifyOptions`, fields tool, width, height, angle, intensity %, usePressure, detail, simplify, simplifyOn, rate, complexity, horizontal %, vertical %, affectAnchors, affectIn, affectOut, showBrush; OK runs tool.setOption {tool, values})", + "{tool: id} what double-clicking a tool button opens: hand → fits the artboard in the window (view.fitArtboard), zoom → 100% (view.actualSize), rotate|scale|reflect|shear → that Object › Transform dialog (dialog `rotate`, `scale`, `reflect` or `shear`; error `nothing selected` without a selection), selection|directSelection|groupSelection → the Move dialog (dialog `move`; the same error), gradient → the Gradient panel (window.panel), eyedropper → Eyedropper Options (dialog `eyedropperOptions`, fields sampleSize, pickUp, apply; OK runs eyedropper.setOptions), printTiling → resets the print tiling (print.tiling.set {reset: true}), warp|twirl|pucker|bloat|scallop|crystallize|wrinkle → that tool's Tool Options (dialog `liquifyOptions`, fields tool, width, height, angle, intensity %, usePressure, detail, simplify, simplifyOn, rate, complexity, horizontal %, vertical %, affectAnchors, affectIn, affectOut, showBrush; OK runs tool.setOption {tool, values}), pencil|paintbrush|smooth|blobBrush|eraser → that tool's Tool Options (dialog `freehandOptions`, fields tool and the options the tool keeps: fidelity (pt), fill, closeWithin and editWithin (px, 0 is off), size (pt); OK runs tool.setOption {tool, values})", ), ( "ui.colorGuideOptions", diff --git a/crates/ui-egui/src/toolbar.rs b/crates/ui-egui/src/toolbar.rs index 1c3b889f..87e5d100 100644 --- a/crates/ui-egui/src/toolbar.rs +++ b/crates/ui-egui/src/toolbar.rs @@ -219,8 +219,9 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { /// Options dialog, the Blend tool's Blend /// Options; the Print Tiling tool's resets the print tiling. As in the reference app, the Hand /// tool's fits the artboard in the window, the Zoom tool's shows it at 100%, the Rotate, Scale, -/// Reflect and Shear tools' are their Object › Transform dialogs, and the selection tools' are the -/// Move dialog. +/// Reflect and Shear tools' are their Object › Transform dialogs, the selection tools' are the +/// Move dialog, and the Pencil, Paintbrush, Smooth, Blob Brush and Eraser tools' are their Tool +/// Options. pub fn open_options(app: &mut VectorcraftApp, tool: &str) -> Result { match tool { "hand" => app.run("view.fitArtboard", json!({})), @@ -252,6 +253,8 @@ pub fn open_options(app: &mut VectorcraftApp, tool: &str) -> Result app.run("print.tiling.set", json!({ "reset": true })), // The Liquify tools: their Tool Options (the Global Brush Dimensions and the tool's own). _ if vectorcraft_tools::settings::LIQUIFY.contains(&tool) => crate::dialogs::liquify::open(app, tool), + // The freehand tools: their Tool Options (Fidelity, fill, the tolerances, the brush size). + _ if crate::dialogs::freehand::TOOLS.contains(&tool) => crate::dialogs::freehand::open(app, tool), _ if vectorcraft_tools::tool_info(tool).is_none() => Err(format!("unknown tool `{tool}`")), _ => Err(format!("the {tool} tool has no options")), } diff --git a/docs/control-protocol.md b/docs/control-protocol.md index 1983f61b..547f2daa 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -523,6 +523,12 @@ then the tool's options `detail`, `simplify` and `simplifyOn` (Warp, Twirl, Puck `vertical` (Wrinkle, %), and `showBrush`. `ui.dialog.confirm` runs `tool.setOption {tool, values}`. `ui.pointer` events take `pressure` (0..1, default 1): it is the Liquify intensity while Use Pressure Pen is on. +Freehand Tool Options: double-clicking the Pencil, Paintbrush, Smooth, Blob Brush or Eraser tool +(`tool.options {tool: "pencil"}`) opens a `freehandOptions` dialog. Its fields are `tool` and the options the tool +keeps: `fidelity` (pt; Pencil, Paintbrush, Smooth), `fill` (Pencil, Paintbrush), `closeWithin` and `editWithin` (screen +pixels, 0 turns it off; Pencil, Paintbrush) and `size` (pt; Blob Brush, Eraser). `ui.dialog.confirm` runs +`tool.setOption {tool, values}`. + `ui.pointer` events also take `holdMs` (0..60000): the pointer then holds still that long, button down, before the next event. Twirl, Pucker and Bloat keep applying while held (a repeat of the last point every 0.1 s), exactly as for the same time held with the mouse. Liquify leaves type, symbols, images, graphs, meshes, envelopes, repeats and