mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-08 08:20:13 +00:00
Set Corners: round all, odd-numbered or even-numbered anchors (#5546)
addon.pathtools.cornerSet takes `every` ("all", "odd" or "even"; default
"all"), which keeps, of the anchors already chosen (named, direct-selected
or all), only the odd- or even-numbered ones, counted along each path from
its first anchor as number 1. Anchors that take no corner still count, so
the pattern follows the path's anchor order on open and closed paths; when
nothing is left the command says so. Unknown values are refused.
Object › Plug-ins › Path Precision Tools › Set Corners… (also a button in
the Anchor Inspector's Corners section) opens a dialog with Radius, Corner
kind and Anchors (All / Odd-Numbered / Even-Numbered) that previews on the
canvas and commits as one undo step.
This commit is contained in:
@@ -20,7 +20,7 @@ pub(super) fn specs() -> Vec<CommandSpec> {
|
||||
"Set Corners",
|
||||
[],
|
||||
None,
|
||||
"{ids?: [id…] (the selection), anchors?: [index…] (of each path's outline with no corners cut; else its direct-selected anchors, or all of them), items?: [{id, anchors?}…] (instead of ids: each path with its own anchors), radius: pt 0..1000000 (drawn no larger than fits), kind?: \"round\"|\"invertedRound\"|\"chamfer\" (each corner's own)} cut live corners at the anchors that can have one, as one undo step → {ids, corners (how many)}",
|
||||
"{ids?: [id…] (the selection), anchors?: [index…] (of each path's outline with no corners cut; else its direct-selected anchors, or all of them), items?: [{id, anchors?}…] (instead of ids: each path with its own anchors), radius: pt 0..1000000 (drawn no larger than fits), kind?: \"round\"|\"invertedRound\"|\"chamfer\" (each corner's own), every?: \"all\"|\"odd\"|\"even\" (default all: of those anchors, only the odd- or even-numbered ones, counting each path's anchors from 1)} cut live corners at the anchors that can have one, as one undo step → {ids, corners (how many)}",
|
||||
has_doc,
|
||||
corner_set
|
||||
),
|
||||
@@ -50,8 +50,53 @@ fn anchors_param(p: &Value, cmd: &str) -> Result<Option<BTreeSet<usize>>> {
|
||||
.map(Some)
|
||||
}
|
||||
|
||||
/// The corners a request names: each path with the corners (anchor indices of its uncut outline).
|
||||
fn named_corners(s: &Session, p: &Value, cmd: &str) -> Result<Vec<(NodeId, BTreeSet<usize>)>> {
|
||||
/// Which of the chosen anchors `cornerSet` takes, by their number along the path's uncut outline
|
||||
/// (the first anchor is number 1).
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
enum Every {
|
||||
All,
|
||||
/// The 1st, 3rd, 5th… anchors: indices 0, 2, 4….
|
||||
Odd,
|
||||
/// The 2nd, 4th, 6th… anchors: indices 1, 3, 5….
|
||||
Even,
|
||||
}
|
||||
|
||||
impl Every {
|
||||
/// The `every` of `p` (default: all).
|
||||
fn of(p: &Value, cmd: &str) -> Result<Self> {
|
||||
match p.get("every").filter(|v| !v.is_null()) {
|
||||
None => Ok(Self::All),
|
||||
Some(v) => match v.as_str() {
|
||||
Some("all") => Ok(Self::All),
|
||||
Some("odd") => Ok(Self::Odd),
|
||||
Some("even") => Ok(Self::Even),
|
||||
_ => Err(bad(cmd, format!("`every` must be \"all\", \"odd\" or \"even\", not {v}"))),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Does it take the anchor at `index`? Anchors that take no corner still count.
|
||||
fn takes(self, index: usize) -> bool {
|
||||
match self {
|
||||
Self::All => true,
|
||||
Self::Odd => index.is_multiple_of(2),
|
||||
Self::Even => !index.is_multiple_of(2),
|
||||
}
|
||||
}
|
||||
|
||||
/// The error when none of the chosen corners is one it takes.
|
||||
fn none(self) -> &'static str {
|
||||
match self {
|
||||
Self::All => "choose anchors where two sides meet at an angle: only those take a corner",
|
||||
Self::Odd => "none of the chosen corners is at an odd-numbered anchor (the 1st, 3rd, 5th…)",
|
||||
Self::Even => "none of the chosen corners is at an even-numbered anchor (the 2nd, 4th, 6th…)",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The corners a request names: each path with the corners (anchor indices of its uncut outline)
|
||||
/// that `every` takes.
|
||||
fn named_corners(s: &Session, p: &Value, cmd: &str, every: Every) -> Result<Vec<(NodeId, BTreeSet<usize>)>> {
|
||||
let shared = anchors_param(p, cmd)?;
|
||||
// Each path with its own `anchors`, or sharing the request's.
|
||||
let mut own: Vec<Option<BTreeSet<usize>>> = vec![];
|
||||
@@ -71,10 +116,10 @@ fn named_corners(s: &Session, p: &Value, cmd: &str) -> Result<Vec<(NodeId, BTree
|
||||
let d = &st.doc;
|
||||
check_size(ids.iter().filter_map(|id| d.node(*id)?.path_data()), cmd)?;
|
||||
let editable = d.editable();
|
||||
let mut out = vec![];
|
||||
let (mut out, mut chosen) = (vec![], false);
|
||||
for (i, id) in ids.iter().enumerate() {
|
||||
let Some(lc) = d.node(*id).filter(|_| editable(*id)).and_then(LiveCorners::of) else { continue };
|
||||
let corners = match own.get(i).and_then(Option::as_ref).or(shared.as_ref()) {
|
||||
let corners: BTreeSet<usize> = match own.get(i).and_then(Option::as_ref).or(shared.as_ref()) {
|
||||
Some(given) => {
|
||||
let n = lc.base.anchor_count();
|
||||
if let Some(past) = given.iter().find(|k| **k >= n) {
|
||||
@@ -84,12 +129,14 @@ fn named_corners(s: &Session, p: &Value, cmd: &str) -> Result<Vec<(NodeId, BTree
|
||||
}
|
||||
None => st.selection.partial(*id).filter(|a| !a.is_empty()).map_or_else(|| lc.all(), |a| lc.corners_of(a)),
|
||||
};
|
||||
chosen |= !corners.is_empty();
|
||||
let corners: BTreeSet<usize> = corners.into_iter().filter(|k| every.takes(*k)).collect();
|
||||
if !corners.is_empty() {
|
||||
out.push((*id, corners));
|
||||
}
|
||||
}
|
||||
if out.is_empty() {
|
||||
return Err(bad(cmd, "choose anchors where two sides meet at an angle: only those take a corner"));
|
||||
return Err(bad(cmd, if chosen { every.none() } else { Every::All.none() }));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
@@ -134,11 +181,12 @@ fn corner_set(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
.map_err(|_| bad(C, format!("`kind` must be \"round\", \"invertedRound\" or \"chamfer\", not {v}")))
|
||||
})
|
||||
.transpose()?;
|
||||
let corners = named_corners(s, p, C)?;
|
||||
let every = Every::of(p, C)?;
|
||||
let corners = named_corners(s, p, C, every)?;
|
||||
edit_corners(s, "Set Corners", corners, radius, kind)
|
||||
}
|
||||
|
||||
fn corner_remove(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let corners = named_corners(s, p, "addon.pathtools.cornerRemove")?;
|
||||
let corners = named_corners(s, p, "addon.pathtools.cornerRemove", Every::All)?;
|
||||
edit_corners(s, "Remove Corners", corners, 0.0, Some(CornerKind::Round))
|
||||
}
|
||||
|
||||
@@ -567,6 +567,93 @@ fn corner_commands_refuse_bad_params() {
|
||||
}
|
||||
}
|
||||
|
||||
/// A closed five-pointed star about (200, 150): its points at the even indices 0, 2… (the odd
|
||||
/// numbers counting from 1), its inner corners at the odd ones.
|
||||
fn star_path(s: &mut Session) -> NodeId {
|
||||
let pts: Vec<(f64, f64)> = (0..10)
|
||||
.map(|i| {
|
||||
let (r, a) = (if i % 2 == 0 { 100.0 } else { 40.0 }, std::f64::consts::PI * f64::from(i) / 5.0);
|
||||
(200.0 + r * a.sin(), 150.0 - r * a.cos())
|
||||
})
|
||||
.collect();
|
||||
line(s, &pts, true)
|
||||
}
|
||||
|
||||
/// The cut corners of `id`: anchor indices of its uncut outline with a radius drawn.
|
||||
fn cut(s: &Session, id: NodeId) -> Vec<usize> {
|
||||
let lc = LiveCorners::of(s.doc().unwrap().doc.node(id).unwrap()).unwrap();
|
||||
(0..lc.base.anchor_count()).filter(|k| lc.drawn_radius(*k) > 0.0).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corner_set_every_takes_the_odd_or_even_numbered_anchors_of_each_path() {
|
||||
let mut s = session();
|
||||
let star = star_path(&mut s);
|
||||
// An open zig-zag: its ends (1st and 5th) take no corner but still count.
|
||||
let zig = line(&mut s, &[(10.0, 250.0), (30.0, 210.0), (50.0, 250.0), (70.0, 210.0), (90.0, 250.0)], false);
|
||||
select(&mut s, &[star, zig]);
|
||||
let n = steps(&s);
|
||||
// Odd: the 1st, 3rd, 5th… of each path, as one step.
|
||||
let r = s.execute("addon.pathtools.cornerSet", &json!({"radius": 4, "every": "odd"})).unwrap();
|
||||
assert_eq!((r["corners"].clone(), steps(&s)), (json!(6), n + 1));
|
||||
assert_eq!((cut(&s, star), cut(&s, zig)), (vec![0, 2, 4, 6, 8], vec![2]));
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!((cut(&s, star), cut(&s, zig), steps(&s)), (vec![], vec![], n));
|
||||
// Even: the 2nd, 4th, 6th….
|
||||
let r = s.execute("addon.pathtools.cornerSet", &json!({"radius": 4, "every": "even"})).unwrap();
|
||||
assert_eq!((r["corners"].clone(), steps(&s)), (json!(7), n + 1));
|
||||
assert_eq!((cut(&s, star), cut(&s, zig)), (vec![1, 3, 5, 7, 9], vec![1, 3]));
|
||||
// "all" (and no `every`) is every chosen corner, as before.
|
||||
s.execute("addon.pathtools.cornerSet", &json!({"ids": [zig.0], "radius": 4, "every": "all"})).unwrap();
|
||||
assert_eq!(cut(&s, zig), vec![1, 2, 3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corner_set_every_filters_the_named_and_direct_selected_anchors() {
|
||||
let mut s = session();
|
||||
let star = star_path(&mut s);
|
||||
// Of the named anchors, only the even-numbered ones (indices 1 and 3); numbers stay the path's.
|
||||
let r = s.execute("addon.pathtools.cornerSet", &json!({"ids": [star.0], "anchors": [1, 2, 3, 4], "radius": 3, "every": "even"})).unwrap();
|
||||
assert_eq!(r["corners"], json!(2));
|
||||
assert_eq!(cut(&s, star), vec![1, 3]);
|
||||
// Per path through `items`.
|
||||
s.execute("addon.pathtools.cornerSet", &json!({"items": [{"id": star.0, "anchors": [5, 6, 7]}], "radius": 3, "every": "odd"})).unwrap();
|
||||
assert_eq!(cut(&s, star), vec![1, 3, 6]);
|
||||
// Of the direct-selected anchors.
|
||||
s.execute("addon.pathtools.cornerRemove", &json!({"ids": [star.0]})).unwrap();
|
||||
select_anchors(&mut s, star, &[7, 8, 9]);
|
||||
s.execute("addon.pathtools.cornerSet", &json!({"radius": 3, "every": "odd"})).unwrap();
|
||||
assert_eq!(cut(&s, star), vec![8]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corner_set_every_refuses_bad_values_and_says_when_nothing_is_left() {
|
||||
let mut s = session();
|
||||
let zig = line(&mut s, &[(10.0, 250.0), (30.0, 210.0), (50.0, 250.0), (70.0, 210.0)], false);
|
||||
select(&mut s, &[zig]);
|
||||
let i = zig.0;
|
||||
refuses(
|
||||
&mut s,
|
||||
"addon.pathtools.cornerSet",
|
||||
&[
|
||||
json!({"ids": [i], "radius": 5, "every": "third"}),
|
||||
json!({"ids": [i], "radius": 5, "every": "Odd"}),
|
||||
json!({"ids": [i], "radius": 5, "every": 2}),
|
||||
json!({"ids": [i], "radius": 5, "every": true}),
|
||||
json!({"ids": [i], "anchors": [1], "radius": 5, "every": "odd"}),
|
||||
json!({"ids": [i], "anchors": [0, 1], "radius": 5, "every": "odd"}),
|
||||
],
|
||||
);
|
||||
let e = s.execute("addon.pathtools.cornerSet", &json!({"ids": [i], "radius": 5, "every": "third"})).unwrap_err().to_string();
|
||||
assert!(e.contains("`every` must be \"all\", \"odd\" or \"even\""), "{e}");
|
||||
// The 2nd anchor is a corner, but not an odd-numbered one.
|
||||
let e = s.execute("addon.pathtools.cornerSet", &json!({"ids": [i], "anchors": [1], "radius": 5, "every": "odd"})).unwrap_err().to_string();
|
||||
assert!(e.contains("odd-numbered"), "{e}");
|
||||
// An open end is no corner at all, whatever `every` says.
|
||||
let e = s.execute("addon.pathtools.cornerSet", &json!({"ids": [i], "anchors": [0], "radius": 5, "every": "odd"})).unwrap_err().to_string();
|
||||
assert!(e.contains("two sides meet"), "{e}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_corner_tool_clicks_drags_and_removes_in_one_step_each() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -593,6 +593,9 @@ fn structured_junk() {
|
||||
("addon.pathtools.cornerSet", json!({"items": [1, null, {"anchors": "x"}], "radius": 1})),
|
||||
("addon.pathtools.cornerSet", json!({"ids": [2], "radius": 1e308, "kind": "spiky"})),
|
||||
("addon.pathtools.cornerSet", json!({"items": "all", "radius": f64::NAN, "anchors": [-1, 1.5, "x"]})),
|
||||
("addon.pathtools.cornerSet", json!({"ids": [2, 3], "radius": 5, "every": "odd"})),
|
||||
("addon.pathtools.cornerSet", json!({"items": [{"id": 2, "anchors": [1, u64::MAX]}], "radius": 5, "every": "even"})),
|
||||
("addon.pathtools.cornerSet", json!({"ids": [2], "radius": 5, "every": [1, "x", null]})),
|
||||
("addon.pathtools.cornerRemove", json!({"ids": [0, 1, 2, 3, u64::MAX]})),
|
||||
("addon.pathtools.cornerRemove", json!({"items": [{"id": 2, "anchors": [u64::MAX]}]})),
|
||||
("addon.pathtools.slideAnchor", json!({"id": 2, "anchor": 1, "t": 0.98})),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Path Precision Tools in the interface: its submenu (Object › Plug-ins › Path Precision Tools),
|
||||
//! which selects its tools, opens its panels and runs Split / Add Points at Crossings, Divide into
|
||||
//! which selects its tools, opens its panels and runs Split / Add Points at Crossings, Set Corners… ([`set_corners`]), Divide into
|
||||
//! Dashes ([`dashes`]), Expand Dashes into Paths and Convert to Parametric Shape; the Anchor Inspector panel (the selected
|
||||
//! anchor's position and handles as numbers, the add-on's anchor edits, the Corner Tool's values,
|
||||
//! [`corner`], and the Shared Edge Merge Tool's, [`shared_edge`]); the Parametric Shape panel
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
pub mod corner;
|
||||
pub mod dashes;
|
||||
pub mod set_corners;
|
||||
pub mod shape;
|
||||
pub mod shared_edge;
|
||||
|
||||
@@ -56,6 +57,7 @@ pub fn menu() -> Vec<Item> {
|
||||
tool("Measure & Inspect Tool", measure_inspect::ID),
|
||||
tool("Parametric Shape Tool", parametric_shape::ID),
|
||||
Item::Cmd("Corner Tool", corner::COMMAND, json!({})),
|
||||
Item::Cmd("Set Corners…", set_corners::COMMAND, Value::Null),
|
||||
tool("Slide Anchor Along Path", slide_anchor::ID),
|
||||
Item::Cmd("Shared Edge Merge Tool", shared_edge::COMMAND, json!({})),
|
||||
Item::Sep,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! The Anchor Inspector panel's Corners section: the Corner Tool's radius and kind, how many corners
|
||||
//! the selection holds, and Apply (`addon.pathtools.cornerSet`).
|
||||
//! the selection holds, Apply (`addon.pathtools.cornerSet`) and Set Corners… (its dialog, which
|
||||
//! can also take every other anchor).
|
||||
|
||||
use egui::Ui;
|
||||
use serde_json::{Value, json};
|
||||
@@ -55,10 +56,14 @@ pub(super) fn section(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
});
|
||||
let width = ui.available_width();
|
||||
let apply = ui.add_enabled_ui(count > 0, |ui| widgets::flat_button(ui, tl!("Apply to Selected Anchors"), width).clicked()).inner;
|
||||
let dialog = ui.add_enabled_ui(count > 0, |ui| widgets::flat_button(ui, tl!("Set Corners…"), width).clicked()).inner;
|
||||
if let Some(values) = set {
|
||||
run(app, "tool.setOption", json!({ "tool": ID, "values": values }));
|
||||
}
|
||||
if apply {
|
||||
run(app, "addon.pathtools.cornerSet", json!({ "radius": radius, "kind": kind }));
|
||||
}
|
||||
if dialog {
|
||||
crate::menus::invoke(app, super::set_corners::COMMAND, json!({}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
//! Set Corners in the interface: its dialog (radius, corner kind, and which anchors: all, the
|
||||
//! odd-numbered or the even-numbered ones), which previews on the canvas and runs
|
||||
//! `addon.pathtools.cornerSet` on OK, one undo step. It opens on the Corner Tool's radius and kind.
|
||||
|
||||
use egui::Ui;
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_tools::addons::corner_tool::{ID, MAX_RADIUS, RADIUS};
|
||||
|
||||
use crate::VectorcraftApp;
|
||||
use crate::dialogs::{DialogSpec, form};
|
||||
use crate::state::Dialog;
|
||||
use crate::widgets;
|
||||
|
||||
/// The command, and its dialog's kind.
|
||||
pub const COMMAND: &str = "addon.pathtools.cornerSet";
|
||||
pub const KIND: &str = "setCorners";
|
||||
/// The corner kinds, as the command names them, and their labels.
|
||||
const KINDS: [(&str, &str); 3] = [("round", "Round"), ("invertedRound", "Inverted Round"), ("chamfer", "Chamfer")];
|
||||
/// Which anchors, as the command's `every` names them, and their labels.
|
||||
const EVERY: [(&str, &str); 3] = [("all", "All Anchors"), ("odd", "Odd-Numbered Anchors"), ("even", "Even-Numbered Anchors")];
|
||||
const LABEL_W: f32 = 64.0;
|
||||
const FIELD_W: f32 = 170.0;
|
||||
|
||||
/// The dialog's fields to start with: the Corner Tool's radius and kind, every anchor, Preview on.
|
||||
pub(crate) fn fields(app: &VectorcraftApp) -> Value {
|
||||
let options = app.session.tool_options_of(ID);
|
||||
let radius = options["radius"].as_f64().unwrap_or(RADIUS);
|
||||
let kind = options["kind"].as_str().unwrap_or("round").to_string();
|
||||
json!({ "radius": radius, "kind": kind, "every": "all", "preview": true })
|
||||
}
|
||||
|
||||
pub(crate) const SPEC: DialogSpec =
|
||||
DialogSpec { heading: |_| tl!("Set Corners").to_string(), body, confirm, preview: true, min_width: 280.0, ..DialogSpec::FORM };
|
||||
|
||||
fn body(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog) -> bool {
|
||||
let unit = app.session.general_unit();
|
||||
widgets::label_row(ui, tl!("Radius:"), LABEL_W, |ui| {
|
||||
form::length_within(ui, d, "radius", unit, FIELD_W, Some(0.0..=MAX_RADIUS));
|
||||
});
|
||||
form::choice(ui, d, "kind", tl!("Corner:"), (LABEL_W, FIELD_W), &KINDS);
|
||||
form::choice(ui, d, "every", tl!("Anchors:"), (LABEL_W, FIELD_W), &EVERY);
|
||||
form::preview(app, ui, d, "Set Corners", COMMAND, form::params(d));
|
||||
false
|
||||
}
|
||||
|
||||
/// OK: keep the preview (or run the command) and close.
|
||||
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
form::commit_preview(app, COMMAND, form::params(d))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
use vectorcraft_doc::{LiveCorners, NodeId};
|
||||
use vectorcraft_engine::Session;
|
||||
|
||||
use crate::tests_labels::painted_text;
|
||||
use crate::{VectorcraftApp, menus};
|
||||
|
||||
/// The anchor indices of `id`'s uncut outline with a corner drawn.
|
||||
fn cut(app: &VectorcraftApp, id: u64) -> Vec<usize> {
|
||||
let doc = &app.session.doc().unwrap().doc;
|
||||
let lc = LiveCorners::of(doc.node(NodeId(id)).unwrap()).unwrap();
|
||||
(0..lc.base.anchor_count()).filter(|k| lc.drawn_radius(*k) > 0.0).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_corners_opens_from_the_menu_previews_odd_anchors_and_commits_one_step() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
|
||||
let anchors = json!([{"x": 0, "y": 0}, {"x": 100, "y": 0}, {"x": 100, "y": 100}, {"x": 0, "y": 100}, {"x": -50, "y": 50}]);
|
||||
let id = app.run("path.create", json!({"anchors": anchors, "closed": true})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("select.set", json!({"ids": [id]})).unwrap();
|
||||
let entries = menus::menu_entries(&app);
|
||||
let item = entries.iter().find(|e| e.path == ["Object", "Plug-ins", "Path Precision Tools"] && e.label == "Set Corners…").unwrap();
|
||||
let (cmd, p) = menus::click_target(&item.label, item.command.as_deref().unwrap(), &item.params);
|
||||
menus::invoke(&mut app, &cmd, p);
|
||||
assert_eq!(app.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some(super::KIND));
|
||||
let shown = painted_text(&mut app, |app, ui| crate::dialogs::show(app, ui.ctx()));
|
||||
for s in ["Set Corners", "Radius:", "Corner:", "Anchors:", "All Anchors", "Preview"] {
|
||||
assert!(shown.contains(s), "{s} in {shown}");
|
||||
}
|
||||
assert_eq!(cut(&app, id), vec![0, 1, 2, 3, 4], "all previewed");
|
||||
let n = app.session.doc().unwrap().history.undo.len();
|
||||
app.ui.dialog.as_mut().unwrap().fields.insert("every".into(), json!("odd"));
|
||||
let shown = painted_text(&mut app, |app, ui| crate::dialogs::show(app, ui.ctx()));
|
||||
assert!(shown.contains("Odd-Numbered Anchors"), "{shown}");
|
||||
assert_eq!(cut(&app, id), vec![0, 2, 4], "odd previewed");
|
||||
crate::dialogs::cancel(&mut app);
|
||||
assert_eq!(cut(&app, id), Vec::<usize>::new(), "rolled back");
|
||||
menus::invoke(&mut app, &cmd, serde_json::Value::Null);
|
||||
let d = app.ui.dialog.as_mut().unwrap();
|
||||
d.fields.insert("every".into(), json!("even"));
|
||||
d.fields.insert("radius".into(), json!(6));
|
||||
painted_text(&mut app, |app, ui| crate::dialogs::show(app, ui.ctx()));
|
||||
crate::dialogs::confirm(&mut app).unwrap();
|
||||
assert!(app.ui.dialog.is_none());
|
||||
assert_eq!(app.session.doc().unwrap().history.undo.len(), n + 1);
|
||||
assert_eq!(cut(&app, id), vec![1, 3]);
|
||||
}
|
||||
}
|
||||
@@ -417,6 +417,7 @@ registry! {
|
||||
RearrangeArtboards: [rearrange_artboards::KIND] => rearrange_artboards::SPEC,
|
||||
PathToolOptions: [crate::addons::pathtools::KIND] => crate::addons::pathtools::OPTIONS_SPEC,
|
||||
DivideDashes: [crate::addons::pathtools::dashes::KIND] => crate::addons::pathtools::dashes::SPEC,
|
||||
SetCorners: [crate::addons::pathtools::set_corners::KIND] => crate::addons::pathtools::set_corners::SPEC,
|
||||
NearestSwatch: [crate::addons::color::nearest::KIND] => crate::addons::color::nearest::SPEC,
|
||||
SwatchLegend: [crate::addons::color::legend::KIND] => crate::addons::color::legend::SPEC,
|
||||
TextTweaker: [crate::addons::typetools::KIND] => crate::addons::typetools::SPEC,
|
||||
|
||||
@@ -68,6 +68,9 @@ pub(super) fn menu_dialog(app: &VectorcraftApp, id: &str) -> Option<(&'static st
|
||||
("splitIntoGrid", crate::dialogs::path_ops::split_fields(size.0, size.1))
|
||||
}
|
||||
"object.vectorHalftone" => (crate::dialogs::halftone::KIND, crate::dialogs::halftone::fields()),
|
||||
crate::addons::pathtools::set_corners::COMMAND => {
|
||||
(crate::addons::pathtools::set_corners::KIND, crate::addons::pathtools::set_corners::fields(app))
|
||||
}
|
||||
crate::addons::pathtools::dashes::COMMAND => (crate::addons::pathtools::dashes::KIND, crate::addons::pathtools::dashes::fields()),
|
||||
crate::addons::color::nearest::COMMAND => (crate::addons::color::nearest::KIND, crate::addons::color::nearest::fields()),
|
||||
crate::addons::color::legend::COMMAND => (crate::addons::color::legend::KIND, crate::addons::color::legend::fields()),
|
||||
|
||||
+5
-2
@@ -6627,13 +6627,16 @@ Commands:
|
||||
drawn, and the path's fills stay on it, below its dashes (a path without fills goes). A compound path's dashes are
|
||||
plain paths with its appearance. Paths without a dashed stroke stay as they are, and it is an error if none has one.
|
||||
The dashes are selected; more than 100,000 (or a pattern too fine to draw) is refused. → `{ids, dashes}`.
|
||||
- `addon.pathtools.cornerSet {ids?, anchors?, items?, radius, kind?}` cuts live corners, the ones Direct Selection's
|
||||
- `addon.pathtools.cornerSet {ids?, anchors?, items?, radius, kind?, every?}` cuts live corners, the ones Direct Selection's
|
||||
corner widgets and the Corners dialog edit, as one undo step. `ids` (default: the selection) are the paths and
|
||||
`anchors` the anchor indices of each path's outline with no corners cut; without `anchors`, a path's direct-selected
|
||||
anchors, or all of them. `items: [{id, anchors?}…]` gives each path its own instead. Only anchors where two sides meet
|
||||
at an angle take a corner: smooth anchors and open ends are skipped, and it is an error if none is left. `radius` is
|
||||
0 to 1,000,000 pt, drawn no larger than fits; `kind` is `round`, `invertedRound` or `chamfer` (default: each corner's
|
||||
own). → `{ids, corners}`.
|
||||
own). `every` (`all`, `odd` or `even`; default `all`) keeps, of those anchors, only the odd- or even-numbered ones,
|
||||
counting each path's anchors from 1 (the 1st, 3rd, 5th… are odd): an anchor that takes no corner still counts, and it
|
||||
is an error if none is left. Object › Plug-ins › Path Precision Tools › Set Corners… (also in the Anchor Inspector's
|
||||
Corners section) sets these with Preview. → `{ids, corners}`.
|
||||
- `addon.pathtools.cornerRemove {ids?, anchors?, items?}` makes those corners sharp again, as one undo step; a plain path
|
||||
with none left is plain again. → `{ids, corners}`.
|
||||
- `addon.pathtools.slideAnchor {id, subpath?, anchor, t}` moves an anchor along its path, the segments beside it refitted
|
||||
|
||||
@@ -23,6 +23,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-04 | minor | Path Precision Tools add-on (a VectorCraft addition): Set Corners… and `addon.pathtools.cornerSet` `every` round all, the odd-numbered or the even-numbered anchors of each path (#5546); evidence added; scores unchanged. |
|
||||
| 2026-11-04 | minor | Transform Each's Scale has the documented Relative and Absolute radio buttons (`object.transformEach` `relative`: percentages of each object's size, or the width and height it becomes, measured on each object's own bounds) and the link that keeps the two scales in proportion; the panel lists Scale, Move, Rotate, the Options in two columns, the Reference Point and Preview (level with Copy, Cancel and OK) in the documented order. Scores and estimates unchanged. |
|
||||
| 2026-11-05 | minor | PDF import: a DeviceN gradient of a spot ink over process plates keeps the ink and the plates, and PDF export writes it as a DeviceN shading again (#3235); scores unchanged |
|
||||
| 2026-11-05 | minor | Optical kerning moves each pair part of the way toward the font's own stem spacing, with a minimum gap and tighter limits, and leaves ideographs, kana, fullwidth forms and dashes on the font's spacing, so text is no longer squeezed (#5336). Scores and estimates unchanged. |
|
||||
@@ -1473,6 +1474,8 @@ shows an anchor that can take a corner, a click cuts one with the tool's radius
|
||||
drag sizes it (Shift: whole values), a cut corner's marker selects it and loads its values, resizes it, and removes it
|
||||
at 0 or on a double-click, a corner never grows past what fits, and an Alt-marquee cuts every eligible anchor inside. A
|
||||
Corners section in the Anchor Inspector sets the radius and kind, counts the selected corners and applies to them.
|
||||
Set Corners… (in the submenu and the Corners section) sets the radius and kind with Preview, on all the chosen anchors
|
||||
or only the odd- or even-numbered ones (`every`, #5546).
|
||||
Slide Anchor Along Path drags an anchor along its curve, the two segments beside it refitted by the keep-shape kernel
|
||||
(Shift: where the tangent is a multiple of 45°). Commands `addon.pathtools.cornerSet`, `cornerRemove` and `slideAnchor`,
|
||||
each one undo step; `vectorcraft_pathops::addons::pathtools::slide_anchor`. Tests: `addons::pathtools` in
|
||||
|
||||
+3
-1
@@ -1,6 +1,6 @@
|
||||
# UI and interaction parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-04 · **Change:** minor (Artboards panel multi-select with any tool, Fill for every selected artboard; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-04 · **Change:** minor (Path Precision Tools: Set Corners… rounds every other anchor; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged)
|
||||
|
||||
## Feature submission evidence
|
||||
|
||||
@@ -379,6 +379,7 @@ next unit in the order a right-click on a ruler lists them, from Feet back to Po
|
||||
| Smart Pen add-on (VectorCraft addition): Object › Plug-ins › Smart Pen selects the Smart Pen Tool, which the All Tools drawer lists under its own heading but neither toolbar shows as shipped (no shortcut), and opens the Smart Pen panel (next segment straight or curved; Remove Points Keep Shape, Make Smooth, Make Corner, Retract Handles, Close or Join). The tool draws (click: straight or curved segment, a curve after a straight one tangent; drag: smooth anchor, Alt breaks the handle, Shift 45°, Space moves it; Tab switches a segment; rubber band; close, join, continue) and edits without switching tools (click selects a point, drag moves it, Alt-click removes it keeping the shape, Shift+Alt-click deletes it, double-click toggles smooth/corner; handle drags with Shift and Alt; segment drags reshape, Alt-click adds a point) | done | `addon.pen.*`, `crates/tools/src/addons/smart_pen.rs`, `crates/ui-egui/src/addons/pen.rs` and their tests; not a feature of the reference app, so it is not scored |
|
||||
| Path Precision Tools add-on (VectorCraft addition): Object › Plug-ins › Path Precision Tools selects its seven tools (Anchor Eraser Brush, Extend Path, Measure & Inspect, Parametric Shape, Corner Tool, Slide Anchor Along Path, Shared Edge Merge Tool), which the All Tools drawer lists under their own heading but neither toolbar shows as shipped, opens the Anchor Inspector panel (the selected anchor's X/Y and handle lengths and angles as fields; Make Smooth, Make Corner, Equalize Handles, Remove Points Keep Shape, Add Points, the three ways to close) and the Parametric Shape panel (kind, count, W/H, rotation, the kind's own values and corner radius of the selected parametric shape, or of the tool's next one), and runs Add Points at / Split at Crossings and Convert to Parametric Shape; the Parametric Shape Tool draws ten re-editable kinds from the centre (↑/↓ and ←/→ change their numbers while dragging) and edits a selected one with handles; the Corner Tool cuts editable corners on any path (hover shows the anchor, a click uses its radius and kind, a drag sizes the corner, Shift whole values; a corner's marker selects it and loads its values, drags to resize, to 0 or double-clicked removes it; never past what fits; Alt-marquee cuts every anchor inside) with a Corners section in the Anchor Inspector (radius, kind, selected count, Apply to Selected Anchors); Slide Anchor Along Path drags an anchor along its curve with the segments beside it refitted, Shift snapping where the tangent is a multiple of 45°; the Shared Edge Merge Tool shows, over two paths touching at two coincident anchors, the two stretches and the edge merging them, a click merges them (half way; Shift the upper path, Alt the lower), and near anchors meet or an anchor one path lacks is added to match (Shift+Alt asks for this inside a stretch), with a Shared Edges section in the Anchor Inspector (brush diameter, coincidence 0 to 3 pt, Keep Shared Points Together, which takes coincident anchors along with the add-on's own anchor moves); a double-click on the brush or Extend Path tool opens its options | done | `addon.pathtools.*`, `crates/tools/src/addons/`, `crates/ui-egui/src/addons/pathtools.rs`, `crates/ui-egui/src/addons/pathtools/` and their tests; not a feature of the reference app, so it is not scored |
|
||||
| Path Precision Tools add-on (VectorCraft addition): Object › Plug-ins › Path Precision Tools › Divide into Dashes… (Dash and Gap in any unit, Fit to Corners and Ends, Remove Gaps; Preview) cuts the selected paths, or only their direct-selected segments, into open pieces along the curve, keeping their appearance, one undo step | done | #4950; `addon.pathtools.divideDashes`, `crates/pathops/src/addons/pathtools/dashes.rs`, `crates/ui-egui/src/addons/pathtools/dashes.rs` and their tests; not a feature of the reference app, so it is not scored |
|
||||
| Path Precision Tools add-on (VectorCraft addition): Object › Plug-ins › Path Precision Tools › Set Corners… (also a button in the Anchor Inspector's Corners section: Radius in any unit, Corner kind, Anchors: All, Odd-Numbered or Even-Numbered, counted from each path's first anchor; Preview) cuts corners on the chosen anchors of every selected path, one undo step | done | #5546; `addon.pathtools.cornerSet` (`every`), `crates/ui-egui/src/addons/pathtools/set_corners.rs` and their tests; not a feature of the reference app, so it is not scored |
|
||||
| Path Precision Tools add-on (VectorCraft addition): Object › Plug-ins › Path Precision Tools › Expand Dashes into Paths turns each selected path's dashed stroke into separate open paths, one per dash, laid out as the stroke draws them (pattern, offset, Align Dashes to Corners and Path Ends), each keeping the stroke with the dashes off; one undo step | done | #4922; `addon.pathtools.expandDashes` (in `crates/engine/src/cmd/addons/pathtools.rs`, using the renderer's dash layout) and its tests; not a feature of the reference app, so it is not scored |
|
||||
| Path Precision Tools add-on (VectorCraft addition): Effect › Path Effects lists its live effects (Dashes to Geometry, Add Anchors, Extend Path, Close Path, Smooth Anchors, Retract Handles, Reverse Direction, Simplify Anchors, Split at Anchors), each with the effect dialog's live Preview except Reverse Direction, which has no options, and listed in the Appearance panel like any effect | done | `addon.path.*`, `crates/effects/src/addons/path.rs`, `crates/ui-egui/src/addons/pathtools.rs` and their tests; not a feature of the reference app, so it is not scored |
|
||||
| Path Precision Tools add-on (VectorCraft addition): Object › Plug-ins › Path Precision Tools selects its four tools (Anchor Eraser Brush, Extend Path, Measure & Inspect, Parametric Shape), which the All Tools drawer lists under their own heading but neither toolbar shows as shipped, opens the Anchor Inspector panel (the selected anchor's X/Y and handle lengths and angles as fields; Make Smooth, Make Corner, Equalize Handles, Remove Points Keep Shape, Add Points, the three ways to close) and the Parametric Shape panel (kind, count, W/H, rotation, the kind's own values and corner radius of the selected parametric shape, or of the tool's next one), and runs Add Points at / Split at Crossings and Convert to Parametric Shape; the Parametric Shape Tool draws ten re-editable kinds from the centre (↑/↓ and ←/→ change their numbers while dragging) and edits a selected one with handles; a double-click on the brush or Extend Path tool opens its options | done | `addon.pathtools.*`, `crates/tools/src/addons/`, `crates/ui-egui/src/addons/pathtools.rs`, `crates/ui-egui/src/addons/pathtools/shape.rs` and their tests; not a feature of the reference app, so it is not scored |
|
||||
@@ -432,6 +433,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-04 | minor | Path Precision Tools add-on (a VectorCraft addition): Set Corners… rounds all, the odd-numbered or the even-numbered anchors (#5546); scores and estimates unchanged |
|
||||
| 2026-11-04 | minor | New Document: the units chosen in Preset Details belong to the chosen document, as the pane's options are listed "for the selected preset" ([public source](https://helpx.adobe.com/illustrator/desktop/add-and-import-files/start-a-new-file/create-and-save-custom-document-presets.html)); the chosen card shows them, every other Recent and Saved card keeps the units its document or preset was made in, and the built-in tabs still follow the chosen units (#2078); scores and estimates unchanged |
|
||||
| 2026-11-01 | minor | Object › Image Trace › Batch Trace Folder asks for a source and a destination folder and traces each image into it (#4921); scores and estimates unchanged |
|
||||
| 2026-11-01 | minor | Edit › Copy and Cut take the selected ruler guides, with the objects or alone; pasting puts them on the layer new art goes to, moved as the objects are (alone: where they were), Paste in Place onto the active artboard and Paste on All Artboards onto each (#3226); scores and estimates unchanged |
|
||||
|
||||
Reference in New Issue
Block a user