epiphany/crates/epiphany-render-svg
Levi Neuwirth 4132a7ac05 Inter-staff review fix: make staff attribution y-aware (stems tore off)
Self-review of the inter-staff solve found a real bug: stroke->staff attribution
reused component_glyph, whose fallback picks the nearest glyph by X ALONE. That
is correct for a SLOT — both staves of a system share their x columns, hence
their spring slots, so the horizontal delta is the same either way — but wrong
for a STAFF: it handed a lower-staff stem to the UPPER staff's notehead. The
stem then kept the wrong vertical shift and tore off its own head (measured
worst stem->notehead distance 5.837 on the two-staff fixture vs 1.150, the stem
x-inset, on the single-staff one), and it polluted the upper staff's content
extent, inflating the computed gap.

Fix: the staff attribution uses a 2-D nearest for that fallback (a ledger still
resolves via owning_glyph's shared Pitch source; a staff line via its Staff
source). component_glyph is unchanged and still serves the horizontal path.

Also corrected: staff-attributed primitives now contribute their y ONLY through
the shifted path (Extent::add_x for x, add_y for the shifted staff extent), so a
lower staff's UNSHIFTED content can no longer inflate a system's max_y. Dead
Extent::add removed.

The corrected attribution yields a smaller, more accurate separation (two-staff
view_box height 36.1 -> 31.1). Regression multi_staff_stems_stay_on_their_own_
staff (verified to fail at 5.837 without the fix). Only the two-staff engrave
golden churned; single-staff goldens byte-stable. 948 tests, clippy 0, docs
-D warnings, conformance 8/8.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 09:42:51 -04:00
..
examples Phase 3 tranche 1: casting-off, K1 schema-fill, value-restoring undo 2026-07-02 21:55:26 -04:00
src Push 3 (slur quality) 1/3: render dashed/dotted slurs faithfully 2026-07-08 17:13:32 -04:00
tests Inter-staff review fix: make staff attribution y-aware (stems tore off) 2026-07-09 09:42:51 -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