mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 00:17:42 +00:00
The History panel gets a New Document from State button at its bottom,
which runs the new edit.newDocumentFromState command. The command makes
a new document holding the art of a History panel row ({index?}, the
current state when omitted) and makes it the active document. An index
that names no state is an error and changes nothing.
The new document takes the next Untitled-N name, is marked unsaved, has
nothing selected, no steps to undo or redo and no modified date, and
leaves symbol, opacity-mask and pattern editing; the art drawn in a
pattern's tile is stored in the pattern, as Done stores it. Its links point at the files the
source finds, including files found in the source's folder away from the
links' paths. Those files are looked for only within the folders the
asking caller may read; for a caller without limits, within the folders
the caller that opened the source may read. The source
document, its steps and its saved state stay as they are; typing or a
drag in progress is first kept as a step, as Undo keeps it. While an IME
composes, the command waits, as Undo does.
Docs: docs/mcp.md, docs/target-app-parity.md.
130 lines
5.3 KiB
Rust
130 lines
5.3 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); 22] = [
|
||
("file.new", "created"),
|
||
("edit.newDocumentFromState", "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())
|
||
}
|