Shapes: addon.shapes commands refuse values outside their ranges

addon.shapes.polygon and addon.shapes.star rounded a fractional side or
point count and clamped it to 3..1000, so 2.5, 2 or 5000 drew a shape
with a different count than asked for. addon.shapes.configure read
sides only as an unsigned integer, ignored a fraction and cut a large
value to 32 bits before clamping it, so 4294967299 gave a triangle.

The commands now refuse a supplied value outside its range with an
error that names the parameter and the range, and draw or change
nothing; an omitted or null value still takes its default:

- sides and points: a whole number from 3 to 1000
- rounded and randomized: -10 to 10; spokeRatio: 0.01 to 1
- turns: 0.01 to 1024; divergence: 0 to 1000; innerRadius: 0 to 0.999
- width, height, radius and radius1: 0 or more (a negative size was
  made positive before, and a negative corner radius became 0)
- radius2: 0.01 to 1 times radius1 (the ratio was clamped before)
- radii: four numbers, each 0 or more (a malformed array was ignored)
- arcMode: whole, pie (slice), chord or arc (open); any other value
  drew a whole shape before

addon.shapes.configure checks every value before it changes any of the
selected shapes, keeps fitting a corner radius to half each rectangle's
shorter side, and records no undo step when no shape changes.

Docs: docs/mcp.md (3D Box and shapes add-on), docs/target-app-parity.md.
This commit is contained in:
dotCooCoo
2026-11-13 14:29:23 -08:00
parent 4558196e16
commit 12bc3a653d
27 changed files with 403 additions and 79 deletions
+179 -79
View File
@@ -9,6 +9,8 @@
#[cfg(test)]
mod tests;
use std::ops::RangeInclusive;
use serde_json::{Value, json};
use vectorcraft_doc::{LiveShape, NodeId, NodeKind};
use vectorcraft_geom::{Affine, FillRule, Point, Rect, shapes};
@@ -16,9 +18,25 @@ use vectorcraft_tools::addons::box3d::CustomShape;
use super::super::create::insert_look;
use super::super::{
CommandSpec, Result, Session, bool_or, check_numbers, check_switches, cmd, f64_or, f64_req, has_doc, has_selection, str_param, targets,
CommandSpec, Result, Session, bad, bool_or, check_numbers, check_switches, cmd, f64_or, f64_req, has_doc, has_selection, number_or, str_param,
targets,
};
/// The sides of a polygon and the points of a star: whole numbers in this range.
const COUNTS: RangeInclusive<f64> = 3.0..=1000.0;
/// `rounded` and `randomized` of a polygon or star.
const ROUNDING: RangeInclusive<f64> = -10.0..=10.0;
/// A star's inner radius as a fraction of its outer radius.
const SPOKE_RATIO: RangeInclusive<f64> = 0.01..=1.0;
/// A spiral's `turns`.
const TURNS: RangeInclusive<f64> = 0.01..=1024.0;
/// A spiral's `divergence`.
const DIVERGENCE: RangeInclusive<f64> = 0.0..=1000.0;
/// A spiral's `innerRadius`, as a fraction of its radius.
const INNER_RADIUS: RangeInclusive<f64> = 0.0..=0.999;
/// The `arcMode` names; `slice` and `open` are also read as `pie` and `arc`.
const ARC_MODES: [&str; 4] = ["whole", "pie", "chord", "arc"];
pub fn specs() -> Vec<CommandSpec> {
vec![
cmd!(
@@ -26,7 +44,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Rectangle",
[],
None,
"{x, y, width, height, radius?: pt, radii?: [tl,tr,br,bl], startAngle?, endAngle?: degrees (the part of the outline kept, clockwise from its first anchor, the top-left corner; each side an equal share of 360; default 0 and 360), arcMode?: whole|pie|chord|arc, rotation?: deg clockwise about the centre (0: upright)} → {id}",
"{x, y, width, height: pt, 0 or more, radius?: pt, 0 or more, radii?: [tl,tr,br,bl], each 0 or more, startAngle?, endAngle?: degrees (the part of the outline kept, clockwise from its first anchor, the top-left corner; each side an equal share of 360; default 0 and 360), arcMode?: whole|pie|chord|arc, rotation?: deg clockwise about the centre (0: upright)} → {id}",
has_doc,
rectangle
),
@@ -35,7 +53,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Ellipse",
[],
None,
"{x, y, width, height, startAngle?, endAngle?: degrees (counter-clockwise from 3 o'clock, as object.setLiveShape's pieStart and pieEnd; default 0 and 360), arcMode?: whole|pie|chord|arc, rotation?: deg clockwise about the centre (0: upright)} → {id}",
"{x, y, width, height: pt, 0 or more, startAngle?, endAngle?: degrees (counter-clockwise from 3 o'clock, as object.setLiveShape's pieStart and pieEnd; default 0 and 360), arcMode?: whole|pie|chord|arc, rotation?: deg clockwise about the centre (0: upright)} → {id}",
has_doc,
ellipse
),
@@ -44,7 +62,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Polygon",
[],
None,
"{cx, cy, radius, sides=6, rounded=0, randomized=0, startAngle?, endAngle?: degrees (the part of the outline kept, clockwise from its first anchor, the top corner; each side an equal share of 360; default 0 and 360), arcMode?: whole|pie|chord|arc, rotation?: deg clockwise (0: a corner straight up)} → {id}",
"{cx, cy, radius: pt, 0 or more, sides=6: a whole number 3 to 1000, rounded=0, randomized=0: -10 to 10, startAngle?, endAngle?: degrees (the part of the outline kept, clockwise from its first anchor, the top corner; each side an equal share of 360; default 0 and 360), arcMode?: whole|pie|chord|arc, rotation?: deg clockwise (0: a corner straight up)} → {id}",
has_doc,
polygon
),
@@ -53,7 +71,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Star",
[],
None,
"{cx, cy, radius1, radius2?, points=5, rounded=0, randomized=0, startAngle?, endAngle?: degrees (the part of the outline kept, clockwise from its first anchor, the top point; each side an equal share of 360; default 0 and 360), arcMode?: whole|pie|chord|arc, rotation?: deg clockwise (0: a point straight up)} → {id}",
"{cx, cy, radius1: pt, 0 or more, radius2?: pt, 0.01 to 1 times radius1 (default half of it), points=5: a whole number 3 to 1000, rounded=0, randomized=0: -10 to 10, startAngle?, endAngle?: degrees (the part of the outline kept, clockwise from its first anchor, the top point; each side an equal share of 360; default 0 and 360), arcMode?: whole|pie|chord|arc, rotation?: deg clockwise (0: a point straight up)} → {id}",
has_doc,
star
),
@@ -62,7 +80,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Spiral",
[],
None,
"{cx, cy, radius, turns=3, divergence=1, innerRadius=0, clockwise=true} → {id}",
"{cx, cy, radius: pt, 0 or more, turns=3: 0.01 to 1024, divergence=1: 0 to 1000, innerRadius=0: 0 to 0.999 of radius, clockwise=true} → {id}",
has_doc,
spiral
),
@@ -71,7 +89,7 @@ pub fn specs() -> Vec<CommandSpec> {
"3D Box",
[],
None,
"{x, y, width, height, angleX=30 (degrees about the level axis: positive tips the top away from you), angleY=30 (degrees about the upright axis: positive turns the right side away from you), angleZ=0 (degrees clockwise in the page), parallelX=false, parallelY=true, parallelZ=false} → {id}",
"{x, y, width, height: pt, 0 or more, angleX=30 (degrees about the level axis: positive tips the top away from you), angleY=30 (degrees about the upright axis: positive turns the right side away from you), angleZ=0 (degrees clockwise in the page), parallelX=false, parallelY=true, parallelZ=false} → {id}",
has_doc,
box3d
),
@@ -80,33 +98,96 @@ pub fn specs() -> Vec<CommandSpec> {
"Configure Shape",
[],
None,
"{ids?, radius?, radii?: [tl,tr,br,bl], startAngle?, endAngle?: degrees (as addon.shapes.rectangle, ellipse, polygon and star take them: an ellipse's counter-clockwise from 3 o'clock, the others' clockwise along the outline), arcMode?: whole|pie|chord|arc, sides?, isStar?, spokeRatio?, rounded?, randomized?, turns?, divergence?, innerRadius?, angleX?, angleY?: degrees (as addon.shapes.box3d: positive tips the top, or turns the right side, away from you), angleZ?: degrees clockwise in the page, parallelX?, parallelY?, parallelZ?, reset?: bool} → {updated}",
"{ids?, radius?: pt, 0 or more (fitted to half each rectangle's shorter side), radii?: [tl,tr,br,bl], each 0 or more, startAngle?, endAngle?: degrees (as addon.shapes.rectangle, ellipse, polygon and star take them: an ellipse's counter-clockwise from 3 o'clock, the others' clockwise along the outline), arcMode?: whole|pie|chord|arc, sides?: a whole number 3 to 1000, isStar?, spokeRatio?: 0.01 to 1, rounded?, randomized?: -10 to 10, turns?: 0.01 to 1024, divergence?: 0 to 1000, innerRadius?: 0 to 0.999, angleX?, angleY?: degrees (as addon.shapes.box3d: positive tips the top, or turns the right side, away from you), angleZ?: degrees clockwise in the page, parallelX?, parallelY?, parallelZ?, reset?: bool} → {updated}",
has_selection,
configure
),
]
}
fn parse_arc_params(p: &Value) -> ((f64, f64), shapes::EllipseMode) {
/// The number `p` gives for `key`, or `default` when `p` leaves it out or gives null. A number
/// outside `range` is an error that names `key` and the range.
fn ranged(p: &Value, key: &str, default: f64, range: &RangeInclusive<f64>, cmd: &str) -> Result<f64> {
let x = number_or(p, key, default, cmd)?;
if !range.contains(&x) {
let (min, max) = (range.start(), range.end());
return Err(bad(cmd, format!("`{key}` must be between {min} and {max} (got {x})")));
}
Ok(x)
}
/// The number of sides or points `p` gives for `key`, or `default` when `p` leaves it out or gives
/// null. A fraction or a number outside [`COUNTS`] is an error.
fn count(p: &Value, key: &str, default: u32, cmd: &str) -> Result<u32> {
let x = number_or(p, key, f64::from(default), cmd)?;
if x.fract() != 0.0 || !COUNTS.contains(&x) {
let (min, max) = (COUNTS.start(), COUNTS.end());
return Err(bad(cmd, format!("`{key}` must be a whole number between {min} and {max} (got {x})")));
}
Ok(x as u32)
}
/// The size `p` gives for `key`: a number that is not negative.
fn size(p: &Value, key: &str, cmd: &str) -> Result<f64> {
not_negative(f64_req(p, key, cmd)?, key, cmd)
}
/// The size `p` gives for `key`, or `default` when `p` leaves it out or gives null.
fn size_or(p: &Value, key: &str, default: f64, cmd: &str) -> Result<f64> {
not_negative(number_or(p, key, default, cmd)?, key, cmd)
}
fn not_negative(x: f64, key: &str, cmd: &str) -> Result<f64> {
if x < 0.0 {
return Err(bad(cmd, format!("`{key}` must be a number not below 0")));
}
Ok(x)
}
/// The four corner radii `p` gives as `radii` ([tl, tr, br, bl]), or `None` when `p` leaves it out
/// or gives null. Anything but four numbers that are not negative is an error.
fn radii_param(p: &Value, cmd: &str) -> Result<Option<[f64; 4]>> {
let key = "radii";
match p.get(key) {
None | Some(Value::Null) => Ok(None),
Some(v) => {
let four = v.as_array().filter(|a| a.len() == 4).and_then(|a| {
let mut out = [0.0; 4];
for (slot, item) in out.iter_mut().zip(a) {
*slot = item.as_f64().filter(|r| *r >= 0.0)?;
}
Some(out)
});
four.map(Some).ok_or_else(|| bad(cmd, format!("`{key}` must be four numbers not below 0: [tl, tr, br, bl]")))
}
}
}
/// The mode `p` gives as `arcMode`, or `None` when `p` leaves it out or gives null. Any other
/// value than one of [`ARC_MODES`] (or `slice` or `open`) is an error.
fn arc_mode(p: &Value, cmd: &str) -> Result<Option<shapes::EllipseMode>> {
let key = "arcMode";
match p.get(key) {
None | Some(Value::Null) => Ok(None),
Some(v) => v.as_str().and_then(parse_mode).map(Some).ok_or_else(|| bad(cmd, format!("`{key}` must be one of {}", ARC_MODES.join(", ")))),
}
}
fn parse_arc_params(p: &Value, cmd: &str) -> Result<((f64, f64), shapes::EllipseMode)> {
let start = f64_or(p, "startAngle", 0.0);
let end = f64_or(p, "endAngle", 360.0);
let mode = match str_param(p, "arcMode") {
Some("pie" | "slice") => shapes::EllipseMode::Slice,
Some("chord") => shapes::EllipseMode::Chord,
Some("arc" | "open") => shapes::EllipseMode::Arc,
_ => shapes::EllipseMode::Whole,
};
let mode = arc_mode(p, cmd)?.unwrap_or(shapes::EllipseMode::Whole);
let span = (end - start).rem_euclid(360.0);
let pie = if mode == shapes::EllipseMode::Whole && (span < 1e-6 || (span - 360.0).abs() < 1e-6) { (0.0, 360.0) } else { (start, end) };
(pie, mode)
Ok((pie, mode))
}
fn rect_of(p: &Value, c: &str) -> Result<Rect> {
let x = f64_req(p, "x", c)?;
let y = f64_req(p, "y", c)?;
let w = f64_req(p, "width", c)?;
let h = f64_req(p, "height", c)?;
Ok(Rect::new(x, y, x + w.abs(), y + h.abs()).abs())
let w = size(p, "width", c)?;
let h = size(p, "height", c)?;
Ok(Rect::new(x, y, x + w, y + h))
}
fn rect_xf(r: Rect, rot: f64) -> Affine {
@@ -141,18 +222,9 @@ fn rectangle(s: &mut Session, p: &Value) -> Result<Value> {
let c = "addon.shapes.rectangle";
check_numbers(p, &["radius", "startAngle", "endAngle", "rotation"], c)?;
let r = rect_of(p, c)?;
let radius = f64_or(p, "radius", 0.0).max(0.0);
let mut radii = [radius; 4];
if let Some(arr) = p.get("radii").and_then(Value::as_array)
&& arr.len() == 4
{
for (i, item) in arr.iter().enumerate() {
if let Some(val) = item.as_f64().filter(|n| n.is_finite()) {
radii[i] = val.max(0.0);
}
}
}
let (pie, arc_mode) = parse_arc_params(p);
let radius = size_or(p, "radius", 0.0, c)?;
let radii = radii_param(p, c)?.unwrap_or([radius; 4]);
let (pie, arc_mode) = parse_arc_params(p, c)?;
let xf = rect_xf(r, f64_or(p, "rotation", 0.0));
let live = LiveShape::Rectangle { w: r.width(), h: r.height(), radii, kinds: Default::default(), radii_relative: false, xf };
let cs = CustomShape::Rectangle { w: r.width(), h: r.height(), radii, kinds: Default::default(), radii_relative: false, xf, pie, arc_mode };
@@ -164,7 +236,7 @@ fn ellipse(s: &mut Session, p: &Value) -> Result<Value> {
let c = "addon.shapes.ellipse";
check_numbers(p, &["startAngle", "endAngle", "rotation"], c)?;
let r = rect_of(p, c)?;
let (pie, arc_mode) = parse_arc_params(p);
let (pie, arc_mode) = parse_arc_params(p, c)?;
let xf = rect_xf(r, f64_or(p, "rotation", 0.0));
let live = LiveShape::Ellipse { w: r.width(), h: r.height(), pie, xf };
let cs = CustomShape::Ellipse { w: r.width(), h: r.height(), pie, arc_mode, xf };
@@ -174,13 +246,13 @@ fn ellipse(s: &mut Session, p: &Value) -> Result<Value> {
fn polygon(s: &mut Session, p: &Value) -> Result<Value> {
let c = "addon.shapes.polygon";
let center = Point::new(f64_req(p, "cx", c)?, f64_req(p, "cy", c)?);
let r = f64_req(p, "radius", c)?.abs();
let r = size(p, "radius", c)?;
check_numbers(p, &["sides", "rotation", "rounded", "randomized", "startAngle", "endAngle"], c)?;
let sides = (f64_or(p, "sides", 6.0).round() as i64).clamp(3, 1000) as u32;
let sides = count(p, "sides", 6, c)?;
let rot = f64_or(p, "rotation", 0.0);
let rounded = f64_or(p, "rounded", 0.0).clamp(-10.0, 10.0);
let randomized = f64_or(p, "randomized", 0.0).clamp(-10.0, 10.0);
let (pie, arc_mode) = parse_arc_params(p);
let rounded = ranged(p, "rounded", 0.0, &ROUNDING, c)?;
let randomized = ranged(p, "randomized", 0.0, &ROUNDING, c)?;
let (pie, arc_mode) = parse_arc_params(p, c)?;
let xf = Affine::translate(center.to_vec2()) * vectorcraft_geom::rotate_deg(rot);
let live = LiveShape::Polygon { radius: r, sides, xf, radii: vec![], kinds: vec![], radii_relative: false };
let cs = CustomShape::Polygon {
@@ -202,16 +274,17 @@ fn polygon(s: &mut Session, p: &Value) -> Result<Value> {
fn star(s: &mut Session, p: &Value) -> Result<Value> {
let c = "addon.shapes.star";
let center = Point::new(f64_req(p, "cx", c)?, f64_req(p, "cy", c)?);
let r1 = f64_req(p, "radius1", c)?.abs();
let r1 = size(p, "radius1", c)?;
check_numbers(p, &["radius2", "points", "rotation", "rounded", "randomized", "startAngle", "endAngle"], c)?;
let r2 = f64_or(p, "radius2", r1 / 2.0).abs();
let points = (f64_or(p, "points", 5.0).round() as i64).clamp(3, 1000) as u32;
// The inner radius is a fraction of the outer one, in `SPOKE_RATIO`.
let r2 = ranged(p, "radius2", r1 / 2.0, &(r1 * SPOKE_RATIO.start()..=r1 * SPOKE_RATIO.end()), c)?;
let points = count(p, "points", 5, c)?;
let rot = f64_or(p, "rotation", 0.0);
let rounded = f64_or(p, "rounded", 0.0).clamp(-10.0, 10.0);
let randomized = f64_or(p, "randomized", 0.0).clamp(-10.0, 10.0);
let (pie, arc_mode) = parse_arc_params(p);
let rounded = ranged(p, "rounded", 0.0, &ROUNDING, c)?;
let randomized = ranged(p, "randomized", 0.0, &ROUNDING, c)?;
let (pie, arc_mode) = parse_arc_params(p, c)?;
let xf = Affine::translate(center.to_vec2()) * vectorcraft_geom::rotate_deg(rot);
let ratio = if r1 > 1e-9 { (r2 / r1).clamp(0.01, 1.0) } else { 0.5 };
let ratio = if r1 > 1e-9 { (r2 / r1).clamp(*SPOKE_RATIO.start(), *SPOKE_RATIO.end()) } else { 0.5 };
let cs = CustomShape::Polygon {
radius: r1,
sides: points,
@@ -231,12 +304,12 @@ fn star(s: &mut Session, p: &Value) -> Result<Value> {
fn spiral(s: &mut Session, p: &Value) -> Result<Value> {
let c = "addon.shapes.spiral";
let center = Point::new(f64_req(p, "cx", c)?, f64_req(p, "cy", c)?);
let r = f64_req(p, "radius", c)?.abs();
let r = size(p, "radius", c)?;
check_numbers(p, &["turns", "divergence", "innerRadius"], c)?;
check_switches(p, &["clockwise"], c)?;
let turns = f64_or(p, "turns", 3.0).clamp(0.01, 1024.0);
let divergence = f64_or(p, "divergence", 1.0).clamp(0.0, 1000.0);
let inner_radius = f64_or(p, "innerRadius", 0.0).clamp(0.0, 0.999);
let turns = ranged(p, "turns", 3.0, &TURNS, c)?;
let divergence = ranged(p, "divergence", 1.0, &DIVERGENCE, c)?;
let inner_radius = ranged(p, "innerRadius", 0.0, &INNER_RADIUS, c)?;
let clockwise = bool_or(p, "clockwise", true);
let cs = CustomShape::Spiral { center, radius: r, turns, divergence, inner_radius, clockwise };
insert_custom(s, "Spiral", None, cs)
@@ -299,24 +372,54 @@ fn configure(s: &mut Session, p: &Value) -> Result<Value> {
c,
)?;
check_switches(p, &["isStar", "parallelX", "parallelY", "parallelZ", "reset"], c)?;
check_configure(p, c)?;
let ids = targets(s, p)?;
let reset = bool_or(p, "reset", false);
let spiral_hint = p.get("turns").is_some() || p.get("divergence").is_some() || p.get("innerRadius").is_some();
// The shapes the params change, worked out first so that a call that changes nothing records
// no undo step.
let changes: Vec<(NodeId, CustomShape)> = {
let doc = &s.doc()?.doc;
ids.into_iter()
.filter_map(|id| {
let n = doc.node(id)?;
let before = CustomShape::read_for_update(n, spiral_hint)?;
let mut cs = before.clone();
let stored = CustomShape::read(n).is_some();
(apply_shape_params(&mut cs, p, reset) && (cs != before || !stored)).then_some((id, cs))
})
.collect()
};
if changes.is_empty() {
return Ok(json!({ "updated": 0 }));
}
let updated = s.edit("Live Shape", |d, _| {
let mut count = 0usize;
for id in ids {
let Some(n) = d.node_mut(id) else { continue };
let Some(mut cs) = CustomShape::read_for_update(n, spiral_hint) else { continue };
if apply_shape_params(&mut cs, p, reset) {
cs.write_to_node(n);
count += 1;
}
for (id, cs) in &changes {
let Some(n) = d.node_mut(*id) else { continue };
cs.write_to_node(n);
count += 1;
}
Ok(count)
})?;
Ok(json!({ "updated": updated }))
}
/// Error unless every bounded value `p` gives is in its range, before any shape changes.
fn check_configure(p: &Value, c: &str) -> Result<()> {
size_or(p, "radius", 0.0, c)?;
radii_param(p, c)?;
arc_mode(p, c)?;
count(p, "sides", 6, c)?;
ranged(p, "spokeRatio", 0.5, &SPOKE_RATIO, c)?;
ranged(p, "rounded", 0.0, &ROUNDING, c)?;
ranged(p, "randomized", 0.0, &ROUNDING, c)?;
ranged(p, "turns", 3.0, &TURNS, c)?;
ranged(p, "divergence", 1.0, &DIVERGENCE, c)?;
ranged(p, "innerRadius", 0.0, &INNER_RADIUS, c)?;
Ok(())
}
fn num_param(p: &Value, k: &str) -> Option<f64> {
p.get(k).and_then(Value::as_f64).filter(|v| v.is_finite())
}
@@ -331,8 +434,8 @@ fn apply_arc_params(p: &Value, pie: &mut (f64, f64), arc_mode: &mut shapes::Elli
pie.1 = a1.rem_euclid(360.0);
changed = true;
}
if let Some(m) = str_param(p, "arcMode") {
*arc_mode = parse_mode(m);
if let Some(m) = str_param(p, "arcMode").and_then(parse_mode) {
*arc_mode = m;
if *arc_mode == shapes::EllipseMode::Whole {
*pie = (0.0, 360.0);
} else if (pie.1 - pie.0).abs() < 1e-6 || ((pie.1 - pie.0).abs() - 360.0).abs() < 1e-6 {
@@ -351,19 +454,14 @@ fn apply_rect_params(p: &Value, reset: bool, max_r: f64, radii: &mut [f64; 4], p
*arc_mode = shapes::EllipseMode::Whole;
changed = true;
}
// A radius larger than half the rectangle's shorter side is fitted to that size.
if let Some(r) = num_param(p, "radius") {
*radii = [r.clamp(0.0, max_r); 4];
changed = true;
}
if let Some(arr) = p.get("radii").and_then(Value::as_array)
&& arr.len() == 4
{
for (i, v) in arr.iter().enumerate() {
if let Some(r) = v.as_f64().filter(|x| x.is_finite()) {
radii[i] = r.clamp(0.0, max_r);
changed = true;
}
}
if let Ok(Some(four)) = radii_param(p, "addon.shapes.configure") {
*radii = four.map(|r| r.clamp(0.0, max_r));
changed = true;
}
changed | apply_arc_params(p, pie, arc_mode)
}
@@ -378,8 +476,9 @@ fn apply_poly_params(p: &Value, reset: bool, sides: &mut u32, spoke_ratio: &mut
}
changed = true;
}
if let Some(n_sides) = p.get("sides").and_then(Value::as_u64) {
*sides = (n_sides as u32).clamp(3, 1000);
// `check_configure` has refused values outside their ranges.
if let Some(n_sides) = num_param(p, "sides") {
*sides = n_sides as u32;
changed = true;
}
if let Some(star) = p.get("isStar").and_then(Value::as_bool) {
@@ -387,15 +486,15 @@ fn apply_poly_params(p: &Value, reset: bool, sides: &mut u32, spoke_ratio: &mut
changed = true;
}
if let Some(sr) = num_param(p, "spokeRatio") {
*spoke_ratio = Some(sr.clamp(0.01, 1.0));
*spoke_ratio = Some(sr);
changed = true;
}
if let Some(rnd) = num_param(p, "rounded") {
*rounded = rnd.clamp(-10.0, 10.0);
*rounded = rnd;
changed = true;
}
if let Some(rand) = num_param(p, "randomized") {
*randomized = rand.clamp(-10.0, 10.0);
*randomized = rand;
changed = true;
}
changed
@@ -429,15 +528,15 @@ fn apply_shape_params(cs: &mut CustomShape, p: &Value, reset: bool) -> bool {
changed = true;
}
if let Some(t) = num_param(p, "turns") {
*turns = t.clamp(0.01, 1024.0);
*turns = t;
changed = true;
}
if let Some(div) = num_param(p, "divergence") {
*divergence = div.clamp(0.0, 1000.0);
*divergence = div;
changed = true;
}
if let Some(ir) = num_param(p, "innerRadius") {
*inner_radius = ir.clamp(0.0, 0.999);
*inner_radius = ir;
changed = true;
}
changed
@@ -470,11 +569,12 @@ fn apply_shape_params(cs: &mut CustomShape, p: &Value, reset: bool) -> bool {
}
}
fn parse_mode(m: &str) -> shapes::EllipseMode {
fn parse_mode(m: &str) -> Option<shapes::EllipseMode> {
match m {
"pie" | "slice" => shapes::EllipseMode::Slice,
"chord" => shapes::EllipseMode::Chord,
"arc" | "open" => shapes::EllipseMode::Arc,
_ => shapes::EllipseMode::Whole,
"whole" => Some(shapes::EllipseMode::Whole),
"pie" | "slice" => Some(shapes::EllipseMode::Slice),
"chord" => Some(shapes::EllipseMode::Chord),
"arc" | "open" => Some(shapes::EllipseMode::Arc),
_ => None,
}
}
@@ -135,3 +135,169 @@ fn addon_shapes_commands_and_format_roundtrip_preserve_paths() {
assert_eq!(CustomShape::read(a), CustomShape::read(b));
}
}
fn id_of(s: &mut Session, cmd: &str, p: serde_json::Value) -> NodeId {
NodeId(s.execute(cmd, &p).unwrap_or_else(|e| panic!("{cmd} {p}: {e}"))["id"].as_u64().unwrap())
}
fn shape_of(s: &Session, id: NodeId) -> CustomShape {
CustomShape::read(s.doc().unwrap().doc.node(id).unwrap()).unwrap()
}
/// Runs `cmd` with `p`, which must fail with `msg` and leave the document and its history as they
/// were.
fn refused(s: &mut Session, cmd: &str, p: serde_json::Value, msg: &str) {
let saved = vectorcraft_format::save(&s.doc().unwrap().doc, false);
let undo = s.doc().unwrap().history.undo.len();
let e = s.execute(cmd, &p).map(|v| v.to_string()).unwrap_err().to_string();
assert!(e.contains(msg), "{cmd} {p}: {e}");
assert_eq!(s.doc().unwrap().history.undo.len(), undo, "{cmd} {p}");
assert_eq!(vectorcraft_format::save(&s.doc().unwrap().doc, false), saved, "{cmd} {p}");
}
#[test]
fn polygon_and_star_refuse_counts_that_are_not_whole_numbers_from_3_to_1000() {
let mut s = new_session();
for n in [json!(2.5), json!(2), json!(0), json!(-6), json!(1000.5), json!(5000), json!(4_294_967_299u64)] {
let x = n.as_f64().unwrap();
let p = json!({"cx": 0.0, "cy": 0.0, "radius": 10.0, "sides": n});
refused(&mut s, "addon.shapes.polygon", p, &format!("`sides` must be a whole number between 3 and 1000 (got {x})"));
let p = json!({"cx": 0.0, "cy": 0.0, "radius1": 10.0, "points": n});
refused(&mut s, "addon.shapes.star", p, &format!("`points` must be a whole number between 3 and 1000 (got {x})"));
}
refused(&mut s, "addon.shapes.polygon", json!({"cx": 0.0, "cy": 0.0, "radius": 10.0, "sides": "6"}), "`sides` must be a number");
refused(&mut s, "addon.shapes.star", json!({"cx": 0.0, "cy": 0.0, "radius1": 10.0, "points": [5]}), "`points` must be a number");
// The ends of the range are taken as given, and an omitted or null count is the default.
for (n, want) in [(json!(3), 3), (json!(1000), 1000), (json!(7.0), 7), (json!(null), 6)] {
let id = id_of(&mut s, "addon.shapes.polygon", json!({"cx": 0.0, "cy": 0.0, "radius": 10.0, "sides": n}));
assert!(matches!(shape_of(&s, id), CustomShape::Polygon { sides, spoke_ratio: None, .. } if sides == want), "{n}");
}
let id = id_of(&mut s, "addon.shapes.polygon", json!({"cx": 0.0, "cy": 0.0, "radius": 10.0}));
assert!(matches!(shape_of(&s, id), CustomShape::Polygon { sides: 6, .. }));
for (n, want) in [(json!(3), 3), (json!(1000), 1000), (json!(null), 5)] {
let id = id_of(&mut s, "addon.shapes.star", json!({"cx": 0.0, "cy": 0.0, "radius1": 10.0, "points": n}));
assert!(matches!(shape_of(&s, id), CustomShape::Polygon { sides, spoke_ratio: Some(_), .. } if sides == want), "{n}");
}
}
#[test]
fn shapes_refuse_numbers_outside_their_ranges() {
let mut s = new_session();
let poly = json!({"cx": 0.0, "cy": 0.0, "radius": 10.0});
let star = json!({"cx": 0.0, "cy": 0.0, "radius1": 10.0});
let spiral = json!({"cx": 0.0, "cy": 0.0, "radius": 10.0});
let with = |base: &serde_json::Value, key: &str, v: f64| {
let mut p = base.clone();
p[key] = json!(v);
p
};
for (cmd, base, key, bad, good) in [
("addon.shapes.polygon", &poly, "rounded", [-10.5, 10.5], [-10.0, 10.0]),
("addon.shapes.polygon", &poly, "randomized", [-11.0, 10.01], [-10.0, 10.0]),
("addon.shapes.star", &star, "rounded", [-10.5, 10.5], [-10.0, 10.0]),
("addon.shapes.star", &star, "randomized", [-11.0, 10.01], [-10.0, 10.0]),
("addon.shapes.star", &star, "radius2", [0.05, 11.0], [0.1, 10.0]),
("addon.shapes.spiral", &spiral, "turns", [0.0, 1024.5], [0.01, 1024.0]),
("addon.shapes.spiral", &spiral, "divergence", [-0.1, 1000.5], [0.0, 1000.0]),
("addon.shapes.spiral", &spiral, "innerRadius", [-0.1, 1.0], [0.0, 0.999]),
] {
let (min, max) = (good[0], good[1]);
for x in bad {
refused(&mut s, cmd, with(base, key, x), &format!("`{key}` must be between {min} and {max} (got {x})"));
}
for x in good {
s.execute(cmd, &with(base, key, x)).unwrap_or_else(|e| panic!("{cmd} {key} {x}: {e}"));
}
}
// Sizes are not negative.
for (cmd, p, key) in [
("addon.shapes.polygon", json!({"cx": 0.0, "cy": 0.0, "radius": -10.0}), "radius"),
("addon.shapes.star", json!({"cx": 0.0, "cy": 0.0, "radius1": -10.0}), "radius1"),
("addon.shapes.spiral", json!({"cx": 0.0, "cy": 0.0, "radius": -10.0}), "radius"),
("addon.shapes.rectangle", json!({"x": 0.0, "y": 0.0, "width": -10.0, "height": 10.0}), "width"),
("addon.shapes.rectangle", json!({"x": 0.0, "y": 0.0, "width": 10.0, "height": 10.0, "radius": -1.0}), "radius"),
("addon.shapes.ellipse", json!({"x": 0.0, "y": 0.0, "width": 10.0, "height": -10.0}), "height"),
("addon.shapes.box3d", json!({"x": 0.0, "y": 0.0, "width": -10.0, "height": 10.0}), "width"),
] {
refused(&mut s, cmd, p, &format!("`{key}` must be a number not below 0"));
}
// Corner radii are four numbers that are not negative.
for radii in [json!([1.0, 2.0, 3.0]), json!([1.0, 2.0, 3.0, -4.0]), json!([1.0, 2.0, 3.0, "4"]), json!(4.0)] {
let p = json!({"x": 0.0, "y": 0.0, "width": 100.0, "height": 80.0, "radii": radii});
refused(&mut s, "addon.shapes.rectangle", p, "`radii` must be four numbers not below 0: [tl, tr, br, bl]");
}
// An arc mode is one of the names the commands take.
for mode in [json!("slices"), json!(""), json!(3)] {
let p = json!({"x": 0.0, "y": 0.0, "width": 100.0, "height": 80.0, "arcMode": mode});
refused(&mut s, "addon.shapes.rectangle", p.clone(), "`arcMode` must be one of whole, pie, chord, arc");
refused(&mut s, "addon.shapes.ellipse", p, "`arcMode` must be one of whole, pie, chord, arc");
}
for mode in ["whole", "pie", "slice", "chord", "arc", "open"] {
s.execute("addon.shapes.ellipse", &json!({"x": 0.0, "y": 0.0, "width": 100.0, "height": 80.0, "arcMode": mode})).unwrap();
}
}
#[test]
fn configure_refuses_values_outside_their_ranges_before_changing_any_shape() {
let mut s = new_session();
let rect = id_of(&mut s, "addon.shapes.rectangle", json!({"x": 0.0, "y": 0.0, "width": 100.0, "height": 80.0, "radius": 4.0}));
let poly = id_of(&mut s, "addon.shapes.polygon", json!({"cx": 50.0, "cy": 50.0, "radius": 40.0, "sides": 5}));
let spiral = id_of(&mut s, "addon.shapes.spiral", json!({"cx": 60.0, "cy": 60.0, "radius": 45.0}));
let ids = json!([rect.0, poly.0, spiral.0]);
let cfg = |key: &str, v: serde_json::Value| json!({"ids": ids.clone(), key: v});
let c = "addon.shapes.configure";
for n in [json!(2.5), json!(2), json!(-1), json!(5000), json!(4_294_967_299u64)] {
let x = n.as_f64().unwrap();
refused(&mut s, c, cfg("sides", n), &format!("`sides` must be a whole number between 3 and 1000 (got {x})"));
}
for (key, bad, min, max) in [
("spokeRatio", [0.0, 1.5], 0.01, 1.0),
("rounded", [-10.5, 11.0], -10.0, 10.0),
("randomized", [-11.0, 10.5], -10.0, 10.0),
("turns", [0.0, 2000.0], 0.01, 1024.0),
("divergence", [-1.0, 1000.5], 0.0, 1000.0),
("innerRadius", [-0.5, 1.0], 0.0, 0.999),
] {
for x in bad {
refused(&mut s, c, cfg(key, json!(x)), &format!("`{key}` must be between {min} and {max} (got {x})"));
}
}
refused(&mut s, c, cfg("radius", json!(-1.0)), "`radius` must be a number not below 0");
for radii in [json!([1.0, 2.0]), json!([1.0, 2.0, 3.0, -1.0]), json!("4")] {
refused(&mut s, c, cfg("radii", radii), "`radii` must be four numbers not below 0: [tl, tr, br, bl]");
}
refused(&mut s, c, cfg("arcMode", json!("bogus")), "`arcMode` must be one of whole, pie, chord, arc");
// A valid value next to a refused one changes no shape either.
refused(&mut s, c, json!({"ids": ids.clone(), "sides": 8, "turns": 0.0}), "`turns` must be between 0.01 and 1024 (got 0)");
// In range, the values are taken as given; a radius larger than the rectangle takes is fitted to it.
s.execute(c, &json!({"ids": [poly.0], "sides": 1000, "rounded": -10.0, "randomized": 10.0})).unwrap();
assert!(matches!(shape_of(&s, poly), CustomShape::Polygon { sides: 1000, rounded, randomized, .. } if rounded == -10.0 && randomized == 10.0));
s.execute(c, &json!({"ids": [poly.0], "sides": 3})).unwrap();
assert!(matches!(shape_of(&s, poly), CustomShape::Polygon { sides: 3, .. }));
s.execute(c, &json!({"ids": [spiral.0], "turns": 1024.0, "divergence": 0.0, "innerRadius": 0.999})).unwrap();
assert!(
matches!(shape_of(&s, spiral), CustomShape::Spiral { turns, divergence, inner_radius, .. } if turns == 1024.0 && divergence == 0.0 && inner_radius == 0.999)
);
s.execute(c, &json!({"ids": [rect.0], "radius": 1000.0})).unwrap();
assert!(matches!(shape_of(&s, rect), CustomShape::Rectangle { radii, .. } if radii == [40.0; 4]));
}
#[test]
fn configure_records_no_undo_step_when_nothing_changes() {
let mut s = new_session();
let poly = id_of(&mut s, "addon.shapes.polygon", json!({"cx": 50.0, "cy": 50.0, "radius": 40.0, "sides": 5, "rounded": 0.5}));
let undo = s.doc().unwrap().history.undo.len();
let r = s.execute("addon.shapes.configure", &json!({"ids": [poly.0], "sides": 5, "rounded": 0.5})).unwrap();
assert_eq!(r["updated"], 0);
assert_eq!(s.doc().unwrap().history.undo.len(), undo);
let r = s.execute("addon.shapes.configure", &json!({"ids": [poly.0], "sides": 6})).unwrap();
assert_eq!(r["updated"], 1);
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1);
assert!(matches!(shape_of(&s, poly), CustomShape::Polygon { sides: 6, .. }));
}
+1
View File
@@ -5263,6 +5263,7 @@ digits Malayalam Malaiàlam
@msg `{key}` must be a #rrggbb colour `{key}` ha de ser un color #rrggbb
@msg `{key}` must be a number `{key}` ha de ser un nombre
@msg `{key}` must be a number not below 0 `{key}` ha de ser un nombre no inferior a 0
@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` han de ser quatre nombres no inferiors a 0: [tl, tr, br, bl]
@msg `{key}` must be a number, not {v} `{key}` ha de ser un nombre, no {v}
@msg `{key}` must be a string `{key}` ha de ser una cadena
@msg `{key}` must be a profile name or null `{key}` ha de ser un nom de perfil o null
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4767,6 +4767,7 @@ imageTrace Outline Obrys
@msg `select` must be true or false `select` musí být true nebo false
@msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` a `maxAnchors` musí být celá čísla
@msg `{key}` must be a number not below 0 `{key}` musí být číslo nejméně 0
@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` musí být čtyři čísla nejméně 0: [tl, tr, br, bl]
@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` musí být [x, y, width, height]: čtyři konečná čísla, šířka a výška nesmí být záporné
@msg `{min_key}` is greater than `{max_key}` `{min_key}` je větší než `{max_key}`
@msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` zadejte alespoň jeden z parametrů `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors`
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4381,6 +4381,7 @@ theme Appearance Erscheinungsbild
@msg `select` must be true or false `select` muss true oder false sein
@msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` und `maxAnchors` müssen ganze Zahlen sein
@msg `{key}` must be a number not below 0 `{key}` muss eine Zahl nicht kleiner als 0 sein
@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` muss aus vier Zahlen nicht kleiner als 0 bestehen: [tl, tr, br, bl]
@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` muss [x, y, width, height] sein: vier endliche Zahlen, Breite und Höhe nicht negativ
@msg `{min_key}` is greater than `{max_key}` `{min_key}` ist größer als `{max_key}`
@msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` geben Sie mindestens einen der Parameter `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` an
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2528,6 +2528,7 @@ axis Both Και τα δύο
@msg `select` must be true or false Το `select` πρέπει να είναι true ή false
@msg `minAnchors` and `maxAnchors` must be whole numbers Τα `minAnchors` και `maxAnchors` πρέπει να είναι ακέραιοι αριθμοί
@msg `{key}` must be a number not below 0 Το `{key}` πρέπει να είναι αριθμός όχι μικρότερος από 0
@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] Το `{key}` πρέπει να είναι τέσσερις αριθμοί όχι μικρότεροι από 0: [tl, tr, br, bl]
@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative Το `{key}` πρέπει να είναι [x, y, width, height]: τέσσερις πεπερασμένοι αριθμοί, με πλάτος και ύψος όχι αρνητικά
@msg `{min_key}` is greater than `{max_key}` Το `{min_key}` είναι μεγαλύτερο από το `{max_key}`
@msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` δώστε τουλάχιστον ένα από τα `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors`
Can't render this file because it is too large.
+1
View File
@@ -4458,6 +4458,7 @@ axis Both Ambos
@msg `select` must be true or false `select` debe ser true o false
@msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` y `maxAnchors` deben ser números enteros
@msg `{key}` must be a number not below 0 `{key}` debe ser un número no inferior a 0
@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` debe ser cuatro números no inferiores a 0: [tl, tr, br, bl]
@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` debe ser [x, y, width, height]: cuatro números finitos, con anchura y altura no negativas
@msg `{min_key}` is greater than `{max_key}` `{min_key}` es mayor que `{max_key}`
@msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` indica al menos uno de `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors`
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4681,6 +4681,7 @@ imageTrace Outline Ääriviiva
@msg `select` must be true or false `select` on oltava true tai false
@msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` ja `maxAnchors` on oltava kokonaislukuja
@msg `{key}` must be a number not below 0 `{key}` on oltava luku, joka on vähintään 0
@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` on oltava neljä lukua, jotka ovat vähintään 0: [tl, tr, br, bl]
@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` on oltava [x, y, width, height]: neljä äärellistä lukua, joista leveys ja korkeus eivät ole negatiivisia
@msg `{min_key}` is greater than `{max_key}` `{min_key}` on suurempi kuin `{max_key}`
@msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` anna vähintään yksi parametreista `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors`
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4447,6 +4447,7 @@ axis Both Les deux
@msg `select` must be true or false `select` doit valoir true ou false
@msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` et `maxAnchors` doivent être des nombres entiers
@msg `{key}` must be a number not below 0 `{key}` doit être un nombre supérieur ou égal à 0
@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` doit être quatre nombres supérieurs ou égaux à 0 : [tl, tr, br, bl]
@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` doit être [x, y, width, height] : quatre nombres finis, largeur et hauteur non négatives
@msg `{min_key}` is greater than `{max_key}` `{min_key}` est supérieur à `{max_key}`
@msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` indiquez au moins l’un des paramètres `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors`
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5913,6 +5913,7 @@ antiAlias Sharp शार्प
@msg `{key}` must be a number `{key}` एक संख्या होना चाहिए
@msg `{key}` must be a number {_1}..{_2} `{key}` {_1}..{_2} की एक संख्या होना चाहिए
@msg `{key}` must be a number not below 0 `{key}` 0 से कम नहीं होने वाली संख्या होना चाहिए
@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` 0 से कम नहीं होने वाली चार संख्याएँ होनी चाहिए: [tl, tr, br, bl]
@msg `{key}` must be a number, not {v} `{key}` एक संख्या होना चाहिए, {v} नहीं
@msg `{key}` must be a profile name or null `{key}` को प्रोफ़ाइल नाम या null होना चाहिए
@msg `{key}` must be a string `{key}` एक स्ट्रिंग होना चाहिए
Can't render this file because it is too large.
+1
View File
@@ -4887,6 +4887,7 @@ imageTrace Outline Körvonal
@msg `select` must be true or false `select`: true vagy false kell
@msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` és `maxAnchors`: egész szám kell
@msg `{key}` must be a number not below 0 `{key}`: legalább 0 értékű szám kell
@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}`: négy legalább 0 értékű szám kell: [tl, tr, br, bl]
@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}`: [x, y, width, height] kell, négy véges szám, a szélesség és a magasság nem negatív
@msg `{min_key}` is greater than `{max_key}` `{min_key}` nagyobb, mint `{max_key}`
@msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` adj meg legalább egyet ezek közül: `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors`
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4510,6 +4510,7 @@ axis Both Entrambi
@msg `select` must be true or false `select` deve essere true o false
@msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` e `maxAnchors` devono essere numeri interi
@msg `{key}` must be a number not below 0 `{key}` deve essere un numero non inferiore a 0
@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` deve essere quattro numeri non inferiori a 0: [tl, tr, br, bl]
@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` deve essere [x, y, width, height]: quattro numeri finiti, con larghezza e altezza non negative
@msg `{min_key}` is greater than `{max_key}` `{min_key}` è maggiore di `{max_key}`
@msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` indica almeno uno tra `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors`
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4981,6 +4981,7 @@ swatch Bright Violet ビビッドバイオレット
@msg `select` must be true or false `select` には true または false を指定してください
@msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` と `maxAnchors` は整数にしてください
@msg `{key}` must be a number not below 0 `{key}` には 0 以上の数値を指定してください
@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` には 0 以上の数値を 4 つ指定してください: [tl, tr, br, bl]
@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` は [x, y, width, height] の形式で指定してください: 有限の数値 4 つで、幅と高さは負の値にできません
@msg `{min_key}` is greater than `{max_key}` `{min_key}` が `{max_key}` より大きくなっています
@msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` 次のうち少なくとも 1 つを指定してください: `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors`
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4157,6 +4157,7 @@ font-technology Variable ცვლადი
@msg `select` must be true or false `select` უნდა იყოს true ან false
@msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` და `maxAnchors` უნდა იყოს მთელი რიცხვები
@msg `{key}` must be a number not below 0 `{key}` უნდა იყოს რიცხვი, არანაკლებ 0
@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` უნდა იყოს ოთხი რიცხვი, თითოეული არანაკლებ 0: [tl, tr, br, bl]
@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` უნდა იყოს [x, y, width, height]: ოთხი სასრული რიცხვი, სიგანე და სიმაღლე არ უნდა იყოს უარყოფითი
@msg `{min_key}` is greater than `{max_key}` `{min_key}` მეტია, ვიდრე `{max_key}`
@msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` მიუთითეთ ერთი მაინც: `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors`
Can't render this file because it is too large.
+1
View File
@@ -4908,6 +4908,7 @@ theme Appearance 외관
@msg `select` must be true or false `select`은(는) true 또는 false여야 합니다
@msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors`와 `maxAnchors`는 정수여야 합니다
@msg `{key}` must be a number not below 0 `{key}`은(는) 0 이상의 숫자여야 합니다
@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}`은(는) 0 이상의 숫자 네 개여야 합니다: [tl, tr, br, bl]
@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}`은(는) [x, y, width, height]여야 합니다: 유한한 숫자 네 개이며 너비와 높이는 음수가 아니어야 합니다
@msg `{min_key}` is greater than `{max_key}` `{min_key}`이(가) `{max_key}`보다 큽니다
@msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` 다음 중 하나 이상을 지정하세요: `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors`
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -157,6 +157,7 @@
@msg `select` must be true or false `select` moet true of false zijn
@msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` en `maxAnchors` moeten gehele getallen zijn
@msg `{key}` must be a number not below 0 `{key}` moet een getal van minstens 0 zijn
@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` moet uit vier getallen van minstens 0 bestaan: [tl, tr, br, bl]
@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` moet [x, y, width, height] zijn: vier eindige getallen, width en height niet negatief
@msg `{min_key}` is greater than `{max_key}` `{min_key}` is groter dan `{max_key}`
@msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` geef minstens een van `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors`
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4591,6 +4591,7 @@ imageTrace Outline Kontur
@msg `select` must be true or false `select` musi mieć wartość true lub false
@msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` i `maxAnchors` muszą być liczbami całkowitymi
@msg `{key}` must be a number not below 0 `{key}` musi być liczbą nie mniejszą niż 0
@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` musi być czterema liczbami nie mniejszymi niż 0: [tl, tr, br, bl]
@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` musi mieć postać [x, y, width, height]: cztery skończone liczby, szerokość i wysokość nieujemne
@msg `{min_key}` is greater than `{max_key}` `{min_key}` jest większe niż `{max_key}`
@msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` podaj co najmniej jeden z parametrów `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors`
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3921,6 +3921,7 @@ axis Both Ambos
@msg `select` must be true or false `select` deve ser true ou false
@msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` e `maxAnchors` devem ser números inteiros
@msg `{key}` must be a number not below 0 `{key}` deve ser um número não inferior a 0
@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` deve ser quatro números não inferiores a 0: [tl, tr, br, bl]
@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` deve ser [x, y, width, height]: quatro números finitos, com largura e altura não negativas
@msg `{min_key}` is greater than `{max_key}` `{min_key}` é maior que `{max_key}`
@msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` informe pelo menos um de `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors`
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -4495,6 +4495,7 @@ axis Both Оба
@msg `select` must be true or false `select` должно быть true или false
@msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` и `maxAnchors` должны быть целыми числами
@msg `{key}` must be a number not below 0 `{key}` должно быть числом не меньше 0
@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` должно быть четырьмя числами не меньше 0: [tl, tr, br, bl]
@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` должно быть [x, y, width, height]: четыре конечных числа, ширина и высота не отрицательные
@msg `{min_key}` is greater than `{max_key}` `{min_key}` больше, чем `{max_key}`
@msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` укажите хотя бы один из параметров `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors`
Can't render this file because it is too large.
+1
View File
@@ -4367,6 +4367,7 @@ axis Both Båda
@msg `select` must be true or false `select` måste vara true eller false
@msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` och `maxAnchors` måste vara heltal
@msg `{key}` must be a number not below 0 `{key}` måste vara ett tal som inte är under 0
@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` måste vara fyra tal som inte är under 0: [tl, tr, br, bl]
@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` måste vara [x, y, width, height]: fyra ändliga tal, där bredd och höjd inte är negativa
@msg `{min_key}` is greater than `{max_key}` `{min_key}` är större än `{max_key}`
@msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` ange minst en av `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors`
Can't render this file because it is too large.
+1
View File
@@ -4241,6 +4241,7 @@ axis Both Her İkisi
@msg `select` must be true or false `select` true veya false olmalıdır
@msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` ve `maxAnchors` tam sayı olmalıdır
@msg `{key}` must be a number not below 0 `{key}` 0'dan küçük olmayan bir sayı olmalıdır
@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` 0'dan küçük olmayan dört sayı olmalıdır: [tl, tr, br, bl]
@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` [x, y, width, height] olmalıdır: dört sonlu sayı, genişlik ve yükseklik negatif olmamalı
@msg `{min_key}` is greater than `{max_key}` `{min_key}`, `{max_key}` değerinden büyük
@msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` şunlardan en az birini verin: `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors`
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1
View File
@@ -4494,6 +4494,7 @@ axis Both Обидві
@msg `select` must be true or false `select` має бути true або false
@msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` і `maxAnchors` мають бути цілими числами
@msg `{key}` must be a number not below 0 `{key}` має бути числом, не меншим за 0
@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` має бути чотирма числами, не меншими за 0: [tl, tr, br, bl]
@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` має бути [x, y, width, height]: чотири скінченні числа, ширина й висота невід’ємні
@msg `{min_key}` is greater than `{max_key}` `{min_key}` більше за `{max_key}`
@msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` укажіть принаймні один із параметрів `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors`
Can't render this file because it is too large.
+1
View File
@@ -4547,6 +4547,7 @@ theme Appearance Giao diện
@msg `select` must be true or false `select` phải là true hoặc false
@msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` và `maxAnchors` phải là số nguyên
@msg `{key}` must be a number not below 0 `{key}` phải là một số không nhỏ hơn 0
@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` phải là bốn số không nhỏ hơn 0: [tl, tr, br, bl]
@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` phải là [x, y, width, height]: bốn số hữu hạn, chiều rộng và chiều cao không âm
@msg `{min_key}` is greater than `{max_key}` `{min_key}` lớn hơn `{max_key}`
@msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` hãy cung cấp ít nhất một trong `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors`
Can't render this file because it contains an unexpected character in line 204 and column 5.
+1
View File
@@ -4667,6 +4667,7 @@ imageTrace Outline 轮廓
@msg `select` must be true or false `select` 必须是 true 或 false
@msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` 和 `maxAnchors` 必须是整数
@msg `{key}` must be a number not below 0 `{key}` 必须是不小于 0 的数字
@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` 必须是四个不小于 0 的数字:[tl, tr, br, bl]
@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` 必须是 [x, y, width, height]:四个有限数字,宽度和高度不能为负
@msg `{min_key}` is greater than `{max_key}` `{min_key}` 大于 `{max_key}`
@msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` 请至少提供以下其中之一:`name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors`
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2406,6 +2406,7 @@ file-filter Templates 範本
@msg `select` must be true or false `select` 必須是 true 或 false
@msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` 和 `maxAnchors` 必須是整數
@msg `{key}` must be a number not below 0 `{key}` 必須是不小於 0 的數字
@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` 必須是四個不小於 0 的數字:[tl, tr, br, bl]
@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` 必須是 [x, y, width, height]:四個有限數字,寬度和高度不能為負
@msg `{min_key}` is greater than `{max_key}` `{min_key}` 大於 `{max_key}`
@msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` 請至少提供下列其中之一:`name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors`
Can't render this file because it contains an unexpected character in line 6 and column 47.
+26
View File
@@ -10872,6 +10872,32 @@ whole points and Escape cancels. A drag keeps a Contour's own corners and miter
options `corners` and `miterLimit` (`tool.setOption`). A border with a gap is two Contours of one shape (the gap first, so
it ends up in front), baked with `effect.expandAppearance`, then `object.pathfinder.minusFront`.
## 3D Box and shapes add-on
The `addon.shapes.*` commands draw rotatable 3D boxes (`addon.shapes.box3d`) and rectangles, ellipses, polygons, stars and
spirals that keep their settings (`addon.shapes.rectangle`, `ellipse`, `polygon`, `star`, `spiral`), and
`addon.shapes.configure` changes those settings on the selected shapes (or `ids`). An omitted or null value takes its
default. A value outside its range is an error that names the parameter and the range, and the command then draws or
changes nothing. The ranges are VectorCraft's own:
| Parameter | Commands | Range (default) |
|---|---|---|
| `width`, `height` | `rectangle`, `ellipse`, `box3d` | pt, 0 or more |
| `radius` | `polygon`, `spiral` | pt, 0 or more |
| `radius` (corners) | `rectangle`, `configure` | pt, 0 or more (0); `configure` fits a larger radius to half each rectangle's shorter side |
| `radii` | `rectangle`, `configure` | four numbers, 0 or more: `[tl, tr, br, bl]` |
| `radius1` | `star` | pt, 0 or more |
| `radius2` | `star` | 0.01 to 1 times `radius1` (half of it) |
| `sides`, `points` | `polygon`, `star`, `configure` (`sides`) | a whole number from 3 to 1000 (6 sides, 5 points) |
| `rounded`, `randomized` | `polygon`, `star`, `configure` | −10 to 10 (0) |
| `spokeRatio` | `configure` | 0.01 to 1 |
| `turns` | `spiral`, `configure` | 0.01 to 1024 (3) |
| `divergence` | `spiral`, `configure` | 0 to 1000 (1) |
| `innerRadius` | `spiral`, `configure` | 0 to 0.999 of the radius (0) |
| `arcMode` | `rectangle`, `ellipse`, `polygon`, `star`, `configure` | `whole`, `pie` (or `slice`), `chord`, `arc` (or `open`) (`whole`) |
A `configure` call that changes no shape records no undo step and returns `{updated: 0}`.
## Cutting Tools add-on
Cutting Tools is a built-in add-on that prepares art for cutting. Its commands are `addon.cutting.bridges` and
+9
View File
@@ -2031,6 +2031,15 @@ stored data), `cmd::addons::pathtools::tests` in `vectorcraft-engine` (each comm
anchor edits and transforms, the SVG export filter, the tool's gestures), `addons::pathtools::tests` in
`vectorcraft-ui-egui` (submenu and panel) and `command_sweep`.
The 3D Box and shapes add-on's commands (`addon.shapes.*`, 2026-11-13) refuse a value outside its range with an error
that names the parameter and the range, and draw or change nothing: polygon sides and star points are whole numbers
from 3 to 1,000 ([the help page](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/modify-live-shapes/live-shape-properties.html)),
sizes and corner radii are not negative, a star's inner radius is 0.01 to 1 times its outer radius, and rounding,
randomizing, spoke ratio, spiral turns, divergence and inner radius keep VectorCraft's own ranges; `arcMode` takes
only the names it lists. `addon.shapes.configure` checks every value before it changes any selected shape and records
no undo step when nothing changes. Tests: `cmd::addons::shapes::tests` in `vectorcraft-engine`. Commands:
`docs/mcp.md` › 3D Box and shapes add-on.
The add-on's Corner Tool (2026-10-31) edits the Live Corners every path already keeps (no second corner store): hover
shows an anchor that can take a corner, a click cuts one with the tool's radius and kind (round, inverted, chamfer), a
drag sizes it (Shift: whole values), a cut corner's marker selects it and loads its values, resizes it, and removes it