Context menu: Smooth… on a selected path opens the Smooth slider

A right-click on a selection that holds a path (a path, a group of
paths, a compound path, or anchors selected with Direct Selection) lists
Smooth…, after Join and Average… and the anchor entries when those are
listed. It opens the slider that Object › Path › Smooth opens, which
previews the result until OK keeps it as one undo step. With nothing,
only type or a group of type selected, Smooth… is not listed.

Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
dotCooCoo
2026-11-13 08:35:40 -08:00
parent 9395fb9cec
commit d1c8ff78de
6 changed files with 101 additions and 5 deletions
+1 -1
View File
@@ -1236,7 +1236,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
"ui.smoothAuto",
"Auto-Smooth",
"",
"{} the open Smooth slider (Object › Path › Smooth opens it: dialog `smoothSlider` with field `level` 0..100, object.path.smooth's level, opening at the level object.path.smooth {auto: true} picks, previewed on the canvas until ui.dialog.confirm keeps it as one undo step) moves to the level object.path.smooth {auto: true} picks and previews it → {level}; an error when the slider isn't open",
"{} the open Smooth slider (Object › Path › Smooth and the canvas context menu's Smooth… open it: dialog `smoothSlider` with field `level` 0..100, object.path.smooth's level, opening at the level object.path.smooth {auto: true} picks, previewed on the canvas until ui.dialog.confirm keeps it as one undo step) moves to the level object.path.smooth {auto: true} picks and previews it → {level}; an error when the slider isn't open",
),
(
"ui.simplifyMoreOptions",
+4
View File
@@ -73,6 +73,10 @@ pub fn context_items(app: &VectorcraftApp) -> Vec<Item> {
if !st.selection.anchors.is_empty() {
v.extend([c("Remove Anchor Points", "path.removeAnchors"), c("Cut Path at Selected Anchor Points", "path.cutAtAnchors")]);
}
// Object › Path › Smooth…, which opens the Smooth slider (`invoke`). `prune` leaves it out
// unless object.path.smooth can run: the selection holds a path, in a group or a compound
// path too.
v.push(c("Smooth…", "object.path.smooth"));
if several {
v.push(c("Make Clipping Mask", "object.clippingMask.make"));
}
+1 -1
View File
@@ -1237,7 +1237,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
"ui.smoothAuto",
"Auto-Smooth",
"",
"{} the open Smooth slider (Object › Path › Smooth opens it: dialog `smoothSlider` with field `level` 0..100, object.path.smooth's level, opening at the level object.path.smooth {auto: true} picks, previewed on the canvas until ui.dialog.confirm keeps it as one undo step) moves to the level object.path.smooth {auto: true} picks and previews it → {level}; an error when the slider isn't open",
"{} the open Smooth slider (Object › Path › Smooth and the canvas context menu's Smooth… open it: dialog `smoothSlider` with field `level` 0..100, object.path.smooth's level, opening at the level object.path.smooth {auto: true} picks, previewed on the canvas until ui.dialog.confirm keeps it as one undo step) moves to the level object.path.smooth {auto: true} picks and previews it → {level}; an error when the slider isn't open",
),
(
"ui.simplifyMoreOptions",
+90
View File
@@ -248,6 +248,96 @@ fn the_menu_follows_the_selection() {
assert!(items.windows(2).all(|w| !matches!(w, [Item::Sep, Item::Sep])));
}
/// A closed path of 60 jagged anchors around (150, 150), filled so that a right-click inside it
/// hits it, with nothing selected.
fn jagged(app: &mut VectorcraftApp) -> NodeId {
let anchors: Vec<_> = (0..60)
.map(|i| {
let a = f64::from(i) * std::f64::consts::TAU / 60.0;
let r = 60.0 + f64::from(i % 2) * 4.0;
json!({"x": 150.0 + r * a.cos(), "y": 150.0 + r * a.sin()})
})
.collect();
let id = NodeId(app.run("path.create", json!({"anchors": anchors, "closed": true})).unwrap()["id"].as_u64().unwrap());
app.run("paint.setFill", json!({"color": "#ff8010"})).unwrap();
app.run("select.none", json!({})).unwrap();
id
}
/// The anchor points of every path in the document.
fn anchor_total(app: &VectorcraftApp) -> usize {
let mut n = 0;
app.session.active().unwrap().doc.walk(|node| {
if let NodeKind::Path { path, .. } = &node.kind {
n += path.anchor_count();
}
});
n
}
/// A right-click on a path lists Object › Path › Smooth… after Join and Average…. Smooth… opens
/// the Smooth slider at Auto-Smooth's level and adds no undo step while it is open; OK keeps the
/// smoothed path as one undo step.
#[test]
fn right_click_on_a_path_opens_the_smooth_slider() {
let mut app = app();
let id = jagged(&mut app);
let ctx = egui::Context::default();
frame(&mut app, &ctx, vec![]);
let p = screen(&app, 150.0, 150.0);
let texts = click(&mut app, &ctx, p, PointerButton::Secondary);
assert_eq!(app.session.active().unwrap().selection.objects, [id], "the path right-clicked is selected");
let y = |label| at(&texts, label).y;
assert!(y("Join") < y("Average…") && y("Average…") < y("Smooth…"), "{texts:?}");
let (undo, before) = (app.session.active().unwrap().history.undo.len(), anchor_total(&app));
let after = click(&mut app, &ctx, at(&texts, "Smooth…"), PointerButton::Primary);
assert!(!has(&after, "Arrange"), "choosing Smooth… closes the menu");
let open = app.ui.dialog.as_ref().map(|d| (d.kind.clone(), d.f64("level", -1.0)));
assert_eq!(open, Some((crate::dialogs::smooth_slider::KIND.to_string(), vectorcraft_engine::SMOOTH_AUTO_LEVEL)));
assert_eq!(app.session.active().unwrap().history.undo.len(), undo, "no undo step while the slider is open");
crate::dialogs::confirm(&mut app).unwrap();
assert!(app.ui.dialog.is_none() && !app.session.in_interaction());
let st = app.session.active().unwrap();
assert_eq!(st.history.undo.len(), undo + 1, "OK keeps the result as one undo step");
assert!(anchor_total(&app) < before, "the path was smoothed: {} anchor points from {before}", anchor_total(&app));
}
/// Smooth… is listed while the selection holds a path: a path, a group of paths, a compound path,
/// a path with type, and anchors selected with Direct Selection (whose Remove Anchor Points
/// stays). It is left out with nothing, only type or a group of type selected.
#[test]
fn smooth_follows_the_selections_paths() {
let mut app = app();
let (a, b) = (rect(&mut app, 0.0, 0.0), rect(&mut app, 150.0, 0.0));
let t = NodeId(app.run("text.create", json!({"x": 10, "y": 250, "text": "Type"})).unwrap()["id"].as_u64().unwrap());
let smooth = |app: &VectorcraftApp| labels(&menus::context_items(app)).contains(&"Smooth…");
app.run("select.none", json!({})).unwrap();
assert!(!smooth(&app), "nothing selected");
app.run("select.set", json!({"ids": [t.0]})).unwrap();
assert!(!smooth(&app), "only type selected");
let u = NodeId(app.run("text.create", json!({"x": 10, "y": 300, "text": "More"})).unwrap()["id"].as_u64().unwrap());
app.run("select.set", json!({"ids": [t.0, u.0]})).unwrap();
app.run("object.group", json!({})).unwrap();
assert!(!smooth(&app), "a group of type only");
app.run("object.ungroup", json!({})).unwrap();
app.run("select.set", json!({"ids": [a.0]})).unwrap();
assert!(smooth(&app), "a path");
app.run("select.set", json!({"ids": [a.0, t.0]})).unwrap();
assert!(smooth(&app), "a path and type");
app.run("select.set", json!({"ids": [a.0, b.0]})).unwrap();
app.run("object.group", json!({})).unwrap();
assert!(smooth(&app), "a group of paths");
app.run("object.ungroup", json!({})).unwrap();
app.run("select.set", json!({"ids": [a.0, b.0]})).unwrap();
app.run("object.compoundPath.make", json!({})).unwrap();
assert!(smooth(&app), "a compound path");
app.run("object.compoundPath.release", json!({})).unwrap();
app.select_tool("directSelection");
app.run("select.anchors", json!({"id": a.0, "anchors": [[0, 1]]})).unwrap();
assert!(smooth(&app), "an anchor point direct-selected");
assert!(labels(&menus::context_items(&app)).contains(&"Remove Anchor Points"), "the anchor entries stay");
}
/// Selected type has Bullets and Numbering › Apply Bullets in the context menu and the Type menu,
/// which makes its paragraphs a bulleted list; other art has no such item.
#[test]
+4 -3
View File
@@ -4989,9 +4989,10 @@ round, such as a rectangle, stays as it is. Closed paths stay closed and open pa
{"name":"run_command","arguments":{"command":"object.path.smooth","params":{"level":80}}}
```
In the desktop app, Object › Path › Smooth opens the slider (`ui.menuDialog {command: "object.path.smooth"}`, dialog
`smoothSlider` with field `level`) at the Auto-Smooth level and previews the result until `ui.dialog.confirm`;
`ui.smoothAuto` moves the open slider back to the Auto-Smooth level.
In the desktop app, Object › Path › Smooth and the canvas context menu's Smooth… (listed while the selection holds a
path) open the slider (`ui.menuDialog {command: "object.path.smooth"}`, dialog `smoothSlider` with field `level`) at
the Auto-Smooth level, and the slider previews the result until `ui.dialog.confirm`; `ui.smoothAuto` moves the open
slider back to the Auto-Smooth level.
## Editing a path with the Curvature tool
+1
View File
@@ -909,6 +909,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- **Dry Media library** ([help page](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/apply-and-edit-strokes/about-brushes.html)): Window › Brush Libraries › Dry Media holds four art brushes with textured strokes like dry drawing materials, each 100 pt long: Chalk Grain (about 8 pt high), Graphite Pencil (about 2.3 pt), Wax Crayon (about 5.5 pt) and Charcoal Dust (about 7 pt). The brushes, their names, sizes and art are VectorCraft's own: each is filled vector paths from a fixed seed, at most 80 paths and 500 anchors, stretched along the path with Tints colorization, and the chalk and crayon art has even-odd gaps where the paper shows (`brush.libraries`, `brush.addFromLibrary`; `the_dry_media_library_holds_grainy_art_brushes_with_bounded_art`).
- A canvas context menu: right-click selects the object under the pointer and lists what applies to the selection (Undo/Redo, clipboard, group, isolation, join, Remove Anchor Points when anchors are selected, masks, compound paths, guides, Transform, Arrange, Select, Export Selection, and on type Bullets and Numbering and Spelling › Auto Spell Check, checked while it is on), or the view commands on empty canvas.
- A right-click over locked objects adds an Unlock submenu to the canvas context menu. The submenu lists up to 20 locked objects under the pointer, topmost first, by the name the Layers panel shows, and choosing one unlocks it (`layer.setProps`). A locked group is listed as one item. Objects on hidden, locked or template layers are not listed.
- The canvas context menu lists Object › Path › Smooth… while the selection holds a path, in a group or a compound path too. It opens the Smooth slider, as the Object menu does (`right_click_on_a_path_opens_the_smooth_slider`, `smooth_follows_the_selections_paths`).
- Native macOS save sheets offer a localised Format menu (#866), built from the request's existing format filters. It selects the suggested filename's format and updates the suffix when changed, including legacy `.drawcraft` names. `apps/vectorcraft/src/mac_save_format.rs` has filename regressions; an actual native SVG save, selection of PDF and cancellation without changing the saved document were verified.
- Save / Don't Save / Cancel before closing or quitting with unsaved documents (tabs, Close, Close All, Quit, window close).
- Four brightness themes, persistent preferences.