mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-27 19:27:21 -06:00
Align to Artboard aligned the selection to the artboard under its center, or to the first artboard when no artboard was under the center. A press with a selection tool inside an artboard makes that artboard the active one (#693). With the Selection tool, a Shift-click on an empty spot inside an artboard also makes it active and keeps the selection. Align to Artboard did not use the active artboard. object.align takes artboard, a 0-based artboard index, and uses it when its to param is "artboard". The app passes its active artboard, the one the Artboards panel highlights, to each object.align it runs that names none, as it does for Paste in Place, Paste in Front, Paste in Back (#693) and Select › All on Active Artboard (#1006). MCP calls in remote mode also get it. Without artboard (headless MCP and the CLI), or with an index the document doesn't have, object.align aligns to the artboard under the center of the selection, or to the first artboard when no artboard is under the center. command.batch takes artboard too, and the app passes it the active artboard. The batch passes it on to each step of these five commands that names none, when the step runs. When the document a step runs in has no artboard at that index (for example, after an earlier step deleted the active artboard when it was the last one), the step gets that document's last artboard. A step in a document that an earlier step opened, created or switched to gets the same index, not that document's own active artboard. With Align to Selection, one selected object aligned from the Align panel, the Control bar or the Properties panel is aligned to the active artboard too, unless it is the key object. Recorded actions leave out the artboard that the app passes to these commands and to the batch. The Actions panel plays an action as a batch. Its Align to Artboard, All on Active Artboard and in-place paste steps that name no artboard use the artboard that is active when it plays, and the default Center on Artboard action now centers on the active artboard. In-place paste steps recorded since #693 and before this change still paste onto the artboard that was active when they were recorded. Tests cover object.align with and without artboard on three artboards; a Shift-click that makes an artboard active, after which Align to Artboard aligns to that artboard but keeps an artboard the call names; a batch that passes its artboard to each step when the step runs, after a step deletes the last artboard and in a new document; recorded actions that leave the artboard out; and recorded Align to Artboard and Paste in Place steps and the default Center on Artboard action, which play on the artboard that is active when they play. Docs: docs/mcp.md (Align to Artboard), docs/target-app-parity.md.
103 lines
4.8 KiB
Rust
103 lines
4.8 KiB
Rust
//! `command.batch`: several commands as one undo step (in each document they edit).
|
|
|
|
use serde_json::{Value, json};
|
|
|
|
use super::super::*;
|
|
use crate::{DocState, EngineError};
|
|
|
|
pub(super) fn batch(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let mut cmds = p.get("commands").and_then(Value::as_array).ok_or_else(|| bad("command.batch", "missing commands"))?.clone();
|
|
let label = str_param(p, "label").unwrap_or("Batch").to_string();
|
|
// The active artboard the app passes, for the steps that act on it.
|
|
let artboard = p.get("artboard").and_then(Value::as_u64);
|
|
// What an error rolls back besides the documents: session-level state a step may change
|
|
// (paint defaults, drawing mode, clipboard, the Untitled-N count).
|
|
let saved = (s.paint.clone(), s.fill_active, s.draw_mode, s.draw_inside, s.clipboard.clone(), s.untitled_counter);
|
|
let (start, active): (Vec<u64>, _) = (s.docs.iter().map(|d| d.uid).collect(), s.active);
|
|
s.batch_stash = Some(vec![]);
|
|
let r = run_steps(s, &label, artboard, &mut cmds);
|
|
let stash = s.batch_stash.take().unwrap_or_default();
|
|
let results = match r {
|
|
Ok(results) => results,
|
|
Err(e) => {
|
|
roll_back(s, &start, stash);
|
|
s.active = active;
|
|
(s.paint, s.fill_active, s.draw_mode, s.draw_inside, s.clipboard, s.untitled_counter) = saved;
|
|
s.pending_paint = None;
|
|
return Err(e);
|
|
}
|
|
};
|
|
// Each document the steps edited keeps them as one undo step.
|
|
s.remember_pending_paint();
|
|
for st in &mut s.docs {
|
|
if let Some(it) = st.interaction.take() {
|
|
st.keep_interaction(it);
|
|
}
|
|
}
|
|
// The batch journals itself, whichever document it ends in.
|
|
let mut entry = p.clone();
|
|
if let Some(o) = entry.as_object_mut() {
|
|
o.insert("commands".into(), Value::Array(cmds));
|
|
}
|
|
s.journal.push(("command.batch".into(), entry));
|
|
Ok(json!({ "results": results }))
|
|
}
|
|
|
|
/// Run the steps → their results. Each runs inside an interaction of the document active when it
|
|
/// 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.
|
|
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 {
|
|
let id = c.get("command").and_then(Value::as_str).unwrap_or("").to_string();
|
|
if id == "command.batch" {
|
|
return Err(bad("command.batch", "batches cannot nest"));
|
|
}
|
|
if s.active().is_some() {
|
|
s.begin_interaction(label)?;
|
|
}
|
|
let mut params = c.get("params").cloned().unwrap_or(json!({}));
|
|
if let Some(a) = artboard
|
|
&& crate::ON_ACTIVE_ARTBOARD.contains(&id.as_str())
|
|
&& params.get("artboard").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))));
|
|
}
|
|
let (v, noted) =
|
|
s.execute_step(&id, ¶ms).map_err(|e| EngineError::Other(format!("batch step {} (`{id}`) failed: {e}", results.len())))?;
|
|
// 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
|
|
&& let Some(c) = c.as_object_mut()
|
|
{
|
|
c.insert("params".into(), noted);
|
|
}
|
|
results.push(v);
|
|
}
|
|
Ok(results)
|
|
}
|
|
|
|
/// The documents as they were before the batch (`start`: their uids in order): the ones it opened
|
|
/// close, the ones it closed or replaced (`stash`) come back, and each one's interaction is undone.
|
|
fn roll_back(s: &mut Session, start: &[u64], mut stash: Vec<DocState>) {
|
|
let mut open = std::mem::take(&mut s.docs);
|
|
for &uid in start {
|
|
// The newest revision the document reached, so the restored one redraws.
|
|
let newest = open.iter().chain(&stash).filter(|d| d.uid == uid).map(|d| d.revision).max().unwrap_or_default();
|
|
// Its state before the batch: the first one a step closed or replaced, else the open one.
|
|
let st = match stash.iter().position(|d| d.uid == uid) {
|
|
Some(i) => Some(stash.remove(i)),
|
|
None => open.iter().position(|d| d.uid == uid).map(|i| open.remove(i)),
|
|
};
|
|
if let Some(mut st) = st {
|
|
st.undo_interaction();
|
|
st.revision = newest.saturating_add(1);
|
|
s.docs.push(st);
|
|
}
|
|
}
|
|
}
|