Clipboard: effect properties in twirl-down groups, numbered as After Effects does (#1908)

This commit is contained in:
tylerjenningsw committed 2026-10-20 10:23:52 -04:00
1 parent 05f2daa4ca
commit dac6421ec3
2 files changed
+60 -4

No files matched your search

+37 -3
View File
@@ -181,16 +181,50 @@ fn mask(chain: &[&Node], pr: &Property) -> Option<Channel> {
}
/// An effect property: `Effects`, the effect's name (not a custom one) and place on the layer,
/// then the property's name and place, counting the effect's hidden input layer as the first.
/// then the property's name and After Effects index. Properties in twirl-down groups are
/// written without their group, as After Effects does.
fn effect(chain: &[&Node], pr: &Property) -> Option<Channel> {
let [Node::Group(effects), Node::Group(fx), _] = chain else { return None };
let [Node::Group(effects), Node::Group(fx), ..] = chain else { return None };
let GroupKind::Effect { effect } = &fx.kind else { return None };
let name = effectcraft_effects::find(effect).map_or(fx.name.as_str(), |spec| spec.name);
let (labels, values) = by_ui(pr)?;
let header = format!("Effects\t{name} #{}\t{} #{}", place(effects, fx.uid)?, pr.name, place(fx, pr.uid)? + 1);
let header = format!("Effects\t{name} #{}\t{} #{}", place(effects, fx.uid)?, pr.name, effect_index(fx, pr.uid)?);
Some(Channel { header, labels, values })
}
/// After Effects' index of an effect property: the effect's hidden input layer is #1, and a
/// twirl-down group takes an index where it opens and another where it closes. The hidden
/// source choice EffectCraft keeps beside a layer parameter has none.
fn effect_index(fx: &PropGroup, uid: Uid) -> Option<usize> {
fn count(g: &PropGroup, uid: Uid, n: &mut usize) -> bool {
for c in &g.children {
match c {
Node::Prop(p) if is_layer_source(g, p) => {}
Node::Prop(p) => {
*n += 1;
if p.uid == uid {
return true;
}
}
Node::Group(sub) => {
*n += 1;
if count(sub, uid, n) {
return true;
}
*n += 1;
}
}
}
false
}
let mut n = 1;
count(fx, uid, &mut n).then_some(n)
}
fn is_layer_source(g: &PropGroup, p: &Property) -> bool {
p.ui == ParamUi::Hidden && g.props().any(|l| l.ui == ParamUi::Layer && effectcraft_effects::layer_source_id(&l.match_id) == p.match_id)
}
/// 1-based position of a child in its group.
fn place(group: &PropGroup, uid: Uid) -> Option<usize> {
group.children.iter().position(|c| c.uid() == uid).map(|i| i + 1)
+23 -1
View File
@@ -186,7 +186,8 @@ fn copy_props(s: &mut Session, lid: LayerId, paths: &[&str]) -> String {
#[test]
fn effect_properties_are_written_as_after_effects_does() {
// The blocks After Effects 2019 wrote for the same effects and values.
// The blocks After Effects 2019 wrote for the same effects and values (2019 has no Dropdown
// Menu Control: that block follows its other popups, written 1-based with no unit).
let (mut s, l) = session();
for fx in ["Fill", "Checkbox Control", "Dropdown Menu Control", "Slider Control", "Point Control"] {
s.execute("effect.apply", json!({"layer": l.0, "effect": fx})).unwrap();
@@ -216,6 +217,27 @@ fn effect_properties_are_written_as_after_effects_does() {
);
}
#[test]
fn effect_properties_have_after_effects_indices() {
// After Effects 2019 numbered Fractal Noise's Rotation (in the Transform group) #9,
// Complexity (after it) #17 and Evolution #25: a group takes an index where it opens and
// closes, and its properties are written without it. Set Matte's Use For Matte is #3: the
// layer parameter before it has no extra index for its source choice.
let (mut s, l) = session();
for fx in ["Fractal Noise", "Set Matte"] {
s.execute("effect.apply", json!({"layer": l.0, "effect": fx})).unwrap();
}
let text = copy_props(&mut s, l, &["effects/#1/transform/rotation", "effects/#1/complexity", "effects/#1/evolution", "effects/#2/takeMatteFrom"]);
for header in [
"Effects\tFractal Noise #1\tRotation #9\n",
"Effects\tFractal Noise #1\tComplexity #17\n",
"Effects\tFractal Noise #1\tEvolution #25\n",
"Effects\tSet Matte #2\tUse For Matte #3\n",
] {
assert!(text.contains(header), "{header}in\n{text}");
}
}
#[test]
fn mask_properties_are_written_as_after_effects_does() {
// The blocks After Effects 2019 wrote for the same masks and values: masks go by place, not