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 <noreply@anthropic.com>
This commit is contained in:
theironchef 2026-10-07 15:39:00 -07:00 committed by bflatastic
parent a80d9b8195
commit 02582f318d
10 changed files with 239 additions and 4 deletions

View File

@ -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.

View File

@ -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<Value, String> {
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<String, Value> = 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<Value, String> {
let values: Map<String, Value> = 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::<usize>())
};
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());
}
}

View File

@ -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,

View File

@ -660,6 +660,7 @@
< 1 sec 1 秒未満
<Opacity Mask> <不透明マスク>
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 フォントを検索

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -99,6 +99,7 @@
< 1 sec < 1 segundo
<Opacity Mask> <máscara de opacidade>
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

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -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 填充规则

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -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 以預留位置文字填滿

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -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",

View File

@ -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<serde_json::Value, String> {
match tool {
"hand" => app.run("view.fitArtboard", json!({})),
@ -252,6 +253,8 @@ pub fn open_options(app: &mut VectorcraftApp, tool: &str) -> Result<serde_json::
"printTiling" => 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")),
}

View File

@ -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