diff --git a/crates/ui-egui/src/app.rs b/crates/ui-egui/src/app.rs index 67e67ac444..997da6a711 100644 --- a/crates/ui-egui/src/app.rs +++ b/crates/ui-egui/src/app.rs @@ -344,6 +344,8 @@ impl VectorcraftApp { screen_color::poll(self, ctx); self.poll_finished_work(); crate::script_runs::poll(self); + // A panel's, a dialog's or the text input's commit journaled in the frame before. + crate::panels::actions::after_journaled_edit(self); crate::scripts_menu::poll(self, ctx); if self.session.script_running() && !self.session.script_waiting() && self.session.script_dialog().is_none() { // A script in `$.sleep` goes on when its time is up; until then the app can idle. diff --git a/crates/ui-egui/src/app/commands.rs b/crates/ui-egui/src/app/commands.rs index 1d2493385e..50c7b1be30 100644 --- a/crates/ui-egui/src/app/commands.rs +++ b/crates/ui-egui/src/app/commands.rs @@ -224,13 +224,10 @@ impl VectorcraftApp { } // 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, ¶ms) { - Ok(recorded) => recorded, - Err(e) => { - self.ui.status = e.clone(); - return Err(e); - } - }; + if let Err(e) = panels::actions::before_recorded_command(self, id, ¶ms) { + 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 @@ -240,7 +237,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, ¶ms).map_err(|e| e.to_string()); - let r = panels::actions::after_recorded_command(self, recorded, r); + let r = panels::actions::after_recorded_command(self, r); if r.is_ok() && cursor_settings != (self.session.caps_lock, self.session.prefs.use_precise_cursors) { self.canvas.hover_cursor = None; } diff --git a/crates/ui-egui/src/canvas/drag.rs b/crates/ui-egui/src/canvas/drag.rs index 1a385d5642..414746310f 100644 --- a/crates/ui-egui/src/canvas/drag.rs +++ b/crates/ui-egui/src/canvas/drag.rs @@ -399,7 +399,9 @@ fn pressed_with(app: &mut VectorcraftApp, index: Option, pos: Point, tool } } -/// Act on what a tool event asked the UI for (dialogs, tool switches), or show its error. +/// Act on what a tool event asked the UI for (dialogs, tool switches), or show its error. When an +/// edit the tool committed or ran no longer fits in the action being recorded, the edit stays and +/// the status bar names the limit ([`crate::panels::actions::after_journaled_edit`]). pub fn apply_requests(app: &mut VectorcraftApp, r: vectorcraft_engine::Result>) { match r { Ok(reqs) => { @@ -420,6 +422,7 @@ pub fn apply_requests(app: &mut VectorcraftApp, r: vectorcraft_engine::Result app.status(e.to_string()), } diff --git a/crates/ui-egui/src/panels/actions.rs b/crates/ui-egui/src/panels/actions.rs index 8196ad27c2..b65c15c5af 100644 --- a/crates/ui-egui/src/panels/actions.rs +++ b/crates/ui-egui/src/panels/actions.rs @@ -1201,36 +1201,56 @@ fn recorded_steps_fit(app: &mut VectorcraftApp) -> Result<(), String> { } /// Before the engine command `id` runs with `params`: Err when the recording in progress would keep -/// it as a step that does not fit in its action ([`recording_room`]); else whether the recording -/// would keep it, for [`after_recorded_command`]. A command is kept as a step when it is journaled, -/// a batch (`command.batch`, which journals itself) too, and Find Objects when it selects what it -/// finds. Undo and Redo are steps too, and are refused when they do not fit. -pub(crate) fn before_recorded_command(app: &mut VectorcraftApp, id: &str, params: &Value) -> Result { +/// it as a step that does not fit in its action ([`recording_room`]). A command is kept as a step +/// when it is journaled, a batch (`command.batch`, which journals itself) too, and Find Objects +/// when it selects what it finds. Undo and Redo are steps too, and are refused when they do not +/// fit. +pub(crate) fn before_recorded_command(app: &mut VectorcraftApp, id: &str, params: &Value) -> Result<(), String> { let journaled = vectorcraft_engine::find_command(id).is_some_and(|c| c.journal) || id == "command.batch" || (id == "document.find" && params.get("select").and_then(Value::as_bool) == Some(true)); if app.ui.recording.is_none() || !journaled || app.session.active().is_some_and(|d| d.interaction.is_some()) { - return Ok(false); + return Ok(()); } - recording_room(app, id, params)?; - Ok(true) + recording_room(app, id, params) } -/// After a command that the recording keeps as a step (`recorded`, [`before_recorded_command`]) has -/// run with result `r`: when the values the command added to its step as it ran make the steps no -/// longer fit ([`recorded_steps_fit`]), the command's edit and its result stay, the status bar -/// names the limit, no later recorded command runs, and Stop Playing/Recording leaves the step out -/// ([`steps_that_fit`]) → the command's result. -pub(crate) fn after_recorded_command(app: &mut VectorcraftApp, recorded: bool, r: Result) -> Result { - if recorded - && r.is_ok() - && let Err(e) = recorded_steps_fit(app) - { - app.status(e); +/// After an engine command has run with result `r`: when the steps it journaled, the values a +/// command added to its step as it ran or the commands a script ran (`script.run`), make the steps +/// no longer fit ([`after_journaled_edit`]), the command's edit and its result stay, the status bar +/// names the limit, no later recorded command runs, and Stop Playing/Recording leaves the steps +/// out ([`steps_that_fit`]) → the command's result. +pub(crate) fn after_recorded_command(app: &mut VectorcraftApp, r: Result) -> Result { + if r.is_ok() { + after_journaled_edit(app); } r } +/// After an edit that journals without [`before_recorded_command`]'s check (a script run's +/// commands, a tool's or a panel's edit, a dialog's commit): when the journal changed since the +/// last check and the steps that the recording in progress keeps no longer fit in its action +/// ([`recorded_steps_fit`]), the edit and the journal stay, the status bar names the limit, no +/// later recorded command runs, and Stop Playing/Recording keeps the steps that fit +/// ([`steps_that_fit`]) → whether the status bar names a limit. +pub(crate) fn after_journaled_edit(app: &mut VectorcraftApp) -> bool { + if app.ui.recording.is_none() { + return false; + } + let journal = (app.session.journal.rewrites(), app.session.journal.len()); + if app.ui.recording_checked == Some(journal) { + return false; + } + app.ui.recording_checked = Some(journal); + match recorded_steps_fit(app) { + Ok(()) => false, + Err(e) => { + app.status(e); + true + } + } +} + /// What Stop Playing/Recording adds to a set ([`recorded_into`]). struct Recorded { /// The set's action that the recording records into; None for the new action that the @@ -1400,6 +1420,7 @@ fn start_recording(app: &mut VectorcraftApp, set: usize, name: String, into: Opt app.ui.recording_into = into; app.ui.recording_base = None; app.ui.recording_tail = None; + app.ui.recording_checked = Some((app.session.journal.rewrites(), start)); } /// How many of `steps`, from the first, Stop Playing/Recording can give the recording's action so @@ -1446,6 +1467,7 @@ fn stop_recording(app: &mut VectorcraftApp) { let Some((set, name, _)) = app.ui.recording.take() else { return }; app.ui.recording_base = None; app.ui.recording_tail = None; + app.ui.recording_checked = None; app.session.journal.hold(None); match app.ui.recording_into.take() { Some(id) => { @@ -5003,6 +5025,212 @@ mod tests { assert!(files::read_set(&files::write_set(&app.ui.action_sets[0]), "Test").is_ok()); } + /// A script that moves the selection twice. + const TWO_MOVES: &str = "vc.run('object.move', {dx: 1, dy: 0}); vc.run('object.move', {dx: 1, dy: 0});"; + + /// Runs `source` to its end: in one call (`frames` false, a run with nobody to ask), or a frame + /// at a time, as the app does. + fn run_script(app: &mut VectorcraftApp, source: &str, frames: bool) { + app.session.script_frames = frames; + app.run("script.run", json!({"source": source, "name": "t.js"})).unwrap(); + for _ in 0..10_000 { + if !app.session.script_running() { + return; + } + crate::script_runs::poll(app); + } + panic!("the script did not end"); + } + + /// Begin Recording with `filler` steps already recorded. + fn recording_with(app: &mut VectorcraftApp, filler: usize) { + app.run(START_RECORDING, json!({})).unwrap(); + for _ in 0..filler { + app.session.journal.push(("object.group".to_string(), json!({}))); + } + } + + /// A script whose commands take a recording past 10,000 steps keeps its edit and the whole + /// journal, the status bar names the limit as the run ends, the next recorded command does not + /// run, and Stop Playing/Recording keeps the 10,000 steps that fit. One that fills the action + /// exactly names no limit. + #[test] + fn script_run_checks_recording_step_limit_on_completion() { + let steps = "the action already has 10,000 steps"; + for frames in [false, true] { + let (mut app, x) = app_with_three_moves(); + recording_with(&mut app, files::MAX_ACTION_STEPS - 1); + app.ui.status.clear(); + run_script(&mut app, TWO_MOVES, frames); + assert_eq!(x(&app), 2.0, "frames {frames}: the script's edit stays"); + let start = recording_start(&app).unwrap(); + assert_eq!(app.session.journal.len() - start, files::MAX_ACTION_STEPS + 1, "frames {frames}: the journal keeps every step"); + assert_eq!(app.ui.status, steps, "frames {frames}: the status bar names the limit now"); + assert!(app.ui.recording.is_some(), "the recording goes on"); + assert_eq!(app.run("object.move", json!({"dx": 1, "dy": 0})).unwrap_err(), steps, "no later command runs"); + assert_eq!(x(&app), 2.0); + app.run(STOP_RECORDING, json!({})).unwrap(); + let recorded = app.ui.action_sets[0].actions.last().unwrap(); + assert_eq!(recorded.steps.len(), files::MAX_ACTION_STEPS); + assert_eq!(recorded.steps.last().map(|(c, _)| c.as_str()), Some("object.move"), "the first move fits"); + let saved = files::write_set(&app.ui.action_sets[0]); + assert_eq!(files::read_set(&saved, "Test").unwrap().actions[1].steps.len(), files::MAX_ACTION_STEPS, "and loads again"); + + let (mut app, _) = app_with_three_moves(); + recording_with(&mut app, files::MAX_ACTION_STEPS - 2); + app.ui.status.clear(); + run_script(&mut app, TWO_MOVES, frames); + let ran = if frames { "Ran t.js" } else { "" }; + assert_eq!(app.ui.status, ran, "frames {frames}: 10,000 steps fit"); + } + } + + /// A script whose commands give the recording's set a file larger than 8 MiB keeps its edit, + /// the status bar names the limit as the run ends, and Stop Playing/Recording keeps the steps + /// that fit in a set that Load Actions… reads. One that fills the file exactly names no limit. + #[test] + fn script_run_checks_recording_byte_limit_on_completion() { + let size = "the action set would be larger than 8 MiB"; + for frames in [false, true] { + for spare in [0, 1] { + let (mut app, x) = app_with_three_moves(); + // The two steps as an action keeps them. + let before = app.session.journal.len(); + run_script(&mut app, TWO_MOVES, false); + let both: usize = app.session.journal_for_action(before).iter().map(|(c, p)| files::step_bytes(c, p)).sum(); + app.run("edit.undo", json!({})).unwrap(); + assert_eq!(x(&app), 0.0); + let pad = |n: usize| ("object.move".to_string(), json!({"dx": 1, "dy": 0, "pad": "x".repeat(n)})); + app.ui.action_sets[0].actions[0].steps = vec![pad(0)]; + let room = files::MAX_SET_FILE - files::write_set(&app.ui.action_sets[0]).len(); + // Room for both moves, or for one move and all but a byte of the other. + app.ui.action_sets[0].actions[0].steps = vec![pad(room - both + 1 - spare)]; + let into = app.ui.action_sets[0].actions[0].id; + app.run(START_RECORDING, json!({})).unwrap(); + app.ui.recording_into = Some(into); + app.ui.status.clear(); + run_script(&mut app, TWO_MOVES, frames); + assert_eq!(x(&app), 2.0, "frames {frames}: the script's edit stays"); + let what = format!("frames {frames}, spare {spare}"); + if spare == 1 { + assert_eq!(app.ui.status, if frames { "Ran t.js" } else { "" }, "{what}: the file is exactly 8 MiB"); + continue; + } + assert_eq!(app.ui.status, size, "{what}: the status bar names the limit now"); + assert_eq!(app.session.journal.since(recording_start(&app).unwrap()).len(), 2, "the journal keeps both moves"); + assert_eq!(app.run("object.move", json!({"dx": 1, "dy": 0})).unwrap_err(), size, "no later command runs"); + app.run(STOP_RECORDING, json!({})).unwrap(); + assert_eq!(app.ui.action_sets[0].actions[0].steps.len(), 2, "{what}: the first move fits"); + let saved = files::write_set(&app.ui.action_sets[0]); + assert!(saved.len() <= files::MAX_SET_FILE); + assert!(files::read_set(&saved, "Test").is_ok()); + } + } + } + + /// Presses, drags and releases the active tool from `from` to `to`, as the canvas hands its + /// results on ([`crate::canvas::apply_requests`]). + fn tool_drag(app: &mut VectorcraftApp, from: (f64, f64), to: (f64, f64)) { + use vectorcraft_tools::{PointerEvent, PointerKind}; + let view = app.view_info(); + for (kind, (x, y)) in [(PointerKind::Down, from), (PointerKind::Drag, to), (PointerKind::Up, to)] { + let r = app.session.pointer(&PointerEvent::new(kind, x, y), view); + crate::canvas::apply_requests(app, r); + } + } + + /// A drag with the Selection tool (a committed preview) or the Rectangle tool (a command the + /// tool runs) that takes a recording past 10,000 steps keeps its edit and the journal, the + /// status bar names the limit as the tool ends, and Stop Playing/Recording keeps the 10,000 + /// steps that fit. One that fills the action exactly names no limit. + #[test] + fn tool_commit_checks_recording_step_limit() { + let steps = "the action already has 10,000 steps"; + for tool in ["selection", "rectangle"] { + for filler in [files::MAX_ACTION_STEPS, files::MAX_ACTION_STEPS - 1] { + let (mut app, _) = app_with_three_moves(); + app.select_tool(tool); + recording_with(&mut app, filler); + app.ui.status.clear(); + let (journal, art) = (app.session.journal.len(), app.session.active().unwrap().doc.art_bounds()); + tool_drag(&mut app, (20.0, 20.0), (60.0, 50.0)); + assert_eq!(app.session.journal.len(), journal + 1, "{tool}: the tool journals one step"); + assert_ne!(app.session.active().unwrap().doc.art_bounds(), art, "{tool}: the edit stays"); + if filler < files::MAX_ACTION_STEPS { + assert_eq!(app.ui.status, "", "{tool}: 10,000 steps fit"); + continue; + } + assert_eq!(app.ui.status, steps, "{tool}: the status bar names the limit now"); + assert!(app.ui.recording.is_some(), "the recording goes on"); + app.run(STOP_RECORDING, json!({})).unwrap(); + assert_eq!(app.ui.action_sets[0].actions.last().unwrap().steps.len(), files::MAX_ACTION_STEPS); + } + } + } + + /// A tool's edit whose step gives the recording's set a file larger than 8 MiB keeps its edit, + /// the status bar names the limit as the tool ends, and Stop Playing/Recording leaves the step + /// out of a set that Load Actions… reads. + #[test] + fn tool_commit_checks_recording_byte_limit() { + let size = "the action set would be larger than 8 MiB"; + for tool in ["selection", "rectangle"] { + let (mut app, _) = app_with_three_moves(); + app.select_tool(tool); + app.run(START_RECORDING, json!({})).unwrap(); + let pad = |n: usize| ("object.move".to_string(), json!({"dx": 1, "dy": 0, "pad": "x".repeat(n)})); + app.ui.action_sets[0].actions[0].steps = vec![pad(0)]; + let room = files::MAX_SET_FILE - files::write_set(&app.ui.action_sets[0]).len(); + app.ui.action_sets[0].actions[0].steps = vec![pad(room)]; + app.ui.recording_into = Some(app.ui.action_sets[0].actions[0].id); + app.ui.status.clear(); + let art = app.session.active().unwrap().doc.art_bounds(); + tool_drag(&mut app, (20.0, 20.0), (60.0, 50.0)); + assert_ne!(app.session.active().unwrap().doc.art_bounds(), art, "{tool}: the edit stays"); + assert_eq!(app.ui.status, size, "{tool}: the status bar names the limit now"); + assert_eq!(app.session.journal.since(recording_start(&app).unwrap()).len(), 1, "{tool}: the journal keeps the step"); + app.run(STOP_RECORDING, json!({})).unwrap(); + assert_eq!(app.ui.action_sets[0].actions[0].steps.len(), 1, "{tool}: the step is left out"); + assert!(files::read_set(&files::write_set(&app.ui.action_sets[0]), "Test").is_ok()); + } + } + + /// A step that reaches the journal outside a command, as a panel's or a dialog's commit does, is + /// checked on the app's next frame. A journal rewrite that makes a checked step larger is + /// checked again, and a frame with no new step does not name the limit again. + #[test] + fn a_journal_rewrite_or_a_direct_commit_is_checked_on_the_next_frame() { + let size = "the action set would be larger than 8 MiB"; + let (mut app, _) = app_with_three_moves(); + let small = ("object.move".to_string(), json!({"dx": 1, "dy": 0})); + let pad = |n: usize| ("object.move".to_string(), json!({"dx": 1, "dy": 0, "pad": "x".repeat(n)})); + app.ui.action_sets[0].actions[0].steps = vec![pad(0)]; + let room = files::MAX_SET_FILE - files::write_set(&app.ui.action_sets[0]).len(); + app.ui.action_sets[0].actions[0].steps = vec![pad(room - files::step_bytes(&small.0, &small.1))]; + let into = app.ui.action_sets[0].actions[0].id; + app.run(START_RECORDING, json!({})).unwrap(); + app.ui.recording_into = Some(into); + let ctx = egui::Context::default(); + let frame = |app: &mut VectorcraftApp| { + app.begin_host_iteration(); + let mut out = ctx.run_ui(egui::RawInput::default(), |ui| app.tick(ui.ctx())); + out.textures_delta.clear(); + }; + app.session.journal.push(small); + app.ui.status.clear(); + frame(&mut app); + assert_eq!(app.ui.status, "", "a step that fills the file exactly"); + let start = recording_start(&app).unwrap(); + app.session.journal.replace(start, pad(1)); + frame(&mut app); + assert_eq!(app.ui.status, size, "the rewritten step no longer fits"); + app.ui.status.clear(); + frame(&mut app); + assert_eq!(app.ui.status, "", "named once"); + app.run(STOP_RECORDING, json!({})).unwrap(); + assert_eq!(app.ui.action_sets[0].actions[0].steps.len(), 1, "the step is left out"); + } + /// When the action New Action made for the recording in progress is deleted, Stop /// Playing/Recording keeps none of its steps, so no limit stops a recorded command, even with /// the set's file at 8 MiB. diff --git a/crates/ui-egui/src/script_runs.rs b/crates/ui-egui/src/script_runs.rs index 02c2229741..ddba7618d1 100644 --- a/crates/ui-egui/src/script_runs.rs +++ b/crates/ui-egui/src/script_runs.rs @@ -34,7 +34,12 @@ pub fn poll(app: &mut VectorcraftApp) { } script_run::settle_close(app); match result { - Ok(_) => app.status(format!("Ran {name}")), + // The commands it ran are journaled now: a recording they no longer fit in names its limit. + Ok(_) => { + if !crate::panels::actions::after_journaled_edit(app) { + app.status(format!("Ran {name}")); + } + } Err(e) => failed(app, e.to_string()), } app.sync_views(); diff --git a/crates/ui-egui/src/state.rs b/crates/ui-egui/src/state.rs index f84b8f53b0..e8051bb8bf 100644 --- a/crates/ui-egui/src/state.rs +++ b/crates/ui-egui/src/state.rs @@ -632,6 +632,10 @@ pub struct UiState { /// ([`vectorcraft_engine::Journal::rewrites`]). #[serde(skip)] pub recording_tail: Option<(u64, usize, usize, usize)>, + /// The journal as the recording in progress last checked it against the limits of an action + /// set file: (its rewrites, its length) ([`crate::panels::actions::after_journaled_edit`]). + #[serde(skip)] + pub recording_checked: Option<(u64, 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, @@ -1152,6 +1156,7 @@ impl Default for UiState { recording_into: None, recording_base: None, recording_tail: None, + recording_checked: None, action_playback: Default::default(), action_playing: None, shortcut_overrides: Default::default(), diff --git a/docs/mcp.md b/docs/mcp.md index 274db05a34..eccc5185d8 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -3063,7 +3063,9 @@ already walks Image Trace. Short agent entry points: 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 + `ui.action.stop` leaves its step out. So does a `script.run` whose commands, or a tool's or panel's edit whose + step, takes the recording past a limit: the status bar names the limit when the run or the edit ends, and + `ui.action.stop` keeps the steps that fit. 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 diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index f04ee3c910..5e2a79bbe1 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -744,7 +744,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 command whose step passes a limit only as it runs, from values it adds to its step, keeps its edit; the status bar names the limit, later recorded commands do not run, and Stop Playing/Recording leaves its step out. 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: 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 command whose step passes a limit only as it runs, from values it adds to its step, keeps its edit; the status bar names the limit, later recorded commands do not run, and Stop Playing/Recording leaves its step out. The same holds when a script run, a tool's drag or a panel's or dialog's edit takes the recording past a limit: the edit and the journal stay, the status bar names the limit as soon as the edit ends (instead of the script's "Ran" message), and Stop Playing/Recording keeps only the steps that fit. 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, script_run_checks_recording_step_limit_on_completion, script_run_checks_recording_byte_limit_on_completion, tool_commit_checks_recording_step_limit, tool_commit_checks_recording_byte_limit, a_journal_rewrite_or_a_direct_commit_is_checked_on_the_next_frame, 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.