Set Corners: round only the corners in an angle range (#5545)

Squashed from #5737 onto main.
This commit is contained in:
Don Omondi
2026-11-05 11:04:52 +07:00
committed by bflatastic
parent 077ef54d70
commit 62c6d42dff
34 changed files with 264 additions and 23 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), 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<Self> {
let every = Every::of(p, cmd)?;
let angle = |key: &str, default: f64| -> Result<f64> {
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<Vec<(NodeId, BTreeSet<usize>)>> {
/// that `pick` takes.
fn named_corners(s: &Session, p: &Value, cmd: &str, pick: Pick) -> 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![];
@@ -130,13 +185,13 @@ fn named_corners(s: &Session, p: &Value, cmd: &str, every: Every) -> Result<Vec<
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();
let corners: BTreeSet<usize> = 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<Value> {
.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<Value> {
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))
}
@@ -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();
+3
View File
@@ -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})),
@@ -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};
@@ -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<f64> = 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]);
}
}
+2
View File
@@ -6201,3 +6201,5 @@ breakText Glyphs Глифи
All Anchors Всички опорни точки
Odd-Numbered Anchors Нечетни опорни точки
Even-Numbered Anchors Четни опорни точки
Minimum angle Минимален ъгъл
Maximum angle Максимален ъгъл
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -6302,3 +6302,5 @@ breakText Glyphs Γλύφοι
All Anchors Όλα τα σημεία αγκύρωσης
Odd-Numbered Anchors Μονά σημεία αγκύρωσης
Even-Numbered Anchors Ζυγά σημεία αγκύρωσης
Minimum angle Ελάχιστη γωνία
Maximum angle Μέγιστη γωνία
Can't render this file because it contains an unexpected character in line 163 and column 5.
+2
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2235 and column 5.
+2
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -6035,3 +6035,5 @@ breakText Glyphs 字形
All Anchors すべてのアンカー
Odd-Numbered Anchors 奇数番目のアンカー
Even-Numbered Anchors 偶数番目のアンカー
Minimum angle 最小角度
Maximum angle 最大角度
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -6361,3 +6361,5 @@ font-script Japanese იაპონური
All Anchors ყველა საყრდენი წერტილი
Odd-Numbered Anchors კენტი საყრდენი წერტილები
Even-Numbered Anchors ლუწი საყრდენი წერტილები
Minimum angle მინიმალური კუთხე
Maximum angle მაქსიმალური კუთხე
Can't render this file because it is too large.
+2
View File
@@ -5931,3 +5931,5 @@ breakText Glyphs 글리프
All Anchors 모든 앵커
Odd-Numbered Anchors 홀수 번째 앵커
Even-Numbered Anchors 짝수 번째 앵커
Minimum angle 최소 각도
Maximum angle 최대 각도
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -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
Can't render this file because it contains an unexpected character in line 6 and column 47.
+2
View File
@@ -6439,3 +6439,5 @@ shape Open Открытый
All Anchors Все опорные точки
Odd-Numbered Anchors Нечётные опорные точки
Even-Numbered Anchors Чётные опорные точки
Minimum angle Минимальный угол
Maximum angle Максимальный угол
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -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
Can't render this file because it contains an unexpected character in line 5 and column 25.
+2
View File
@@ -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çı
Can't render this file because it contains an unexpected character in line 5 and column 25.
+2
View File
@@ -5950,3 +5950,5 @@ breakText Glyphs Гліфи
All Anchors Усі опорні точки
Odd-Numbered Anchors Непарні опорні точки
Even-Numbered Anchors Парні опорні точки
Minimum angle Мінімальний кут
Maximum angle Максимальний кут
Can't render this file because it contains an unexpected character in line 177 and column 5.
+2
View File
@@ -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
Can't render this file because it contains an unexpected character in line 199 and column 5.
+2
View File
@@ -6191,3 +6191,5 @@ breakText Glyphs 字形
All Anchors 所有锚点
Odd-Numbered Anchors 奇数锚点
Even-Numbered Anchors 偶数锚点
Minimum angle 最小角度
Maximum angle 最大角度
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5988,3 +5988,5 @@ breakText Glyphs 字符
All Anchors 所有錨點
Odd-Numbered Anchors 奇數錨點
Even-Numbered Anchors 偶數錨點
Minimum angle 最小角度
Maximum angle 最大角度
Can't render this file because it contains an unexpected character in line 6 and column 47.
+4 -2
View File
@@ -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}`.
+3 -1
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` `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
+3 -2
View File
@@ -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 |