Merge pull request #2356 from dotCooCoo/feat/script-shortcuts

Scripts: File ▸ Scripts entries take keyboard shortcuts
This commit is contained in:
BFlat
2026-10-30 07:05:58 +07:00
committed by GitHub
12 changed files with 202 additions and 21 deletions
+62
View File
@@ -236,6 +236,7 @@ fn install(s: &mut Session, p: &Value, panel: bool) -> Result<Value> {
let cfg = s.config.clone().ok_or_else(|| EngineError::Other("this session has no settings folder to install scripts into".into()))?;
let dir = if panel { PANELS_DIR } else { SCRIPTS_DIR };
cfg.write(&format!("{dir}/{name}"), &code).map_err(|e| EngineError::Other(format!("cannot install {name}: {e}")))?;
s.scripts_changed();
// After Effects asks for a restart; our menus pick it up at once.
s.toast(if panel {
format!("Installed ScriptUI panel {name}: find it at the bottom of the Window menu")
@@ -271,6 +272,7 @@ fn uninstall(s: &mut Session, p: &Value) -> Result<Value> {
removed = true;
}
}
s.scripts_changed();
if removed { Ok(json!({"removed": name})) } else { Err(bad(c, format!("`{name}` is not installed"))) }
}
@@ -1060,4 +1062,64 @@ mod tests {
assert!(bad_params(&s.execute("file.uninstallScript", json!({"name": "Hello.jsx"}))), "no longer installed");
let _ = std::fs::remove_dir_all(&root);
}
/// Edit ▸ Keyboard Shortcuts lists the scripts File ▸ Scripts lists (not ScriptUI panels),
/// as soon as one is installed and until it is uninstalled; a key given to one is saved with
/// the preset.
#[test]
fn file_scripts_entries_take_keyboard_shortcuts() {
use crate::shortcuts::{Keymaps, SHORTCUTS_FILE, binding_key};
let (mut s, root) = session_in_tmp("shortcuts");
let (script, panel) = (root.join("Hello.jsx"), root.join("Hello Panel.jsx"));
std::fs::write(&script, "hello").unwrap();
std::fs::write(&panel, "panel").unwrap();
let listed = |s: &Session, name: &str| {
s.shortcuts().find(&binding_key("file.runScript", &json!({"name": name}))).map(|b| (b.label.clone(), b.path.join(" ▸ "), b.defaults.clone()))
};
assert_eq!(listed(&s, "Rename Layers.jsx"), Some(("Rename Layers.jsx".into(), "File ▸ Scripts".into(), vec![])), "the bundled samples");
assert_eq!(listed(&s, "Hello.jsx"), None);
s.execute("file.installScript", json!({"path": script.to_string_lossy()})).unwrap();
s.execute("file.installScriptUIPanel", json!({"path": panel.to_string_lossy()})).unwrap();
assert_eq!(listed(&s, "Hello.jsx"), Some(("Hello.jsx".into(), "File ▸ Scripts".into(), vec![])));
assert_eq!(listed(&s, "Hello Panel.jsx"), None, "panels are in the Window menu");
let r = s.execute("shortcuts.set", json!({"command": "file.runScript", "params": {"name": "Hello.jsx"}, "keys": "Cmd+Alt+Shift+F7"})).unwrap();
let key = binding_key("file.runScript", &json!({"name": "Hello.jsx"}));
assert_eq!((&r["command"], &r["keys"]), (&json!(key), &json!(["Cmd+Alt+Shift+F7"])), "{r}");
assert_eq!(s.shortcuts().shortcut_of("file.runScript", &json!({"name": "Hello.jsx"})), Some("Cmd+Alt+Shift+F7"));
let saved = Keymaps::from_json(&std::fs::read_to_string(root.join("settings").join(SHORTCUTS_FILE)).unwrap());
assert_eq!(saved.active_preset().and_then(|p| p.overrides.get(&key)), Some(&vec!["Cmd+Alt+Shift+F7".to_string()]));
s.execute("file.uninstallScript", json!({"name": "Hello.jsx"})).unwrap();
assert_eq!(listed(&s, "Hello.jsx"), None);
let _ = std::fs::remove_dir_all(&root);
}
/// `shortcuts.list` and `shortcuts.set` see a script copied into the Scripts folder by hand
/// after the shortcut table was built; a script the folder doesn't hold (never there, or
/// removed by hand) can't take a key, and Run Script File… keeps its own keys.
#[test]
fn shortcut_commands_find_scripts_copied_into_the_folder() {
let (mut s, root) = session_in_tmp("copied");
assert!(s.shortcuts().find("file.runScript").is_some(), "the table is built");
std::fs::create_dir_all(root.join("settings").join(SCRIPTS_DIR)).unwrap();
std::fs::write(root.join("settings").join(SCRIPTS_DIR).join("Copied.jsx"), "copied").unwrap();
let list = s.execute("shortcuts.list", json!({"query": "copied.jsx"})).unwrap();
assert_eq!(list["count"], 1, "{list}");
let r = s.execute("shortcuts.set", json!({"command": "file.runScript", "params": {"name": "Copied.jsx"}, "keys": "Cmd+Alt+Shift+F7"})).unwrap();
assert_eq!(r["command"], r#"file.runScript {"name":"Copied.jsx"}"#, "{r}");
let r = s.execute("shortcuts.set", json!({"command": "file.runScript", "params": {"name": "Missing.jsx"}, "keys": "Cmd+Alt+Shift+F8"}));
assert!(bad_params(&r), "{r:?}");
// The binding-key form finds a script copied in since the last lookup, too.
std::fs::write(root.join("settings").join(SCRIPTS_DIR).join("Second.jsx"), "second").unwrap();
let r = s.execute("shortcuts.set", json!({"command": r#"file.runScript {"name":"Second.jsx"}"#, "keys": "Cmd+Alt+Shift+F10"})).unwrap();
assert_eq!(r["command"], r#"file.runScript {"name":"Second.jsx"}"#, "{r}");
// Removed by hand: no longer in the folder.
std::fs::remove_file(root.join("settings").join(SCRIPTS_DIR).join("Copied.jsx")).unwrap();
std::fs::remove_file(root.join("settings").join(SCRIPTS_DIR).join("Second.jsx")).unwrap();
let r = s.execute("shortcuts.set", json!({"command": r#"file.runScript {"name":"Second.jsx"}"#, "keys": "Cmd+Alt+Shift+F11"}));
assert!(bad_params(&r), "{r:?}");
let r = s.execute("shortcuts.set", json!({"command": "file.runScript", "params": {"name": "Copied.jsx"}, "keys": "Cmd+Alt+Shift+F9"}));
assert!(bad_params(&r), "{r:?}");
assert_eq!(s.shortcuts().shortcut_of("file.runScript", &Value::Null), None, "Run Script File… has no key");
let _ = std::fs::remove_dir_all(&root);
}
}
+14 -4
View File
@@ -73,13 +73,21 @@ fn prefs_pages(_: &mut Session, _: &Value) -> Result<Value> {
// ---------------------------------------------------------------- shortcuts
/// The bindable named by `command` (+ `params`), or by a binding key (`app.settings {...}`).
fn bindable(s: &Session, p: &Value, cmd: &str) -> Result<crate::shortcuts::Bindable> {
/// The bindable named by `command` (+ `params`), or by a binding key (`app.settings {...}`). A
/// File ▸ Scripts entry (`file.runScript {"name": …}`) is looked up in File ▸ Scripts' list as it
/// is now (the Scripts folder and the bundled scripts), and a script it doesn't list is an error,
/// not Run Script File….
fn bindable(s: &mut Session, p: &Value, cmd: &str) -> Result<crate::shortcuts::Bindable> {
let c = str_p(p, "command").ok_or_else(|| bad(cmd, "missing `command`"))?;
let params = p.get("params").cloned().unwrap_or(Value::Null);
let t = s.shortcuts();
let key = binding_key(c, &params);
t.find(&key).or_else(|| t.find(c)).cloned().ok_or_else(|| bad(cmd, format!("`{key}` can't have a shortcut (see shortcuts.list)")))
let script = (c == "file.runScript" && params.get("name").is_some()) || c.starts_with("file.runScript {");
if script {
s.scripts_changed();
}
let t = s.shortcuts();
let found = if script { t.find(&key) } else { t.find(&key).or_else(|| t.find(c)) };
found.cloned().ok_or_else(|| bad(cmd, format!("`{key}` can't have a shortcut (see shortcuts.list)")))
}
fn keys_param(p: &Value, cmd: &str) -> Result<Vec<String>> {
@@ -93,6 +101,8 @@ fn keys_param(p: &Value, cmd: &str) -> Result<Vec<String>> {
}
fn sc_list(s: &mut Session, p: &Value) -> Result<Value> {
// Scripts copied into the Scripts folder since the table was built are listed too.
s.scripts_changed();
let q = str_p(p, "query").unwrap_or("").to_lowercase();
let assigned = p.get("assigned").and_then(Value::as_bool).unwrap_or(false);
let keys_q = str_p(p, "keys").and_then(normalize);
+12 -2
View File
@@ -195,9 +195,19 @@ impl Session {
self.shortcut_table = std::sync::OnceLock::new();
}
/// The active shortcut preset resolved against every bindable command.
/// The scripts in the Scripts folder changed: list File ▸ Scripts' entries again in the
/// shortcut table.
pub fn scripts_changed(&mut self) {
self.shortcut_table = std::sync::OnceLock::new();
}
/// The active shortcut preset resolved against every bindable command, File ▸ Scripts'
/// entries included.
pub fn shortcuts(&self) -> &ShortcutTable {
self.shortcut_table.get_or_init(|| ShortcutTable::build(&self.keymaps, &self.ui_commands))
self.shortcut_table.get_or_init(|| {
let scripts: Vec<String> = crate::commands::scripts::scripts(self).into_iter().filter(|e| !e.panel).map(|e| e.name).collect();
ShortcutTable::build_with_scripts(&self.keymaps, &self.ui_commands, &scripts)
})
}
/// Remember a project in File ▸ Open Recent and in the crash-recovery sentinel.
+33 -2
View File
@@ -1,6 +1,7 @@
//! Keyboard shortcut presets (Edit ▸ Keyboard Shortcuts).
//!
//! Every menu entry, registry command and frontend command is *bindable*: it has a binding key
//! Every menu entry, registry command and frontend command, and every script File ▸ Scripts
//! lists, is *bindable*: it has a binding key
//! (the command id, plus its bound parameters for entries such as `app.settings
//! {"page":"general"}`), a scope (application-wide or a panel) and default keys. The built-in
//! preset "EffectCraft Default" is After Effects' default keyboard layout and is read-only; custom
@@ -157,6 +158,27 @@ pub fn bindables(ui: &[UiCommand]) -> Vec<Bindable> {
out
}
/// The File ▸ Scripts entries of the scripts `names` (file names, as the menu lists them), with
/// no default keys. A key is bound to the script's name, so it stays with that script when
/// scripts are added to or removed from the folder.
pub fn script_bindables(names: &[String]) -> Vec<Bindable> {
names
.iter()
.map(|n| {
let params = json!({"name": n});
Bindable {
key: binding_key("file.runScript", &params),
command: "file.runScript".into(),
params,
label: n.clone(),
path: vec!["File".into(), "Scripts".into()],
scope: APP_SCOPE.into(),
defaults: vec![],
}
})
.collect()
}
/// Canonical shortcut text (`cmd+shift+k`, `Shift+Cmd+K`, `⌘⇧K` → `Cmd+Shift+K`); `None` if it
/// has no key.
pub fn normalize(s: &str) -> Option<String> {
@@ -436,7 +458,16 @@ pub struct ShortcutTable {
impl ShortcutTable {
pub fn build(maps: &Keymaps, ui: &[UiCommand]) -> ShortcutTable {
let bindables = bindables(ui);
Self::build_with_scripts(maps, ui, &[])
}
/// [`ShortcutTable::build`] with the File ▸ Scripts entries of the scripts `scripts`, listed
/// after the other File ▸ Scripts entries.
pub fn build_with_scripts(maps: &Keymaps, ui: &[UiCommand], scripts: &[String]) -> ShortcutTable {
let mut bindables = bindables(ui);
let at = bindables.iter().rposition(|b| b.path.starts_with(&["File".to_string(), "Scripts".to_string()])).map_or(bindables.len(), |i| i + 1);
let new: Vec<Bindable> = script_bindables(scripts).into_iter().filter(|s| !bindables.iter().any(|b| b.key == s.key)).collect();
bindables.splice(at..at, new);
let keys = bindables.iter().map(|b| (b.key.clone(), maps.keys_of(b))).collect();
ShortcutTable { bindables, keys }
}
+6 -1
View File
@@ -179,7 +179,12 @@ fn menu_nodes(
"recentScripts" => app.session.prefs.recent_scripts.get(i).map(|r| r.text().to_string()),
_ => None,
};
let r = ui.add_enabled(entry_enabled(app, e), egui::Button::new((gutter(entry_checked(app, e) == Some(true)), e.label.as_str())));
let mut b = egui::Button::new((gutter(entry_checked(app, e) == Some(true)), e.label.as_str()));
// File ▸ Scripts entries show the key a script was given.
if let Some(s) = entry_shortcut(app, e) {
b = b.shortcut_text(shortcut_text(&s));
}
let r = ui.add_enabled(entry_enabled(app, e), b);
let r = match &tip {
Some(t) => r.on_hover_text(t),
None => r,
+27 -3
View File
@@ -196,8 +196,9 @@ pub fn build(app: &EffectcraftApp) -> NativeMenu {
label: e.label.clone(),
command: e.command.clone(),
params: e.params.clone(),
shortcut: None,
accelerator: None,
// File ▸ Scripts entries show the key a script was given.
shortcut: crate::menus::entry_shortcut(app, e),
accelerator: crate::menus::entry_shortcut(app, e).as_deref().and_then(accelerator),
enabled: crate::menus::entry_enabled(app, e),
checked: crate::menus::entry_checked(app, e),
}));
@@ -360,7 +361,12 @@ pub fn state_key(app: &EffectcraftApp) -> u64 {
s.history.undo.iter().for_each(|(l, _)| l.hash(&mut h));
s.state.open_comps.hash(&mut h);
s.active_comp_id().and_then(|c| s.state.views3d.get(&c)).map(|v| format!("{:?}", v.current)).hash(&mut h);
s.shortcuts().keys.len().hash(&mut h);
// Each binding's keys: a key changed to another one gives the item a new accelerator.
let shortcuts = s.shortcuts();
for b in &shortcuts.bindables {
b.key.hash(&mut h);
shortcuts.keys.get(&b.key).hash(&mut h);
}
s.keymaps.active.hash(&mut h);
let v = &app.ui.viewer;
(v.rulers, v.guides, v.snap_guides, v.lock_guides, v.grid, v.snap_grid, v.show_layer_controls, v.pasteboard).hash(&mut h);
@@ -722,6 +728,24 @@ mod tests {
assert!(spec2.items().iter().any(|i| i.command == "file.openRecent" && i.label == "Recent Test.ecproj"));
}
/// A script's key changed to another key changes the key, so the File ▸ Scripts entry gets
/// the new accelerator even though the number of bindings stays the same.
#[test]
fn a_changed_shortcut_changes_the_state_key() {
let mut app = app();
let script = json!({"name": "Rename Layers.jsx"});
let b = app.session.shortcuts().find(&effectcraft_engine::shortcuts::binding_key("file.runScript", &script)).cloned().unwrap();
app.session.keymaps.set(&b, &["Cmd+Alt+Shift+F7".to_string()]).unwrap();
app.session.shortcuts_changed();
let k0 = state_key(&app);
app.session.keymaps.set(&b, &["Cmd+Alt+Shift+F8".to_string()]).unwrap();
app.session.shortcuts_changed();
assert_ne!(state_key(&app), k0);
let spec = build(&app);
let entry = spec.items().into_iter().find(|i| i.command == "file.runScript" && i.params == script).unwrap();
assert_eq!(entry.shortcut.as_deref(), Some("Cmd+Alt+Shift+F8"));
}
/// #1630: a checked Window menu entry closes its panel; an unchecked one opens it.
#[test]
fn window_menu_entry_toggles_its_panel() {
@@ -49,6 +49,8 @@ const PANEL_COLOR: Color32 = Color32::from_rgb(0x3f, 0xa8, 0x6c);
pub fn open(app: &mut EffectcraftApp) {
app.dialog_state.shortcuts = EditorState::default();
// Scripts copied into the Scripts folder since the table was built are listed too.
app.session.scripts_changed();
app.dialog = Some(Dialog::Shortcuts);
}
+36
View File
@@ -113,6 +113,42 @@ fn panels_open_at_quit_dock_again_at_launch() {
assert_eq!(store.read(effectcraft_engine::commands::scripts::OPEN_PANELS_FILE).as_deref(), Some(r#"{"open":[]}"#));
}
/// A script copied into the Scripts folder while the app runs is listed in Edit ▸ Keyboard
/// Shortcuts and takes a key there: File ▸ Scripts shows the key next to the script, and
/// pressing it runs the script as its menu entry does.
#[test]
fn a_key_given_to_a_script_runs_it() {
use effectcraft_engine::config::{ConfigStore, MemoryConfig};
let store = std::sync::Arc::new(MemoryConfig::default());
let mut s = effectcraft_host::session();
s.config = Some(store.clone());
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_| EffectcraftApp::new(s));
h.run_steps(3);
assert!(h.state().session.shortcuts().find(r#"file.runScript {"name":"Rename Layers.jsx"}"#).is_some(), "the shortcut table is built");
store.write("Scripts/Add Comp.jsx", "app.project.items.addComp('From Key', 64, 64, 1, 1, 25);").unwrap();
effectcraft_ui_egui::panels::shortcut_editor::open(h.state_mut());
let script = json!({"name": "Add Comp.jsx"});
let r = h.state_mut().session.execute("shortcuts.set", json!({"command": "file.runScript", "params": script, "keys": "Cmd+Alt+Shift+F7"})).unwrap();
assert_eq!(r["command"], r#"file.runScript {"name":"Add Comp.jsx"}"#, "{r}");
assert_eq!(r["keys"], json!(["Cmd+Alt+Shift+F7"]), "{r}");
assert_eq!(r["conflicts"], json!([]), "{r}");
h.state_mut().dialog = None;
let spec = effectcraft_ui_egui::native_menu::build(h.state());
let entry = spec.items().into_iter().find(|i| i.command == "file.runScript" && i.params == script).expect("the File ▸ Scripts entry");
assert_eq!(entry.shortcut.as_deref(), Some("Cmd+Alt+Shift+F7"));
let (m, k, ..) = effectcraft_ui_egui::menus::bindings(&h.state().session)
.into_iter()
.find(|b| b.2 == "file.runScript" && b.3 == script)
.expect("the key is bound to the script");
let made = |h: &Harness<'_, EffectcraftApp>| h.state().session.project.items.values().filter(|i| i.name == "From Key").count();
assert_eq!(made(&h), 0);
for pressed in [true, false] {
h.event(Event::Key { key: k, physical_key: Some(k), pressed, repeat: false, modifiers: m });
h.step();
}
assert_eq!(made(&h), 1, "the key runs the script");
}
/// The bundled Ease Presets ScriptUI panel (Window ▸ Ease Presets.jsx) docks, and clicking a
/// preset in its list eases the selected keyframes in one undo step.
#[test]
+6 -6
View File
@@ -37,7 +37,7 @@ because 179 issues were closed with regression tests between 5 and 10 October.
| UI/UX fidelity | ≈ 65% | 60–120 | [ui-parity.md](ui-parity.md) |
| File formats | ≈ 50% | 130–230 | [file-format-parity.md](file-format-parity.md) |
| Hardware | ≈ 40% | 60–120 | [hardware-parity.md](hardware-parity.md) |
| Localization | ≈ 18% | 60–110 + review | [localization-parity.md](localization-parity.md) |
| Localization | ≈ 24% | 60–110 + review | [localization-parity.md](localization-parity.md) |
| Stability | ≈ 45% | 40–80 | 23 never-crash PRs landed on 4 October and the "Never crash" rules bind every crate, but crash reports are open (#580 Chromatic Aberrations pan/tilt, #501 crash on startup, #426 timeline drag while playing, #341 quit on Intel Mac, #234 macOS 27 exit, #879 `--gpu` export aborts on a failed allocation; #591, #306 and #511 are closed), and no CI workflow runs `cargo xtask ci` on pull requests |
| Real-user experience | ≈ 60%, uneven by platform | (in G2) | 179 issues closed 5–10 October (as counted on 10 October), nearly all with regression tests ([log below](#real-user-report-log-510-october)); 216 open and 328 closed on 12 October, many from Linux and Windows. Most checking still happens on macOS |
| Performance | unknown against After Effects, ≈ 35% | 50–100 | Internal numbers only (e.g. Advanced 3D 290 ms/frame at 1080p on the GPU on an M4 Pro). Users report stutter on cached frames (#595, #304; the RAM preview now prefetches nearest the playhead, 271000b2), late audio (#504, #335); slow scrubbing (#329, closed by #784) and #554 are closed. No 4K / hundreds-of-layers project tested |
@@ -72,7 +72,7 @@ are grouped by area; the issue tracker has the rest.
| 13 | **3D model formats and materials** | After Effects 26 imports FBX, USD/USDZ, STL besides glTF and OBJ (`usd_plugins`), and Substance 3D materials with mesh displacement (26.0–26.2); we read glTF, OBJ, STL (binary and ASCII, with facet colours) and FBX 7.x (binary and ASCII: meshes, hierarchy, Lambert/Phong materials and textures; no FBX animation or skinning yet) | Product shots and 3D scenes come as FBX/USDZ | 25–45 h | — · [file-format-parity.md](file-format-parity.md) |
| 14 | **Object Matte** (26.2, disk cache in 26.5) | First slice (#783): the Object Matte effect (Effect ▸ Matte) and `objectMatte.apply` turn a point (and optional box) on a reference frame into a matte and follow the object up to 600 frames each way, or segment every frame on its own; Roto Brush's engine, cache and model (MobileSAM from Settings ▸ Roto Brush, classical graph cut without it). Since then: the Object Matte tool in the viewer (click / drag a box, hover crosshair), the box as the model's box prompt, background tracking with progress and cancel, Freeze, and segmentations kept in the disk cache (26.5). Missing: a comparison with After Effects ([plan](fidelity/object-matte.md)) | The new one-click roto | 10–20 h | G7 · [target-app-parity.md](target-app-parity.md) |
| 15 | **Hardware video decode and encode** | No VideoToolbox, NVENC, Quick Sync or VA-API; #498 asks for it (answered: CUDA / NVENC need Nvidia's proprietary SDK through FFI, which the no-`unsafe` rule forbids); open PR #819 adds hardware decoding via `filmcraft-platform`; FilmCraft has VideoToolbox / NVENC / VA-API paths to borrow | 4K playback and export speed | 25–45 h | G5/G6 · [hardware-parity.md](hardware-parity.md) |
| 16 | **Localization depth** | Menus in 11 languages (ja, zh-hans, zh-hant, uk, es, pt-br, ru, fr, de, cs, ka: 628 rows each); panels in 7 (ja, zh-hans, zh-hant, uk, pt-br, cs, ka); effect names translated in Simplified Chinese and Georgian (307, plus the generated Effect menu), their ~1,650 parameter names untranslated; Korean and Italian (After Effects ships them) absent; Hangul probe added (#599 closed) | After Effects is used heavily in Japan, China, Korea, Brazil, Europe | 60–110 h + native review | G9 · [localization-parity.md](localization-parity.md) |
| 16 | **Localization depth** | Menus in 11 languages (ja, zh-hans, zh-hant, uk, es, pt-br, ru, fr, de, cs, ka: 630 rows each, 629 in Georgian); panels in 7 (ja 996 rows, zh-hans 998, zh-hant 951, uk 524, pt-br 695, cs 518, ka 885); effect names translated in Simplified Chinese (307), Japanese (297) and Georgian (307, plus the generated Effect menu), their ~1,650 parameter names untranslated; Korean and Italian (After Effects ships them) absent; Hangul probe added (#599 closed) | After Effects is used heavily in Japan, China, Korea, Brazil, Europe | 60–110 h + native review | G9 · [localization-parity.md](localization-parity.md) |
| 17 | **Media Browser lists files import refuses** | `media_browser.rs` lists AVI; `media::is_importable` has none of them (Targa and Radiance HDR import since #745; DPX and Cineon since #756, log shown as stored unless Interpret Footage ▸ Cineon Settings converts it; MXF OP1a / OP-Atom since #767; FilmCraft has no AVI demuxer); #431 and #324 closed | Confusing failures on common VFX formats | 15–30 h | G6 · [file-format-parity.md](file-format-parity.md) |
| 18 | **Relative paths and relinking** | Projects store absolute footage paths; no relink when a project moves | Moving or sharing a project breaks it | 6–12 h | G4 · [file-format-parity.md](file-format-parity.md) |
| 19 | **Plug-in ecosystem** | No After Effects SDK hosting (#600, #327 OpenFX, #334, #376 plug-in manager); our WebAssembly API has no third-party plug-ins | Many pro projects depend on Trapcode, Element, Saber… | 40–80 h for API growth; SDK hosting needs an owner decision | G8 · [scripting-parity.md](scripting-parity.md) |
@@ -311,10 +311,10 @@ Issues #41–#47 (all from the Linux AppImage 0.1.1, 5 October): panning the vie
([plugins.md](plugins.md#extension-points-compared-with-after-effects) compares them). ScriptUI
panels in the user's ScriptUI Panels folder are listed in the Window menu, dock like any panel
and open again at the next launch. The scripts in the Startup folder run at launch, and the desktop
app runs the Shutdown folder's scripts as it quits. Missing: a global scope that scripts share
for the session (a Startup script's variables and functions are not available to later
scripts), keyboard
shortcuts for scripts and `.jsxbin`; `#include` / `#target` directives landed (#759,
app runs the Shutdown folder's scripts as it quits. Each File ▸ Scripts entry can be given a key in
Edit ▸ Keyboard Shortcuts. Missing: a global scope that scripts share for the session (a Startup
script's variables and functions are not available to later scripts) and `.jsxbin`;
`#include` / `#target` directives landed (#759,
[scripting-parity.md](scripting-parity.md)).
- Done so far: ease presets (#254) cover what ease-curve plug-ins are used for: easing curves kept
by name and applied to pairs of keyframes. Modelled on a third-party panel, they are not core: a
+1 -1
View File
@@ -18,7 +18,7 @@ Guide):
| ExtendScript preprocessor directives (`#include`, `#target`, `#targetengine`) | `#include` / `//@include` / `#includepath` inline files relative to the script (once each, depth-limited, file gate applies); `#target`, `#targetengine`, `#strict` are ignored | Same |
| `Scripts/Startup` and `Scripts/Shutdown`: run in alphabetical order when the app starts and quits | `Scripts/Startup/` and `Scripts/Shutdown/` in the settings folder, in alphabetical order (case ignored): the Startup scripts run at launch before a project opens, and the Shutdown scripts run as the desktop app quits, after the project is closed. A Startup script that fails is reported in a toast and in the log, a Shutdown script that fails is reported in the log, and the next script still runs | Partial: each script runs in its own global scope, so variables and functions a Startup script defines are not available to later scripts; a Startup script's modal dialog stays open while the next scripts run and the app starts; a Shutdown script's modal dialogs close as if canceled; the web app runs no Shutdown scripts |
| `Scripts/ScriptUI Panels`: listed at the bottom of the Window menu, dockable like the app's own panels, `this` is the panel | `Scripts/ScriptUI Panels/` in the settings folder (File ▸ Scripts ▸ Install ScriptUI Panel… copies one there), listed at the bottom of the Window menu (a subfolder, one level, is a submenu named after it), docked like any panel, `this` is the panel; the panels open when the app quits open again at the next launch | Same. Where a panel was docked isn't kept: workspace layouts aren't saved between launches |
| Keyboard shortcuts for scripts (the Keyboard Shortcuts editor lists the scripts of the Scripts folder) | Shortcuts bind to commands and menu entries; the File ▸ Scripts and Window menu entries of scripts aren't offered | Gap |
| Keyboard shortcuts for scripts (the Keyboard Shortcuts editor lists the scripts of the Scripts folder) | Edit ▸ Keyboard Shortcuts lists the scripts of File ▸ Scripts under File ▸ Scripts (`file.runScript {"name": "<file name>"}`); a key given to one is saved with the preset, File ▸ Scripts shows it, and pressing it runs the script as its menu entry does | Same. After Effects binds a key to a script's place in the alphabetical list (`ExecuteScriptMenuItem01`…), and EffectCraft binds it to the script's file name. The Window menu entries of ScriptUI panels aren't offered |
| Running a script from the command line (`afterfx -r script.jsx`) | `effectcraft-cli script`, the control channel's and MCP's `script.run` | Same purpose, other entry points |
| `app.settings` (kept in the preferences between sessions) | Kept in `script_settings.json` in the settings folder (`script.settings.get` / `script.settings.save`) | Same |
| Allow Scripts to Write Files and Access Network | Settings ▸ Scripting & Expressions | Same |
+2 -1
View File
@@ -190,7 +190,8 @@ Effect Controls uses compact, fixed 20 pt parameter rows, as After Effects does:
Edit ▸ Keyboard Shortcuts (`Cmd+Alt+'`) opens the shortcut editor: an on-screen keyboard showing
which keys are assigned for the held modifiers (purple: application-wide, green: panel-specific,
both: split), modifier toggles, a searchable list of every command (all engine commands, menu
entries with bound parameters and the frontend's tool / panel commands), and a preset menu.
entries with bound parameters, the scripts File ▸ Scripts lists and the frontend's tool / panel
commands), and a preset menu.
- **Presets.** "EffectCraft Default" is After Effects' default layout and is read-only; editing
it creates a "Custom" copy. Custom presets store only their differences from the default and
+1 -1
View File
@@ -26,7 +26,7 @@ our own scripts; no real-world script library has been run against both apps.
| Startup / Shutdown folders | yes | `Scripts/Startup` runs at launch and `Scripts/Shutdown` runs as the desktop app quits, after the project is closed, each in alphabetical order | partial | `crates/script/src/tests/startup.rs`; each script has its own global scope, so a Startup script's variables and functions are not available to later scripts ([plugins.md](plugins.md)) |
| `#include`, `#target` directives | yes | `#include` / `//@include` / `#includepath` inline files relative to the including script (once each, nested at most 32 deep, under the scripts' file gate; errors name the included file and line); `#target`, `#targetengine`, `#strict` are ignored | good | `tests_include.rs` (#759); installed scripts and panels have no folder of their own, so they include from the project's folder or absolute paths |
| `.jsxbin` | yes | no | out of scope | No public specification |
| Keyboard shortcuts for scripts | yes | no | missing | plugins.md |
| Keyboard shortcuts for scripts | yes: the scripts of the Scripts folder take keys, by their place in the alphabetical list | Edit ▸ Keyboard Shortcuts lists the scripts of File ▸ Scripts; a key stays with the script's file name | same | `file_scripts_entries_take_keyboard_shortcuts`, `a_key_given_to_a_script_runs_it` ([plugins.md](plugins.md)); ScriptUI panels in the Window menu take no key |
| Recent Script Files | File ▸ Scripts ▸ Recent Script Files (10 scripts), a key to rerun | Same list, kept in the settings; Cmd+Alt+Shift+D reruns the most recent script | same | `recent_script_files_run_again` ([plugins.md](plugins.md)) |
| Command-line rendering `aerender` | yes | `effectcraft-cli render`, `script`, `exec` | same purpose | `apps/effectcraft-cli` |
| Essential Graphics scripting hooks | `addToMotionGraphicsTemplate…` | yes (M13.7) | good | — |