From 7d97c633b7ea123a0771880ced4ff65e3a610a0b Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Tue, 27 Oct 2026 17:46:04 -0700 Subject: [PATCH] Pencil and Blob Brush: Alt smooths with the Smooth tool's Fidelity When Alt switches the Pencil (with Alt key toggles to Smooth Tool on) or the Blob Brush to the Smooth tool, the drag sent the drawing tool's own Fidelity (1.5 by default) to path.smoothRegion. The Smooth tool sends its own Fidelity (2.5 by default, set in Smooth Tool Options). Both tools now smooth with the Smooth tool's Fidelity. The engine gives them the stored Smooth Fidelity when it makes the tool, and again when Smooth Tool Options changes while one of them is active. Each tool's own Fidelity still sets how its strokes are fitted. --- crates/engine/src/tests/toolsettings.rs | 35 +++++++++++++++++++++++++ crates/engine/src/tooling.rs | 9 ++++++- crates/tools/src/draw2/gesture.rs | 13 +++++++-- crates/tools/src/draw2/gesture/tests.rs | 33 ++++++++++++++++++++++- 4 files changed, 86 insertions(+), 4 deletions(-) diff --git a/crates/engine/src/tests/toolsettings.rs b/crates/engine/src/tests/toolsettings.rs index 7584aa9cb..3f7d1513b 100644 --- a/crates/engine/src/tests/toolsettings.rs +++ b/crates/engine/src/tests/toolsettings.rs @@ -96,3 +96,38 @@ fn options_are_saved_with_the_preferences() { t.execute("prefs.reset", &json!({})).unwrap(); assert_eq!(t.prefs.tool_settings["polygon"]["sides"], json!(9)); } + +/// The Fidelity an Alt smoothing drag uses on a selected path. +fn alt_smooth_fidelity(s: &mut Session) -> Option { + use vectorcraft_tools::{Action, Mods, PointerEvent, PointerKind}; + let mods = Mods { alt: true, ..Default::default() }; + let mut last = vec![]; + for (kind, x, y) in [(PointerKind::Down, 50.0, 150.0), (PointerKind::Drag, 150.0, 150.0), (PointerKind::Up, 250.0, 160.0)] { + let ev = PointerEvent { mods, ..PointerEvent::new(kind, x, y) }; + last = s.with_tool_cx(ViewInfo::default(), |t, cx| t.pointer(cx, &ev)); + } + last.iter().find_map(|a| match a { + Action::Exec(c, v) if c == "path.smoothRegion" => v["fidelity"].as_f64(), + _ => None, + }) +} + +/// Alt switches Pencil and Blob Brush to Smooth, using Smooth Tool Options' stored Fidelity. +#[test] +fn alt_smoothing_takes_the_smooth_tools_fidelity() { + for id in ["pencil", "blobBrush"] { + let mut s = session(); + s.execute("shape.rectangle", &json!({"x": 100, "y": 100, "width": 100, "height": 100})).unwrap(); + select(&mut s, "smooth"); + s.set_tool_option("fidelity", &json!(6)); + select(&mut s, id); + if id == "pencil" { + s.set_tool_option("altSmooth", &json!(true)); + } + s.set_tool_option("fidelity", &json!(3)); + assert_eq!(alt_smooth_fidelity(&mut s), Some(6.0), "{id}"); + s.set_tool_option_cmd(&json!({"tool": "smooth", "key": "fidelity", "value": 9})).unwrap(); + assert_eq!(alt_smooth_fidelity(&mut s), Some(9.0), "{id}"); + assert_eq!(s.tool_options()["fidelity"], json!(3.0), "{id}"); + } +} diff --git a/crates/engine/src/tooling.rs b/crates/engine/src/tooling.rs index 13a7c2862..6dbfb133d 100644 --- a/crates/engine/src/tooling.rs +++ b/crates/engine/src/tooling.rs @@ -216,6 +216,12 @@ impl Session { } } } + // Alt switches the Pencil or Blob Brush to Smooth, using Smooth Tool Options' Fidelity. + if matches!(t.id(), "pencil" | "blobBrush") + && let Some(v) = prefs.tool_settings.get("smooth").and_then(|s| s.get("fidelity")) + { + t.set_option("smoothFidelity", v); + } } /// Store the persistent options of `t` in `prefs` (so they are saved with them too). @@ -623,7 +629,8 @@ impl Session { /// Set options of tool `id` (default: the active tool). Another tool's persistent options are /// stored for when it is chosen, and the active tool takes those it shares with it (the - /// Liquify tools' Global Brush Dimensions). Returns the tool's options. + /// Liquify tools' Global Brush Dimensions, the Smooth tool's Fidelity for the Pencil's and + /// Blob Brush's Alt smoothing). Returns the tool's options. pub fn set_tool_options(&mut self, id: Option<&str>, values: &Map) -> Value { let active = id.is_none_or(|id| id == self.tool.id()); self.tool_set_since_reset |= active; diff --git a/crates/tools/src/draw2/gesture.rs b/crates/tools/src/draw2/gesture.rs index eade2a524..308264137 100644 --- a/crates/tools/src/draw2/gesture.rs +++ b/crates/tools/src/draw2/gesture.rs @@ -34,6 +34,8 @@ const SMOOTH_RADIUS: f64 = 12.0; /// a fit's cost grows as its Fidelity shrinks (at 0.01 pt a long pen stroke lagged, #4565), and /// a preview within half a pixel of the stroke looks the same. The release fits it at its Fidelity. const PREVIEW_FIDELITY_PX: f64 = 0.5; +/// The Smooth tool's Fidelity until Smooth Tool Options sets another. +const SMOOTH_FIDELITY: f64 = 2.5; pub struct GestureTool { /// How many points the last preview of the stroke had ([`crate::previews_at`]); 0 when it was @@ -87,6 +89,9 @@ pub struct GestureTool { /// preview shows the points joined by straight segments and the release fits them (Pencil /// only, on by default). pub live_fit: bool, + /// The Smooth tool's Fidelity, used when Alt switches the Pencil or Blob Brush to Smooth. + /// The engine sets it from Smooth Tool Options (`smoothFidelity`). + smooth_fidelity: f64, /// Active drag is smoothing with the Pencil or the Blob Brush (Alt held). smoothing: bool, /// The pointer has moved from the press point, by any distance (a move too short to add a @@ -133,7 +138,7 @@ impl GestureTool { "join" => "join", _ => "pencil", }; - let fidelity = if id == "smooth" { 2.5 } else { 1.5 }; + let fidelity = if id == "smooth" { SMOOTH_FIDELITY } else { 1.5 }; Self { id, previewed: 0, @@ -157,6 +162,7 @@ impl GestureTool { alt_smooth: false, live_preview: true, live_fit: true, + smooth_fidelity: SMOOTH_FIDELITY, smoothing: false, moved: false, straight: false, @@ -383,7 +389,7 @@ impl GestureTool { if has_sel && pts.len() >= 2 { return vec![Action::Exec( "path.smoothRegion".into(), - json!({"points": points_json(pts), "radius": radius, "fidelity": self.fidelity}), + json!({"points": points_json(pts), "radius": radius, "fidelity": self.smooth_fidelity}), )]; } return vec![]; @@ -783,6 +789,9 @@ impl Tool for GestureTool { "altSmooth" => self.alt_smooth = v.as_bool().unwrap_or(self.alt_smooth), "livePreview" if self.id == "pencil" => self.live_preview = v.as_bool().unwrap_or(self.live_preview), "liveFit" if self.id == "pencil" => self.live_fit = v.as_bool().unwrap_or(self.live_fit), + "smoothFidelity" if matches!(self.id, "pencil" | "blobBrush") => { + self.smooth_fidelity = v.as_f64().unwrap_or(self.smooth_fidelity).clamp(super::min_fidelity("smooth"), 100.0) + } "keepSelected" => self.keep_selected = v.as_bool().unwrap_or(self.keep_selected), "mergeOnlySelection" => self.merge_only_selection = v.as_bool().unwrap_or(self.merge_only_selection), "editWithin" => self.edit_within = v.as_f64().unwrap_or(self.edit_within).clamp(0.0, 100.0), diff --git a/crates/tools/src/draw2/gesture/tests.rs b/crates/tools/src/draw2/gesture/tests.rs index 0b6da81f5..c511091b5 100644 --- a/crates/tools/src/draw2/gesture/tests.rs +++ b/crates/tools/src/draw2/gesture/tests.rs @@ -420,7 +420,7 @@ fn blob_brush_alt_switches_to_the_smooth_tool() { let acts = drag_held(&mut t, &cx, &stroke, alt); assert!(acts[..acts.len() - 1].iter().all(|a| a.is_empty()), "{acts:?}"); let v = smoothed(&acts).unwrap_or_else(|| panic!("{acts:?}")); - assert_eq!(v, json!({"points": [[50.0, 150.0], [150.0, 150.0], [250.0, 160.0]], "radius": cx.tol(12.0), "fidelity": 1.5})); + assert_eq!(v, json!({"points": [[50.0, 150.0], [150.0, 150.0], [250.0, 160.0]], "radius": cx.tol(12.0), "fidelity": SMOOTH_FIDELITY})); // Mid-drag the cursor stays the Smooth tool's when Alt is let go, and the line is thin with // no length label. t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 50.0, 150.0).with_mods(alt)); @@ -872,6 +872,37 @@ fn pencil_alt_smooths_within_the_smooth_tools_radius() { } } +/// An Alt smoothing drag uses the Smooth tool's Fidelity for both Pencil and Blob Brush. +#[test] +fn alt_smoothing_uses_the_smooth_tools_fidelity() { + let (d, id) = doc_with_rect(); + let mut s = Selection::default(); + s.set([id]); + let p = paint(); + let mut cx = cx(&d, &s, &p); + cx.smart_guides = false; + let pts = [(50.0, 150.0), (150.0, 150.0), (250.0, 160.0)]; + let fidelity = |acts: &[Vec]| match acts.last().and_then(|a| a.first()) { + Some(Action::Exec(c, v)) if c == "path.smoothRegion" => v["fidelity"].as_f64().unwrap(), + a => panic!("expected path.smoothRegion, got: {a:?}"), + }; + let alt = Mods { alt: true, ..Default::default() }; + let want = fidelity(&drag(&mut GestureTool::new("smooth"), &cx, &pts, Mods::default())); + for id in ["pencil", "blobBrush"] { + let mut t = GestureTool::new(id); + if id == "pencil" { + t.set_option("altSmooth", &json!(true)); + } + t.set_option("fidelity", &json!(10.0)); + assert_eq!(fidelity(&drag_held(&mut t, &cx, &pts, alt)), want, "{id}"); + t.set_option("smoothFidelity", &json!(7.5)); + assert_eq!(fidelity(&drag_held(&mut t, &cx, &pts, alt)), 7.5, "{id}"); + // Drawing keeps the tool's own Fidelity; Smooth's setting stays out of its options. + assert_eq!(t.options()["fidelity"], json!(10.0), "{id}"); + assert!(t.options().get("smoothFidelity").is_none(), "{id}"); + } +} + /// #3799: Alt's line starts and ends on Smart Guides as a stroke does, and pressed on an end of /// a selected open path it continues the path from that end to the pointer. #[test]