mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-31 21:27:00 -06:00
Path: Remove Anchor Points is Object › Path's one item and shares the Delete Anchor Point refit (review of #241)
- Parity: Object › Path › Remove Anchor Points now runs the new command (`path.removeAnchors`,
the menu's feature name) instead of `path.deleteAnchors`, which deleted the anchors with their
segments and opened closed paths; that stays the Delete key's command ("Delete Anchor Points",
off the menu). No second "Smart" item.
- Geometry: the refit moves to `vectorcraft_pathops::remove_anchor` on the existing least-squares
fitter (fixed end tangents, a golden-section search for where the curve passes the removed
point) instead of a ~1,000-candidate grid search in geom (about 4 ms per anchor, now well under
0.1 ms). Two straight sides stay one straight side (a rectangle loses a corner and becomes a
triangle), as Illustrator does; non-finite input leaves the handles alone.
- The Delete Anchor Point tool (`path.removeAnchor`) uses the same function, replacing its
heuristic handle stretch; the explicit id/anchor and `fit` params of the new command are dropped
as duplicates of it. The command is enabled by direct-selected anchors (`has_anchors`), not a
UI override.
- The Control bar shows an icon button (the Delete Anchor Point icon, tooltip "Remove Anchor
Points") instead of a 204 pt text button; the labels reuse the translated "Remove Anchor
Points"; zh-hant gains "Delete Anchor Points". The Pen test that pinned Alt-click as plain Pen
(Illustrator's Alt over an anchor is Convert Anchor Point) is dropped.
- Tests for the refit, closed paths, straight sides and bad input; docs/mcp.md and ROADMAP.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f5c3ecb47c
commit
d5a060a009
@@ -1,6 +1,5 @@
|
||||
//! Drawing / path-editing tools driven through the session (tools → actions → `path.*` commands).
|
||||
|
||||
use kurbo::ParamCurveNearest;
|
||||
use serde_json::json;
|
||||
use vectorcraft_doc::{NodeId, NodeKind};
|
||||
use vectorcraft_geom::{FillRule, PathData, Point};
|
||||
@@ -376,8 +375,7 @@ fn draw2_commands_reject_bad_params() {
|
||||
("path.freehand", json!({"points": [[1, 2]]})),
|
||||
("path.curvature", json!({"points": []})),
|
||||
("path.removeAnchor", json!({"id": id.0, "anchor": 99})),
|
||||
("path.smartRemoveAnchor", json!({})),
|
||||
("path.smartRemoveAnchor", json!({"id": id.0, "anchor": 99})),
|
||||
("path.removeAnchors", json!({})),
|
||||
("path.convertAnchor", json!({"id": 9999, "anchor": 0, "to": "corner"})),
|
||||
("path.reshapeSegment", json!({"id": id.0, "segment": 42})),
|
||||
("path.split", json!({"id": id.0})),
|
||||
@@ -485,7 +483,7 @@ fn shift_drag_keeps_a_smooth_anchor_smooth_and_alt_breaks_it() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn smart_remove_refits_a_split_curve_and_undo_restores_it() {
|
||||
fn removing_an_anchor_refits_a_split_curve_and_undo_restores_it() {
|
||||
let mut s = session();
|
||||
let made = s
|
||||
.execute(
|
||||
@@ -500,7 +498,7 @@ fn smart_remove_refits_a_split_curve_and_undo_restores_it() {
|
||||
let before = path(&s, id);
|
||||
let inserted = s.execute("path.insertAnchor", &json!({"id": id.0, "segment": 0, "t": 0.4})).unwrap();
|
||||
let ai = inserted["anchor"].as_u64().unwrap();
|
||||
s.execute("path.smartRemoveAnchor", &json!({"id": id.0, "anchor": ai})).unwrap();
|
||||
s.execute("path.removeAnchor", &json!({"id": id.0, "anchor": ai})).unwrap();
|
||||
let after = path(&s, id);
|
||||
assert_eq!(after.subpaths[0].anchors.len(), 2);
|
||||
let (got, orig) = (&after.subpaths[0].anchors, &before.subpaths[0].anchors);
|
||||
@@ -511,49 +509,28 @@ fn smart_remove_refits_a_split_curve_and_undo_restores_it() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn smart_remove_without_fit_leaves_handles() {
|
||||
fn remove_anchor_points_keeps_the_paths_closed() {
|
||||
// Object › Path › Remove Anchor Points, unlike the Delete key: a rectangle loses a corner and
|
||||
// stays closed, a straight-sided triangle; a curve keeps its shape round a removed point.
|
||||
let mut s = session();
|
||||
let made = s
|
||||
.execute(
|
||||
"path.create",
|
||||
&json!({"anchors": [
|
||||
{"x": 0, "y": 0, "out": [10, 30]},
|
||||
{"x": 50, "y": 10},
|
||||
{"x": 100, "y": 0, "in": [80, 25]}
|
||||
]}),
|
||||
)
|
||||
.unwrap();
|
||||
let id = NodeId(made["id"].as_u64().unwrap());
|
||||
s.execute("path.smartRemoveAnchor", &json!({"id": id.0, "anchor": 1, "fit": false})).unwrap();
|
||||
let pd = path(&s, id);
|
||||
let sp = &pd.subpaths[0];
|
||||
assert_eq!(sp.anchors.len(), 2);
|
||||
assert_eq!(sp.anchors[0].h_out, Point::new(10.0, 30.0));
|
||||
assert_eq!(sp.anchors[1].h_in, Point::new(80.0, 25.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn smart_remove_uses_the_direct_selected_anchors() {
|
||||
let mut s = session();
|
||||
let made = s
|
||||
.execute(
|
||||
"path.create",
|
||||
&json!({"anchors": [
|
||||
{"x": 0, "y": 0},
|
||||
{"x": 50, "y": 40},
|
||||
{"x": 100, "y": 0}
|
||||
]}),
|
||||
)
|
||||
.unwrap();
|
||||
let id = NodeId(made["id"].as_u64().unwrap());
|
||||
s.execute("select.anchors", &json!({"id": id.0, "anchors": [[0, 1]]})).unwrap();
|
||||
s.execute("path.smartRemoveAnchor", &json!({})).unwrap();
|
||||
let sp = &path(&s, id).subpaths[0];
|
||||
assert_eq!(sp.anchors.len(), 2);
|
||||
let d = sp.segment(0).nearest(Point::new(50.0, 40.0), 1e-4).distance_sq.sqrt();
|
||||
assert!(d < 40.0, "the refit stays closer to the removed corner than the chord ({d})");
|
||||
let r = rect(&mut s, 0.0, 0.0, 100.0, 80.0);
|
||||
let made = s.execute("path.create", &json!({"anchors": [{"x": 300, "y": 0, "out": [330, 80]}, {"x": 400, "y": 0, "in": [370, 80]}]})).unwrap();
|
||||
let c = NodeId(made["id"].as_u64().unwrap());
|
||||
let before = path(&s, c).subpaths[0].segment(0);
|
||||
s.execute("path.insertAnchor", &json!({"id": c.0, "segment": 0, "t": 0.5})).unwrap();
|
||||
s.execute("select.anchors", &json!({"id": r.0, "anchors": [[0, 0]]})).unwrap();
|
||||
s.execute("select.anchors", &json!({"id": c.0, "anchors": [[0, 1]], "mode": "add"})).unwrap();
|
||||
assert_eq!(s.execute("path.removeAnchors", &json!({})).unwrap()["removedObjects"], 0);
|
||||
let sp = &path(&s, r).subpaths[0];
|
||||
assert!(sp.closed && sp.anchors.len() == 3, "{sp:?}");
|
||||
assert!(!sp.anchors.iter().any(|a| a.has_in() || a.has_out()), "straight sides stay straight: {sp:?}");
|
||||
let after = path(&s, c).subpaths[0].segment(0);
|
||||
assert_eq!(path(&s, c).subpaths[0].anchors.len(), 2);
|
||||
assert!(after.p1.distance(before.p1) < 0.5 && after.p2.distance(before.p2) < 0.5, "{after:?}");
|
||||
assert_eq!(s.doc().unwrap().history.undo.last().unwrap().label, "Remove Anchor Points");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(path(&s, id).subpaths[0].anchors.len(), 3);
|
||||
let spec = find_command("path.smartRemoveAnchor").unwrap();
|
||||
assert_eq!(path(&s, r).subpaths[0].anchors.len(), 4);
|
||||
assert_eq!(path(&s, c).subpaths[0].anchors.len(), 3);
|
||||
let spec = find_command("path.removeAnchors").unwrap();
|
||||
assert_eq!(spec.menu, &["Object", "Path"][..]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user