#809: layer.source and a Layer Control's layer work in expressions

layer.source was undefined in expressions, and a layer from a Layer Control had no transform.
Both now work.

Co-Authored-By: Claude Haiku 5.5 <[email protected]>
This commit is contained in:
Kristian MyramandClaude Haiku 5.5 committed 2026-10-12 19:19:11 +02:00
1 parent 470ea7a3af
commit 4d105f02d4
4 files changed
+79 -2

No files matched your search

+26
View File
@@ -63,6 +63,10 @@ pub enum Req {
Sample { comp: u64, layer: u64, x: Secs, y: Secs, rx: Secs, ry: Secs, post: bool, t: Secs },
/// `thisProject`: `[bitsPerChannel, linearBlending, workingSpace]`.
Project,
/// A layer's source item (AE's `Layer.source`): `[kind, id, name, width, height, duration,
/// frameDuration, pixelAspect]`, kind `"comp"` (id = the precomp's composition) or `"footage"`
/// (footage, solids and models), or null for layers without an item (text, shape, null…).
SourceInfo { comp: u64, layer: u64 },
}
/// Parse CSV/TSV text (RFC 4180 quoting) into rows of cells.
@@ -457,6 +461,28 @@ impl<'a> Resolver<'a> {
Resp::Bool(project.settings.blend_linear || project.settings.linearize),
Resp::Str(project.settings.working_space.map(|w| format!("{w:?}")).unwrap_or_default()),
]),
Req::SourceInfo { comp, layer } => {
let c = self.comp(*comp)?;
let l = self.layer(*comp, *layer)?;
let it = project.item(l.source.item()?)?;
let (kind, aspect, frame, duration) = match &it.kind {
ItemKind::Comp(nc) => ("comp", nc.pixel_aspect, nc.frame_duration().seconds(), nc.duration.seconds()),
ItemKind::Footage(f) => ("footage", f.pixel_aspect, f.frame_rate.frame_duration().seconds(), f.duration.seconds()),
ItemKind::Solid(s) => ("footage", s.pixel_aspect, c.frame_duration().seconds(), c.duration.seconds()),
ItemKind::Folder => return None,
};
let (w, h) = it.dimensions().unwrap_or((0, 0));
Resp::List(vec![
Resp::Str(kind.into()),
Resp::Num(it.id.0 as f64),
Resp::Str(it.name.clone()),
Resp::Num(w as f64),
Resp::Num(h as f64),
Resp::Num(duration),
Resp::Num(frame),
Resp::Num(aspect),
])
}
Req::Sample { comp, layer, x, y, rx, ry, post, t } => {
let l = self.layer(*comp, *layer)?;
let f = |b: &Secs| f64::from_bits(*b);
+18 -2
View File
@@ -18,7 +18,7 @@ var selectorValue = [100, 100, 100];
// Request ops (must match runtime.rs).
var __COMP = 0, __COMPNAME = 1, __LAYER = 2, __LINFO = 3, __CHILD = 4, __PINFO = 5, __VALUE = 6, __XFORM = 7, __RECT = 8, __MARKERS = 9, __DOC = 10;
var __FOOT = 11, __FDATA = 12, __SAMPLE = 13, __PROJ = 14;
var __FOOT = 11, __FDATA = 12, __SAMPLE = 13, __PROJ = 14, __SOURCE = 15;
function __begin(t, v, c, l, path, uid, idx, fd, ti, tt, sv) {
time = t;
@@ -569,6 +569,14 @@ Layer.prototype = {
get enabled() { return this.__info[11]; },
get active() { return this.__info[11] && time >= this.inPoint && time < this.outPoint; },
get containingComp() { return new Comp(this.__c); },
// The source item (AE's Layer.source): a Comp for a precomp layer, a Footage for footage, solid
// and model layers, null for text, shape, null, camera and light layers (#809).
get source() {
var s = __h(__SOURCE, this.__c, this.__l);
if (s === null) return null;
if (s[0] === 'comp') return new Comp(s[1]);
return new Footage([s[2], s[3], s[4], s[5], s[6], s[7], s[0], null, null]);
},
__group: function (k) {
var g = __child(this.__c, this.__l, '', k);
if (g === undefined) __err('Layer "' + this.name + '" has no property "' + k + '"');
@@ -810,10 +818,18 @@ Prop.prototype = {
var __propHandler = {
get: function (t, k) {
if (typeof k !== 'string' || k in t) return t[k];
if (typeof k !== 'string') return t[k];
// A layer-valued property (a Layer Control's Layer) is named as its layer (#809).
if (k === 'name' && t.__info[2] === 'layer') {
var lv = t.value;
if (lv !== null && lv !== undefined && lv.__isLayer) return lv.name;
}
if (k in t) return t[k];
var v = t.value;
if (v === null || v === undefined) return undefined;
var x = v[k];
// Property groups are callable proxies: hand them over as they are, not bound like methods (#809).
if (x !== null && x !== undefined && x.__isGroup) return x;
return typeof x === 'function' ? x.bind(v) : x;
},
};
+1
View File
@@ -185,6 +185,7 @@ fn native_host(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsV
t: secs(num(args, 8, ctx)?),
},
14 => Req::Project,
15 => Req::SourceInfo { comp: id(args, 1, ctx)?, layer: id(args, 2, ctx)? },
_ => return Err(JsNativeError::typ().with_message("unknown host request").into()),
};
let found = FRAMES.with(|f| {
+34
View File
@@ -1251,3 +1251,37 @@ fn pending_text_style_baseline_setters_are_visible_to_expression_getters() {
assert_eq!(x.num("thisComp.layer('Title').text.sourceText.style.setBaselineOption('superscript').isSuperscript ? 1 : 0", 0.0), 1.0);
assert_eq!(x.text("var s = text.sourceText.style.setSuperscript(true); s.setText(s.isSuperscript ? 'super' : 'normal')", 0.0), "super");
}
#[test]
fn layer_source_is_the_layers_source_item() {
let mut f = fx();
// A solid's source is an item of the solid's size and name (#809).
assert_close!(f.num("thisComp.layer('A').source.width", 0.0), 20.0);
assert_close!(f.num("thisComp.layer('A').source.height", 0.0), 20.0);
assert_close!(f.num("thisComp.layer('A').source.name == 'A' ? 1 : 0", 0.0), 1.0);
// Text and shape layers have no source item.
assert_close!(f.num("thisComp.layer('Title').source == null ? 1 : 0", 0.0), 1.0);
// A precomp layer's source is its composition.
let main = f.p.comp(f.cid).unwrap().clone();
let other = f.p.items.values().find(|i| i.name == "Other").unwrap().id;
let pre = build::layer(&mut f.p, &main, "Pre", LayerSource::Comp { item: other }, (64, 32), None);
f.p.comp_mut(f.cid).unwrap().layers.push(pre);
assert_close!(f.num("thisComp.layer('Pre').source.width", 0.0), 64.0);
assert_close!(f.num("thisComp.layer('Pre').source.numLayers", 0.0), 1.0);
}
#[test]
fn layer_control_property_stands_for_its_layer() {
let mut f = fx();
// Layer A gets a Layer Control ("Map") that points at layer B (#809).
let spec = effectcraft_effects::find("ec.control.layer").unwrap();
let mut map = group_push(&mut f.p, |ids| effectcraft_effects::instantiate(spec, ids, "Map", [20.0, 20.0]));
map.prop_mut("layer").unwrap().value = Value::Layer(Some(f.b.0));
f.p.comp_mut(f.cid).unwrap().layers[0].props.sub_mut("effects").unwrap().children.push(map.into());
// effect("Map")(1) is the layer itself: its name, index, transform and value.
assert_close!(f.num("effect('Map')(1).name == 'B' ? 1 : 0", 0.0), 1.0);
assert_close!(f.num("effect('Map')(1).index", 0.0), 2.0);
assert_close!(f.num("effect('Map')(1).transform.position[0]", 0.0), 100.0);
assert_close!(f.num("effect('Map')(1).transform.scale[0]", 0.0), 100.0);
assert_close!(f.num("effect('Map')(1).value.name == 'B' ? 1 : 0", 0.0), 1.0);
}