Skip to content

M7: Every-line Composer for ragged text - #587

Merged
bflatastic merged 1 commit into
storytold:mainfrom
Haven-King:feat/text-engine/text-composer-ragged
Oct 8, 2026
Merged

bflatastic merged 1 commit into
storytold:mainfrom
Haven-King:feat/text-engine/text-composer-ragged

Conversation

@Haven-King

Copy link
Copy Markdown
Contributor

The Every-line Composer now breaks ragged (left, centre, right and auto aligned) text as well as justified text, and each text object chooses Single-line or Every-line (ParaStyle.composer, Every-line by default as in Illustrator).

What and why

Illustrator's Every-line Composer balances line breaks over the whole paragraph for ragged text too: it will move a short word down to the next line, even when it fits, to avoid a lone word on the last line and an uneven rag. VectorCraft only composed justified area text, so ragged text always broke greedily. The composer choice wasn't stored, and the Paragraph panel's composer menu items were disabled stubs.

  • Model: ParaStyle.composer (vectorcraft_doc::Composer, re-exported by vectorcraft-text). It's saved only when Single-line, so older files read as Every-line. LayoutOptions.composer becomes an Option override for tests.
  • Composer:
    • The justified-only gate is gone.
    • Ragged lines never shrink.
    • Badness is 100·r³, where r is the line's leftover space over a rag zone of 1/6 of the width.
    • The last line is free unless it's shorter than 1/3 of the width.
    • Hyphens cost 25 in ragged text (50 in justified).
    • Fitness classes add adjacency demerits.
    • Work is bounded: at most 4,096 active nodes, and greedy breaking above 200k glyphs.
    • Auto alignment counts as ragged.
    • A wrapped line's flush opening bracket (Mojikumi) gives its half em back to the composer.
    • Paragraphs that may hang punctuation (burasagari) keep upstream's line-by-line fallback.
  • Command and UI: text.setFormat {composer: "singleLine"|"everyLine"}. The Paragraph panel menu items work and show a check mark; Reset Panel returns to Every-line.

How it was tested

  • crates/text/src/tests_typo.rs:
    • every_line_composer_balances_ragged_lines: a word that greedy breaking leaves alone on the last line gets company (Every-line moves the word before it down), in left, centre, right, auto and right-to-left text, also with Standard burasagari.
    • every_line_composer_evens_the_rag: no worse than greedy on leftover space or on neighbouring line differences; every character placed.
    • single_line_composer_is_greedy.
  • Command test with undo (tests_panelcmds.rs); native format round trip (native_v3.rs, and the rich_session fixture uses Single-line).
  • No existing test needed changing. Upstream's mojikumi bracket test caught an interaction, which this commit fixes.
  • 10k-character layout in release: ragged Every-line 5.8 ms against 4.8 ms Single-line; justified 5.6 ms.

Checklist

  • cargo xtask ci passes (fmt, clippy, tests, assets, brands, layers, wasm). Run on 85aabf2: fmt, clippy, assets, brands, layers and wasm pass; the test step fails. The only failing test is cmd::typecmd::area_tests::leading_model_is_set_per_paragraph_and_saved_only_when_top_to_top, an upstream test that fails the same way on main (it depends on the machine's CJK font).
  • No panics in shipped code: no unwrap(), expect(), panic!, unreachable!, todo! or
    unimplemented! outside tests, and no unchecked indexing on data from files, parameters or
    messages. Errors are returned as Result and shown to the user
    (why and how). Clippy's no-panic lints pass on 85aabf2.
  • Clean-room: nothing copied from Adobe products or GPL/AGPL code (AGENTS.md).
  • Every new asset has a row in ASSETS.md (no new assets).

Illustrator's Every-line Composer balances the breaks of the whole
paragraph for left/centre/right aligned text too, not only justified
text. VectorCraft only composed justified area text; ragged text was
always broken greedily.

- Model: ParaStyle.composer (vectorcraft_doc::Composer, re-exported by
  vectorcraft-text), Every-line by default like Illustrator, saved only
  when Single-line. layout() reads it from the object;
  LayoutOptions.composer is now an Option override for tests.
- Composer: the justified gate is gone. Ragged lines never shrink (a
  line that doesn't fit ends the node), badness is 100*r^3 with r = the
  line's leftover space over a rag zone of 1/6 of the width (finite for
  lines without spaces); the last line is free unless shorter than 1/3
  of the width (measured in zones of 1/3), so a lone short word on the
  last line pulls a word down. Hyphens cost 25 in ragged text (50 in
  justified). Knuth-Plass fitness classes with adjacency demerits
  (justified 10000, ragged 1000). Lines past the last width change
  share one node key, active nodes are capped (4096) and paragraphs
  over 200k glyphs break single-line, so work stays bounded; ragged
  tries a tolerance of one rag zone, then any fitting breaks, then
  falls back to greedy.
- text.setFormat {composer: "singleLine"|"everyLine"}; the Paragraph
  panel menu's composer items work with a check mark (Reset Panel goes
  back to Every-line).
- Tests: ragged Every-line moves "or" down in "Destroy all creatures
  with toughness X or less." where greedy leaves "less." alone; the rag
  of long paragraphs is no worse than greedy (sum of squared leftover
  and of neighbour differences); Single-line is greedy; the 10k-char
  layout budget covers ragged text; command test with undo; native
  format round trip (rich_session's text is now Single-line).
  No existing test needed changing: the justified tests and
  hyphenated_layout_adds_hyphens (ragged, now Every-line) still pass.

Auto alignment (start of the paragraph direction) is ragged; a wrapped
line's Mojikumi flush opening bracket gives its half em back to the
composer, as in the line loop; tests cover Auto in both directions.

Layout of 10k chars, release: justified Every-line 5.6 ms, ragged
Every-line 5.8 ms, ragged Single-line 4.8 ms.
@Haven-King
Haven-King force-pushed the feat/text-engine/text-composer-ragged branch from 85aabf2 to d2aad22 Compare October 8, 2026 21:05
@bflatastic
bflatastic merged commit 0869f51 into storytold:main Oct 8, 2026
@bflatastic

Copy link
Copy Markdown
Member

Merged, thank you @Haven-King! The Every-line Composer for ragged text is a real typographic upgrade. The rag-zone badness, the free-unless-short last line, cheaper hyphens in ragged text and fitness-class demerits are all sensible choices, the work bound and greedy fallback keep it safe on huge paragraphs, and the Paragraph panel's composer items finally do something.

I checked it with the CLI: a 14 pt left-aligned paragraph at 150 pt wide breaks into a visibly more even rag than Single-line, and at other widths both agree. One thing I flagged in the landing notes: since a missing composer reads as Every-line, ragged text in existing files may reflow when reopened. That's fine at this stage, and it matches Illustrator's default.

Haven-King pushed a commit to Haven-King/vectorcraft that referenced this pull request Oct 8, 2026
…e rich fixture

storytold#587 and storytold#600 landed together: the fixture's new area type (Every-line,
the default) is a second text object the composer test now sees.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants