diff --git a/crates/engine/src/cmd/patterncmds.rs b/crates/engine/src/cmd/patterncmds.rs index 55c4390c2..ef7820b71 100644 --- a/crates/engine/src/cmd/patterncmds.rs +++ b/crates/engine/src/cmd/patterncmds.rs @@ -15,13 +15,13 @@ use serde_json::{Value, json}; use vectorcraft_color::Paint; use vectorcraft_doc::pattern::{self, Overlap, PATTERN_EDIT_LAYER, PatternDef, PatternEdit, RepeatKind, RepeatSpec, TileType, pattern_paint}; use vectorcraft_doc::{Document, Node, NodeId, NodeKind, Selection}; -use vectorcraft_geom::Rect; +use vectorcraft_geom::{Point, Rect}; use super::edit::{outermost, selected_roots}; use super::pathops::Beside; use super::{ - CommandSpec, EngineError, Result, Session, bad, bool_or, cmd, f64_or, has_doc, has_selection, ids_param, matrix_param, ok, point_param, - str_param, targets, + CommandSpec, EngineError, Result, Session, bad, check_numbers, check_switches, cmd, f64_or, finite_numbers, has_doc, has_selection, ids_param, + matrix_param, ok, switch_or, targets, text_param, }; pub fn specs() -> Vec { @@ -134,7 +134,7 @@ pub fn specs() -> Vec { "Options…", ["Object", "Repeat"], None, - "{instances?, radius?, reverseOverlap?, startAngle?, endAngle?, center?: [x,y] | hSpacing?, vSpacing?, rows?, cols?, gridType?, flipRows?, flipCols? | angle?} set the options of the selected repeats", + "{instances?, radius?, reverseOverlap?, startAngle?, endAngle?, center?: [x,y] | hSpacing?, vSpacing?, rows?, cols?, gridType?, brickOffset?, flipRows?, flipCols? | angle?} set the options of the selected repeats", has_repeat, repeat_options ), @@ -287,9 +287,12 @@ fn after_edit_mode(s: &mut Session, layer: Option) -> Result<()> { Ok(()) } +/// The tile type `p` gives `cmd` (`tileType`, `brickOffset`), else `current` with a given brick offset. +/// A `tileType` that is not a string, or a `brickOffset` that is not a number, is an error. fn tile_type_param(p: &Value, cmd: &str, current: TileType) -> Result { + check_numbers(p, &["brickOffset"], cmd)?; let off = p.get("brickOffset").and_then(Value::as_f64).map(|o| if o > 1.0 { o / 100.0 } else { o }); - match str_param(p, "tileType") { + match text_param(p, "tileType", cmd)? { Some(t) => TileType::parse(t, off).ok_or_else(|| bad(cmd, format!("unknown tileType `{t}` ({})", TileType::IDS.join("|")))), None => Ok(match (current, off) { (TileType::BrickByRow { .. }, Some(o)) => TileType::BrickByRow { offset: o }, @@ -311,12 +314,13 @@ fn pattern_make(s: &mut Session, p: &Value) -> Result { return Err(bad(C, "select the art for the pattern tile")); } let tt = tile_type_param(p, C, TileType::Grid)?; + check_numbers(p, &["width", "height"], C)?; let (w, h) = (p.get("width").and_then(Value::as_f64), p.get("height").and_then(Value::as_f64)); if w.is_some_and(|v| !(v.is_finite() && v > 0.0)) || h.is_some_and(|v| !(v.is_finite() && v > 0.0)) { return Err(bad(C, "width/height must be > 0")); } - let edit = bool_or(p, "edit", true); - let wanted = str_param(p, "name").map(str::trim).filter(|n| !n.is_empty()).map(str::to_string); + let edit = switch_or(p, "edit", true, C)?; + let wanted = text_param(p, "name", C)?.map(str::trim).filter(|n| !n.is_empty()).map(str::to_string); let name = s.edit("Make Pattern", |d, sel| { let name = match &wanted { Some(n) => unique_pattern_name(d, n), @@ -343,7 +347,7 @@ fn pattern_make(s: &mut Session, p: &Value) -> Result { fn pattern_edit(s: &mut Session, p: &Value) -> Result { const C: &str = "object.pattern.edit"; - let name = match str_param(p, "name") { + let name = match text_param(p, "name", C)? { Some(n) => n.to_string(), None => { // The selected object's fill (or stroke) pattern. @@ -419,7 +423,7 @@ fn pattern_cancel(s: &mut Session, _: &Value) -> Result { } fn pattern_save_copy(s: &mut Session, p: &Value) -> Result { - let wanted = str_param(p, "name").map(str::trim).filter(|n| !n.is_empty()).map(str::to_string); + let wanted = text_param(p, "name", "object.pattern.saveCopy")?.map(str::trim).filter(|n| !n.is_empty()).map(str::to_string); let name = s.edit("Save a Copy", |d, _| { let pe = d.pattern_edit.clone().ok_or_else(|| EngineError::Other("not in pattern editing mode".into()))?; let mut def = d.pattern(&pe.pattern).cloned().ok_or_else(|| EngineError::Other("pattern missing".into()))?; @@ -438,19 +442,21 @@ fn pattern_save_copy(s: &mut Session, p: &Value) -> Result { fn pattern_options(s: &mut Session, p: &Value) -> Result { const C: &str = "pattern.options"; - let name = match str_param(p, "name") { + let name = match text_param(p, "name", C)? { Some(n) => n.to_string(), None => editing(s).map(|e| e.pattern.clone()).ok_or_else(|| bad(C, "give `name` (or edit a pattern)"))?, }; let cur = s.doc()?.doc.pattern(&name).cloned().ok_or_else(|| bad(C, format!("no pattern `{name}`")))?; let tt = tile_type_param(p, C, cur.tile_type)?; + check_numbers(p, &["width", "height", "hSpacing", "vSpacing", "copies", "dimCopies"], C)?; + check_switches(p, &["sizeTileToArt", "showTileEdge", "showSwatchBounds"], C)?; let num = |k: &str| p.get(k).and_then(Value::as_f64); for k in ["width", "height"] { if num(k).is_some_and(|v| !(v.is_finite() && v > 0.0)) { return Err(bad(C, format!("{k} must be > 0"))); } } - let new_name = str_param(p, "newName").map(str::trim).filter(|n| !n.is_empty() && *n != name).map(str::to_string); + let new_name = text_param(p, "newName", C)?.map(str::trim).filter(|n| !n.is_empty() && *n != name).map(str::to_string); if let Some(n) = &new_name { let d = &s.doc()?.doc; if d.pattern(n).is_some() { @@ -461,11 +467,15 @@ fn pattern_options(s: &mut Session, p: &Value) -> Result { } } let overlap = match p.get("overlap") { - Some(o) => Overlap { - right_in_front: str_param(o, "h").map_or(cur.overlap.right_in_front, |v| v == "right"), - bottom_in_front: str_param(o, "v").map_or(cur.overlap.bottom_in_front, |v| v == "bottom"), + None | Some(Value::Null) => cur.overlap, + Some(o) if o.is_object() => Overlap { + right_in_front: overlap_side(o, "h", ["left", "right"], cur.overlap.right_in_front, C)?, + bottom_in_front: overlap_side(o, "v", ["top", "bottom"], cur.overlap.bottom_in_front, C)?, }, - None => cur.overlap, + Some(got) => { + let (path, expected) = ("overlap", "an object"); + return Err(bad(C, format!("`{path}` must be {expected}, not {got}"))); + } }; let edit_layer = editing(s).filter(|e| e.pattern == name).map(|e| e.layer); let out_name = new_name.clone().unwrap_or(name.clone()); @@ -514,6 +524,19 @@ fn pattern_options(s: &mut Session, p: &Value) -> Result { Ok(json!({ "name": out_name })) } +/// Side `side` of the `overlap` object `o` gives `cmd`: true for the second of `names`, false for the +/// first, `current` when `o` leaves it out or gives null. Any other value is an error. +fn overlap_side(o: &Value, side: &str, names: [&str; 2], current: bool, cmd: &str) -> Result { + match o.get(side) { + None | Some(Value::Null) => Ok(current), + Some(Value::String(v)) if names.contains(&v.as_str()) => Ok(v == names[1]), + Some(_) => { + let key = format!("overlap.{side}"); + Err(bad(cmd, format!("`{key}` must be one of {}", names.join(", ")))) + } + } +} + /// Is `new` taken for pattern `old` by a swatch or a color group (they share one namespace, /// [`Document::swatch_name_taken`])? It is free when the swatches named `new` are one or more /// swatches of the pattern and none of the pattern's swatches is named `old` (those take the new @@ -571,7 +594,7 @@ fn nodes_using(d: &Document, name: Option<&str>) -> Vec { fn pattern_delete(s: &mut Session, p: &Value) -> Result { const C: &str = "pattern.delete"; - let name = str_param(p, "name").ok_or_else(|| bad(C, "missing name"))?.to_string(); + let name = text_param(p, "name", C)?.ok_or_else(|| bad(C, "missing name"))?.to_string(); if s.doc()?.doc.pattern(&name).is_none() { return Err(bad(C, format!("no pattern `{name}`"))); } @@ -694,18 +717,25 @@ fn make_repeat(s: &mut Session, p: &Value, cmd: &str, label: &str, make: impl Fn } fn repeat_radial(s: &mut Session, p: &Value) -> Result { + const C: &str = "object.repeat.radial"; + check_numbers(p, &["instances", "radius"], C)?; let n = f64_or(p, "instances", 8.0); if !(1.0..=1000.0).contains(&n) { - return Err(bad("object.repeat.radial", "instances must be 1..1000")); + return Err(bad(C, "instances must be 1..1000")); } - let radius = p.get("radius").and_then(Value::as_f64).filter(|r| r.is_finite() && *r >= 0.0); - make_repeat(s, p, "object.repeat.radial", "Radial Repeat", |src| RepeatSpec::radial(src, n as u32, radius)) + let radius = p.get("radius").and_then(Value::as_f64); + if radius.is_some_and(|r| r < 0.0) { + let key = "radius"; + return Err(bad(C, format!("`{key}` must be a number not below 0"))); + } + make_repeat(s, p, C, "Radial Repeat", |src| RepeatSpec::radial(src, n as u32, radius)) } fn repeat_grid(s: &mut Session, p: &Value) -> Result { + check_numbers(p, &["hSpacing", "vSpacing", "rows", "cols"], "object.repeat.grid")?; let (hs, vs) = (p.get("hSpacing").and_then(Value::as_f64), p.get("vSpacing").and_then(Value::as_f64)); - let rows = p.get("rows").and_then(Value::as_u64).map(|v| v.clamp(1, 500) as u32); - let cols = p.get("cols").and_then(Value::as_u64).map(|v| v.clamp(1, 500) as u32); + let rows = p.get("rows").and_then(Value::as_f64).map(|v| (v as u32).clamp(1, 500)); + let cols = p.get("cols").and_then(Value::as_f64).map(|v| (v as u32).clamp(1, 500)); make_repeat(s, p, "object.repeat.grid", "Grid Repeat", |src| { let b = vectorcraft_doc::live::nodes_bounds(&src).unwrap_or(Rect::new(0.0, 0.0, 10.0, 10.0)); let mut r = RepeatSpec::grid(src, hs.unwrap_or(b.width() / 4.0), vs.unwrap_or(b.height() / 4.0)); @@ -718,6 +748,7 @@ fn repeat_grid(s: &mut Session, p: &Value) -> Result { } fn repeat_mirror(s: &mut Session, p: &Value) -> Result { + check_numbers(p, &["angle", "offset"], "object.repeat.mirror")?; let angle = f64_or(p, "angle", 90.0); let offset = f64_or(p, "offset", 10.0); make_repeat(s, p, "object.repeat.mirror", "Mirror Repeat", |src| RepeatSpec::mirror(src, angle, offset)) @@ -803,16 +834,26 @@ pub(crate) fn expand_repeats(d: &mut Document, roots: &[NodeId]) -> Result Result { const C: &str = "object.repeat.options"; let reps = selected_repeats(s); + let numbers = ["instances", "radius", "startAngle", "endAngle", "hSpacing", "vSpacing", "rows", "cols", "brickOffset", "angle"]; + check_numbers(p, &numbers, C)?; + check_switches(p, &["reverseOverlap", "flipRows", "flipCols"], C)?; let num = |k: &str| p.get(k).and_then(Value::as_f64).filter(|v| v.is_finite()); let flag = |k: &str| p.get(k).and_then(Value::as_bool); if num("instances").is_some_and(|n| !(1.0..=1000.0).contains(&n)) { return Err(bad(C, "instances must be 1..1000")); } - let grid_type = match str_param(p, "gridType") { + let grid_type = match text_param(p, "gridType", C)? { Some(t) => Some(TileType::parse(t, num("brickOffset")).ok_or_else(|| bad(C, format!("unknown gridType `{t}`")))?), None => None, }; - let center = point_param(p, "center"); + let center = match p.get("center") { + None | Some(Value::Null) => None, + Some(v) => { + let key = "center"; + let [x, y] = finite_numbers::<2>(v).ok_or_else(|| bad(C, format!("{key} must be [x, y] with finite coordinates")))?; + Some(Point::new(x, y)) + } + }; s.edit("Repeat Options", |d, _| { for r in &reps { let Some(NodeKind::Repeat(spec)) = d.node_mut(*r).map(|n| &mut n.kind) else { continue }; @@ -852,6 +893,8 @@ fn repeat_options(s: &mut Session, p: &Value) -> Result { } if let Some(t) = grid_type { *gt = t; + } else if let (Some(v), TileType::BrickByRow { offset } | TileType::BrickByColumn { offset }) = (num("brickOffset"), &mut *gt) { + *offset = v; } if let Some(v) = flag("flipRows") { *flip_rows = v; diff --git a/crates/engine/src/tests/pattern.rs b/crates/engine/src/tests/pattern.rs index c36d84fd9..0b948242d 100644 --- a/crates/engine/src/tests/pattern.rs +++ b/crates/engine/src/tests/pattern.rs @@ -1112,3 +1112,184 @@ fn mirror_axis_hidden_controls_and_other_repeat_kinds_do_not_capture_drags() { assert_eq!(node(&s, id), before); } } + +/// The document, its revision, the undo and redo stacks and the selection: what a refused call +/// must leave as it was. +fn untouched(s: &Session) -> (std::sync::Arc, u64, (usize, usize, bool), Vec) { + let st = s.doc().unwrap(); + (st.doc.clone(), st.revision, crate::tests::history(s), st.selection.objects.to_vec()) +} + +/// Each call in `calls` is refused with the parameter error `want` and changes nothing. +fn refused(s: &mut Session, calls: Vec<(&str, Value, String)>) { + let before = untouched(s); + for (cmd, p, want) in calls { + let r = s.execute(cmd, &p); + assert!(matches!(&r, Err(EngineError::BadParams { msg, .. }) if *msg == want), "{cmd} {p}: {r:?}, want {want:?}"); + assert!(untouched(s) == before, "{cmd} {p}: nothing changes"); + } +} + +fn number(k: &str, v: Value) -> String { + format!("`{k}` must be a number, not {v}") +} + +fn switch(k: &str) -> String { + format!("`{k}` must be true or false") +} + +fn string(k: &str) -> String { + format!("`{k}` must be a string") +} + +/// Radial, Grid, Mirror and Repeat Options refuse a param of the wrong type with an error naming +/// it, and change nothing; a distance given as text with a unit is refused like any other string. +/// Null counts as a param left out, which keeps its default. +#[test] +fn repeat_commands_refuse_a_param_of_the_wrong_type() { + let mut s = session(); + let r = rect(&mut s, 100.0, 100.0, 60.0, 80.0); + sel(&mut s, &[r]); + refused( + &mut s, + vec![ + ("object.repeat.radial", json!({"instances": "8"}), number("instances", json!("8"))), + ("object.repeat.radial", json!({"radius": true}), number("radius", json!(true))), + ("object.repeat.radial", json!({"radius": "5 mm"}), number("radius", json!("5 mm"))), + ("object.repeat.radial", json!({"radius": -5}), "`radius` must be a number not below 0".into()), + ("object.repeat.grid", json!({"hSpacing": "5 mm"}), number("hSpacing", json!("5 mm"))), + ("object.repeat.grid", json!({"vSpacing": [1]}), number("vSpacing", json!([1]))), + ("object.repeat.grid", json!({"rows": "3"}), number("rows", json!("3"))), + ("object.repeat.grid", json!({"cols": false}), number("cols", json!(false))), + ("object.repeat.mirror", json!({"angle": "45"}), number("angle", json!("45"))), + ("object.repeat.mirror", json!({"offset": {}}), number("offset", json!({}))), + ], + ); + // Null params keep the defaults: each repeat matches the one made with `{}`. + for (cmd, nulls) in [ + ("object.repeat.radial", json!({"instances": null, "radius": null})), + ("object.repeat.grid", json!({"hSpacing": null, "vSpacing": null, "rows": null, "cols": null})), + ("object.repeat.mirror", json!({"angle": null, "offset": null})), + ] { + let made = |p: &Value| { + let mut s = session(); + let r = rect(&mut s, 100.0, 100.0, 60.0, 80.0); + sel(&mut s, &[r]); + let id = id_of(&s.execute(cmd, p).unwrap()); + node(&s, id) + }; + assert_eq!(made(&nulls), made(&json!({})), "{cmd}"); + } + // A grid's rows and cols take any number, as Repeat Options does: 2.5 rows are 2. + let id = id_of(&s.execute("object.repeat.grid", &json!({"rows": 2.5, "cols": 4})).unwrap()); + assert!(matches!(node(&s, id).kind, NodeKind::Repeat(ref r) if matches!(r.kind, RepeatKind::Grid { rows: 2, cols: 4, .. }))); + // `brickOffset` alone sets the offset of a brick grid, and leaves a plain grid as it is. + let grid_type = |s: &Session| match node(s, id).kind { + NodeKind::Repeat(r) => match r.kind { + RepeatKind::Grid { grid_type, .. } => grid_type, + other => panic!("not a grid: {other:?}"), + }, + other => panic!("not a repeat: {other:?}"), + }; + s.execute("object.repeat.options", &json!({"brickOffset": 0.25})).unwrap(); + assert_eq!(grid_type(&s), TileType::Grid); + s.execute("object.repeat.options", &json!({"gridType": "brickByRow"})).unwrap(); + s.execute("object.repeat.options", &json!({"brickOffset": 0.25})).unwrap(); + assert_eq!(grid_type(&s), TileType::BrickByRow { offset: 0.25 }); + + // Repeat Options checks every param, whichever kind the selected repeat is. + s.execute("object.repeat.radial", &json!({"instances": 4, "radius": 50})).unwrap(); + let kind = |s: &Session| match node(s, id).kind { + NodeKind::Repeat(r) => r.kind, + other => panic!("not a repeat: {other:?}"), + }; + let radial = kind(&s); + refused( + &mut s, + vec![ + ("object.repeat.options", json!({"instances": "4"}), number("instances", json!("4"))), + ("object.repeat.options", json!({"radius": "50 pt"}), number("radius", json!("50 pt"))), + ("object.repeat.options", json!({"startAngle": true}), number("startAngle", json!(true))), + ("object.repeat.options", json!({"endAngle": "x"}), number("endAngle", json!("x"))), + ("object.repeat.options", json!({"hSpacing": "1"}), number("hSpacing", json!("1"))), + ("object.repeat.options", json!({"vSpacing": [1]}), number("vSpacing", json!([1]))), + ("object.repeat.options", json!({"rows": "2"}), number("rows", json!("2"))), + ("object.repeat.options", json!({"cols": [2]}), number("cols", json!([2]))), + ("object.repeat.options", json!({"brickOffset": "0.5"}), number("brickOffset", json!("0.5"))), + ("object.repeat.options", json!({"angle": "10"}), number("angle", json!("10"))), + ("object.repeat.options", json!({"reverseOverlap": "true"}), switch("reverseOverlap")), + ("object.repeat.options", json!({"flipRows": 1}), switch("flipRows")), + ("object.repeat.options", json!({"flipCols": "no"}), switch("flipCols")), + ("object.repeat.options", json!({"gridType": 3}), string("gridType")), + ("object.repeat.options", json!({"center": [1]}), "center must be [x, y] with finite coordinates".into()), + ("object.repeat.options", json!({"center": "0, 0"}), "center must be [x, y] with finite coordinates".into()), + ], + ); + let all_null = json!({"instances": null, "radius": null, "startAngle": null, "endAngle": null, "reverseOverlap": null, "center": null}); + s.execute("object.repeat.options", &all_null).unwrap(); + assert_eq!(kind(&s), radial, "null params keep the options"); + s.execute("object.repeat.options", &json!({"instances": 6, "reverseOverlap": true, "center": [10, 20]})).unwrap(); + assert!(matches!(kind(&s), RepeatKind::Radial { instances: 6, reverse_overlap: true, .. })); +} + +/// Make, Edit Pattern, Save a Copy, Pattern Options and Delete Pattern refuse a param of the wrong +/// type (and Pattern Options an `overlap` side it doesn't know) with an error naming it, and change +/// nothing. Null counts as a param left out, which keeps its default. +#[test] +fn pattern_commands_refuse_a_param_of_the_wrong_type() { + let mut s = session(); + let r = make_dots(&mut s); + sel(&mut s, &[r]); + let options = |p: Value| { + let mut p = p; + p["name"] = json!("Dots"); + p + }; + refused( + &mut s, + vec![ + ("object.pattern.make", json!({"name": 5}), string("name")), + ("object.pattern.make", json!({"tileType": 2}), string("tileType")), + ("object.pattern.make", json!({"brickOffset": "0.5"}), number("brickOffset", json!("0.5"))), + ("object.pattern.make", json!({"width": "20"}), number("width", json!("20"))), + ("object.pattern.make", json!({"height": true}), number("height", json!(true))), + ("object.pattern.make", json!({"edit": "false"}), switch("edit")), + ("object.pattern.edit", json!({"name": 5}), string("name")), + ("pattern.options", json!({"name": ["Dots"]}), string("name")), + ("pattern.options", options(json!({"newName": 5})), string("newName")), + ("pattern.options", options(json!({"tileType": []})), string("tileType")), + ("pattern.options", options(json!({"width": "20"})), number("width", json!("20"))), + ("pattern.options", options(json!({"hSpacing": "1"})), number("hSpacing", json!("1"))), + ("pattern.options", options(json!({"vSpacing": true})), number("vSpacing", json!(true))), + ("pattern.options", options(json!({"copies": "5"})), number("copies", json!("5"))), + ("pattern.options", options(json!({"dimCopies": "50"})), number("dimCopies", json!("50"))), + ("pattern.options", options(json!({"sizeTileToArt": "true"})), switch("sizeTileToArt")), + ("pattern.options", options(json!({"showTileEdge": 1})), switch("showTileEdge")), + ("pattern.options", options(json!({"showSwatchBounds": "no"})), switch("showSwatchBounds")), + ("pattern.options", options(json!({"overlap": "right"})), "`overlap` must be an object, not \"right\"".into()), + ("pattern.options", options(json!({"overlap": {"h": "center"}})), "`overlap.h` must be one of left, right".into()), + ("pattern.options", options(json!({"overlap": {"v": 1}})), "`overlap.v` must be one of top, bottom".into()), + ("pattern.delete", json!({"name": 5}), string("name")), + ], + ); + // Null params keep the defaults: a new pattern named "New Pattern 1", a grid tile the size of + // the art, edited. + let made = s + .execute("object.pattern.make", &json!({"name": null, "tileType": null, "brickOffset": null, "width": null, "height": null, "edit": null})) + .unwrap(); + assert_eq!(made["name"], "New Pattern 1"); + let def = doc(&s).pattern("New Pattern 1").unwrap().clone(); + assert_eq!(def.tile_type, TileType::Grid); + assert!((def.tile.width() - 10.0).abs() < 1e-6, "{:?}", def.tile); + assert_eq!(doc(&s).pattern_edit.as_ref().map(|e| e.pattern.as_str()), Some("New Pattern 1")); + // In editing mode: Save a Copy refuses a name that isn't a string, and Pattern Options with + // null params keeps the edited pattern's options. + refused(&mut s, vec![("object.pattern.saveCopy", json!({"name": 5}), string("name"))]); + s.execute("pattern.options", &json!({"name": null, "overlap": {"h": null, "v": null}, "copies": null, "sizeTileToArt": null})).unwrap(); + assert!(doc(&s).pattern("New Pattern 1").unwrap() == &def); + s.execute("pattern.options", &json!({"overlap": {"h": "right"}})).unwrap(); + assert!(doc(&s).pattern("New Pattern 1").unwrap().overlap.right_in_front); + assert_eq!(s.execute("object.pattern.saveCopy", &json!({"name": null})).unwrap()["name"], "New Pattern 1 copy"); + s.execute("object.pattern.done", &json!({})).unwrap(); + assert!(s.execute("pattern.delete", &json!({"name": null})).is_err(), "no name given"); +} diff --git a/docs/mcp.md b/docs/mcp.md index 58d235b4c..4c49ca675 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -3081,6 +3081,14 @@ the old name. A rename gives the new name to the pattern's swatches that had the old name keeps it. A blank name gives "New Pattern N" in `object.pattern.make` and "`` copy" in `object.pattern.saveCopy`, and `pattern.options` keeps the name. +The pattern commands (`object.pattern.make`, `object.pattern.edit`, `object.pattern.saveCopy`, `pattern.options`, +`pattern.delete`) and the Repeat commands (`object.repeat.radial`, `object.repeat.grid`, `object.repeat.mirror`, +`object.repeat.options`) reject a param of the wrong type, such as `"instances": "8"`, `"edit": "false"` or +`"tileType": 2`, and change nothing. `pattern.options` also rejects an `overlap` that is not an object, and an +`overlap.h` other than `left` or `right` or an `overlap.v` other than `top` or `bottom`. `object.repeat.options` rejects +a `center` that is not `[x, y]`, and `object.repeat.radial` rejects a negative `radius`. Omitting a param or giving null +keeps its default. + ```json {"name":"run_command","arguments":{"command":"pattern.options","params":{"showSwatchBounds":true}}} {"name":"run_command","arguments":{"command":"prefs.set","params":{"key":"patternTileEdgeColor","value":"#ff4f4f"}}}