mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 05:24:44 +00:00
Path Precision Tools: Corner commands refuse unknown item keys
addon.pathtools.cornerSet and cornerRemove now refuse a key other than id and anchors in an `items` member, with the standard unknown-parameter error, instead of ignoring it. A misspelled `anchor` used to fall back to the request's `anchors`, else the path's direct-selected anchors, else every corner of the path. They also refuse `items` given together with `ids`, which `items` replaces. Saved steps played with replay still run as recorded. Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
@@ -713,6 +713,71 @@ fn corner_commands_refuse_bad_params() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corner_commands_refuse_an_unknown_item_key() {
|
||||
let mut s = session();
|
||||
let id = square(&mut s);
|
||||
let i = id.0;
|
||||
let set = json!({"items": [{"id": i, "anchor": [0]}], "radius": 5});
|
||||
let e = s.execute("addon.pathtools.cornerSet", &set).unwrap_err().to_string();
|
||||
assert!(e.contains("unknown parameter `items[0].anchor`; did you mean `items[0].anchors`?"), "{e}");
|
||||
let set_late = json!({"items": [{"id": i, "anchors": [0]}, {"id": i, "radius": 3}], "radius": 5});
|
||||
let e = s.execute("addon.pathtools.cornerSet", &set_late).unwrap_err().to_string();
|
||||
assert!(e.contains("unknown parameter `items[1].radius`; the command takes: items[1].id, items[1].anchors"), "{e}");
|
||||
refuses(&mut s, "addon.pathtools.cornerSet", &[set, set_late]);
|
||||
s.execute("addon.pathtools.cornerSet", &json!({"radius": 10})).unwrap();
|
||||
let remove = json!({"items": [{"id": i, "anchor": [0]}]});
|
||||
let e = s.execute("addon.pathtools.cornerRemove", &remove).unwrap_err().to_string();
|
||||
assert!(e.contains("unknown parameter `items[0].anchor`; did you mean `items[0].anchors`?"), "{e}");
|
||||
refuses(&mut s, "addon.pathtools.cornerRemove", &[remove]);
|
||||
assert_eq!(drawn(&s, id, 0), 10.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corner_items_take_their_documented_keys() {
|
||||
let mut s = session();
|
||||
let id = square(&mut s);
|
||||
let i = id.0;
|
||||
let r = s.execute("addon.pathtools.cornerSet", &json!({"items": [{"id": i, "anchors": [0]}], "radius": 5})).unwrap();
|
||||
assert_eq!((r["corners"].clone(), drawn(&s, id, 0), drawn(&s, id, 1)), (json!(1), 5.0, 0.0));
|
||||
// A member without `anchors` takes the request's.
|
||||
s.execute("addon.pathtools.cornerSet", &json!({"items": [{"id": i}], "anchors": [2], "radius": 6})).unwrap();
|
||||
assert_eq!(drawn(&s, id, 2), 6.0);
|
||||
let r = s.execute("addon.pathtools.cornerRemove", &json!({"items": [{"id": i, "anchors": [0]}]})).unwrap();
|
||||
assert_eq!((r["corners"].clone(), drawn(&s, id, 0), drawn(&s, id, 2)), (json!(1), 0.0, 6.0));
|
||||
// A null `ids` beside `items` counts as not given.
|
||||
s.execute("addon.pathtools.cornerRemove", &json!({"items": [{"id": i}], "ids": null})).unwrap();
|
||||
assert_eq!(drawn(&s, id, 2), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corner_commands_refuse_items_with_ids() {
|
||||
let mut s = session();
|
||||
let id = square(&mut s);
|
||||
let other = line(&mut s, &[(350.0, 10.0), (390.0, 10.0), (390.0, 50.0)], false);
|
||||
let both = json!({"items": [{"id": id.0, "anchors": [0]}], "ids": [other.0], "radius": 5});
|
||||
let e = s.execute("addon.pathtools.cornerSet", &both).unwrap_err().to_string();
|
||||
assert!(e.contains("give `items` or `ids`, not both"), "{e}");
|
||||
refuses(&mut s, "addon.pathtools.cornerSet", &[both]);
|
||||
s.execute("addon.pathtools.cornerSet", &json!({"ids": [id.0], "radius": 5})).unwrap();
|
||||
let both = json!({"items": [{"id": id.0}], "ids": [id.0]});
|
||||
let e = s.execute("addon.pathtools.cornerRemove", &both).unwrap_err().to_string();
|
||||
assert!(e.contains("give `items` or `ids`, not both"), "{e}");
|
||||
refuses(&mut s, "addon.pathtools.cornerRemove", &[both]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn saved_corner_steps_with_older_item_keys_still_play() {
|
||||
let mut s = session();
|
||||
let id = square(&mut s);
|
||||
let other = line(&mut s, &[(350.0, 10.0), (390.0, 10.0), (390.0, 50.0)], false);
|
||||
let saved = json!({"items": [{"id": id.0, "anchors": [0], "old": 1}], "ids": [other.0], "radius": 5});
|
||||
s.replay("addon.pathtools.cornerSet", &saved).unwrap();
|
||||
assert_eq!((drawn(&s, id, 0), drawn(&s, id, 1)), (5.0, 0.0));
|
||||
s.replay("addon.pathtools.cornerRemove", &json!({"items": [{"id": id.0, "old": 1}]})).unwrap();
|
||||
assert_eq!(drawn(&s, id, 0), 0.0);
|
||||
}
|
||||
|
||||
/// A closed five-pointed star about (200, 150): its points at the even indices 0, 2… (the odd
|
||||
/// numbers counting from 1), its inner corners at the odd ones.
|
||||
fn star_path(s: &mut Session) -> NodeId {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
//! save or Export for Screens: those are checked against the options of the formats the command
|
||||
//! can write, in `marks`, `bleed` and the other objects too ([`check_export_options`]); and in the
|
||||
//! objects of an array whose keys `MEMBER_KEYS` lists, such as the `items` of
|
||||
//! `object.setLiveShape`.
|
||||
//! `object.setLiveShape` and `addon.pathtools.cornerSet`.
|
||||
//!
|
||||
//! [`Session::execute`] answers a key the description doesn't name with a parameter error
|
||||
//! ([`check_params`]), for every caller (the app, the control channel, MCP, the CLI) and for the
|
||||
@@ -256,6 +256,22 @@ pub fn check_keys(id: &str, doc: &'static str, params: &Value) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parameters a command takes instead of one another: (command, key, other key). Giving both,
|
||||
/// neither null, is a parameter error ([`check_exclusive_keys`]).
|
||||
const EXCLUSIVE_KEYS: &[(&str, &str, &str)] = &[("addon.pathtools.cornerSet", "items", "ids"), ("addon.pathtools.cornerRemove", "items", "ids")];
|
||||
|
||||
/// `params` of command `id` give at most one of each pair in [`EXCLUSIVE_KEYS`], or the parameter
|
||||
/// error naming the pair.
|
||||
fn check_exclusive_keys(id: &str, params: &Value) -> Result<()> {
|
||||
let given = |key: &str| params.get(key).filter(|v| !v.is_null());
|
||||
for (_, key, other) in EXCLUSIVE_KEYS.iter().filter(|(c, ..)| *c == id) {
|
||||
if given(key).is_some() && given(other).is_some() {
|
||||
return Err(bad(id, format!("give `{key}` or `{other}`, not both")));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The parameter error of command `id` for `key`, which isn't one of `keys`: with the nearest of
|
||||
/// them, or all of them.
|
||||
pub(crate) fn unknown(id: &str, key: &str, keys: &[&str]) -> crate::EngineError {
|
||||
@@ -273,7 +289,11 @@ pub(crate) fn unknown(id: &str, key: &str, keys: &[&str]) -> crate::EngineError
|
||||
|
||||
/// Commands whose params hold an array of objects with keys of their own: the command, the key of
|
||||
/// the array and the keys each of its objects takes.
|
||||
const MEMBER_KEYS: &[(&str, &str, &[&str])] = &[("object.setLiveShape", "items", &["id", "corners"])];
|
||||
const MEMBER_KEYS: &[(&str, &str, &[&str])] = &[
|
||||
("object.setLiveShape", "items", &["id", "corners"]),
|
||||
("addon.pathtools.cornerSet", "items", &["id", "anchors"]),
|
||||
("addon.pathtools.cornerRemove", "items", &["id", "anchors"]),
|
||||
];
|
||||
|
||||
/// Each object in the arrays [`MEMBER_KEYS`] names for command `id` has only the keys it lists, or
|
||||
/// the parameter error naming the first other one with its place (`items[0].corner`), and the
|
||||
@@ -848,9 +868,9 @@ fn check_export_options(id: &str, params: &Value) -> Result<()> {
|
||||
check_option_keys(id, "", given, &keys, why_not, &trees)
|
||||
}
|
||||
|
||||
/// [`check_keys`] for an engine command, the steps of a `command.batch` or an `action.batch` included,
|
||||
/// [`check_effect_params`] for the effect `effect.apply` adds and [`check_export_options`] for an
|
||||
/// export. A command the engine doesn't have passes: running it reports that.
|
||||
/// [`check_keys`], [`check_exclusive_keys`] and [`check_member_keys`] for an engine command, the steps of a `command.batch` or an
|
||||
/// `action.batch` included, [`check_effect_params`] for the effect `effect.apply` adds and
|
||||
/// [`check_export_options`] for an export. A command the engine doesn't have passes: running it reports that.
|
||||
pub fn check_params(id: &str, params: &Value) -> Result<()> {
|
||||
check_params_in(id, params, false)
|
||||
}
|
||||
@@ -869,6 +889,7 @@ fn check_params_in(id: &str, params: &Value, in_batch: bool) -> Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
check_keys(id, spec.params, params)?;
|
||||
check_exclusive_keys(id, params)?;
|
||||
check_member_keys(id, params)?;
|
||||
if in_batch {
|
||||
crate::schemas::check(spec, params, false)?;
|
||||
|
||||
+3
-2
@@ -10753,7 +10753,8 @@ Commands:
|
||||
- `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
|
||||
anchors, or all of them. `items: [{id, anchors?}…]` gives each path its own instead; a member key other than `id`
|
||||
and `anchors`, or `items` given with `ids`, is an error. Only anchors where two sides meet
|
||||
at an angle take a corner: smooth anchors and open ends are skipped, and it is an error if none is left. `radius` is
|
||||
0 to 1,000,000 pt, drawn no larger than fits; `kind` is `round`, `invertedRound` or `chamfer` (default: each corner's
|
||||
own). `every` (`all`, `odd` or `even`; default `all`) keeps, of those anchors, only the odd- or even-numbered ones,
|
||||
@@ -10763,7 +10764,7 @@ Commands:
|
||||
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}`.
|
||||
with none left is plain again. Its `items` take the same keys as `cornerSet`'s. → `{ids, corners}`.
|
||||
- `addon.pathtools.slideAnchor {id, subpath?, anchor, t}` moves an anchor along its path, the segments beside it refitted
|
||||
so the outline stays (the anchor count and index stay). `t`, -0.98 to 0.98, says where: positive, a curve parameter on
|
||||
the segment after the anchor; negative, back from the end of the one before. An anchor at a sharp corner loses it, as
|
||||
|
||||
@@ -2064,6 +2064,9 @@ Corners section in the Anchor Inspector sets the radius and kind, counts the sel
|
||||
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), and only the corners whose angle is in a range
|
||||
(`minAngle`/`maxAngle`, #5545).
|
||||
`cornerSet` and `cornerRemove` refuse an `items` member key other than `id` and `anchors`, and `items` given with `ids`;
|
||||
saved steps still play (`corner_commands_refuse_an_unknown_item_key`, `corner_commands_refuse_items_with_ids`,
|
||||
`saved_corner_steps_with_older_item_keys_still_play`).
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user