From 62c6d42dffa938c37408cbcd2001bce8d61cdc98 Mon Sep 17 00:00:00 2001 From: Don Omondi <282319807+mockydevs@users.noreply.github.com> Date: Thu, 5 Nov 2026 11:04:52 +0700 Subject: [PATCH] Set Corners: round only the corners in an angle range (#5545) Squashed from #5737 onto main. --- .../src/cmd/addons/pathtools/corners.rs | 81 ++++++++++++++++--- .../engine/src/cmd/addons/pathtools/tests.rs | 77 ++++++++++++++++++ crates/engine/tests/command_sweep.rs | 3 + crates/ui-egui/src/addons/pathtools/corner.rs | 2 +- .../src/addons/pathtools/set_corners.rs | 57 ++++++++++++- crates/ui-egui/src/i18n/bg.tsv | 2 + crates/ui-egui/src/i18n/bs.tsv | 2 + crates/ui-egui/src/i18n/ca.tsv | 2 + crates/ui-egui/src/i18n/cs.tsv | 2 + crates/ui-egui/src/i18n/da.tsv | 2 + crates/ui-egui/src/i18n/de.tsv | 2 + crates/ui-egui/src/i18n/el.tsv | 2 + crates/ui-egui/src/i18n/es.tsv | 2 + crates/ui-egui/src/i18n/fi.tsv | 2 + crates/ui-egui/src/i18n/fr.tsv | 2 + crates/ui-egui/src/i18n/hu.tsv | 2 + crates/ui-egui/src/i18n/id.tsv | 2 + crates/ui-egui/src/i18n/it.tsv | 2 + crates/ui-egui/src/i18n/ja.tsv | 2 + crates/ui-egui/src/i18n/ka.tsv | 2 + crates/ui-egui/src/i18n/ko.tsv | 2 + crates/ui-egui/src/i18n/nl.tsv | 2 + crates/ui-egui/src/i18n/pl.tsv | 2 + crates/ui-egui/src/i18n/pt-br.tsv | 2 + crates/ui-egui/src/i18n/ru.tsv | 2 + crates/ui-egui/src/i18n/sv.tsv | 2 + crates/ui-egui/src/i18n/tr.tsv | 2 + crates/ui-egui/src/i18n/uk.tsv | 2 + crates/ui-egui/src/i18n/vi.tsv | 2 + crates/ui-egui/src/i18n/zh-hans.tsv | 2 + crates/ui-egui/src/i18n/zh-hant.tsv | 2 + docs/mcp.md | 6 +- docs/target-app-parity.md | 4 +- docs/ui-parity.md | 5 +- 34 files changed, 264 insertions(+), 23 deletions(-) diff --git a/crates/engine/src/cmd/addons/pathtools/corners.rs b/crates/engine/src/cmd/addons/pathtools/corners.rs index 2da1a5c17..2f035d8ad 100644 --- a/crates/engine/src/cmd/addons/pathtools/corners.rs +++ b/crates/engine/src/cmd/addons/pathtools/corners.rs @@ -20,7 +20,7 @@ pub(super) fn specs() -> Vec { "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), 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)}", + "{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), minAngle?: degrees 0..180 (default 0), maxAngle?: degrees 0..180 (default 180; of those anchors, only the corners whose sides meet at an angle in this range: small for a sharp spike, near 180 for a shallow bend)} cut live corners at the anchors that can have one, as one undo step → {ids, corners (how many)}", has_doc, corner_set ), @@ -84,19 +84,74 @@ impl Every { } } - /// The error when none of the chosen corners is one it takes. - fn none(self) -> &'static str { + /// Where the corners it takes are, for an error: "" for all of them. + fn at(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…)", + Self::All => "", + Self::Odd => " at an odd-numbered anchor (the 1st, 3rd, 5th…)", + Self::Even => " at an even-numbered anchor (the 2nd, 4th, 6th…)", + } + } +} + +/// How far (degrees) past the ends of the angle range a corner's angle may be, so that a typed 90 +/// takes a rectangle's corners whatever the rounding. +const ANGLE_SLACK: f64 = 1e-6; +/// The error when no anchor named is a corner at all. +const NO_CORNER: &str = "choose anchors where two sides meet at an angle: only those take a corner"; + +/// Which of the chosen corners `cornerSet` takes: by their number (`every`) and by the angle their +/// sides meet at (`minAngle`..`maxAngle`, degrees). +#[derive(Clone, Copy, Debug, PartialEq)] +struct Pick { + every: Every, + min_angle: f64, + max_angle: f64, +} + +impl Pick { + /// Every chosen corner. + const ALL: Self = Self { every: Every::All, min_angle: 0.0, max_angle: 180.0 }; + + /// The `every`, `minAngle` and `maxAngle` of `p`. + fn of(p: &Value, cmd: &str) -> Result { + let every = Every::of(p, cmd)?; + let angle = |key: &str, default: f64| -> Result { + let a = number_or(p, key, default, cmd)?; + if !(a.is_finite() && (0.0..=180.0).contains(&a)) { + return Err(bad(cmd, format!("`{key}` must be from 0 to 180 degrees, not {a}"))); + } + Ok(a) + }; + let (min_angle, max_angle) = (angle("minAngle", 0.0)?, angle("maxAngle", 180.0)?); + if min_angle > max_angle { + return Err(bad(cmd, format!("`minAngle` ({min_angle}°) can't be more than `maxAngle` ({max_angle}°)"))); + } + Ok(Self { every, min_angle, max_angle }) + } + + /// Does it take corner `index` of `lc`? + fn takes(&self, lc: &LiveCorners, index: usize) -> bool { + self.every.takes(index) + && lc.corner(index).is_some_and(|c| (self.min_angle - ANGLE_SLACK..=self.max_angle + ANGLE_SLACK).contains(&c.angle_deg())) + } + + /// The error when it takes none of the chosen corners. + fn none(&self) -> String { + let at = self.every.at(); + if self.min_angle > 0.0 || self.max_angle < 180.0 { + format!("none of the chosen corners{at} has an angle from {}° to {}°", self.min_angle, self.max_angle) + } else if at.is_empty() { + NO_CORNER.into() + } else { + format!("none of the chosen corners is{at}") } } } /// 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)>> { +/// that `pick` takes. +fn named_corners(s: &Session, p: &Value, cmd: &str, pick: Pick) -> Result)>> { let shared = anchors_param(p, cmd)?; // Each path with its own `anchors`, or sharing the request's. let mut own: Vec>> = vec![]; @@ -130,13 +185,13 @@ fn named_corners(s: &Session, p: &Value, cmd: &str, every: Every) -> Result 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 = corners.into_iter().filter(|k| every.takes(*k)).collect(); + let corners: BTreeSet = corners.into_iter().filter(|k| pick.takes(&lc, *k)).collect(); if !corners.is_empty() { out.push((*id, corners)); } } if out.is_empty() { - return Err(bad(cmd, if chosen { every.none() } else { Every::All.none() })); + return Err(bad(cmd, if chosen { pick.none() } else { NO_CORNER.into() })); } Ok(out) } @@ -181,12 +236,12 @@ fn corner_set(s: &mut Session, p: &Value) -> Result { .map_err(|_| bad(C, format!("`kind` must be \"round\", \"invertedRound\" or \"chamfer\", not {v}"))) }) .transpose()?; - let every = Every::of(p, C)?; - let corners = named_corners(s, p, C, every)?; + let pick = Pick::of(p, C)?; + let corners = named_corners(s, p, C, pick)?; edit_corners(s, "Set Corners", corners, radius, kind) } fn corner_remove(s: &mut Session, p: &Value) -> Result { - let corners = named_corners(s, p, "addon.pathtools.cornerRemove", Every::All)?; + let corners = named_corners(s, p, "addon.pathtools.cornerRemove", Pick::ALL)?; edit_corners(s, "Remove Corners", corners, 0.0, Some(CornerKind::Round)) } diff --git a/crates/engine/src/cmd/addons/pathtools/tests.rs b/crates/engine/src/cmd/addons/pathtools/tests.rs index 33e7eff8e..e31aadf56 100644 --- a/crates/engine/src/cmd/addons/pathtools/tests.rs +++ b/crates/engine/src/cmd/addons/pathtools/tests.rs @@ -654,6 +654,83 @@ fn corner_set_every_refuses_bad_values_and_says_when_nothing_is_left() { assert!(e.contains("two sides meet"), "{e}"); } +/// A closed house: a 100 × 100 square under a roof, its corners 90°, 90°, 135°, 90° and 135°. +fn house(s: &mut Session) -> NodeId { + line(s, &[(250.0, 100.0), (350.0, 100.0), (350.0, 200.0), (300.0, 250.0), (250.0, 200.0)], true) +} + +#[test] +fn corner_set_angle_range_rounds_only_the_corners_in_it_on_each_path() { + let mut s = session(); + // The star's points meet at about 38°, its inner corners at about 110°. + let (star, home) = (star_path(&mut s), house(&mut s)); + select(&mut s, &[star, home]); + let n = steps(&s); + // The sharp spikes only: the house has none, the star five, as one step. + let r = s.execute("addon.pathtools.cornerSet", &json!({"radius": 3, "maxAngle": 60})).unwrap(); + assert_eq!((r["ids"].clone(), r["corners"].clone(), steps(&s)), (json!([star.0]), json!(5), n + 1)); + assert_eq!((cut(&s, star), cut(&s, home)), (vec![0, 2, 4, 6, 8], vec![])); + s.execute("edit.undo", &json!({})).unwrap(); + assert_eq!((cut(&s, star), steps(&s)), (vec![], n)); + // Exactly 90°: the house's square corners. + s.execute("addon.pathtools.cornerSet", &json!({"radius": 3, "minAngle": 90, "maxAngle": 90})).unwrap(); + assert_eq!((cut(&s, star), cut(&s, home)), (vec![], vec![0, 1, 3])); + // 100° to 140°: the star's inner corners and the roof's; the square corners stay as they are. + let r = s.execute("addon.pathtools.cornerSet", &json!({"radius": 2, "minAngle": 100, "maxAngle": 140})).unwrap(); + assert_eq!(r["corners"], json!(7)); + assert_eq!((cut(&s, star), cut(&s, home)), (vec![1, 3, 5, 7, 9], vec![0, 1, 2, 3, 4])); + assert_eq!((drawn(&s, home, 0), drawn(&s, home, 2)), (3.0, 2.0)); + // On top of the named anchors and of `every`. + s.execute("addon.pathtools.cornerRemove", &json!({})).unwrap(); + s.execute("addon.pathtools.cornerSet", &json!({"ids": [home.0], "anchors": [1, 2, 3], "radius": 4, "minAngle": 80, "maxAngle": 100})).unwrap(); + assert_eq!(cut(&s, home), vec![1, 3]); + s.execute("addon.pathtools.cornerSet", &json!({"ids": [star.0], "radius": 4, "every": "even", "minAngle": 100})).unwrap(); + assert_eq!(cut(&s, star), vec![1, 3, 5, 7, 9]); +} + +#[test] +fn corner_set_angle_range_on_a_three_anchor_path() { + let mut s = session(); + // An open pen path of three anchors: one corner, of 90°. + let id = line(&mut s, &[(10.0, 10.0), (60.0, 60.0), (110.0, 10.0)], false); + select(&mut s, &[id]); + let e = s.execute("addon.pathtools.cornerSet", &json!({"radius": 5, "maxAngle": 45})).unwrap_err().to_string(); + assert!(e.contains("none of the chosen corners has an angle from 0° to 45°"), "{e}"); + let r = s.execute("addon.pathtools.cornerSet", &json!({"radius": 5, "minAngle": 80, "maxAngle": 100})).unwrap(); + assert_eq!(r["corners"], json!(1)); + assert_eq!(cut(&s, id), vec![1]); +} + +#[test] +fn corner_set_angle_range_refuses_bad_values_and_says_when_nothing_matches() { + let mut s = session(); + let star = star_path(&mut s); + select(&mut s, &[star]); + let i = star.0; + refuses( + &mut s, + "addon.pathtools.cornerSet", + &[ + json!({"ids": [i], "radius": 5, "minAngle": -1}), + json!({"ids": [i], "radius": 5, "maxAngle": 181}), + json!({"ids": [i], "radius": 5, "minAngle": 1e308}), + json!({"ids": [i], "radius": 5, "maxAngle": "sharp"}), + json!({"ids": [i], "radius": 5, "minAngle": 120, "maxAngle": 60}), + json!({"ids": [i], "radius": 5, "minAngle": 50, "maxAngle": 100}), + json!({"ids": [i], "radius": 5, "every": "odd", "minAngle": 100}), + ], + ); + let err = |s: &mut Session, p: Value| s.execute("addon.pathtools.cornerSet", &p).unwrap_err().to_string(); + let e = err(&mut s, json!({"ids": [i], "radius": 5, "maxAngle": 200})); + assert!(e.contains("`maxAngle` must be from 0 to 180 degrees"), "{e}"); + let e = err(&mut s, json!({"ids": [i], "radius": 5, "minAngle": 120, "maxAngle": 60})); + assert!(e.contains("`minAngle` (120°) can't be more than `maxAngle` (60°)"), "{e}"); + let e = err(&mut s, json!({"ids": [i], "radius": 5, "minAngle": 50, "maxAngle": 100})); + assert!(e.contains("none of the chosen corners has an angle from 50° to 100°"), "{e}"); + let e = err(&mut s, json!({"ids": [i], "radius": 5, "every": "odd", "minAngle": 100})); + assert!(e.contains("none of the chosen corners at an odd-numbered anchor (the 1st, 3rd, 5th…) has an angle from 100° to 180°"), "{e}"); +} + #[test] fn the_corner_tool_clicks_drags_and_removes_in_one_step_each() { let mut s = session(); diff --git a/crates/engine/tests/command_sweep.rs b/crates/engine/tests/command_sweep.rs index 8d973f267..b66083178 100644 --- a/crates/engine/tests/command_sweep.rs +++ b/crates/engine/tests/command_sweep.rs @@ -596,6 +596,9 @@ fn structured_junk() { ("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.cornerSet", json!({"ids": [2, 3], "radius": 5, "minAngle": 0, "maxAngle": 0})), + ("addon.pathtools.cornerSet", json!({"ids": [2], "radius": 5, "minAngle": 180, "maxAngle": 180, "every": "odd"})), + ("addon.pathtools.cornerSet", json!({"ids": [2], "radius": 5, "minAngle": f64::MAX, "maxAngle": -1e-300})), ("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})), diff --git a/crates/ui-egui/src/addons/pathtools/corner.rs b/crates/ui-egui/src/addons/pathtools/corner.rs index 358e4f0b3..36ee1bd1c 100644 --- a/crates/ui-egui/src/addons/pathtools/corner.rs +++ b/crates/ui-egui/src/addons/pathtools/corner.rs @@ -1,6 +1,6 @@ //! The Anchor Inspector panel's Corners section: the Corner Tool's radius and kind, how many corners //! the selection holds, Apply (`addon.pathtools.cornerSet`) and Set Corners… (its dialog, which -//! can also take every other anchor). +//! can also take every other anchor, or only the corners in an angle range). use egui::Ui; use serde_json::{Value, json}; diff --git a/crates/ui-egui/src/addons/pathtools/set_corners.rs b/crates/ui-egui/src/addons/pathtools/set_corners.rs index 52800fed6..59ba7035a 100644 --- a/crates/ui-egui/src/addons/pathtools/set_corners.rs +++ b/crates/ui-egui/src/addons/pathtools/set_corners.rs @@ -1,6 +1,7 @@ -//! 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. +//! Set Corners in the interface: its dialog (radius, corner kind, which anchors: all, the +//! odd-numbered or the even-numbered ones, and the range of corner angles to round), 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, every corner angle (0° to 180°). use egui::Ui; use serde_json::{Value, json}; @@ -20,13 +21,16 @@ const KINDS: [(&str, &str); 3] = [("round", "Round"), ("invertedRound", "Inverte 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; +const ANGLE_W: f32 = 64.0; +/// The corner angles the command takes (degrees). +const ANGLES: std::ops::RangeInclusive = 0.0..=180.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 }) + json!({ "radius": radius, "kind": kind, "every": "all", "minAngle": 0.0, "maxAngle": 180.0, "preview": true }) } pub(crate) const SPEC: DialogSpec = @@ -39,6 +43,24 @@ fn body(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog) -> bool { }); form::choice(ui, d, "kind", tl!("Corner:"), (LABEL_W, FIELD_W), &KINDS); form::choice(ui, d, "every", tl!("Anchors:"), (LABEL_W, FIELD_W), &EVERY); + // Only the corners whose sides meet at an angle in this range: small for a sharp spike, near + // 180° for a shallow bend. Typing one end past the other moves the other along. + let (min, max) = (d.f64("minAngle", 0.0), d.f64("maxAngle", 180.0)); + widgets::label_row(ui, tl!("Angle:"), LABEL_W, |ui| { + if let Some(v) = widgets::range_field_named(ui, "set-corners-min-angle", Some(tl!("Minimum angle")), min, ANGLES, "°", 1, ANGLE_W) { + d.fields.insert("minAngle".into(), json!(v)); + if v > max { + d.fields.insert("maxAngle".into(), json!(v)); + } + } + ui.label(tl!("to")); + if let Some(v) = widgets::range_field_named(ui, "set-corners-max-angle", Some(tl!("Maximum angle")), max, ANGLES, "°", 1, ANGLE_W) { + d.fields.insert("maxAngle".into(), json!(v)); + if v < min { + d.fields.insert("minAngle".into(), json!(v)); + } + } + }); form::preview(app, ui, d, "Set Corners", COMMAND, form::params(d)); false } @@ -98,4 +120,31 @@ mod tests { assert_eq!(app.session.doc().unwrap().history.undo.len(), n + 1); assert_eq!(cut(&app, id), vec![1, 3]); } + + #[test] + fn set_corners_previews_only_the_corners_in_its_angle_range() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 400, "height": 300})).unwrap(); + // A house: square corners at 0, 1 and 3, the roof's 135° corners at 2 and 4. + let anchors = json!([{"x": 0, "y": 0}, {"x": 100, "y": 0}, {"x": 100, "y": 100}, {"x": 50, "y": 150}, {"x": 0, "y": 100}]); + let id = app.run("path.create", json!({"anchors": anchors, "closed": true})).unwrap()["id"].as_u64().unwrap(); + app.run("select.set", json!({"ids": [id]})).unwrap(); + menus::invoke(&mut app, super::COMMAND, json!({})); + let shown = painted_text(&mut app, |app, ui| crate::dialogs::show(app, ui.ctx())); + for s in ["Angle:", "to", "0°", "180°"] { + assert!(shown.contains(s), "{s} in {shown}"); + } + let n = app.session.doc().unwrap().history.undo.len(); + let d = app.ui.dialog.as_mut().unwrap(); + d.fields.insert("minAngle".into(), json!(120)); + painted_text(&mut app, |app, ui| crate::dialogs::show(app, ui.ctx())); + assert_eq!(cut(&app, id), vec![2, 4], "the roof previewed"); + app.ui.dialog.as_mut().unwrap().fields.insert("maxAngle".into(), json!(100)); + app.ui.dialog.as_mut().unwrap().fields.insert("minAngle".into(), json!(80)); + painted_text(&mut app, |app, ui| crate::dialogs::show(app, ui.ctx())); + assert_eq!(cut(&app, id), vec![0, 1, 3], "the square corners previewed"); + crate::dialogs::confirm(&mut app).unwrap(); + assert_eq!(app.session.doc().unwrap().history.undo.len(), n + 1); + assert_eq!(cut(&app, id), vec![0, 1, 3]); + } } diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 4c43cceb5..8948a1482 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -6201,3 +6201,5 @@ breakText Glyphs Глифи All Anchors Всички опорни точки Odd-Numbered Anchors Нечетни опорни точки Even-Numbered Anchors Четни опорни точки + Minimum angle Минимален ъгъл + Maximum angle Максимален ъгъл diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index 611cc970c..f6410a827 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -6235,3 +6235,5 @@ shape Open Otvoren All Anchors Sva sidra Odd-Numbered Anchors Neparna sidra Even-Numbered Anchors Parna sidra + Minimum angle Najmanji ugao + Maximum angle Najveći ugao diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 3c0158af2..afedb11f9 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -6336,3 +6336,5 @@ breakText Glyphs Glifs All Anchors Tots els ancoratges Odd-Numbered Anchors Ancoratges senars Even-Numbered Anchors Ancoratges parells + Minimum angle Angle mínim + Maximum angle Angle màxim diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index f19c56d39..fc3a7c5cc 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -6350,3 +6350,5 @@ breakText Glyphs Glyfy All Anchors Všechny kotevní body Odd-Numbered Anchors Liché kotevní body Even-Numbered Anchors Sudé kotevní body + Minimum angle Minimální úhel + Maximum angle Maximální úhel diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index e4c908156..d58f0610b 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -6217,3 +6217,5 @@ breakText Glyphs Glyffer All Anchors Alle ankre Odd-Numbered Anchors Ulige ankre Even-Numbered Anchors Lige ankre + Minimum angle Mindste vinkel + Maximum angle Største vinkel diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 8e2e67985..d88d86b5f 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -5936,3 +5936,5 @@ breakText Glyphs Glyphen All Anchors Alle Ankerpunkte Odd-Numbered Anchors Ungerade Ankerpunkte Even-Numbered Anchors Gerade Ankerpunkte + Minimum angle Minimaler Winkel + Maximum angle Maximaler Winkel diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index f05becf60..1c116fe3d 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -6302,3 +6302,5 @@ breakText Glyphs Γλύφοι All Anchors Όλα τα σημεία αγκύρωσης Odd-Numbered Anchors Μονά σημεία αγκύρωσης Even-Numbered Anchors Ζυγά σημεία αγκύρωσης + Minimum angle Ελάχιστη γωνία + Maximum angle Μέγιστη γωνία diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 074fccb60..d9bad3fc8 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -6292,3 +6292,5 @@ breakText Glyphs Glifos All Anchors Todas las anclas Odd-Numbered Anchors Anclas impares Even-Numbered Anchors Anclas pares + Minimum angle Ángulo mínimo + Maximum angle Ángulo máximo diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 9651fdd43..d66e806d8 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -6385,3 +6385,5 @@ breakText Glyphs Merkit All Anchors Kaikki ankkurit Odd-Numbered Anchors Parittomat ankkurit Even-Numbered Anchors Parilliset ankkurit + Minimum angle Pienin kulma + Maximum angle Suurin kulma diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 2cd0ae07c..4ddf66a51 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -6345,3 +6345,5 @@ breakText Glyphs Glyphes All Anchors Tous les points d’ancrage Odd-Numbered Anchors Points d’ancrage impairs Even-Numbered Anchors Points d’ancrage pairs + Minimum angle Angle minimal + Maximum angle Angle maximal diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index a30e8e794..b42cb25bb 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -6334,3 +6334,5 @@ breakText Glyphs Glifák All Anchors Összes horgonypont Odd-Numbered Anchors Páratlan horgonypontok Even-Numbered Anchors Páros horgonypontok + Minimum angle Legkisebb szög + Maximum angle Legnagyobb szög diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 678e3fb42..843de1391 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -6217,3 +6217,5 @@ breakText Glyphs Glif All Anchors Semua jangkar Odd-Numbered Anchors Jangkar bernomor ganjil Even-Numbered Anchors Jangkar bernomor genap + Minimum angle Sudut minimum + Maximum angle Sudut maksimum diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 9c92dc5e4..2a90453d4 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -5970,3 +5970,5 @@ breakText Glyphs Glifi All Anchors Tutti gli ancoraggi Odd-Numbered Anchors Ancoraggi dispari Even-Numbered Anchors Ancoraggi pari + Minimum angle Angolo minimo + Maximum angle Angolo massimo diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index c0ec9be3e..a73e548d8 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -6035,3 +6035,5 @@ breakText Glyphs 字形 All Anchors すべてのアンカー Odd-Numbered Anchors 奇数番目のアンカー Even-Numbered Anchors 偶数番目のアンカー + Minimum angle 最小角度 + Maximum angle 最大角度 diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index a64323105..78de8173f 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -6361,3 +6361,5 @@ font-script Japanese იაპონური All Anchors ყველა საყრდენი წერტილი Odd-Numbered Anchors კენტი საყრდენი წერტილები Even-Numbered Anchors ლუწი საყრდენი წერტილები + Minimum angle მინიმალური კუთხე + Maximum angle მაქსიმალური კუთხე diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index c31f973b2..aefbd17df 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -5931,3 +5931,5 @@ breakText Glyphs 글리프 All Anchors 모든 앵커 Odd-Numbered Anchors 홀수 번째 앵커 Even-Numbered Anchors 짝수 번째 앵커 + Minimum angle 최소 각도 + Maximum angle 최대 각도 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index f2a20670e..dbb31fd12 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -6261,3 +6261,5 @@ breakText Glyphs Glyphs All Anchors Alle ankerpunten Odd-Numbered Anchors Oneven ankerpunten Even-Numbered Anchors Even ankerpunten + Minimum angle Minimale hoek + Maximum angle Maximale hoek diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 80e3e3f1e..88322303b 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -6205,3 +6205,5 @@ breakText Glyphs Glify All Anchors Wszystkie punkty Odd-Numbered Anchors Punkty nieparzyste Even-Numbered Anchors Punkty parzyste + Minimum angle Kąt minimalny + Maximum angle Kąt maksymalny diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index f9c911e85..0293e24e3 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -6423,3 +6423,5 @@ swatch Yellow Green Verde-amarelado All Anchors Todas as Âncoras Odd-Numbered Anchors Âncoras Ímpares Even-Numbered Anchors Âncoras Pares + Minimum angle Ângulo Mínimo + Maximum angle Ângulo Máximo diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 9d80cb092..9791fe1c3 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -6439,3 +6439,5 @@ shape Open Открытый All Anchors Все опорные точки Odd-Numbered Anchors Нечётные опорные точки Even-Numbered Anchors Чётные опорные точки + Minimum angle Минимальный угол + Maximum angle Максимальный угол diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 8ee422b5c..58f82f37b 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -6406,3 +6406,5 @@ digits Hindi Hindi All Anchors Alla ankare Odd-Numbered Anchors Udda ankare Even-Numbered Anchors Jämna ankare + Minimum angle Minsta vinkel + Maximum angle Största vinkel diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index e7d4b3ca1..ae5a58dd6 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -6307,3 +6307,5 @@ breakText Glyphs Glifler All Anchors Tüm Çapalar Odd-Numbered Anchors Tek Numaralı Çapalar Even-Numbered Anchors Çift Numaralı Çapalar + Minimum angle En Küçük Açı + Maximum angle En Büyük Açı diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index e00c216d5..363a8f6fb 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -5950,3 +5950,5 @@ breakText Glyphs Гліфи All Anchors Усі опорні точки Odd-Numbered Anchors Непарні опорні точки Even-Numbered Anchors Парні опорні точки + Minimum angle Мінімальний кут + Maximum angle Максимальний кут diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index f026b728f..2275c27d8 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -5951,3 +5951,5 @@ breakText Glyphs Glyph 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 + Minimum angle Góc tối thiểu + Maximum angle Góc tối đa diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index ad37f23cf..1d44f8cc6 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -6191,3 +6191,5 @@ breakText Glyphs 字形 All Anchors 所有锚点 Odd-Numbered Anchors 奇数锚点 Even-Numbered Anchors 偶数锚点 + Minimum angle 最小角度 + Maximum angle 最大角度 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index d4805dd9b..6d6b10751 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -5988,3 +5988,5 @@ breakText Glyphs 字符 All Anchors 所有錨點 Odd-Numbered Anchors 奇數錨點 Even-Numbered Anchors 偶數錨點 + Minimum angle 最小角度 + Maximum angle 最大角度 diff --git a/docs/mcp.md b/docs/mcp.md index c4506fc83..63f41074c 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -6627,7 +6627,7 @@ 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?, every?}` cuts live corners, the ones Direct Selection's +- `addon.pathtools.cornerSet {ids?, anchors?, items?, radius, kind?, every?, minAngle?, maxAngle?}` 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 @@ -6635,7 +6635,9 @@ Commands: 0 to 1,000,000 pt, drawn no larger than fits; `kind` is `round`, `invertedRound` or `chamfer` (default: each corner's 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 + is an error if none is left. `minAngle` and `maxAngle` (degrees 0 to 180; default 0 and 180) keep only the corners + whose sides meet at an angle in that range (small for a sharp spike, near 180 for a shallow bend); `minAngle` can't + be more than `maxAngle`, and it is an error, saying so, if no chosen corner is in the range. 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}`. diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index cbbb336c8..2f851ca41 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -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` `minAngle`/`maxAngle` round only the corners whose angle is in a range (#5545); evidence added; scores unchanged. | | 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 | @@ -1479,7 +1480,8 @@ drag sizes it (Shift: whole values), a cut corner's marker selects it and loads 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). +or only the odd- or even-numbered ones (`every`, #5546), and only the corners whose angle is in a range +(`minAngle`/`maxAngle`, #5545). 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 diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 35bdd5d62..3bf544019 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI and interaction parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-05 · **Change:** minor (Gradient panel: Delete Stop beside the ramp, #5403; 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-05 · **Change:** minor (Path Precision Tools: Set Corners… rounds only the corners in an angle range; 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 @@ -381,7 +381,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 › 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; Angle: from … to …°, only the corners whose sides meet at an angle in that range; Preview) cuts corners on the chosen anchors of every selected path, one undo step | done | #5546, #5545; `addon.pathtools.cornerSet` (`every`, `minAngle`, `maxAngle`), `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 | @@ -435,6 +435,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 only the corners in an angle range (#5545); scores and estimates unchanged | | 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 |