From cf1898df4f36fa30e90d6277595f151b84ef5b8c Mon Sep 17 00:00:00 2001 From: Sean Hawkey <62113798+GenuineMoron@users.noreply.github.com> Date: Fri, 13 Nov 2026 14:26:36 +0800 Subject: [PATCH] Expressions: a text style's justification and direction use After Effects' names (#3607) (#3609) * Expressions: a text style's justification and direction use After Effects' names After Effects reads a paragraph's justification as alignLeft, alignCenter, alignRight, justifyLastLeft, justifyLastCenter, justifyLastRight or justifyFull, and its direction as left-to-right or right-to-left; its setters take those names. EffectCraft returned its own keys (left, ltr, justifyAll...), and the document refused alignLeft/alignCenter/alignRight and justifyFull. The getters now return After Effects' names and the parser accepts them; EffectCraft's own names are still accepted. * Expressions: every accepted justification spelling reads back as After Effects' name setJustification('CENTER_JUSTIFY').justification returned CENTER_JUSTIFY. The setter and getter now share one table of the spellings Justify::parse accepts, looked up as own properties only, so an alias reads back as alignCenter and an inherited name such as toString is never matched. --- crates/expr/src/prelude.js | 22 ++++++++++++++++++--- crates/expr/src/tests.rs | 35 +++++++++++++++++++++++++++++++++ crates/keyframe/src/text_doc.rs | 8 ++++---- 3 files changed, 58 insertions(+), 7 deletions(-) diff --git a/crates/expr/src/prelude.js b/crates/expr/src/prelude.js index a57145568..678dd6816 100644 --- a/crates/expr/src/prelude.js +++ b/crates/expr/src/prelude.js @@ -987,6 +987,22 @@ var __mkProp = (function (P) { // attributes at, and the setter calls made on it ([key, value, start, count]). Setters return // new objects; returning a style from a Source Text expression applies it. +// After Effects' names for a paragraph's justification, which its expressions read and write, +// from every spelling `Justify::parse` accepts (case, '_', '-' and spaces ignored). +var __AE_JUSTIFY = { + left: 'alignLeft', leftjustify: 'alignLeft', alignleft: 'alignLeft', + center: 'alignCenter', centre: 'alignCenter', centerjustify: 'alignCenter', aligncenter: 'alignCenter', + right: 'alignRight', rightjustify: 'alignRight', alignright: 'alignRight', + justify: 'justifyLastLeft', justifyleft: 'justifyLastLeft', justifylastleft: 'justifyLastLeft', fulljustifylastlineleft: 'justifyLastLeft', + justifycenter: 'justifyLastCenter', justifylastcenter: 'justifyLastCenter', fulljustifylastlinecenter: 'justifyLastCenter', + justifyright: 'justifyLastRight', justifylastright: 'justifyLastRight', fulljustifylastlineright: 'justifyLastRight', + justifyall: 'justifyFull', justifyfull: 'justifyFull', fulljustifylastlinefull: 'justifyFull', fulljustify: 'justifyFull', +}; +function __aeJustify(v) { + var k = String(v).toLowerCase().replace(/[_ -]/g, ''); + return Object.prototype.hasOwnProperty.call(__AE_JUSTIFY, k) ? __AE_JUSTIFY[k] : String(v); +} + function __TextStyle(doc, idx, ops, empty) { this.__d = doc; this.__i = idx; this.__ops = ops; this.__empty = empty; } function __styleSetter(key, conv) { @@ -1062,13 +1078,13 @@ __TextStyle.prototype = { get kerningType() { var k = this.__get('kerning'); return typeof k === 'number' ? 'manual' : k; }, get kerning() { var k = this.__get('kerning'); return typeof k === 'number' ? k : 0; }, get isLigature() { return this.__get('ligatures'); }, - get justification() { return this.__get('justify', true); }, + get justification() { return __aeJustify(this.__get('justify', true)); }, get firstLineIndent() { return this.__get('indentFirst', true); }, get startIndent() { return this.__get('indentLeft', true); }, get endIndent() { return this.__get('indentRight', true); }, get spaceBefore() { return this.__get('spaceBefore', true); }, get spaceAfter() { return this.__get('spaceAfter', true); }, - get direction() { return this.__get('direction', true); }, + get direction() { var d = this.__get('direction', true); return d === 'rtl' ? 'right-to-left' : d === 'ltr' ? 'left-to-right' : d; }, get isEveryLineComposer() { return this.__get('composer', true) === 'everyLine'; }, get isHangingRoman() { return this.__get('hangingPunctuation', true); }, setText: __styleSetter('text', function (v) { return String(v); }), @@ -1099,7 +1115,7 @@ __TextStyle.prototype = { setKerningType: __styleSetter('kerning', function (v) { v = String(v).toLowerCase(); return v === 'manual' ? 0 : v; }), setKerning: __styleSetter('kerning', Number), setLigature: __styleSetter('ligatures', Boolean), - setJustification: __styleSetter('justify', function (v) { return String(v); }), + setJustification: __styleSetter('justify', __aeJustify), setFirstLineIndent: __styleSetter('indentFirst'), setStartIndent: __styleSetter('indentLeft'), setEndIndent: __styleSetter('indentRight'), diff --git a/crates/expr/src/tests.rs b/crates/expr/src/tests.rs index a80f47b82..361f12b87 100644 --- a/crates/expr/src/tests.rs +++ b/crates/expr/src/tests.rs @@ -1258,6 +1258,41 @@ fn source_text_style_api() { assert!(f.ev(f.a, ROT, "transform.rotation.style.fontSize", 0.0).is_err()); } +#[test] +fn paragraph_settings_read_and_write_after_effects_names() { + // After Effects 26.3 reads justification as alignLeft / alignCenter / alignRight / + // justifyLastLeft / justifyLastCenter / justifyLastRight / justifyFull and direction as + // left-to-right / right-to-left, and its setters take those names. + let f = fx(); + let doc = |f: &Fx, e: &str| -> TextDoc { + match f.ev(f.title, "text/sourceText", e, 0.0) { + Ok(Value::Text(d)) => *d, + r => panic!("{e}: {r:?}"), + } + }; + let s = |e: &str| doc(&f, e).text; + assert_eq!(s("text.sourceText.style.justification + ' ' + text.sourceText.style.direction"), "alignLeft left-to-right"); + for (ae, want) in [ + ("alignLeft", effectcraft_keyframe::Justify::Left), + ("alignCenter", effectcraft_keyframe::Justify::Center), + ("alignRight", effectcraft_keyframe::Justify::Right), + ("justifyLastLeft", effectcraft_keyframe::Justify::JustifyLastLeft), + ("justifyLastCenter", effectcraft_keyframe::Justify::JustifyLastCenter), + ("justifyLastRight", effectcraft_keyframe::Justify::JustifyLastRight), + ("justifyFull", effectcraft_keyframe::Justify::JustifyAll), + ] { + let d = doc(&f, &format!("text.sourceText.style.setJustification('{ae}')")); + assert_eq!(d.justify, want, "{ae}"); + } + let d = doc(&f, "text.sourceText.style.setDirection('right-to-left').setJustification('justifyLastRight')"); + assert_eq!(d.direction, effectcraft_keyframe::Direction::Rtl); + assert_eq!(s("text.sourceText.style.setJustification('alignRight').justification"), "alignRight"); + // Every accepted alias reads back as After Effects' name; an inherited name isn't one. + assert_eq!(s("text.sourceText.style.setJustification('CENTER_JUSTIFY').justification"), "alignCenter"); + assert_eq!(s("text.sourceText.style.setJustification('justifyAll').justification"), "justifyFull"); + assert_eq!(s("String(text.sourceText.style.setJustification('toString').justification)"), "toString"); +} + #[test] fn numbers_to_source_text() { let f = fx(); diff --git a/crates/keyframe/src/text_doc.rs b/crates/keyframe/src/text_doc.rs index f91d8be4d..e8fcb52be 100644 --- a/crates/keyframe/src/text_doc.rs +++ b/crates/keyframe/src/text_doc.rs @@ -85,13 +85,13 @@ impl Justify { } pub fn parse(s: &str) -> Option { Some(match s.to_ascii_lowercase().replace(['_', ' ', '-'], "").as_str() { - "left" | "leftjustify" => Justify::Left, - "center" | "centre" | "centerjustify" => Justify::Center, - "right" | "rightjustify" => Justify::Right, + "left" | "leftjustify" | "alignleft" => Justify::Left, + "center" | "centre" | "centerjustify" | "aligncenter" => Justify::Center, + "right" | "rightjustify" | "alignright" => Justify::Right, "justify" | "justifyleft" | "justifylastleft" | "fulljustifylastlineleft" => Justify::JustifyLastLeft, "justifycenter" | "justifylastcenter" | "fulljustifylastlinecenter" => Justify::JustifyLastCenter, "justifyright" | "justifylastright" | "fulljustifylastlineright" => Justify::JustifyLastRight, - "justifyall" | "fulljustifylastlinefull" | "fulljustify" => Justify::JustifyAll, + "justifyall" | "justifyfull" | "fulljustifylastlinefull" | "fulljustify" => Justify::JustifyAll, _ => return None, }) }