From 1deb1ec152e613577bf6cbb8b4f0e33421f6cb71 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 28 Oct 2026 03:45:00 -0700 Subject: [PATCH] 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 "`` must be a number, not ", 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. --- crates/engine/src/cmd/layer.rs | 15 +++++++++++-- crates/engine/src/tests/panelcmds.rs | 33 ++++++++++++++++++++++++++++ docs/mcp.md | 4 ++++ docs/target-app-parity.md | 1 + 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/cmd/layer.rs b/crates/engine/src/cmd/layer.rs index 83df1ff3f..9bb73b7b4 100644 --- a/crates/engine/src/cmd/layer.rs +++ b/crates/engine/src/cmd/layer.rs @@ -829,6 +829,16 @@ fn artboard_color(p: &Value, cmd: &str) -> Result(p: &'a Value, key: &str, cmd: &str) -> Result> { + 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> { @@ -849,12 +859,13 @@ fn artboard_new(s: &mut Session, p: &Value) -> Result { 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 { 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, diff --git a/crates/engine/src/tests/panelcmds.rs b/crates/engine/src/tests/panelcmds.rs index 44ff73563..fabf5cc54 100644 --- a/crates/engine/src/tests/panelcmds.rs +++ b/crates/engine/src/tests/panelcmds.rs @@ -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 = 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] { diff --git a/docs/mcp.md b/docs/mcp.md index 2bef0d6fb..0b0b3bf23 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -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 diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index d99d1595f..0eaca59ad 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -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.