Merge pull request #4929 from storytold/fix/smooth-parity

Smooth: Auto-Smooth takes a shaky line to one curve, and direct-selected anchors smooth only their section
This commit is contained in:
BFlat
2026-10-31 23:28:24 +07:00
committed by GitHub
10 changed files with 179 additions and 35 deletions
+4 -1
View File
@@ -45,7 +45,10 @@ fn fit_freehand(pts: &[Point], tol: f64, closed: bool) -> SubPath {
if sp.anchors.len() < 3 {
return sp;
}
let fitted = po::simplify_with(&PathData::single(sp.clone()), &SimplifyOptions { tolerance: tol, corner_angle_deg: 60.0, straight_lines: false });
let fitted = po::simplify_with(
&PathData::single(sp.clone()),
&SimplifyOptions { tolerance: tol, corner_angle_deg: 60.0, straight_lines: false, ..Default::default() },
);
fitted.subpaths.into_iter().next().filter(|s| s.anchors.len() >= 2).unwrap_or(sp)
}
+5 -3
View File
@@ -9,7 +9,7 @@ use super::{
/// Refit the anchors of an open run `a[lo..=hi]` keeping its end points and outer handles.
fn refit_run(anchors: &[Anchor], tol: f64) -> Vec<Anchor> {
let piece = PathData::single(SubPath::new(anchors.to_vec(), false));
let fitted = po::simplify_with(&piece, &SimplifyOptions { tolerance: tol, corner_angle_deg: 179.0, straight_lines: false });
let fitted = po::simplify_with(&piece, &SimplifyOptions { tolerance: tol, corner_angle_deg: 179.0, straight_lines: false, ..Default::default() });
let Some(mut out) = fitted.subpaths.into_iter().next().map(|s| s.anchors).filter(|a| a.len() >= 2) else { return anchors.to_vec() };
let (f, l) = (anchors[0], anchors[anchors.len() - 1]);
let last = out.len() - 1;
@@ -32,8 +32,10 @@ fn smooth_subpath(sp: &SubPath, near: &dyn Fn(Point) -> bool, tol: f64) -> Optio
if sp.closed {
let Some(k) = marked.iter().position(|m| !m) else {
// Everything is in range: smooth the whole closed path.
let f =
po::simplify_with(&PathData::single(sp.clone()), &SimplifyOptions { tolerance: tol, corner_angle_deg: 179.0, straight_lines: false });
let f = po::simplify_with(
&PathData::single(sp.clone()),
&SimplifyOptions { tolerance: tol, corner_angle_deg: 179.0, straight_lines: false, ..Default::default() },
);
return f.subpaths.into_iter().next();
};
// Rotate so an untouched anchor comes first, duplicate it at the end and treat as open.
+29 -9
View File
@@ -161,7 +161,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Smooth…",
["Object", "Path"],
None,
"{level?: 0..100 (50; the Smooth slider: 0 minimum smoothing, the path staying within 0.1% of its longer side, 100 maximum: refitted with curves within up to 2.5% of it, corners kept only where the path turns back, never with more anchor points), auto?: bool (Auto-Smooth: level 50), ids?: [id…] (default: the selection)} smooth each path under the objects, compound path members included, as one undo step → {ids, before, after} anchor counts, and the level used",
"{level?: 0..100 (50; the Smooth slider: 0 minimum smoothing, the path staying within 0.1% of its longer side, 100 maximum: smoothed along its length by up to 5% of it and refitted with curves within up to 2.5% of it, corners kept only where the path turns back, never with more anchor points), auto?: bool (Auto-Smooth: level 50), ids?: [id…] (default: the selection)} smooth each path under the objects, compound path members included, as one undo step; without ids, a path with anchor points or segments direct-selected smooths only the sections between them, which stay selected → {ids, before, after} anchor counts, and the level used",
has_selection,
smooth
),
@@ -1089,6 +1089,7 @@ fn simplify(s: &mut Session, p: &Value) -> Result<Value> {
tolerance: tolerance.clamp(1e-4, 1e4),
corner_angle_deg: num_param(p, "cornerAngle").unwrap_or(30.0).clamp(0.0, 180.0),
straight_lines: bool_or(p, "straightLines", false),
smooth: 0.0,
};
let whole = bool_or(p, "wholePaths", false);
// A path with segments direct-selected simplifies only their runs, and keeps those runs (and the
@@ -1099,13 +1100,7 @@ fn simplify(s: &mut Session, p: &Value) -> Result<Value> {
return (po::simplify_with(path, &opts), None);
};
let part = po::simplify_part(path, &picked, &opts);
let mut kept: BTreeSet<AnchorRef> = anchors.into_iter().flatten().filter_map(|a| part.anchors.get(a).copied()).collect();
for &(si, i) in &part.segments {
let m = part.path.subpaths.get(si).map_or(1, |sp| sp.anchors.len().max(1));
kept.extend([(si, i), (si, (i + 1) % m)]);
}
let segs = segments.map(|_| part.segments.clone());
(part.path, Some((kept, segs)))
refitted_part(part, anchors, segments)
})?;
let mut out = json!({ "ids": ids.iter().map(|i| i.0).collect::<Vec<_>>(), "before": before, "after": after });
if let Some(l) = level {
@@ -1114,14 +1109,39 @@ fn simplify(s: &mut Session, p: &Value) -> Result<Value> {
Ok(out)
}
/// A path with only the runs of `part` refitted, and what stays direct-selected: the anchors it had
/// selected that are still there, and the refitted runs (their anchors, and their segments when
/// segments were selected).
fn refitted_part(
part: po::SimplifiedPart,
anchors: Option<&BTreeSet<AnchorRef>>,
segments: Option<&BTreeSet<SegmentRef>>,
) -> (PathData, PartSelection) {
let mut kept: BTreeSet<AnchorRef> = anchors.into_iter().flatten().filter_map(|a| part.anchors.get(a).copied()).collect();
for &(si, i) in &part.segments {
let m = part.path.subpaths.get(si).map_or(1, |sp| sp.anchors.len().max(1));
kept.extend([(si, i), (si, (i + 1) % m)]);
}
let segs = segments.map(|_| part.segments.clone());
(part.path, Some((kept, segs)))
}
/// Object › Path › Smooth (`object.path.smooth`): [`po::smooth_level`] on each path under the
/// objects given or selected, as one undo step; Auto-Smooth (`auto`) uses [`po::SMOOTH_AUTO_LEVEL`].
/// A path with anchor points or segments direct-selected (and no `ids` given) smooths only the runs
/// between them, which stay selected, as Simplify does.
fn smooth(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "object.path.smooth";
let level = number_or(p, "level", po::SMOOTH_AUTO_LEVEL, C)?.round().clamp(0.0, 100.0);
let level = if switch_or(p, "auto", false, C)? { po::SMOOTH_AUTO_LEVEL } else { level };
let given = p.get("ids").is_some_and(|v| !v.is_null());
let roots = checked_ids_or_selection(s, p, C)?;
let (ids, before, after) = edit_paths(s, &roots, "Smooth", |path, _, _| (po::smooth_level(path, level), None))?;
let (ids, before, after) = edit_paths(s, &roots, "Smooth", |path, anchors, segments| {
let Some(picked) = selected_segments(path, anchors, segments).filter(|_| !given) else {
return (po::smooth_level(path, level), None);
};
refitted_part(po::simplify_part(path, &picked, &po::smooth_options(path, level)), anchors, segments)
})?;
Ok(json!({ "ids": ids.iter().map(|i| i.0).collect::<Vec<_>>(), "before": before, "after": after, "level": level }))
}
+53
View File
@@ -161,3 +161,56 @@ fn bad_params_are_refused_and_change_nothing() {
// Levels past the ends count as the ends.
assert_eq!(s.execute("object.path.smooth", &json!({"level": 250})).unwrap()["level"], json!(100.0));
}
/// A wave 360 pt long with an anchor every 9 pt pushed up to 6 pt off it, as a shaky hand draws.
fn shaky_wave(s: &mut Session) -> NodeId {
let pts: Vec<(f64, f64)> =
(0..=40u32).map(|i| (20.0 + f64::from(i) * 9.0, 150.0 + 60.0 * (f64::from(i) / 6.0).sin() + f64::from((i * 37) % 13) - 6.0)).collect();
polyline(s, &pts, false)
}
/// The largest turn (degrees) at any anchor between two segments of `path`: a kink when large.
fn sharpest_turn(path: &PathData) -> f64 {
path.subpaths
.iter()
.flat_map(|sp| (1..sp.segment_count()).map(move |i| (sp.segment(i - 1), sp.segment(i))))
.map(|(a, b)| {
let (out, inn) = ((a.p3 - a.p2).normalize(), (b.p1 - b.p0).normalize());
out.dot(inn).clamp(-1.0, 1.0).acos().to_degrees()
})
.fold(0.0, f64::max)
}
/// Auto-Smooth takes a shaky line to one smooth curve: no kinks left at its anchors, far fewer of
/// them, and the wave still where it was.
#[test]
fn auto_smooth_leaves_no_jags_in_a_shaky_line() {
let mut s = session();
let id = shaky_wave(&mut s);
let before = path_of(&s, id);
s.execute("object.path.smooth", &json!({"auto": true})).unwrap();
let after = path_of(&s, id);
assert!(sharpest_turn(&after) < 12.0, "a kink of {}°", sharpest_turn(&after));
assert!(after.anchor_count() <= before.anchor_count() / 3, "{} anchors", after.anchor_count());
assert!(farthest(&after, &before) < 8.0, "{}", farthest(&after, &before));
}
/// With anchor points of a path direct-selected, Smooth changes only the section between them: the
/// anchors outside it stay exactly where they were, and the section is smoothed and stays selected.
#[test]
fn direct_selected_anchors_smooth_only_their_section() {
let mut s = session();
let id = shaky_wave(&mut s);
let before = path_of(&s, id);
let anchors: Vec<Value> = (10..=30).map(|i| json!([0, i])).collect();
s.execute("select.anchors", &json!({"id": id.0, "anchors": anchors})).unwrap();
s.execute("object.path.smooth", &json!({"level": 100})).unwrap();
let after = path_of(&s, id);
let (a, b) = (&before.subpaths[0].anchors, &after.subpaths[0].anchors);
assert!(b.len() < a.len(), "{} → {}", a.len(), b.len());
// Anchors 0..=10 and 30..=40 are untouched, the section between them refitted.
assert_eq!(a[..=10].iter().map(|x| x.p).collect::<Vec<_>>(), b[..=10].iter().map(|x| x.p).collect::<Vec<_>>());
assert_eq!(a[30..].iter().map(|x| x.p).collect::<Vec<_>>(), b[b.len() - 11..].iter().map(|x| x.p).collect::<Vec<_>>());
let sel = &s.doc().unwrap().selection;
assert!(sel.anchors.get(&id).is_some_and(|set| !set.is_empty()), "the section stays direct-selected");
}
+76 -14
View File
@@ -16,13 +16,16 @@ pub struct SimplifyOptions {
pub tolerance: f64,
/// Anchors where the path turns by more than this many degrees stay corners.
pub corner_angle_deg: f64,
/// Produce only straight segments (Illustrator's "Convert to Straight Lines").
/// Produce only straight segments ("Convert to Straight Lines").
pub straight_lines: bool,
/// How far (points, the spread of a Gaussian along the path) the path is smoothed before it is
/// refitted, so that bumps and jags come out as one curve ([`smooth_level`]); 0 for none.
pub smooth: f64,
}
impl Default for SimplifyOptions {
fn default() -> Self {
Self { tolerance: 0.5, corner_angle_deg: 30.0, straight_lines: false }
Self { tolerance: 0.5, corner_angle_deg: 30.0, straight_lines: false, smooth: 0.0 }
}
}
@@ -116,6 +119,9 @@ fn simplify_run(run: &[Seg], opts: &SimplifyOptions) -> Vec<Seg> {
let m = ((len / (tol * 0.5)).ceil() as usize).clamp(4, 256);
sample(&s.c, m, &mut pts, k == 0);
}
if opts.smooth > 0.0 {
pts = smooth_points(&pts, opts.smooth);
}
if opts.straight_lines {
let keep = rdp(&pts, tol);
result.extend(keep.windows(2).map(|p| Seg::line(p[0], p[1])));
@@ -131,6 +137,48 @@ fn simplify_run(run: &[Seg], opts: &SimplifyOptions) -> Vec<Seg> {
result
}
/// `pts` (a run's samples, in order) smoothed along the path: each point becomes the Gaussian-weighted
/// mean of the points around it, `sigma` points of arc length wide, the window narrowing towards the
/// ends so that the first and last points stay where they are. Most points looked at per point is
/// bounded by the samples within three spreads of it.
fn smooth_points(pts: &[Point], sigma: f64) -> Vec<Point> {
if pts.len() < 3 || !(sigma.is_finite() && sigma > 0.0) {
return pts.to_vec();
}
let mut s = Vec::with_capacity(pts.len());
let mut total = 0.0;
for (i, p) in pts.iter().enumerate() {
if let Some(prev) = i.checked_sub(1).and_then(|j| pts.get(j)) {
total += prev.distance(*p);
}
s.push(total);
}
let (mut lo, mut hi) = (0usize, 0usize);
pts.iter()
.zip(&s)
.map(|(&p, &si)| {
let reach = (3.0 * sigma).min(si).min(total - si);
if reach <= 1e-9 {
return p;
}
let spread = reach / 3.0;
while s.get(lo).is_some_and(|&v| v < si - reach) {
lo += 1;
}
while s.get(hi + 1).is_some_and(|&v| v <= si + reach) {
hi += 1;
}
let (mut sum, mut weight) = (Vec2::ZERO, 0.0);
for (q, &sj) in pts.iter().zip(&s).take(hi + 1).skip(lo) {
let w = (-(sj - si) * (sj - si) / (2.0 * spread * spread)).exp();
sum += q.to_vec2() * w;
weight += w;
}
if weight > 0.0 { (sum / weight).to_point() } else { p }
})
.collect()
}
/// What [`simplify_part`] made of a path.
#[derive(Clone, Debug, PartialEq)]
pub struct SimplifiedPart {
@@ -320,22 +368,36 @@ pub fn smooth(path: &PathData, amount: f64) -> PathData {
/// [`smooth_level`] already scales with each path's size, so one level suits every path.
pub const SMOOTH_AUTO_LEVEL: f64 = 50.0;
/// Object › Path › Smooth at slider level `level`, from 0 (minimum smoothing: the path stays within
/// 0.1 % of its longer side of where it was) to 100 (maximum). The help page names only the two
/// ends; the rest is VectorCraft's own rule. Each subpath is refitted with curves as Simplify does,
/// but keeping a corner only where the path turns back on itself (so jags and kinks become curves),
/// within a tolerance from 0.1 % of the path's longer side at level 0 to 2.5 % at level 100. A refit
/// never has more anchor points than the subpath had: a shape whose corners the tolerance can't
/// round, such as a rectangle, stays as it is. Closed subpaths stay closed, with no kink left where
/// they start unless rounding it would pass the tolerance, and open ones keep their end points. A level outside 0..=100 counts as the nearer end, and one that is not a number as
/// [`SMOOTH_AUTO_LEVEL`].
pub fn smooth_level(path: &PathData, level: f64) -> PathData {
/// The refit Object › Path › Smooth makes of `path` at slider level `level` ([`smooth_level`]): a
/// corner only where the path turns back on itself, the path smoothed along its length by up to 5 %
/// of its longer side (none at level 0) and refitted within 0.1 % (level 0) to 2.5 % (level 100) of
/// it. A level outside 0..=100 counts as the nearer end, and one that is not a number as
/// [`SMOOTH_AUTO_LEVEL`]; a path of no finite size gets no smoothing.
pub fn smooth_options(path: &PathData, level: f64) -> SimplifyOptions {
let level = if level.is_finite() { level.clamp(0.0, 100.0) } else { SMOOTH_AUTO_LEVEL };
let size = path.bounds().map_or(0.0, |b| b.width().max(b.height()));
if !size.is_finite() {
let size = if size.is_finite() { size } else { 0.0 };
SimplifyOptions {
tolerance: size * (0.001 + 0.024 * level / 100.0),
corner_angle_deg: SMOOTH_CORNER_DEG,
straight_lines: false,
smooth: size * 0.05 * level / 100.0,
}
}
/// Object › Path › Smooth at slider level `level`, from 0 (minimum smoothing: the path stays within
/// 0.1 % of its longer side of where it was) to 100 (maximum). The help page names only the two
/// ends; the rest is VectorCraft's own rule ([`smooth_options`]). Each subpath is smoothed along its
/// length, so that bumps and jags give way to one curve, then refitted with curves as Simplify
/// does, keeping a corner only where the path turns back on itself. A refit never has more anchor
/// points than the subpath had: a shape whose corners it can't round, such as a rectangle, stays as
/// it is. Closed subpaths stay closed, with no kink left where they start unless rounding it would
/// pass the tolerance, and open ones keep their end points.
pub fn smooth_level(path: &PathData, level: f64) -> PathData {
let opts = smooth_options(path, level);
if opts.tolerance <= 0.0 {
return path.clone();
}
let opts = SimplifyOptions { tolerance: size * (0.001 + 0.024 * level / 100.0), corner_angle_deg: SMOOTH_CORNER_DEG, straight_lines: false };
let smoothed = |sp: &SubPath| {
let Some(mut fit) = simplify_subpath(sp, &opts).filter(|f| f != sp) else { return sp.clone() };
smooth_seam(&mut fit, sp, opts.tolerance);
+2 -1
View File
@@ -22,7 +22,8 @@ pub use boolean::{
};
pub use edit::{
AverageAxis, SIMPLIFY_LEVEL, SMOOTH_AUTO_LEVEL, SimplifiedPart, SimplifyOptions, add_anchor_points, auto_simplify_level, average, join,
remove_anchor, remove_redundant_points, simplify, simplify_level_tolerance, simplify_part, simplify_with, smooth, smooth_level, split_into_grid,
remove_anchor, remove_redundant_points, simplify, simplify_level_tolerance, simplify_part, simplify_with, smooth, smooth_level, smooth_options,
split_into_grid,
};
pub use offset::{Cap, Join, offset_path, outline_stroke, stroke_region};
pub use pathfinder::{FaceMerger, PathfinderOp, Region, Shape, merge_regions, pathfinder, pathfinder_with, region_at, regions};
+2 -2
View File
@@ -19,8 +19,8 @@ fn edit_functions_survive_hostile_numbers() {
for paths in [square(), zigzag()] {
for &v in &HOSTILE {
let _ = simplify(&paths, v);
let _ = simplify_with(&paths, &SimplifyOptions { tolerance: v, corner_angle_deg: v, straight_lines: false });
let _ = simplify_with(&paths, &SimplifyOptions { tolerance: 0.5, corner_angle_deg: v, straight_lines: true });
let _ = simplify_with(&paths, &SimplifyOptions { tolerance: v, corner_angle_deg: v, straight_lines: false, ..Default::default() });
let _ = simplify_with(&paths, &SimplifyOptions { tolerance: 0.5, corner_angle_deg: v, straight_lines: true, ..Default::default() });
let _ = smooth(&paths, v);
let _ = smooth_level(&paths, v);
let _ = remove_redundant_points(&paths, v);
+4 -3
View File
@@ -2711,9 +2711,10 @@ the dialog on those settings next time.
`object.path.smooth` smooths the selected paths (or the objects in `ids`), compound path members included, as one undo
step. `level` is the Smooth slider's, a whole number from 0 (minimum smoothing: the path stays within 0.1% of its longer
side) to 100 (maximum smoothing); `auto: true` (Auto-Smooth) uses level 50, the default. Each path is refitted with
curves within 0.1% to 2.5% of its longer side, keeping a corner only where it turns back on itself, so jags and kinks
become curves; a refit never has more anchor points than the path had, so a shape whose corners the tolerance can't
side) to 100 (maximum smoothing); `auto: true` (Auto-Smooth) uses level 50, the default. Each path is smoothed along
its length (by up to 5% of its longer side; none at level 0) and refitted with curves within 0.1% to 2.5% of it,
keeping a corner only where it turns back on itself, so bumps, jags and kinks become one curve; with anchor points or
segments of a path direct-selected (and no `ids`), only the sections between them are smoothed, as Simplify does. A refit never has more anchor points than the path had, so a shape whose corners the tolerance can't
round, such as a rectangle, stays as it is. Closed paths stay closed and open paths keep their end points. The reply has the anchor counts `before` and `after` and the `level` used:
```json
+2 -1
View File
@@ -19,6 +19,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard
| Date | Change | Summary |
|---|---|---|
| 2026-10-31 | minor | Object › Path › Smooth smooths a path along its length before refitting it, so Auto-Smooth takes a shaky line to one smooth curve instead of leaving its jags, and with anchor points direct-selected it smooths only that section; scores unchanged. |
| 2026-10-31 | minor | New Document: More Settings has the Save Preset button beside the name too, saving its settings (artboard layout included) as a preset (#4893); scores unchanged. |
| 2026-10-31 | minor | Object › Rasterize in a CMYK document with its CMYK colour model makes a CMYK image of the art's ink amounts (as CMYK exports draw them), so the Properties panel says CMYK and CMYK exports keep its inks; a transparent background becomes the image's opacity mask (#4882); scores unchanged. |
| 2026-10-31 | minor | Export for Screens is laid out as Illustrator's: Artboards and Assets tabs with a grid or list of thumbnails, Clear Selection and Prefix under them, Create Sub-folders by Scale or Format (on by default), Export PDFs As Single File or Multiple Files (`pdfs`), iOS and Android rows, the format presets in a ☰ menu, and a Selected / files-to-export count; the naming pattern, Layers and Selection pages and folders list stay in `document.exportForScreens` for scripts and MCP; scores unchanged. |
@@ -831,7 +832,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
The Simplify dialog also has Corner Point Angle Threshold, Convert to Straight Lines and Show Original Path. The threshold is a slider from 0° to 180° and opens at 150°: an anchor whose corner is sharper than the threshold stays a corner, so moving the slider right keeps more corners, and the dialog sends `cornerAngle` as 180° less the threshold. While Preview is on, Show Original Path draws the paths as they were before the preview in a wide, faint band of their layer's color under the selection's outlines.
Object › Path › Simplify opens a slider under the selection ([Manually simplify paths](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/modify-paths/manually-simplify-paths.html), [Auto simplify paths](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/modify-paths/auto-simplify-paths.html)) with Auto-Simplify, a rail from the fewest anchor points to the closest fit, and More Options, which opens the dialog at the slider's tolerance. The rail's levels are VectorCraft's own scale (`object.path.simplify {level}`): level 50 is 1 pt, and every 10 levels halve or double the tolerance, from 32 pt at 0 to 1/32 pt at 100. Auto-Simplify picks the lowest level whose tolerance is at most 1/400 of the selection's longer side, or level 100 when that side is under 12.5 pt; the slider opens at that level, with its result previewed, as the manual-simplify page describes. The dialog's "Open this dialog with these settings next time" makes the menu item open the dialog on the settings of its last OK.
With anchors or segments of a path selected with Direct Selection, Simplify acts on just that part of the path, as the [Manually simplify paths](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/modify-paths/manually-simplify-paths.html) page puts it: the slider, Auto-Simplify and the dialog refit only each run of selected segments, between its end anchors, which keep their place and their handles on the side away from the run. The anchors and segments outside the runs stay exactly as they were, a closed path's run may cross its start point, separate runs on one path are refitted independently in one undo step, and the runs stay selected. A path with only lone anchors selected is still simplified whole, and `object.path.simplify {wholePaths: true}` simplifies partly selected paths whole. Tests: `crates/engine/src/tests/simplify_part.rs` and `simplify_slider_and_dialog_simplify_only_the_direct_selected_part_of_a_path`.
Object › Path › Smooth… (#4115) opens a slider under the selection from Minimum to Maximum smoothing with an Auto-Smooth button, as the [Smooth slider](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/modify-paths/refine-path-segments-with-the-smooth-slider.html) page describes; the canvas previews it and Enter or a click outside keeps it as one undo step (`object.path.smooth {level, auto, ids}`). The page names only the ends, so the rest is VectorCraft's own rule: each selected path, compound path members included, is refitted with curves within 0.1% (level 0) to 2.5% (level 100) of its longer side, corners kept only where it turns back and never with more anchors (a rectangle stays as it is); Auto-Smooth is level 50. Closed paths stay closed and open ones keep their end points. Tests: `crates/engine/src/tests/smooth_path.rs` and `smooth_slider_previews_auto_smooth_and_keeps_the_result`.
Object › Path › Smooth… (#4115) opens a slider under the selection from Minimum to Maximum smoothing with an Auto-Smooth button, as the [Smooth slider](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/modify-paths/refine-path-segments-with-the-smooth-slider.html) page describes; the canvas previews it and Enter or a click outside keeps it as one undo step (`object.path.smooth {level, auto, ids}`). The page names only the ends, so the rest is VectorCraft's own rule: each selected path, compound path members included, is smoothed along its length (by up to 5% of its longer side, so a shaky line comes out as one curve at Auto-Smooth) and refitted with curves within 0.1% (level 0) to 2.5% (level 100) of it, corners kept only where it turns back and never with more anchors (a rectangle stays as it is); Auto-Smooth is level 50. With anchor points of a path direct-selected, only the section between them is smoothed. Closed paths stay closed and open ones keep their end points. Tests: `crates/engine/src/tests/smooth_path.rs` and `smooth_slider_previews_auto_smooth_and_keeps_the_result`.
- **Devanagari shaping:** with an OpenType Devanagari font, Ka, virama and Ssa (क्ष) form one conjunct glyph, Ra and virama before a consonant (र्क) become a reph drawn after it, the i matra (कि) is drawn before its consonant, and Ka with a nukta is drawn like the precomposed क़ (U+0958), as [Microsoft's Devanagari shaping specification](https://learn.microsoft.com/en-us/typography/script-development/devanagari) describes. Each syllable is one cluster of its source bytes, and the arrow keys stop at its ends. A click inside a syllable drawn with several glyphs puts the caret at the syllable's nearer end, in point, area and path type. Tests: `crates/text/src/tests/devanagari.rs`; they print SKIPPED when no such font is installed.
- **Vertical type spacing shortcuts:** The [Default keyboard shortcuts](https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html) page gives Alt+Down/Up for kerning at a caret or tracking of selected text, Alt+Left/Right for leading, and Alt+Shift+Right/Left for baseline shift. Alt+Shift+Down/Up also steps kerning or tracking. Down increases kerning or tracking, Left increases leading, and Shift+Right increases baseline shift; the opposite arrows decrease them. Cmd/Ctrl makes five steps. Horizontal type keeps its existing mapping; mixed horizontal and vertical text-object selections take the horizontal mapping for both objects in one undo step. Selected vertical point, area and path type use the same directions. Tests: `spacing_arrows_follow_each_text_objects_orientation`, `alt_arrows_rotate_for_selected_vertical_type`, `mixed_type_objects_take_horizontal_spacing_chords_in_both_orders`, and `selected_vertical_area_and_path_type_use_vertical_spacing_chords`.
- **Vertical punctuation:** fullwidth `!` (U+FF01) and `?` (U+FF1F) use their nominal glyph upright when no vertical alternate is present, following the Tu fallback in [Unicode UAX #50 §3.1](https://www.unicode.org/reports/tr50/#vo). Code-built test fonts cover point and area type with and without `vert` alternates, retain the rotated fallback for a bracket and long vowel mark, and leave horizontal type unchanged. Tests: `crates/text/src/tests/vertical.rs`.
+2 -1
View File
@@ -99,7 +99,7 @@ tools, never checked side by side) isn't captured row by row.
| Smooth tool smooths only the stretch of path under the drag | done | #1512 |
| Drawing modes apply to pastes: Draw Inside takes Paste, Paste in Place and Paste on All Artboards inside the object; Draw Behind puts Paste in Place and Paste on All Artboards behind the selection; Shift+D cycles past Draw Behind with any selection ([drawing modes](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/learn-drawing-basics/drawing-modes-overview.html)) | done | #3705; `pasting_in_draw_inside_puts_the_copies_inside_the_object`, `paste_in_place_in_draw_behind_goes_behind_the_selection`, `shift_d_goes_past_draw_behind_whatever_is_selected` |
| Simplify (slider, auto, corner angle threshold), of a whole path or of the part of it selected with Direct Selection ([manually simplify paths](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/modify-paths/manually-simplify-paths.html)) | done | Tolerance, default-on Preview and original/new point counts (#1272); Corner Point Angle Threshold, Convert to Straight Lines and Show Original Path; the slider under the selection with Auto-Simplify and More Options, opening at the Auto-Simplify level for the selection with that result previewed, and the dialog's option to open it directly with its last settings. With anchors or segments direct-selected, the slider, Auto-Simplify and the dialog refit only each run of selected segments between its end anchors, a closed path's run across its start point included; the rest of the path stays exactly as it was (`tests/simplify_part.rs`, `simplify_slider_and_dialog_simplify_only_the_direct_selected_part_of_a_path`) |
| Smooth (Object › Path › Smooth…: slider from minimum to maximum smoothing, Auto-Smooth) ([smooth slider](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/modify-paths/refine-path-segments-with-the-smooth-slider.html)) | done | #4115: the slider under the selection previews `object.path.smooth {level}`, Auto-Smooth picks level 50, Enter or a click outside keeps one undo step and Escape restores; the smoothing rule beyond the two ends is VectorCraft's own (`tests/smooth_path.rs`, `smooth_slider_previews_auto_smooth_and_keeps_the_result`) |
| Smooth (Object › Path › Smooth…: slider from minimum to maximum smoothing, Auto-Smooth) ([smooth slider](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/modify-paths/refine-path-segments-with-the-smooth-slider.html)) | done | #4115: the slider under the selection previews `object.path.smooth {level}`, Auto-Smooth picks level 50 and takes a shaky line to one smooth curve, direct-selected anchors smooth only their section, Enter or a click outside keeps one undo step and Escape restores; the smoothing rule beyond the two ends is VectorCraft's own (`tests/smooth_path.rs`, `smooth_slider_previews_auto_smooth_and_keeps_the_result`) |
| Draw Inside in isolation mode: art drawn or pasted inside the isolated object takes isolation mode into its clip group, clickable and undimmed ([isolation mode](https://helpx.adobe.com/illustrator/desktop/manage-objects/select-objects/isolate-objects.html), [drawing modes](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/learn-drawing-basics/drawing-modes-overview.html)) | done | #3705; `drawing_inside_an_isolated_object_keeps_the_new_art_in_isolation_mode` |
| Draw Inside marks the object it draws into: a dotted open rectangle round its corners, in its layer's colour, while the mode lasts, selected or not | done | #4319, `canvas::tests::draw_inside_marks_the_corners_of_its_object` ([drawing modes](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/learn-drawing-basics/drawing-modes-overview.html)) |
| Simplify (slider, auto, corner angle threshold) | partial | Tolerance, default-on Preview and original/new point counts (#1272); Corner Point Angle Threshold, Convert to Straight Lines and Show Original Path; the slider under the selection with Auto-Simplify and More Options, and the dialog's option to open it directly with its last settings; the slider opens at 1 pt rather than at the Auto-Simplify level, and simplifying only a Direct Selection part of a path remains missing |
@@ -379,6 +379,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development
| Date | Change | Summary |
|---|---|---|
| 2026-10-31 | minor | Object › Path › Smooth: Auto-Smooth takes a shaky line to one smooth curve, and direct-selected anchors smooth only their section; scores unchanged |
| 2026-10-31 | minor | New Document: More Settings has Save Preset beside the name, as Preset Details does, and saves its settings (#4893); scores and estimates unchanged |
| 2026-10-31 | minor | Export for Screens is laid out as the reference app's: Artboards and Assets tabs with a grid or list of thumbnails, Clear Selection and Prefix under them, Create Sub-folders by Scale or Format (on by default), Export PDFs As Single File or Multiple Files (`pdfs`), iOS and Android rows, the format presets in a ☰ menu, and a Selected / files-to-export count; the naming pattern, Layers and Selection pages and folders list stay in `document.exportForScreens` for scripts and MCP; scores unchanged |
| 2026-10-31 | minor | Direct Selection: with Alt held at the press, a marquee selects every path it touches whole and keeps the Group Selection pointer while dragging (#4890); a click on a selected object's centre point (Show Center) selects the object whole, and a drag from it moves the object (#4891); scores unchanged |