epiphany/crates/epiphany-glyphs/DECISIONS.md

120 lines
6.7 KiB
Markdown

# epiphany-glyphs — decisions
Editor T4-pre W2 (`spec/CONTRACT_EDITOR_T4PRE_W2_GLYPHS.md`): the shared
typed glyph-asset seam a canvas tessellator needs, populated on top of the
already-designed `layout-ir` interface (`PathCommand`, `GlyphRenderData`,
`GlyphCatalog::render_data`) rather than designing a new one.
## Scope and status
New crate. Owns the bundled Bravura outline data (`src/outlines_generated.rs`,
moved from `epiphany-render-svg`, which held it `pub(crate)` and private to
itself), the extractor tool and its OFL license (`tools/`), an in-crate
grammar-specific parser from the bundled SVG path `d` strings into
`epiphany_layout_ir::PathCommand` (`src/path.rs`, private — reachable only
through the catalog), and `BravuraGlyphCatalog` (`src/catalog.rs`): a real
`GlyphCatalog` whose `render_data` returns genuine outlines, unlike
`epiphany_layout_ir::BravuraCatalog`, which is metrics-only and honestly
returns `None`.
`BRAVURA_METRICS` does not move here and is not edited (contract pin 3): it
stays in `epiphany-layout-ir`, and this crate depends on it read-only. No
cycle: `epiphany-glyphs``epiphany-layout-ir`, never the reverse.
## The grammar the contract described vs. the grammar the generator emits
The contract's "verified starting point" states the bundled `d` strings use
"absolute `M`/`L`/`C`/`Z`, decimal coordinates, 4 decimal places". Verified
against all 37 bundled glyphs before writing the parser (not assumed), the
real grammar is wider on both counts, traced to
`tools/extract_bravura_outlines.py`'s use of
`fontTools.pens.svgPathPen.SVGPathPen`, whose default `optimizeCommands`
behavior the extractor never disables:
* **`V` (vertical-only) and `H` (horizontal-only) lineto shorthand** are also
emitted, absolute, whenever a lineto's target shares an axis with the
current point — 23 of the 37 bundled glyphs use at least one. `PathCommand`
(the already-designed shared type) has no shorthand variant, so `parse_d`
lowers `V`/`H` to `PathCommand::LineTo`; `emit_d` reconstructs the
shorthand byte-for-byte purely from geometry — comparing the lineto's
target to the current point (`V` when only *x* is unchanged, `H` when only
*y* is, `L` otherwise) — which is exactly what the round-trip test proves
for every bundled glyph. No information is lost: the typed form and the
stored string are geometrically and (after this reconstruction)
byte-for-byte equivalent.
* **Coordinates are rounded to *at most* 4 decimals**, with trailing zeros
and a bare `-0` stripped by the generator's own `round_d`
(`tools/extract_bravura_outlines.py:180-185`: `f"{v:.4f}".rstrip('0').rstrip('.')`,
normalising `""`/`"-0"` to `"0"`). Printed precision therefore varies per
number (0-3 fractional digits observed in the bundled data; 4 is the
ceiling, not the width). `emit_d`'s number formatter reproduces this exact
rule.
* Every command in the observed grammar carries exactly one point/coordinate
group (`M`, `L`, `V`, `H` one; `C` three points; `Z` none) — the generator
never merges consecutive same-type commands into a multi-coordinate group
(SVG allows this generally; the fontTools pen never emits it), so the
parser does not implement that generality either.
This is a correction to the contract's stated facts, not a deviation from its
design pins: pin 4 ("an in-crate parser … covers exactly the grammar the
generator emits") is satisfied by parsing the *real* grammar, which is what
"exactly the grammar the generator emits" requires once the grammar is
actually read from the data rather than assumed from the contract's summary.
## Round-trip proof, not the sanctioned fallback (pin 5)
Pin 5 sanctions a weaker fallback — comparing parsed coordinate sequences as
exact `f32` — if the generator's number formatting cannot be reproduced
exactly. It can: `emit_d`'s formatter is a direct transcription of
`round_d`'s Python (round to 4, strip trailing zeros/dot, normalise `-0`),
verified by an actual byte-for-byte `assert_eq!` against every bundled
glyph's stored `d` string (`path.rs`'s
`every_bundled_glyph_round_trips_byte_for_byte`, the load-bearing test). The
fallback was not needed and was not taken.
## `render_data` caching (pin 6)
`BravuraGlyphCatalog::render_data` looks up a `std::sync::OnceLock`-cached
`BTreeMap<&'static str, Vec<PathCommand>>`, built once by parsing every
bundled outline's `d` string on first use. `parse_d` is pure, so the cached
value a lookup clones is exactly what a fresh parse of the same bundled
string would produce on every call — caching changes only *when* the parse
work happens, never *what* it returns. No interior mutability leaks into the
result; two calls for the same name compare equal (`GlyphRenderData:
PartialEq`), asserted directly.
## `epiphany-render-svg`'s byte-neutrality (pin 2, pin 5)
`render-svg`'s `outline()`/`bundled_glyph_count()`/`smufl_codepoint()` now
delegate to this crate (`src/outline.rs` in each crate — `render-svg`'s copy
is a thin re-export/delegation, this crate's is the real lookup). Critically,
`render-svg`'s SVG emission (`svg.rs`'s `GlyphMode::PathOutline` arm) still
reads `outline(name).path` — the *stored* string — directly into the `<path
d="…">` attribute. It never routes through `parse_d`/`emit_d`; those exist
only for the round-trip proof and are not reachable from `render-svg` at all
(`path` is a private module here, and its functions are `pub(crate)`, not
exported). This is why the byte-neutrality probe (every SVG golden, every
`ResolvedLayoutIR::canonical_bytes()` reference-suite fixture) is unaffected
by this packet's existence — verified before and after against the base
commit, `diff -r` empty.
## Why a new crate and not a `layout-ir` module
The contract left this open (§CONTRACT_EDITOR_T4PRE_IR.md W2 framing: "the
open decision W2's contract resolves is the home … judged on dependency
weight and the MSRV/CI job structure"), and `CONTRACT_EDITOR_T4PRE_W2_GLYPHS.md`
pin 1 settles it: a new crate. Reasons that held once the data was in hand:
* `layout-ir` is deliberately data-light — it defines the `GlyphCatalog`
*interface* and a small in-tree metrics slice, not asset payloads; the
bundled outline table is ~37 KB of generated Rust literal that has nothing
to do with the constraint-solver interface `layout-ir` exists to define.
* A separate crate keeps the "moved verbatim, byte-for-byte identical output"
claim easy to audit: the outline table's home changed, its content and the
bytes it produces did not.
* It matches the existing pattern (`epiphany-render-svg` already held this
exact data privately) — moving it sideways into a shared crate is a
smaller, more reviewable diff than folding it into `layout-ir` and then
re-exporting it back out for `render-svg`'s and a future canvas renderer's
use.