mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 07:16:52 +00:00
Artboards: a Cut or Copy with the Artboard tool replays on its artboards
edit.copy and edit.cut chose the artboard route by the tool chosen, which the journal does not record. A replay in a new session acted on the objects selected there, or stopped with "nothing selected" when there were none. With the Artboard tool chosen, both now record selectedArtboards and art in the journal entry. A given selectedArtboards runs the artboard route with any tool and needs only a document. A recorded action leaves both values out, so it follows the tool chosen when it plays. Cut and Copy of objects record nothing new.
This commit is contained in:
@@ -139,7 +139,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Cut",
|
||||
["Edit"],
|
||||
Some("Cmd+X"),
|
||||
"{} (selected ruler guides go too; the Artboard tool chosen: artboard.cut)",
|
||||
"{selectedArtboards?: [n…], art?: bool} (as edit.copy) (selected ruler guides go too; the Artboard tool chosen, or selectedArtboards given: artboard.cut)",
|
||||
has_selection_guides_or_artboard_tool,
|
||||
cut
|
||||
),
|
||||
@@ -148,7 +148,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Copy",
|
||||
["Edit"],
|
||||
Some("Cmd+C"),
|
||||
"{} the selection with what it uses, and the selected ruler guides (pasted on the layer new art goes to, moved as the objects are; alone, where they were); the Artboard tool chosen: artboard.copy → {copied, guides}",
|
||||
"{selectedArtboards?: [n…] (the artboards the Artboard tool had selected, which the journal records for a replay; it runs artboard.copy with any tool chosen), art?: bool (default: the Artboard tool's moveArt option; the journal records it beside selectedArtboards)} the selection with what it uses, and the selected ruler guides (pasted on the layer new art goes to, moved as the objects are; alone, where they were); the Artboard tool chosen: artboard.copy → {copied, guides}",
|
||||
has_selection_guides_or_artboard_tool,
|
||||
copy
|
||||
),
|
||||
@@ -701,10 +701,27 @@ fn copy_cut_targets(s: &Session) -> Result<(bool, Vec<NodeId>)> {
|
||||
Ok((direct, roots))
|
||||
}
|
||||
|
||||
fn copy(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
/// Copy or Cut takes artboards: the Artboard tool is chosen, or the journal recorded the artboards
|
||||
/// it took then.
|
||||
fn takes_artboards(s: &Session, p: &Value) -> bool {
|
||||
s.tool_id() == "artboard" || p.get("selectedArtboards").is_some()
|
||||
}
|
||||
|
||||
/// Copy or Cut of artboards: `selectedArtboards` and `art` of `p`, or their defaults from the
|
||||
/// Artboard tool, which the journal entry records.
|
||||
fn copy_artboards(s: &mut Session, p: &Value, cut: bool) -> Result<Value> {
|
||||
let given: serde_json::Map<String, Value> =
|
||||
["selectedArtboards", "art"].into_iter().filter_map(|key| p.get(key).map(|v| (key.to_string(), v.clone()))).collect();
|
||||
let given = Value::Object(given);
|
||||
super::panelcmds::boards_and_art_noted(s, &given, if cut { "edit.cut" } else { "edit.copy" })?;
|
||||
super::panelcmds::artboard_copy(s, &given, cut)
|
||||
}
|
||||
|
||||
fn copy(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
switch_or(p, "art", false, "edit.copy")?;
|
||||
// The Artboard tool copies its artboard (and the art on it).
|
||||
if s.tool_id() == "artboard" {
|
||||
return super::panelcmds::artboard_copy(s, &json!({}), false);
|
||||
if takes_artboards(s, p) {
|
||||
return copy_artboards(s, p, false);
|
||||
}
|
||||
let (direct, targets) = copy_cut_targets(s)?;
|
||||
let st = s.doc()?;
|
||||
@@ -743,8 +760,9 @@ fn copy(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
}
|
||||
|
||||
fn cut(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if s.tool_id() == "artboard" {
|
||||
return super::panelcmds::artboard_copy(s, &json!({}), true);
|
||||
switch_or(p, "art", false, "edit.cut")?;
|
||||
if takes_artboards(s, p) {
|
||||
return copy_artboards(s, p, true);
|
||||
}
|
||||
// Ruler guides selected on their own: Copy takes them, Clear removes them.
|
||||
if s.doc()?.selection.is_empty() {
|
||||
|
||||
@@ -111,9 +111,8 @@ fn boards_and_art(s: &Session, p: &Value, cmd: &str) -> Result<(Vec<usize>, bool
|
||||
}
|
||||
|
||||
/// [`boards_and_art`], with both values added to the running command's journal entry, so a replay
|
||||
/// acts on the same artboards and art whatever the Artboard tool's state is then. `edit.cut` and
|
||||
/// `edit.copy` call [`artboard_copy`] without this and take no such params.
|
||||
fn boards_and_art_noted(s: &mut Session, p: &Value, cmd: &str) -> Result<(Vec<usize>, bool)> {
|
||||
/// acts on the same artboards and art whatever the Artboard tool's state is then.
|
||||
pub(crate) fn boards_and_art_noted(s: &mut Session, p: &Value, cmd: &str) -> Result<(Vec<usize>, bool)> {
|
||||
let (indices, art) = boards_and_art(s, p, cmd)?;
|
||||
note_artboards(s, p, &indices);
|
||||
s.note_journal("art", json!(art));
|
||||
|
||||
@@ -25,9 +25,10 @@ pub const FIT_ACTIVE_ARTBOARD: [&str; 2] = ["artboard.fitToArt", "artboard.fitTo
|
||||
/// `(command, param)` pairs the journal records only so a replay reproduces the original run
|
||||
/// (dates from the clock: `cmd::clock_date`; the artboards the app passes to the
|
||||
/// [`ON_ACTIVE_ARTBOARD`] and [`FIT_ACTIVE_ARTBOARD`] commands and to a batch; the artboards the
|
||||
/// Artboard tool had selected for its commands; the view's center the app passes to Edit Symbol);
|
||||
/// an action leaves them out ([`Session::journal_for_action`]).
|
||||
const REPLAY_ONLY: [(&str, &str); 21] = [
|
||||
/// Artboard tool had selected for its commands; the artboards and art Cut and Copy took with that
|
||||
/// tool chosen; the view's center the app passes to Edit Symbol); an action leaves them out
|
||||
/// ([`Session::journal_for_action`]).
|
||||
const REPLAY_ONLY: [(&str, &str); 25] = [
|
||||
("file.new", "created"),
|
||||
("document.save", "modified"),
|
||||
("file.saveAs", "modified"),
|
||||
@@ -45,6 +46,10 @@ const REPLAY_ONLY: [(&str, &str); 21] = [
|
||||
("artboard.cut", "selectedArtboards"),
|
||||
("artboard.rename", "selectedArtboards"),
|
||||
("artboard.switchOrientation", "selectedArtboards"),
|
||||
("edit.copy", "selectedArtboards"),
|
||||
("edit.copy", "art"),
|
||||
("edit.cut", "selectedArtboards"),
|
||||
("edit.cut", "art"),
|
||||
("select.allOnArtboard", "artboard"),
|
||||
("symbol.edit", "artboard"),
|
||||
("symbol.edit", "viewCenter"),
|
||||
|
||||
@@ -54,9 +54,13 @@ impl Session {
|
||||
self.check_given_ids(id, params)?;
|
||||
}
|
||||
// Clear's menu needs a selection; an agent supplying explicit targets only needs a
|
||||
// document. The command validates those targets before making any edit.
|
||||
// document. The command validates those targets before making any edit. Cut and Copy given
|
||||
// the artboards the journal recorded need only a document too, whatever is selected and
|
||||
// whichever tool is chosen.
|
||||
let given = given_ids(spec.params, params);
|
||||
let enabled = if id == "edit.clear" && params.get("ids").is_some() {
|
||||
let enabled = if (id == "edit.clear" && params.get("ids").is_some())
|
||||
|| (matches!(id, "edit.copy" | "edit.cut") && params.get("selectedArtboards").is_some())
|
||||
{
|
||||
cmd::has_doc(self)
|
||||
} else if let Some(ids) = given.clone() {
|
||||
// Objects given as `ids` (or `id`) stand in for the selection: the command is enabled
|
||||
|
||||
@@ -328,23 +328,134 @@ fn artboard_commands_read_the_recorded_selected_artboards() {
|
||||
}
|
||||
}
|
||||
|
||||
/// `edit.copy` and `edit.cut` run the Artboard tool's copy and take no artboards, so their entries
|
||||
/// stay as given.
|
||||
/// A Cut or Copy with the Artboard tool replays on the artboards it took in a new session, where
|
||||
/// another tool is chosen: with nothing selected there, and with an object selected that a Cut or
|
||||
/// Copy of objects would take. What it pastes is the same.
|
||||
#[test]
|
||||
fn edit_copy_and_cut_with_the_artboard_tool_record_their_params_as_given() {
|
||||
fn edit_copy_and_cut_with_the_artboard_tool_replay_on_its_artboards() {
|
||||
for id in ["edit.copy", "edit.cut"] {
|
||||
for select in ["select.all", "select.none"] {
|
||||
let mut s = Session::new();
|
||||
second_artboard_selected(&mut s);
|
||||
s.execute(select, &json!({})).unwrap();
|
||||
s.execute(id, &json!({})).unwrap();
|
||||
let boards = if id == "edit.cut" { 2 } else { 3 };
|
||||
assert_eq!(s.doc().unwrap().doc.artboards.len(), boards, "{id} after {select}");
|
||||
assert_eq!(documents(&replayed(&s)), documents(&s), "{id} after {select}");
|
||||
let entry = s.journal.last().unwrap().clone();
|
||||
s.execute("edit.paste", &json!({})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.artboards.len(), boards + 1, "{id} after {select}");
|
||||
let r = replayed(&s);
|
||||
assert_ne!(r.tool_id(), "artboard");
|
||||
assert_eq!(documents(&r), documents(&s), "{id} after {select}");
|
||||
assert_eq!(entry, (id.to_string(), json!({"selectedArtboards": [1], "art": true})), "{id} after {select}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A Copy after a Paste takes the pasted artboards, which the Artboard tool selected, and so does its
|
||||
/// replay, though `edit.paste` selects nothing in a new session's tool.
|
||||
#[test]
|
||||
fn edit_copy_after_a_paste_with_the_artboard_tool_replays_on_the_pasted_artboards() {
|
||||
let mut s = Session::new();
|
||||
second_artboard_selected(&mut s);
|
||||
s.execute("edit.copy", &json!({})).unwrap();
|
||||
s.execute("edit.paste", &json!({})).unwrap();
|
||||
s.execute("edit.copy", &json!({})).unwrap();
|
||||
assert_eq!(s.journal.last().unwrap().1, json!({"selectedArtboards": [3], "art": true}));
|
||||
s.execute("edit.paste", &json!({})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.artboards.len(), 5);
|
||||
assert_eq!(documents(&replayed(&s)), documents(&s));
|
||||
}
|
||||
|
||||
/// A recorded action keeps `edit.copy` and `edit.cut` as given, so when it plays they follow the tool
|
||||
/// chosen then, and a batch step records and leaves out the same values.
|
||||
#[test]
|
||||
fn edit_copy_and_cut_with_the_artboard_tool_record_nothing_for_an_action() {
|
||||
for id in ["edit.copy", "edit.cut"] {
|
||||
let mut s = Session::new();
|
||||
second_artboard_selected(&mut s);
|
||||
let start = second_artboard_selected(&mut s);
|
||||
s.execute(id, &json!({})).unwrap();
|
||||
let action = s.journal_for_action(start);
|
||||
assert_eq!(action, [(id.to_string(), json!({}))], "{id}");
|
||||
let mut played = Session::new();
|
||||
played.execute("file.new", &json!({"width": 400, "height": 200, "created": null})).unwrap();
|
||||
played.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 100, "height": 50})).unwrap();
|
||||
played.execute(id, &action[0].1).unwrap();
|
||||
assert_eq!(played.doc().unwrap().doc.artboards.len(), 1, "{id}");
|
||||
assert!(played.clipboard.artboards.is_empty(), "{id}");
|
||||
assert_eq!(played.clipboard.nodes.len(), 1, "{id}");
|
||||
|
||||
let mut s = Session::new();
|
||||
let start = second_artboard_selected(&mut s);
|
||||
let steps = json!([{"command": id, "params": {}}, {"command": "edit.paste", "params": {}}]);
|
||||
s.execute("command.batch", &json!({ "commands": steps })).unwrap();
|
||||
assert_eq!(s.journal.last().unwrap().1["commands"][0]["params"], json!({"selectedArtboards": [1], "art": true}), "{id}");
|
||||
assert_eq!(documents(&replayed(&s)), documents(&s), "{id}");
|
||||
assert_eq!(s.journal_for_action(start)[0].1["commands"][0]["params"], json!({}), "{id}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Without the Artboard tool, Cut and Copy of objects record nothing more than they did.
|
||||
#[test]
|
||||
fn edit_copy_and_cut_of_objects_record_their_params_as_given() {
|
||||
for id in ["edit.copy", "edit.cut"] {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 400, "height": 200, "created": null})).unwrap();
|
||||
s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 100, "height": 50})).unwrap();
|
||||
s.execute(id, &json!({})).unwrap();
|
||||
assert_eq!(s.journal.last().unwrap(), &(id.to_string(), json!({})), "{id}");
|
||||
assert!(s.clipboard.artboards.is_empty(), "{id}");
|
||||
assert_eq!(documents(&replayed(&s)), documents(&s), "{id}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A recorded `selectedArtboards` needs only a document and is checked as `indices` is, as are `art`
|
||||
/// and the rule that a cut leaves one artboard. A failed call changes neither the document nor the
|
||||
/// clipboard.
|
||||
#[test]
|
||||
fn edit_copy_and_cut_read_the_recorded_artboards() {
|
||||
for id in ["edit.copy", "edit.cut"] {
|
||||
assert!(matches!(Session::new().execute(id, &json!({"selectedArtboards": [0]})), Err(crate::EngineError::Disabled(..))));
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 400, "height": 200, "created": null})).unwrap();
|
||||
s.execute("artboard.new", &json!({"x": 1000, "y": 0, "width": 300, "height": 100})).unwrap();
|
||||
s.execute("shape.rectangle", &json!({"x": 1010, "y": 10, "width": 100, "height": 50})).unwrap();
|
||||
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"}),
|
||||
] {
|
||||
let err = s.execute(id, &bad).unwrap_err().to_string();
|
||||
assert!(err.contains(id) || err.contains("no such artboard"), "{id} {bad}: {err}");
|
||||
assert_eq!(documents(&s), before, "{id} {bad}");
|
||||
assert!(s.clipboard.artboards.is_empty(), "{id} {bad}");
|
||||
}
|
||||
s.execute(id, &json!({"selectedArtboards": [1], "art": false})).unwrap();
|
||||
assert_eq!(s.clipboard.artboards.len(), 1, "{id}");
|
||||
assert!(s.clipboard.nodes.is_empty(), "{id}");
|
||||
assert_eq!(s.journal.last().unwrap().1, json!({"selectedArtboards": [1], "art": false}), "{id}");
|
||||
// Objects selected, `art` is still checked.
|
||||
s.execute("select.all", &json!({})).unwrap();
|
||||
let err = s.execute(id, &json!({"art": "yes"})).unwrap_err().to_string();
|
||||
assert!(err.contains(id), "{id}: {err}");
|
||||
}
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 400, "height": 200, "created": null})).unwrap();
|
||||
assert!(s.execute("edit.cut", &json!({"selectedArtboards": [0]})).is_err());
|
||||
assert_eq!(s.doc().unwrap().doc.artboards.len(), 1);
|
||||
}
|
||||
|
||||
/// `artboard.duplicate` and the cut and copy record the tool's Move/Copy Artwork option: replayed in
|
||||
/// a new session, where the option is on, the art stays behind as it did.
|
||||
#[test]
|
||||
fn artboard_commands_record_the_tools_move_art_option() {
|
||||
for id in ["artboard.duplicate", "artboard.cut", "artboard.copy"] {
|
||||
for id in ["artboard.duplicate", "artboard.cut", "artboard.copy", "edit.cut", "edit.copy"] {
|
||||
let mut s = Session::new();
|
||||
second_artboard_selected(&mut s);
|
||||
s.set_tool_option("moveArt", &json!(false));
|
||||
|
||||
@@ -4024,6 +4024,13 @@ When a call to `artboard.duplicate`, `artboard.copy`, `artboard.cut`, `artboard.
|
||||
names no artboard, its journal entry records the artboards it acted on as `selectedArtboards`. A replay of the journal
|
||||
acts on those artboards, and a recorded action leaves the value out and acts on the selection when it plays.
|
||||
|
||||
`edit.copy` and `edit.cut` with the Artboard tool chosen record the same `selectedArtboards`, and `art` beside it.
|
||||
`edit.copy {selectedArtboards: [n…], art?}` and `edit.cut {selectedArtboards: [n…], art?}` run `artboard.copy` and
|
||||
`artboard.cut` on those artboards whichever tool is chosen and whatever is selected, so a replay of the journal in a new
|
||||
session takes the same artboards. A value of `selectedArtboards` that names no artboard fails as `indices` does, and `art`
|
||||
that is not true or false fails. A recorded action leaves both values out: when it plays, `edit.copy` and `edit.cut` take
|
||||
artboards when the Artboard tool is chosen then, and the selection otherwise.
|
||||
|
||||
`artboard.selectAll {}` chooses the Artboard tool when another tool is in use and selects every artboard. The active
|
||||
artboard stays active, the art's selection stays as it is, and no undo step is recorded → `{selected: [index…]}`, the
|
||||
active artboard first.
|
||||
|
||||
@@ -925,6 +925,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
Object › Artboards › Fit to Selected Art resizes the active artboard (`artboard.fitToSelection {index}`). It used to resize artboard 1 whichever artboard was active. When an action plays, its Fit to Artwork Bounds and Fit to Selected Art steps that name no artboard resize the artboard that is active then.
|
||||
Object › Artboards › Fit to Artwork Bounds and the Artboards panel menu's Fit to Artwork Bounds fit each selected artboard (those the Artboard tool has selected, else the active one) to the art on it, as the public [Use the artboard context menu](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/organize-manage-artboards/use-artboard-context-menu.html) page describes (`artboard.fitToArt {indices}`). An object partly on an artboard counts for each artboard it overlaps, and an artboard with no art on it keeps its size and place. They used to fit the active artboard to all the art in the document.
|
||||
The Artboards panel menu's Duplicate Artboards copies each selected artboard (those the Artboard tool has selected, else the active one) with its art, in one undo step, and the copies are then selected, as the public [Use the artboard context menu](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/organize-manage-artboards/use-artboard-context-menu.html) page describes. With the Artboard tool, Copy and Cut take every selected artboard with its art, as the public [Cut, copy, and paste artboards](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/organize-manage-artboards/cut-copy-and-paste-artboards.html) page describes, and Paste brings them all back in one undo step (`artboard.duplicate`, `artboard.copy` and `artboard.cut` with `indices`). The copies keep their places relative to each other, together right of the last artboard. All three used to act on the active artboard only.
|
||||
A replay of the journal repeats a Cut or Copy made with the Artboard tool on the artboards it took (`edit.cut` and `edit.copy` record `selectedArtboards` and `art`), whichever tool is chosen in the new session and whatever is selected there. A recorded action follows the tool chosen when it plays. The replay used to act on the objects selected then, or stop with nothing selected. This rule is VectorCraft's own.
|
||||
With the Artboard tool, the canvas context menu lists the entries of the public [Use the artboard context menu](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/organize-manage-artboards/use-artboard-context-menu.html) page in its order: Cut, Copy, Paste at the pointer, Undo, Redo, New Artboard, Duplicate Artboards, Rename (`artboard.rename`), Delete Artboards, Delete Empty Artboards, Select All Artboards (`artboard.selectAll`), Lock Artboards and Unlock Artboards, Rearrange, Fit to Artwork Bounds, Switch Orientation (`artboard.switchOrientation`), Artboard Options and Export. A right-click on an artboard that is not selected selects it first.
|
||||
The Artboards panel menu's New Artboard adds the artboard in one undo step and makes it the active one, as the panel's button, the Control bar and Properties do; with the Artboard tool it is then the only one selected. This is VectorCraft's own rule. The menu item used to leave the active artboard where it was.
|
||||
With the Artboard tool, Paste and Paste in Place make the first pasted artboard the active one (the navigator's and the Artboards panel's), and Cut makes the artboard before the first cut one active (the new first artboard when artboard 1 was cut). A command that chooses another document (`document.activate`) starts the tool on that document's active artboard, as choosing its tab does, and File › Revert keeps the active artboard and starts the tool on it. Paste used to leave the active artboard where it was while the tool had the pasted artboard active, and Cut left the active artboard on the artboard after the cut one while the tool had the one before it.
|
||||
|
||||
Reference in New Issue
Block a user