mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-07 23:00:35 +00:00
With the Artboard tool, the canvas context menu lists the entries of the public Use the artboard context menu page in its order, in place of the entries for objects and the view. Lock Artboards and Unlock Artboards are two submenus. An entry that cannot run is left out, and Undo and Redo stay dimmed. A right-click on an artboard that is not selected selects it alone first, and the menu measures the art for Delete Empty Artboards and Fit to Artwork Bounds once, at the right-click. Cut, Copy, Duplicate Artboards, Rename, Delete Artboards, Lock Selected, Unlock Selected, Fit to Artwork Bounds, Switch Orientation and Export act on the artboards the tool has selected, else the active one. docs/mcp.md lists what the other entries act on. - artboard.rename renames in one undo step and numbers several names. The menu's Rename opens a dialog that runs it, and the Artboards panel's name field runs it too. - artboard.switchOrientation swaps width and height and keeps the top-left corner, skipping square and locked artboards. - artboard.selectAll chooses the tool and selects every artboard. - edit.paste takes artboardsAt, and the menu's Paste centers copied artboards on the right-click's point. - The menu's Cut and Copy run artboard.cut and artboard.copy. In the desktop app those commands publish to the system clipboard, and the menu shows the Cut and Copy shortcuts for them. - artboard.fitToArt and the panel's name field record no undo step when nothing changes. artboard.duplicate, copy, cut, rename and switchOrientation take their artboards from the tool's selection when a call names none. Each now records them in the journal as selectedArtboards, which a replay reads and a recorded action leaves out. The copy commands also record art. switchOrientation records scaleArt, and it takes patterns and lockedAndHidden and records what its artboard.setProps step resolved for them. While the Artboard tool is in use, a command chosen from any menu or the command palette moves the tool's selection as the Artboards panel does: artboard.new makes the new artboard active, artboard.duplicate selects the copies, and artboard.delete and artboard.deleteEmpty make a remaining artboard active.
129 lines
5.2 KiB
Rust
129 lines
5.2 KiB
Rust
use super::{NodeId, Value, cmd};
|
||
|
||
/// The commands that act on the active artboard (#693), which the app passes as their `artboard`:
|
||
/// Paste in Place, in Front and in Back paste onto it, Align to Artboard aligns to it, Distribute
|
||
/// Objects and Distribute Spacing with Align to Artboard distribute objects across it, All on
|
||
/// Active Artboard selects on it, and Edit Symbol from the Symbols panel centers the symbol's art
|
||
/// on it. A `command.batch` given `artboard` passes it on to each of their steps that names none.
|
||
pub const ON_ACTIVE_ARTBOARD: [&str; 8] = [
|
||
"edit.pasteInPlace",
|
||
"edit.pasteInFront",
|
||
"edit.pasteInBack",
|
||
"object.align",
|
||
"object.distribute",
|
||
"object.distributeSpacing",
|
||
"select.allOnArtboard",
|
||
"symbol.edit",
|
||
];
|
||
|
||
/// 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.
|
||
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 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] = [
|
||
("file.new", "created"),
|
||
("document.save", "modified"),
|
||
("file.saveAs", "modified"),
|
||
("edit.pasteInPlace", "artboard"),
|
||
("edit.pasteInFront", "artboard"),
|
||
("edit.pasteInBack", "artboard"),
|
||
("object.align", "artboard"),
|
||
("object.distribute", "artboard"),
|
||
("object.distributeSpacing", "artboard"),
|
||
("artboard.fitToArt", "index"),
|
||
("artboard.fitToArt", "indices"),
|
||
("artboard.fitToSelection", "index"),
|
||
("artboard.duplicate", "selectedArtboards"),
|
||
("artboard.copy", "selectedArtboards"),
|
||
("artboard.cut", "selectedArtboards"),
|
||
("artboard.rename", "selectedArtboards"),
|
||
("artboard.switchOrientation", "selectedArtboards"),
|
||
("select.allOnArtboard", "artboard"),
|
||
("symbol.edit", "artboard"),
|
||
("symbol.edit", "viewCenter"),
|
||
("command.batch", "artboard"),
|
||
];
|
||
|
||
/// `p`, the params of command `id`, without its [`REPLAY_ONLY`] values (a batch: its steps').
|
||
pub(crate) fn strip_replay_only(id: &str, p: &mut Value) {
|
||
let strip = |id: &str, p: &mut Value| {
|
||
if let Value::Object(m) = p {
|
||
m.retain(|k, _| !REPLAY_ONLY.iter().any(|&(c, key)| c == id && key == k));
|
||
}
|
||
};
|
||
strip(id, p);
|
||
// Batches don't nest: one level of steps.
|
||
if id == "command.batch"
|
||
&& let Some(Value::Array(steps)) = p.get_mut("commands")
|
||
{
|
||
for step in steps {
|
||
let id = step.get("command").and_then(Value::as_str).unwrap_or_default().to_string();
|
||
if let Some(p) = step.get_mut("params") {
|
||
strip(&id, p);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Commands that act on the selection and take `ids` by selecting them first (`select.set`, with
|
||
/// its checks), then running as on the selection: the result and the selection afterwards are the
|
||
/// menu command's (#2130).
|
||
pub(crate) const SELECTS_GIVEN_IDS: &[&str] = &[
|
||
"edit.duplicate",
|
||
"object.arrange.bringForward",
|
||
"object.arrange.bringToFront",
|
||
"object.arrange.sendBackward",
|
||
"object.arrange.sendToBack",
|
||
"object.arrange.sendToCurrentLayer",
|
||
"object.clippingMask.make",
|
||
"object.clippingMask.release",
|
||
"object.compoundPath.make",
|
||
"object.compoundPath.release",
|
||
"object.compoundShape.expand",
|
||
"object.compoundShape.make",
|
||
"object.compoundShape.release",
|
||
"object.compoundShape.setMode",
|
||
"object.group",
|
||
"object.hide",
|
||
"object.lock",
|
||
"object.move",
|
||
"object.pathfinder.crop",
|
||
"object.pathfinder.divide",
|
||
"object.pathfinder.exclude",
|
||
"object.pathfinder.intersect",
|
||
"object.pathfinder.merge",
|
||
"object.pathfinder.minusBack",
|
||
"object.pathfinder.minusFront",
|
||
"object.pathfinder.outline",
|
||
"object.pathfinder.trap",
|
||
"object.pathfinder.trim",
|
||
"object.pathfinder.unite",
|
||
"object.reflect",
|
||
"object.rotate",
|
||
"object.scale",
|
||
"object.shear",
|
||
"object.ungroup",
|
||
];
|
||
|
||
/// The non-empty `ids` given to a command whose params description names `ids` as a key (the
|
||
/// key callers outside the app may pass it, [`cmd::params::takes_key`]), or its `id` as `ids` of
|
||
/// one when no `ids` are given and the description takes `id` so
|
||
/// ([`cmd::params::id_stands_for_ids`]), as [`cmd::targets`] reads them.
|
||
pub(crate) fn given_ids(description: &'static str, params: &Value) -> Option<Vec<NodeId>> {
|
||
if !cmd::params::takes_key(description, "ids") {
|
||
return None;
|
||
}
|
||
match params.get("ids").filter(|v| !v.is_null()) {
|
||
Some(_) => cmd::ids_param(params, "ids"),
|
||
None => cmd::id_param(params, "id").filter(|_| cmd::params::id_stands_for_ids(description)).map(|id| vec![id]),
|
||
}
|
||
.filter(|ids| !ids.is_empty())
|
||
}
|