From 1b44c5c98aac86b3e2d9f5d7b33b7125c292cc63 Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Wed, 14 Oct 2026 09:15:08 +0200 Subject: [PATCH] Scripting: Stroke and Hue/Saturation parameters use After Effects' match-name numbers (part of #1405) --- crates/script/src/matchnames.rs | 44 ++++++++++++++++++++---- crates/script/src/tests.rs | 59 +++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 6 deletions(-) diff --git a/crates/script/src/matchnames.rs b/crates/script/src/matchnames.rs index 60a40e69..205697b6 100644 --- a/crates/script/src/matchnames.rs +++ b/crates/script/src/matchnames.rs @@ -399,7 +399,8 @@ pub fn shape_kind(name: &str) -> Option<&'static str> { } /// Effects whose documented parameter match names are not numbered in display order: -/// (effect id, param match id, number). Fill lists All Masks second but it is `-0007`. +/// (effect id, param match id, number). Fill lists All Masks second but it is `-0007`. Params of +/// these effects that After Effects doesn't have are numbered after the listed ones (#1405). const PARAM_NUMBERS: &[(&str, &str, usize)] = &[ ("ec.generate.fill", "fillMask", 1), ("ec.generate.fill", "color", 2), @@ -408,8 +409,42 @@ const PARAM_NUMBERS: &[(&str, &str, usize)] = &[ ("ec.generate.fill", "opacity", 5), ("ec.generate.fill", "invert", 6), ("ec.generate.fill", "allMasks", 7), + ("ec.generate.stroke", "path", 1), + ("ec.generate.stroke", "color", 2), + ("ec.generate.stroke", "brushSize", 3), + ("ec.generate.stroke", "brushHardness", 4), + ("ec.generate.stroke", "opacity", 5), + ("ec.generate.stroke", "spacing", 6), + ("ec.generate.stroke", "paintStyle", 7), + ("ec.generate.stroke", "start", 8), + ("ec.generate.stroke", "end", 9), + ("ec.generate.stroke", "allMasks", 10), + ("ec.generate.stroke", "strokeSequentially", 11), + // -0001 is a hidden parameter and -0003 Channel Range, which we don't have; the per-channel + // controls are ours (After Effects edits the selected channel with the Master ones). + ("ec.color.huesaturation", "channelControl", 2), + ("ec.color.huesaturation", "hue", 4), + ("ec.color.huesaturation", "saturation", 5), + ("ec.color.huesaturation", "lightness", 6), + ("ec.color.huesaturation", "colorize", 7), + ("ec.color.huesaturation", "colorizeHue", 8), + ("ec.color.huesaturation", "colorizeSaturation", 9), + ("ec.color.huesaturation", "colorizeLightness", 10), ]; +/// The `-NNNN` number of the param `id` at 0-based `pos` among `fx`'s `params`. +fn param_number(fx: &str, params: &[Node], id: &str, pos: usize) -> usize { + let listed = |p: &str| PARAM_NUMBERS.iter().find(|(f, q, _)| *f == fx && *q == p).map(|(_, _, n)| *n); + if let Some(n) = listed(id) { + return n; + } + let Some(last) = PARAM_NUMBERS.iter().filter(|(f, _, _)| *f == fx).map(|(_, _, n)| *n).max() else { + return pos + 1; + }; + let unlisted_before = params.iter().take(pos).filter(|c| listed(c.match_id()).is_none()).count(); + last + unlisted_before + 1 +} + /// Match name of the node at the end of `chain` (layer root's children → … → node). `effect` /// params are numbered `-0001…` in parameter order, like After Effects. pub fn node(chain: &[&Node]) -> String { @@ -432,11 +467,8 @@ pub fn node(chain: &[&Node]) -> String { let fx = parent.and_then(Node::as_group); match fx { Some(g) => { - let i = PARAM_NUMBERS - .iter() - .find(|(fx, p, _)| *fx == g.match_id && *p == id) - .map(|(_, _, n)| *n) - .unwrap_or_else(|| g.children.iter().position(|c| c.uid() == last.uid()).unwrap_or(0) + 1); + let pos = g.children.iter().position(|c| c.uid() == last.uid()).unwrap_or(0); + let i = param_number(&g.match_id, &g.children, id, pos); format!("{}-{i:04}", effect(&g.match_id)) } None => own(), diff --git a/crates/script/src/tests.rs b/crates/script/src/tests.rs index 2c821e6a..f47c212f 100644 --- a/crates/script/src/tests.rs +++ b/crates/script/src/tests.rs @@ -126,6 +126,65 @@ fn fill_parameters_use_documented_match_names() { assert_eq!(o.result, json!("Color|All Masks|ADBE Fill-0007")); } +#[test] +fn stroke_parameters_use_after_effects_match_names() { + // #1405: `ADBE Stroke-0002` is Stroke's Color in After Effects, not All Masks. + let mut s = session(); + let o = ok( + &mut s, + r#"var c = app.project.items.addComp("S", 100, 100, 1, 1, 12); var l = c.layers.addSolid([1,1,1], "S", 100, 100, 1); + var fx = l.property("ADBE Effect Parade").addProperty("ADBE Stroke"), out = []; + for (var i = 1; i <= 11; i++) out.push(fx.property("ADBE Stroke-" + ("000" + i).slice(-4)).name); + out.push(fx.property("Color").matchName); out"#, + ); + assert_eq!( + o.result, + json!([ + "Path", + "Color", + "Brush Size", + "Brush Hardness", + "Opacity", + "Spacing", + "Paint Style", + "Start", + "End", + "All Masks", + "Stroke Sequentially", + "ADBE Stroke-0002" + ]) + ); +} + +#[test] +fn hue_saturation_parameters_use_after_effects_match_names() { + // #1405: `ADBE HUE SATURATION-0004` is Master Hue in After Effects. The per-channel controls, + // which After Effects doesn't expose, are numbered after its parameters so none takes an AE number. + let mut s = session(); + let o = ok( + &mut s, + r#"var c = app.project.items.addComp("H", 100, 100, 1, 1, 12); var l = c.layers.addSolid([1,1,1], "S", 100, 100, 1); + var fx = l.property("ADBE Effect Parade").addProperty("ADBE HUE SATURATION"), out = []; + var n = [2, 4, 5, 6, 7, 8, 9, 10]; + for (var i = 0; i < n.length; i++) out.push(fx.property("ADBE HUE SATURATION-" + ("000" + n[i]).slice(-4)).name); + out.push(fx.property("Red Hue").matchName); out"#, + ); + assert_eq!( + o.result, + json!([ + "Channel Control", + "Master Hue", + "Master Saturation", + "Master Lightness", + "Colorize", + "Colorize Hue", + "Colorize Saturation", + "Colorize Lightness", + "ADBE HUE SATURATION-0011" + ]) + ); +} + #[test] fn undo_steps_without_a_group() { let mut s = session();