mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 21:17:48 +00:00
Merge pull request #7136 from dotCooCoo/fix/command-batch-artboards
Artboards: a batch's Fit to Artwork Bounds step fits every selected artboard
This commit is contained in:
@@ -78,8 +78,11 @@ pub(super) fn batch(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
/// `artboard`, as the Artboards panel shows it in the document the step runs in: past that
|
||||
/// document's last artboard, the last one; a Paste in Place, in Front or in Back step that names
|
||||
/// `artboards` keeps them, and one that names neither, while the Artboard tool is chosen with
|
||||
/// several artboards selected, pastes on those. A step that resizes the active artboard
|
||||
/// ([`crate::FIT_ACTIVE_ARTBOARD`]) and names no `index` gets that artboard as its `index`.
|
||||
/// several artboards selected, pastes on those. A Fit to Artwork Bounds step that names neither
|
||||
/// `index` nor `indices`, while the Artboard tool is chosen with several artboards selected when the
|
||||
/// step runs, gets those as its `indices`, which its journal entry records. Otherwise a step that
|
||||
/// resizes the active artboard ([`crate::FIT_ACTIVE_ARTBOARD`]) and names no `index` (nor
|
||||
/// `indices`, for Fit to Artwork Bounds) gets that artboard as its `index`.
|
||||
/// `grouped` gets each step's undo groups, as [`Session::batch_steps`] keeps them.
|
||||
fn run_steps(
|
||||
s: &mut Session,
|
||||
@@ -110,9 +113,22 @@ fn run_steps(
|
||||
// Artboard tool is chosen with several artboards selected, pastes on those.
|
||||
let names_artboards = id.starts_with("edit.pasteIn")
|
||||
&& (params.get("artboards").is_some() || (params.get("artboard").is_none() && !crate::cmd::edit::artboard_tool_picks(s).is_empty()));
|
||||
// A Fit to Artwork Bounds step that names neither `index` nor `indices`, while the Artboard
|
||||
// tool is chosen with several artboards selected, fits those, as the command does on its own.
|
||||
let fit_picks = if id == "artboard.fitToArt" && params.get("index").is_none() && params.get("indices").is_none() {
|
||||
crate::cmd::edit::artboard_tool_picks(s)
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
if !fit_picks.is_empty()
|
||||
&& let Some(o) = params.as_object_mut()
|
||||
{
|
||||
o.insert("indices".into(), json!(fit_picks));
|
||||
}
|
||||
if let Some(a) = artboard
|
||||
&& let Some(key) = key
|
||||
&& params.get(key).is_none()
|
||||
&& !(id == "artboard.fitToArt" && params.get("indices").is_some())
|
||||
&& !names_artboards
|
||||
&& let Some(last) = s.active().and_then(|d| d.doc.artboards.len().checked_sub(1))
|
||||
&& let Some(o) = params.as_object_mut()
|
||||
@@ -129,8 +145,9 @@ fn run_steps(
|
||||
}
|
||||
grouped.push(step_groups(s, start, &held));
|
||||
// The step's journal entry records what it took from the preferences or the clock (a
|
||||
// save's date, a new document's), as a command of its own would.
|
||||
if noted != params
|
||||
// save's date, a new document's), as a command of its own would, and the artboards a Fit to
|
||||
// Artwork Bounds step took from the Artboard tool.
|
||||
if (noted != params || !fit_picks.is_empty())
|
||||
&& let Some(c) = c.as_object_mut()
|
||||
{
|
||||
c.insert("params".into(), noted);
|
||||
|
||||
@@ -108,7 +108,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Batch",
|
||||
[],
|
||||
None,
|
||||
"{label?, commands: [{command, params}], artboard?: index (the active artboard; the app passes it: each edit.pasteInPlace, edit.pasteInFront, edit.pasteInBack, object.align, object.distribute, object.distributeSpacing, select.allOnArtboard or symbol.edit step that names none gets it, and each artboard.fitToArt or artboard.fitToSelection step that names no index gets it as index, or the last artboard of a document with fewer)} run several commands as ONE undo step (one in each document they edit: steps may open, switch, close or revert documents); stops at the first error and rolls everything back, documents included",
|
||||
"{label?, commands: [{command, params}], artboard?: index (the active artboard; the app passes it: each edit.pasteInPlace, edit.pasteInFront, edit.pasteInBack, object.align, object.distribute, object.distributeSpacing, select.allOnArtboard or symbol.edit step that names none gets it, and each artboard.fitToArt or artboard.fitToSelection step that names no index gets it as index, or the last artboard of a document with fewer; an artboard.fitToArt step that names neither index nor indices, while the Artboard tool is chosen with several artboards selected when the step runs, gets those as indices instead, and the journal records them)} run several commands as ONE undo step (one in each document they edit: steps may open, switch, close or revert documents); stops at the first error and rolls everything back, documents included",
|
||||
has_doc,
|
||||
batch::batch
|
||||
),
|
||||
|
||||
@@ -19,7 +19,9 @@ pub const ON_ACTIVE_ARTBOARD: [&str; 8] = [
|
||||
/// The commands that resize the artboards the app passes (Object › Artboards): the app passes the
|
||||
/// selected artboards to Fit to Artwork Bounds as its `indices`, and the active artboard to Fit to
|
||||
/// Selected Art as its `index`. A `command.batch` given `artboard` passes it on, as `index`, to
|
||||
/// each of their steps that names none.
|
||||
/// each of their steps that names none; a Fit to Artwork Bounds step that names neither `index` nor
|
||||
/// `indices`, while the Artboard tool is chosen with several artboards selected, gets those as its
|
||||
/// `indices` instead.
|
||||
pub const FIT_ACTIVE_ARTBOARD: [&str; 2] = ["artboard.fitToArt", "artboard.fitToSelection"];
|
||||
|
||||
/// `(command, param)` pairs the journal records only so a replay reproduces the original run
|
||||
|
||||
@@ -192,6 +192,87 @@ fn fit_steps_take_the_batchs_artboard_and_actions_leave_out_their_index() {
|
||||
assert_eq!(action, [fit("artboard.fitToArt"), fit("artboard.fitToArt"), fit("artboard.fitToSelection")]);
|
||||
}
|
||||
|
||||
/// A document of three artboards with art of its own on each → its artboards before any fit.
|
||||
fn three_artboards_with_art(s: &mut Session) -> Vec<vectorcraft_geom::Rect> {
|
||||
s.execute("file.new", &json!({"width": 100, "height": 100, "artboards": 3, "created": null})).unwrap();
|
||||
for (x, h) in [(10, 30), (130, 20), (250, 40)] {
|
||||
s.execute("shape.rectangle", &json!({"x": x, "y": 10, "width": 20, "height": h})).unwrap();
|
||||
}
|
||||
s.execute("select.all", &json!({})).unwrap();
|
||||
s.execute("paint.setStroke", &json!({"none": true})).unwrap();
|
||||
s.execute("select.none", &json!({})).unwrap();
|
||||
s.doc().unwrap().doc.artboards.iter().map(|a| a.rect).collect()
|
||||
}
|
||||
|
||||
/// The artboards of [`three_artboards_with_art`], each fitted to its art.
|
||||
fn three_fitted_artboards() -> [vectorcraft_geom::Rect; 3] {
|
||||
let rect = vectorcraft_geom::Rect::new;
|
||||
[rect(10.0, 10.0, 30.0, 40.0), rect(130.0, 10.0, 150.0, 30.0), rect(250.0, 10.0, 270.0, 50.0)]
|
||||
}
|
||||
|
||||
/// A Fit to Artwork Bounds step that names neither `index` nor `indices`, while the Artboard tool
|
||||
/// is chosen with several artboards selected, fits each of those (not the batch's artboard), as the
|
||||
/// command does on its own; a step's own `index` or `indices` wins, and with one artboard or none
|
||||
/// selected the step takes the batch's artboard.
|
||||
#[test]
|
||||
fn a_batch_fit_step_uses_the_artboard_tool_selection() {
|
||||
let fitted = three_fitted_artboards();
|
||||
let boards = |s: &Session| s.doc().unwrap().doc.artboards.iter().map(|a| a.rect).collect::<Vec<_>>();
|
||||
let fit = |params: Value| json!({"artboard": 1, "commands": [{"command": "artboard.fitToArt", "params": params}]});
|
||||
let session = |picks: &Value| {
|
||||
let mut s = Session::new();
|
||||
let was = three_artboards_with_art(&mut s);
|
||||
s.select_tool("artboard", crate::tooling::ViewInfo::default()).unwrap();
|
||||
s.set_tool_option("selected", picks);
|
||||
(s, was)
|
||||
};
|
||||
let (mut s, was) = session(&json!([2, 0]));
|
||||
s.execute("command.batch", &fit(json!({}))).unwrap();
|
||||
assert_eq!(boards(&s), [fitted[0], was[1], fitted[2]], "artboards 1 and 3, the selected ones, and not the batch's artboard 2");
|
||||
let (mut s, was) = session(&json!([2, 0]));
|
||||
s.execute("command.batch", &fit(json!({"index": 1}))).unwrap();
|
||||
assert_eq!(boards(&s), [was[0], fitted[1], was[2]], "a step's own index");
|
||||
let (mut s, was) = session(&json!([2, 0]));
|
||||
s.execute("command.batch", &fit(json!({"indices": [1, 2]}))).unwrap();
|
||||
assert_eq!(boards(&s), [was[0], fitted[1], fitted[2]], "a step's own indices");
|
||||
for picks in [json!([2]), json!([])] {
|
||||
let (mut s, was) = session(&picks);
|
||||
s.execute("command.batch", &fit(json!({}))).unwrap();
|
||||
assert_eq!(boards(&s), [was[0], fitted[1], was[2]], "the batch's artboard with {picks} selected");
|
||||
}
|
||||
}
|
||||
|
||||
/// The journal records the artboards a batch's Fit to Artwork Bounds step took from the Artboard
|
||||
/// tool, so a replay in a new session fits the same ones; an action leaves them out and fits the
|
||||
/// artboards selected when it plays.
|
||||
#[test]
|
||||
fn a_batch_fit_selection_replays_but_an_action_uses_the_current_selection() {
|
||||
let fitted = three_fitted_artboards();
|
||||
let boards = |s: &Session| s.doc().unwrap().doc.artboards.iter().map(|a| a.rect).collect::<Vec<_>>();
|
||||
let mut s = Session::new();
|
||||
let was = three_artboards_with_art(&mut s);
|
||||
s.select_tool("artboard", crate::tooling::ViewInfo::default()).unwrap();
|
||||
s.set_tool_option("selected", &json!([0, 2]));
|
||||
let start = s.journal.len();
|
||||
let steps = json!([{"command": "artboard.fitToArt", "params": {}}]);
|
||||
s.execute("command.batch", &json!({"artboard": 1, "commands": steps})).unwrap();
|
||||
assert_eq!(boards(&s), [fitted[0], was[1], fitted[2]]);
|
||||
let (id, p) = s.journal.last().unwrap().clone();
|
||||
assert_eq!((id.as_str(), &p["commands"][0]["params"]), ("command.batch", &json!({"indices": [0, 2]})));
|
||||
let r = replayed(&s);
|
||||
assert_ne!(r.tool_id(), "artboard", "the new session's tool selects no artboards");
|
||||
assert_eq!(boards(&r), boards(&s), "the replay fits the recorded artboards");
|
||||
let action = s.journal_for_action(start);
|
||||
assert_eq!(action, [("command.batch".to_string(), json!({"commands": steps}))]);
|
||||
// Played on a fresh copy of the document with other artboards selected.
|
||||
let mut a = Session::new();
|
||||
three_artboards_with_art(&mut a);
|
||||
a.select_tool("artboard", crate::tooling::ViewInfo::default()).unwrap();
|
||||
a.set_tool_option("selected", &json!([1, 2]));
|
||||
a.execute("command.batch", &json!({"artboard": 0, "commands": action[0].1["commands"]})).unwrap();
|
||||
assert_eq!(boards(&a), [was[0], fitted[1], fitted[2]], "the action fits the artboards selected when it plays");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_save_records_its_date_and_replays_it() {
|
||||
let path = vectorcraft_testkit::temp_dir("journal-save").join("a.vectorcraft").to_string_lossy().into_owned();
|
||||
|
||||
@@ -95,6 +95,28 @@ fn fit_to_artwork_bounds_fits_each_selected_artboard_to_its_own_art() {
|
||||
assert_eq!(boards(&app), [rect(10.0, 10.0, 30.0, 40.0), was[1], rect(250.0, 10.0, 270.0, 50.0)]);
|
||||
}
|
||||
|
||||
/// A batch the app runs (as the Actions panel plays an action) with a Fit to Artwork Bounds step
|
||||
/// fits artboards 1 and 3, selected with the Artboard tool, and leaves artboard 2, the active one,
|
||||
/// as it was.
|
||||
#[test]
|
||||
fn a_batchs_fit_to_artwork_bounds_step_fits_each_selected_artboard() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 100, "height": 100, "units": "Points", "artboards": 3})).unwrap();
|
||||
for (x, h) in [(10, 30), (130, 20), (250, 40)] {
|
||||
app.session.execute("shape.rectangle", &json!({"x": x, "y": 10, "width": 20, "height": h})).unwrap();
|
||||
}
|
||||
app.session.execute("select.all", &json!({})).unwrap();
|
||||
app.session.execute("paint.setStroke", &json!({"none": true})).unwrap();
|
||||
let boards = |app: &VectorcraftApp| app.session.active().unwrap().doc.artboards.iter().map(|a| a.rect).collect::<Vec<_>>();
|
||||
let was = boards(&app);
|
||||
app.select_tool("artboard");
|
||||
select(&mut app, 1);
|
||||
app.session.set_tool_option("selected", &json!([0, 2]));
|
||||
app.run("command.batch", json!({"commands": [{"command": "artboard.fitToArt", "params": {}}]})).unwrap();
|
||||
let rect = vectorcraft_geom::Rect::new;
|
||||
assert_eq!(boards(&app), [rect(10.0, 10.0, 30.0, 40.0), was[1], rect(250.0, 10.0, 270.0, 50.0)]);
|
||||
}
|
||||
|
||||
/// With the Artboard tool, Shift-click on a row selects the artboards from the last row clicked
|
||||
/// without Shift to it and makes it the active one, Ctrl/Cmd-click adds its artboard to the
|
||||
/// tool's selection or takes it out (#1139), a plain click selects it alone, and every selected
|
||||
|
||||
+6
-2
@@ -5447,8 +5447,12 @@ the selected artboards (those the Artboard tool has selected, else the active on
|
||||
`artboard.fitToSelection` calls it runs without `index`. Both include MCP's `run_command` calls in remote mode. The
|
||||
Artboards panel menu's Fit to Artwork Bounds also passes the selected artboards. A batch passes its `artboard` on to
|
||||
each of their steps that names no `index`, as the step's `index`. When the document a step runs in has no artboard at
|
||||
that index, the step gets that document's last artboard. An action recorded in the Actions panel leaves `index` and
|
||||
`indices` out of these steps, and they resize the artboard that is active when the action plays.
|
||||
that index, the step gets that document's last artboard. An `artboard.fitToArt` step that names neither `index` nor
|
||||
`indices`, while the Artboard tool is chosen with several artboards selected when the step runs, gets those artboards
|
||||
as its `indices` instead, and the batch's journal entry records them, so a replay of the journal fits the same
|
||||
artboards. A step's own `index` or `indices` is kept. An action recorded in the Actions panel leaves `index` and
|
||||
`indices` out of these steps: when the action plays, Fit to Artwork Bounds resizes the artboards the Artboard tool then
|
||||
has selected (two or more), else the active artboard, and Fit to Selected Art resizes the active artboard.
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"object.align","params":{"horizontal":"left","to":"artboard","artboard":1}}}
|
||||
|
||||
@@ -1160,8 +1160,9 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
With Align to Artboard chosen, the Align panel's Distribute Objects and Distribute Spacing buttons use the active artboard (`object.distribute` and `object.distributeSpacing` with `to: "artboard"` and `artboard`). The first object's left or top edge goes to the artboard's left or top edge, and the last object's right or bottom edge goes to the artboard's right or bottom edge. Distribute Objects spaces the other objects evenly by its button's line, and two objects are enough. Distribute Spacing makes the gaps between the objects equal. With anchor points selected, the buttons space the points among themselves and do not use the artboard. When an action plays, its Distribute Objects and Distribute Spacing steps with Align to Artboard use the artboard that is active then.
|
||||
With a key object, the Align panel's Distribute Objects buttons use the Distribute Spacing value (`object.distribute {spacing}`): the key object, or the compound path or selected group that contains it, stays in place, and the objects' left edges, centers or right edges (top edges, centers or bottom edges) end up that far apart, outward from the key object's, in their order. With the field's default value of 0, a click puts every object on the key object's line (behavior reference: [Ask Mordy: Distributing Objects](http://rwillustrator.blogspot.com/2008/01/ask-mordy-distribute.html)).
|
||||
Choosing the Artboard tool (Shift+O, its toolbar button, Edit Artboards in Properties or Document Setup, or the Artboards panel menu's Artboard Options…) starts it on the active artboard: the canvas outlines that artboard, and Delete, Cut, Copy, the Transform panel and Artboard Options act on it.
|
||||
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 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 Selected Art steps that name no artboard resize the artboard that is active then, and its Fit to Artwork Bounds steps resize the artboards selected 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.
|
||||
A Fit to Artwork Bounds step of a batch run from the app (`command.batch`), including an action played from the Actions panel, fits each artboard the Artboard tool has selected when the step runs, as the command does on its own and as the [help page](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/organize-manage-artboards/use-artboard-context-menu.html) describes. A step's own `index` or `indices` is kept, and the journal records the artboards the step fitted, so a replay fits the same ones. The step used to fit only the active artboard.
|
||||
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.
|
||||
With the Artboard tool selecting several artboards, Paste in Place, Paste in Front and Paste in Back put a copy of the copied art on each, where it was on the artboard it was copied from, in one undo step, 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 (`artboards` on the three commands). Art copied from no artboard keeps its place on the first selected artboard and its offset to it on the others, and an `artboard` or `artboards` index the document does not have is an error; these rules are VectorCraft's own. The three commands used to paste one copy, on the active artboard, and an `artboard` index the document did not have pasted the art where it was.
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user