160 lines
10 KiB
Markdown
160 lines
10 KiB
Markdown
# epiphany-render-svg — decisions and Pass 12 candidates
|
|
|
|
This file records (a) the QUICKSTART decisions Agent I made for the renderer,
|
|
and (b) ambiguities batched as **Pass 12 candidates** (`spec/PASS12_BATCH.md`)
|
|
rather than improvised in code.
|
|
|
|
## Scope and status
|
|
|
|
`epiphany-render-svg` is the SVG renderer behind the Chapter 7 `RenderIR`
|
|
interface: it turns a `ResolvedLayoutIR` into well-formed **SVG 1.1**, drawing
|
|
glyphs from genuine Bravura SMuFL data either as inline outline `<path>`s
|
|
(`GlyphMode::PathOutline`) or as `<text>` set in an embedded subset font
|
|
(`GlyphMode::EmbeddedFont`). Per the QUICKSTART development pattern it was
|
|
golden-locked against the stub solver's output first, then against the real
|
|
`epiphany-engrave` solver once the score→real-notation pass and re-spacing
|
|
landed. The renderer consumes any solver's `ResolvedLayoutIR`; it proves
|
|
renderer faithfulness — resolved geometry preserved, provenance traced,
|
|
output XML-valid and deterministic — independent of engraving quality.
|
|
|
|
## The non-overreach rule (Chapter 7 / QUICKSTART, Agent I)
|
|
|
|
The renderer makes **SVG-encoding choices only** and **no engraving-semantic**
|
|
choices. In the default **archival** mode every emitted element traces to a
|
|
`ResolvedGlyph` (and thus a score-graph source, via `data-prov`/`data-source-kind`)
|
|
or to a declared renderer wrapper (the `<svg>` root, the metadata comment, the
|
|
y-flip `<g>`, a per-layer `<g>`). Traces can be turned off
|
|
(`RenderOptions::emit_provenance = false`) for a smaller **display-only** SVG; that
|
|
is an explicit mode the **metadata comment declares** (archival → "carries a
|
|
data-prov trace", display-only → "provenance traces suppressed"), so a trace-free
|
|
SVG — including the empty canvas — announces itself rather than passing as
|
|
archival. A glyph with no bundled outline is **surfaced as a diagnostic** and drawn
|
|
as a visible bounding-box fallback `<rect>` — never silently dropped and never
|
|
invented. The acceptance harness (archival mode) asserts one drawn element per
|
|
glyph and one provenance trace per drawn element.
|
|
|
|
## Implementation decisions (QUICKSTART "Decisions you'll need to make")
|
|
|
|
1. **Spelling / 2. Solver architecture** — N/A here (Agent H; `epiphany-engrave`).
|
|
3. **Renderer SVG dialect — SVG 1.1 + inline presentation attributes.** Maximum
|
|
portability for what is effectively a viewer/print artifact; no CSS, no SVG 2
|
|
features. (CSS styling can come later if needed.) Output validates under the
|
|
system `xmllint` (libxml2) as well as the in-crate well-formedness checker.
|
|
4. **Catalog / 5. Binary Format versioning** — N/A (Agents K, J).
|
|
|
|
### Local decisions
|
|
|
|
- **Glyph rendering — two self-contained modes; inline outlines
|
|
(`GlyphMode::PathOutline`) is the default and the verified reference.** Path
|
|
outlines make the SVG self-contained (it renders in any browser, image tool, or
|
|
print pipeline with no font installed) and are byte-golden-locked, at the cost of
|
|
file size — the QUICKSTART's recommendation. `GlyphMode::EmbeddedFont` instead
|
|
references each glyph by SMuFL codepoint with a `<text>` element drawn from an
|
|
`@font-face`-embedded Bravura *subset* (only the ~33 named glyphs), so the SVG is
|
|
still self-contained (the font travels in it) and the text is selectable, at a
|
|
larger file size. The two modes anchor glyphs at the same origin (em = 4 staff
|
|
spaces), so placement is consistent by construction; the embedded mode is
|
|
structurally tested rather than byte-golden-locked, and exact rasterisation is
|
|
the consumer's font renderer's, so path mode remains the pixel-verified one.
|
|
- **Embedded-font subset — generated, not a vendored binary.** The subset is a
|
|
deterministic base64 OTF emitted into `src/font_subset_generated.rs` by
|
|
`tools/extract_bravura_outlines.py --font-out`, keeping the "only generated
|
|
artifacts committed" rule (no font binary is vendored). It retains the font's
|
|
OFL copyright/license name records (belt-and-suspenders with `tools/OFL.txt`).
|
|
Caveat: unlike the geometry-only outlines, the binary subset's exact bytes
|
|
depend on the fontTools version, which the generated header records.
|
|
- **Outline source — the official OFL `Bravura.otf`, extracted reproducibly.**
|
|
`tools/extract_bravura_outlines.py` fetches the font + SMuFL `glyphnames.json`
|
|
and emits `src/outlines_generated.rs`. The font is **not vendored**; only the
|
|
generated Rust is committed (`tools/OFL.txt` carries the SIL Open Font License
|
|
1.1 under which the outlines are redistributed). Exactly the glyph set the v0
|
|
pipeline can name (`layout-ir`'s `BRAVURA_METRICS`) is bundled; a test asserts
|
|
every pipeline glyph has an outline (so the table cannot silently fall behind).
|
|
- **Coordinate system — staff spaces, y-up, one global flip.** Outlines are
|
|
extracted in staff-space units (SMuFL em = 4 staff spaces, Bravura
|
|
unitsPerEm = 1000 ⇒ 1 staff space = 250 font units), **y-up** (musical
|
|
convention). The whole document is wrapped in one
|
|
`translate(-min_x, max_y) scale(1, -1)` group, so every glyph is placed with a
|
|
plain `translate(x, y)` and the `viewBox` is `0 0 W H` in staff spaces;
|
|
`width`/`height` carry the px display scale. The renderer never bakes the flip
|
|
into per-glyph data.
|
|
- **Determinism — fixed 4-decimal number formatting, `-0` normalised to `0`.**
|
|
Outline `d` data is fixed bundled text; every computed coordinate is formatted
|
|
identically, so identical input yields byte-identical SVG. The acceptance
|
|
harness golden-locks the full SVG and a machine snapshot (object/glyph/path/
|
|
provenance/layer/per-class/hard-constraint counts + XML validity).
|
|
- **No external XML dependency.** The workspace is deliberately dependency-light;
|
|
`xml::check_well_formed` is a hand-rolled validator for the subset the renderer
|
|
emits, and the acceptance test additionally cross-checks with `xmllint` when
|
|
present so the "XML-validates" claim rests on a real parser too.
|
|
|
|
## Pass 12 candidates
|
|
|
|
See `spec/PASS12_BATCH.md` (rows P12-I1, P12-I2, P12-I3). Most relevant here:
|
|
|
|
- **P12-I1 (resolved by I-1/I-3)** — the original stub-only renderer output was
|
|
a structural placeholder, so the human-review visual-acceptance gate ("the SVG
|
|
visually parses as standard music notation") was deferred until real engraving
|
|
landed. The real notation pass and real-Engraver goldens now close that gate;
|
|
the stub path remains locked as an interface/reference mode, not the visual
|
|
deliverable.
|
|
- **P12-I2 (resolved)** — the stable layout-object id derivation (`MUSCLOID`,
|
|
Pass-11 item 2.6) is wired: `epiphany-determinism` reserves the built-in
|
|
`MUSCLOID` tag and `layout-ir` provenance routes through it. The renderer traces
|
|
provenance by the (now `MUSCLOID`-tagged) `stable_id`; only the `data-prov` hex in
|
|
the goldens changed (the ids are non-canonical).
|
|
|
|
## Repeat glyphs bundled + `repeat` glyph class (E1, 2026-07-07)
|
|
|
|
The E1 repeat tranche added `repeatLeft`, `repeatRight`, `repeatRightLeft`,
|
|
and `repeatDots` to the extractor's `NAMES` set; outlines, companion
|
|
layout-ir metrics, and the embedded font subset were regenerated from the
|
|
same SHA-pinned `bravura-1.392` (fontTools 4.63.0 preserved, so the subset
|
|
diff is content-only). `GlyphClass` gained a `Repeat` class (token
|
|
`repeat`) so snapshots and `data-class` attributes separate repeat signs
|
|
from plain barlines. Volta ending numerals arrive as `timeSig` digit glyphs
|
|
(there is still no free-text primitive — unchanged).
|
|
|
|
## Curve primitive → stroked `<path>`, `GlyphClass` unaffected (E2, 2026-07-08)
|
|
|
|
A resolved `Curve` (slur) emits a stroked, unfilled cubic-bézier `<path d="M..
|
|
C.."` with `data-kind="curve"` and a `curve_provenance_attrs` trace — drawn
|
|
after strokes and before glyphs (a slur sits over the staff lines, under the
|
|
noteheads it joins), in its own layer-grouped loop parallel to the stroke
|
|
loop. `content_bounds` grows by each curve's control-point hull ± half-thickness
|
|
(a cubic's ink never bows past its hull). `RenderStats` gained `curve_count`;
|
|
the acceptance snapshot prints it and the `provenance_count == glyph + stroke`
|
|
invariant became `+ curve`. `GlyphClass` is untouched — curves are not glyphs,
|
|
so they carry no `data-class`.
|
|
|
|
## Bundled outline table moved to `epiphany-glyphs` (Editor T4-pre W2, 2026-07-24)
|
|
|
|
`src/outlines_generated.rs` and `tools/{extract_bravura_outlines.py,OFL.txt}`
|
|
moved out of this crate into the new `epiphany-glyphs` crate — the shared
|
|
typed glyph-asset seam a canvas tessellator needs (`spec/PLAN_EDITOR_APP.md`
|
|
§3.7 / Ruling A), populated on top of `layout-ir`'s already-designed
|
|
`PathCommand`/`GlyphRenderData`/`GlyphCatalog::render_data` interface. This
|
|
crate now depends on `epiphany-glyphs` instead of owning the table; `outline()`,
|
|
`bundled_glyph_count()`, and `smufl_codepoint()` in `src/outline.rs` became
|
|
thin delegations (the latter two stay `pub` here per the W2 contract's pin 2,
|
|
even though nothing outside this crate calls them — an API that costs one
|
|
`pub use` is not worth breaking). The table's own tests (sortedness, pipeline
|
|
coverage, metric/outline bbox containment, finite-bounds sanity) moved with
|
|
it to `epiphany-glyphs`; this crate's `outline.rs` test module now holds only
|
|
the font-subset-specific tests (`font_subset_generated.rs` deliberately
|
|
**stayed** — an embeddable font subset is a renderer concern, not a shared
|
|
asset) plus a thin delegation smoke test.
|
|
|
|
**Byte-neutrality is unconditional.** `svg.rs`'s `GlyphMode::PathOutline` arm
|
|
still reads `outline(name).path` — the stored `d` string — directly into the
|
|
emitted `<path d="…">`. `epiphany-glyphs` additionally parses that same
|
|
string into typed `PathCommand`s (for `BravuraGlyphCatalog::render_data`,
|
|
consumed by a future canvas renderer, not by this crate), but that parser and
|
|
its round-trip re-emitter are private to `epiphany-glyphs` and unreachable
|
|
from here — this renderer has no code path that could route through the
|
|
typed form even by mistake. Verified before/after against the base commit: a
|
|
throwaway probe captured `ResolvedLayoutIR::canonical_bytes()` for every
|
|
reference-suite fixture plus the two named W1 fixtures, and all five GUI
|
|
goldens were re-run; every byte was identical (reported in the W2 packet
|
|
report, not committed here).
|