feat(renderer): scene 2 text: bundled fonts, measurement before drawing, stated fit rules (P2) #16

Merged
james.maes merged 3 commits from feat/scene2-text into develop 2026-11-12 23:11:04 +00:00
3 Commits
Author SHA1 Message Date
james.maesandClaude Fable 5.2 589b6d41ee fix(renderer): scene 2 text after independent review: a proof that never cuts, a bounded shrink search, named refusals
maven / build (push) Successful in 10m28s
build / maven (push) Successful in 10m28s
The proof of `maxLength`
- The proof never cuts. With `overflow: truncate` the proof was given a cut
  block, which counts as fitting, so a bound element whose box held ten
  characters validated clean with `maxLength: 40` and every long value was cut
  at print. The proof now runs with cutting off. When it fails, an element
  that did not ask for cutting is refused as before; one that did still
  validates, since cutting is what its author chose, with the warning
  FIT_NOT_PROVEN at every validation carrying `measured.provenCharacters`,
  the longest length proven to fit whole (found by halving: fewer characters
  never fit worse), against `limit.characters`. No new code: FIT_NOT_PROVEN
  already means that the fit of a bound value is not proven.
- A bound element with `maxLength` is judged for legibility, when validated,
  at the size the proof shrank to (the floor when it fails and the element
  cuts), not at its stated size.

The shrink search
- The size chosen is still exactly the one a search of every quarter-point
  step gives, but that search is no longer made. A count that errs only
  toward yes says whether a size could hold the text: lines filled at any
  character, each held to a width no real line can be under, and a block to
  its lines' ascent, pitches and descent. It never refuses a size that fits
  and, once it allows a size, allows every smaller one, so the largest size it
  allows is found by halving and only the steps from there are laid out. When
  one line at the floor is higher than the box nothing is searched, and a cut
  tries no line count the box is too low for.
- 1024 characters at 600 dpi from 1000 pt to 1 pt took 180 to 250 ms a fit and
  now takes under 1 ms: at most 21 555 characters visited where the search of
  every size visits 15.6 million.
- A text is fitted once: what `check` composed is what `paint` draws.
- The text of one scene may take 64 million steps (a character visited while
  fitting is one, a character to be drawn 16, a glyph the scene draws for the
  first time at a size 4096, since the runtime rasterises it once). The
  element that passes it, and every text after it, is SCENE_INVALID at the
  element with `textWorkSteps`, as the painted-area limit is. The count is
  integer arithmetic: the same on every machine, and the same validated or
  rendered. A thousand of the worst elements validate in well under a second.

Ink outside the measure
- TEXT_INK_OUTSIDE_MEASURE was an IllegalStateException thrown while
  painting, which failed the whole render after part of the text was on the
  label. The dots of a text are now gathered first and reach the label only
  when none strayed; otherwise the one element is refused as
  TEXT_DOES_NOT_FIT with `glyphsOutsideMeasure` and `dotsOutsideMeasure`. A
  literal is held to the glyph bounds when the scene is validated.
- The bound is tested at 1, 2, 300 and 1000 pt as well as from 5 to 72 pt, for
  every character of every face at the three resolutions; nothing strays.

Schema and vocabulary
- `fit` declares `maxLines`, `shrinkTo` and `overflow` under `properties` with
  `additionalProperties: false`. The three names are in the renderer's and the
  host bridge's pointer vocabularies, and a wrong member is reported at the
  member (`/elements/3/fit/maxLines`, or in the override that states `fit`).
- A bound number is printed as the schema says: plain decimal digits, no
  exponent, no fraction that is only zeros (`1.0E10` prints `10000000000`,
  `2.50` prints `2.5`). A number that is not finite is VALUE_MISSING.

Tests
- The widest-advance check covers every character a face draws, inked or not
  (a no-break space advances the pen), so a font update cannot break the proof
  unseen.
- Property: `fit` equals the search of every size, block for block, over 3600
  generated texts, boxes, sizes, floors and line counts.
- The goldens are unchanged. A README beside them records the JDK builds and the
  FreeType each ships.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 14:58:53 -06:00
