Artboards: artboard.new and artboard.setProps refuse a number or name of the wrong type

artboard.setProps refused an x, y, width or height given as text, but
artboard.new read such a value as the key left out: {"x": "10"} added
an artboard at the default position and recorded an undo step. Both
commands read a name that is not a string, such as 5, as no name, so
artboard.new gave the default name and artboard.setProps kept the old
one.

artboard.new now refuses an x, y, width or height that is not a number
with "`<key>` must be a number, not <value>", the message
artboard.setProps gives, and both commands refuse such a name with
"`name` must be a string", for each entry of artboard.setProps'
artboards too. They refuse it before they change anything. A null is
still read as the key left out.

Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
dotCooCoo
2026-10-29 09:57:33 +07:00
committed by bflatastic
parent ad43722a56
commit 1deb1ec152
4 changed files with 51 additions and 2 deletions
+13 -2
View File
@@ -829,6 +829,16 @@ fn artboard_color(p: &Value, cmd: &str) -> Result<Option<Option<vectorcraft_colo
}
}
/// An artboard command's text param `key`: None when absent or null, else the string. Any other
/// value, such as 5, is an error: "`{key}` must be a string".
fn artboard_str<'a>(p: &'a Value, key: &str, cmd: &str) -> Result<Option<&'a str>> {
match p.get(key) {
None | Some(Value::Null) => Ok(None),
Some(Value::String(v)) => Ok(Some(v)),
Some(_) => Err(bad(cmd, format!("`{key}` must be a string"))),
}
}
/// An artboard command's `videoRulerPixelAspectRatio` param: None when absent, else a number within
/// [`vectorcraft_doc::VIDEO_PIXEL_ASPECT_RANGE`].
fn video_pixel_aspect(p: &Value, cmd: &str) -> Result<Option<f64>> {
@@ -849,12 +859,13 @@ fn artboard_new(s: &mut Session, p: &Value) -> Result<Value> {
let r = rect_from(p, def);
let color = artboard_color(p, "artboard.new")?.flatten();
check_switches(p, &["showCenterMark", "showCrossHairs", "showVideoSafeAreas"], "artboard.new")?;
check_numbers(p, &["x", "y", "width", "height"], "artboard.new")?;
let given = artboard_str(p, "name", "artboard.new")?.map(str::trim).filter(|n| !n.is_empty());
let (show_center_mark, show_cross_hairs) = (bool_or(p, "showCenterMark", false), bool_or(p, "showCrossHairs", false));
let show_video_safe_areas = bool_or(p, "showVideoSafeAreas", false);
let video_ruler_pixel_aspect_ratio = video_pixel_aspect(p, "artboard.new")?.unwrap_or(1.0);
let idx = s.edit("New Artboard", |d, _| {
let id = d.next_artboard_id();
let given = str_param(p, "name").map(str::trim).filter(|n| !n.is_empty());
let name = given.map_or_else(|| format!("Artboard {}", d.artboards.len() + 1), str::to_string);
d.artboards.push(Artboard {
id,
@@ -994,7 +1005,7 @@ fn artboard_set(s: &mut Session, p: &Value) -> Result<Value> {
edits.push(ArtboardEdit {
index,
now,
name: str_param(q, "name").map(str::trim),
name: artboard_str(q, "name", "artboard.setProps")?.map(str::trim),
color,
lock,
center_mark,
+33
View File
@@ -311,6 +311,39 @@ fn artboard_commands_refuse_a_boolean_or_number_given_as_text() {
assert_eq!(node(&s, r).geometric_bounds().map(|b| b.x0), Some(20.0));
}
/// New Artboard refuses a position or size given as text or as another value that is not a number,
/// such as "10", and New Artboard and Artboard Options refuse a name that is not text, such as 5.
/// The artboards and the undo history stay as they were; a null reads as the key left out.
#[test]
fn artboard_commands_refuse_a_number_or_name_of_the_wrong_type() {
use crate::tests::{not_a_number, not_refused};
let mut s = session();
s.execute("artboard.new", &json!({})).unwrap();
let mut calls: Vec<_> = [
("artboard.new", "x", json!({"x": "10"})),
("artboard.new", "y", json!({"x": 2000, "y": [0]})),
("artboard.new", "width", json!({"width": "200"})),
("artboard.new", "height", json!({"height": true})),
]
.into_iter()
.map(|(cmd, key, p)| not_a_number(cmd, p, key))
.collect();
let not_text = |cmd: &str, p: serde_json::Value| (cmd.to_string(), p, "`name` must be a string".to_string());
calls.extend([
not_text("artboard.new", json!({"name": 5})),
not_text("artboard.setProps", json!({"index": 0, "name": 5})),
not_text("artboard.setProps", json!({"index": 0, "x": 50, "name": true})),
not_text("artboard.setProps", json!({"artboards": [{"index": 0, "name": "Front"}, {"index": 1, "name": ["Back"]}]})),
]);
let wrong = not_refused(&mut s, &calls);
assert!(wrong.is_empty(), "not refused: {wrong:#?}");
s.execute("artboard.new", &json!({"x": null, "y": null, "width": null, "height": null, "name": null})).unwrap();
s.execute("artboard.setProps", &json!({"index": 0, "name": "Front"})).unwrap();
s.execute("artboard.setProps", &json!({"index": 0, "name": null})).unwrap();
let names: Vec<String> = boards(&s).into_iter().map(|(_, n)| n).collect();
assert_eq!(names, ["Front", "Artboard 2", "Artboard 3"]);
}
/// Artboards 1 and 2, each with a square on it, and an artboard 3 below artboard 1 with a square on
/// it, with the Artboard tool selecting artboards 1 and 2 → the squares' ids, artboard by artboard.
fn two_selected_artboards(s: &mut Session) -> [u64; 3] {
+4
View File
@@ -3192,6 +3192,10 @@ were: `moveArt`, `scaleArt`, `strokes`, `corners`, `patterns`, `lockedAndHidden`
and `art` in `artboard.copy` and `artboard.cut`. `artboard.setProps` refuses an artboard's `x`, `y`, `width` or
`height` that is not a number, and `artboard.move` refuses such a `dx` or `dy`, with
`` `dx` must be a number, not "10" ``. A null is read as a key left out.
`artboard.new` refuses an `x`, `y`, `width` or `height` that is not a number with the same message, and
`artboard.new` and `artboard.setProps` refuse a `name` that is not a string, such as 5, with
`` `name` must be a string ``. Both leave the artboards and the undo history as they were, and read a null as the
key left out.
Scale Artwork with Artboard (#602): `artboard.setProps {index, x?, y?, width?, height?, scaleArt: true}` resizes an
artboard and scales the art fully inside it, and its artboard guides, from the old rectangle onto the new one, each side
+1
View File
@@ -894,6 +894,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
Object Properties (its opacity), Live Shape Properties (corner radius and polygon sides), the Transform panel's reference point and Align, Distribute and Distribute Spacing to an artboard (the artboard's number) refuse a value other than a number or null, such as "50", and leave the document as it was.
Transform Each (its scales, moves and rotation), Rasterize (its resolution), Create Object Mosaic (its new width and height and its spacing), Create Trim Marks (its offset, length, bleed and weight) and Rearrange All Artboards (its spacing) refuse a value other than a number or null, such as "50", and change nothing. Crop Image refuses a rectangle that is not four numbers, such as "0, 0, 20, 20".
Transform Each (Reflect X, Reflect Y, Random, Copy, Scale Strokes & Effects, Scale Corners, Transform Objects and Transform Patterns), Create Object Mosaic (gray tiles and deleting the image), Crop Image (its trim), Create Trim Marks (all artboards) and Rearrange All Artboards (by column, Move Artwork with Artboard and Transform Patterns) refuse a value other than true, false or null, such as "false", and change nothing. Create Trim Marks refuses a style that is not text, such as 5.
New Artboard refuses a position or size given as text, such as "10", and New Artboard and Artboard Options refuse a name that is not text, such as 5; each leaves the artboards and the undo history as they were.
Artboard Options (Move Artwork, Scale Artwork, Scale Strokes & Effects, Scale Corners, Transform Patterns, locked and hidden art and the lock), Move Artboard (Move Artwork, the copy and Transform Patterns), Duplicate Artboards (Transform Patterns) and Duplicate, Copy and Cut Artboard (the art) refuse a value other than true, false or null, such as "true", and Artboard Options and Move Artboard refuse a position, size or move given as text, such as "10"; each leaves the document, its undo history and the clipboard as they were.
Change Case, Smart Punctuation, Find and Replace, Convert To Area Type, Convert To Point Type, Type on a Path Options, the Type on a Path effects and Fill with Placeholder Text record no undo step when they change nothing.
The commands that take `keepModel` (fills and strokes, Appearance rows, Live Paint, Shape Builder fills, gradient stops and freeform points, gradient mesh points, type colors, the Symbol Stainer and the swatch commands) refuse a `keepModel` other than true, false or null, such as "true". Toggle Fill/Stroke Focus (`fill`), Invert, Complement, Apply Last Color and Apply Last Gradient (`stroke`), `paint.setFill` and `paint.setStroke` (`focus`, `none`), and `appearance.setItem`, `livePaint.fill`, `livePaint.strokeEdge`, a `shapeBuilder.merge` `fill`, `swatch.new` and `swatch.edit`'s `paint` (`none`) refuse such a value too. Each leaves the document, its undo history and the defaults for new art as they were.