mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 14:14:11 +00:00
169 lines
7.2 KiB
Rust
169 lines
7.2 KiB
Rust
use super::{NodeId, Value, cmd, find_command};
|
||
|
||
/// 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 artboards and art Cut and Copy took with that
|
||
/// tool chosen; the view's center the app passes to Edit Symbol); an action leaves them out
|
||
/// ([`Session::journal_for_action`]).
|
||
const REPLAY_ONLY: [(&str, &str); 26] = [
|
||
("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"),
|
||
("edit.copy", "selectedArtboards"),
|
||
("edit.copy", "art"),
|
||
("edit.cut", "selectedArtboards"),
|
||
("edit.cut", "art"),
|
||
("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);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Queries that apply the edit the active tool is computing before they run, as edits do
|
||
/// ([`Session::finish_tool_edit`]): a batch runs edits, Find Objects with `select`, Select All
|
||
/// Instances and Lock Guides change the selection, and the proof views, the preferences and added
|
||
/// fonts give the documents new revisions (they draw again, and type lays out again).
|
||
const WAIT_FOR_TOOL_EDIT: &[&str] = &[
|
||
"command.batch",
|
||
"document.find",
|
||
"symbol.selectInstances",
|
||
"view.guides.lock",
|
||
"view.proofSetup",
|
||
"view.proofColors",
|
||
"view.overprintPreview",
|
||
"view.separationsPreview",
|
||
"prefs.set",
|
||
"prefs.reset",
|
||
"text.addFontFiles",
|
||
];
|
||
|
||
/// Does command `id`, run on its own, first apply the edit the active tool is computing on
|
||
/// another thread and the input held for it ([`Session::finish_tool_edit`])? Every command that
|
||
/// can edit, select, save or give a document a new revision does; queries that only read do not
|
||
/// (`commands_that_do_not_wait_for_a_tool_edit_change_no_document` in the command sweep checks
|
||
/// them). A frontend that adds params from
|
||
/// its own state (the active artboard) applies them first itself.
|
||
pub fn waits_for_tool_edit(id: &str) -> bool {
|
||
find_command(id).is_some_and(|spec| spec.journal || WAIT_FOR_TOOL_EDIT.contains(&id))
|
||
}
|
||
|
||
/// Journaled commands that write a file and change nothing in the document: with an object of a
|
||
/// dynamic symbol instance's art selected, they read the instance as queries do
|
||
/// ([`crate::Session::execute`]).
|
||
pub(crate) const JOURNALED_READS: &[&str] = &["css.export"];
|
||
|
||
/// 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. Entries of `ids` that are
|
||
/// not ids are left out of this list; `Session::execute` refuses them before it reads it.
|
||
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(v) => v.as_array().map(|a| a.iter().filter_map(Value::as_u64).map(NodeId).collect()),
|
||
None => cmd::id_param(params, "id").filter(|_| cmd::params::id_stands_for_ids(description)).map(|id| vec![id]),
|
||
}
|
||
.filter(|ids| !ids.is_empty())
|
||
}
|