mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-25 18:47:07 -06:00
The After Effects stagger idiom id = thisProperty.propertyGroup(2).propertyIndex; failed with "not a callable function": the expression object model had no propertyGroup() and property groups had no propertyIndex. - Property.propertyGroup(countUp = 1) and PropertyGroup.propertyGroup() return the group countUp levels up; from a top-level group they reach the layer. A count below 1, not a number, or above the layer is an error naming propertyGroup, never a crash. - PropertyGroup.propertyIndex: the group's 1-based index among its siblings (Child requests now carry it). - One new host request (Ancestor) walks up from the node's full uid path, so it also works for the property being evaluated. - Tests for the stagger expression, effect parameters, reaching the layer, other layers' properties and groups and out-of-range counts; the fuzzer seeds and tokens include the new members.
125 lines
6.2 KiB
Rust
125 lines
6.2 KiB
Rust
//! `propertyGroup(countUp)` on properties and property groups, and a group's `propertyIndex`
|
||
//! (#1619): name-independent references up the property tree, as in After Effects.
|
||
|
||
use super::*;
|
||
|
||
const SCALE_1: &str = "contents/#1/transform/scale";
|
||
const SCALE_2: &str = "contents/#2/transform/scale";
|
||
|
||
/// The test comp with a second shape group "Rectangle 2" on layer "Shape", and "Rectangle 1"'s
|
||
/// Transform ▸ Scale keyed 0 → 100 over 0–1 s.
|
||
fn staggered() -> Fx {
|
||
let mut f = fx();
|
||
let g = group_push(&mut f.p, |ids| {
|
||
let rect = build::shape_rect(ids, [20.0, 20.0], [0.0, 0.0], 0.0);
|
||
build::shape_group(ids, "Rectangle 2", vec![rect])
|
||
});
|
||
let shape = f.layer_mut(f.shape);
|
||
shape.props.sub_mut("contents").unwrap().children.push(g.into());
|
||
shape.props.prop_mut(SCALE_1).unwrap().keys =
|
||
vec![Keyframe::new(Tick::ZERO, Value::Vec2([0.0, 0.0])), Keyframe::new(secs(1.0), Value::Vec2([100.0, 100.0]))];
|
||
f
|
||
}
|
||
|
||
impl Fx {
|
||
fn shape_num(&self, path: &str, text: &str) -> f64 {
|
||
match self.ev(self.shape, path, &format!("var r = ({text}); [r, r]"), 0.0) {
|
||
Ok(v) => v.as_vec2()[0],
|
||
Err(e) => panic!("{text}: {e}"),
|
||
}
|
||
}
|
||
fn shape_err(&self, path: &str, text: &str) -> String {
|
||
match self.ev(self.shape, path, text, 0.0) {
|
||
Ok(v) => panic!("{text}: expected an error, got {v:?}"),
|
||
Err(e) => e,
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn shape_group_stagger_from_the_adobe_community_answer() {
|
||
let f = staggered();
|
||
// thisProperty = Scale, propertyGroup(1) = Transform, propertyGroup(2) = the shape group.
|
||
let text = "offset = .1;\nid = thisProperty.propertyGroup(2).propertyIndex;\ncontent(\"Rectangle 1\").transform.scale.valueAtTime(time-id*offset)";
|
||
let v = f.ev(f.shape, SCALE_2, text, 0.5).unwrap().as_vec2();
|
||
// "Rectangle 2" is the second group in Contents: Rectangle 1's scale 0.2 s earlier.
|
||
assert_close!(v[0], 30.0);
|
||
assert_close!(v[1], 30.0);
|
||
}
|
||
|
||
#[test]
|
||
fn property_group_walks_up_from_the_evaluated_property() {
|
||
let f = staggered();
|
||
assert_close!(f.shape_num(SCALE_2, "thisProperty.propertyGroup(1).name == 'Transform' ? 1 : 0"), 1.0);
|
||
assert_close!(f.shape_num(SCALE_2, "thisProperty.propertyGroup().name == 'Transform' ? 1 : 0"), 1.0);
|
||
assert_close!(f.shape_num(SCALE_2, "thisProperty.propertyGroup(2).name == 'Rectangle 2' ? 1 : 0"), 1.0);
|
||
assert_close!(f.shape_num(SCALE_2, "thisProperty.propertyGroup(3).name == 'Contents' ? 1 : 0"), 1.0);
|
||
assert_close!(f.shape_num(SCALE_1, "thisProperty.propertyGroup(2).propertyIndex"), 1.0);
|
||
assert_close!(f.shape_num(SCALE_2, "thisProperty.propertyGroup(2).propertyIndex"), 2.0);
|
||
// A shape group holds Blend Mode, Contents and Transform, in After Effects' order.
|
||
assert_close!(f.shape_num(SCALE_2, "thisProperty.propertyGroup(1).propertyIndex"), 3.0);
|
||
assert_close!(f.shape_num(SCALE_2, "thisProperty.propertyGroup(2).numProperties"), 3.0);
|
||
assert_close!(f.shape_num(SCALE_2, "thisProperty.propertyGroup(3).numProperties"), 2.0);
|
||
// Groups walk on: Transform → group → Contents.
|
||
assert_close!(f.shape_num(SCALE_2, "thisProperty.propertyGroup(1).propertyGroup(2).name == 'Contents' ? 1 : 0"), 1.0);
|
||
assert_close!(f.shape_num(SCALE_2, "thisProperty.propertyGroup(1).propertyGroup(1).propertyIndex"), 2.0);
|
||
}
|
||
|
||
#[test]
|
||
fn property_group_reaches_the_layer() {
|
||
let f = fx();
|
||
assert_close!(f.num("thisProperty.propertyGroup(1).name == 'Transform' ? 1 : 0", 0.0), 1.0);
|
||
assert_close!(f.num("thisProperty.propertyGroup(2).name == 'A' ? 1 : 0", 0.0), 1.0);
|
||
assert_close!(f.num("thisProperty.propertyGroup(2).index", 0.0), 1.0);
|
||
assert_close!(f.num("thisProperty.propertyGroup(1).propertyGroup(1).name == 'A' ? 1 : 0", 0.0), 1.0);
|
||
let s = staggered();
|
||
assert_close!(s.shape_num(SCALE_2, "thisProperty.propertyGroup(4).name == 'Shape' ? 1 : 0"), 1.0);
|
||
}
|
||
|
||
#[test]
|
||
fn property_group_of_an_effect_parameter_is_the_effect() {
|
||
let f = fx();
|
||
let path = "effects/#1/slider";
|
||
let ev = |text: &str| f.ev(f.a, path, text, 0.0).unwrap().as_f64();
|
||
assert_close!(ev("thisProperty.propertyGroup(1).name == 'Slider Control' ? 1 : 0"), 1.0);
|
||
assert_close!(ev("thisProperty.propertyGroup(2).name == 'Effects' ? 1 : 0"), 1.0);
|
||
// The name-independent rig: read the effect this parameter belongs to by its own name.
|
||
assert_close!(ev("effect(thisProperty.propertyGroup(1).name)('Slider') + 1"), 43.0);
|
||
assert_close!(ev("thisProperty.propertyGroup(1).propertyIndex"), 1.0);
|
||
}
|
||
|
||
#[test]
|
||
fn property_group_on_other_layers_properties_and_groups() {
|
||
let f = staggered();
|
||
// From layer B: another layer's property and group, reached by name.
|
||
let ev = |text: &str| f.ev(f.b, ROT, text, 0.0).unwrap().as_f64();
|
||
assert_close!(ev("thisComp.layer('A').effect(1)('Slider').propertyGroup(1).name == 'Slider Control' ? 1 : 0"), 1.0);
|
||
assert_close!(ev("thisComp.layer('Shape').content('Rectangle 2').propertyIndex"), 2.0);
|
||
assert_close!(ev("thisComp.layer('Shape').content('Rectangle 2').transform.scale.propertyGroup(2).propertyIndex"), 2.0);
|
||
assert_close!(ev("thisComp.layer('Shape').content('Rectangle 1').content('Fill 1').propertyGroup(2).name == 'Rectangle 1' ? 1 : 0"), 1.0);
|
||
assert_close!(ev("thisComp.layer('Shape').content('Rectangle 1').propertyGroup(2).name == 'Shape' ? 1 : 0"), 1.0);
|
||
assert_close!(ev("thisComp.layer('A').effect('Slider Control').propertyIndex"), 1.0);
|
||
assert_close!(ev("thisComp.layer('A').mask('Mask 1').propertyGroup(1).numProperties"), 1.0);
|
||
}
|
||
|
||
#[test]
|
||
fn property_group_out_of_range_is_an_error_not_a_crash() {
|
||
let f = staggered();
|
||
for text in [
|
||
"thisProperty.propertyGroup(0)",
|
||
"thisProperty.propertyGroup(-1)",
|
||
"thisProperty.propertyGroup(5)",
|
||
"thisProperty.propertyGroup(1e300)",
|
||
"thisProperty.propertyGroup(NaN)",
|
||
"thisProperty.propertyGroup('x')",
|
||
"thisProperty.propertyGroup(1).propertyGroup(4)",
|
||
] {
|
||
let e = f.shape_err(SCALE_2, text);
|
||
assert!(e.contains("propertyGroup"), "{text}: {e}");
|
||
}
|
||
let e = f.err("thisProperty.propertyGroup(3)");
|
||
assert!(e.contains("propertyGroup"), "{e}");
|
||
// A fractional count rounds down, as an index does.
|
||
assert_close!(f.shape_num(SCALE_2, "thisProperty.propertyGroup(2.7).propertyIndex"), 2.0);
|
||
}
|