diff --git a/crates/text/src/layout.rs b/crates/text/src/layout.rs index d8bdee4fd..3ce08b84f 100644 --- a/crates/text/src/layout.rs +++ b/crates/text/src/layout.rs @@ -635,7 +635,11 @@ fn paragraph(text: &str, base: usize, runs: &[(Range, TextStyle)], primar first_ascent(primaries[si].face, st.size).unwrap_or_else(|| fonts::face(primaries[si].face).metrics(st.size).ascent) }; let eff_align = |last: bool| -> Align { - if (last || width.is_none()) && para.align == Align::Justify { + if rtl_para && width.is_none() && para.align == Align::Justify { + // Right-to-left point text with a justified paragraph sits flush at its start, the + // right (After Effects 2026, every justify code; left-to-right not measured). + Align::Right + } else if (last || width.is_none()) && para.align == Align::Justify { if width.is_none() && para.justify_last == Align::Justify { Align::Left } else { para.justify_last } } else { para.align @@ -1499,6 +1503,25 @@ mod tests { assert!(r.lines[0].x + r.lines[0].width > 401.0, "closing quote hangs right"); } + /// After Effects 2026 (EXPORTER read-back t7): right-to-left point text with any justified + /// code (justify last left / centre / right, justify all) sits flush right at its anchor, as + /// right-aligned text does. + #[test] + fn rtl_point_text_with_a_justified_paragraph_is_flush_at_its_start() { + let text = "Right to left"; + let s = st(30.0); + let at = |align: Align, last: Align| { + let p = ParagraphStyle { align, justify_last: last, rtl: Some(true), ..Default::default() }; + let l = layout_rich_uncached(text, &[(0..text.len(), s.clone())], &[p]); + (l.lines[0].x, l.lines[0].x + l.lines[0].width) + }; + let right = at(Align::Right, Align::Left); + for last in [Align::Left, Align::Center, Align::Right, Align::Justify] { + let got = at(Align::Justify, last); + assert!((got.0 - right.0).abs() < 1e-3 && (got.1 - right.1).abs() < 1e-3, "{last:?}: {got:?} vs right-aligned {right:?}"); + } + } + #[test] fn rtl_paragraph_carets_run_right_to_left() { let text = "\u{5d0}\u{5d1}\u{5d2}"; diff --git a/docs/fidelity/README.md b/docs/fidelity/README.md index 6c59acd43..d4c1bf788 100644 --- a/docs/fidelity/README.md +++ b/docs/fidelity/README.md @@ -80,6 +80,16 @@ every-line composer ignored the last line's slack and left "twelve" alone on a s counts that slack for Justify All, and every word starts within 1 px of AE's. Regression test: `justify_all_balances_the_lines_with_the_every_line_composer`. +## Right-to-left point text with a justified paragraph + +Measured on 19 October 2026 with After Effects 2026 (26.2) (EXPORTER read-back t7): right-to-left +point text whose paragraph has a justified code (justify last left / centre / right, justify all) +sits flush right at its anchor, like right-aligned text. EffectCraft placed it by the last-line +side (left / centre / left). Now flush right; left-to-right point text is unchanged (not measured). +Regression test: `rtl_point_text_with_a_justified_paragraph_is_flush_at_its_start`. Still open: +AE wraps right-to-left box text slightly wider (its word gaps are 1–2 px narrower than in +left-to-right text with the same font), so a 300 px box fits a word more per line. + ## Still unmeasured Rendered frame comparisons now have a deterministic `effectcraft_effects::fidelity::compare_frames`