From 55f4d163f8a06943211fef6bc012d9a20694090a Mon Sep 17 00:00:00 2001 From: MarekSpodBiedry <73122968+MarekSpodBiedry@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:11:17 +0200 Subject: [PATCH] Scripting: text range selectors resolve by match name and limit the animator (#837) --- crates/engine/src/commands/text_anim.rs | 7 +++++++ crates/script/src/matchnames.rs | 18 ++++++++++++++++++ crates/script/src/tests.rs | 22 ++++++++++++++++++++++ 3 files changed, 47 insertions(+) diff --git a/crates/engine/src/commands/text_anim.rs b/crates/engine/src/commands/text_anim.rs index 1496af84..8bd74cdc 100644 --- a/crates/engine/src/commands/text_anim.rs +++ b/crates/engine/src/commands/text_anim.rs @@ -115,6 +115,13 @@ fn add_animator(s: &mut Session, p: &Value) -> Result { if added == 0 && !kinds.is_empty() { return Err(bad(C, "unknown animator property")); } + // Like After Effects, a scripted animator has no selectors until the script adds one + // (addProperty("ADBE Text Selector")); a default 0-100% one would select every character. + if kinds.is_empty() + && let Some(sels) = g.sub_mut("selectors") + { + sels.children.clear(); + } let uid = g.uid; anims.children.push(g.into()); proj.next_id = next; diff --git a/crates/script/src/matchnames.rs b/crates/script/src/matchnames.rs index a48a36a0..f43d2071 100644 --- a/crates/script/src/matchnames.rs +++ b/crates/script/src/matchnames.rs @@ -63,6 +63,11 @@ const TEXT: &[(&str, &str)] = &[ const TEXT_ANIMATOR: &[(&str, &str)] = &[("selectors", "ADBE Text Selectors"), ("properties", "ADBE Text Animator Properties")]; +const TEXT_SELECTORS: &[(&str, &str)] = + &[("rangeSelector", "ADBE Text Selector"), ("wigglySelector", "ADBE Text Wiggly Selector"), ("expressionSelector", "ADBE Text Expressible Selector")]; + +const TEXT_RANGE_SELECTOR: &[(&str, &str)] = &[("start", "ADBE Text Percent Start"), ("end", "ADBE Text Percent End"), ("offset", "ADBE Text Percent Offset")]; + const CAMERA: &[(&str, &str)] = &[ ("zoom", "ADBE Camera Zoom"), ("dof", "ADBE Camera Depth of Field"), @@ -412,6 +417,8 @@ pub fn node(chain: &[&Node]) -> String { } "text" if chain.len() == 2 => find(TEXT, id).map(str::to_string).unwrap_or_else(own), "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 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), @@ -458,4 +465,15 @@ mod tests { assert_eq!(shape_kind("ADBE Vector Graphic - Fill"), Some("fill")); assert_eq!(shape_kind("nope"), None); } + + #[test] + fn text_range_selector_names() { + let n = |m: &str| Node::from(effectcraft_project::PropGroup::new(1, m, m)); + let (text, animators, animator, selectors, range, start, end, offset) = + (n("text"), n("animators"), n("animator"), n("selectors"), n("rangeSelector"), n("start"), n("end"), n("offset")); + assert_eq!(node(&[&text, &animators, &animator, &selectors, &range]), "ADBE Text Selector"); + assert_eq!(node(&[&text, &animators, &animator, &selectors, &range, &start]), "ADBE Text Percent Start"); + assert_eq!(node(&[&text, &animators, &animator, &selectors, &range, &end]), "ADBE Text Percent End"); + assert_eq!(node(&[&text, &animators, &animator, &selectors, &range, &offset]), "ADBE Text Percent Offset"); + } } diff --git a/crates/script/src/tests.rs b/crates/script/src/tests.rs index b50a7bfb..3f2b9ab2 100644 --- a/crates/script/src/tests.rs +++ b/crates/script/src/tests.rs @@ -730,3 +730,25 @@ fn import_options_sequence_picks_one_still_or_the_run() { assert_eq!(kind_of(&s, "shot_[0001-0003].png"), FootageKind::Sequence); let _ = std::fs::remove_dir_all(&dir); } + +/// A scripted text animator has only the selectors the script adds, and the Range Selector and +/// its Start/End/Offset resolve by After Effects match name (#837). +#[test] +fn scripted_text_range_selector_resolves_and_is_the_only_selector() { + let mut s = session(); + let o = ok( + &mut s, + r#" + var c = app.project.items.addComp("Sel", 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"); + a.property("ADBE Text Animator Properties").addProperty("ADBE Text Opacity").setValue(0); + var sels = a.property("ADBE Text Selectors"); + var sel = sels.addProperty("ADBE Text Selector"); + sel.property("ADBE Text Percent Start").setValue(50); + var end = sel.property("ADBE Text Percent End").value; + [sels.numProperties, sel.matchName, sel.property("start").value, end, sel.property("ADBE Text Percent Offset").matchName] + "#, + ); + assert_eq!(o.result, json!([1, "ADBE Text Selector", 50, 100, "ADBE Text Percent Offset"])); +}