mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 07:10:13 +00:00
Merge pull request #7623 from dotCooCoo/fix/artboard-history-index-wrong-type
MCP: a malformed artboard or history index is reported as not a whole number
This commit is contained in:
@@ -6,8 +6,8 @@ use vectorcraft_doc::Scaling;
|
||||
use vectorcraft_geom::{Rect, Vec2};
|
||||
|
||||
use super::object::choice;
|
||||
use super::panelcmds::{artboard_index, artboards_art, locked_and_hidden, named_or_selected, note_artboards};
|
||||
use super::{Affine, CommandSpec, NodeId, Result, Session, bad, check_switches, cmd, has_doc, switch_or};
|
||||
use super::panelcmds::{artboard_at, artboards_art, locked_and_hidden, named_or_selected, note_artboards, on_board};
|
||||
use super::{Affine, CommandSpec, NodeId, Result, Session, bad, check_switches, cmd, has_doc, index, switch_or};
|
||||
use crate::EngineError;
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
@@ -63,13 +63,7 @@ fn boards(s: &mut Session, p: &Value, cmd: &str) -> Result<Boards> {
|
||||
check_switches(p, &["patterns"], cmd)?;
|
||||
let to_artboard = choice(p, cmd, "to", &["selection", "artboard"])? == Some("artboard");
|
||||
let indices = named_or_selected(s, p, cmd)?;
|
||||
let given = |key: &str| -> Result<Option<usize>> {
|
||||
match p.get(key).filter(|v| !v.is_null()) {
|
||||
Some(v) => artboard_index(&s.doc()?.doc, &json!({ "index": v }), cmd).map(Some),
|
||||
None => Ok(None),
|
||||
}
|
||||
};
|
||||
let (named, recorded) = (given("artboard")?, given("activeArtboard")?);
|
||||
let (named, recorded) = (artboard_at(&s.doc()?.doc, p, "artboard", cmd)?, artboard_at(&s.doc()?.doc, p, "activeArtboard", cmd)?);
|
||||
let move_art = switch_or(p, "moveArt", s.tool_options_of("artboard")["moveArt"].as_bool().unwrap_or(true), cmd)?;
|
||||
let locked_art = locked_and_hidden(s, p, move_art, cmd)?;
|
||||
note_artboards(s, p, &indices);
|
||||
@@ -78,8 +72,8 @@ fn boards(s: &mut Session, p: &Value, cmd: &str) -> Result<Boards> {
|
||||
(false, _) => None,
|
||||
(true, Some(i)) => Some(i),
|
||||
(true, None) => {
|
||||
let active = s.tool_options_of("artboard")["active"].clone();
|
||||
Some(artboard_index(&s.doc()?.doc, &json!({ "index": active }), cmd)?)
|
||||
let active = s.tool_options_of("artboard")["active"].as_u64().map(index);
|
||||
Some(active.map_or_else(|| Err(EngineError::Other("no such artboard".into())), |i| on_board(&s.doc()?.doc, i))?)
|
||||
}
|
||||
};
|
||||
if let Some(i) = reference.filter(|_| named.is_none()) {
|
||||
|
||||
@@ -808,11 +808,11 @@ fn new_document_from_state(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let created = date_param(p, "created", C)?;
|
||||
let st = s.doc()?;
|
||||
// The row as the panel lists it now: keeping an edit in progress below changes the rows.
|
||||
let chosen = match p.get("index") {
|
||||
None | Some(Value::Null) => None,
|
||||
Some(v) => {
|
||||
let state = v.as_u64().and_then(|row| st.history.state(&st.doc, index(row)));
|
||||
Some(state.ok_or_else(|| bad(C, format!("no history state {v}")))?.clone())
|
||||
let chosen = match super::index_opt(p, "index", C)? {
|
||||
None => None,
|
||||
Some(row) => {
|
||||
let v = &p["index"];
|
||||
Some(st.history.state(&st.doc, row).ok_or_else(|| bad(C, format!("no history state {v}")))?.clone())
|
||||
}
|
||||
};
|
||||
settle_interaction(s)?;
|
||||
@@ -1276,23 +1276,28 @@ pub(crate) fn place_clip(s: &mut Session, clip: &Clipboard, center: vectorcraft_
|
||||
/// The artboards Paste in Place, in Front or in Back puts a copy on, in artboard order: `artboard`
|
||||
/// (the active one, #693), else `artboards`, else, while the Artboard tool is chosen with several
|
||||
/// artboards selected, those (noted in the journal as `artboards`, so a replay pastes on the same
|
||||
/// ones); `None` when none is named. A named index that is not an artboard of the document, a
|
||||
/// repeated one or an empty list is an error.
|
||||
/// ones); `None` when none is named. A named value that is not a whole number (null included), a
|
||||
/// named index that is not an artboard of the document, a repeated one or an empty list is an
|
||||
/// error; every value is checked for its form before any is checked against the artboards.
|
||||
fn paste_targets(s: &mut Session, p: &Value, cmd: &str) -> Result<Option<Vec<usize>>> {
|
||||
let n = s.doc()?.doc.artboards.len();
|
||||
let one = |v: &Value| v.as_u64().and_then(|i| usize::try_from(i).ok()).filter(|i| *i < n);
|
||||
let single = match p.get("artboard") {
|
||||
Some(v) => Some(one(v).ok_or_else(|| bad(cmd, "`artboard` must be the index of an artboard of the document"))?),
|
||||
None => None,
|
||||
};
|
||||
let listed = match p.get("artboards") {
|
||||
let single = p.get("artboard").map(|v| v.as_u64().map(index).ok_or_else(|| bad(cmd, "`artboard` must be a whole number"))).transpose()?;
|
||||
let entries = match p.get("artboards") {
|
||||
Some(v) => {
|
||||
let list = v.as_array().filter(|a| !a.is_empty()).ok_or_else(|| bad(cmd, "`artboards` must be a list of artboard indices"))?;
|
||||
let mut all = list
|
||||
.iter()
|
||||
.map(one)
|
||||
.collect::<Option<Vec<usize>>>()
|
||||
.ok_or_else(|| bad(cmd, "`artboards` names an artboard the document does not have"))?;
|
||||
let all = list.iter().map(|i| i.as_u64().map(index)).collect::<Option<Vec<usize>>>();
|
||||
Some(all.ok_or_else(|| bad(cmd, "`artboards` must contain whole numbers"))?)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
if single.is_some_and(|i| i >= n) {
|
||||
return Err(bad(cmd, "`artboard` must be the index of an artboard of the document"));
|
||||
}
|
||||
let listed = match entries {
|
||||
Some(mut all) => {
|
||||
if all.iter().any(|i| *i >= n) {
|
||||
return Err(bad(cmd, "`artboards` names an artboard the document does not have"));
|
||||
}
|
||||
all.sort_unstable();
|
||||
if all.windows(2).any(|w| w[0] == w[1]) {
|
||||
return Err(bad(cmd, "`artboards` names an artboard twice"));
|
||||
|
||||
@@ -5,7 +5,7 @@ use serde_json::{Value, json};
|
||||
use vectorcraft_doc::{Composer, JustificationOptions, NodeKind, Scaling, SingleWordJustify, TextRun};
|
||||
use vectorcraft_geom::{Rect, Vec2};
|
||||
|
||||
use super::{Affine, CommandSpec, NodeId, Result, Session, bad, bool_or, check_switches, cmd, has_doc, index, ok, point_indices, switch_or};
|
||||
use super::{Affine, CommandSpec, NodeId, Result, Session, bad, bool_or, check_switches, cmd, has_doc, index, ok, switch_or};
|
||||
use crate::{Clipboard, EngineError};
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
@@ -137,19 +137,16 @@ pub(crate) fn named_or_selected(s: &Session, p: &Value, cmd: &str) -> Result<Vec
|
||||
return artboard_indices(d, p, cmd);
|
||||
}
|
||||
if let Some(recorded) = p.get("selectedArtboards") {
|
||||
return artboard_indices(d, &json!({ "indices": recorded }), cmd);
|
||||
return listed_artboards(d, recorded, "selectedArtboards", cmd);
|
||||
}
|
||||
let mut picked: Vec<u64> = t["selected"]
|
||||
.as_array()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(Value::as_u64)
|
||||
.filter(|i| usize::try_from(*i).is_ok_and(|i| i < d.artboards.len()))
|
||||
.collect();
|
||||
let mut picked: Vec<usize> =
|
||||
t["selected"].as_array().into_iter().flatten().filter_map(Value::as_u64).map(index).filter(|i| *i < d.artboards.len()).collect();
|
||||
if picked.is_empty() {
|
||||
picked.push(t["active"].as_u64().unwrap_or(0));
|
||||
picked.push(on_board(d, t["active"].as_u64().map_or(0, index))?);
|
||||
}
|
||||
artboard_indices(d, &json!({ "indices": picked }), cmd)
|
||||
picked.sort_unstable();
|
||||
picked.dedup();
|
||||
Ok(picked)
|
||||
}
|
||||
|
||||
/// Rename Artboards: one artboard takes the name, and several take it numbered in artboard order, in
|
||||
@@ -221,23 +218,51 @@ fn artboard_switch_orientation(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(json!({ "switched": switched }))
|
||||
}
|
||||
|
||||
/// The one artboard `cmd` acts on: `index`, an artboard of `d`, with the errors of
|
||||
/// [`artboard_indices`] (`missing index`, `no such artboard`).
|
||||
/// The one artboard `cmd` acts on: `index`, an artboard of `d`. Left out or null, it is
|
||||
/// `missing index`; a value that is not a whole number is "`index` must be a whole number"; a
|
||||
/// whole number past the last artboard is `no such artboard`.
|
||||
pub(crate) fn artboard_index(d: &vectorcraft_doc::Document, p: &Value, cmd: &str) -> Result<usize> {
|
||||
let i = p.get("index").ok_or_else(|| bad(cmd, "missing index"))?;
|
||||
artboard_indices(d, &json!({ "index": i }), cmd)?.into_iter().next().ok_or_else(|| EngineError::Other("no such artboard".into()))
|
||||
artboard_at(d, p, "index", cmd)?.ok_or_else(|| bad(cmd, "missing index"))
|
||||
}
|
||||
|
||||
/// The artboard of `d` that `key` of `p` names, or `None` when `p` leaves it out or gives null. A
|
||||
/// value that is not a whole number is "`{key}` must be a whole number", and a whole number past the
|
||||
/// last artboard is `no such artboard`.
|
||||
pub(crate) fn artboard_at(d: &vectorcraft_doc::Document, p: &Value, key: &str, cmd: &str) -> Result<Option<usize>> {
|
||||
super::index_opt(p, key, cmd)?.map(|i| on_board(d, i)).transpose()
|
||||
}
|
||||
|
||||
/// `i` when it is an artboard of `d`, else `no such artboard`.
|
||||
pub(crate) fn on_board(d: &vectorcraft_doc::Document, i: usize) -> Result<usize> {
|
||||
match i < d.artboards.len() {
|
||||
true => Ok(i),
|
||||
false => Err(EngineError::Other("no such artboard".into())),
|
||||
}
|
||||
}
|
||||
|
||||
/// The artboards `cmd` acts on: `indices` (several), else `index` (one), each an artboard of `d`,
|
||||
/// in order and once each.
|
||||
/// in order and once each. `indices` is read as [`listed_artboards`] reads it, and `index` as
|
||||
/// [`artboard_index`] reads it.
|
||||
pub(crate) fn artboard_indices(d: &vectorcraft_doc::Document, p: &Value, cmd: &str) -> Result<Vec<usize>> {
|
||||
let listed = match (p.get("indices"), p.get("index")) {
|
||||
(Some(v), _) => point_indices(v),
|
||||
(None, Some(i)) => point_indices(&json!([i])),
|
||||
(None, None) => return Err(bad(cmd, "missing index")),
|
||||
};
|
||||
let mut indices =
|
||||
listed.filter(|v| !v.is_empty() && v.iter().all(|i| *i < d.artboards.len())).ok_or_else(|| EngineError::Other("no such artboard".into()))?;
|
||||
match p.get("indices") {
|
||||
Some(v) => listed_artboards(d, v, "indices", cmd),
|
||||
None => Ok(vec![artboard_index(d, p, cmd)?]),
|
||||
}
|
||||
}
|
||||
|
||||
/// `v`, the value of `key`, as artboards of `d`, in order and once each. A value that is not a list
|
||||
/// is "`{key}` must be a list of artboard indices", an empty list is "`{key}` must be a non-empty
|
||||
/// list of artboard indices", and a list with an entry that is not a whole number is "`{key}` must
|
||||
/// contain whole numbers"; a whole number past the last artboard is `no such artboard`. Every entry
|
||||
/// is checked for its form before any is checked against the artboards.
|
||||
fn listed_artboards(d: &vectorcraft_doc::Document, v: &Value, key: &str, cmd: &str) -> Result<Vec<usize>> {
|
||||
let entries = v.as_array().ok_or_else(|| bad(cmd, format!("`{key}` must be a list of artboard indices")))?;
|
||||
if entries.is_empty() {
|
||||
return Err(bad(cmd, format!("`{key}` must be a non-empty list of artboard indices")));
|
||||
}
|
||||
let listed = entries.iter().map(Value::as_u64).collect::<Option<Vec<u64>>>();
|
||||
let listed = listed.ok_or_else(|| bad(cmd, format!("`{key}` must contain whole numbers")))?;
|
||||
let mut indices = listed.into_iter().map(|i| on_board(d, index(i))).collect::<Result<Vec<usize>>>()?;
|
||||
indices.sort_unstable();
|
||||
indices.dedup();
|
||||
Ok(indices)
|
||||
|
||||
@@ -1034,14 +1034,39 @@ fn in_place_pastes_put_a_copy_on_each_artboard_named() {
|
||||
// `artboard` wins over `artboards`.
|
||||
let ids = ids_of(&s.execute(cmd, &json!({"artboard": 2, "artboards": [0, 3]})).unwrap());
|
||||
assert_eq!(doc(&s).bounds_of(&ids, false).unwrap().origin(), Point::new(boards[2].x0 + 10.0, boards[2].y0 + 5.0), "{cmd}");
|
||||
// A list that is empty, names an artboard the document lacks or names one twice, or isn't a
|
||||
// list: an error, and nothing pasted.
|
||||
let before = doc(&s).clone();
|
||||
for bad in [json!([]), json!([0, 4]), json!([1, 1]), json!([0, -1]), json!(2), json!("0,1")] {
|
||||
assert!(s.execute(cmd, &json!({"artboards": bad})).is_err(), "{cmd} with artboards {bad}");
|
||||
// A list that is empty, names an artboard the document lacks or names one twice, holds a
|
||||
// value that is not a whole number, or isn't a list, and an `artboard` that is not a whole
|
||||
// number or not an artboard: an error naming the fault, and nothing pasted. Both keys are
|
||||
// checked when both are given.
|
||||
let (before, undo) = (doc(&s).clone(), s.doc().unwrap().history.undo.len());
|
||||
let (not_list, missing, not_whole) = (
|
||||
"`artboards` must be a list of artboard indices",
|
||||
"`artboards` names an artboard the document does not have",
|
||||
"`artboards` must contain whole numbers",
|
||||
);
|
||||
let cases = [
|
||||
(json!({"artboards": []}), not_list),
|
||||
(json!({"artboards": 2}), not_list),
|
||||
(json!({"artboards": "0,1"}), not_list),
|
||||
(json!({"artboards": [0, 4]}), missing),
|
||||
(json!({"artboards": [1, 1]}), "`artboards` names an artboard twice"),
|
||||
(json!({"artboards": [0, -1]}), not_whole),
|
||||
(json!({"artboards": [0, 1.5]}), not_whole),
|
||||
(json!({"artboards": [4, "1"]}), not_whole),
|
||||
(json!({"artboard": 1, "artboards": [7]}), missing),
|
||||
(json!({"artboard": 1, "artboards": ["1"]}), not_whole),
|
||||
(json!({"artboard": 4}), "`artboard` must be the index of an artboard of the document"),
|
||||
(json!({"artboard": -1}), "`artboard` must be a whole number"),
|
||||
(json!({"artboard": 1.5}), "`artboard` must be a whole number"),
|
||||
(json!({"artboard": "1"}), "`artboard` must be a whole number"),
|
||||
(json!({"artboard": null}), "`artboard` must be a whole number"),
|
||||
(json!({"artboard": "1", "artboards": [9]}), "`artboard` must be a whole number"),
|
||||
];
|
||||
for (p, want) in cases {
|
||||
let e = s.execute(cmd, &p).unwrap_err().to_string();
|
||||
assert_eq!(e, format!("invalid parameters for `{cmd}`: {want}"), "{cmd} {p}");
|
||||
}
|
||||
assert!(s.execute(cmd, &json!({"artboard": 1, "artboards": [7]})).is_err(), "{cmd}: a bad list with `artboard`");
|
||||
assert_eq!(doc(&s), &before, "{cmd}");
|
||||
assert_eq!((doc(&s), s.doc().unwrap().history.undo.len()), (&before, undo), "{cmd}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -79,18 +79,29 @@ fn the_source_stays_as_it_is() {
|
||||
assert!(s.doc().unwrap().is_dirty(), "the new document is unsaved");
|
||||
}
|
||||
|
||||
/// An index that names no state, or is not a whole number, is an error and changes nothing: no
|
||||
/// document, no step and no Untitled number is used up.
|
||||
/// An index that names no state (`no history state N`), or is not a whole number ("`index` must be
|
||||
/// a whole number"), is an error and changes nothing: no document, no step and no Untitled number
|
||||
/// is used up.
|
||||
#[test]
|
||||
fn a_bad_index_is_an_error_and_changes_nothing() {
|
||||
let mut s = three_rectangles_one_undone();
|
||||
let source = s.doc().unwrap().doc.clone();
|
||||
let untitled = s.peek_untitled();
|
||||
let bad: [Value; 8] = [json!(4), json!(99), json!(-1), json!(1.5), json!("1"), json!(true), json!([1]), json!(u64::MAX)];
|
||||
for index in bad {
|
||||
let bad: [(Value, bool); 8] = [
|
||||
(json!(4), true),
|
||||
(json!(99), true),
|
||||
(json!(u64::MAX), true),
|
||||
(json!(-1), false),
|
||||
(json!(1.5), false),
|
||||
(json!("1"), false),
|
||||
(json!(true), false),
|
||||
(json!([1]), false),
|
||||
];
|
||||
for (index, whole) in bad {
|
||||
let e = s.execute(CMD, &json!({ "index": index })).unwrap_err();
|
||||
assert!(matches!(e, EngineError::BadParams { .. }), "{index}: {e}");
|
||||
assert!(e.to_string().contains(&format!("no history state {index}")), "{e}");
|
||||
let want = if whole { format!("no history state {index}") } else { "`index` must be a whole number".to_string() };
|
||||
assert_eq!(e.to_string(), format!("invalid parameters for `{CMD}`: {want}"), "{index}");
|
||||
assert_eq!(s.documents().len(), 1, "{index}");
|
||||
assert_eq!(steps(&s), (2, 1), "{index}");
|
||||
assert!(Arc::ptr_eq(&s.doc().unwrap().doc, &source), "{index}");
|
||||
|
||||
@@ -611,15 +611,18 @@ fn edit_copy_and_cut_read_the_recorded_artboards() {
|
||||
s.execute("select.none", &json!({})).unwrap();
|
||||
assert!(matches!(s.execute(id, &json!({})), Err(crate::EngineError::Disabled(..))), "{id}");
|
||||
let before = documents(&s);
|
||||
for bad in [
|
||||
json!({"selectedArtboards": [9]}),
|
||||
json!({"selectedArtboards": []}),
|
||||
json!({"selectedArtboards": "1"}),
|
||||
json!({"selectedArtboards": null}),
|
||||
json!({"selectedArtboards": [1], "art": "yes"}),
|
||||
for (bad, want) in [
|
||||
(json!({"selectedArtboards": [9]}), "no such artboard"),
|
||||
(json!({"selectedArtboards": []}), "`selectedArtboards` must be a non-empty list of artboard indices"),
|
||||
(json!({"selectedArtboards": "1"}), "`selectedArtboards` must be a list of artboard indices"),
|
||||
(json!({"selectedArtboards": null}), "`selectedArtboards` must be a list of artboard indices"),
|
||||
(json!({"selectedArtboards": [1, "0"]}), "`selectedArtboards` must contain whole numbers"),
|
||||
(json!({"selectedArtboards": [1, -1]}), "`selectedArtboards` must contain whole numbers"),
|
||||
(json!({"selectedArtboards": [1], "art": "yes"}), "`art` must be true or false"),
|
||||
] {
|
||||
let err = s.execute(id, &bad).unwrap_err().to_string();
|
||||
assert!(err.contains(id) || err.contains("no such artboard"), "{id} {bad}: {err}");
|
||||
assert!(err.ends_with(want), "{id} {bad}: {err}");
|
||||
assert!(err.contains(id) || want == "no such artboard", "{id} {bad}: {err}");
|
||||
assert_eq!(documents(&s), before, "{id} {bad}");
|
||||
assert!(s.clipboard.artboards.is_empty(), "{id} {bad}");
|
||||
}
|
||||
|
||||
@@ -1979,9 +1979,10 @@ fn fit_to_artwork_bounds_fits_each_artboard_to_its_own_art() {
|
||||
assert_eq!(boards(&s), [Rect::new(100.0, 100.0, 1050.0, 280.0), was[1], was[2]], "the first artboard by default");
|
||||
}
|
||||
|
||||
/// Artboard Options and Fit to Selected Art fail with `no such artboard` on an `index` that names
|
||||
/// no artboard (past the last, negative, fractional, a string or null), and the artboards stay as
|
||||
/// they were. Artboard Options needs an `index`; Fit to Selected Art without one fits the first
|
||||
/// Artboard Options and Fit to Selected Art fail with `no such artboard` on a whole-number `index`
|
||||
/// past the last artboard, with "`index` must be a whole number" on one that is negative,
|
||||
/// fractional or a string, and with `missing index` on a null one, and the artboards stay as they
|
||||
/// were. Artboard Options needs an `index`; Fit to Selected Art without one fits the first
|
||||
/// artboard.
|
||||
#[test]
|
||||
fn artboard_options_and_fit_to_selected_art_refuse_an_index_that_names_no_artboard() {
|
||||
@@ -1992,9 +1993,16 @@ fn artboard_options_and_fit_to_selected_art_refuse_an_index_that_names_no_artboa
|
||||
s.execute("paint.setStroke", &json!({"none": true})).unwrap();
|
||||
let boards = |s: &Session| s.doc().unwrap().doc.artboards.iter().map(|a| a.rect).collect::<Vec<_>>();
|
||||
let (was, n) = (boards(&s), undo_len(&s));
|
||||
for index in [json!(99), json!(-1), json!(1.5), json!("1"), Value::Null] {
|
||||
for (index, error) in [
|
||||
(json!(99), None),
|
||||
(json!(-1), Some("`index` must be a whole number")),
|
||||
(json!(1.5), Some("`index` must be a whole number")),
|
||||
(json!("1"), Some("`index` must be a whole number")),
|
||||
(Value::Null, Some("missing index")),
|
||||
] {
|
||||
for (cmd, p) in [("artboard.setProps", json!({"index": index, "width": 300})), ("artboard.fitToSelection", json!({"index": index}))] {
|
||||
assert_eq!(s.execute(cmd, &p).unwrap_err().to_string(), "no such artboard", "{cmd} {p}");
|
||||
let want = error.map_or_else(|| "no such artboard".to_string(), |e| format!("invalid parameters for `{cmd}`: {e}"));
|
||||
assert_eq!(s.execute(cmd, &p).unwrap_err().to_string(), want, "{cmd} {p}");
|
||||
}
|
||||
}
|
||||
let e = s.execute("artboard.setProps", &json!({"width": 300})).unwrap_err().to_string();
|
||||
|
||||
@@ -492,9 +492,11 @@ fn new_and_renamed_artboards_trim_their_names() {
|
||||
assert_eq!(names(&s), ["Back", "Artboard 2", "Artboard 3", "Artboard 4"]);
|
||||
}
|
||||
|
||||
/// Duplicate, Copy and Cut Artboard and Move Artboard Up/Down fail with `no such artboard` on an
|
||||
/// `index` that names no artboard (past the last, negative, fractional, a string or null), and the
|
||||
/// artboards, the art and the clipboard stay as they were.
|
||||
/// Duplicate, Copy and Cut Artboard and Move Artboard Up/Down fail with `no such artboard` on a
|
||||
/// whole-number `index` past the last artboard, with "`index` must be a whole number" on one that
|
||||
/// is negative, fractional, a string, true or a list, and with `missing index` on a null one,
|
||||
/// instead of acting on the Artboard tool's active artboard. The artboards, the art and the
|
||||
/// clipboard stay as they were.
|
||||
#[test]
|
||||
fn artboard_copies_refuse_an_index_that_names_no_artboard() {
|
||||
let mut s = session();
|
||||
@@ -502,10 +504,22 @@ fn artboard_copies_refuse_an_index_that_names_no_artboard() {
|
||||
s.execute("artboard.new", &json!({})).unwrap();
|
||||
s.set_tool_options(Some("artboard"), json!({"active": 1}).as_object().unwrap());
|
||||
let (was, n) = (boards(&s), s.doc().unwrap().history.undo.len());
|
||||
for index in [json!(99), json!(-1), json!(1.5), json!("1"), serde_json::Value::Null] {
|
||||
let not_whole = Some("`index` must be a whole number");
|
||||
let cases = [
|
||||
(json!(99), None),
|
||||
(json!(u64::MAX), None),
|
||||
(json!(-1), not_whole),
|
||||
(json!(1.5), not_whole),
|
||||
(json!("1"), not_whole),
|
||||
(json!(true), not_whole),
|
||||
(json!([1]), not_whole),
|
||||
(serde_json::Value::Null, Some("missing index")),
|
||||
];
|
||||
for (index, error) in cases {
|
||||
for cmd in ["artboard.duplicate", "artboard.copy", "artboard.cut", "artboard.reorder"] {
|
||||
let p = if cmd == "artboard.reorder" { json!({"index": index, "to": 0}) } else { json!({"index": index}) };
|
||||
assert_eq!(s.execute(cmd, &p).unwrap_err().to_string(), "no such artboard", "{cmd} {p}");
|
||||
let want = error.map_or_else(|| "no such artboard".to_string(), |e| format!("invalid parameters for `{cmd}`: {e}"));
|
||||
assert_eq!(s.execute(cmd, &p).unwrap_err().to_string(), want, "{cmd} {p}");
|
||||
}
|
||||
}
|
||||
let st = s.doc().unwrap();
|
||||
@@ -513,6 +527,90 @@ fn artboard_copies_refuse_an_index_that_names_no_artboard() {
|
||||
assert!(s.clipboard.nodes.is_empty() && s.clipboard.artboards.is_empty(), "nothing copied");
|
||||
}
|
||||
|
||||
/// The artboard commands that take `indices` (and `selectedArtboards`, a replay of the journal)
|
||||
/// name the fault: a value that is not a list, an empty list and a list with an entry that is not a
|
||||
/// whole number each have their own message naming the key, and a whole number past the last
|
||||
/// artboard is `no such artboard`, also when another entry is malformed after it. The artboards,
|
||||
/// the art, the undo history and the clipboard stay as they were.
|
||||
#[test]
|
||||
fn artboard_lists_name_the_fault_and_the_key() {
|
||||
let mut s = session();
|
||||
rect(&mut s);
|
||||
s.execute("artboard.new", &json!({})).unwrap();
|
||||
let (was, n) = (boards(&s), s.doc().unwrap().history.undo.len());
|
||||
let cmds = [
|
||||
("artboard.duplicate", json!({})),
|
||||
("artboard.copy", json!({})),
|
||||
("artboard.cut", json!({})),
|
||||
("artboard.rename", json!({"name": "A"})),
|
||||
("artboard.switchOrientation", json!({})),
|
||||
("artboard.delete", json!({})),
|
||||
("artboard.fitToArt", json!({})),
|
||||
("artboard.move", json!({"dx": 10, "dy": 0})),
|
||||
("artboard.align", json!({"horizontal": "left"})),
|
||||
];
|
||||
for key in ["indices", "selectedArtboards"] {
|
||||
let cases = [
|
||||
(json!(null), format!("`{key}` must be a list of artboard indices")),
|
||||
(json!(1), format!("`{key}` must be a list of artboard indices")),
|
||||
(json!("0,1"), format!("`{key}` must be a list of artboard indices")),
|
||||
(json!([]), format!("`{key}` must be a non-empty list of artboard indices")),
|
||||
(json!([0, -1]), format!("`{key}` must contain whole numbers")),
|
||||
(json!([0, 1.5]), format!("`{key}` must contain whole numbers")),
|
||||
(json!(["1"]), format!("`{key}` must contain whole numbers")),
|
||||
(json!([null]), format!("`{key}` must contain whole numbers")),
|
||||
(json!([9, "1"]), format!("`{key}` must contain whole numbers")),
|
||||
(json!([0, 9]), "no such artboard".to_string()),
|
||||
];
|
||||
for (cmd, base) in &cmds {
|
||||
if key == "selectedArtboards" && matches!(*cmd, "artboard.delete" | "artboard.fitToArt" | "artboard.move") {
|
||||
continue;
|
||||
}
|
||||
for (value, want) in &cases {
|
||||
let mut p = base.clone();
|
||||
p[key] = value.clone();
|
||||
let want = if want.starts_with('`') { format!("invalid parameters for `{cmd}`: {want}") } else { want.clone() };
|
||||
assert_eq!(s.execute(cmd, &p).unwrap_err().to_string(), want, "{cmd} {p}");
|
||||
}
|
||||
}
|
||||
}
|
||||
let st = s.doc().unwrap();
|
||||
assert_eq!((boards(&s), st.doc.layers[0].children().unwrap().len(), st.history.undo.len()), (was, 1, n));
|
||||
assert!(s.clipboard.nodes.is_empty() && s.clipboard.artboards.is_empty(), "nothing copied");
|
||||
}
|
||||
|
||||
/// Align, Distribute and Distribute Spacing for artboards name the key that holds a malformed
|
||||
/// artboard to align to (`artboard` or `activeArtboard`), not `index`; a null one is left out, and
|
||||
/// a whole number past the last artboard is `no such artboard`.
|
||||
#[test]
|
||||
fn artboard_align_names_the_key_of_a_malformed_reference() {
|
||||
let mut s = session();
|
||||
s.execute("artboard.new", &json!({})).unwrap();
|
||||
s.execute("artboard.new", &json!({})).unwrap();
|
||||
let cmds = [
|
||||
("artboard.align", json!({"indices": [0, 1, 2], "horizontal": "left", "to": "artboard"})),
|
||||
("artboard.distribute", json!({"indices": [0, 1, 2], "horizontal": "left", "to": "artboard"})),
|
||||
("artboard.distributeSpacing", json!({"indices": [0, 1, 2], "to": "artboard"})),
|
||||
];
|
||||
for (cmd, base) in &cmds {
|
||||
for key in ["artboard", "activeArtboard"] {
|
||||
let (was, n) = (boards(&s), s.doc().unwrap().history.undo.len());
|
||||
for value in [json!(-1), json!(1.5), json!("1"), json!(true)] {
|
||||
let mut p = base.clone();
|
||||
p[key] = value;
|
||||
let want = format!("invalid parameters for `{cmd}`: `{key}` must be a whole number");
|
||||
assert_eq!(s.execute(cmd, &p).unwrap_err().to_string(), want, "{cmd} {p}");
|
||||
}
|
||||
let mut p = base.clone();
|
||||
p[key] = json!(9);
|
||||
assert_eq!(s.execute(cmd, &p).unwrap_err().to_string(), "no such artboard", "{cmd} {p}");
|
||||
assert_eq!((boards(&s), s.doc().unwrap().history.undo.len()), (was, n), "{cmd} {key}: nothing moved");
|
||||
p[key] = json!(null);
|
||||
s.execute(cmd, &p).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Artboard Options, Move Artboard, Duplicate Artboards, Copy Artboard and Cut Artboard refuse an
|
||||
/// option given as text or as another value that is not true or false, such as "true", and
|
||||
/// Artboard Options and Move Artboard a position, size or move given as text, such as "10". The
|
||||
|
||||
+14
-5
@@ -667,7 +667,7 @@ sampler.
|
||||
| `transform` | `{ids?, dx?, dy?, rotate?, scale?, scaleX?, scaleY?, reflect?, shear?, origin?, copy?}` | Runs move, rotate, scale, reflect, shear in that order. `rotate` is in degrees counter-clockwise; a positive `shear` moves the top to the right. With `copy`, the first step duplicates. |
|
||||
| `create_graph` | `{type?, x, y, width, height, csv? \| series?, categories?, rows?}` | The nine Illustrator graph types: column, stacked column, bar, stacked bar, line, area, scatter, pie and radar. An empty CSV cell or a `null` in `rows` is a blank value; a number in straight quotes is a label. Edit later with `graph.setData` / `graph.setType` via `run_command`. `graph.column {design, columnType: vertical\|uniform\|repeating\|sliding, each, fractions: chop\|scale}` draws the columns or bars of the series picked with Group Selection (or `seriesIndexes`, or all of them) with a graph design saved by `graph.design`; with no `design` it returns the series' current design and options. Type in the design that holds a `%` followed by two digits shows each column's value, with at least the first digit's places before the decimal point and the second digit's places after it. |
|
||||
| `text_wrap` | `{ids?, offset?, invert?, release?}` | Area type below the objects (same layer) flows around them. |
|
||||
| `undo` / `redo` | `{}` | Many calls that change nothing record no undo step, for example `object.align` with nothing to move, `object.move` by 0 without `copy` and `object.showAll` with nothing hidden. `undo` (`edit.undo`) after such a call undoes the step before it. `inspect_document` lists the undo steps as `history`. `run_command` with `edit.clearHistory` removes every undo and redo step of the active document and returns `{cleared}`, the number of steps removed; the document stays as it is, and the command records no undo step. `run_command` with `edit.newDocumentFromState` and `{index?, created?}` makes a new document holding the art as it was at that state and makes it the active document, and returns `{index, title}`. `index` is a row of the History panel, with 0 the oldest state kept, and an omitted `index` uses the current state. An `index` that names no state is an error and changes nothing. `created` is File Info's created date in Unix seconds or null, and defaults to now. The new document takes the next `Untitled-N` name, is marked unsaved, has nothing selected and has no steps to undo or redo. It is out of symbol, opacity-mask and pattern editing, and the art drawn in a pattern's tile is stored in the pattern, as `object.pattern.done` stores it. The source document and its steps stay as they are, except that typing or a drag in progress is first kept as a step. When a command keeps a drag whose preview Real-time Drawing off had not applied yet (`edit.undo`, `edit.redo`, `edit.clearHistory`, `edit.newDocumentFromState` and the other commands that end a drag first), and applying that preview gives a warning, the command's result carries it as `previewWarning`, and as `warning` too when the command gives no warning of its own; the app's status bar shows `warning`. While the Type tool edits type, `undo` and `redo` put its caret, with no characters selected, at the end of the text the step changed: after the text it puts back or retypes, or where the text it takes out was. A step that changes no characters, such as a formatting change, leaves the caret and the selection where they are. |
|
||||
| `undo` / `redo` | `{}` | Many calls that change nothing record no undo step, for example `object.align` with nothing to move, `object.move` by 0 without `copy` and `object.showAll` with nothing hidden. `undo` (`edit.undo`) after such a call undoes the step before it. `inspect_document` lists the undo steps as `history`. `run_command` with `edit.clearHistory` removes every undo and redo step of the active document and returns `{cleared}`, the number of steps removed; the document stays as it is, and the command records no undo step. `run_command` with `edit.newDocumentFromState` and `{index?, created?}` makes a new document holding the art as it was at that state and makes it the active document, and returns `{index, title}`. `index` is a row of the History panel, with 0 the oldest state kept, and an omitted or null `index` uses the current state. An `index` that names no state fails with `no history state N`, one that is not a whole number with `` `index` must be a whole number ``, and either changes nothing. `created` is File Info's created date in Unix seconds or null, and defaults to now. The new document takes the next `Untitled-N` name, is marked unsaved, has nothing selected and has no steps to undo or redo. It is out of symbol, opacity-mask and pattern editing, and the art drawn in a pattern's tile is stored in the pattern, as `object.pattern.done` stores it. The source document and its steps stay as they are, except that typing or a drag in progress is first kept as a step. When a command keeps a drag whose preview Real-time Drawing off had not applied yet (`edit.undo`, `edit.redo`, `edit.clearHistory`, `edit.newDocumentFromState` and the other commands that end a drag first), and applying that preview gives a warning, the command's result carries it as `previewWarning`, and as `warning` too when the command gives no warning of its own; the app's status bar shows `warning`. While the Type tool edits type, `undo` and `redo` put its caret, with no characters selected, at the end of the text the step changed: after the text it puts back or retypes, or where the text it takes out was. A step that changes no characters, such as a formatting change, leaves the caret and the selection where they are. |
|
||||
|
||||
For example, create editable Japanese area type with the engine's vertical writing mode:
|
||||
|
||||
@@ -6799,7 +6799,10 @@ copies in artboard order); `artboard` wins when both are given. Without either,
|
||||
several artboards selected, they paste a copy on each of those, and the journal records them as `artboards`; a
|
||||
recorded action leaves both out. Art copied from no artboard keeps its place on the first artboard named and its
|
||||
offset to that one on the others. An index the document does not have, a repeated index, a value that is not an index
|
||||
or an empty list fails, and nothing is pasted. Copied artboards come back once, where they were, whichever artboards
|
||||
or an empty list fails, and nothing is pasted. An `artboard` that is not a whole number, null included, fails with
|
||||
`` `artboard` must be a whole number ``, and an `artboards` entry that is not one with
|
||||
`` `artboards` must contain whole numbers ``; both keys are checked when both are given. Copied artboards come back
|
||||
once, where they were, whichever artboards
|
||||
are named. `layer.pasteRemembersLayers {on?}` (a document option, `document.inspect` → `pasteRemembersLayers`)
|
||||
pastes objects back into the layers and sublayers they came from: in the same document while those are still there,
|
||||
else by the names of the layer and the layers around it (the topmost chain that matches the most names, a shown and
|
||||
@@ -6842,9 +6845,15 @@ also move the pattern tiles of the copies when `patterns` is true (default: pref
|
||||
`artboard.duplicate`, `artboard.copy`, `artboard.cut`, `artboard.move` and `artboard.rearrange` take `lockedAndHidden?`
|
||||
(default: prefs `moveLockedWithArtboard`), and with the Artboard tool chosen `edit.copy` and `edit.cut` pass it on. When
|
||||
art goes along, the journal records the value, so a replay under another preference takes the same art.
|
||||
Every artboard command that takes an `index` fails with `no such artboard` when the `index` is not the
|
||||
0-based index of an artboard (past the last one, negative, fractional, a string or null), and the document and the
|
||||
clipboard stay as they were.
|
||||
Every artboard command that takes an `index` fails with `no such artboard` when the `index` is a whole number past
|
||||
the last artboard, with `` `index` must be a whole number `` when it is negative, fractional, a string or another value
|
||||
that is not a whole number, and with `missing index` when it is null, also in a command that picks an artboard when
|
||||
`index` is left out. An `indices` (or a replayed `selectedArtboards`) that is not a list fails with
|
||||
`` `indices` must be a list of artboard indices ``, an empty one with
|
||||
`` `indices` must be a non-empty list of artboard indices ``, one with an entry that is not a whole number with
|
||||
`` `indices` must contain whole numbers ``, each naming its key, and one with a whole number past the last artboard
|
||||
with `no such artboard`. `artboard.align`, `artboard.distribute` and `artboard.distributeSpacing` name `artboard` or
|
||||
`activeArtboard` the same way. The document and the clipboard stay as they were.
|
||||
These artboard commands refuse a string, a number or another value that is not true or false with
|
||||
`` `art` must be true or false ``, naming the key, and leave the document, its undo history and the clipboard as they
|
||||
were: `moveArt`, `scaleArt`, `strokes`, `corners`, `patterns`, `lockedAndHidden` and an artboard's `locked`,
|
||||
|
||||
@@ -1144,6 +1144,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
`path.reshapeSegment` refuses a delta of 1e9 pt or longer, as `object.blend.spine.reshapeSegment` does, and `path.convertAnchor` and `path.freehand` refuse a point whose x or y is 1e9 pt or more in size.
|
||||
`path.reshapeSegment`, `object.blend.spine.reshapeSegment`, `path.convertAnchor`, `path.curvatureEdit`, `path.insertAnchor`, `path.moveAnchors`, `path.reshape`, `path.close` and the Pencil, Curvature, Knife, Eraser, Path Eraser, Smooth, Join and Blob Brush commands refuse a number, point or handle given as text, such as "10", and leave the document unchanged; `path.convertAnchor` refuses an `x` without a `y`, or a `y` without an `x`.
|
||||
`path.setHandle`, `path.moveAnchor`, `text.setPathHandle` (`anchor`), `path.insertAnchor` (`segment`), `artboard.reorder`, `library.moveGroup`, `graphicStyle.move` (`to`), `path.appendAnchor`, `path.close`, `path.reverse` (`subpath`) and the ruler origin commands (`artboard`) refuse a value that is not a whole number of 0 or more, such as `"3"` or `2.5`, with `` `anchor` must be a whole number ``, naming the key, instead of reporting it as missing or reading it as the default subpath or artboard; a null `artboard` is artboard 0 (`tests::params::a_malformed_index_is_not_missing_or_its_default`).
|
||||
The artboard commands that take `index`, `indices` or a replayed `selectedArtboards`, the artboard Align and Distribute commands (`artboard`, `activeArtboard`), `edit.newDocumentFromState` (`index`) and Paste in Place, in Front and in Back (`artboard`, `artboards`) report a value that is not a whole number as such, naming the key, and keep `no such artboard`, `no history state N` and the paste range messages for a whole number that names nothing; a null artboard `index` is `missing index`, and an `indices` value that is not a list, an empty list and a malformed entry each have their own message (`tests::panelcmds::artboard_lists_name_the_fault_and_the_key`).
|
||||
`path.close` and `path.setHandle` (`independent`), `path.moveAnchors` (`patterns`), the Pencil (`closed`, `keepSelected`, `fill`), Curvature (`closed` and a point's `corner`), `path.curvatureEdit` (`corner`), `path.reshapeSegment` (`perpendicular`) and the Blob Brush (`keepSelected`, `mergeOnlySelection`, `merge`) refuse a value other than true, false or null, such as "true", and leave the document unchanged.
|
||||
Direct Selection's marquee selects the frame anchors of area type that lie inside it, and a click on a frame anchor selects that one; Shift toggles them. The selected frame anchors show filled. A drag of one of them moves all of them, the arrow keys nudge them, and the type reflows at its size (`text.reshapeArea`, which takes `items` to move the anchors of several frames in one step), as [Create text](http://web.archive.org/web/20220127090642/https://helpx.adobe.com/illustrator/using/creating-text.html) and [Selecting objects](http://web.archive.org/web/20211230015528/https://helpx.adobe.com/illustrator/using/selecting-objects.html) describe. When the selection also holds other objects, path anchors or ruler guides, or when another change to the document or the selection follows the pick, the type is selected whole.
|
||||
A press on a selected object's centre point (Attributes panel › Show Center) selects the object whole, and a drag from it moves the object as one Move, held by that point (#4891; test `the_centre_point_selects_and_moves_the_object_whole`).
|
||||
|
||||
Reference in New Issue
Block a user