mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 20:08:42 +00:00
Merge pull request #7192 from dotCooCoo/feat/context-menu-smooth
Context menu: Smooth… on a selected path opens the Smooth slider
This commit is contained in:
@@ -1258,7 +1258,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",
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -1259,7 +1259,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",
|
||||
|
||||
@@ -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
@@ -5087,9 +5087,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
|
||||
|
||||
|
||||
@@ -915,6 +915,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.
|
||||
|
||||
Reference in New Issue
Block a user