epiphany/crates/epiphany-render-svg
Levi Neuwirth 24b6a34db9 Slurs: side from the stems, endpoints on the notes, apex clear of both
The rendered slurs were wrong in three independent ways, all visible in the
two-staff and three-staff goldens.

  1. Side. SlurDirection::Auto always arced above. The single-voice rule is
     OPPOSITE the stems -- all stems up puts the slur under the noteheads, all
     down puts it over them, and a mixed-stem span (which has no notehead side)
     goes above. Every Auto slur over a stem-up passage was drawn through its own
     stems. This is why stem direction had to land first: with every stem pointing
     up, "opposite the stems" means nothing.

  2. Endpoints. They sat at staff_top + gap -- a constant offset from the STAFF,
     not from the notes -- so a slur between two C6s hung below its own noteheads
     and crossed their ledger lines. They now sit a gap outside the endpoint
     column's ink, at the notehead's centre. Where the stem points the same way as
     the slur, that ink includes the stem, so the endpoint clears the stem tip.

  3. Clearance. The apex was span-proportional and blind, so a note between the
     endpoints poked straight through the arc. ColumnInk -- per staff, per column:
     top, bottom, stem direction, notehead centre -- is the obstacle field. The
     control points sit on the chord at thirds, so x is exactly linear in t and
     the arc's departure from the chord is 3*lift*t*(1-t); a column at t needing d
     more clearance forces an apex of at least d/(4*t*(1-t)).

An authored height is a floor, not a ceiling: clearance may raise it, so obeying
an author cannot draw a slur through a note. An authored direction still wins.

Obstacles are measured at the notehead CENTRE, the same x the endpoints use. The
first cut used the raw column x, which skews t and silently over-lifts: the
two-staff slur cleared its C6 by 4.05 spaces where 3.5 was needed. The clearance
test now asserts an upper bound as well as a lower one.

SLUR_INSET is gone. Endpoints at the notehead centres are what its 0.6-space
"tuck" approximated for the start point -- and got wrong for the end, where it
tucked a full notehead width to the LEFT of the final note.

Four mutations verified: always-above, staff-relative endpoints, no clearance
pass, and obstacles at the column x. The staff-relative-endpoint mutation PASSED
at first -- the tests asserted only "above the staff" / "below the staff", which a
staff-relative endpoint satisfies by construction. The exact-endpoint assertion
exists because that mutation survived.

Projection change, so no version moves; goldens churn.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 13:58:56 -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 Slurs: side from the stems, endpoints on the notes, apex clear of both 2026-07-09 13:58:56 -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