Merge pull request #763 from vangel76/script-758-callable-props

#758: layers, property groups and properties are callable
This commit is contained in:
Brandon Thomas 2026-10-12 03:03:29 -04:00
commit fef320e9dd
4 changed files with 122 additions and 5 deletions

View File

@ -83,6 +83,8 @@ pub fn run_code(s: &mut Session, code: &str, name: &str) -> Outcome {
#[cfg(test)]
mod tests;
#[cfg(test)]
mod tests_callable;
#[cfg(test)]
mod tests_ease_presets;
#[cfg(test)]
mod tests_panels;

View File

@ -340,6 +340,25 @@ var __attr = {
},
};
// Layers, property groups and properties are callable, like After Effects: `layer("Effects")`,
// `fx(1)` and `effect("Slider Control")("Slider")` are `.property(...)`. A Proxy is callable
// only around a function, so the object's state moves onto an arrow function (no `prototype`;
// its own `name` and `length` are removed so the object's getters show) with the object's
// prototype; `instanceof` and attribute access are unchanged.
function __callable(o) {
var f = () => undefined;
delete f.name;
delete f.length;
Object.setPrototypeOf(f, Object.getPrototypeOf(o));
var keys = Object.keys(o);
for (var i = 0; i < keys.length; i++) f[keys[i]] = o[keys[i]];
var self = new Proxy(f, {
get: __attr.get,
apply: function (t, thisArg, args) { return self.property(args[0]); },
});
return self;
}
// 1-based collections with `[n]` access.
var __coll = {
get: function (t, k, r) {
@ -604,7 +623,7 @@ function PropertyGroup(layer, uid) {
this.__layer = layer;
this.__uid = uid;
this.__kind = "PropertyGroup";
return new Proxy(this, __attr);
return __callable(this);
}
PropertyGroup.prototype = Object.create(PropertyBase.prototype);
PropertyGroup.prototype.constructor = PropertyGroup;
@ -647,7 +666,7 @@ function MaskPropertyGroup(layer, uid) {
this.__layer = layer;
this.__uid = uid;
this.__kind = "MaskPropertyGroup";
return new Proxy(this, __attr);
return __callable(this);
}
MaskPropertyGroup.prototype = Object.create(PropertyGroup.prototype);
MaskPropertyGroup.prototype.constructor = MaskPropertyGroup;
@ -679,7 +698,7 @@ function Property(layer, uid) {
this.__layer = layer;
this.__uid = uid;
this.__kind = "Property";
return new Proxy(this, __attr);
return __callable(this);
}
Property.prototype = Object.create(PropertyBase.prototype);
Property.prototype.constructor = Property;
@ -981,7 +1000,7 @@ Layer.prototype = {
this.__comp = comp;
this.__id = id;
this.__kind = kind;
return new Proxy(this, __attr);
return __callable(this);
},
__info: function () { return __get("layer", { comp: this.__comp, layer: this.__id }); },
__p: function (o) {

View File

@ -0,0 +1,96 @@
//! Layers, property groups and properties are callable like After Effects (#758):
//! `layer("ADBE Effect Parade")`, `fxGroup(1)` and `fx("Slider")` are `.property(...)`.
use std::sync::Arc;
use effectcraft_engine::Session;
use effectcraft_engine::keyframe::Value as KV;
use serde_json::json;
use crate::{Outcome, run_code};
fn session() -> Session {
let mut s = Session { expr: Some(Arc::new(effectcraft_expr::Expressions)), expr_check: Some(effectcraft_expr::check_syntax), ..Default::default() };
crate::install(&mut s);
s
}
fn ok(s: &mut Session, code: &str) -> Outcome {
let o = run_code(s, code, "test.jsx");
assert!(o.error.is_none(), "script failed: {:?}\noutput: {:?}", o.error, o.output);
o
}
#[test]
fn layers_groups_and_properties_are_callable() {
let mut s = session();
let o = ok(
&mut s,
r#"
var c = app.project.items.addComp("M", 200, 200, 1, 2, 30);
var l = c.layers.addSolid([1, 1, 1], "S", 200, 200, 1);
l("ADBE Effect Parade").addProperty("ADBE Slider Control");
var fxGroup = l("Effects");
var fx = fxGroup(1);
fx("Slider").setValue(42);
l("ADBE Mask Parade").addProperty("ADBE Mask Atom");
var pos = l("ADBE Transform Group")("ADBE Position");
[
l("ADBE Effect Parade").matchName, fxGroup.numProperties, fx.matchName, fx.name,
l("Effects")("Slider Control")("Slider").value, fxGroup(1)(1).value,
l("Masks")(1).matchName, l("Masks")(1)("ADBE Mask Shape").name,
pos.matchName, pos.name, pos.propertyIndex, pos.parentProperty("Opacity").value,
pos.parentProperty.parentProperty(1).matchName === l.property(1).matchName,
l("Nope") === null, fxGroup(9) === null, pos("anything") === null,
l instanceof AVLayer, fxGroup instanceof PropertyGroup, pos instanceof Property, l.name,
l.transform.position.matchName, l.Effects(1).matchName
]
"#,
);
assert_eq!(
o.result,
json!([
"ADBE Effect Parade",
1,
"ADBE Slider Control",
"Slider Control",
42,
42,
"ADBE Mask Atom",
"Mask Path",
"ADBE Position",
"Position",
2,
100,
true,
true,
true,
true,
true,
true,
true,
"S",
"ADBE Position",
"ADBE Slider Control"
])
);
let c = s.project.items.values().find_map(|i| i.as_comp()).expect("comp");
let l = c.layers.first().expect("layer");
assert_eq!(l.props.prop("effects/#1/slider").map(|p| p.value.clone()), Some(KV::Scalar(42.0)));
}
#[test]
fn calling_without_a_name_is_a_script_error() {
let mut s = session();
for code in [
"var c = app.project.items.addComp('M', 100, 100, 1, 1, 25); var l = c.layers.addNull(); l();",
"var c = app.project.items.addComp('M', 100, 100, 1, 1, 25); var l = c.layers.addNull(); l('Transform')(null);",
"var c = app.project.items.addComp('M', 100, 100, 1, 1, 25); var l = c.layers.addNull(); new l('Transform');",
] {
let o = run_code(&mut s, code, "test.jsx");
assert!(o.error.is_some(), "expected an error for {code}");
}
// A called layer still reports itself as a layer, and its JSON result is unchanged.
let o = ok(&mut s, "var c = app.project.items.addComp('N', 100, 100, 1, 1, 25); var l = c.layers.addNull(); l.name = 'Ctl'; l");
assert_eq!(o.result, json!({ "type": "[object AVLayer]", "name": "Ctl" }));
}

View File

@ -18,7 +18,7 @@ our own scripts; no real-world script library has been run against both apps.
| Expression object model | `thisComp`, `thisLayer`, `wiggle`, `loopOut`, `valueAtTime`, `sampleImage`, `footage()`, text style API… | Same members, from the reference | good | `crates/expr/src/tests.rs`; behaviour of edge cases unmeasured (G1) |
| Expression editing | Inline field, error bar, pick whip, Expression Language menu, external editors | Inline field that grows, error bar, pick whips (property and value), language menu | partial | External editor support (#364) |
| Expression performance | Cached, multi-frame | Per-thread boa contexts | unknown | No benchmark |
| Scripting object model | `app`, `Project`, items, layers, properties, keys, eases, render queue, `TextDocument`, `Shape`, markers, `File`/`Folder`, `Socket`, `app.settings` | Same classes, edits run through undoable engine commands; match names table | good | `crates/script` tests (≈ 10 test files) |
| Scripting object model | `app`, `Project`, items, layers, properties, keys, eases, render queue, `TextDocument`, `Shape`, markers, `File`/`Folder`, `Socket`, `app.settings` | Same classes, edits run through undoable engine commands; match names table; layers, groups and properties are callable (`layer("Effects")(1)` is `.property(...)`) | good | `crates/script` tests (≈ 10 test files) |
| ScriptUI | Dialogs, palettes, dockable panels, resource strings, `onDraw` / ScriptUIGraphics | Same, dockable panels reopen at launch | good | `ui_scriptui`, `tests_ui*` |
| Scripts menu, ScriptUI Panels folder | yes | yes | same | [plugins.md](plugins.md) |
| Startup / Shutdown folders | yes | not run | missing | plugins.md |