Merge pull request #5600 from dotCooCoo/feat/actions-playback-options

Actions: Playback Options plays actions accelerated, step by step or with a pause between steps
This commit is contained in:
BFlat
2026-11-04 20:34:33 +07:00
committed by GitHub
36 changed files with 956 additions and 17 deletions
+12
View File
@@ -370,6 +370,12 @@ impl VectorcraftApp {
}
self.flush_clipboard_out(ctx);
self.drain_inbox();
// An action playing at Step by Step or Pause For runs its next step when it is due, after
// the control requests and opened files above: a Stop or a document switch they bring
// comes first.
if let Some(wait) = crate::panels::actions::tick_playback(self, now) {
ctx.request_repaint_after(std::time::Duration::from_secs_f64(wait));
}
if self.viewport_hidden(ctx) && (self.needs_hidden_paint() || (!dialog_was_open && self.ui.dialog.is_some())) {
// A plain repaint runs only logic in a hidden window: the ui that applies injected
// input, shows a queued dialog or issues a capture needs this command. egui-winit
@@ -494,6 +500,8 @@ impl VectorcraftApp {
return;
}
crate::state::sync_zoom_stops_display_print_size(self.session.prefs.display_print_size);
// Whether this pass draws the last step of an action playing at Step by Step.
let step_held = crate::panels::actions::playback_held(self);
let t0 = now_ms();
widgets::set_dialog_open(&ctx, self.ui.dialog.is_some());
// The canvas records this pass's IME caret geometry; a pass that draws no canvas has none.
@@ -558,6 +566,10 @@ impl VectorcraftApp {
if !ctx.will_discard() {
self.perf.frame_ms = now_ms() - t0;
self.issue_screenshots(&ctx);
// A hidden window's pass shows nothing, so the step waits for a pass the user sees.
if step_held && !self.viewport_hidden(&ctx) {
crate::panels::actions::playback_drawn(self);
}
}
let _ = json!(null);
}
+2
View File
@@ -9,6 +9,7 @@
mod about;
pub mod action_batch;
pub mod action_new;
pub mod action_playback;
pub mod action_set_options;
pub mod artboard_options;
pub mod blend_options;
@@ -365,6 +366,7 @@ registry! {
ActionBatch: [action_batch::KIND] => action_batch::SPEC,
ActionSetOptions: [action_set_options::KIND] => action_set_options::SPEC,
ActionNew: [action_new::KIND] => action_new::SPEC,
ActionPlayback: [action_playback::KIND] => action_playback::SPEC,
SliceOptions: [slices::OPTIONS] => slices::OPTIONS_SPEC,
DivideSlices: [slices::DIVIDE] => slices::DIVIDE_SPEC,
EpsOptions: [eps_options::KIND] => eps_options::SPEC,
@@ -0,0 +1,48 @@
//! Actions panel menu › Playback Options…: the speed actions play at (`ui.action.playbackOptions`).
//!
//! Fields: `speed` (`accelerated`, `step` or `pause`) and `seconds`, the pause between steps with
//! `pause`. OK sets them; an invalid value is an error, and the dialog stays open.
use serde_json::{Value, json};
use super::DialogSpec;
use crate::state::Dialog;
use crate::{VectorcraftApp, widgets};
/// The dialog kind of Playback Options.
pub const KIND: &str = "actionPlayback";
pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| tl!("Playback Options").into(), body, confirm, min_width: 260.0, ..DialogSpec::FORM };
/// Open Playback Options with the speed and pause actions play with now.
pub fn open(app: &mut VectorcraftApp) {
let o = app.ui.action_playback;
app.ui.dialog = Some(Dialog::new(KIND, json!({"speed": o.speed, "seconds": o.seconds})));
}
fn body(_app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
let speed = d.str("speed");
for (value, label) in [("accelerated", tl!("Accelerated")), ("step", tl!("Step by Step"))] {
if widgets::radio(ui, label, speed == value, true) {
d.fields.insert("speed".into(), json!(value));
}
}
ui.horizontal(|ui| {
let pause = tl!("Pause For");
if widgets::radio(ui, pause, speed == "pause", true) {
d.fields.insert("speed".into(), json!("pause"));
}
let range = f64::from(*crate::panels::actions::PAUSE_SECONDS.start())..=f64::from(*crate::panels::actions::PAUSE_SECONDS.end());
if let Some(n) = widgets::range_field_named(ui, "action-pause-seconds", Some(pause), d.f64("seconds", *range.start()), range, "s", 0, 50.0) {
d.fields.insert("seconds".into(), json!(n as u64));
}
});
false
}
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
let params = json!({"speed": d.str("speed"), "seconds": d.fields.get("seconds").cloned().unwrap_or(Value::Null)});
let r = app.run(crate::panels::actions::PLAYBACK_OPTIONS, params)?;
app.ui.dialog = None;
Ok(r)
}
+9
View File
@@ -5502,6 +5502,10 @@ theme Appearance Външен вид
Movie Clip Филмов клип
@msg name is empty името е празно
@msg an action is already recording вече се записва действие
@msg an action is already playing вече се възпроизвежда действие
@msg the action stopped playing: its document is no longer active действието спря: документът му вече не е активен
@msg speed must be accelerated, step or pause speed трябва да е accelerated, step или pause
@msg seconds must be a whole number from 1 to 60 seconds трябва да е цяло число от 1 до 60
@msg no such function key няма такъв функционален клавиш
@msg no such color няма такъв цвят
@msg {_1} cannot be used on Windows {_1} не може да се използва в Windows
@@ -5652,6 +5656,11 @@ axis Y Axis Y ос
Set Options Опции на набора
Action Options… Опции на действието…
Action Options Опции на действието
Playback Options… Опции за възпроизвеждане…
Playback Options Опции за възпроизвеждане
Accelerated Ускорено
Step by Step Стъпка по стъпка
Pause For Пауза за
Rename Action Преименуване на действие
New Action… Ново действие…
New Action Ново действие
Can't render this file because it contains an unexpected character in line 2 and column 73.
+9
View File
@@ -5506,6 +5506,10 @@ mark-style Japanese Japanski
Movie Clip Filmski klip
@msg name is empty naziv je prazan
@msg an action is already recording akcija se već snima
@msg an action is already playing akcija se već reproducira
@msg the action stopped playing: its document is no longer active reprodukcija akcije je zaustavljena: njen dokument više nije aktivan
@msg speed must be accelerated, step or pause speed mora biti accelerated, step ili pause
@msg seconds must be a whole number from 1 to 60 seconds mora biti cijeli broj od 1 do 60
@msg no such function key nema takve funkcijske tipke
@msg no such color nema takve boje
@msg {_1} cannot be used on Windows {_1} se ne može koristiti na Windowsu
@@ -5675,6 +5679,11 @@ axis Y Axis Y osa
Set Options Opcije skupa
Action Options… Opcije akcije…
Action Options Opcije akcije
Playback Options… Opcije reprodukcije…
Playback Options Opcije reprodukcije
Accelerated Ubrzano
Step by Step Korak po korak
Pause For Pauza od
Rename Action Preimenuj akciju
New Action… Nova akcija…
New Action Nova akcija
Can't render this file because it contains an unexpected character in line 2 and column 73.
+9
View File
@@ -3791,6 +3791,11 @@ digits Malayalam Malaiàlam
Set Options Defineix les opcions
Action Options… Opcions de l’acció…
Action Options Opcions de l’acció
Playback Options… Opcions de reproducció…
Playback Options Opcions de reproducció
Accelerated Accelerada
Step by Step Pas a pas
Pause For Pausa de
Rename Action Canvia el nom de l’acció
New Action… Acció nova…
New Action Acció nova
@@ -5187,6 +5192,10 @@ digits Malayalam Malaiàlam
@msg Multichannel PSD files can't be read: save it in RGB, CMYK or Grayscale mode Els fitxers PSD multicanal no es poden llegir: deseu-los en mode RGB, CMYK o escala de grisos
@msg name is empty el nom és buit
@msg an action is already recording ja s’està enregistrant una acció
@msg an action is already playing ja s’està reproduint una acció
@msg the action stopped playing: its document is no longer active l’acció s’ha aturat: el seu document ja no és l’actiu
@msg speed must be accelerated, step or pause speed ha de ser accelerated, step o pause
@msg seconds must be a whole number from 1 to 60 seconds ha de ser un nombre enter de l’1 al 60
@msg no such function key no existeix aquesta tecla de funció
@msg no such color no existeix aquest color
@msg {_1} cannot be used on Windows {_1} no es pot fer servir al Windows
Can't render this file because it contains an unexpected character in line 2 and column 73.
+9
View File
@@ -5592,6 +5592,10 @@ mark-style Japanese Japonské
Movie Clip Filmový klip
@msg name is empty název je prázdný
@msg an action is already recording akce se už zaznamenává
@msg an action is already playing akce se už přehrává
@msg the action stopped playing: its document is no longer active přehrávání akce se zastavilo: její dokument už není aktivní
@msg speed must be accelerated, step or pause speed musí být accelerated, step nebo pause
@msg seconds must be a whole number from 1 to 60 seconds musí být celé číslo od 1 do 60
@msg no such function key taková funkční klávesa neexistuje
@msg no such color taková barva neexistuje
@msg {_1} cannot be used on Windows Zkratku {_1} nelze ve Windows použít
@@ -5766,6 +5770,11 @@ axis Y Axis osa Y
Set Options Možnosti sady
Action Options… Možnosti akce…
Action Options Možnosti akce
Playback Options… Možnosti přehrávání…
Playback Options Možnosti přehrávání
Accelerated Zrychleně
Step by Step Krok za krokem
Pause For Pozastavit na
Rename Action Přejmenovat akci
New Action… Nová akce…
New Action Nová akce
Can't render this file because it contains an unexpected character in line 2 and column 73.
+9
View File
@@ -5387,6 +5387,10 @@ swatch Bright Violet Kraftig violet
Movie Clip Filmklip
@msg name is empty navnet er tomt
@msg an action is already recording en handling optages allerede
@msg an action is already playing en handling afspilles allerede
@msg the action stopped playing: its document is no longer active handlingen stoppede: dens dokument er ikke længere aktivt
@msg speed must be accelerated, step or pause speed skal være accelerated, step eller pause
@msg seconds must be a whole number from 1 to 60 seconds skal være et heltal fra 1 til 60
@msg no such function key ingen sådan funktionstast
@msg no such color ingen sådan farve
@msg {_1} cannot be used on Windows {_1} kan ikke bruges i Windows
@@ -5669,6 +5673,11 @@ axis Y Axis Y-akse
Set Options Indstillinger for sæt
Action Options… Indstillinger for handling…
Action Options Indstillinger for handling
Playback Options… Afspilningsindstillinger…
Playback Options Afspilningsindstillinger
Accelerated Accelereret
Step by Step Trin for trin
Pause For Pause på
Rename Action Omdøb handling
New Action… Ny handling…
New Action Ny handling
Can't render this file because it contains an unexpected character in line 2 and column 73.
+9
View File
@@ -5170,6 +5170,10 @@ effect Soft Weich
Movie Clip Movieclip
@msg name is empty Name ist leer
@msg an action is already recording eine Aktion wird bereits aufgezeichnet
@msg an action is already playing eine Aktion wird bereits abgespielt
@msg the action stopped playing: its document is no longer active die Aktion wurde angehalten: ihr Dokument ist nicht mehr aktiv
@msg speed must be accelerated, step or pause speed muss accelerated, step oder pause sein
@msg seconds must be a whole number from 1 to 60 seconds muss eine ganze Zahl von 1 bis 60 sein
@msg no such function key keine solche Funktionstaste
@msg no such color keine solche Farbe
@msg {_1} cannot be used on Windows {_1} kann unter Windows nicht verwendet werden
@@ -5348,6 +5352,11 @@ axis Y Axis Y-Achse
Set Options Satzoptionen
Action Options… Aktionsoptionen…
Action Options Aktionsoptionen
Playback Options… Wiedergabeoptionen…
Playback Options Wiedergabeoptionen
Accelerated Beschleunigt
Step by Step Schrittweise
Pause For Pause von
Rename Action Aktion umbenennen
New Action… Neue Aktion…
New Action Neue Aktion
Can't render this file because it contains an unexpected character in line 2 and column 73.
+9
View File
@@ -5543,6 +5543,10 @@ effect Points Σημεία
Movie Clip Κλιπ ταινίας
@msg name is empty το όνομα είναι κενό
@msg an action is already recording μια ενέργεια καταγράφεται ήδη
@msg an action is already playing μια ενέργεια αναπαράγεται ήδη
@msg the action stopped playing: its document is no longer active η αναπαραγωγή της ενέργειας σταμάτησε: το έγγραφό της δεν είναι πλέον ενεργό
@msg speed must be accelerated, step or pause το speed πρέπει να είναι accelerated, step ή pause
@msg seconds must be a whole number from 1 to 60 το seconds πρέπει να είναι ακέραιος από 1 έως 60
@msg no such function key δεν υπάρχει τέτοιο πλήκτρο λειτουργίας
@msg no such color δεν υπάρχει τέτοιο χρώμα
@msg {_1} cannot be used on Windows Το {_1} δεν μπορεί να χρησιμοποιηθεί στα Windows
@@ -5717,6 +5721,11 @@ axis Y Axis Άξονας Y
Set Options Επιλογές συνόλου
Action Options… Επιλογές ενέργειας…
Action Options Επιλογές ενέργειας
Playback Options… Επιλογές αναπαραγωγής…
Playback Options Επιλογές αναπαραγωγής
Accelerated Επιταχυμένα
Step by Step Βήμα προς βήμα
Pause For Παύση για
Rename Action Μετονομασία ενέργειας
New Action… Νέα ενέργεια…
New Action Νέα ενέργεια
Can't render this file because it contains an unexpected character in line 163 and column 5.
+9
View File
@@ -5533,6 +5533,10 @@ mark-style Japanese Japonesas
Movie Clip Clip de película
@msg name is empty el nombre está vacío
@msg an action is already recording ya se está grabando una acción
@msg an action is already playing ya se está reproduciendo una acción
@msg the action stopped playing: its document is no longer active la acción se detuvo: su documento ya no es el activo
@msg speed must be accelerated, step or pause speed debe ser accelerated, step o pause
@msg seconds must be a whole number from 1 to 60 seconds debe ser un número entero de 1 a 60
@msg no such function key no existe esa tecla de función
@msg no such color no existe ese color
@msg {_1} cannot be used on Windows {_1} no se puede usar en Windows
@@ -5707,6 +5711,11 @@ axis Y Axis Eje Y
Set Options Opciones de conjunto
Action Options… Opciones de acción…
Action Options Opciones de acción
Playback Options… Opciones de reproducción…
Playback Options Opciones de reproducción
Accelerated Acelerada
Step by Step Paso a paso
Pause For Pausa de
Rename Action Cambiar nombre de acción
New Action… Nueva acción…
New Action Nueva acción
Can't render this file because it contains an unexpected character in line 2 and column 73.
+9
View File
@@ -5555,6 +5555,10 @@ mark-style Japanese Japanilainen
Movie Clip Elokuvaleike
@msg name is empty nimi on tyhjä
@msg an action is already recording toimintoa nauhoitetaan jo
@msg an action is already playing toimintoa toistetaan jo
@msg the action stopped playing: its document is no longer active toiminnon toisto pysähtyi: sen asiakirja ei ole enää aktiivinen
@msg speed must be accelerated, step or pause speed on oltava accelerated, step tai pause
@msg seconds must be a whole number from 1 to 60 seconds on oltava kokonaisluku 1:stä 60:een
@msg no such function key toimintonäppäintä ei ole
@msg no such color väriä ei ole
@msg {_1} cannot be used on Windows {_1} ei ole käytettävissä Windowsissa
@@ -5796,6 +5800,11 @@ axis Y Axis Y-akseli
Set Options Sarjan asetukset
Action Options… Toiminnon asetukset…
Action Options Toiminnon asetukset
Playback Options… Toistoasetukset…
Playback Options Toistoasetukset
Accelerated Nopeutettu
Step by Step Vaihe kerrallaan
Pause For Tauko
Rename Action Nimeä toiminto uudelleen
New Action… Uusi toiminto…
New Action Uusi toiminto
Can't render this file because it contains an unexpected character in line 2 and column 73.
+9
View File
@@ -5587,6 +5587,10 @@ theme Appearance Apparence
Movie Clip Clip
@msg name is empty le nom est vide
@msg an action is already recording un script est déjà en cours d’enregistrement
@msg an action is already playing un script est déjà en cours d’exécution
@msg the action stopped playing: its document is no longer active le script s’est arrêté : son document n’est plus le document actif
@msg speed must be accelerated, step or pause speed doit valoir accelerated, step ou pause
@msg seconds must be a whole number from 1 to 60 seconds doit être un nombre entier de 1 à 60
@msg no such function key touche de fonction inexistante
@msg no such color couleur inexistante
@msg {_1} cannot be used on Windows {_1} ne peut pas être utilisé sous Windows
@@ -5761,6 +5765,11 @@ axis Y Axis Axe Y
Set Options Options du jeu
Action Options… Options de script…
Action Options Options de script
Playback Options… Options d’exécution…
Playback Options Options d’exécution
Accelerated Accélérée
Step by Step Pas à pas
Pause For Pause de
Rename Action Renommer le script
New Action… Nouveau script…
New Action Nouveau script
Can't render this file because it contains an unexpected character in line 2 and column 73.
+9
View File
@@ -5575,6 +5575,10 @@ imageTrace Outline Körvonal
Movie Clip Mozgóklip
@msg name is empty a név üres
@msg an action is already recording már folyik egy művelet felvétele
@msg an action is already playing már folyik egy művelet lejátszása
@msg the action stopped playing: its document is no longer active a művelet lejátszása leállt: a dokumentuma már nem az aktív
@msg speed must be accelerated, step or pause a speed értéke accelerated, step vagy pause lehet
@msg seconds must be a whole number from 1 to 60 a seconds értéke 1 és 60 közötti egész szám lehet
@msg no such function key nincs ilyen funkcióbillentyű
@msg no such color nincs ilyen szín
@msg {_1} cannot be used on Windows A(z) {_1} nem használható Windows rendszeren
@@ -5749,6 +5753,11 @@ axis Y Axis Y tengely
Set Options Készlet beállításai
Action Options… Művelet beállításai…
Action Options Művelet beállításai
Playback Options… Lejátszási beállítások…
Playback Options Lejátszási beállítások
Accelerated Gyorsított
Step by Step Lépésenként
Pause For Szünet
Rename Action Művelet átnevezése
New Action… Új művelet…
New Action Új művelet
Can't render this file because it contains an unexpected character in line 2 and column 73.
+9
View File
@@ -5387,6 +5387,10 @@ swatch Bright Violet Violet cerah
Movie Clip Klip film
@msg name is empty nama kosong
@msg an action is already recording sebuah tindakan sedang direkam
@msg an action is already playing sebuah tindakan sedang diputar
@msg the action stopped playing: its document is no longer active tindakan berhenti diputar: dokumennya tidak lagi aktif
@msg speed must be accelerated, step or pause speed harus accelerated, step, atau pause
@msg seconds must be a whole number from 1 to 60 seconds harus bilangan bulat dari 1 sampai 60
@msg no such function key tidak ada tombol fungsi seperti itu
@msg no such color tidak ada warna seperti itu
@msg {_1} cannot be used on Windows {_1} tidak dapat digunakan di Windows
@@ -5669,6 +5673,11 @@ axis Y Axis Sumbu Y
Set Options Opsi set
Action Options… Opsi tindakan…
Action Options Opsi tindakan
Playback Options… Opsi pemutaran…
Playback Options Opsi pemutaran
Accelerated Dipercepat
Step by Step Langkah demi langkah
Pause For Jeda selama
Rename Action Ganti nama tindakan
New Action… Tindakan baru…
New Action Tindakan baru
Can't render this file because it contains an unexpected character in line 2230 and column 5.
+9
View File
@@ -5205,6 +5205,10 @@ lighting Light Luce
Movie Clip Clip filmato
@msg name is empty il nome è vuoto
@msg an action is already recording è già in corso la registrazione di un’azione
@msg an action is already playing è già in corso la riproduzione di un’azione
@msg the action stopped playing: its document is no longer active la riproduzione dell’azione si è interrotta: il suo documento non è più attivo
@msg speed must be accelerated, step or pause speed deve essere accelerated, step o pause
@msg seconds must be a whole number from 1 to 60 seconds deve essere un numero intero da 1 a 60
@msg no such function key tasto funzione inesistente
@msg no such color colore inesistente
@msg {_1} cannot be used on Windows {_1} non può essere usato su Windows
@@ -5382,6 +5386,11 @@ axis Y Axis Asse Y
Set Options Opzioni set
Action Options… Opzioni azione…
Action Options Opzioni azione
Playback Options… Opzioni di riproduzione…
Playback Options Opzioni di riproduzione
Accelerated Accelerata
Step by Step Passo per passo
Pause For Pausa di
Rename Action Rinomina azione
New Action… Nuova azione…
New Action Nuova azione
Can't render this file because it contains an unexpected character in line 2 and column 73.
+9
View File
@@ -5248,6 +5248,10 @@ lighting Light ライト
Movie Clip ムービークリップ
@msg name is empty 名前が空です
@msg an action is already recording すでにアクションを記録中です
@msg an action is already playing すでにアクションを再生中です
@msg the action stopped playing: its document is no longer active アクションの再生を停止しました: そのドキュメントはアクティブではなくなりました
@msg speed must be accelerated, step or pause speed には accelerated、step または pause を指定してください
@msg seconds must be a whole number from 1 to 60 seconds には 1 から 60 までの整数を指定してください
@msg no such function key 指定されたファンクションキーが見つかりません
@msg no such color 指定されたカラーが見つかりません
@msg {_1} cannot be used on Windows {_1} は Windows では使用できません
@@ -5447,6 +5451,11 @@ axis Y Axis Y 軸
Set Options セットオプション
Action Options… アクションオプション…
Action Options アクションオプション
Playback Options… 再生オプション…
Playback Options 再生オプション
Accelerated 高速
Step by Step ステップ実行
Pause For 一時停止
Rename Action アクション名を変更
New Action… 新規アクション…
New Action 新規アクション
Can't render this file because it contains an unexpected character in line 2 and column 73.
+9
View File
@@ -5300,6 +5300,10 @@ imageTrace Outline კონტური
Movie Clip ანიმაციური კლიპი
@msg name is empty სახელი ცარიელია
@msg an action is already recording მოქმედება უკვე იწერება
@msg an action is already playing მოქმედება უკვე სრულდება
@msg the action stopped playing: its document is no longer active მოქმედება შეჩერდა: მისი დოკუმენტი აღარ არის აქტიური
@msg speed must be accelerated, step or pause speed უნდა იყოს accelerated, step ან pause
@msg seconds must be a whole number from 1 to 60 seconds უნდა იყოს მთელი რიცხვი 1-დან 60-მდე
@msg no such function key ასეთი ფუნქციური კლავიში არ არის
@msg no such color ასეთი ფერი არ არის
@msg {_1} cannot be used on Windows {_1} ვერ გამოიყენება Windows-ში
@@ -5688,6 +5692,11 @@ axis Y Axis Y ღერძი
Set Options ნაკრების პარამეტრები
Action Options… მოქმედების პარამეტრები…
Action Options მოქმედების პარამეტრები
Playback Options… შესრულების პარამეტრები…
Playback Options შესრულების პარამეტრები
Accelerated დაჩქარებული
Step by Step ნაბიჯ-ნაბიჯ
Pause For პაუზა
Rename Action მოქმედების სახელის შეცვლა
New Action… ახალი მოქმედება…
New Action ახალი მოქმედება
Can't render this file because it is too large.
+9
View File
@@ -5166,6 +5166,10 @@ lighting Light 광원
Movie Clip 무비 클립
@msg name is empty 이름이 비어 있습니다
@msg an action is already recording 이미 액션을 기록하고 있습니다
@msg an action is already playing 이미 액션을 재생하고 있습니다
@msg the action stopped playing: its document is no longer active 액션 재생이 중지되었습니다: 해당 문서가 더 이상 활성 상태가 아닙니다
@msg speed must be accelerated, step or pause speed는 accelerated, step 또는 pause여야 합니다
@msg seconds must be a whole number from 1 to 60 seconds는 1에서 60 사이의 정수여야 합니다
@msg no such function key 해당 기능 키가 없습니다
@msg no such color 해당 색상이 없습니다
@msg {_1} cannot be used on Windows {_1}은(는) Windows에서 사용할 수 없습니다
@@ -5343,6 +5347,11 @@ axis Y Axis Y축
Set Options 세트 옵션
Action Options… 액션 옵션…
Action Options 액션 옵션
Playback Options… 재생 옵션…
Playback Options 재생 옵션
Accelerated 가속
Step by Step 단계별
Pause For 일시 정지
Rename Action 액션 이름 바꾸기
New Action… 새 액션…
New Action 새 액션
Can't render this file because it contains an unexpected character in line 2 and column 73.
+9
View File
@@ -5395,6 +5395,10 @@ swatch Bright Violet Felviolet
Movie Clip Filmclip
@msg name is empty naam is leeg
@msg an action is already recording er wordt al een actie opgenomen
@msg an action is already playing er wordt al een actie afgespeeld
@msg the action stopped playing: its document is no longer active de actie is gestopt: het document ervan is niet meer actief
@msg speed must be accelerated, step or pause speed moet accelerated, step of pause zijn
@msg seconds must be a whole number from 1 to 60 seconds moet een geheel getal van 1 tot 60 zijn
@msg no such function key functietoets bestaat niet
@msg no such color kleur bestaat niet
@msg {_1} cannot be used on Windows {_1} kan niet worden gebruikt in Windows
@@ -5713,6 +5717,11 @@ axis Y Axis Y-as
Set Options Setopties
Action Options… Actieopties…
Action Options Actieopties
Playback Options… Afspeelopties…
Playback Options Afspeelopties
Accelerated Versneld
Step by Step Stap voor stap
Pause For Pauzeren
Rename Action Actie hernoemen
New Action… Nieuwe actie…
New Action Nieuwe actie
Can't render this file because it contains an unexpected character in line 2 and column 73.
+9
View File
@@ -5445,6 +5445,10 @@ mark-style Japanese Japońskie
Movie Clip Klip filmowy
@msg name is empty nazwa jest pusta
@msg an action is already recording operacja jest już nagrywana
@msg an action is already playing operacja jest już odtwarzana
@msg the action stopped playing: its document is no longer active odtwarzanie operacji zatrzymano: jej dokument nie jest już aktywny
@msg speed must be accelerated, step or pause speed musi mieć wartość accelerated, step lub pause
@msg seconds must be a whole number from 1 to 60 seconds musi być liczbą całkowitą od 1 do 60
@msg no such function key brak takiego klawisza funkcyjnego
@msg no such color brak takiego koloru
@msg {_1} cannot be used on Windows Skrótu {_1} nie można używać w systemie Windows
@@ -5619,6 +5623,11 @@ axis Y Axis Oś Y
Set Options Opcje zestawu
Action Options… Opcje operacji…
Action Options Opcje operacji
Playback Options… Opcje odtwarzania…
Playback Options Opcje odtwarzania
Accelerated Przyspieszone
Step by Step Krok po kroku
Pause For Pauza
Rename Action Zmień nazwę operacji
New Action… Nowa operacja…
New Action Nowa operacja
Can't render this file because it contains an unexpected character in line 2 and column 73.
+9
View File
@@ -5507,6 +5507,10 @@ mark-style Japanese Japonesas
# Symbol Options › Export Type
@msg name is empty o nome está vazio
@msg an action is already recording uma ação já está sendo gravada
@msg an action is already playing uma ação já está sendo reproduzida
@msg the action stopped playing: its document is no longer active a ação parou: o documento dela não está mais ativo
@msg speed must be accelerated, step or pause speed deve ser accelerated, step ou pause
@msg seconds must be a whole number from 1 to 60 seconds deve ser um número inteiro de 1 a 60
@msg no such function key tecla de função inexistente
@msg no such color cor inexistente
@msg {_1} cannot be used on Windows {_1} não pode ser usado no Windows
@@ -5681,6 +5685,11 @@ axis Y Axis Eixo Y
Set Options Opções do Conjunto
Action Options… Opções da Ação…
Action Options Opções da Ação
Playback Options… Opções de Reprodução…
Playback Options Opções de Reprodução
Accelerated Acelerada
Step by Step Passo a Passo
Pause For Pausar Por
Rename Action Renomear Ação
New Action… Nova Ação…
New Action Nova Ação
Can't render this file because it contains an unexpected character in line 6 and column 47.
+9
View File
@@ -5200,6 +5200,10 @@ lighting Light Свет
Movie Clip Видеоклип
@msg name is empty имя пустое
@msg an action is already recording действие уже записывается
@msg an action is already playing действие уже воспроизводится
@msg the action stopped playing: its document is no longer active воспроизведение действия остановлено: его документ больше не активен
@msg speed must be accelerated, step or pause speed должен быть accelerated, step или pause
@msg seconds must be a whole number from 1 to 60 seconds должно быть целым числом от 1 до 60
@msg no such function key такой функциональной клавиши нет
@msg no such color такого цвета нет
@msg {_1} cannot be used on Windows Сочетание {_1} нельзя использовать в Windows
@@ -5377,6 +5381,11 @@ axis Y Axis Ось Y
Set Options Параметры набора
Action Options… Параметры действия…
Action Options Параметры действия
Playback Options… Параметры воспроизведения…
Playback Options Параметры воспроизведения
Accelerated Ускоренно
Step by Step Пошагово
Pause For Пауза
Rename Action Переименовать действие
New Action… Новое действие…
New Action Новое действие
Can't render this file because it contains an unexpected character in line 2 and column 73.
+9
View File
@@ -5547,6 +5547,10 @@ theme Appearance Utseende
Movie Clip Filmklipp
@msg name is empty namnet är tomt
@msg an action is already recording en åtgärd spelas redan in
@msg an action is already playing en åtgärd spelas redan upp
@msg the action stopped playing: its document is no longer active åtgärden stoppades: dess dokument är inte längre aktivt
@msg speed must be accelerated, step or pause speed måste vara accelerated, step eller pause
@msg seconds must be a whole number from 1 to 60 seconds måste vara ett heltal från 1 till 60
@msg no such function key funktionstangenten finns inte
@msg no such color färgen finns inte
@msg {_1} cannot be used on Windows {_1} kan inte användas i Windows
@@ -5789,6 +5793,11 @@ axis Y Axis Y-axel
Set Options Alternativ för uppsättning
Action Options… Alternativ för åtgärd…
Action Options Alternativ för åtgärd
Playback Options… Uppspelningsalternativ…
Playback Options Uppspelningsalternativ
Accelerated Accelererad
Step by Step Steg för steg
Pause For Paus i
Rename Action Byt namn på åtgärd
New Action… Ny åtgärd…
New Action Ny åtgärd
Can't render this file because it contains an unexpected character in line 5 and column 25.
+9
View File
@@ -5256,6 +5256,10 @@ theme Appearance Görünüm
Movie Clip Film Klibi
@msg name is empty isim boş
@msg an action is already recording zaten bir eylem kaydediliyor
@msg an action is already playing zaten bir eylem oynatılıyor
@msg the action stopped playing: its document is no longer active eylemin oynatılması durdu: belgesi artık etkin değil
@msg speed must be accelerated, step or pause speed accelerated, step veya pause olmalıdır
@msg seconds must be a whole number from 1 to 60 seconds 1 ile 60 arasında bir tam sayı olmalıdır
@msg no such function key öyle bir işlev tuşu yok
@msg no such color öyle bir renk yok
@msg {_1} cannot be used on Windows {_1} Windows'ta kullanılamaz
@@ -5723,6 +5727,11 @@ axis Y Axis Y ekseni
Set Options Set Seçenekleri
Action Options… Eylem Seçenekleri…
Action Options Eylem Seçenekleri
Playback Options… Oynatma Seçenekleri…
Playback Options Oynatma Seçenekleri
Accelerated Hızlandırılmış
Step by Step Adım Adım
Pause For Duraklat
Rename Action Eylemi Yeniden Adlandır
New Action… Yeni Eylem…
New Action Yeni Eylem
Can't render this file because it contains an unexpected character in line 5 and column 25.
+9
View File
@@ -5185,6 +5185,10 @@ lighting Light Світло
Movie Clip Кліп
@msg name is empty ім’я порожнє
@msg an action is already recording дія вже записується
@msg an action is already playing дія вже відтворюється
@msg the action stopped playing: its document is no longer active відтворення дії зупинено: її документ більше не активний
@msg speed must be accelerated, step or pause speed має бути accelerated, step або pause
@msg seconds must be a whole number from 1 to 60 seconds має бути цілим числом від 1 до 60
@msg no such function key такої функціональної клавіші немає
@msg no such color такого кольору немає
@msg {_1} cannot be used on Windows Сполучення {_1} не можна використовувати у Windows
@@ -5362,6 +5366,11 @@ axis Y Axis Вісь Y
Set Options Параметри набору
Action Options… Параметри дії…
Action Options Параметри дії
Playback Options… Параметри відтворення…
Playback Options Параметри відтворення
Accelerated Прискорено
Step by Step Покроково
Pause For Пауза
Rename Action Перейменувати дію
New Action… Нова дія…
New Action Нова дія
Can't render this file because it contains an unexpected character in line 177 and column 5.
+9
View File
@@ -5186,6 +5186,10 @@ theme Appearance Giao diện
Movie Clip Clip phim
@msg name is empty tên trống
@msg an action is already recording đang ghi một hành động
@msg an action is already playing đang phát một hành động
@msg the action stopped playing: its document is no longer active hành động đã dừng phát: tài liệu của nó không còn hoạt động
@msg speed must be accelerated, step or pause speed phải là accelerated, step hoặc pause
@msg seconds must be a whole number from 1 to 60 seconds phải là số nguyên từ 1 đến 60
@msg no such function key không có phím chức năng như vậy
@msg no such color không có màu như vậy
@msg {_1} cannot be used on Windows không thể dùng {_1} trên Windows
@@ -5363,6 +5367,11 @@ axis Y Axis Trục Y
Set Options Tùy chọn bộ
Action Options… Tùy chọn hành động…
Action Options Tùy chọn hành động
Playback Options… Tùy chọn phát lại…
Playback Options Tùy chọn phát lại
Accelerated Tăng tốc
Step by Step Từng bước
Pause For Tạm dừng
Rename Action Đổi tên hành động
New Action… Hành động mới…
New Action Hành động mới
Can't render this file because it contains an unexpected character in line 199 and column 5.
+9
View File
@@ -5425,6 +5425,10 @@ lighting Light 光源
Movie Clip 影片剪辑
@msg name is empty 名称为空
@msg an action is already recording 已在记录动作
@msg an action is already playing 已在播放动作
@msg the action stopped playing: its document is no longer active 动作已停止播放:其文档不再是现用文档
@msg speed must be accelerated, step or pause speed 必须为 accelerated、step 或 pause
@msg seconds must be a whole number from 1 to 60 seconds 必须为 1 到 60 之间的整数
@msg no such function key 没有此功能键
@msg no such color 没有此颜色
@msg {_1} cannot be used on Windows {_1} 在 Windows 上不能使用
@@ -5604,6 +5608,11 @@ axis Y Axis Y 轴
Set Options 组选项
Action Options… 动作选项…
Action Options 动作选项
Playback Options… 播放选项…
Playback Options 播放选项
Accelerated 加速
Step by Step 逐步
Pause For 暂停
Rename Action 重命名动作
New Action… 新建动作…
New Action 新建动作
Can't render this file because it contains an unexpected character in line 2 and column 73.
+9
View File
@@ -5243,6 +5243,10 @@ lighting Light 光源
Movie Clip 影片片段
@msg name is empty 名稱為空
@msg an action is already recording 已在錄製動作
@msg an action is already playing 已在播放動作
@msg the action stopped playing: its document is no longer active 動作已停止播放:其文件不再是作用中文件
@msg speed must be accelerated, step or pause speed 必須為 accelerated、step 或 pause
@msg seconds must be a whole number from 1 to 60 seconds 必須為 1 到 60 之間的整數
@msg no such function key 沒有這個功能鍵
@msg no such color 沒有這個顏色
@msg {_1} cannot be used on Windows {_1} 無法在 Windows 上使用
@@ -5400,6 +5404,11 @@ axis Y Axis Y 軸
Set Options 組選項
Action Options… 動作選項…
Action Options 動作選項
Playback Options… 播放選項…
Playback Options 播放選項
Accelerated 加速
Step by Step 逐步
Pause For 暫停
Rename Action 重新命名動作
New Action… 新增動作…
New Action 新增動作
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -826,6 +826,7 @@ fn run_ui_part11(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<Result
crate::panels::actions::NEW_ACTION => crate::panels::actions::new_action(app, p),
crate::panels::actions::ACTION_OPTIONS => crate::panels::actions::action_options(app, p),
crate::panels::actions::PLAY_ACTION => crate::panels::actions::play_action(app, p),
crate::panels::actions::PLAYBACK_OPTIONS => crate::panels::actions::playback_options(app, p),
crate::panels::actions::START_RECORDING => crate::panels::actions::record(app, p),
crate::panels::actions::STOP_RECORDING => crate::panels::actions::stop(app, p),
"ui.batchDialog" => {
+8 -2
View File
@@ -1239,7 +1239,13 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
crate::panels::actions::PLAY_ACTION,
"Play Action",
"",
"{action: name, set?: name} play the Actions panel's action named action as one undoable batch; set names the set to look in (default: every set). A name no action has, or one that more than one action in the sets looked in has, is an error and nothing plays → what command.batch returns",
"{action: name, set?: name} play the Actions panel's action named action at the speed ui.action.playbackOptions sets; set names the set to look in (default: every set). Accelerated plays it as one undoable batch → what command.batch returns. Step by Step and Pause For play its first step now and the others one at a time, each its own undo step, until the last, ui.action.stop, or a step that comes due while a different document is active than after the step before → {results: [the first step's result], pending: the steps left}. A name no action has, or one that more than one action in the sets looked in has, or an action played while another is still playing step by step or with pauses, is an error and nothing plays",
),
(
crate::panels::actions::PLAYBACK_OPTIONS,
"Playback Options",
"",
"{speed?: accelerated|step|pause, seconds?: 1-60} set how the Actions panel plays actions: accelerated (the default) plays every step at once, step plays one step at a time and draws the window after each, and pause waits seconds (a whole number, 1 to 60, VectorCraft's own range) between steps. A param left out keeps its value; the setting is kept with the UI preferences. Any other speed or seconds is an error and changes nothing. The panel menu's Playback Options… (dialog `actionPlayback` with speed and seconds) runs this on OK → {speed, seconds}",
),
(
crate::panels::actions::START_RECORDING,
@@ -1251,7 +1257,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
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. With nothing recording 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: the steps played stay and the rest do not run. With nothing recording or playing it changes nothing → {stopped}",
),
(
"ui.batchDialog",
+619 -10
View File
@@ -61,12 +61,18 @@ impl Action {
}
}
/// The included steps as `command.batch` takes them. A recorded `command.batch` step (an action
/// played, or a panel operation that runs as one batch, while recording) gives its own steps,
/// since batches do not nest.
pub(crate) fn included_commands(&self) -> Vec<Value> {
self.steps
.iter()
.enumerate()
.filter(|(i, _)| !self.excluded_steps.contains(i))
.map(|(_, (command, params))| json!({"command": command, "params": params}))
.flat_map(|(_, (command, params))| match params.get("commands").and_then(Value::as_array) {
Some(steps) if command == "command.batch" => steps.clone(),
_ => vec![json!({"command": command, "params": params})],
})
.collect()
}
}
@@ -203,14 +209,212 @@ pub fn default_sets() -> Vec<ActionSet> {
)]
}
/// Run an action as a single undoable batch.
/// Plays an action at the speed [`UiState::action_playback`](crate::UiState::action_playback) sets.
/// Accelerated runs every included step at once as a single undoable batch → what `command.batch`
/// returns. Step by Step and Pause For run the first step now, each as its own undo step, and leave
/// the rest to [`tick_playback`] → {results: [the first step's result], pending: the steps left}.
/// While a paced playback is in progress, another action does not play.
pub fn play(app: &mut VectorcraftApp, set: usize, idx: usize) -> Result<Value, String> {
let Some(a) = app.ui.action_sets.get(set).and_then(|s| s.actions.get(idx)).cloned() else { return Err("no such action".into()) };
let commands = a.included_commands();
if commands.is_empty() {
return Err("the action has no included steps".into());
}
app.run("command.batch", json!({"label": a.name, "commands": commands}))
if app.ui.action_playing.is_some() {
return Err("an action is already playing".into());
}
let options = app.ui.action_playback;
if options.speed == PlaybackSpeed::Accelerated {
return app.run("command.batch", json!({"label": a.name, "commands": commands}));
}
let mut steps: std::collections::VecDeque<Value> = commands.into();
let Some(first) = steps.pop_front() else { return Err("the action has no included steps".into()) };
let result = play_step(app, &a.name, first)?;
let pending = steps.len();
if pending > 0 {
let hold = options.speed == PlaybackSpeed::Step;
let doc = active_doc(app);
let played_in = app.tick_iteration.filter(|&t| t == app.host_iteration);
app.ui.action_playing = Some(Playing { label: a.name, steps, options, next_at: None, hold, doc, played_in });
if let Some(ctx) = &app.ui_ctx {
ctx.request_repaint();
}
}
Ok(json!({"results": [result], "pending": pending}))
}
/// Runs one step of a paced playback as its own undo step, named `label` → the step's result.
fn play_step(app: &mut VectorcraftApp, label: &str, step: Value) -> Result<Value, String> {
let r = app.run("command.batch", json!({"label": label, "commands": [step]}))?;
Ok(r.get("results").and_then(|r| r.get(0)).cloned().unwrap_or(Value::Null))
}
/// The speed actions play at (Actions panel menu › Playback Options…).
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum PlaybackSpeed {
/// Every included step at once.
#[default]
Accelerated,
/// One step at a time, with the window drawn after each step before the next step runs.
Step,
/// [`PlaybackOptions::seconds`] between steps.
Pause,
}
/// The pause between steps that Pause For takes, in whole seconds: VectorCraft's own range.
pub const PAUSE_SECONDS: std::ops::RangeInclusive<u32> = 1..=60;
/// How actions play, kept with the UI preferences.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PlaybackOptions {
pub speed: PlaybackSpeed,
/// The pause between steps with [`PlaybackSpeed::Pause`], within [`PAUSE_SECONDS`]. It is kept
/// while another speed is chosen.
pub seconds: u32,
}
impl Default for PlaybackOptions {
fn default() -> Self {
Self { speed: PlaybackSpeed::Accelerated, seconds: *PAUSE_SECONDS.start() }
}
}
/// Reads saved [`PlaybackOptions`]: a value that is not a valid one loads as the default.
pub fn deserialize_playback<'de, D: serde::Deserializer<'de>>(d: D) -> Result<PlaybackOptions, D::Error> {
let v = <Value as serde::Deserialize>::deserialize(d)?;
Ok(serde_json::from_value::<PlaybackOptions>(v).ok().filter(|o| PAUSE_SECONDS.contains(&o.seconds)).unwrap_or_default())
}
/// A paced playback in progress: the steps still to run.
#[derive(Clone, Debug)]
pub struct Playing {
/// The action's name, which names each step's undo step.
label: String,
steps: std::collections::VecDeque<Value>,
/// The options the action started playing with.
options: PlaybackOptions,
/// With Pause For, when the next step runs (seconds on the app's clock); none until the frame
/// after each step, which starts the pause before the next one.
next_at: Option<f64>,
/// With Step by Step, set after each step until the window shows a frame drawn after it
/// ([`playback_drawn`]): no step runs while it is set, so the window shows each step's result
/// before the next one runs, also when Play in the panel ran after the canvas drew and while the
/// window is minimized or covered.
hold: bool,
/// The [`uid`](vectorcraft_engine::DocState::uid) of the document that was active after the
/// last step played, if any.
doc: Option<u64>,
/// The host iteration in which Play ran the first step, when that iteration's [`tick`] had
/// already begun (a control request, or the UI pass after it): the pause before the second step
/// starts in a later iteration.
///
/// [`tick`]: VectorcraftApp::tick
played_in: Option<u64>,
}
fn active_doc(app: &VectorcraftApp) -> Option<u64> {
app.session.active().map(|d| d.uid)
}
/// Runs the next step of the paced playback in progress when it is due at `now` (seconds on the
/// app's clock): with Step by Step once the window has shown a frame drawn after the step before
/// ([`playback_drawn`]), so it shows each step before the next one runs; with Pause For once
/// the pause has passed, which starts in the frame after the step before, so a slow step or a late
/// frame does not shorten it. A step that fails ends the playback and shows its error. When a step
/// is due and the active document is not the one that was active after the last step (another
/// document was made active, or that one was closed), the playback ends with a status and the rest
/// do not run. → the seconds until the window should draw again while playback goes on (none while
/// Step by Step waits for a UI pass, which asks for the next frame itself)
pub fn tick_playback(app: &mut VectorcraftApp, now: f64) -> Option<f64> {
let doc = active_doc(app);
let playing = app.ui.action_playing.as_mut()?;
if playing.hold {
return None;
}
let pause = f64::from(playing.options.seconds);
if playing.options.speed == PlaybackSpeed::Pause {
if playing.next_at.is_none() && playing.played_in == Some(app.host_iteration) {
return Some(0.0);
}
let due = *playing.next_at.get_or_insert(now + pause);
if now < due {
return Some(due - now);
}
}
if playing.doc != doc {
app.ui.action_playing = None;
app.status("the action stopped playing: its document is no longer active");
return None;
}
let Some(step) = playing.steps.pop_front() else {
app.ui.action_playing = None;
return None;
};
let label = playing.label.clone();
if playing.steps.is_empty() {
app.ui.action_playing = None;
}
if let Err(e) = play_step(app, &label, step) {
app.ui.action_playing = None;
app.status(e);
return None;
}
let doc = active_doc(app);
let playing = app.ui.action_playing.as_mut()?;
// A step of the action may open, close or switch documents itself.
playing.doc = doc;
// With Pause For, the next frame starts the next pause.
playing.next_at = None;
playing.hold = playing.options.speed == PlaybackSpeed::Step;
Some(0.0)
}
/// Whether a Step by Step playback waits for a frame that shows its last step ([`playback_drawn`]).
pub fn playback_held(app: &VectorcraftApp) -> bool {
app.ui.action_playing.as_ref().is_some_and(|p| p.hold)
}
/// Called after a UI pass that the window shows, when [`playback_held`] was true as the pass began:
/// the Step by Step playback runs its next step in the frame after this one, which it asks for.
pub fn playback_drawn(app: &mut VectorcraftApp) {
if let Some(playing) = app.ui.action_playing.as_mut().filter(|p| p.hold) {
playing.hold = false;
if let Some(ctx) = &app.ui_ctx {
ctx.request_repaint();
}
}
}
/// The command behind Playback Options… ([`playback_options`]).
pub const PLAYBACK_OPTIONS: &str = "ui.action.playbackOptions";
/// `ui.action.playbackOptions {speed?, seconds?}`: sets how actions play. `speed` is
/// `accelerated` (every step at once, the default), `step` (one step at a time, the window drawn
/// after each) or `pause` (`seconds` between steps); `seconds` is a whole number in
/// [`PAUSE_SECONDS`]. A param left out keeps its value. Any other value is an error, and then
/// nothing changes. An action already playing keeps its speed. → {speed, seconds}
pub fn playback_options(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
let current = app.ui.action_playback;
let speed = match p.get("speed") {
None => current.speed,
Some(v) => match v.as_str() {
Some("accelerated") => PlaybackSpeed::Accelerated,
Some("step") => PlaybackSpeed::Step,
Some("pause") => PlaybackSpeed::Pause,
_ => return Err("speed must be accelerated, step or pause".into()),
},
};
let seconds = match p.get("seconds") {
None => current.seconds,
Some(v) => v
.as_u64()
.and_then(|s| u32::try_from(s).ok())
.filter(|s| PAUSE_SECONDS.contains(s))
.ok_or("seconds must be a whole number from 1 to 60")?,
};
app.ui.action_playback = PlaybackOptions { speed, seconds };
Ok(json!({"speed": speed, "seconds": seconds}))
}
/// The command behind the Actions panel's set, action and step checkmarks.
@@ -380,10 +584,11 @@ pub fn action_options(app: &mut VectorcraftApp, p: &Value) -> Result<Value, Stri
/// The command that plays an action by its name ([`play_action`]).
pub const PLAY_ACTION: &str = "ui.action.play";
/// `ui.action.play {action, set?}`: plays the action named `action` as one undoable batch, as
/// Play Current Selection does. `set` names the set to look in (default: every set); a `set` that
/// is not a string is an error. A name that no action has is an error, and so is a name that more
/// than one action in the sets looked in has: then nothing plays. → what `command.batch` returns
/// `ui.action.play {action, set?}`: plays the action named `action` at the Playback Options speed,
/// as Play Current Selection does ([`play`]). `set` names the set to look in (default: every set); a
/// `set` that is not a string is an error. A name that no action has is an error, and so is a name
/// that more than one action in the sets looked in has: then nothing plays. → what [`play`] returns:
/// at Accelerated, what `command.batch` returns; at Step by Step and Pause For, {results, pending}
pub fn play_action(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
let name = p.get("action").and_then(Value::as_str).ok_or("no such action")?;
let in_set = match p.get("set") {
@@ -444,11 +649,14 @@ pub fn record(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
/// `ui.action.stop {}`: ends the recording in progress and keeps its steps, in the action New
/// Action made for it, else in a new action after the last action of its set, as the bottom bar's
/// Stop Playing/Recording does. With nothing recording it changes nothing. → {stopped}
/// Stop Playing/Recording does. It also ends a playback at Step by Step or Pause For: the steps
/// already played stay, and the rest do not run. With nothing recording or playing it changes
/// nothing. → {stopped}
pub fn stop(app: &mut VectorcraftApp, _: &Value) -> Result<Value, String> {
let stopped = app.ui.recording.is_some();
let playing = app.ui.action_playing.take().is_some();
let recording = app.ui.recording.is_some();
stop_recording(app);
Ok(json!({ "stopped": stopped }))
Ok(json!({ "stopped": recording || playing }))
}
/// The command that `chord` runs under `km`, pressed once or pressed twice.
@@ -801,6 +1009,9 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut egui::Ui) {
{
crate::dialogs::action_set_options::open_action(app, s, a);
}
if widgets::menu_item(ui, tl!("Playback Options…"), true, false) {
crate::dialogs::action_playback::open(app);
}
ui.separator();
// As commands, so a file dialog shown off the UI thread saves or loads when it answers.
// Save Actions… names the current set by its id, so a file dialog that answers later saves that set.
@@ -2662,4 +2873,402 @@ mod tests {
"the command keeps its shortcut"
);
}
/// A document with one rectangle selected and the set "Moves" with one action that moves it
/// right three times, by 1, 2 and 4 pt → (the app, the rectangle's left edge as it moves).
fn app_with_three_moves() -> (VectorcraftApp, impl Fn(&VectorcraftApp) -> f64) {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 200, "height": 100})).unwrap();
let id = vectorcraft_doc::NodeId(
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 20, "height": 20})).unwrap()["id"].as_u64().unwrap(),
);
app.run("select.set", json!({"ids": [id.0]})).unwrap();
let step = |dx: u32| ("object.move".to_string(), json!({"dx": dx, "dy": 0}));
app.ui.action_sets = vec![ActionSet::new("Moves", vec![Action::new("Three Moves", vec![step(1), step(2), step(4)])])];
let x = move |app: &VectorcraftApp| app.session.active().unwrap().doc.node(id).unwrap().geometric_bounds().unwrap().x0 - 10.0;
(app, x)
}
fn undo_steps(app: &VectorcraftApp) -> usize {
app.session.active().unwrap().history.undo.len()
}
/// One frame of a shown window at `now`: the logic pass, then a UI pass that draws → what the
/// logic pass's [`tick_playback`] returns.
fn frame(app: &mut VectorcraftApp, now: f64) -> Option<f64> {
let wait = tick_playback(app, now);
playback_drawn(app);
wait
}
/// Draws frames until the playback in progress ends (at most 20).
fn play_out(app: &mut VectorcraftApp) {
for _ in 0..20 {
if app.ui.action_playing.is_none() {
return;
}
frame(app, 0.0);
}
panic!("the playback did not end");
}
#[test]
fn playback_options_sets_each_speed_and_refuses_bad_values() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
assert_eq!(app.ui.action_playback, PlaybackOptions { speed: PlaybackSpeed::Accelerated, seconds: 1 }, "Accelerated is the default");
assert_eq!(app.run(PLAYBACK_OPTIONS, json!({"speed": "step"})).unwrap(), json!({"speed": "step", "seconds": 1}));
assert_eq!(app.run(PLAYBACK_OPTIONS, json!({"speed": "pause", "seconds": 2})).unwrap(), json!({"speed": "pause", "seconds": 2}));
assert_eq!(
app.run(PLAYBACK_OPTIONS, json!({"speed": "accelerated"})).unwrap(),
json!({"speed": "accelerated", "seconds": 2}),
"seconds kept"
);
assert_eq!(app.run(PLAYBACK_OPTIONS, json!({"seconds": 60})).unwrap(), json!({"speed": "accelerated", "seconds": 60}), "speed kept");
let kept = app.ui.action_playback;
for bad in [json!({"speed": "fast"}), json!({"speed": null}), json!({"speed": 1})] {
assert_eq!(app.run(PLAYBACK_OPTIONS, bad.clone()).unwrap_err(), "speed must be accelerated, step or pause", "{bad}");
}
for bad in [json!(0), json!(61), json!(2.5), json!(-1), json!("2"), json!(null)] {
let p = json!({"speed": "pause", "seconds": bad});
assert_eq!(app.run(PLAYBACK_OPTIONS, p).unwrap_err(), "seconds must be a whole number from 1 to 60", "{bad}");
}
assert_eq!(app.ui.action_playback, kept, "a refused value changes nothing");
// Kept with the UI preferences; a saved value that is not valid loads as the default.
app.run(PLAYBACK_OPTIONS, json!({"speed": "pause", "seconds": 5})).unwrap();
let mut saved = serde_json::to_value(&app.ui).unwrap();
let ui = serde_json::from_value::<crate::UiState>(saved.clone()).unwrap();
assert_eq!(ui.action_playback, PlaybackOptions { speed: PlaybackSpeed::Pause, seconds: 5 });
for bad in [json!({"speed": "pause", "seconds": 0}), json!({"speed": "slow", "seconds": 5}), json!("step")] {
saved["action_playback"] = bad;
assert_eq!(serde_json::from_value::<crate::UiState>(saved.clone()).unwrap().action_playback, PlaybackOptions::default());
}
}
#[test]
fn the_playback_options_dialog_sets_the_speed_and_pause() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run(PLAYBACK_OPTIONS, json!({"speed": "step", "seconds": 3})).unwrap();
crate::dialogs::action_playback::open(&mut app);
let d = app.ui.dialog.as_mut().unwrap();
assert_eq!((d.kind.as_str(), d.str("speed"), d.fields["seconds"].clone()), ("actionPlayback", "step".to_string(), json!(3)));
d.fields.insert("speed".into(), json!("pause"));
d.fields.insert("seconds".into(), json!(4));
crate::dialogs::confirm(&mut app).unwrap();
assert!(app.ui.dialog.is_none());
assert_eq!(app.ui.action_playback, PlaybackOptions { speed: PlaybackSpeed::Pause, seconds: 4 });
}
#[test]
fn accelerated_playback_plays_every_step_as_one_undo_step() {
let (mut app, x) = app_with_three_moves();
let before = undo_steps(&app);
play(&mut app, 0, 0).unwrap();
assert_eq!(x(&app), 7.0);
assert_eq!(undo_steps(&app), before + 1);
assert!(app.ui.action_playing.is_none());
assert_eq!(tick_playback(&mut app, 0.0), None, "nothing waits for a frame");
}
#[test]
fn step_by_step_plays_one_step_per_frame_and_redraws_between_steps() {
let (mut app, x) = app_with_three_moves();
app.run(PLAYBACK_OPTIONS, json!({"speed": "step"})).unwrap();
let before = undo_steps(&app);
assert_eq!(play(&mut app, 0, 0).unwrap()["pending"], json!(2));
assert_eq!(x(&app), 1.0, "the first step runs at once");
assert_eq!(frame(&mut app, 0.0), None, "the frame after Play draws the first step");
assert_eq!(x(&app), 1.0, "and runs no step, even when Play ran after the canvas drew");
assert_eq!(frame(&mut app, 0.0), Some(0.0), "the window draws again before the next step");
assert_eq!(x(&app), 3.0, "one more step in this frame");
assert_eq!(frame(&mut app, 0.0), None, "the last step ends the playback");
assert_eq!(x(&app), 7.0);
assert!(app.ui.action_playing.is_none());
assert_eq!(frame(&mut app, 0.0), None);
assert_eq!(x(&app), 7.0);
assert_eq!(undo_steps(&app), before + 3, "each step is its own undo step");
assert_eq!(app.session.active().unwrap().history.undo.last().unwrap().label, "Three Moves");
}
#[test]
fn step_by_step_runs_no_step_until_the_window_draws_the_one_before() {
let (mut app, x) = app_with_three_moves();
app.run(PLAYBACK_OPTIONS, json!({"speed": "step"})).unwrap();
play(&mut app, 0, 0).unwrap();
// A hidden window runs logic passes only, which draw nothing.
for _ in 0..3 {
assert_eq!(tick_playback(&mut app, 0.0), None, "no step runs and no frame is asked for");
}
assert_eq!(x(&app), 1.0);
playback_drawn(&mut app);
assert_eq!(tick_playback(&mut app, 0.0), Some(0.0));
assert_eq!(x(&app), 3.0, "the step after a drawn frame runs");
for _ in 0..3 {
assert_eq!(tick_playback(&mut app, 0.0), None);
}
assert_eq!(x(&app), 3.0, "the next step waits until the window draws this one");
playback_drawn(&mut app);
assert_eq!(tick_playback(&mut app, 0.0), None, "the last step ends the playback");
assert_eq!(x(&app), 7.0);
}
#[test]
fn step_by_step_waits_while_the_window_is_hidden() {
let (mut app, x) = app_with_three_moves();
app.run(PLAYBACK_OPTIONS, json!({"speed": "step"})).unwrap();
let ctx = egui::Context::default();
// One host iteration: `tick`, then a UI pass, in a shown or an occluded window.
let frame = |app: &mut VectorcraftApp, hidden: bool| {
let mut raw =
egui::RawInput { screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1200.0, 800.0))), ..Default::default() };
if hidden {
raw.viewports.entry(egui::ViewportId::ROOT).or_default().occluded = Some(true);
}
app.begin_host_iteration();
let mut out = ctx.run_ui(raw, |ui| {
app.tick(ui.ctx());
app.ui(ui);
});
out.textures_delta.clear();
};
// Fonts install on the first pass and are ready on the second.
frame(&mut app, false);
frame(&mut app, false);
play(&mut app, 0, 0).unwrap();
for _ in 0..3 {
frame(&mut app, true);
}
assert_eq!(x(&app), 1.0, "no step runs while the window is hidden, even in a UI pass");
frame(&mut app, false);
assert_eq!(x(&app), 1.0, "the shown frame draws the first step");
frame(&mut app, true);
assert_eq!(x(&app), 3.0, "the first step was shown, so the second runs");
for _ in 0..3 {
frame(&mut app, true);
}
assert_eq!(x(&app), 3.0, "the third step waits until the window shows the second");
frame(&mut app, false);
assert_eq!(x(&app), 3.0, "the shown frame draws the second step");
frame(&mut app, false);
assert_eq!(x(&app), 7.0);
assert!(app.ui.action_playing.is_none());
}
#[test]
fn pause_for_waits_the_seconds_between_steps() {
let (mut app, x) = app_with_three_moves();
app.run(PLAYBACK_OPTIONS, json!({"speed": "pause", "seconds": 2})).unwrap();
play(&mut app, 0, 0).unwrap();
assert_eq!(x(&app), 1.0);
assert_eq!(tick_playback(&mut app, 10.0), Some(2.0), "the pause starts in the frame after the first step");
assert_eq!(tick_playback(&mut app, 11.5), Some(0.5));
assert_eq!(x(&app), 1.0, "nothing runs before the pause has passed");
assert_eq!(tick_playback(&mut app, 12.0), Some(0.0), "the next frame comes promptly");
assert_eq!(x(&app), 3.0, "the second step runs after 2 s");
assert_eq!(tick_playback(&mut app, 12.0), Some(2.0), "and starts the next pause");
assert!(tick_playback(&mut app, 13.9).is_some_and(|wait| (wait - 0.1).abs() < 1e-9));
assert_eq!(x(&app), 3.0);
assert_eq!(tick_playback(&mut app, 14.0), None);
assert_eq!(x(&app), 7.0, "the third step runs 2 s after the second");
assert!(app.ui.action_playing.is_none());
}
#[test]
fn pause_for_starts_each_pause_in_the_frame_after_the_step() {
let (mut app, x) = app_with_three_moves();
app.run(PLAYBACK_OPTIONS, json!({"speed": "pause", "seconds": 2})).unwrap();
play(&mut app, 0, 0).unwrap();
assert_eq!(tick_playback(&mut app, 0.0), Some(2.0));
assert!(tick_playback(&mut app, 2.0).is_some());
assert_eq!(x(&app), 3.0, "the second step runs");
// The next frame comes later than the pause, for example after a slow step.
assert_eq!(tick_playback(&mut app, 9.0), Some(2.0), "the pause starts in this frame");
assert_eq!(x(&app), 3.0, "no step runs without a pause before it");
assert_eq!(tick_playback(&mut app, 11.0), None);
assert_eq!(x(&app), 7.0, "the third step runs 2 s after the frame that started the pause");
}
#[test]
fn stop_ends_a_paced_playback_and_another_action_waits_for_it() {
let (mut app, x) = app_with_three_moves();
app.run(PLAYBACK_OPTIONS, json!({"speed": "pause", "seconds": 1})).unwrap();
play(&mut app, 0, 0).unwrap();
assert_eq!(play(&mut app, 0, 0).unwrap_err(), "an action is already playing");
assert_eq!(app.run(PLAY_ACTION, json!({"action": "Three Moves"})).unwrap_err(), "an action is already playing");
assert_eq!(x(&app), 1.0);
assert_eq!(app.run(STOP_RECORDING, json!({})).unwrap(), json!({"stopped": true}));
assert_eq!(tick_playback(&mut app, 100.0), None);
assert_eq!(x(&app), 1.0, "the steps after Stop do not run; the one played stays");
assert_eq!(app.run(STOP_RECORDING, json!({})).unwrap(), json!({"stopped": false}));
play(&mut app, 0, 0).unwrap();
assert_eq!(x(&app), 2.0, "an action plays again after Stop");
}
#[test]
fn control_requests_come_before_the_step_that_is_due() {
let ctx = egui::Context::default();
// One host iteration's `tick` at `now`.
let tick = |app: &mut VectorcraftApp, now: f64| {
app.now_override = Some(now);
app.begin_host_iteration();
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| app.tick(ui.ctx()));
out.textures_delta.clear();
};
// The same, with `method` and `params` queued on the control channel → its reply.
let iteration =
|app: &mut VectorcraftApp, tx: &std::sync::mpsc::Sender<crate::control::ControlRequest>, now: f64, method: &str, params: Value| {
let (req, reply) = crate::control::ControlRequest::new(method, params);
tx.send(req).unwrap();
tick(app, now);
reply.try_recv().unwrap()
};
// Stop: the step that is due does not run.
let (app, x) = app_with_three_moves();
let (tx, rx) = std::sync::mpsc::channel();
let mut app = app.with_control(rx);
app.run(PLAYBACK_OPTIONS, json!({"speed": "pause", "seconds": 1})).unwrap();
play(&mut app, 0, 0).unwrap();
assert_eq!(tick_playback(&mut app, 0.0), Some(1.0));
let r = iteration(&mut app, &tx, 5.0, "engine.execute", json!({"command": STOP_RECORDING}));
assert_eq!(r["result"], json!({"stopped": true}), "{r}");
assert!(app.ui.action_playing.is_none());
assert_eq!(x(&app), 1.0, "the step that was due did not run");
// A switch to another document: the playback ends, and the step does not run in the first one.
let (app, x) = app_with_three_moves();
let (tx, rx) = std::sync::mpsc::channel();
let mut app = app.with_control(rx);
app.run(PLAYBACK_OPTIONS, json!({"speed": "pause", "seconds": 1})).unwrap();
play(&mut app, 0, 0).unwrap();
assert_eq!(tick_playback(&mut app, 0.0), Some(1.0));
let r = iteration(&mut app, &tx, 5.0, "engine.execute", json!({"command": "file.new", "params": {"width": 50, "height": 50}}));
assert_eq!(r["ok"], true, "{r}");
assert!(app.ui.action_playing.is_none(), "the playback ends in the same iteration");
assert_eq!(app.ui.status, "the action stopped playing: its document is no longer active");
app.run("document.activate", json!({"index": 0})).unwrap();
assert_eq!(x(&app), 1.0, "the step that was due did not run in the first document");
// Play: the pause after the first step starts in the next iteration.
let (app, x) = app_with_three_moves();
let (tx, rx) = std::sync::mpsc::channel();
let mut app = app.with_control(rx);
app.run(PLAYBACK_OPTIONS, json!({"speed": "pause", "seconds": 1})).unwrap();
let r = iteration(&mut app, &tx, 0.0, "engine.execute", json!({"command": PLAY_ACTION, "params": {"action": "Three Moves"}}));
assert_eq!(r["result"]["pending"], json!(2), "{r}");
assert_eq!(x(&app), 1.0);
tick(&mut app, 0.5);
tick(&mut app, 1.0);
assert_eq!(x(&app), 1.0, "the pause started at 0.5");
tick(&mut app, 1.5);
assert_eq!(x(&app), 3.0);
}
#[test]
fn an_action_recorded_while_another_plays_plays_its_steps() {
let (mut app, x) = app_with_three_moves();
// Accelerated: the recording gets the played action as one batch step.
app.run(START_RECORDING, json!({"set": 0})).unwrap();
play(&mut app, 0, 0).unwrap();
app.run(STOP_RECORDING, json!({})).unwrap();
assert_eq!(app.ui.action_sets[0].actions[1].steps.iter().map(|s| s.0.as_str()).collect::<Vec<_>>(), ["command.batch"]);
assert_eq!(x(&app), 7.0);
play(&mut app, 0, 1).unwrap();
assert_eq!(x(&app), 14.0, "the batch step plays its moves");
// Step by Step: recording begins after the first step, and gets each later step as a batch.
app.run(PLAYBACK_OPTIONS, json!({"speed": "step"})).unwrap();
play(&mut app, 0, 0).unwrap();
app.run(START_RECORDING, json!({"set": 0})).unwrap();
play_out(&mut app);
app.run(STOP_RECORDING, json!({})).unwrap();
assert_eq!(x(&app), 21.0);
assert_eq!(app.ui.action_sets[0].actions[2].steps.len(), 2);
play(&mut app, 0, 2).unwrap();
play_out(&mut app);
assert_eq!(x(&app), 27.0, "paced: the recorded batches play the second and third moves");
app.run(PLAYBACK_OPTIONS, json!({"speed": "accelerated"})).unwrap();
play(&mut app, 0, 2).unwrap();
assert_eq!(x(&app), 33.0, "accelerated: the recorded batches play inside the action's batch");
}
#[test]
fn a_paced_playback_ends_when_another_document_becomes_active() {
let (mut app, x) = app_with_three_moves();
app.run(PLAYBACK_OPTIONS, json!({"speed": "pause", "seconds": 1})).unwrap();
play(&mut app, 0, 0).unwrap();
assert_eq!(tick_playback(&mut app, 0.0), Some(1.0));
app.run("file.new", json!({"width": 50, "height": 50})).unwrap();
assert_eq!(tick_playback(&mut app, 0.5), Some(0.5), "nothing ends before the next step is due");
app.run("document.activate", json!({"index": 0})).unwrap();
assert_eq!(tick_playback(&mut app, 1.0), Some(0.0));
assert_eq!(x(&app), 3.0, "the document is active again when the step is due, so it runs");
app.run("document.activate", json!({"index": 1})).unwrap();
let other = app.session.active().unwrap().doc.clone();
assert_eq!(tick_playback(&mut app, 5.0), Some(1.0), "the next pause starts");
assert_eq!(tick_playback(&mut app, 6.0), None, "the playback ends");
assert!(app.ui.action_playing.is_none());
assert_eq!(app.ui.status, "the action stopped playing: its document is no longer active");
assert!(std::sync::Arc::ptr_eq(&app.session.active().unwrap().doc, &other), "no step ran in the other document");
app.run("document.activate", json!({"index": 0})).unwrap();
assert_eq!(tick_playback(&mut app, 10.0), None);
assert_eq!(x(&app), 3.0, "the rest of the steps do not run in the first document either");
// A step that makes another document active itself does not end the playback.
app.ui.action_sets[0].actions[0].steps.insert(0, ("document.activate".into(), json!({"index": 0})));
app.run("document.activate", json!({"index": 1})).unwrap();
app.run(PLAYBACK_OPTIONS, json!({"speed": "step"})).unwrap();
app.ui.status.clear();
play(&mut app, 0, 0).unwrap();
assert_eq!(frame(&mut app, 0.0), None);
play_out(&mut app);
assert_ne!(app.ui.status, "the action stopped playing: its document is no longer active");
assert_eq!(x(&app), 10.0, "the moves after the step ran in the document it made active");
}
#[test]
fn closing_the_playing_document_ends_a_paced_playback() {
let stopped = "the action stopped playing: its document is no longer active";
// File › Close with `p`, answering Don't Save for the modified document.
let close = |app: &mut VectorcraftApp, p: Value| {
assert_eq!(app.run("file.close", p).unwrap(), json!({"pending": crate::unsaved::KIND}));
app.ui.dialog.as_mut().unwrap().fields.insert("discard".into(), json!(true));
crate::unsaved::confirm(app).unwrap();
};
// Step by Step, the only document closed: no document is active when the next step is due.
let (mut app, x) = app_with_three_moves();
app.run(PLAYBACK_OPTIONS, json!({"speed": "step"})).unwrap();
play(&mut app, 0, 0).unwrap();
assert_eq!(x(&app), 1.0);
close(&mut app, json!({}));
assert!(app.session.active().is_none());
assert_eq!(frame(&mut app, 0.0), None, "the frame after Play draws");
assert_eq!(frame(&mut app, 0.0), None, "the next step is due, and the playback ends");
assert!(app.ui.action_playing.is_none());
assert_eq!(app.ui.status, stopped);
// Pause For, another document left active: no later step runs in it.
let (mut app, x) = app_with_three_moves();
app.run("file.new", json!({"width": 50, "height": 50})).unwrap();
let other = app.session.active().unwrap().doc.clone();
app.run("document.activate", json!({"index": 0})).unwrap();
app.run(PLAYBACK_OPTIONS, json!({"speed": "pause", "seconds": 1})).unwrap();
play(&mut app, 0, 0).unwrap();
assert_eq!(x(&app), 1.0);
assert_eq!(tick_playback(&mut app, 0.0), Some(1.0));
close(&mut app, json!({"index": 0}));
assert!(std::sync::Arc::ptr_eq(&app.session.active().unwrap().doc, &other));
assert_eq!(tick_playback(&mut app, 1.0), None, "the playback ends when the next step is due");
assert!(app.ui.action_playing.is_none());
assert_eq!(app.ui.status, stopped);
assert_eq!(tick_playback(&mut app, 10.0), None);
assert!(std::sync::Arc::ptr_eq(&app.session.active().unwrap().doc, &other), "no step ran in the document left open");
}
#[test]
fn a_failing_step_ends_a_paced_playback_with_its_error() {
let (mut app, x) = app_with_three_moves();
app.run(PLAYBACK_OPTIONS, json!({"speed": "step"})).unwrap();
app.ui.action_sets[0].actions[0].steps[1] = ("no.such.command".into(), json!({}));
play(&mut app, 0, 0).unwrap();
assert_eq!(frame(&mut app, 0.0), None, "the frame after Play draws the first step");
assert_eq!(frame(&mut app, 0.0), None);
assert!(app.ui.action_playing.is_none());
assert!(!app.ui.status.is_empty(), "the step's error shows");
assert_eq!(frame(&mut app, 0.0), None);
assert_eq!(x(&app), 1.0, "the step after the failing one does not run");
}
}
+8
View File
@@ -496,6 +496,12 @@ pub struct UiState {
/// adds a new action, as after Begin Recording.
#[serde(skip)]
pub recording_into: Option<u64>,
/// 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,
/// The playback at Step by Step or Pause For in progress, if any. Not saved.
#[serde(skip)]
pub action_playing: Option<crate::panels::actions::Playing>,
/// Keyboard shortcut overrides: command id, `tool:<id>` or `panel:<id>` → chord ("" = none),
/// stored in the form every earlier version reads.
#[serde(default, with = "crate::keymap::stored")]
@@ -970,6 +976,8 @@ impl Default for UiState {
action_sets_selected: vec![],
recording: None,
recording_into: None,
action_playback: Default::default(),
action_playing: None,
shortcut_overrides: Default::default(),
shortcut_set: default_shortcut_set(),
workspace: default_workspace(),
+22 -5
View File
@@ -1848,12 +1848,29 @@ already walks Image Trace. Short agent entry points:
a text field has the keyboard, or while a tool is busy. Editing command shortcuts (`shortcuts.set`, Keyboard Shortcuts), loading preferences and
`ui.actionSet.load` preserve action function keys. When several actions have the same key, the first in panel order
plays. A stored key reserved on the current platform does not play an action.
- Actions: `ui.action.play {action, set?}` plays the action named `action` as one undoable batch; `set` names the set
to look in (default: every set), and a `set` that is not a string is an error. A name that no action has, or that
more than one action in the sets looked in has, or whose every step is excluded, is an error, and then nothing plays.
- Actions: `ui.action.play {action, set?}` plays the action named `action` at the speed `ui.action.playbackOptions`
sets; `set` names the set to look in (default: every set), and a `set` that is not a string is an error. At
Accelerated it plays the action as one undoable batch and returns what `command.batch` returns. At Step by Step and
Pause For it plays the first step at once and returns `{results: [the first step's result], pending}`, where
`pending` counts the steps still to play; the app plays them one at a time, each as its own undo step. When the next
step is due and the active document is not the one that was active after the step before (another document was
made active, or that document was closed), the playback ends with a status and the rest do not play; a step that
makes another document active itself does not end it. A name that no
action has, or that more than one action in the sets looked in has, or whose every step is excluded, is an error, and
then nothing plays. So is an action played while another is still playing at Step by Step or Pause For. A
`command.batch` step, which an action played or a panel operation run as one batch records, plays as its own steps,
here and in Batch….
- Actions: `ui.action.playbackOptions {speed?, seconds?}` sets how the Actions panel plays actions and returns
`{speed, seconds}`. `speed` is `accelerated` (the default: every step at once), `step` (one step at a time, with the
window drawn after each step before the next runs) or `pause` (a pause of `seconds` between steps). `seconds` is a
whole number from 1 to 60, VectorCraft's own range, and stays set while another speed is chosen. A param left out
keeps its value. Any other `speed` or `seconds` is an error, and then nothing changes. The setting is kept with the UI
preferences. The panel menu's Playback Options… opens the `actionPlayback` dialog (`speed`, `seconds`), whose OK runs
this. Batch… (`action.batch`) plays every step at once whatever the setting.
- Actions: `ui.action.stop {}` ends the recording in progress and keeps its steps, in the action `ui.action.new` made
for it, else in a new action after the last action of its set, and returns `{stopped}`. With nothing recording it
changes nothing. The panel's Stop Playing/Recording button runs this.
for it, else in a new action after the last action of its set, and returns `{stopped}`. It also ends an action
playing at Step by Step or Pause For: the steps already played stay, and the rest do not play. With nothing
recording or playing it changes nothing. The panel's Stop Playing/Recording button runs this.
- Action sets: `ui.actionSet.delete {sets?: [index], ids?: [id]}` deletes the Actions panel's sets at these indexes
(0 is the top set) with their actions, by default the current set, and returns `{deleted}`. An index past the last
set is an error, and then no set is deleted. `ids` holds the panel's own set ids, which its question passes: the
+1
View File
@@ -581,6 +581,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: 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: Delete Selection deletes the selected action set with its actions and keeps the other sets ([Actions panel overview](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/actions-panel-overview.html); `ui.actionSet.delete`). VectorCraft's own rules, which the overview does not cover: deleting a set asks first, Shift-click on a set's name selects a range of sets to delete together, the panel menu's Delete does the same, and Default Actions and the last set can be deleted. Undo does not bring a deleted set back.
- Actions panel: the panel menu's Duplicate copies the selected action, else the current action set with all its actions ([Actions panel overview](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/actions-panel-overview.html); `ui.actionSet.duplicate`). VectorCraft's own rules, which the overview does not cover: the copy goes right after the original, is named after it with " copy" added and is selected.
- Batch… over data sets names each file after the document and the data set (`action.batch`). It writes `/`, `\`, `:`, `<`, `>`, `"`, `|`, `?`, `*` and control characters in the data set's name as `-`, and adds `-2`, `-3`… to a name an earlier data set used.
+1
View File
@@ -320,6 +320,7 @@ next unit in the order a right-click on a ruler lists them, from Feet back to Po
| Toolbar without group labels by default, one or two columns; All Tools drawer by flyout group with shortcuts, its menu (Reset, Basic, Advanced) and Show section | done | #1751; a preference brings the labels back; `toolbar.show` sets the Fill/Stroke, Coloring, Draw Mode and Screen Mode controls |
| Actions panel: ≡ › Save Actions… saves the current set with all its actions to a file and ≡ › Load Actions… adds a saved set ([Manage a set of actions](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/manage-a-set-of-actions.html)) | partial | `ui.actionSet.save`, `ui.actionSet.load`; `panels/actions.rs` tests. The file is VectorCraft's own `.vcactions` JSON; the reference app's `.aia` files and Reset, Clear and Replace Actions remain |
| Actions panel: ≡ › Action Options… renames the selected action and sets its function key and color ([Actions panel overview](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/actions-panel-overview.html)) | partial | `ui.action.options`; `panels/actions.rs` tests (part of #1629). The color shows nowhere until Button Mode exists |
| Actions panel: ≡ › Playback Options… plays actions Accelerated, Step by Step or with a pause between steps ([Specify playback speed](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/specify-playback-speed.html)) | done | `ui.action.playbackOptions`; `panels/actions.rs` tests. The pause is a whole number of seconds from 1 to 60 (VectorCraft's own range); Batch… plays at full speed |
| Actions panel: a clicked action stays selected for Play Current Selection and Delete Selection | partial | #1853 (part of #1629); editing actions and action shortcuts remain |
| Actions panel: Create New Set (bottom bar, ≡ › New Set…) names a new set and Set Options… renames the current one; a click on a set's name makes it current and a recording goes into the current set ([helpx: Automation with actions](https://helpx.adobe.com/illustrator/using/automation-actions.html)); the current set is kept with the UI preferences, and with no set current a recording goes into the first set, which becomes current | partial | #3712; dragging actions between sets remains |
| Actions panel: Create New Action (bottom bar, ≡ › New Action…) asks for the name, set, function key and color, and Record adds the action and records into it ([Create new actions](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/record-actions.html)) | partial | `ui.action.new`, `ui.action.play`; `panels/actions.rs` tests. A function key plays its action, even when a command has the same shortcut, and the command keeps it. Button Mode, which shows the color, remains |