Artboards: Fit to Artwork Bounds and Fit to Selected Art resize the active artboard

Object › Artboards › Fit to Artwork Bounds and Fit to Selected Art ran
artboard.fitToArt and artboard.fitToSelection without an index, so
they resized artboard 1 whichever artboard was active. The Artboards
panel menu's Fit to Artwork Bounds already passed the active artboard.

The app now passes its active artboard as the index of the two
commands when a call names none, as it passes the artboard to Align to
Artboard, All on Active Artboard and Paste in Place, in Front and in
Back. That covers the menu items, keyboard shortcuts, the command
palette and MCP's run_command in remote mode. A batch passes its
artboard as the index to their steps that name none, and a recorded
action leaves the index out, so an action resizes the artboard that is
active when it plays. The params texts of the two commands and of
command.batch describe where the index comes from.

Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
dotCooCoo
2026-10-16 04:04:50 +07:00
committed by bflatastic
parent 867e72b085
commit b34b9dadcc
9 changed files with 98 additions and 10 deletions
+10 -4
View File
@@ -55,7 +55,8 @@ pub(super) fn batch(s: &mut Session, p: &Value) -> Result<Value> {
/// starts (one for all the steps in that document), so a step may open, switch or close documents.
/// A step that acts on the active artboard ([`crate::ON_ACTIVE_ARTBOARD`]) and names none gets
/// `artboard`, as the Artboards panel shows it in the document the step runs in: past that
/// document's last artboard, the last one.
/// document's last artboard, the last one. A step that resizes the active artboard
/// ([`crate::FIT_ACTIVE_ARTBOARD`]) and names no `index` gets that artboard as its `index`.
fn run_steps(s: &mut Session, label: &str, artboard: Option<u64>, cmds: &mut [Value]) -> Result<Vec<Value>> {
let mut results = Vec::with_capacity(cmds.len());
for c in cmds {
@@ -67,13 +68,18 @@ fn run_steps(s: &mut Session, label: &str, artboard: Option<u64>, cmds: &mut [Va
s.begin_interaction(label)?;
}
let mut params = c.get("params").cloned().unwrap_or(json!({}));
let key = if crate::ON_ACTIVE_ARTBOARD.contains(&id.as_str()) {
Some("artboard")
} else {
crate::FIT_ACTIVE_ARTBOARD.contains(&id.as_str()).then_some("index")
};
if let Some(a) = artboard
&& crate::ON_ACTIVE_ARTBOARD.contains(&id.as_str())
&& params.get("artboard").is_none()
&& let Some(key) = key
&& params.get(key).is_none()
&& let Some(last) = s.active().and_then(|d| d.doc.artboards.len().checked_sub(1))
&& let Some(o) = params.as_object_mut()
{
o.insert("artboard".into(), json!(usize::try_from(a).map_or(last, |a| a.min(last))));
o.insert(key.into(), json!(usize::try_from(a).map_or(last, |a| a.min(last))));
}
let (v, noted) =
s.execute_step(&id, &params).map_err(|e| EngineError::Other(format!("batch step {} (`{id}`) failed: {e}", results.len())))?;
+1 -1
View File
@@ -142,7 +142,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 or select.allOnArtboard step that names none gets it, 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 or select.allOnArtboard 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",
has_doc,
batch::batch
),
+10 -2
View File
@@ -152,11 +152,19 @@ pub fn specs() -> Vec<CommandSpec> {
"Fit to Artwork Bounds",
["Object", "Artboards"],
None,
"{index?} the artboard fits the art that shows (with its strokes and effects; hidden objects and layers, guides and template layers don't count)",
"{index?: n (0-based; the app passes the active artboard; default: the first)} the artboard fits the art that shows (with its strokes and effects; hidden objects and layers, guides and template layers don't count)",
has_doc,
artboard_fit_art
),
cmd!("artboard.fitToSelection", "Fit to Selected Art", ["Object", "Artboards"], None, "{index?}", has_selection, artboard_fit_sel),
cmd!(
"artboard.fitToSelection",
"Fit to Selected Art",
["Object", "Artboards"],
None,
"{index?: n (0-based; the app passes the active artboard; default: the first)}",
has_selection,
artboard_fit_sel
),
cmd!(
"layer.clippingMask.toggle",
"Make/Release Clipping Mask",
+10 -3
View File
@@ -563,11 +563,16 @@ pub const ON_ACTIVE_ARTBOARD: [&str; 7] = [
"select.allOnArtboard",
];
/// The commands that resize the active artboard, which the app passes as their `index`: Fit to
/// Artwork Bounds and Fit to Selected Art (Object › Artboards). A `command.batch` given `artboard`
/// passes it on, as `index`, to each of their steps that names none.
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
/// (dates from the clock: `cmd::clock_date`; the active artboard the app passes to the
/// [`ON_ACTIVE_ARTBOARD`] commands and to a batch); an action leaves them out
/// ([`Session::journal_for_action`]).
const REPLAY_ONLY: [(&str, &str); 11] = [
/// [`ON_ACTIVE_ARTBOARD`] and [`FIT_ACTIVE_ARTBOARD`] commands and to a batch); an action leaves
/// them out ([`Session::journal_for_action`]).
const REPLAY_ONLY: [(&str, &str); 13] = [
("file.new", "created"),
("document.save", "modified"),
("file.saveAs", "modified"),
@@ -577,6 +582,8 @@ const REPLAY_ONLY: [(&str, &str); 11] = [
("object.align", "artboard"),
("object.distribute", "artboard"),
("object.distributeSpacing", "artboard"),
("artboard.fitToArt", "index"),
("artboard.fitToSelection", "index"),
("select.allOnArtboard", "artboard"),
("command.batch", "artboard"),
];
+27
View File
@@ -128,6 +128,33 @@ fn a_batch_passes_its_artboard_to_each_step_when_it_runs() {
assert_eq!(s.doc().unwrap().doc.artboards.len(), 1, "in the new document");
}
/// A batch passes its `artboard` to each Fit to Artwork Bounds and Fit to Selected Art step that
/// names no `index`, as their `index` (past the document's last artboard, the last one), and a
/// step's own `index` is kept. A recorded action leaves out the `index` the app passes them, so
/// the action resizes the artboard active when it plays.
#[test]
fn fit_steps_take_the_batchs_artboard_and_actions_leave_out_their_index() {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 200, "height": 100, "artboards": 3})).unwrap();
s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 20, "height": 30})).unwrap();
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, art) = (boards(&s), vectorcraft_geom::Rect::new(10.0, 10.0, 30.0, 40.0));
let step = |command: &str, params: Value| json!({"command": command, "params": params});
s.execute("command.batch", &json!({"artboard": 9, "commands": [step("artboard.fitToArt", json!({}))]})).unwrap();
assert_eq!(boards(&s), [was[0], was[1], art], "artboard 3, the last one, for a batch's artboard past it");
s.execute("command.batch", &json!({"artboard": 1, "commands": [step("artboard.fitToSelection", json!({}))]})).unwrap();
assert_eq!(boards(&s), [was[0], art, art], "the batch's artboard 2");
s.execute("command.batch", &json!({"artboard": 2, "commands": [step("artboard.fitToArt", json!({"index": 0}))]})).unwrap();
assert_eq!(boards(&s)[0], art, "an index the step names is kept");
let start = s.journal.len();
s.execute("artboard.fitToArt", &json!({"index": 1})).unwrap();
s.execute("artboard.fitToSelection", &json!({"index": 1})).unwrap();
assert!(s.journal.since(start).iter().all(|(_, p)| p["index"] == json!(1)), "the journal keeps it: {:?}", s.journal.since(start));
let action = s.journal_for_action(start);
assert_eq!(action, [("artboard.fitToArt".to_string(), json!({})), ("artboard.fitToSelection".to_string(), json!({}))]);
}
#[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();
+10
View File
@@ -686,6 +686,16 @@ impl VectorcraftApp {
{
p.insert("artboard".into(), serde_json::json!(panels::artboards::selected(self, n)));
}
// Object › Artboards › Fit to Artwork Bounds and Fit to Selected Art resize the active
// artboard, which the app passes as their `index`.
if vectorcraft_engine::FIT_ACTIVE_ARTBOARD.contains(&id)
&& params.get("index").is_none()
&& self.view().is_some()
&& let Some(n) = self.session.active().map(|d| d.doc.artboards.len())
&& let Some(p) = params.as_object_mut()
{
p.insert("index".into(), serde_json::json!(panels::artboards::selected(self, n)));
}
if id.starts_with("edit.paste") {
if let Err(e) = self.adopt_system_clipboard() {
self.ui.status = e.clone();
+21
View File
@@ -312,6 +312,27 @@ mod tests {
assert_eq!(picked(&mut app, json!({})), json!([a]));
assert_eq!(picked(&mut app, json!({"artboard": 1})), json!([b]));
}
/// Object › Artboards › Fit to Artwork Bounds and Fit to Selected Art resize the artboard made
/// active here, as the panel menu's Fit to Artwork Bounds does, and leave artboard 1 as it was;
/// an explicit index still wins.
#[test]
fn fit_to_artwork_bounds_and_selected_art_resize_the_active_artboard() {
for id in ["artboard.fitToArt", "artboard.fitToSelection"] {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.session.execute("file.new", &json!({"width": 100, "height": 100, "units": "Points", "artboards": 2})).unwrap();
app.session.execute("shape.rectangle", &json!({"x": 130, "y": 10, "width": 20, "height": 30})).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 (first, art) = (boards(&app)[0], vectorcraft_geom::Rect::new(130.0, 10.0, 150.0, 40.0));
select(&mut app, 1);
crate::menus::invoke(&mut app, id, json!({}));
assert_eq!(boards(&app), [first, art], "{id} resizes Artboard 2, the active one");
crate::menus::invoke(&mut app, id, json!({"index": 0}));
assert_eq!(boards(&app), [art, art], "{id} with an index resizes that artboard");
}
}
/// With the Artboard tool, Shift-click on a row adds its artboard to the tool's selection or
/// takes it out (#1139), and every selected row is highlighted.
#[test]
+8
View File
@@ -1774,6 +1774,14 @@ Direct Selection or Group Selection tool inside an artboard makes that artboard
tool, a Shift-click on an empty spot inside an artboard makes it active and keeps the selection. Choosing the Artboard
tool (`tool.select {tool: "artboard"}`) starts it on the active artboard: its `active` option is that artboard's index.
`artboard.fitToArt {index?}` (Object › Artboards › Fit to Artwork Bounds) and `artboard.fitToSelection {index?}`
(Object › Artboards › Fit to Selected Art) resize artboard `index` (a 0-based index), or the first artboard when the
call gives none. The desktop app adds its active artboard as `index` to the calls of these two commands that it runs
without `index`, MCP's `run_command` calls in remote mode included. 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` out of these
steps, and they resize the artboard that is active when the action plays.
```json
{"name":"run_command","arguments":{"command":"object.align","params":{"horizontal":"left","to":"artboard","artboard":1}}}
{"name":"run_command","arguments":{"command":"object.distribute","params":{"horizontal":"left","to":"artboard","artboard":1}}}
+1
View File
@@ -495,6 +495,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
Align to Artboard uses the active artboard, the one the Artboards panel highlights (`object.align {artboard}`). A press with a selection tool inside an artboard makes that artboard active; with the Selection tool, a Shift-click on an empty spot inside an artboard makes it active and keeps the selection. One selected object is aligned to the active artboard too, whatever Align To says (a lone object is never the key object). When an action plays, its Align to Artboard, All on Active Artboard, Paste in Place, Paste in Front and Paste in Back steps that name no artboard use the artboard that is active then.
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.
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 Artwork Bounds and Fit to Selected Art resize the active artboard, as the Artboards panel menu's Fit to Artwork Bounds does (`artboard.fitToArt {index}`, `artboard.fitToSelection {index}`). They 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.
The canvas draws the active artboard's edge in black, as the public [Select artboards](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/add-edit-artboards/select-artboards.html) page describes. It draws the other artboards' edges in gray. With several artboards selected by the Artboard tool, the canvas draws the black edge around the active one, which the tool shows with handles and its name. It used to draw artboard 1's edge in black whichever artboard was active.
View › Rotate View turns the view to the status bar's preset angles (`view.rotate {angle}`, which takes any angle).
When the view angle changes (a Rotate View drag, the status bar's angle menu, Reset Rotate View or a saved view) and the desktop app renders the frame at the new angle on the render worker, the canvas draws the last frame turned to the new angle until that frame is ready. It used to draw no art until then.