mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 06:01:18 +00:00
Repeat, Pattern: the Repeat and pattern commands reject a param of the wrong type
object.repeat.radial, object.repeat.grid, object.repeat.mirror and object.repeat.options, and object.pattern.make, object.pattern.edit, object.pattern.saveCopy, pattern.options and pattern.delete, read their params with helpers that fell back to the default for a value of the wrong type: "instances": "8" made 8 instances whatever the text said, "rows": "2" made 3 rows, "edit": "false" entered pattern editing mode, and a `name` that was not a string edited the selected object's pattern. Each such value is now an error naming the param, and the document, the selection and the undo history stay as they were. An omitted or null param keeps its default. - pattern.options refuses 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 refuses a `center` that is not [x, y]. - object.repeat.radial refuses a negative `radius`; it used the automatic radius before. - object.repeat.grid reads `rows` and `cols` as object.repeat.options does: a fraction is cut to a whole number, within 1 to 500. - object.repeat.options applies `brickOffset` given without `gridType` to a brick grid, and its params text lists the key. Docs: docs/mcp.md.
This commit is contained in:
@@ -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<CommandSpec> {
|
||||
@@ -134,7 +134,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"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<NodeId>) -> 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<TileType> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
|
||||
fn pattern_edit(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
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<Value> {
|
||||
}
|
||||
|
||||
fn pattern_save_copy(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
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<Value> {
|
||||
|
||||
fn pattern_options(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
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<Value> {
|
||||
}
|
||||
}
|
||||
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<Value> {
|
||||
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<bool> {
|
||||
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<NodeId> {
|
||||
|
||||
fn pattern_delete(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
}
|
||||
|
||||
fn repeat_mirror(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
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<usize
|
||||
fn repeat_options(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
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<Value> {
|
||||
}
|
||||
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;
|
||||
|
||||
@@ -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<vectorcraft_doc::Document>, u64, (usize, usize, bool), Vec<NodeId>) {
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -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 "`<pattern>` 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"}}}
|
||||
|
||||
Reference in New Issue
Block a user