From 60cb4c45ab297a2c858424ab8a2681d257d4c8f3 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sun, 25 Oct 2026 13:56:20 -0700 Subject: [PATCH] Pencil: Live curve fitting in Pencil Tool Options Pencil Tool Options has a Live curve fitting checkbox, on by default. With it off, the path shown while a Pencil stroke is drawn joins the stroke's points with straight segments, and the release fits the curve at the Fidelity, as it does with the option on. Choosing another tool while a stroke is drawn also keeps the stroke fitted. The option is saved with the tool's other options. path.freehand takes fit (true by default): with fit false, a new path, a continued path and a redrawn section join the points with straight segments. --- crates/engine/src/cmd/draw2.rs | 28 ++++++++---- crates/engine/src/tests_draw2.rs | 36 +++++++++++++++ crates/tools/src/draw2/gesture.rs | 63 ++++++++++++++++++++++++-- crates/tools/src/settings.rs | 5 +- crates/ui-egui/src/dialogs/freehand.rs | 58 +++++++++++++++++++++++- crates/ui-egui/src/i18n/bg.tsv | 1 + crates/ui-egui/src/i18n/cs.tsv | 1 + crates/ui-egui/src/i18n/da.tsv | 1 + crates/ui-egui/src/i18n/de.tsv | 1 + crates/ui-egui/src/i18n/el.tsv | 1 + crates/ui-egui/src/i18n/es.tsv | 1 + crates/ui-egui/src/i18n/fi.tsv | 1 + crates/ui-egui/src/i18n/fr.tsv | 1 + crates/ui-egui/src/i18n/hu.tsv | 1 + crates/ui-egui/src/i18n/id.tsv | 1 + crates/ui-egui/src/i18n/it.tsv | 1 + crates/ui-egui/src/i18n/ja.tsv | 1 + crates/ui-egui/src/i18n/ka.tsv | 1 + crates/ui-egui/src/i18n/ko.tsv | 1 + crates/ui-egui/src/i18n/nl.tsv | 1 + crates/ui-egui/src/i18n/pl.tsv | 1 + crates/ui-egui/src/i18n/pt-br.tsv | 1 + crates/ui-egui/src/i18n/ru.tsv | 1 + crates/ui-egui/src/i18n/sv.tsv | 1 + crates/ui-egui/src/i18n/tr.tsv | 1 + crates/ui-egui/src/i18n/uk.tsv | 1 + crates/ui-egui/src/i18n/vi.tsv | 1 + crates/ui-egui/src/i18n/zh-hans.tsv | 1 + crates/ui-egui/src/i18n/zh-hant.tsv | 1 + docs/target-app-parity.md | 1 + 30 files changed, 199 insertions(+), 16 deletions(-) diff --git a/crates/engine/src/cmd/draw2.rs b/crates/engine/src/cmd/draw2.rs index 5016e7405..8c1a153f3 100644 --- a/crates/engine/src/cmd/draw2.rs +++ b/crates/engine/src/cmd/draw2.rs @@ -26,7 +26,7 @@ pub fn specs() -> Vec { "Pencil", [], None, - "{points: [[x,y]…] or [[x,y,pressure]…] (pen pressure 0..1: the stroke keeps it, for Calligraphic brushes with Pressure variation; a point without one presses fully), fidelity?: pt (1.5), closed?, style?: \"pencil\"|\"brush\", fill?: bool, keepSelected?: bool (true: the drawn or continued path is selected; false: it is not, and the rest of the selection stays), extend?: {id, end: \"start\"|\"end\"}, redraw?: {id, within?: pt (12)}} fit a freehand stroke → {id}. extend continues the path from that end; redraw (Edit Selected Paths) replaces the section of the path between where the stroke starts and ends, each within `within` of one subpath (the shorter way round a closed one), and draws a new path when the stroke doesn't end on it", + "{points: [[x,y]…] or [[x,y,pressure]…] (pen pressure 0..1: the stroke keeps it, for Calligraphic brushes with Pressure variation; a point without one presses fully), fidelity?: pt (1.5), fit?: bool (true; false: the points are joined by straight segments, as the Pencil previews a stroke with Live curve fitting off), closed?, style?: \"pencil\"|\"brush\", fill?: bool, keepSelected?: bool (true: the drawn or continued path is selected; false: it is not, and the rest of the selection stays), extend?: {id, end: \"start\"|\"end\"}, redraw?: {id, within?: pt (12)}} fit a freehand stroke → {id}. extend continues the path from that end; redraw (Edit Selected Paths) replaces the section of the path between where the stroke starts and ends, each within `within` of one subpath (the shorter way round a closed one), and draws a new path when the stroke doesn't end on it", has_doc, freehand ), @@ -575,11 +575,21 @@ fn fit_freehand(pts: &[Point], tol: f64, closed: bool) -> SubPath { fitted.subpaths.into_iter().next().filter(|s| s.anchors.len() >= 2).unwrap_or(sp) } +/// The path of a freehand stroke: fitted at `tol` ([`fit_freehand`]), or with no `tol` the points +/// joined by straight segments (`path.freehand` with `fit: false`). +fn stroke_subpath(pts: &[Point], tol: Option, closed: bool) -> SubPath { + match tol { + Some(tol) => fit_freehand(pts, tol, closed), + None => SubPath::new(pts.iter().map(|&p| Anchor::corner(p)).collect(), closed), + } +} + /// Pencil's Edit Selected Paths in the middle of a path: a stroke from one point of a subpath of /// `pd` to another, each within `within` of it, replaces the section between them (the shorter -/// way round a closed subpath) with the stroke fitted at `tol`. The subpath's index and its new -/// shape; None when the stroke doesn't start and end on the same subpath, or meets it once. -fn redraw_section(pd: &PathData, pts: &[Point], tol: f64, within: f64) -> Option<(usize, SubPath)> { +/// way round a closed subpath) with the stroke fitted at `tol` (with no `tol`, unfitted). The +/// subpath's index and its new shape; None when the stroke doesn't start and end on the same +/// subpath, or meets it once. +fn redraw_section(pd: &PathData, pts: &[Point], tol: Option, within: f64) -> Option<(usize, SubPath)> { use kurbo::ParamCurveArclen; let (first, last) = (*pts.first()?, *pts.last()?); let (si, _, _, _, d_a) = pd.nearest(first)?; @@ -633,7 +643,7 @@ fn redraw_section(pd: &PathData, pts: &[Point], tol: f64, within: f64) -> Option *s0 = s1.p; let (Some(e0), Some(e1)) = (stroke.last_mut(), sp.anchors.get(end)) else { return None }; *e0 = e1.p; - let fit = fit_freehand(&stroke, tol, false).anchors; + let fit = stroke_subpath(&stroke, tol, false).anchors; let (Some(f0), Some(f1)) = (fit.first().copied(), fit.last().copied()) else { return None }; let inner = fit.get(1..fit.len().saturating_sub(1)).unwrap_or_default(); let (before, after): (Vec, Vec) = if sp.closed { @@ -665,6 +675,8 @@ fn freehand(s: &mut Session, p: &Value) -> Result { let pressure = stroke_pressure(&samples); let mut pts: Vec = samples.into_iter().map(|(pt, _)| pt).collect(); let tol = number_or(p, "fidelity", 1.5, C)?.clamp(0.05, 100.0); + // The tolerance the stroke is fitted at; with `fit: false` it is not fitted. + let fit_tol = switch_or(p, "fit", true, C)?.then_some(tol); let closed = switch_or(p, "closed", false, C)?; let brush = str_param(p, "style") == Some("brush"); let keep_selected = switch_or(p, "keepSelected", true, C)?; @@ -692,7 +704,7 @@ fn freehand(s: &mut Session, p: &Value) -> Result { if closing && let Some(l) = pts.last_mut() { *l = first; } - let fit = fit_freehand(&pts, tol, false); + let fit = stroke_subpath(&pts, fit_tol, false); let mut add = fit.anchors; if let (Some(last), Some(a0)) = (sp.anchors.last_mut(), add.first()) { last.h_out = a0.h_out; @@ -748,7 +760,7 @@ fn freehand(s: &mut Session, p: &Value) -> Result { let (pd, _) = path_of(&s.doc()?.doc, id)?; // shortcut: a stroke pressure profile is left as it was, measured along the old path; fit // it to the new length if Calligraphic pressure strokes get redrawn. - if let Some((si, sp)) = redraw_section(&pd, &pts, tol, within) { + if let Some((si, sp)) = redraw_section(&pd, &pts, fit_tol, within) { s.edit(if brush { "Paintbrush" } else { "Pencil" }, |d, sel| { *path_mut(d, id)?.subpaths.get_mut(si).ok_or_else(|| bad(C, "no such subpath"))? = sp; if keep_selected { @@ -764,7 +776,7 @@ fn freehand(s: &mut Session, p: &Value) -> Result { if pts.len() < 2 { return Err(bad(C, "need at least two points")); } - let sp = fit_freehand(&pts, tol, closed); + let sp = stroke_subpath(&pts, fit_tol, closed); let fill = if filled { s.paint.fill.clone() } else { Paint::None }; // A pencil path with a fill keeps a stroke of None (#1679); only one that would show nothing // gets black. A brush is its stroke. diff --git a/crates/engine/src/tests_draw2.rs b/crates/engine/src/tests_draw2.rs index f31368e96..c6ee784c8 100644 --- a/crates/engine/src/tests_draw2.rs +++ b/crates/engine/src/tests_draw2.rs @@ -267,6 +267,42 @@ fn pencil_with_keep_selected_off_leaves_the_new_path_unselected() { assert!(selected(&s).is_empty()); } +/// `fit: false` (the Pencil's preview with Live curve fitting off) joins the stroke's points with +/// straight segments: a new path, a continued one and a redrawn section; by default the stroke is +/// fitted. +#[test] +fn freehand_with_fit_off_joins_the_points_with_straight_segments() { + let corners = |sp: &vectorcraft_geom::SubPath| sp.anchors.iter().all(|a| a.h_in == a.p && a.h_out == a.p); + let at = |sp: &vectorcraft_geom::SubPath| sp.anchors.iter().map(|a| (a.p.x, a.p.y)).collect::>(); + let pts = wave(100.0, 400.0, 100.0); + let mut s = session(); + let fitted = s.execute("path.freehand", &json!({"points": pts.iter().map(|p| [p.0, p.1]).collect::>()})).unwrap(); + let fitted = path(&s, NodeId(fitted["id"].as_u64().unwrap())); + assert!(!corners(&fitted.subpaths[0]) && fitted.subpaths[0].anchors.len() < pts.len(), "fitted by default"); + let unfitted = s.execute("path.freehand", &json!({"points": pts.iter().map(|p| [p.0, p.1]).collect::>(), "fit": false})).unwrap(); + let sp = path(&s, NodeId(unfitted["id"].as_u64().unwrap())).subpaths[0].clone(); + assert!(corners(&sp) && at(&sp) == pts, "{:?}", at(&sp)); + // Continued from the end of a selected open path: the line's anchors, then the stroke's points. + let l = line(&mut s, 0.0, 300.0, 100.0, 300.0); + s.execute("select.set", &json!({"ids": [l.0]})).unwrap(); + s.execute("path.freehand", &json!({"points": [[100, 300], [150, 330], [200, 290], [250, 320]], "extend": {"id": l.0}, "fit": false})).unwrap(); + let sp = path(&s, l).subpaths[0].clone(); + assert!(corners(&sp) && at(&sp) == [(0.0, 300.0), (100.0, 300.0), (150.0, 330.0), (200.0, 290.0), (250.0, 320.0)], "{:?}", at(&sp)); + // A redrawn section: the stroke's inner points between where it meets the line. + let r = line(&mut s, 0.0, 500.0, 300.0, 500.0); + s.execute("select.set", &json!({"ids": [r.0]})).unwrap(); + s.execute( + "path.freehand", + &json!({"points": [[100, 501], [125, 470], [150, 460], [175, 470], [200, 501]], "redraw": {"id": r.0, "within": 4}, "fit": false}), + ) + .unwrap(); + let sp = path(&s, r).subpaths[0].clone(); + assert!(corners(&sp), "{:?}", sp.anchors); + let inner: Vec<_> = at(&sp).into_iter().filter(|p| p.1 < 499.0).collect(); + assert_eq!(inner, [(125.0, 470.0), (150.0, 460.0), (175.0, 470.0)]); + assert!(s.execute("path.freehand", &json!({"points": [[0, 0], [1, 1]], "fit": "no"})).is_err()); +} + #[test] fn paintbrush_uses_stroke_blob_brush_fills() { let mut s = session(); diff --git a/crates/tools/src/draw2/gesture.rs b/crates/tools/src/draw2/gesture.rs index 859f697ca..c5ab06c8e 100644 --- a/crates/tools/src/draw2/gesture.rs +++ b/crates/tools/src/draw2/gesture.rs @@ -2,7 +2,8 @@ //! Knife, Smooth, Path Eraser and Join. //! //! Pencil and Paintbrush preview the fitted path live (`path.freehand` re-applied on the snapshot) -//! and commit on release; Alt on release (or ending near the start) closes the path, and starting +//! and commit on release; with Live curve fitting off, the Pencil's preview joins the stroke's +//! points with straight segments and the release fits them; Alt on release (or ending near the start) closes the path, and starting //! near an end of a selected open path continues it. Shift held at the press draws a straight //! Pencil line at 45° steps from the Constrain Angle instead, and Alt held at the press (with Alt //! key toggles to Smooth Tool off) draws a straight line at any angle. The Pencil's new path starts and ends on @@ -50,6 +51,10 @@ pub struct GestureTool { /// "Alt key toggles to Smooth Tool": holding Alt with the Pencil tool smooths selected paths /// instead of closing the path (Pencil only, off by default). pub alt_smooth: bool, + /// "Live curve fitting": the Pencil's preview fits the stroke while it is drawn; off, the + /// preview shows the points joined by straight segments and the release fits them (Pencil + /// only, on by default). + pub live_fit: bool, /// Active drag is smoothing with the Pencil tool. smoothing: bool, /// Active Pencil drag draws a straight line (Shift or Alt held at the press): the stroke is @@ -105,6 +110,7 @@ impl GestureTool { keep_selected: id != "blobBrush", // Off, as Pencil Tool Options ships it: Alt keeps drawing a closed path until it is on. alt_smooth: false, + live_fit: true, smoothing: false, straight: false, unconstrained: false, @@ -417,7 +423,12 @@ impl Tool for GestureTool { } if crate::previews_at(self.points.len()) { self.previewed = self.points.len(); - out.push(Action::Preview("path.freehand".into(), self.freehand_params(false))); + let mut v = self.freehand_params(false); + if !self.live_fit { + // Live curve fitting off: the release fits the stroke (`finish`). + v["fit"] = json!(false); + } + out.push(Action::Preview("path.freehand".into(), v)); } out } @@ -492,8 +503,9 @@ impl Tool for GestureTool { } fn deactivate(&mut self, _cx: &ToolContext) -> Vec { let began = self.began; - // Drags past the last preview are in the stroke kept, too. - let stale = (began && self.previewed != self.points.len() && self.draws_path() && !self.smoothing) + // Drags past the last preview are in the stroke kept, too, and with Live curve fitting + // off the stroke kept is fitted. + let stale = (began && (self.previewed != self.points.len() || !self.live_fit) && self.draws_path() && !self.smoothing) .then(|| Action::Preview("path.freehand".into(), self.freehand_params(false))); self.snap.clear(); self.active = false; @@ -557,6 +569,7 @@ impl Tool for GestureTool { "fidelity": self.fidelity, "fill": self.fill, "altSmooth": self.alt_smooth, + "liveFit": self.live_fit, "keepSelected": self.keep_selected, "editWithin": self.edit_within, "closeWithin": self.close_within @@ -583,6 +596,7 @@ impl Tool for GestureTool { "size" => self.size = v.as_f64().unwrap_or(self.size).clamp(0.1, 1000.0), "fill" => self.fill = v.as_bool().unwrap_or(self.fill), "altSmooth" => self.alt_smooth = v.as_bool().unwrap_or(self.alt_smooth), + "liveFit" if self.id == "pencil" => self.live_fit = v.as_bool().unwrap_or(self.live_fit), "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), @@ -677,6 +691,47 @@ mod tests { assert!(!t.busy()); } + /// Live curve fitting is the Pencil's, on by default. Off, the previews while a stroke is drawn + /// ask for the points unfitted (`fit: false`), and the release's preview is fitted as before. + #[test] + fn pencil_with_live_curve_fitting_off_previews_the_stroke_unfitted() { + let (d, _) = doc_with_rect(); + let s = Selection::default(); + let p = paint(); + let cx = cx(&d, &s, &p); + let stroke = [(0.0, 0.0), (10.0, 5.0), (20.0, 0.0), (30.0, 6.0), (40.0, 0.0)]; + // The `fit` each preview sends: those while drawn, then the release's. + let fits = |t: &mut GestureTool| { + let acts = drag(t, &cx, &stroke, Mods::default()); + let fit = |a: &Action| if let Action::Preview(_, v) = a { Some(v.get("fit").cloned()) } else { None }; + let (drawn, release) = acts.split_at(acts.len() - 1); + (drawn.iter().flatten().filter_map(fit).collect::>(), release[0].iter().filter_map(fit).collect::>()) + }; + let mut t = GestureTool::new("pencil"); + assert_eq!(t.options()["liveFit"], json!(true)); + let (drawn, release) = fits(&mut t); + assert!(!drawn.is_empty() && drawn.iter().all(Option::is_none), "{drawn:?}"); + assert_eq!(release, [None]); + t.set_option("liveFit", &json!(false)); + assert_eq!(t.options()["liveFit"], json!(false)); + let (drawn, release) = fits(&mut t); + assert!(!drawn.is_empty() && drawn.iter().all(|f| *f == Some(json!(false))), "{drawn:?}"); + assert_eq!(release, [None], "the release fits the stroke"); + // Choosing another tool while the stroke is drawn keeps it fitted, too. + t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 0.0, 0.0)); + for &(x, y) in &stroke[1..] { + t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, x, y)); + } + let acts = t.deactivate(&cx); + assert!(matches!(acts.as_slice(), [Action::Preview(_, v), Action::Commit] if v.get("fit").is_none()), "{acts:?}"); + // The Paintbrush has no such option, and its previews are fitted. + let mut t = GestureTool::new("paintbrush"); + t.set_option("liveFit", &json!(false)); + assert!(t.options().get("liveFit").is_none()); + let (drawn, _) = fits(&mut t); + assert!(!drawn.is_empty() && drawn.iter().all(Option::is_none), "{drawn:?}"); + } + /// Keep selected is on by default, and every Pencil and Paintbrush preview sends it. #[test] fn pencil_and_paintbrush_send_keep_selected() { diff --git a/crates/tools/src/settings.rs b/crates/tools/src/settings.rs index a682e1204..ffde5c089 100644 --- a/crates/tools/src/settings.rs +++ b/crates/tools/src/settings.rs @@ -35,8 +35,9 @@ const ROWS: &[Row] = &[ // Line Segment Tool Options: the Length and Angle a click's dialog opens with, Fill Line. (&["lineSegment"], None, &["length", "angle", "fillLine"]), (&["pencil", "paintbrush"], None, &["fidelity", "fill", "keepSelected", "editWithin", "closeWithin"]), - // Pencil Tool Options' Alt key toggles to Smooth Tool (the Paintbrush has no such option). - (&["pencil"], None, &["altSmooth"]), + // Pencil Tool Options' Alt key toggles to Smooth Tool and Live curve fitting (the Paintbrush + // has neither option). + (&["pencil"], None, &["altSmooth", "liveFit"]), (&["smooth"], None, &["fidelity"]), (&["blobBrush", "eraser"], None, &["size"]), (&["blobBrush"], None, &["keepSelected", "mergeOnlySelection"]), diff --git a/crates/ui-egui/src/dialogs/freehand.rs b/crates/ui-egui/src/dialogs/freehand.rs index 50e4d2c4c..aa747a111 100644 --- a/crates/ui-egui/src/dialogs/freehand.rs +++ b/crates/ui-egui/src/dialogs/freehand.rs @@ -1,6 +1,7 @@ //! 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}`. +//! preferences. OK sets them with `tool.setOption {tool, values}`. The Pencil also has `liveFit` +//! (Live curve fitting, on: its preview fits the stroke while it is drawn). //! //! Fields: `tool`; `fidelity` (points; Pencil, Paintbrush and Smooth), `fill` (Pencil and //! Paintbrush), `keepSelected` (Pencil, Paintbrush and Blob Brush), `mergeOnlySelection` (Blob @@ -24,7 +25,7 @@ pub(super) const SPEC: DialogSpec = DialogSpec { heading, body, confirm, min_wid 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; 8] = ["fidelity", "size", "fill", "altSmooth", "keepSelected", "mergeOnlySelection", "closeWithin", "editWithin"]; +const KEYS: [&str; 9] = ["fidelity", "size", "fill", "altSmooth", "keepSelected", "mergeOnlySelection", "closeWithin", "editWithin", "liveFit"]; /// Width of the label column: wide enough for the tolerances' labels when there are any. fn label_w(d: &Dialog) -> f32 { @@ -117,6 +118,10 @@ fn body(_app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { if d.fields.contains_key("editWithin") { number(ui, d, ("editWithin", tl!("Edit selected paths within:")), "px", 0, (0.0, 100.0)); } + if d.fields.contains_key("liveFit") { + ui.add_space(6.0); + check(ui, d, "liveFit", tl!("Live curve fitting")); + } false } @@ -209,6 +214,55 @@ mod tests { } } + /// Live curve fitting (the Pencil's, on by default): on, the path shown while a stroke is + /// drawn is fitted; off, it joins the stroke's points with straight segments, and the release + /// fits it as with the option on. + #[test] + fn live_curve_fitting_off_shows_the_points_until_the_release_fits_them() { + let mut app = app(); + // The anchors of the newest path while a wave around height `y0` is drawn and after the + // release, with nothing selected (a stroke starting at the end of a selected path would + // continue it). + let draw = |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); + let newest = |app: &VectorcraftApp| { + let doc = &app.session.doc().unwrap().doc; + doc.layers[0].children().unwrap().last().unwrap().path_data().unwrap().subpaths[0].anchors.clone() + }; + 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); + } + let drawing = newest(app); + crate::canvas::dispatch(app, &pointer(vectorcraft_tools::PointerKind::Up, pts[40]), view); + (drawing, newest(app)) + }; + let corners = |anchors: &[vectorcraft_geom::Anchor]| anchors.iter().all(|a| a.h_in == a.p && a.h_out == a.p); + // On by default: the Pencil's dialog shows it and the Paintbrush's does not. + app.run("tool.options", json!({"tool": "pencil"})).unwrap(); + assert!(painted(&mut app).contains("Live curve fitting")); + assert!(app.ui.dialog.as_ref().unwrap().bool("liveFit")); + super::super::confirm(&mut app).unwrap(); + let (drawing, on) = draw(&mut app, 100.0); + assert!(!corners(&drawing) && drawing.len() < 20, "fitted while drawn: {} anchors", drawing.len()); + // Off: the points while the stroke is drawn, fitted at the release. + app.run("tool.options", json!({"tool": "pencil"})).unwrap(); + app.ui.dialog.as_mut().unwrap().fields.insert("liveFit".into(), json!(false)); + super::super::confirm(&mut app).unwrap(); + app.select_tool("selection"); + assert_eq!(app.session.tool_options_of("pencil")["liveFit"].as_bool(), Some(false), "it lasts while another tool is chosen"); + let (drawing, off) = draw(&mut app, 200.0); + assert!(corners(&drawing) && drawing.len() > 30, "the points while drawn: {} anchors", drawing.len()); + assert_eq!(off.len(), on.len(), "the release fits the stroke as with the option on"); + assert!(off.iter().zip(&on).all(|(a, b)| (a.p.x - b.p.x).abs() < 1e-9 && (a.p.y - b.p.y - 100.0).abs() < 1e-9), "{off:?}"); + app.run("tool.options", json!({"tool": "paintbrush"})).unwrap(); + assert!(!painted(&mut app).contains("Live curve fitting")); + } + #[test] fn a_new_fidelity_changes_what_the_pencil_draws() { let mut app = app(); diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index a39605ff1..38e44a75b 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -1089,6 +1089,7 @@ font-technology Variable Променливи Fill and Stroke Запълване и щрих Fill new strokes Запълване на нови щрихове Alt key toggles to Smooth Tool Клавишът Alt превключва към инструмент „Гладкост“ + Live curve fitting Напасване на кривата в реално време Fill Color Цвят на запълване Fill New Type Objects With Placeholder Text Запълване на нови текстови обекти с примесен текст Fill Rule Правило за запълване diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index ded761f51..19b5fe18e 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -2296,6 +2296,7 @@ lighting Light Světlo Fidelity: Věrnost: Fill new strokes Vyplňovat nové tahy Alt key toggles to Smooth Tool Klávesa Alt přepíná na nástroj Vyhlazení + Live curve fitting Průběžné prokládání křivky Keep selected Ponechat vybrané Merge only with selection Sloučit jen s výběrem Consider Open Filled Paths as Closed Považovat otevřené vyplněné cesty za uzavřené diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index 64e77487e..ee9b566af 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -2106,6 +2106,7 @@ lighting Light Lys Fidelity: Præcision: Fill new strokes Udfyld nye konturer Alt key toggles to Smooth Tool Alt-tasten skifter til Udglatningsværktøj + Live curve fitting Kurvetilpasning undervejs The document has no colour swatches. Dokumentet har ingen farveprøver. Style Name: Typografinavn: Crop To Beskær til diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index a036d1022..e600c2487 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -1086,6 +1086,7 @@ font-technology Variable Variabel Fill and Stroke Fläche und Kontur Fill new strokes Neue Striche füllen Alt key toggles to Smooth Tool Alt-Taste schaltet auf Glätten-Werkzeug um + Live curve fitting Kurvenanpassung beim Zeichnen Keep selected Ausgewählt lassen Merge only with selection Nur mit Auswahl zusammenfügen Consider Open Filled Paths as Closed Offene gefüllte Pfade als geschlossen behandeln diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index c7a68ee82..8555dc81b 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -1055,6 +1055,7 @@ font-technology Variable Μεταβλητές Fill and Stroke Γέμισμα και περίγραμμα Fill new strokes Γέμισμα νέων περιγραμμάτων Alt key toggles to Smooth Tool Το πλήκτρο Alt αλλάζει στο εργαλείο Εξομάλυνση (Smooth Tool) + Live curve fitting Προσαρμογή καμπύλης σε πραγματικό χρόνο Fill Color Χρώμα γεμίσματος Fill New Type Objects With Placeholder Text Συμπλήρωση νέων αντικειμένων κειμένου με κείμενο υπόδειξης Fill Rule Κανόνας γεμίσματος diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 4e4704949..7aa2dd5bb 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -1109,6 +1109,7 @@ font-technology Variable Variables Fill and Stroke Relleno y trazo Fill new strokes Rellenar trazos nuevos Alt key toggles to Smooth Tool La tecla Alt cambia a la herramienta Suavizar + Live curve fitting Ajuste de curva en tiempo real Keep selected Mantener seleccionado Merge only with selection Combinar solo con la selección Consider Open Filled Paths as Closed Tratar los trazados abiertos con relleno como cerrados diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 016b8a7b5..52ab6f4e2 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -2253,6 +2253,7 @@ lighting Light Valo Fidelity: Tarkkuus: Fill new strokes Täytä uudet viivat Alt key toggles to Smooth Tool Alt-näppäin vaihtaa Tasoitustyökaluun + Live curve fitting Käyrän sovitus piirrettäessä The document has no colour swatches. Asiakirjassa ei ole värimalleja. Style Name: Tyylin nimi: Crop To Rajaa kohteeseen diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 4d99c7e07..42ff46812 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -1109,6 +1109,7 @@ font-technology Variable Variables Fill and Stroke Fond et contour Fill new strokes Remplir les nouveaux contours Alt key toggles to Smooth Tool La touche Alt bascule sur l’outil Arrondi + Live curve fitting Ajustement de la courbe en direct Keep selected Garder sélectionné Merge only with selection Fusionner uniquement avec la sélection Consider Open Filled Paths as Closed Traiter les tracés ouverts avec fond comme fermés diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 3473988cc..2a6456d9f 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -1395,6 +1395,7 @@ font-technology Variable Változó Fill Color Kitöltőszín Fill new strokes Új vonások kitöltése Alt key toggles to Smooth Tool Az Alt billentyű a Simítás eszközre vált + Live curve fitting Görbeillesztés rajzolás közben Keep selected Kijelölés megtartása Merge only with selection Összeolvasztás csak a kijelöléssel Consider Open Filled Paths as Closed A nyitott kitöltött útvonalak zártnak számítanak diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 500fd22e3..d268a8d6d 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -2106,6 +2106,7 @@ lighting Light Cahaya Fidelity: Fidelitas: Fill new strokes Isi stroke baru Alt key toggles to Smooth Tool Tombol Alt beralih ke Alat penghalus + Live curve fitting Penyesuaian kurva langsung The document has no colour swatches. Dokumen tidak memiliki swatch warna. Style Name: Nama gaya: Crop To Pangkas ke diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 35482e90d..bcd3656c2 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -1109,6 +1109,7 @@ font-technology Variable Variabili Fill and Stroke Riempimento e traccia Fill new strokes Riempi nuove tracce Alt key toggles to Smooth Tool Il tasto Alt attiva lo strumento Smussa + Live curve fitting Adattamento della curva in tempo reale Keep selected Mantieni selezionato Merge only with selection Unisci solo con la selezione Consider Open Filled Paths as Closed Considera chiusi i tracciati aperti con riempimento diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index acd2b776b..bf0852105 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -1513,6 +1513,7 @@ font-technology Variable バリアブル Fill and Stroke 塗りと線 Fill new strokes 新しいストロークに塗りを適用 Alt key toggles to Smooth Tool Alt キーでスムーズツールに切り替え + Live curve fitting 描画中にカーブを調整 Keep selected 選択を保持 Merge only with selection 選択範囲とのみ結合 Consider Open Filled Paths as Closed 塗りのある開いたパスを閉じたパスとみなす diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index fcf12d546..0355075d4 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -5329,6 +5329,7 @@ imageTrace Outline კონტური Light/Dark Balance ნათელი/მუქი ბალანსი Move Anchor Point საყრდენი წერტილის გადაადგილება Alt key toggles to Smooth Tool Alt კლავიში ჩართავს გაგლუვების ხელსაწყოს + Live curve fitting მრუდის მორგება ხატვისას @msg {label} keeps 1 of the document's {n} artboards: the file reopens with one. Turn on Preserve Editing Capabilities to keep them all. {label} ინახავს დოკუმენტის {n} სამუშაო დაფიდან 1-ს: ფაილი ხელახლა ერთით გაიხსნება. ყველას შესანარჩუნებლად ჩართეთ „რედაქტირების შესაძლებლობების შენარჩუნება“. Chalk & Charcoal… ცარცი და ნახშირი… Chalk & Charcoal ცარცი და ნახშირი diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 465a7eff7..ffbff3f16 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -1508,6 +1508,7 @@ font-technology Variable 가변 Fill and Stroke 칠과 선 Fill new strokes 새 선 칠하기 Alt key toggles to Smooth Tool Alt 키로 매끄럽게 도구로 전환 + Live curve fitting 실시간 곡선 맞춤 Keep selected 선택 유지 Merge only with selection 선택 영역과만 병합 Consider Open Filled Paths as Closed 칠이 있는 열린 패스를 닫힌 패스로 처리 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index 5e60c58fa..ab0495dbe 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -2120,6 +2120,7 @@ lighting Light Licht Fidelity: Nauwkeurigheid: Fill new strokes Nieuwe lijnen vullen Alt key toggles to Smooth Tool Alt-toets schakelt over naar Gladmaken-gereedschap + Live curve fitting Curve aanpassen tijdens het tekenen The document has no colour swatches. Het document heeft geen kleurstalen. Style Name: Stijlnaam: Crop To Bijsnijden tot diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 078c6eb0d..cb371fb37 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -2211,6 +2211,7 @@ lighting Light Światło Fidelity: Wierność: Fill new strokes Wypełniaj nowe pociągnięcia Alt key toggles to Smooth Tool Klawisz Alt przełącza na narzędzie Wygładzanie + Live curve fitting Dopasowywanie krzywej podczas rysowania Keep selected Zachowaj zaznaczenie Merge only with selection Scalaj tylko z zaznaczeniem Consider Open Filled Paths as Closed Traktuj otwarte wypełnione ścieżki jako zamknięte diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 8415173b4..13d3ea4a3 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -986,6 +986,7 @@ Fill and Stroke Preenchimento e Traçado Fill new strokes Preencher novos traçados Alt key toggles to Smooth Tool A tecla Alt alterna para a ferramenta Suavizar + Live curve fitting Ajuste de curva em tempo real Keep selected Manter selecionado Merge only with selection Mesclar somente com a seleção Consider Open Filled Paths as Closed Tratar caminhos abertos preenchidos como fechados diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 136fc6868..5ff84988d 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -1107,6 +1107,7 @@ font-technology Variable Вариативные Fill and Stroke Заливка и обводка Fill new strokes Заполнять новые обводки Alt key toggles to Smooth Tool Клавиша Alt переключает на инструмент «Сглаживание» + Live curve fitting Подгонка кривой во время рисования Keep selected Оставлять выбранным Merge only with selection Объединять только с выделенным Consider Open Filled Paths as Closed Считать открытые контуры с заливкой замкнутыми diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 3e10de425..84924739c 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -1091,6 +1091,7 @@ font-technology Variable Variabla Fill and Stroke Fyllning och kontur Fill new strokes Fyll nya konturer Alt key toggles to Smooth Tool Alt-tangenten växlar till Utjämningsverktyg + Live curve fitting Kurvanpassning medan du ritar Fill Color Fyllnadsfärg Fill New Type Objects With Placeholder Text Fyll nya textobjekt med platshållartext Fill Rule Fyllningsregel diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 6384ae068..82fac9235 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -5220,6 +5220,7 @@ theme Appearance Görünüm @msg {_1} is an exporter (File › Export › Export As…) {_1} bir dışa aktarma eklentisidir (Dosya › Dışa Aktar › Farklı Dışa Aktar…) @msg `documentData` is {size} bytes (limit {MAX_DOCUMENT_DATA}) `documentData` {size} bayt (sınır {MAX_DOCUMENT_DATA}) Alt key toggles to Smooth Tool Alt tuşu Düzleştirme Aracına geçiş yapar + Live curve fitting Çizerken eğri uydurma Auto-Simplify Otomatik Basitleştir Drag an item here, or right-click one, to put it in this group. Bu gruba eklemek için bir öğeyi buraya sürükleyin veya sağ tıklayın. Drop here to take it out of its group Grup dışına çıkarmak için buraya bırakın diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 3b61d4e17..e05fd2189 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -1103,6 +1103,7 @@ font-technology Variable Варіативні Fill and Stroke Заливка та обведення Fill new strokes Заповнювати нові обведення Alt key toggles to Smooth Tool Клавіша Alt перемикає на інструмент «Згладжування» + Live curve fitting Підгонка кривої під час малювання Keep selected Залишати вибраним Merge only with selection Об’єднувати лише з виділеним Consider Open Filled Paths as Closed Вважати відкриті контури із заливкою замкненими diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 4a6ae2137..f0ff4a3db 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -1272,6 +1272,7 @@ font-technology Variable Biến thiên Fill Grid Tô lưới Fill new strokes Tô nét mới Alt key toggles to Smooth Tool Phím Alt chuyển sang công cụ Làm mượt + Live curve fitting Khớp đường cong khi vẽ Keep selected Giữ trạng thái đã chọn Merge only with selection Chỉ hợp nhất với vùng chọn Consider Open Filled Paths as Closed Coi đường dẫn mở có tô là đường dẫn đóng diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index ed0f434aa..1ddbf08fe 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -1048,6 +1048,7 @@ Fill and Stroke 填色和描边 Fill new strokes 为新描边应用填色 Alt key toggles to Smooth Tool Alt 键切换为平滑工具 + Live curve fitting 实时曲线拟合 Keep selected 保持选择状态 Merge only with selection 仅与所选对象合并 Consider Open Filled Paths as Closed 将有填色的开放路径视为闭合路径 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index d64dc9029..6b804a813 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -1074,6 +1074,7 @@ font-technology Variable 可變 Fill Color 填色顏色 Fill new strokes 為新筆畫套用填色 Alt key toggles to Smooth Tool Alt 鍵切換為平滑工具 + Live curve fitting 即時曲線擬合 Keep selected 保持選取狀態 Merge only with selection 僅與選取物件合併 Consider Open Filled Paths as Closed 將有填色的開放路徑視為封閉路徑 diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 70e014745..3724d7210 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -674,6 +674,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur In the Swatches panel, Alt-dragging a swatch onto another swatch of its kind (not a tint swatch) replaces that swatch's color, gradient or pattern, and the swatch keeps its name and place. - **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; Move opens on the offset of the last drag or Move and with the checkboxes it was last confirmed with, so Enter, Enter repeats a move or a copy at the same spacing, as [an Adobe Community thread](https://community.adobe.com/questions-652/move-tool-771221) describes), Gradient, Eyedropper, Blend and the Liquify tools their panels or options, the Pencil, Paintbrush, Smooth, Blob Brush and Eraser tools their Tool Options (Fidelity, fill, Keep selected, the tolerances, the brush size), and (#812) the Flare tool its Flare Tool Options, the Artboard tool the active artboard's Artboard Options, the graph tools Graph Type for the selected graph, the Symbolism tools the Symbolism Tools Options (Diameter, Intensity, Symbol Set Density) and the Magic Wand tool its panel. Double-clicking the Line Segment, Arc, Rectangular Grid or Polar Grid tool opens its Tool Options, as step 3 of the [Rectangular Grid tool](https://helpx.adobe.com/illustrator/using/tool-techniques/rectangular-grid-tool.html) and [Arc tool](https://helpx.adobe.com/illustrator/using/tool-techniques/arc-tool.html) pages and the [Tools](https://helpx.adobe.com/illustrator/using/tools.html) page describe (#2691): the dialog a click with the tool opens, without a place to draw. Line Segment Tool Options has Length, Angle and Fill Line (a new line takes the current fill, `shape.line` `fill`); Arc Segment Tool Options its lengths, Reference Point, Type, Base Along, Slope and Fill Arc; the grids' Tool Options the Reference Point, Default Size, dividers, skews and switches. OK draws nothing: the next drag draws with the options and a click's dialog opens at them, the Length, Angle and Default Size included; Cancel changes nothing. Tests: `toolbar::tests::double_clicking_the_line_arc_and_grid_tools_opens_their_tool_options`, `line_segment_options_and_fill_line`, `grid_default_size_is_an_option_the_drag_leaves_out`, `fill_line_gives_a_new_line_the_current_fill`. + Live curve fitting in the Pencil Tool Options is on by default; while it is off, the path shown while a stroke is drawn joins the stroke's points with straight segments, and the release fits the curve, as [Pencil Tool Options](https://web.archive.org/web/20261002104400/https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/draw-shapes/pencil-tool-options.html) describes (`path.freehand` `fit`). In the Pencil and Paintbrush Tool Options, Edit selected paths within 0 px turns editing off, so a stroke that starts exactly on an end of a selected open path draws a new path; Close paths when ends are within 0 px turns closing off, so a stroke that ends exactly where it started stays open. Keep selected in the Pencil and Paintbrush Tool Options is on by default; while it is off, the path a stroke draws or continues is not selected afterwards and the rest of the selection stays as it was (`path.freehand` `keepSelected`). Double-clicking the Shape Builder tool opens Shape Builder Tool Options ([Shape Builder tool options](https://web.archive.org/web/20260105172244/https://helpx.adobe.com/illustrator/using/building-new-shapes-using-shape.html)): with Consider Open Filled Paths as Closed off, a filled open path cuts regions as an unfilled one does; Pick Color From Color Swatches fills merged shapes with the current fill, the Left and Right arrow keys step it through the document's color swatches, and Cursor Swatch Preview shows the current fill and the swatches before and after it above the pointer; Selection Straight Line merges what the line from a drag's first point to its last crosses ([Setting Illustrator's Shape Builder Options](https://web.archive.org/web/20260615204453/https://creativepro.com/setting-illustrators-shape-builder-options/)); with Highlight Fill off, merge mode shades no regions (`shapeBuilder.merge` `openFilledAsClosed` and `pickColorFrom`).