Actions: recording, step exclusions and set edits keep sets that Load Actions reads

Load Actions refuses an action set file over 8 MiB, with more than 1,000
actions, or with an action of more than 10,000 steps. Several ways of
changing a set could pass those limits, leaving a set that Save Actions
refuses to write.

- While an action records, a command (a batch and Find Objects that
  selects included) whose step would give the action more than 10,000
  steps or its set's file more than 8 MiB does not run, and the recording
  goes on. A stop that playback reaches is left out then. A command whose
  step grows past a limit only as it runs keeps its edit; the status bar
  names the limit and Stop leaves the step out. Stop also leaves out any
  recorded step still past a limit. Steps recorded after an exclusion
  past an action's last step are included.
- An exclusion that would make the set's file larger than 8 MiB changes
  no checkmark; one that keeps the size or shrinks it always works.
- Begin Recording, New Action, Duplicate, renaming an action, Action
  Options, Set Options and New Set refuse a change that would give a set
  more than 1,000 actions or a file larger than 8 MiB. A change that
  leaves a set no further past a limit than it is now is allowed.
- Load Actions refuses a set, .aia sets included, that Save Actions would
  write larger than 8 MiB (a file with less white space, or a set name
  that gets a number added).

The limits are the existing ones from files.rs, and the errors reuse the
existing messages. docs/mcp.md and the commands' params text describe the
new errors.
This commit is contained in:
dotCooCoo
2026-11-12 07:09:36 -08:00
parent 6bd9b9e50d
commit d2e21a38bf
9 changed files with 955 additions and 98 deletions
+40
View File
@@ -25,6 +25,8 @@ pub struct Journal {
bytes: usize,
/// Keep every entry from this number on ([`Journal::hold`]).
held: Option<usize>,
/// How many times kept entries were changed, dropped or renumbered ([`Journal::rewrites`]).
rewrites: u64,
}
/// Journals with the same entries (by number) are equal, whatever their history.
@@ -66,14 +68,24 @@ impl Journal {
self.entries.get(i.checked_sub(self.dropped)?)
}
pub fn get_mut(&mut self, i: usize) -> Option<&mut JournalEntry> {
self.rewrites += 1;
self.entries.get_mut(i.checked_sub(self.dropped)?)
}
pub fn last(&self) -> Option<&JournalEntry> {
self.entries.last()
}
pub fn last_mut(&mut self) -> Option<&mut JournalEntry> {
self.rewrites += 1;
self.entries.last_mut()
}
/// A number that grows whenever a kept entry may have changed, gone or been renumbered
/// ([`Journal::truncate`], [`Journal::remove`], [`Journal::replace`], [`Journal::get_mut`],
/// [`Journal::last_mut`]), and that [`Journal::push`] and letting old entries go leave as it
/// is. While it stays the same, the entries from a held number on are the ones counted before,
/// with the new ones after them.
pub fn rewrites(&self) -> u64 {
self.rewrites
}
pub fn push(&mut self, e: JournalEntry) {
let size = entry_bytes(&e);
self.entries.push(e);
@@ -83,6 +95,7 @@ impl Journal {
}
/// Drop the entries from number `len` on.
pub fn truncate(&mut self, len: usize) {
self.rewrites += 1;
let keep = len.saturating_sub(self.dropped);
self.entries.truncate(keep);
self.sizes.truncate(keep);
@@ -92,6 +105,7 @@ impl Journal {
/// Drop the entries numbered `numbers` (sorted, a number may repeat; the entries of canceled undo
/// groups). The entries after them are numbered down to close the gaps.
pub fn remove(&mut self, numbers: &[usize]) {
self.rewrites += 1;
let dropped = self.dropped;
let gone = |i: usize| numbers.binary_search(&(dropped + i)).is_ok();
let mut i = 0;
@@ -115,6 +129,7 @@ impl Journal {
}
/// Put `e` in place of entry number `i`, if kept.
pub fn replace(&mut self, i: usize, e: JournalEntry) {
self.rewrites += 1;
let Some(k) = i.checked_sub(self.dropped) else { return };
if let (Some(old), Some(size)) = (self.entries.get_mut(k), self.sizes.get_mut(k)) {
*size = entry_bytes(&e);
@@ -253,4 +268,29 @@ mod tests {
j.replace(29_000, big(0));
assert_eq!(j.bytes, j.iter().map(entry_bytes).sum::<usize>());
}
#[test]
fn rewrites_count_changes_to_kept_entries_but_not_new_ones() {
let mut j = Journal::default();
j.hold(Some(0));
for i in 0..100 {
j.push(("object.move".into(), json!({"dx": i})));
}
assert_eq!(j.rewrites(), 0, "pushes add entries after the counted ones");
let changes: [fn(&mut Journal); 5] = [
|j| j.truncate(90),
|j| j.remove(&[3]),
|j| j.replace(4, ("object.group".into(), json!({}))),
|j| {
let _ = j.get_mut(5);
},
|j| {
let _ = j.last_mut();
},
];
for (i, change) in changes.into_iter().enumerate() {
change(&mut j);
assert_eq!(j.rewrites(), i as u64 + 1);
}
}
}
+11
View File
@@ -150,6 +150,7 @@ impl VectorcraftApp {
io::remember_active_artboard(self);
let artboards = self.artboard_tool_state();
if let Some(r) = menus::run_ui_command(self, id, &params) {
self.ui.recording_base = None;
self.follow_artboard_tool(artboards);
return r;
}
@@ -221,6 +222,15 @@ impl VectorcraftApp {
if fileio::SaveMode::of(id).is_some() {
io::remember_view(self);
}
// While an action records, a command that the recording would keep as a step runs only
// when the step fits in the action.
let recorded = match panels::actions::before_recorded_command(self, id, &params) {
Ok(recorded) => recorded,
Err(e) => {
self.ui.status = e.clone();
return Err(e);
}
};
// A command that changes the edited text is an application edit (undo, a panel or menu
// edit, a control request). Capture the bounded committed context around it so `Other`
// dominates the frame even if an IME receipt follows. Selection/view commands that leave
@@ -230,6 +240,7 @@ impl VectorcraftApp {
let ime_before = crate::ime::stamp(&mut self.session, ime_view);
let cursor_settings = (self.session.caps_lock, self.session.prefs.use_precise_cursors);
let r = self.session.execute(id, &params).map_err(|e| e.to_string());
let r = panels::actions::after_recorded_command(self, recorded, r);
if r.is_ok() && cursor_settings != (self.session.caps_lock, self.session.prefs.use_precise_cursors) {
self.canvas.hover_cursor = None;
}
@@ -6,8 +6,9 @@
//! also holds the action's function key and color, as New Action does (`key`, `cmd`, `shift`,
//! `color`, see [`super::action_new`]). OK adds the set and makes it the current
//! one, renames the set, or changes the action's name, function key and color
//! (`ui.action.options`). An invalid `set`, `action` or function key is an error. OK then changes no set or
//! action, and the dialog stays open.
//! (`ui.action.options`). An invalid `set`, `action` or function key is an error, and so is a name
//! that would give the set a file that Load Actions… refuses (larger than 8 MiB). OK then changes no
//! set or action, and the dialog stays open.
use serde_json::{Value, json};
@@ -83,10 +84,17 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
p["name"] = json!(name);
app.run(crate::panels::actions::ACTION_OPTIONS, p)?;
} else if let Some(set) = d.fields.get("set") {
let s = set.as_u64().and_then(|i| usize::try_from(i).ok()).and_then(|i| app.ui.action_sets.get_mut(i)).ok_or("no such action set")?;
s.name = name;
let i = set.as_u64().and_then(|i| usize::try_from(i).ok()).filter(|&i| i < app.ui.action_sets.len()).ok_or("no such action set")?;
let mut edited = app.ui.action_sets[i].clone();
edited.name = name;
crate::panels::actions::edit_keeps_set_loadable(app, Some(i), &edited)?;
app.ui.action_sets[i] = edited;
// A recording into the set measures its file again.
app.ui.recording_base = None;
} else {
app.ui.action_sets.push(crate::panels::actions::ActionSet::new(name, vec![]));
let set = crate::panels::actions::ActionSet::new(name, vec![]);
crate::panels::actions::edit_keeps_set_loadable(app, None, &set)?;
app.ui.action_sets.push(set);
app.ui.action_set = Some(app.ui.action_sets.len() - 1);
}
app.ui.dialog = None;
+8 -8
View File
@@ -1372,25 +1372,25 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
crate::panels::actions::RENAME_ACTION,
"Rename Action",
"",
"{set: index, action: index, name} rename the Actions panel's action at action in set (0 is the top set) to name, with spaces trimmed from both ends; its steps, function key and color stay. An index past the last set or action, or an empty name, is an error and renames nothing → {name}",
"{set: index, action: index, name} rename the Actions panel's action at action in set (0 is the top set) to name, with spaces trimmed from both ends; its steps, function key and color stay. An index past the last set or action, an empty name, or a name that would give the set's action set file more than 8 MiB (counting the steps the recording in progress adds to it; a name that leaves the file as large or makes it smaller is never refused) is an error and renames nothing → {name}",
),
(
crate::panels::actions::INCLUDE_STEPS,
"Include Action Steps",
"",
"{set: index, action?: index, step?: index, included: bool, others?: bool} include or exclude the playback of every step in a set, every step in one action, or one step. With a step and others: true, leave that step unchanged and apply included to every other step in its action (Alt/Option-click). Invalid indexes change nothing → {set, action?, step?, included}",
"{set: index, action?: index, step?: index, included: bool, others?: bool} include or exclude the playback of every step in a set, every step in one action, or one step. With a step and others: true, leave that step unchanged and apply included to every other step in its action (Alt/Option-click). Invalid indexes change nothing. An exclusion that would give the set's action set file more than 8 MiB, counting the steps the recording in progress adds to the set, is an error and changes nothing; a change that leaves the file as large or makes it smaller is never refused → {set, action?, step?, included}",
),
(
crate::panels::actions::NEW_ACTION,
"New Action",
"",
"{set: index, name, key?: 0-15, cmd?, shift?, color?: red|yellow|green|blue|violet} add an action named name (spaces trimmed from both ends) after the last action of the Actions panel's set at set (0 is the top set) and start recording into it; Stop Playing/Recording (ui.action.stop) ends the recording. key 1 to 15 is the function key F1 to F15 that plays the action (0 or null: none), with cmd (Ctrl, Command on macOS) and shift; color is its Button Mode color (null or \"\": none), which the panel does not show. Action function keys take priority over command shortcuts and preserve the command's binding. On Windows F1, Ctrl+F4 and Ctrl+F6 cannot be used. The set becomes current and the new action is selected. The panel's Create New Action button and menu's New Action… (dialog `actionNew` with set, name, key, cmd, shift and color) run this on Record. While an action is recording, and for an index past the last set, an empty name, or a key or color that is not one of these, it is an error and nothing is added → {set, action}",
"{set: index, name, key?: 0-15, cmd?, shift?, color?: red|yellow|green|blue|violet} add an action named name (spaces trimmed from both ends) after the last action of the Actions panel's set at set (0 is the top set) and start recording into it; Stop Playing/Recording (ui.action.stop) ends the recording. key 1 to 15 is the function key F1 to F15 that plays the action (0 or null: none), with cmd (Ctrl, Command on macOS) and shift; color is its Button Mode color (null or \"\": none), which the panel does not show. Action function keys take priority over command shortcuts and preserve the command's binding. On Windows F1, Ctrl+F4 and Ctrl+F6 cannot be used. The set becomes current and the new action is selected. The panel's Create New Action button and menu's New Action… (dialog `actionNew` with set, name, key, cmd, shift and color) run this on Record. While an action is recording, and for an index past the last set, an empty name, a key or color that is not one of these, or an action that would give the set more than 1,000 actions or an action set file larger than 8 MiB, it is an error, nothing is added and no recording starts → {set, action}",
),
(
crate::panels::actions::ACTION_OPTIONS,
"Action Options",
"",
"{set: index, action: index, name?, key?: 0-15, cmd?, shift?, color?: red|yellow|green|blue|violet} change the Actions panel's action at action in set (0 is the top set); its steps stay. name renames it (spaces trimmed from both ends), key 1 to 15 with cmd (Ctrl, Command on macOS) and shift sets the function key F1 to F15 that plays it (0 or null: none), and color sets its Button Mode color (null or \"\": none); a param left out keeps what the action has, and cmd or shift alone changes that modifier of its key. Action function keys take priority over command shortcuts and preserve the command's binding. The panel menu's Action Options… (dialog `actionSetOptions` with set, action, name, key, cmd, shift and color) runs this for the selected action. An index past the last set or action, an empty name, or a key or color that is not one of these is an error and changes nothing → {name, key, color}",
"{set: index, action: index, name?, key?: 0-15, cmd?, shift?, color?: red|yellow|green|blue|violet} change the Actions panel's action at action in set (0 is the top set); its steps stay. name renames it (spaces trimmed from both ends), key 1 to 15 with cmd (Ctrl, Command on macOS) and shift sets the function key F1 to F15 that plays it (0 or null: none), and color sets its Button Mode color (null or \"\": none); a param left out keeps what the action has, and cmd or shift alone changes that modifier of its key. Action function keys take priority over command shortcuts and preserve the command's binding. The panel menu's Action Options… (dialog `actionSetOptions` with set, action, name, key, cmd, shift and color) runs this for the selected action. An index past the last set or action, an empty name, a key or color that is not one of these, or a change that would give the set's action set file more than 8 MiB (counting the steps the recording in progress adds to it; a change that leaves the file as large or makes it smaller is never refused) is an error and changes nothing → {name, key, color}",
),
(
crate::panels::actions::PLAY_ACTION,
@@ -1408,13 +1408,13 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
crate::panels::actions::START_RECORDING,
"Begin Recording",
"",
"{set?: index} begin recording an automatically named Action N in the Actions panel's set at set (0 is the top set), by default the current set or the first set; with no sets, make Set 1. The set becomes current, and ui.action.stop adds the recorded action after that set's existing actions. An explicit invalid set or an existing recording is an error and changes nothing → {set, name}",
"{set?: index} begin recording an automatically named Action N in the Actions panel's set at set (0 is the top set), by default the current set or the first set; with no sets, make Set 1. The set becomes current, and ui.action.stop adds the recorded action after that set's existing actions. An explicit invalid set, an existing recording, or a set where the new action would give it more than 1,000 actions or an action set file larger than 8 MiB is an error and changes nothing. While an action records, a recorded command whose step would give the action more than 10,000 steps or its set's action set file more than 8 MiB is an error and does not run, and the recording goes on → {set, name}",
),
(
crate::panels::actions::STOP_RECORDING,
"Stop Playing/Recording",
"",
"{} end the Actions panel's recording and keep its steps, in the action ui.action.new made for it, else in a new action after the last action of its set, as the panel's Stop Playing/Recording button does. It also ends an action playing step by step or with pauses, or one waiting at a stop, whose message box closes: the steps played stay and the rest do not run. With nothing recording or playing it changes nothing → {stopped}",
"{} end the Actions panel's recording and keep its steps, in the action ui.action.new made for it, else in a new action after the last action of its set, as the panel's Stop Playing/Recording button does. It also ends an action playing step by step or with pauses, or one waiting at a stop, whose message box closes: the steps played stay and the rest do not run. Recorded steps still past 10,000 in the action, or past 8 MiB in its set's action set file, are left out, and the status bar names the limit. With nothing recording or playing it changes nothing → {stopped}",
),
(
crate::panels::actions::INSERT_STOP,
@@ -1456,7 +1456,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
crate::panels::actions::LOAD_SET,
"Load Actions…",
"",
"{path? | data?: the file's text} read an action set file (.vcactions) at path, else a picked one, and add its set after the Actions panel's sets; a set named like one in the panel gets the next free number (Print 2), and the other sets stay. Files of another version, over 8 MiB, with more than 1,000 actions or with an action of more than 10,000 steps are refused; the panel menu's Load Actions… (disabled where there is no open dialog, the web) → {set: index, name, actions: n}. An action set another app saved as text (.aia, starting /version; read with actionSet.read) comes in with its actions. A step with a label and no recorded values comes in when exactly one menu command has that label (in English or the interface language, any case, no final …), and that command is directly in a top menu, can be recorded in actions, is what its menu bar item runs with no values and opens no dialog (never a submenu's item, a file or document command, Paste, Undo, Redo or an add-on's command), with no params and excluded from playback when it is off in the file; the other steps, every step with no label and every step recorded with values (a recorded menu item too) among them, are left out → {set, name, actions, skipped: [{action, step: its label, else its name}] (the first 1,000), leftOut: n}, and the status bar says how many were left out; a set with no name takes the file's name, else Actions; a file actionSet.read marks truncated is refused, and text that starts /version and isn't an action set gives \"not an action set file\"",
"{path? | data?: the file's text} read an action set file (.vcactions) at path, else a picked one, and add its set after the Actions panel's sets; a set named like one in the panel gets the next free number (Print 2), and the other sets stay. Files of another version, over 8 MiB, with more than 1,000 actions or with an action of more than 10,000 steps are refused, and so is any set, .aia ones too, that Save Actions… would write larger than 8 MiB (a file with less white space, or a name that gets a number); the panel menu's Load Actions… (disabled where there is no open dialog, the web) → {set: index, name, actions: n}. An action set another app saved as text (.aia, starting /version; read with actionSet.read) comes in with its actions. A step with a label and no recorded values comes in when exactly one menu command has that label (in English or the interface language, any case, no final …), and that command is directly in a top menu, can be recorded in actions, is what its menu bar item runs with no values and opens no dialog (never a submenu's item, a file or document command, Paste, Undo, Redo or an add-on's command), with no params and excluded from playback when it is off in the file; the other steps, every step with no label and every step recorded with values (a recorded menu item too) among them, are left out → {set, name, actions, skipped: [{action, step: its label, else its name}] (the first 1,000), leftOut: n}, and the status bar says how many were left out; a set with no name takes the file's name, else Actions; a file actionSet.read marks truncated is refused, and text that starts /version and isn't an action set gives \"not an action set file\"",
),
(
crate::panels::actions::DELETE_SETS,
@@ -1468,7 +1468,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
crate::panels::actions::DUPLICATE,
"Duplicate Action or Action Set",
"",
"{set?: index, action?: index} copy the Actions panel's action at action in set (0 is the top set) to right after it in the same set; without action, copy set with all its actions to right after it (default: the current set). The copy is named after the original with \" copy\" added, a copied set gets an id of its own, and the copy is selected. The panel menu's Duplicate runs this for the selected action, else the current set. An index past the last set or action is an error → {set, action?}",
"{set?: index, action?: index} copy the Actions panel's action at action in set (0 is the top set) to right after it in the same set; without action, copy set with all its actions to right after it (default: the current set). The copy is named after the original with \" copy\" added, a copied set gets an id of its own, and the copy is selected. The panel menu's Duplicate runs this for the selected action, else the current set. An index past the last set or action, or a copy that would give its set more than 1,000 actions or an action set file larger than 8 MiB, is an error and copies nothing → {set, action?}",
),
("file.showPackage", "Show Package", "", "{folder} show a package folder (file.package's folder) in the file manager → {folder}"),
(
File diff suppressed because it is too large Load Diff
+55 -3
View File
@@ -31,7 +31,7 @@ pub const ACTIONS_EXT: &str = "vcactions";
pub(super) const MAX_SET_FILE: usize = 8 << 20;
/// The most actions [`read_set`] reads from one file.
const MAX_SET_ACTIONS: usize = 1000;
pub(super) const MAX_SET_ACTIONS: usize = 1000;
/// The most steps [`read_set`] reads in one action.
pub(super) const MAX_ACTION_STEPS: usize = 10_000;
@@ -74,6 +74,51 @@ fn is_false(value: &bool) -> bool {
!value
}
/// A step as [`write_set`] writes it when it is included: [`FileStep`] without `excluded`.
#[derive(Serialize)]
struct IncludedStep<'a> {
command: &'a str,
params: &'a Value,
}
/// The bytes that excluding one step adds to the text [`write_set`] writes: a comma, a line
/// break, the indent and `"excluded": true`.
pub(super) const EXCLUDED_BYTES: usize = 28;
/// The bytes more that a step adds to the text [`write_set`] writes as an action's first step than
/// [`step_bytes`] counts: a line break on each side of the step and the indent of the closing
/// bracket, in place of a comma and a line break.
pub(super) const FIRST_STEP_BYTES: usize = 6;
/// The bytes that the included step `(command, params)` adds to the text [`write_set`] writes when
/// it goes after the last step of an action. As the action's first step it adds
/// [`FIRST_STEP_BYTES`] more.
pub(super) fn step_bytes(command: &str, params: &Value) -> usize {
/// Counts the bytes and the line breaks written to it.
#[derive(Default)]
struct Count {
bytes: usize,
breaks: usize,
}
impl std::io::Write for Count {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.bytes += buf.len();
self.breaks += buf.iter().filter(|&&b| b == b'\n').count();
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
let mut count = Count::default();
if serde_json::to_writer_pretty(&mut count, &IncludedStep { command, params }).is_err() {
return MAX_SET_FILE + 1;
}
// In the file every line of the step is indented by 8 more spaces, and a comma and a line break
// come before it.
count.bytes + 8 * (count.breaks + 1) + 2
}
/// The params of the stops in a step: a stop's own, or those of the stops a `command.batch` step
/// holds, which playback runs as its own steps.
fn stops_in<'a>(command: &str, params: &'a Value) -> Vec<&'a Value> {
@@ -329,6 +374,8 @@ fn load_aia(app: &mut VectorcraftApp, params: Value) -> Result<Value, String> {
}
let AiaLoad { mut set, skipped, left_out } = aia_set(&read, "Actions");
set.name = free_set_name(&app.ui.action_sets, &set.name);
// The set must save as a file that Load Actions… reads, which can be larger than the file read.
super::edit_keeps_set_loadable(app, None, &set)?;
let (name, actions) = (set.name.clone(), set.actions.len());
app.ui.action_sets.push(set);
if left_out > 0 {
@@ -350,14 +397,17 @@ fn load_aia(app: &mut VectorcraftApp, params: Value) -> Result<Value, String> {
/// same name as one in the panel gets the next free number (`Print 2`). Loaded actions keep their
/// function keys. The current set and the other sets stay as they are →
/// `{set: index, name, actions: n}`. Where the host can read part of a file (the desktop), a file
/// larger than 8 MiB is refused after its first 8 MiB and one byte are read.
/// larger than 8 MiB is refused after its first 8 MiB and one byte are read. A set that Save
/// Actions… would write as a file Load Actions… refuses (larger than 8 MiB, as a file with less
/// white space or a name that gets a number can give) is refused too, and no set is added.
///
/// An action set another app saved as text (`.aia`, read with `actionSet.read`) comes in with its
/// actions and the steps that stand for a VectorCraft command ([`step_name`], [`step_commands`]),
/// with no params; a step that is off in the file is excluded from playback. A set with no name is
/// called after the file, else `Actions`. The other steps are left out and listed, and the status
/// bar says how many → `{set, name, actions, skipped: [{action, step}] (at most 1,000), leftOut: n}`.
/// Such a file that `actionSet.read` marks `truncated` is refused, and no set is added.
/// Such a file that `actionSet.read` marks `truncated` is refused, and so is one whose set Save
/// Actions… would write larger than 8 MiB; then no set is added.
pub fn load_set(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
let (text, stem) = match p.get("data") {
Some(Value::String(data)) => {
@@ -398,6 +448,8 @@ pub fn load_set(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
};
let mut set = read_set(&text, if stem.is_empty() { "Actions" } else { &stem })?;
set.name = free_set_name(&app.ui.action_sets, &set.name);
// A number added to the name can make a file at the limit larger than it.
super::edit_keeps_set_loadable(app, None, &set)?;
let (name, actions) = (set.name.clone(), set.actions.len());
app.ui.action_sets.push(set);
Ok(json!({ "set": app.ui.action_sets.len() - 1, "name": name, "actions": actions }))
+12
View File
@@ -590,6 +590,16 @@ pub struct UiState {
/// adds a new action, as after Begin Recording.
#[serde(skip)]
pub recording_into: Option<u64>,
/// The length of the action set file that Stop Playing/Recording would leave without the steps
/// recorded so far, once measured; None until then, and again after an app command, which may
/// change the sets.
#[serde(skip)]
pub recording_base: Option<usize>,
/// The bytes that the steps recorded so far add to that file, at most, as last counted: (the
/// journal's rewrites, the recording's first entry, the journal's length, the bytes)
/// ([`vectorcraft_engine::Journal::rewrites`]).
#[serde(skip)]
pub recording_tail: Option<(u64, usize, usize, usize)>,
/// How actions play (Actions panel menu › Playback Options…), kept with the UI preferences.
#[serde(default, deserialize_with = "crate::panels::actions::deserialize_playback")]
pub action_playback: crate::panels::actions::PlaybackOptions,
@@ -1094,6 +1104,8 @@ impl Default for UiState {
action_sets_selected: vec![],
recording: None,
recording_into: None,
recording_base: None,
recording_tail: None,
action_playback: Default::default(),
action_playing: None,
shortcut_overrides: Default::default(),
+26 -7
View File
@@ -2818,16 +2818,23 @@ already walks Image Trace. Short agent entry points:
use version 2, so older builds refuse them
instead of playing the excluded steps or failing at a stop; other files use version 1. A file with another `format` or a `version` other than
1, 2 or 3, a stop whose message is not text or whose `allowContinue` is not true or false, a file larger than 8 MiB, one with more than 1,000 actions and one with an action of more than 10,000 steps are
refused, and no set is added. The Actions panel menu's Save Actions… and Load Actions… run these; Load Actions… is
refused, and no set is added. So is a file whose set `ui.actionSet.save` would write larger than 8 MiB (a file with
less white space, a set name that gets a number, or an `.aia` set), with the error `the action set would be larger
than 8 MiB`. The Actions panel menu's Save Actions… and Load Actions… run these; Load Actions… is
disabled where there is no open dialog (the web). `ui.actionSet.load` also reads the action sets other apps save as
text (`.aia`); see "Action sets from other apps".
- Actions: `ui.action.rename {set, action, name}` renames the Actions panel's action at `action` in set `set` (0 is
the top set) to `name` with spaces trimmed from both ends, keeps its steps, function key and color, and returns
`{name}`. An index past the last set or action, or an empty name, is an error, and then nothing is renamed.
`{name}`. An index past the last set or action, or an empty name, is an error, and then nothing is renamed. So is a
name that would give the set's action set file more than 8 MiB, counting the steps the recording in progress adds
to the set (`the action set would be larger than 8 MiB`); a name that leaves the file as large or makes it smaller
is never refused. The same holds for `ui.action.options` and for the names Set Options… and New Set… give.
- Actions: `ui.action.stepsIncluded {set, action?, step?, included, others?}` includes or excludes all steps of a set,
one action, or one step. With `step` and `others: true`, it leaves that step as it was and applies `included` to the
other steps of its action (the panel's Alt/Option-click). An invalid set, action or step changes nothing. The panel's
checkmarks run this command; normal playback and Batch… skip excluded steps.
other steps of its action (the panel's Alt/Option-click). An invalid set, action or step changes nothing. So does an
exclusion that would give the set's action set file more than 8 MiB, counting the steps the recording in progress
adds to the set; it is an error. A change that leaves the file as large or makes it smaller is never refused. The
panel's checkmarks run this command; normal playback and Batch… skip excluded steps.
- Actions: `ui.action.new {set, name, key?, cmd?, shift?, color?}` adds an action named `name` (spaces
trimmed) after the last action of set `set` (0 is the top set), starts recording into it and returns
`{set, action}`; Stop Playing/Recording (`ui.action.stop`) ends the recording. `key` 1 to 15 is the function key F1 to F15 that plays the
@@ -2835,12 +2842,23 @@ already walks Image Trace. Short agent entry points:
(`red`, `yellow`, `green`, `blue` or `violet`; null or `""` for none), which the panel does not show. Action function
keys take priority over command shortcuts and preserve the command's binding. On Windows F1, Ctrl+F4 and Ctrl+F6
cannot be used. While an action is recording, and for an index past the last set,
an empty name, or a key or color that is not one of these, it is an error and nothing is added. The panel's Create
an empty name, or a key or color that is not one of these, it is an error and nothing is added. So is an action that
would give the set more than 1,000 actions (`the action set file holds more than 1,000 actions`) or a file larger
than 8 MiB (`the action set would be larger than 8 MiB`), and then no recording starts. The panel's Create
New Action button and menu's New Action… open the `actionNew` dialog, whose Record runs this.
- Actions: `ui.action.record {set?}` starts recording a new action in the chosen set, or the current set when `set`
is left out, and returns `{set, name}`. With no sets, it creates Set 1. The action gets the next available
`Action N` name in that set. The Actions panel's Begin Recording button runs this command. An invalid set or an
existing recording is an error and leaves the sets and recording unchanged.
existing recording is an error and leaves the sets and recording unchanged, and so is a set where the new action
would give it more than 1,000 actions or an action set file larger than 8 MiB. While a recording (this one or
`ui.action.new`'s) is in progress, a recorded command whose step would give the action more than 10,000 steps, or
its set's action set file more than 8 MiB, is an error with the reason (`the action already has 10,000 steps`, `the
action set would be larger than 8 MiB`): it does not run, and the recording goes on. A batch (`command.batch`) is
such a command too. A command whose step grows past a limit only as it runs, from values it adds to its step,
keeps its edit and result; the status bar names the limit, later recorded commands do not run, and
`ui.action.stop` leaves its step out. A stop that playback reaches
then is not recorded, and the status bar shows the reason. `ui.action.stop` leaves out any recorded steps still past
either limit and shows which one in the status bar.
- Actions: `ui.action.options {set, action, name?, key?, cmd?, shift?, color?}` changes the action at
`action` in set `set` and keeps its steps: `name` renames it, `key` with `cmd` and `shift` sets its function key,
and `color` its Button Mode color, as `ui.action.new` takes them; a param left out keeps what the action has, and
@@ -2926,7 +2944,8 @@ already walks Image Trace. Short agent entry points:
`ui.actionSet.duplicate {set?: index, action?: index}` copies the action at `action` in set `set` to right after it
in the same set, and returns `{set, action}`; without `action` it copies the set with all its actions to right after
it, and returns `{set}`. In both cases `set` defaults to the current set. The copy is named after the original with
" copy" added, a copied set gets an id of its own, and the copy is selected. An index past the last set or action is an error. The
" copy" added, a copied set gets an id of its own, and the copy is selected. An index past the last set or action is an error,
and so is a copy that would give its set more than 1,000 actions or a file larger than 8 MiB; then nothing is copied. The
panel menu's Duplicate runs this for the selected action, else the current set.
- Image Trace: `imageTrace.presets`, `imageTrace.make {id?, preset?, params?, view?}` (or `imageTrace.makeAndExpand`),
`imageTrace.setView`, `imageTrace.expand`, `imageTrace.release`. A key inside `params` that is not one of the trace
+1
View File
@@ -725,6 +725,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- Actions panel (record/playback, persisted), generic parameter dialogs for every "…" command.
- Actions panel: the current set is kept with the UI preferences, so after a restart a recording goes into the set that was current, not into the first set. With no set current, Begin Recording records into the first set and makes it current.
- Actions panel: expand an action to show its recorded steps, then clear a step's checkmark to keep it in the action but skip it during playback. The action and set checkmarks include or exclude all their steps; a mixed checkmark includes all. Alt/Option-clicking a step checkmark keeps that step as it was and applies the opposite inclusion state to the other steps in its action. This follows the public [Exclude commands from an action](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/exclude-commands-from-actions.html) guide; the scope of the mixed-state and Alt-click behavior is VectorCraft's design. The exclusion survives UI restart, action duplication, and `.vcactions` save/load. Existing version 1 action set files load with every step included.
- Actions panel: an action keeps to the limits that Load Actions… reads while it records and while its steps are excluded: at most 10,000 steps in an action and 8 MiB in its set's file, which are VectorCraft's own rules. Commands add steps to a recording until it stops ([the help page](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/record-actions.html)); a command whose step would pass a limit does not run, its document stays as it was, and the recording goes on. A stop that playback reaches is not recorded then, and the status bar shows the reason. Clearing a checkmark keeps the step in the action and skips it in playback ([the help page](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/exclude-commands-from-actions.html)); an exclusion that would make the set's file larger than 8 MiB changes no checkmark, and one that makes the file smaller always works. Stop Playing/Recording leaves out any recorded steps still past a limit and names the limit in the status bar. Begin Recording, Duplicate, New Action, renaming an action, Action Options…, Set Options…, New Set… and Load Actions… (`.aia` sets included) also refuse a change that would give a set more than 1,000 actions or a file larger than 8 MiB, and change nothing then. Tests `panels::actions::tests::{recorded_commands_keep_an_action_at_10_000_steps, recorded_commands_keep_an_action_set_file_within_8_mib, a_recorded_command_whose_noted_values_do_not_fit_is_left_out, a_stop_reached_while_recording_a_full_action_is_left_out, stop_leaves_out_recorded_steps_past_the_limits, excluding_steps_keeps_an_action_set_file_within_8_mib, a_recorded_batch_that_edits_another_document_is_left_out_when_it_grows, a_set_renamed_while_it_records_is_measured_again, steps_recorded_after_an_exclusion_past_the_last_step_are_included, duplicate_keeps_action_set_files_within_their_limits, new_action_keeps_an_action_set_file_within_its_limits, renaming_keeps_an_action_set_file_within_8_mib, load_actions_refuses_a_set_that_would_save_larger_than_8_mib}`.
- Actions panel: the panel menu's Playback Options… sets how actions play: Accelerated (the default) plays every step at once, Step by Step plays one step at a time with the canvas drawn after each, and Pause For waits the given number of seconds between steps ([Specify playback speed](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/specify-playback-speed.html); `ui.action.playbackOptions`). VectorCraft's own rules, which the page does not cover: the pause is a whole number of seconds from 1 to 60, the setting is kept with the UI preferences, a paced playback ends at Stop Playing/Recording or when a step comes due while another document is active, each step of a paced playback is its own undo step, and Batch… plays at full speed.
- Actions panel: the panel menu's Insert Stop… opens Record Stop, with a Message box and Allow Continue, and adds a stop to the recording in progress, after the selected command, or at the end of the selected action ([Insert stops in actions](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/insert-stops-in-actions.html); `ui.action.insertStop`). A playing action waits at the stop and shows the message so the user can do a manual task, and Play Current Selection plays the rest ([Illustrator CS5 help](https://help.adobe.com/archive/en_US/illustrator/cs5/illustrator_cs5_help.pdf#page=459); `ui.action.continue`). With Allow Continue, the message box also has Continue. VectorCraft's own rules, which the pages do not cover: an empty message still waits, Stop Playing/Recording ends the playback and keeps the steps already played, the rest plays on the document active then, a stop waits at every playback speed, at Accelerated the steps between two stops are one undo step, Batch… refuses an action with an included stop, and Delete Selection on a selected step deletes that step, a stop or a command, without asking (`ui.action.deleteStep`).
- Actions panel: the panel menu's Insert Menu Item… adds a menu command to the recording in progress, after the selected step, or at the end of the selected action, without running it, and playback runs it as the menu does ([Insert non-recordable tasks into actions](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/insert-non-recordable-tasks-into-actions.html); `ui.action.insertMenuItem`). The dialog's Find box picks the one item that matches the words typed, and a click on a listed item picks it. VectorCraft's own rules, which the page does not cover: items that open a dialog, the Object › Align items, Image Trace's Make and Make and Expand, Pattern's Make and Edit Pattern, and the File, Window and Help menu items are not listed, and an action holds at most 10,000 steps and its set file at most 8 MiB, for stops too.