mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-07 13:23:22 +00:00
Set Corners: round only the corners in an angle range (#5545)
Squashed from #5737 onto main.
This commit is contained in:
@@ -20,7 +20,7 @@ pub(super) fn specs() -> Vec<CommandSpec> {
|
||||
"Set Corners",
|
||||
[],
|
||||
None,
|
||||
"{ids?: [id…] (the selection), anchors?: [index…] (of each path's outline with no corners cut; else its direct-selected anchors, or all of them), items?: [{id, anchors?}…] (instead of ids: each path with its own anchors), radius: pt 0..1000000 (drawn no larger than fits), kind?: \"round\"|\"invertedRound\"|\"chamfer\" (each corner's own), 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();
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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
@@ -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}`.
|
||||
|
||||
@@ -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
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user