mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 13:08:07 -06:00
Merge pull request #2356 from dotCooCoo/feat/script-shortcuts
Scripts: File ▸ Scripts entries take keyboard shortcuts
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, ¶ms);
|
||||
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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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", ¶ms),
|
||||
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 }
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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 | — |
|
||||
|
||||
Reference in New Issue
Block a user