Scripting: After Effects match names for 14 effects and Casts Shadows (#3122)

* #3113: use After Effects' effect match names in scripting

Checked every row of crates/script/src/matchnames.rs against After Effects
26.2 (app.effects, canAddProperty and the layer property trees). Fourteen
effect names were wrong: twelve did not exist (Pixel Motion Blur is
ADBE OFMotionBlur, Shadow/Highlight is ADBE ShadowHighlight, Ellipse is
ADBE ELLIPSE, ...), and Noise and Photo Filter pointed at After Effects'
hidden obsolete versions (now ADBE Noise2 and ADBE PhotoFilterPS). A
light's Casts Shadows is ADBE Casts Shadows.

The old names stay accepted for lookup (addProperty, property(), and
parameters such as "ADBE Noise-0001"), so existing scripts keep working;
.matchName emits only the After Effects name. A names-only fixture of
After Effects' first-party match names backs the new test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Scripting: match names that changed only in case still find effects and parameters

property("ADBE Ellipse") and "ADBE Grow Bounds-0001" found nothing after the names became
ADBE ELLIPSE / ADBE GROW BOUNDS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Valentin KleinandClaude Opus 5.5 authored and GitHub committed 2026-11-06 17:36:28 +01:00
1 parent 9408d70da0
commit fee50dbfd1
4 files changed
+490 -17

No files matched your search

+108 -17
View File
@@ -149,7 +149,7 @@ const LIGHT: &[(&str, &str)] = &[
("falloff", "ADBE Light Falloff Type"),
("radius", "ADBE Light Falloff Start"),
("falloffDistance", "ADBE Light Falloff Distance"),
("castsShadows", "ADBE Light Casts Shadows"),
("castsShadows", "ADBE Casts Shadows"),
("shadowDarkness", "ADBE Light Shadow Darkness"),
("shadowDiffusion", "ADBE Light Shadow Diffusion"),
];
@@ -309,12 +309,12 @@ pub const EFFECTS: &[(&str, &str)] = &[
("ec.color.changetocolor", "ADBE Change To Color"),
("ec.color.colorama", "APC Colorama"),
("ec.color.leavecolor", "ADBE Leave Color"),
("ec.color.photofilter", "ADBE Photo Filter"),
("ec.color.photofilter", "ADBE PhotoFilterPS"),
("ec.color.vibrance", "ADBE Vibrance"),
("ec.color.lumetri", "ADBE Lumetri"),
("ec.color.shadowhighlight", "ADBE Shadow/Highlight"),
("ec.color.selectivecolor", "ADBE Selective Color"),
("ec.color.gammapedestalgain", "ADBE Gamma/Pedestal/Gain"),
("ec.color.shadowhighlight", "ADBE ShadowHighlight"),
("ec.color.selectivecolor", "ADBE SelectiveColor"),
("ec.color.gammapedestalgain", "ADBE Gamma/Pedestal/Gain2"),
("ec.color.equalize", "ADBE Equalize"),
("ec.color.autocontrast", "ADBE AutoContrast"),
("ec.color.autolevels", "ADBE AutoLevels"),
@@ -351,15 +351,15 @@ pub const EFFECTS: &[(&str, &str)] = &[
("ec.generate.grid", "ADBE Grid"),
("ec.generate.checkerboard", "ADBE Checkerboard"),
("ec.generate.circle", "ADBE Circle"),
("ec.generate.ellipse", "ADBE Ellipse"),
("ec.generate.ellipse", "ADBE ELLIPSE"),
("ec.generate.cellpattern", "ADBE Cell Pattern"),
("ec.generate.lensflare", "ADBE Lens Flare"),
("ec.generate.vegas", "ADBE Vegas"),
("ec.generate.writeon", "ADBE Write On"),
("ec.generate.vegas", "APC Vegas"),
("ec.generate.writeon", "ADBE Write-on"),
("ec.generate.audiospectrum", "ADBE AudSpect"),
("ec.generate.audiowaveform", "ADBE AudWave"),
("ec.generate.beam", "ADBE Laser"),
("ec.generate.radiowaves", "ADBE Radio Waves"),
("ec.generate.radiowaves", "APC Radio Waves"),
("ec.generate.scribble", "ADBE Scribble Fill"),
("ec.generate.paintbucket", "ADBE Paint Bucket"),
("ec.key.colorkey", "ADBE Color Key"),
@@ -371,12 +371,12 @@ pub const EFFECTS: &[(&str, &str)] = &[
("ec.key.spill", "ADBE Spill2"),
("ec.matte.simplechoker", "ADBE Simple Choker"),
("ec.matte.mattechoker", "ADBE Matte Choker"),
("ec.noise.noise", "ADBE Noise"),
("ec.noise.noise", "ADBE Noise2"),
("ec.noise.fractal", "ADBE Fractal Noise"),
("ec.noise.median", "ADBE Median"),
("ec.noise.addgrain", "VISINF Grain Implant"),
("ec.noise.dustscratches", "ADBE Dust & Scratches"),
("ec.noise.turbulent", "ADBE Turbulent Noise"),
("ec.noise.turbulent", "ADBE AIF Perlin Noise 3D"),
("ec.perspective.dropshadow", "ADBE Drop Shadow"),
("ec.perspective.radialshadow", "ADBE Radial Shadow"),
("ec.perspective.bevelalpha", "ADBE Bevel Alpha"),
@@ -401,32 +401,73 @@ pub const EFFECTS: &[(&str, &str)] = &[
("ec.time.echo", "ADBE Echo"),
("ec.time.posterizetime", "ADBE Posterize Time"),
("ec.time.timedisplacement", "ADBE Time Displacement"),
("ec.time.timedifference", "ADBE Time Difference"),
("ec.time.pixelmotionblur", "ADBE Pixel Motion Blur"),
("ec.time.timedifference", "ADBE Difference"),
("ec.time.pixelmotionblur", "ADBE OFMotionBlur"),
("ec.time.timewarp", "ADBE Timewarp"),
("ec.transition.linearwipe", "ADBE Linear Wipe"),
("ec.transition.radialwipe", "ADBE Radial Wipe"),
("ec.transition.venetian", "ADBE Venetian Blinds"),
("ec.transition.blockdissolve", "ADBE Block Dissolve"),
("ec.transition.gradientwipe", "ADBE Gradient Wipe"),
("ec.transition.iriswipe", "ADBE Iris Wipe"),
("ec.transition.iriswipe", "ADBE IRIS_WIPE"),
("ec.utility.applylut", "ADBE Apply Color LUT2"),
("ec.utility.cineon", "ADBE Cineon Converter2"),
("ec.utility.growbounds", "ADBE Grow Bounds"),
("ec.utility.growbounds", "ADBE GROW BOUNDS"),
];
fn find<'a>(t: &'a [(&str, &'a str)], k: &str) -> Option<&'a str> {
t.iter().find(|(a, _)| *a == k).map(|(_, b)| *b)
}
/// Effect match names that resolve to one of our effects but are not the name we emit (#3113):
/// After Effects' hidden obsolete versions (`ADBE Noise`, `ADBE Photo Filter`), which its
/// `addProperty` still accepts, and the names earlier EffectCraft versions emitted by mistake,
/// which scripts written against them may use. Lookup only: `effect()` gives the `EFFECTS` name.
const LEGACY_EFFECTS: &[(&str, &str)] = &[
("ADBE Noise", "ec.noise.noise"),
("ADBE Photo Filter", "ec.color.photofilter"),
("ADBE Shadow/Highlight", "ec.color.shadowhighlight"),
("ADBE Selective Color", "ec.color.selectivecolor"),
("ADBE Gamma/Pedestal/Gain", "ec.color.gammapedestalgain"),
("ADBE Vegas", "ec.generate.vegas"),
("ADBE Write On", "ec.generate.writeon"),
("ADBE Radio Waves", "ec.generate.radiowaves"),
("ADBE Turbulent Noise", "ec.noise.turbulent"),
("ADBE Time Difference", "ec.time.timedifference"),
("ADBE Pixel Motion Blur", "ec.time.pixelmotionblur"),
("ADBE Iris Wipe", "ec.transition.iriswipe"),
];
/// AE match name of an effect id (falls back to the id).
pub fn effect(id: &str) -> String {
find(EFFECTS, id).map(str::to_string).unwrap_or_else(|| id.to_string())
}
/// Our effect id for an AE effect match name.
/// Our effect id for an AE effect match name (or a legacy one, see `LEGACY_EFFECTS`).
pub fn effect_from_match(m: &str) -> Option<&'static str> {
EFFECTS.iter().find(|(_, b)| b.eq_ignore_ascii_case(m)).map(|(a, _)| *a)
EFFECTS
.iter()
.find(|(_, b)| b.eq_ignore_ascii_case(m))
.map(|(a, _)| *a)
.or_else(|| LEGACY_EFFECTS.iter().find(|(b, _)| b.eq_ignore_ascii_case(m)).map(|(_, a)| *a))
}
/// The current match name for a legacy name a script may still use: a `LEGACY_EFFECTS` effect
/// name, one of its parameters (`ADBE Noise-0001` → `ADBE Noise2-0001`), or the light's Casts
/// Shadows under its earlier name. `None` when `name` is not a legacy name.
pub fn current_name(name: &str) -> Option<String> {
if name == "ADBE Light Casts Shadows" {
return Some("ADBE Casts Shadows".into());
}
let legacy = |m: &str| LEGACY_EFFECTS.iter().find(|(b, _)| b.eq_ignore_ascii_case(m)).map(|(_, id)| effect(id));
if let Some(m) = legacy(name) {
return Some(m);
}
let (fx, n) = name.rsplit_once('-')?;
if n.len() != 4 || !n.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
legacy(fx).map(|m| format!("{m}-{n}"))
}
/// Our shape item kind (`layer.addShapeItem`) for an AE match name, our match id or a display
@@ -573,6 +614,56 @@ pub fn node(chain: &[&Node]) -> String {
mod tests {
use super::*;
/// After Effects 26.2's `app.effects` (first-party match names and categories), #3113.
const AE_EFFECTS: &str = include_str!("../tests/fixtures/ae-effect-match-names.tsv");
fn ae_effects() -> Vec<(&'static str, &'static str)> {
AE_EFFECTS.lines().filter(|l| !l.starts_with('#') && !l.is_empty()).map(|l| l.split_once('\t').unwrap_or((l, ""))).collect()
}
/// #3113: every effect match name we emit is one After Effects lists (case matters: its
/// `addProperty("ADBE Ellipse")` fails), and the current version, not a hidden obsolete one,
/// unless After Effects only has that one.
#[test]
fn effect_names_are_after_effects_names() {
let ae = ae_effects();
// Obsolete in After Effects without a current replacement (Puppet is added by its tool).
let only_obsolete = ["ADBE Color Key", "ADBE Luma Key", "ADBE FreePin3"];
for (id, m) in EFFECTS {
let Some((_, cat)) = ae.iter().find(|(n, _)| n == m) else { panic!("{id}: After Effects has no effect {m:?}") };
assert!(!cat.contains("Obsolete") || only_obsolete.contains(m), "{id}: {m:?} is After Effects' obsolete version");
}
assert_eq!(effect("ec.noise.noise"), "ADBE Noise2");
assert_eq!(effect("ec.time.pixelmotionblur"), "ADBE OFMotionBlur");
assert_eq!(effect("ec.generate.ellipse"), "ADBE ELLIPSE");
assert_eq!(effect("ec.color.photofilter"), "ADBE PhotoFilterPS");
}
/// #3113: the names earlier versions emitted (and After Effects' obsolete ones) still resolve,
/// but none shadows a current name.
#[test]
fn legacy_effect_names_still_resolve() {
for (old, id) in LEGACY_EFFECTS {
assert_eq!(effect_from_match(old), Some(*id));
assert!(!EFFECTS.iter().any(|(_, m)| m.eq_ignore_ascii_case(old)), "{old} is a current name");
assert_eq!(current_name(old), Some(effect(id)));
}
assert_eq!(effect_from_match("ADBE Pixel Motion Blur"), Some("ec.time.pixelmotionblur"));
assert_eq!(current_name("ADBE Noise-0001").as_deref(), Some("ADBE Noise2-0001"));
assert_eq!(current_name("ADBE Light Casts Shadows").as_deref(), Some("ADBE Casts Shadows"));
assert_eq!(current_name("ADBE Noise2-0001"), None);
assert_eq!(current_name("ADBE Noise-01"), None);
assert_eq!(current_name("ADBE Gaussian Blur 2"), None);
}
#[test]
fn light_casts_shadows_is_after_effects_name() {
let n = |m: &str| Node::from(effectcraft_project::PropGroup::new(1, m, m));
let (light, mat, casts) = (n("lightOptions"), n("materialOptions"), n("castsShadows"));
assert_eq!(node(&[&light, &casts]), "ADBE Casts Shadows");
assert_eq!(node(&[&mat, &casts]), "ADBE Casts Shadows");
}
#[test]
fn effect_names_round_trip() {
for (id, m) in EFFECTS {
+3
View File
@@ -451,6 +451,9 @@ fn find_child<'a>(l: &'a Layer, g: &'a PropGroup, k: &str, depth: u8) -> Option<
g.children
.iter()
.find(|n| chain_of(n) == k)
.or_else(|| matchnames::current_name(k).and_then(|m| g.children.iter().find(|n| chain_of(n) == m)))
// Match names that changed only in case (`ADBE Ellipse` is now `ADBE ELLIPSE`, #3113).
.or_else(|| g.children.iter().find(|n| chain_of(n).eq_ignore_ascii_case(k)))
.or_else(|| g.children.iter().find(|n| n.name() == k))
.or_else(|| g.children.iter().find(|n| n.match_id() == k))
.or_else(|| g.children.iter().find(|n| n.name().eq_ignore_ascii_case(k)))
+36
View File
@@ -139,6 +139,42 @@ fn fill_parameters_use_documented_match_names() {
assert_eq!(o.result, json!("Color|All Masks|ADBE Fill-0007"));
}
/// #3113: effects carry After Effects' match names (`ADBE Noise2`, `ADBE OFMotionBlur`); the
/// names earlier versions emitted still add and find the effect and its parameters.
#[test]
fn effects_use_after_effects_match_names_and_accept_legacy_ones() {
let mut s = session();
let o = ok(
&mut s,
r#"var c = app.project.items.addComp("N", 100, 100, 1, 1, 12); var l = c.layers.addSolid([1,1,1], "S", 100, 100, 1);
var fx = l.property("ADBE Effect Parade");
var a = fx.addProperty("ADBE Noise2"), b = fx.addProperty("ADBE Noise"), m = fx.addProperty("ADBE OFMotionBlur");
var p = fx.addProperty("ADBE Pixel Motion Blur");
[a.matchName, b.matchName, m.matchName, p.matchName, a.property("ADBE Noise2-0001").name,
fx.property("ADBE Noise").matchName, a.property("ADBE Noise-0001").name, fx.numProperties]"#,
);
assert_eq!(
o.result,
json!(["ADBE Noise2", "ADBE Noise2", "ADBE OFMotionBlur", "ADBE OFMotionBlur", "Amount of Noise", "ADBE Noise2", "Amount of Noise", 4])
);
}
/// #3113: names that changed only in case (`ADBE Ellipse` → `ADBE ELLIPSE`, `ADBE Grow Bounds` →
/// `ADBE GROW BOUNDS`) still find the effect and its parameters.
#[test]
fn match_names_that_changed_case_still_find_effects_and_parameters() {
let mut s = session();
let o = ok(
&mut s,
r#"var c = app.project.items.addComp("E", 100, 100, 1, 1, 12); var l = c.layers.addSolid([1,1,1], "S", 100, 100, 1);
var fx = l.property("ADBE Effect Parade");
var e = fx.addProperty("ADBE Ellipse"), g = fx.addProperty("ADBE Grow Bounds");
[e.matchName, fx.property("ADBE Ellipse").matchName, e.property("ADBE Ellipse-0001").matchName,
g.matchName, fx.property("ADBE Grow Bounds").matchName, g.property("ADBE Grow Bounds-0001").matchName]"#,
);
assert_eq!(o.result, json!(["ADBE ELLIPSE", "ADBE ELLIPSE", "ADBE ELLIPSE-0001", "ADBE GROW BOUNDS", "ADBE GROW BOUNDS", "ADBE GROW BOUNDS-0001"]));
}
#[test]
fn stroke_parameters_use_after_effects_match_names() {
// #1405: `ADBE Stroke-0002` is Stroke's Color in After Effects, not All Masks.