Merge pull request #5733 from mockydevs/feat/set-corners-odd-even-5546

Set Corners: round all, odd-numbered or even-numbered anchors (#5546)
This commit is contained in:
BFlat
2026-11-05 10:54:27 +07:00
committed by GitHub
37 changed files with 375 additions and 13 deletions
@@ -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();
+3
View File
@@ -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})),
+3 -1
View File
@@ -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]);
}
}
+1
View File
@@ -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,
+4
View File
@@ -6197,3 +6197,7 @@ breakText Glyphs Глифи
Fully Outside Изцяло извън
No artboards match. Няма съвпадащи работни области.
Chosen: {n} of {total} Избрани: {n} от {total}
Set Corners… Задаване на ъгли…
All Anchors Всички опорни точки
Odd-Numbered Anchors Нечетни опорни точки
Even-Numbered Anchors Четни опорни точки
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -6231,3 +6231,7 @@ shape Open Otvoren
Fully Outside Potpuno vani
No artboards match. Nijedna radna ploča ne odgovara.
Chosen: {n} of {total} Odabrano: {n} od {total}
Set Corners… Postavi uglove…
All Anchors Sva sidra
Odd-Numbered Anchors Neparna sidra
Even-Numbered Anchors Parna sidra
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -6332,3 +6332,7 @@ breakText Glyphs Glifs
Fully Outside Totalment fora
No artboards match. Cap tauler no coincideix.
Chosen: {n} of {total} Triats: {n} de {total}
Set Corners… Defineix les cantonades…
All Anchors Tots els ancoratges
Odd-Numbered Anchors Ancoratges senars
Even-Numbered Anchors Ancoratges parells
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -6346,3 +6346,7 @@ breakText Glyphs Glyfy
Fully Outside Zcela vně
No artboards match. Žádná pracovní plocha neodpovídá.
Chosen: {n} of {total} Zvoleno: {n} z {total}
Set Corners… Nastavit rohy…
All Anchors Všechny kotevní body
Odd-Numbered Anchors Liché kotevní body
Even-Numbered Anchors Sudé kotevní body
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -6213,3 +6213,7 @@ breakText Glyphs Glyffer
Fully Outside Helt udenfor
No artboards match. Ingen tegneflader passer.
Chosen: {n} of {total} Valgt: {n} af {total}
Set Corners… Angiv hjørner…
All Anchors Alle ankre
Odd-Numbered Anchors Ulige ankre
Even-Numbered Anchors Lige ankre
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -5932,3 +5932,7 @@ breakText Glyphs Glyphen
Fully Outside Vollständig außen
No artboards match. Keine Zeichenfläche passt.
Chosen: {n} of {total} Gewählt: {n} von {total}
Set Corners… Ecken festlegen…
All Anchors Alle Ankerpunkte
Odd-Numbered Anchors Ungerade Ankerpunkte
Even-Numbered Anchors Gerade Ankerpunkte
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -6298,3 +6298,7 @@ breakText Glyphs Γλύφοι
Fully Outside Πλήρως εκτός
No artboards match. Κανένας πίνακας σχεδίασης δεν ταιριάζει.
Chosen: {n} of {total} Επιλεγμένοι: {n} από {total}
Set Corners… Ορισμός γωνιών…
All Anchors Όλα τα σημεία αγκύρωσης
Odd-Numbered Anchors Μονά σημεία αγκύρωσης
Even-Numbered Anchors Ζυγά σημεία αγκύρωσης
Can't render this file because it contains an unexpected character in line 163 and column 5.
+4
View File
@@ -6288,3 +6288,7 @@ breakText Glyphs Glifos
Fully Outside Totalmente fuera
No artboards match. Ninguna mesa de trabajo coincide.
Chosen: {n} of {total} Elegidas: {n} de {total}
Set Corners… Definir esquinas…
All Anchors Todas las anclas
Odd-Numbered Anchors Anclas impares
Even-Numbered Anchors Anclas pares
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -6381,3 +6381,7 @@ breakText Glyphs Merkit
Fully Outside Kokonaan ulkona
No artboards match. Mikään piirtoalusta ei vastaa hakua.
Chosen: {n} of {total} Valittu: {n}/{total}
Set Corners… Aseta kulmat…
All Anchors Kaikki ankkurit
Odd-Numbered Anchors Parittomat ankkurit
Even-Numbered Anchors Parilliset ankkurit
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -6341,3 +6341,7 @@ breakText Glyphs Glyphes
Fully Outside Entièrement à l’extérieur
No artboards match. Aucun plan de travail ne correspond.
Chosen: {n} of {total} Choisis : {n} sur {total}
Set Corners… Définir les angles…
All Anchors Tous les points d’ancrage
Odd-Numbered Anchors Points d’ancrage impairs
Even-Numbered Anchors Points d’ancrage pairs
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -6330,3 +6330,7 @@ breakText Glyphs Glifák
Fully Outside Teljesen kívül
No artboards match. Nincs egyező rajztábla.
Chosen: {n} of {total} Kiválasztva: {n} / {total}
Set Corners… Sarkok beállítása…
All Anchors Összes horgonypont
Odd-Numbered Anchors Páratlan horgonypontok
Even-Numbered Anchors Páros horgonypontok
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -6213,3 +6213,7 @@ breakText Glyphs Glif
Fully Outside Sepenuhnya di luar
No artboards match. Tidak ada artboard yang cocok.
Chosen: {n} of {total} Dipilih: {n} dari {total}
Set Corners… Atur sudut…
All Anchors Semua jangkar
Odd-Numbered Anchors Jangkar bernomor ganjil
Even-Numbered Anchors Jangkar bernomor genap
Can't render this file because it contains an unexpected character in line 2235 and column 5.
+4
View File
@@ -5966,3 +5966,7 @@ breakText Glyphs Glifi
Fully Outside Completamente esterno
No artboards match. Nessuna tavola da disegno corrisponde.
Chosen: {n} of {total} Scelte: {n} di {total}
Set Corners… Imposta angoli…
All Anchors Tutti gli ancoraggi
Odd-Numbered Anchors Ancoraggi dispari
Even-Numbered Anchors Ancoraggi pari
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -6031,3 +6031,7 @@ breakText Glyphs 字形
Fully Outside 完全に外側
No artboards match. 一致するアートボードはありません。
Chosen: {n} of {total} 選択中: {n} / {total}
Set Corners… コーナーを設定…
All Anchors すべてのアンカー
Odd-Numbered Anchors 奇数番目のアンカー
Even-Numbered Anchors 偶数番目のアンカー
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -6357,3 +6357,7 @@ font-script Japanese იაპონური
Fully Outside მთლიანად გარეთ
No artboards match. შესაბამისი სამუშაო დაფა არ არის.
Chosen: {n} of {total} არჩეულია: {n} / {total}
Set Corners… კუთხეების დაყენება…
All Anchors ყველა საყრდენი წერტილი
Odd-Numbered Anchors კენტი საყრდენი წერტილები
Even-Numbered Anchors ლუწი საყრდენი წერტილები
Can't render this file because it is too large.
+4
View File
@@ -5927,3 +5927,7 @@ breakText Glyphs 글리프
Fully Outside 완전히 바깥쪽
No artboards match. 일치하는 아트보드가 없습니다.
Chosen: {n} of {total} 선택됨: {n} / {total}
Set Corners… 모퉁이 설정…
All Anchors 모든 앵커
Odd-Numbered Anchors 홀수 번째 앵커
Even-Numbered Anchors 짝수 번째 앵커
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -6257,3 +6257,7 @@ breakText Glyphs Glyphs
Fully Outside Volledig buiten
No artboards match. Geen tekengebied komt overeen.
Chosen: {n} of {total} Gekozen: {n} van {total}
Set Corners… Hoeken instellen…
All Anchors Alle ankerpunten
Odd-Numbered Anchors Oneven ankerpunten
Even-Numbered Anchors Even ankerpunten
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -6201,3 +6201,7 @@ breakText Glyphs Glify
Fully Outside Całkowicie na zewnątrz
No artboards match. Żaden obszar kompozycji nie pasuje.
Chosen: {n} of {total} Wybrano: {n} z {total}
Set Corners… Ustaw narożniki…
All Anchors Wszystkie punkty
Odd-Numbered Anchors Punkty nieparzyste
Even-Numbered Anchors Punkty parzyste
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -6419,3 +6419,7 @@ swatch Yellow Green Verde-amarelado
Fully Outside Totalmente fora
No artboards match. Nenhuma mesa de trabalho corresponde.
Chosen: {n} of {total} Escolhidas: {n} de {total}
Set Corners… Definir Cantos…
All Anchors Todas as Âncoras
Odd-Numbered Anchors Âncoras Ímpares
Even-Numbered Anchors Âncoras Pares
Can't render this file because it contains an unexpected character in line 6 and column 47.
+4
View File
@@ -6435,3 +6435,7 @@ shape Open Открытый
Fully Outside Полностью снаружи
No artboards match. Нет подходящих монтажных областей.
Chosen: {n} of {total} Выбрано: {n} из {total}
Set Corners… Задать углы…
All Anchors Все опорные точки
Odd-Numbered Anchors Нечётные опорные точки
Even-Numbered Anchors Чётные опорные точки
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -6402,3 +6402,7 @@ digits Hindi Hindi
Fully Outside Helt utanför
No artboards match. Ingen rityta matchar.
Chosen: {n} of {total} Valda: {n} av {total}
Set Corners… Ange hörn…
All Anchors Alla ankare
Odd-Numbered Anchors Udda ankare
Even-Numbered Anchors Jämna ankare
Can't render this file because it contains an unexpected character in line 5 and column 25.
+4
View File
@@ -6303,3 +6303,7 @@ breakText Glyphs Glifler
Fully Outside Tamamen dışarıda
No artboards match. Eşleşen çalışma yüzeyi yok.
Chosen: {n} of {total} Seçilen: {n}/{total}
Set Corners… Köşeleri Ayarla…
All Anchors Tüm Çapalar
Odd-Numbered Anchors Tek Numaralı Çapalar
Even-Numbered Anchors Çift Numaralı Çapalar
Can't render this file because it contains an unexpected character in line 5 and column 25.
+4
View File
@@ -5946,3 +5946,7 @@ breakText Glyphs Гліфи
Fully Outside Повністю зовні
No artboards match. Немає відповідних монтажних областей.
Chosen: {n} of {total} Вибрано: {n} з {total}
Set Corners… Задати кути…
All Anchors Усі опорні точки
Odd-Numbered Anchors Непарні опорні точки
Even-Numbered Anchors Парні опорні точки
Can't render this file because it contains an unexpected character in line 177 and column 5.
+4
View File
@@ -5947,3 +5947,7 @@ breakText Glyphs Glyph
Fully Outside Hoàn toàn bên ngoài
No artboards match. Không có bảng vẽ nào khớp.
Chosen: {n} of {total} Đã chọn: {n} trên {total}
Set Corners… Đặt góc…
All Anchors Tất cả điểm neo
Odd-Numbered Anchors Điểm neo số lẻ
Even-Numbered Anchors Điểm neo số chẵn
Can't render this file because it contains an unexpected character in line 199 and column 5.
+4
View File
@@ -6187,3 +6187,7 @@ breakText Glyphs 字形
Fully Outside 完全在外
No artboards match. 没有匹配的画板。
Chosen: {n} of {total} 已选:{n} / {total}
Set Corners… 设置转角…
All Anchors 所有锚点
Odd-Numbered Anchors 奇数锚点
Even-Numbered Anchors 偶数锚点
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -5984,3 +5984,7 @@ breakText Glyphs 字符
Fully Outside 完全在外
No artboards match. 沒有符合的工作區域。
Chosen: {n} of {total} 已選:{n} / {total}
Set Corners… 設定轉角…
All Anchors 所有錨點
Odd-Numbered Anchors 奇數錨點
Even-Numbered Anchors 偶數錨點
Can't render this file because it contains an unexpected character in line 6 and column 47.
+3
View File
@@ -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
View File
@@ -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
+3
View File
@@ -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
View File
@@ -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 |