epiphany/crates/epiphany-render-svg
Levi Neuwirth efaebb9acd Let the band model carry staff attribution instead of inferring it
Stroke and Curve gain `vertical_band: VerticalBandId`, the field GlyphObject has
always carried. Curve's own doc comment used to call it "a *free* primitive (no
vertical band, no spring slot)" -- but the projection computed each primitive's
band, used it for the glyphs, and threw it away for the strokes and curves.

The engraver then reconstructed it geometrically, and got it wrong twice: nearest
glyph by x handed a lower-staff stem to the upper staff (4132a7a), and nearest
glyph to a slur's start endpoint handed a bottom-staff slur to the top staff
(b1bfe04). Both tore primitives off their own notes, both reached a committed
golden, both were caught by review rather than by the gate. A slur is the proof
the inference can never be made safe: its endpoints are deliberately lifted clear
of its own staff, into the zone where the nearest notehead belongs to the
neighbour. No distance metric recovers the owner.

Both fixes were correct and both were the wrong shape -- reconstructing by
inference a fact the projection had in hand and discarded. So: the projection
declares it (a slur's staff is its notes' staff), the engraver's attribution
becomes three map lookups, and ~60 lines of geometric rules, epsilons, and
fallbacks are deleted.

Two bands had to become unconditional, since strokes could otherwise name bands
that no glyph had caused to exist -- validation now rejects that as UnknownBand:

  - a staff band per staff of the region, in the region's own staff order (the
    order y_origin stacks by), not only for staves that emitted a glyph. A staff
    whose clef is unbundled engraves to an anchor *stroke* and no glyph.
  - the margin band unconditionally, because a region's own traced anchor is a
    stroke that names it whether or not a margin glyph puts a member in it.

Both may carry zero members, as an inter-staff gap band already did: membership
realizes the spring solve over glyphs; existence is what attribution needs.
Strokes and curves are deliberately NOT added to VerticalBand::members -- their
band reference is one-way.

vertical_band is not part of ResolvedLayoutIR::canonical_bytes (primitives are
encoded field-by-field), so this is layout metadata outside the canonical
encoding: no companion-version bump, and ENGRAVER_VERSION stays at 11 because the
output is unchanged. Zero goldens churn -- which is the evidence that the declared
owner agrees with the inferred one across the entire corpus.

Locked by every_stroke_and_curve_names_a_band_that_exists, verified by mutation:
making the margin band conditional again fails it on valid_score_rich. The two
tear-off regressions are kept -- they now assert an outcome the data model
guarantees, which is where a dropped declaration would surface.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 10:37:27 -04:00
..
examples Phase 3 tranche 1: casting-off, K1 schema-fill, value-restoring undo 2026-07-02 21:55:26 -04:00
src Let the band model carry staff attribution instead of inferring it 2026-07-09 10:37:27 -04:00
tests Close the inter-staff residual risk: a three-staff cascade fixture 2026-07-09 10:20:35 -04:00
tools Schema major 2 Phase E1: repeat barlines + volta brackets render 2026-07-07 21:51:11 -04:00
Cargo.toml Agent I-4c: embedded @font-face glyph mode (a second self-contained renderer) 2026-06-27 11:09:34 -04:00
DECISIONS.md Schema major 2 Phase E2: slur curves + cubic-bézier primitive 2026-07-08 15:33:00 -04:00
README.md Agent I-4c: embedded @font-face glyph mode (a second self-contained renderer) 2026-06-27 11:09:34 -04:00

README.md

epiphany-render-svg

Agent I's SVG renderer behind the Epiphany RenderIR interface (spec Chapter 7): turns a ResolvedLayoutIR into well-formed SVG 1.1, drawing each glyph from genuine Bravura SMuFL data — inline outline <path>s by default (GlyphMode::PathOutline), or <text> set in an @font-face-embedded Bravura subset (GlyphMode::EmbeddedFont). It is the visible end of the v0 Score → layout IR pipeline.

Status

The Score → layout IR → SVG pipeline renders recognizable notation — clefs, noteheads at clef-relative staff positions, accidentals, key/time signatures, rests, barlines, and the staff lines and stems that connect them. Output is golden-locked against both the interface-only stub solver and Agent I's real epiphany-engrave solver (whose horizontal spacing pass re-spaces the glyphs), and the layout round-trip (criterion 6) runs through both. What the renderer itself guarantees, independent of engraving quality: real Bravura glyphs, provenance preserved to the score graph, output XML-valid and deterministic. The renderer consumes any solver's ResolvedLayoutIR.

Demo

# Render a fixture to SVG (stub solver by default):
cargo run -p epiphany-render-svg --example render_fixture -- \
    ten_measure_single_staff > out.svg

# Drive Agent I's engrave solver instead, to bisect renderer-vs-solver:
cargo run -p epiphany-render-svg --example render_fixture -- \
    ten_measure_single_staff --solver=real > out.svg

# Use the embedded-font glyph mode (<text> + @font-face) instead of inline paths:
cargo run -p epiphany-render-svg --example render_fixture -- \
    ten_measure_single_staff --glyph-mode=embedded > out.svg

Fixtures: ten_measure_single_staff, valid_score_rich, valid_score. Stats and diagnostics go to stderr; the SVG goes to stdout.

Library

use epiphany_render_svg::{render, RenderOptions};

let out = render(&resolved_layout_ir, &RenderOptions::default());
assert!(out.is_well_formed());
println!("{}", out.svg);

render is pure and deterministic. RenderOptions controls SVG-encoding choices only (display scale, margin, provenance attributes, and glyph_mode — inline PathOutline vs EmbeddedFont) — nothing that changes engraving.

Bundled Bravura data

Two generated artifacts come from the official OFL Bravura.otf via tools/extract_bravura_outlines.py — the font is not vendored, only the generated Rust is committed:

  • src/outlines_generated.rs — the inline glyph outlines (geometry-only, so byte-stable across fontTools versions);
  • src/font_subset_generated.rs — a base64 OTF subset (just the pipeline's glyphs) for GlyphMode::EmbeddedFont. As a Modified Version, its primary font name is renamed off the Reserved Font Name "Bravura" per the OFL; a content BLAKE3 + decoded length are committed alongside as an integrity lock.

Bravura is © Steinberg Media Technologies GmbH under the SIL Open Font License 1.1 (tools/OFL.txt); both artifacts are redistributed under the same license. To regenerate both (the subset step also needs the blake3 package):

cd crates/epiphany-render-svg/tools
python3 -m venv .venv && . .venv/bin/activate && pip install fonttools blake3
python3 extract_bravura_outlines.py --font-out ../src/font_subset_generated.rs \
    > ../src/outlines_generated.rs