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]