Merge pull request #1164 from nanox333/fix/1063-text-animator-stacked

Scripts: text animator properties resolve by their After Effects names (refs #1063)
This commit is contained in:
BFlat
2026-10-30 05:07:48 +07:00
committed by GitHub
5 changed files with 119 additions and 31 deletions
+5 -1
View File
@@ -142,6 +142,7 @@ fn add_animator_prop(s: &mut Session, p: &Value) -> Result<Value> {
}
let layer = s.project.comp(cid).and_then(|c| c.layer(lid)).cloned().ok_or_else(|| bad(C, "no layer"))?;
let auid = animator_uid(s, &layer, p, C)?;
let mut index = 0;
s.edit("Add Text Animator Property", None, |proj, _| {
let mut next = proj.next_id;
let mut ids = Ids(&mut next);
@@ -151,10 +152,13 @@ fn add_animator_prop(s: &mut Session, p: &Value) -> Result<Value> {
for k in &kinds {
add_props(&mut ids, anim, k, three);
}
// The requested property's 1-based position in the Properties group. A companion (such as
// Tracking Type) is added before it, so the first new child is not always the one asked for.
index = kinds.first().and_then(|k| anim.sub("properties")?.children.iter().position(|n| n.match_id() == k.as_str())).map_or(0, |i| i + 1);
proj.next_id = next;
Ok(())
})?;
Ok(json!({"animator": auid}))
Ok(json!({"animator": auid, "index": index}))
}
pub(crate) fn add_selector(s: &mut Session, p: &Value) -> Result<Value> {
+65
View File
@@ -90,6 +90,44 @@ pub fn stroke_dash(name: &str) -> Option<&'static str> {
const TEXT_ANIMATOR: &[(&str, &str)] = &[("selectors", "ADBE Text Selectors"), ("properties", "ADBE Text Animator Properties")];
/// Animator property ids and their stable After Effects scripting names. Shared by lookup,
/// `.matchName`, and `addProperty` so the same property has one name in every direction.
pub const TEXT_ANIMATOR_PROPERTIES: &[(&str, &str)] = &[
("anchor", "ADBE Text Anchor Point 3D"),
("position", "ADBE Text Position 3D"),
("scale", "ADBE Text Scale 3D"),
("skew", "ADBE Text Skew"),
("rotation", "ADBE Text Rotation"),
("opacity", "ADBE Text Opacity"),
("fillColor", "ADBE Text Fill Color"),
("fillHue", "ADBE Text Fill Hue"),
("fillSaturation", "ADBE Text Fill Saturation"),
("fillBrightness", "ADBE Text Fill Brightness"),
("fillOpacity", "ADBE Text Fill Opacity"),
("strokeColor", "ADBE Text Stroke Color"),
("strokeHue", "ADBE Text Stroke Hue"),
("strokeSaturation", "ADBE Text Stroke Saturation"),
("strokeBrightness", "ADBE Text Stroke Brightness"),
("strokeOpacity", "ADBE Text Stroke Opacity"),
("strokeWidth", "ADBE Text Stroke Width"),
("tracking", "ADBE Text Tracking Amount"),
("lineAnchor", "ADBE Text Line Anchor"),
("lineSpacing", "ADBE Text Line Spacing"),
("characterOffset", "ADBE Text Character Offset"),
("characterValue", "ADBE Text Character Replace"),
("blur", "ADBE Text Blur"),
];
/// Companion properties that exist below an animator but are not kinds `addProperty` accepts:
/// they come with Tracking (Track Type), Skew (Skew Axis) and per-character 3D Rotation
/// (X and Y Rotation). Lookup and `.matchName` know them; `addProperty` does not.
pub const TEXT_ANIMATOR_COMPANIONS: &[(&str, &str)] = &[
("trackingType", "ADBE Text Track Type"),
("skewAxis", "ADBE Text Skew Axis"),
("rotationX", "ADBE Text Rotation X"),
("rotationY", "ADBE Text Rotation Y"),
];
const TEXT_SELECTORS: &[(&str, &str)] =
&[("rangeSelector", "ADBE Text Selector"), ("wigglySelector", "ADBE Text Wiggly Selector"), ("expressionSelector", "ADBE Text Expressible Selector")];
@@ -487,6 +525,9 @@ pub fn node(chain: &[&Node]) -> String {
"text" if pid == "animators" => "ADBE Text Animator".into(),
"text" if pid == "selectors" => find(TEXT_SELECTORS, id).map(str::to_string).unwrap_or_else(own),
"text" if pid == "rangeSelector" => find(TEXT_RANGE_SELECTOR, id).map(str::to_string).unwrap_or_else(own),
"text" if pid == "properties" && chain.len() == 5 && chain.get(1).is_some_and(|n| n.match_id() == "animators") => {
find(TEXT_ANIMATOR_PROPERTIES, id).or_else(|| find(TEXT_ANIMATOR_COMPANIONS, id)).map(str::to_string).unwrap_or_else(own)
}
"text" if grand.map(|g| g.match_id()) == Some("animators") => find(TEXT_ANIMATOR, id).map(str::to_string).unwrap_or_else(own),
"cameraOptions" => find(CAMERA, id).map(str::to_string).unwrap_or_else(own),
"lightOptions" => find(LIGHT, id).map(str::to_string).unwrap_or_else(own),
@@ -537,6 +578,30 @@ mod tests {
assert_eq!(shape_kind("nope"), None);
}
#[test]
fn text_animator_property_names_are_canonical_and_scoped() {
let n = |m: &str| Node::from(effectcraft_project::PropGroup::new(1, m, m));
let (text, animators, animator, properties) = (n("text"), n("animators"), n("animator"), n("properties"));
for (id, name) in TEXT_ANIMATOR_PROPERTIES {
let prop = n(id);
assert_eq!(node(&[&text, &animators, &animator, &properties, &prop]), *name);
assert_eq!(node(&[&properties, &prop]), *id, "an unrelated Properties group keeps its own ids");
}
}
/// #1063: the companions an animator property brings (Track Type, Skew Axis, X / Y Rotation)
/// have their After Effects names too, though `addProperty` does not accept them.
#[test]
fn text_animator_companions_have_after_effects_names() {
let n = |m: &str| Node::from(effectcraft_project::PropGroup::new(1, m, m));
let (text, animators, animator, properties) = (n("text"), n("animators"), n("animator"), n("properties"));
for (id, name) in TEXT_ANIMATOR_COMPANIONS {
let prop = n(id);
assert_eq!(node(&[&text, &animators, &animator, &properties, &prop]), *name);
assert!(!TEXT_ANIMATOR_PROPERTIES.iter().any(|(k, m)| k == id || m == name), "{id} is not an addProperty kind");
}
}
#[test]
fn text_range_selector_names() {
let n = |m: &str| Node::from(effectcraft_project::PropGroup::new(1, m, m));
+3 -27
View File
@@ -743,36 +743,12 @@ fn resolve_add(s: &Session, a: &J) -> R {
/// A text animator property (`layer.addTextAnimatorProperty` key) from an After Effects match
/// name or a display name.
fn text_animator_property(name: &str) -> Option<&'static str> {
const AE: &[(&str, &str)] = &[
("ADBE Text Anchor Point 3D", "anchor"),
("ADBE Text Position 3D", "position"),
("ADBE Text Scale 3D", "scale"),
("ADBE Text Skew", "skew"),
("ADBE Text Rotation", "rotation"),
("ADBE Text Opacity", "opacity"),
("ADBE Text Fill Color", "fillColor"),
("ADBE Text Fill Hue", "fillHue"),
("ADBE Text Fill Saturation", "fillSaturation"),
("ADBE Text Fill Brightness", "fillBrightness"),
("ADBE Text Fill Opacity", "fillOpacity"),
("ADBE Text Stroke Color", "strokeColor"),
("ADBE Text Stroke Hue", "strokeHue"),
("ADBE Text Stroke Saturation", "strokeSaturation"),
("ADBE Text Stroke Brightness", "strokeBrightness"),
("ADBE Text Stroke Opacity", "strokeOpacity"),
("ADBE Text Stroke Width", "strokeWidth"),
("ADBE Text Tracking Amount", "tracking"),
("ADBE Text Line Anchor", "lineAnchor"),
("ADBE Text Line Spacing", "lineSpacing"),
("ADBE Text Character Offset", "characterOffset"),
("ADBE Text Character Replace", "characterValue"),
("ADBE Text Blur", "blur"),
];
if let Some((_, k)) = AE.iter().find(|(m, _)| m.eq_ignore_ascii_case(name)) {
let table = matchnames::TEXT_ANIMATOR_PROPERTIES;
if let Some((k, _)) = table.iter().find(|(_, m)| m.eq_ignore_ascii_case(name)) {
return Some(k);
}
let n = loose(name);
AE.iter().map(|(_, k)| *k).find(|k| loose(k) == n || (n == "anchorpoint" && *k == "anchor") || (n == "trackingamount" && *k == "tracking"))
table.iter().map(|(k, _)| *k).find(|k| loose(k) == n || (n == "anchorpoint" && *k == "anchor") || (n == "trackingamount" && *k == "tracking"))
}
/// `app.findMenuCommandId(name)`: 1-based index of the command whose label matches (menu items
+2 -3
View File
@@ -734,9 +734,8 @@ PropertyGroup.prototype.addProperty = function (name) {
} else if (r.kind === "animator") {
uid = __call("layer.addTextAnimator", { comp: L.__comp, layer: L.__id, properties: [] }).animator;
} else if (r.kind === "animatorProperty") {
var before = this.numProperties;
__call("layer.addTextAnimatorProperty", { comp: L.__comp, layer: L.__id, animator: r.animator, property: r.property });
return this.property(before + 1);
var added = __call("layer.addTextAnimatorProperty", { comp: L.__comp, layer: L.__id, animator: r.animator, property: r.property });
return this.property(added.index);
} else if (r.kind === "selector") {
var n0 = this.numProperties;
__call("layer.addTextSelector", { comp: L.__comp, layer: L.__id, animator: r.animator, kind: r.selector });
+44
View File
@@ -1719,6 +1719,50 @@ fn layer_property_resolves_transform_and_source_text_shortcuts() {
assert_eq!(layer(c, "s").props.prop("transform/opacity").unwrap().value, KV::Scalar(25.0));
}
/// A text animator property added by its After Effects name is the one asked for, not its Tracking
/// Type companion, and it can be found again by that name (#1063).
#[test]
fn scripted_text_animator_property_is_the_one_asked_for() {
let mut s = session();
let o = ok(
&mut s,
r#"
var c = app.project.items.addComp("Track", 960, 300, 1, 3, 30);
var t = c.layers.addText("MOVARA");
var a = t.property("ADBE Text Properties").property("ADBE Text Animators").addProperty("ADBE Text Animator");
var props = a.property("ADBE Text Animator Properties");
var added = props.addProperty("ADBE Text Tracking Amount");
added.setValue(20);
[added.name, props.property("ADBE Text Tracking Amount").value, props.property("ADBE Text Tracking Amount").name,
added.matchName, props.property(added.matchName).propertyIndex]
"#,
);
assert_eq!(o.result, json!(["Tracking Amount", 20, "Tracking Amount", "ADBE Text Tracking Amount", 2]));
}
/// The companion properties added with Tracking and Skew are found by their After Effects names,
/// but cannot be added on their own (#1063).
#[test]
fn text_animator_companion_properties_resolve_by_name() {
let mut s = session();
let o = ok(
&mut s,
r#"
var c = app.project.items.addComp("Track", 960, 300, 1, 3, 30);
var t = c.layers.addText("MOVARA");
var props = t.property("ADBE Text Properties").property("ADBE Text Animators").addProperty("ADBE Text Animator").property("ADBE Text Animator Properties");
props.addProperty("ADBE Text Tracking Amount");
props.addProperty("ADBE Text Skew");
var type = props.property("ADBE Text Track Type");
var axis = props.property("ADBE Text Skew Axis");
var added = "";
try { props.addProperty("ADBE Text Track Type"); added = "added"; } catch (e) { added = "refused"; }
[type ? type.matchName : null, axis ? axis.matchName : null, added]
"#,
);
assert_eq!(o.result, json!(["ADBE Text Track Type", "ADBE Text Skew Axis", "refused"]));
}
/// #1936: an array whose length doesn't match the property's dimension is an error in After
/// Effects, not a value to truncate or broadcast. Two values for a 3-D property stay legal.
#[test]