diff --git a/crates/epiphany-layout-ir/DECISIONS.md b/crates/epiphany-layout-ir/DECISIONS.md index 2046632..9696159 100644 --- a/crates/epiphany-layout-ir/DECISIONS.md +++ b/crates/epiphany-layout-ir/DECISIONS.md @@ -166,8 +166,12 @@ object is covered); the provenance-preservation contract itself is unchanged. parallel re-engraving) with both `metrics` and `render_data`. The in-tree Bravura catalog bundles metrics but **no** outlines/bitmaps, so its `render_data` honestly returns `None` (reporting `Some` would claim data that - does not exist). `font_version` is `Option`, set to the latest stable - Bravura release (`1.38.0`). Glyph anchors are a *map* keyed by name: the catalog + does not exist). `font_version` is `Option`, set to the SHA-pinned + `bravura-1.392` release the in-tree metrics are extracted from — the same font + the renderer's outlines come from, so reserved metrics and drawn ink agree. The + font declares a single decimal version (`"Version 1.392"`), recorded verbatim as + `SemVer { major: 1, minor: 392, patch: 0 }`; see `BRAVURA_VERSION` for the + canonical mapping rule. Glyph anchors are a *map* keyed by name: the catalog hash sorts them by name and **rejects** a duplicate name (a panic), so the hash never depends on anchor slice order (Appendix D §"Ordered Iteration"). every catalog method, including `identity`, is object-safe; owned font, diff --git a/crates/epiphany-layout-ir/src/constrained.rs b/crates/epiphany-layout-ir/src/constrained.rs index 431a2e4..6b2ead3 100644 --- a/crates/epiphany-layout-ir/src/constrained.rs +++ b/crates/epiphany-layout-ir/src/constrained.rs @@ -1030,7 +1030,11 @@ pub fn try_to_constrained( "barlineSingle" }; let info = column(&key); - let baseline = Point::new(info.x, yo + STAFF_HEIGHT / 2.0); + // The barline glyph's origin is its lower end — Bravura barlines + // run 0..4 staff spaces *up* from the origin — so anchoring it at + // the staff bottom (`yo`) makes it connect the bottom and top + // staff lines rather than float above the midline. + let baseline = Point::new(info.x, yo); emit.glyph(provenance, name, baseline, band_of(staff), staff, info.slot); // The time signature this measure introduces: numerator over // denominator, just right of the barline, each digit a diff --git a/crates/epiphany-layout-ir/src/glyph.rs b/crates/epiphany-layout-ir/src/glyph.rs index 1c26791..41ee3c9 100644 --- a/crates/epiphany-layout-ir/src/glyph.rs +++ b/crates/epiphany-layout-ir/src/glyph.rs @@ -99,9 +99,17 @@ pub struct GlyphCatalogIdentity { pub metrics_hash: [u8; 32], } -/// The Bravura release whose metrics the in-tree table approximates (the latest -/// stable Bravura release). -pub const BRAVURA_VERSION: SemVer = SemVer::new(1, 38, 0); +/// The Bravura release the in-tree metrics table is extracted from — the *same* +/// SHA-pinned font (`bravura-1.392`) the renderer's outlines come from, so the +/// reserved advances/bboxes and the drawn ink agree. +/// +/// **Version mapping (canonical):** the font declares a single decimal version — +/// name-table ID 5 is literally `"Version 1.392"` and `head.fontRevision ≈ 1.392` +/// — so it is recorded verbatim as `major.minor = 1.392`, i.e. `SemVer { major: 1, +/// minor: 392, patch: 0 }`. The minor field carries the font's `392` unchanged +/// rather than inventing a `1.39.2` patch split the font never declares, so this +/// identifier round-trips to the font's own version string. +pub const BRAVURA_VERSION: SemVer = SemVer::new(1, 392, 0); impl Default for GlyphCatalogIdentity { /// The bundled Bravura identity, with `metrics_hash` over the *whole* @@ -186,51 +194,61 @@ const STEM_DOWN_SE: GlyphAnchor = GlyphAnchor { const NOTEHEAD_ANCHORS: &[GlyphAnchor] = &[STEM_UP_NW, STEM_DOWN_SE]; /// A representative in-tree slice of Bravura's SMuFL metrics -/// (`(name, advance, [left, bottom, right, top])`, `1/1024`-staff-space units). -/// Every glyph the v0 pipeline names is in this table; the stub solver checks -/// that, so a missing entry surfaces as [`crate::SolveStatus::InternalError`]. +/// (`(name, advance, [left, bottom, right, top])`, `1/1024`-staff-space units), +/// extracted from the SHA-pinned `bravura-1.392` font by `epiphany-render-svg`'s +/// `tools/extract_bravura_outlines.py` — the **same** font the renderer's outlines +/// come from. Each `bbox` is the corresponding outline's bounds rounded *outward* +/// to the `1/1024` grid (floor the mins, ceil the maxes), so the metric box always +/// **contains** the drawn ink: the engraver evaluates collisions from this box, and +/// a containing box keeps a no-collision result honest on paper (a `render-svg` test +/// proves the containment). Every glyph the v0 pipeline names is in this table; the +/// stub solver checks that, so a missing entry surfaces as +/// [`crate::SolveStatus::InternalError`]. The named anchors are SMuFL +/// engraving-default approximations (font metadata, not glyf bounds — so not part of +/// the outline extraction). pub const BRAVURA_METRICS: &[GlyphMetric] = &[ GlyphMetric::anchored( "noteheadBlack", - 1180, - [0, -512, 1180, 512], + 1208, + [0, -512, 1209, 512], NOTEHEAD_ANCHORS, ), - GlyphMetric::anchored("noteheadHalf", 1180, [0, -512, 1180, 512], NOTEHEAD_ANCHORS), - GlyphMetric::new("noteheadWhole", 1690, [0, -512, 1690, 512]), - GlyphMetric::new("noteheadDoubleWhole", 2616, [0, -512, 2616, 512]), - GlyphMetric::new("gClef", 2684, [0, -2048, 2600, 4660]), - GlyphMetric::new("fClef", 2776, [0, -1024, 2776, 1024]), - GlyphMetric::new("cClef", 2884, [0, -2048, 2884, 2048]), - GlyphMetric::new("accidentalSharp", 994, [0, -1392, 994, 1392]), - GlyphMetric::new("accidentalFlat", 821, [0, -703, 821, 1751]), - GlyphMetric::new("accidentalNatural", 686, [0, -1377, 686, 1377]), - GlyphMetric::new("accidentalDoubleSharp", 1006, [0, -260, 1006, 260]), - GlyphMetric::new("restWhole", 1280, [0, 0, 1280, 512]), - GlyphMetric::new("restHalf", 1280, [0, -512, 1280, 0]), - GlyphMetric::new("restQuarter", 1024, [0, -1536, 1024, 1536]), - GlyphMetric::new("rest8th", 845, [0, -1024, 845, 1024]), - GlyphMetric::new("flag8thUp", 1007, [0, -84, 1007, 2607]), - GlyphMetric::new("flag8thDown", 1007, [0, -2607, 1007, 84]), - GlyphMetric::new("augmentationDot", 400, [0, -154, 308, 154]), - // Time-signature digits and the common-time C, with their genuine Bravura - // advances and tight bounding boxes (centred on the baseline, y ≈ ±1), from - // `tools/extract_bravura_outlines.py` — kept consistent with the outlines. - GlyphMetric::new("timeSig0", 1925, [82, -1024, 1843, 1028]), - GlyphMetric::new("timeSig1", 1368, [82, -1024, 1286, 1028]), - GlyphMetric::new("timeSig2", 1827, [82, -1053, 1745, 1040]), - GlyphMetric::new("timeSig3", 1724, [82, -1028, 1642, 1020]), - GlyphMetric::new("timeSig4", 1925, [82, -1024, 1843, 1028]), - GlyphMetric::new("timeSig5", 1651, [82, -1028, 1569, 1008]), - GlyphMetric::new("timeSig6", 1778, [82, -1020, 1696, 1028]), - GlyphMetric::new("timeSig7", 1806, [82, -1024, 1724, 1020]), - GlyphMetric::new("timeSig8", 1786, [82, -1061, 1704, 1061]), - GlyphMetric::new("timeSig9", 1778, [82, -1020, 1696, 1028]), - GlyphMetric::new("timeSigCommon", 1737, [20, -1020, 1737, 1028]), - GlyphMetric::new("barlineSingle", 160, [0, -2048, 160, 2048]), - GlyphMetric::new("barlineFinal", 620, [0, -2048, 620, 2048]), - GlyphMetric::new("dynamicForte", 1480, [0, -706, 1480, 1565]), - GlyphMetric::new("dynamicPiano", 1700, [0, -509, 1700, 1565]), + GlyphMetric::anchored("noteheadHalf", 1208, [0, -512, 1209, 512], NOTEHEAD_ANCHORS), + GlyphMetric::new("noteheadWhole", 1729, [0, -512, 1729, 512]), + GlyphMetric::new("noteheadDoubleWhole", 2454, [0, -635, 2454, 635]), + GlyphMetric::new("gClef", 2748, [0, -2696, 2749, 4498]), + GlyphMetric::new("fClef", 2802, [-21, -2601, 2802, 1074]), + GlyphMetric::new("cClef", 2863, [0, -2073, 2864, 2073]), + GlyphMetric::new("accidentalSharp", 1020, [0, -1426, 1020, 1434]), + GlyphMetric::new("accidentalFlat", 926, [0, -717, 926, 1799]), + GlyphMetric::new("accidentalNatural", 688, [0, -1373, 689, 1397]), + GlyphMetric::new("accidentalDoubleSharp", 1024, [0, -512, 1012, 521]), + GlyphMetric::new("restWhole", 1159, [0, -553, 1156, 37]), + GlyphMetric::new("restHalf", 1159, [0, -9, 1156, 582]), + GlyphMetric::new("restQuarter", 1106, [4, -1536, 1106, 1528]), + GlyphMetric::new("rest8th", 1024, [0, -1029, 1012, 713]), + GlyphMetric::new("flag8thUp", 1081, [0, -3319, 1082, 37]), + GlyphMetric::new("flag8thDown", 1253, [0, -58, 1254, 3310]), + GlyphMetric::new("augmentationDot", 410, [0, -205, 410, 205]), + // Time-signature digits and the common-time C — centred on the baseline + // (y ≈ ±1), so a numerator/denominator pair straddles the staff midline. + GlyphMetric::new("timeSig0", 1925, [81, -1024, 1844, 1029]), + GlyphMetric::new("timeSig1", 1368, [81, -1024, 1287, 1029]), + GlyphMetric::new("timeSig2", 1827, [81, -1053, 1745, 1041]), + GlyphMetric::new("timeSig3", 1724, [81, -1029, 1643, 1020]), + GlyphMetric::new("timeSig4", 1925, [81, -1024, 1844, 1029]), + GlyphMetric::new("timeSig5", 1651, [81, -1029, 1569, 1008]), + GlyphMetric::new("timeSig6", 1778, [81, -1020, 1696, 1029]), + GlyphMetric::new("timeSig7", 1806, [81, -1024, 1725, 1020]), + GlyphMetric::new("timeSig8", 1786, [81, -1061, 1704, 1061]), + GlyphMetric::new("timeSig9", 1778, [81, -1020, 1696, 1029]), + GlyphMetric::new("timeSigCommon", 1737, [20, -1020, 1737, 1029]), + // Barlines: the origin is the lower end; the line runs 0..4096 (four staff + // spaces) *up* from it, spanning the staff when anchored at the bottom line. + GlyphMetric::new("barlineSingle", 147, [0, 0, 148, 4096]), + GlyphMetric::new("barlineFinal", 938, [0, 0, 934, 4096]), + GlyphMetric::new("dynamicForte", 1491, [-578, -623, 1491, 1819]), + GlyphMetric::new("dynamicPiano", 1495, [-365, -582, 1500, 1123]), ]; /// Looks up one glyph's metrics by SMuFL name, if bundled. diff --git a/crates/epiphany-render-svg/src/outline.rs b/crates/epiphany-render-svg/src/outline.rs index 5b442e8..cf889f8 100644 --- a/crates/epiphany-render-svg/src/outline.rs +++ b/crates/epiphany-render-svg/src/outline.rs @@ -50,6 +50,30 @@ mod tests { } } + #[test] + fn metric_bboxes_contain_the_drawn_outlines() { + // The engraver evaluates collisions from a glyph's metric bounding box, + // while the renderer draws (and bounds) its outline. If a metric box were a + // hair smaller than the ink — e.g. from rounding the bbox to the *nearest* + // 1/1024 — a hard no-collision verdict could be microscopically false on + // paper. The metrics are extracted as the outline bounds rounded *outward* + // to the grid, so every metric box must contain its outline box. + for m in epiphany_layout_ir::BRAVURA_METRICS { + let Some(o) = outline(m.name.as_ref()) else { + continue; + }; + let mb = m.bounding_box(); + let [ol, ob, oright, otop] = o.bbox; + assert!( + mb.left.0 <= ol && mb.bottom.0 <= ob && mb.right.0 >= oright && mb.top.0 >= otop, + "metric bbox {:?} for {} must contain its outline bbox {:?}", + [mb.left.0, mb.bottom.0, mb.right.0, mb.top.0], + m.name, + o.bbox, + ); + } + } + #[test] fn outlines_have_finite_bounds_and_nonempty_paths() { for o in BRAVURA_OUTLINES { diff --git a/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.engrave.snapshot.txt b/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.engrave.snapshot.txt index d3ae548..44b9459 100644 --- a/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.engrave.snapshot.txt +++ b/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.engrave.snapshot.txt @@ -7,7 +7,7 @@ provenance_count=102 layer_count=1 hard_constraint_count=0 xml_well_formed=true -view_box=[-3.0113542 -3.632 82.26242 11.632] +view_box=[-3.059857 -3.632 82.456436 11.024] class_counts: barline=10 clef=1 diff --git a/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.engrave.svg b/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.engrave.svg index d6fc091..08b0108 100644 --- a/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.engrave.svg +++ b/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.engrave.svg @@ -1,110 +1,110 @@ - + - + - - - - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.stub.snapshot.txt b/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.stub.snapshot.txt index 9d394f0..a70b3c4 100644 --- a/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.stub.snapshot.txt +++ b/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.stub.snapshot.txt @@ -7,7 +7,7 @@ provenance_count=102 layer_count=1 hard_constraint_count=0 xml_well_formed=true -view_box=[-3.065 -3.632 88.87701 11.632] +view_box=[-3.065 -3.632 88.87701 11.024] class_counts: barline=10 clef=1 diff --git a/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.stub.svg b/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.stub.svg index 2a8d4ca..ec9bda4 100644 --- a/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.stub.svg +++ b/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.stub.svg @@ -1,7 +1,7 @@ - + - + @@ -95,16 +95,16 @@ - - - - - - - - - - + + + + + + + + + + diff --git a/crates/epiphany-render-svg/tests/golden/valid_score_rich.engrave.snapshot.txt b/crates/epiphany-render-svg/tests/golden/valid_score_rich.engrave.snapshot.txt index 2d34da6..dd06d51 100644 --- a/crates/epiphany-render-svg/tests/golden/valid_score_rich.engrave.snapshot.txt +++ b/crates/epiphany-render-svg/tests/golden/valid_score_rich.engrave.snapshot.txt @@ -7,7 +7,7 @@ provenance_count=44 layer_count=1 hard_constraint_count=0 xml_well_formed=true -view_box=[-3.0113542 -3.632 27.468542 11.632] +view_box=[-3.059857 -3.632 27.953568 11.024] class_counts: barline=1 clef=3 diff --git a/crates/epiphany-render-svg/tests/golden/valid_score_rich.engrave.svg b/crates/epiphany-render-svg/tests/golden/valid_score_rich.engrave.svg index 3d5f346..1346811 100644 --- a/crates/epiphany-render-svg/tests/golden/valid_score_rich.engrave.svg +++ b/crates/epiphany-render-svg/tests/golden/valid_score_rich.engrave.svg @@ -1,52 +1,52 @@ - + - + - - - - - + + + + + - - - + + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + diff --git a/crates/epiphany-render-svg/tests/golden/valid_score_rich.stub.snapshot.txt b/crates/epiphany-render-svg/tests/golden/valid_score_rich.stub.snapshot.txt index c6f222e..263543e 100644 --- a/crates/epiphany-render-svg/tests/golden/valid_score_rich.stub.snapshot.txt +++ b/crates/epiphany-render-svg/tests/golden/valid_score_rich.stub.snapshot.txt @@ -7,7 +7,7 @@ provenance_count=44 layer_count=1 hard_constraint_count=0 xml_well_formed=true -view_box=[-3.065 -3.632 39.329998 11.632] +view_box=[-3.065 -3.632 39.329998 11.024] class_counts: barline=1 clef=3 diff --git a/crates/epiphany-render-svg/tests/golden/valid_score_rich.stub.svg b/crates/epiphany-render-svg/tests/golden/valid_score_rich.stub.svg index 9e5cc65..fe573e5 100644 --- a/crates/epiphany-render-svg/tests/golden/valid_score_rich.stub.svg +++ b/crates/epiphany-render-svg/tests/golden/valid_score_rich.stub.svg @@ -1,7 +1,7 @@ - + - + @@ -40,7 +40,7 @@ - + diff --git a/crates/epiphany-render-svg/tools/extract_bravura_outlines.py b/crates/epiphany-render-svg/tools/extract_bravura_outlines.py index 6b25255..1b2c063 100644 --- a/crates/epiphany-render-svg/tools/extract_bravura_outlines.py +++ b/crates/epiphany-render-svg/tools/extract_bravura_outlines.py @@ -14,7 +14,7 @@ The font is NOT vendored; only the generated Rust is committed. Bravura is © Steinberg Media Technologies GmbH under the SIL Open Font License 1.1; the extracted outlines are redistributed under the same license (see OFL.txt). """ -import hashlib, json, re, sys, urllib.request +import hashlib, json, math, re, sys, urllib.request # Pinned, immutable sources. A moving branch (`master` / `gh-pages`) would make # regeneration non-reproducible: a future font update would silently change the @@ -94,9 +94,14 @@ def main(): l, b, r, t = ([round(v * scale, 4) for v in bp.bounds] if bp.bounds else [0, 0, 0, 0]) rows.append((name, cp, d, (l, b, r, t))) # Companion metrics for layout-ir `BRAVURA_METRICS` (1/1024 staff space): - # the glyph's advance (from hmtx) and tight bbox. + # the advance (from hmtx), and a bbox rounded *outward* from the outline's + # bbox — floor the mins, ceil the maxes — so the integer metric box always + # *contains* the drawn outline. The engraver evaluates collisions from the + # metric box; an inward (nearest) round could leave it a hair smaller than + # the ink, making a no-collision result microscopically false on paper. adv1024 = round(hmtx[g][0] * scale * 1024) - bbox1024 = [round(v * scale * 1024) for v in (bp.bounds or (0, 0, 0, 0))] + bbox1024 = [math.floor(l * 1024), math.floor(b * 1024), + math.ceil(r * 1024), math.ceil(t * 1024)] metrics.append((name, adv1024, bbox1024)) rows.sort() print("// --- BRAVURA_METRICS rows (advance, [l,b,r,t] in 1/1024 staff space) ---",