james.maesandClaude Fable 5.2 f5acf93e3d feat(renderer): scene 2 text: bundled faces, measurement before drawing, stated fit rules (P2)
maven / build (push) Successful in 10m16s
build / maven (push) Successful in 10m16s
A version 2 `text` element is now drawn. It replaces the placeholder that
parsed and placed a text and refused it as RENDERER_TOO_OLD.

What a scene can say
- `font` names one of the five bundled faces (or comes from `defaults`); any
  other name is FONT_UNKNOWN. There is no printer font and no installed font.
- `size` is the em as a physical length (1 pt to 1000 pt), not rounded to dots.
- `value` or `valuePath`; `align`/`valign` place the block in the box and each
  line in the block; `lineHeight`, `rotation`, `reverse`, `threshold`.
- `fit`: one line by default, shrunk in quarter-point steps to 70% of the size
  (not below 5 pt), then refused. Opt in to more lines (`maxLines`) and to
  cutting with an ellipsis (`overflow: truncate`).
- `maxLength`: validation proves the longest value fits, so a length refusal
  happens when a scene is published; printing refuses a longer value first.

How it is measured and drawn
- Measurement is integer arithmetic on the face's own numbers (thousandths of
  the em, read once at a reference size), so it is the same on every machine.
  Every character sits at a whole-dot pen position and is drawn there by the
  Java rasteriser that version 1 raster text uses, then reduced to ink at the
  threshold. A line's box is its advance widened to its ink plus two dots a
  side; a render whose ink would leave it fails instead of clipping.
- At each size a line is broken between words while that fits and at any
  character when it does not, which is also what makes a length provable.
- One glyph for each character, left to right: no kerning, ligatures, mark
  placement or bidirectional text. A character that cannot be drawn that way
  is FONT_MISSING_GLYPH with a count.

Diagnostics (ids and numbers only; no value ever enters one)
TEXT_DOES_NOT_FIT, VALUE_MISSING, VALUE_TOO_LONG, FONT_UNKNOWN,
FONT_MISSING_GLYPH, RESERVED_FONT_NAME; TEXT_TRUNCATED, TEXT_SMALL_FOR_DPI,
TEXT_LIGHT_FACE, FIT_NOT_PROVEN; TEXT_SHRUNK, TEXT_WRAPPED.

Core seams added: `ElementSource.inherit` gives an element the scene's
`defaults` for the properties its type reads, and `ElementSource.refuse`
raises a coded refusal at a property (FONT_UNKNOWN at the place that names
the face, in the element, the defaults or an override).

Tests: goldens for the five faces and for fit, rotation and reverse at 203,
300 and 600 dpi, generated in the CI image (Linux, Temurin 21.0.9) and the
same bytes on Windows (21.0.12) and on Linux Temurin 21.0.13; every character
of every face drawn inside its measured line; seeded property tests of fit
(never shrunk when it fits, the largest size that fits, never below the floor,
never more lines than allowed, cut only when asked and always with the mark,
a proven length never refused); licence, provenance and hash of every face;
the version 2 mutation campaign extended to text. The version 1 corpus is
untouched. 100% line coverage of the scene 2 packages.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 13:37:13 -06:00
james.maesandClaude Fable 5.2 d3378825d7 feat(renderer): bundle five OFL fonts for version 2 label text
Version 2 label scenes draw text in faces shipped inside the renderer, so a
label needs no printer font, no installed font and no object storage:

  qqq-sans, qqq-sans-bold   IBM Plex Sans Regular and Bold (@ibm/plex-sans@1.1.0)
  qqq-condensed-bold        IBM Plex Sans Condensed Bold (@ibm/plex-sans-condensed@2.0.0)
  qqq-mono, qqq-mono-bold   JetBrains Mono Regular and Bold (v2.304)

The files are unmodified, taken from each project's official release archive.
Each licence was confirmed as the SIL Open Font License 1.1 from the archive's
own licence file, which is shipped byte for byte beside the fonts with a
provenance note (release, archive URL and hash, path in the archive, font
version). /fonts/SHA256SUMS lists the hashes. git is told never to rewrite
anything under /fonts.

THIRD-PARTY.md and NOTICE record the fonts. The OFL is not on the K22-DEP-02
allowlist; the maintainer approved one recorded exception for unmodified OFL
fonts shipped as data, and the register entry is proposed with the pull request.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 13:26:38 -06:00