Merge pull request #4706 from dotCooCoo/fix/pencil-alt-smooth-fidelity

Pencil and Blob Brush: Alt smooths with the Smooth tool's Fidelity
This commit is contained in:
BFlat
2026-10-31 00:50:21 +07:00
committed by GitHub
4 changed files with 86 additions and 4 deletions
+35
View File
@@ -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<f64> {
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}");
}
}
+8 -1
View File
@@ -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<String, Value>) -> Value {
let active = id.is_none_or(|id| id == self.tool.id());
self.tool_set_since_reset |= active;
+11 -2
View File
@@ -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),
+32 -1
View File
@@ -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<Action>]| 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]