//! The glyph catalog (Chapter 7 §"Glyph Catalog Interface") and its //! reproducibility identity (§7.3.2), with Bravura metrics bundled in-tree for //! testing (QUICKSTART, Agent E). //! //! The IR references glyphs by name and queries metrics from a font catalog; //! metrics are never embedded in pipeline objects (Chapter 7 §"Glyph metrics //! live elsewhere"). For reproducible layout, the exact catalog consumed by a //! solve MUST be identifiable: [`GlyphCatalogIdentity`] carries the font id, its //! version, the SMuFL version, and a content hash over the canonical //! serialization of every consulted glyph's metrics (bounding box, advance //! width, **and named anchors**), computed with the Appendix D domain tag //! `MUSCFNTM` ([`DomainTag::FONT_METRICS`]). //! //! v0 bundles a small but representative slice of the real //! [Bravura](https://github.com/steinbergmedia/bravura) SMuFL font's metrics, in //! `1/1024`-staff-space units (the catalog's exact, hashable unit), so the //! catalog identity is exercised end to end without shipping a font file. A full //! catalog (and the render-data side of the interface) is an out-of-core concern. use std::borrow::Cow; use std::collections::{BTreeMap, BTreeSet}; use epiphany_determinism::{DomainTag, Preimage}; use crate::spatial::{BoundingBox, Point}; /// The SMuFL version a catalog targets (Chapter 7: `SmuflVersion`). /// /// This is [`epiphany_core::SmuflVersion`] (Chapter 4 §"SMuFL Versioning"): /// the glyph catalog's version and the tuning context's declared SMuFL /// version are **the same type**, unified here in Push 4b tranche 3b-ii /// (P13-S12). Layout-ir previously defined its own `{ major: u16, minor: u16 /// }` storing the minor *literally*, whose derived `Ord` sorted 1.3/1.4 /// before 1.12 — backwards versus SMuFL's real release order. See /// [`epiphany_core::SmuflVersion`]'s doc comment for the fraction-normalized /// `minor_centi` shape that fixes it. pub use epiphany_core::SmuflVersion; /// Identifier of a specific SMuFL font (Chapter 7: `FontId`). #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] pub struct FontId(pub Cow<'static, str>); impl FontId { /// The reference font v0 bundles metrics for. pub const BRAVURA: FontId = FontId(Cow::Borrowed("Bravura")); /// Constructs an identifier for a catalog loaded at runtime. pub fn owned(name: impl Into) -> Self { FontId(Cow::Owned(name.into())) } } /// A font-catalog glyph identifier that may be bundled or loaded at runtime. #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] pub struct GlyphReference(pub Cow<'static, str>); impl GlyphReference { pub const fn borrowed(name: &'static str) -> Self { GlyphReference(Cow::Borrowed(name)) } pub fn owned(name: impl Into) -> Self { GlyphReference(Cow::Owned(name.into())) } pub fn as_str(&self) -> &str { self.0.as_ref() } } /// A semantic font version (Chapter 7 §7.3.2: the optional `font_version`). #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] pub struct SemVer { pub major: u32, pub minor: u32, pub patch: u32, } impl SemVer { pub const fn new(major: u32, minor: u32, patch: u32) -> Self { SemVer { major, minor, patch, } } } /// A reproducibility-quality identifier for the glyph catalog used to produce a /// layout (Chapter 7 §7.3.2). Required for any layout-conformance claim that /// depends on byte-equal output across runs. #[derive(Clone, PartialEq, Eq, Debug)] pub struct GlyphCatalogIdentity { /// SMuFL version targeted. pub smufl_version: SmuflVersion, /// The specific font in use. pub font_id: FontId, /// The font's release version, if the publisher uses versioned releases /// (Chapter 7 §7.3.2: the spec's optional `font_version`). Bravura is /// versioned, so v0 records the release the bundled metrics track. pub font_version: Option, /// Content hash (BLAKE3 / `MUSCFNTM`) over the canonical serialization of /// every consulted glyph's metrics (Chapter 7 §7.3.2). pub metrics_hash: [u8; 32], } /// 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* /// in-tree table. A [`crate::ConstrainedLayoutIR`] overrides the hash with /// one over only the glyphs it references (the solve's true inputs). fn default() -> Self { GlyphCatalogIdentity { smufl_version: SmuflVersion::from_decimal(1, "4") .expect("1.4 is a valid SMuFL version"), font_id: FontId::BRAVURA, font_version: Some(BRAVURA_VERSION), metrics_hash: metrics_hash_for(BRAVURA_METRICS.iter().map(|m| m.name.as_ref())), } } } /// A named anchor on a glyph (Chapter 7 §"Glyph Catalog Interface": /// `GlyphMetrics.anchors`), e.g. a notehead's stem-attachment point, in /// `1/1024`-staff-space units relative to the glyph origin. #[derive(Clone, PartialEq, Eq, Debug)] pub struct GlyphAnchor { pub name: Cow<'static, str>, pub x: i32, pub y: i32, } /// One glyph's metrics: advance width, bounding box, and named anchors, in /// `1/1024`-staff-space units (Chapter 7 §"Glyph Catalog Interface": /// `GlyphMetrics`). Units are exact integers (hashable, deterministic); /// [`GlyphMetric::bounding_box`] converts to staff-space `f32` on demand. #[derive(Clone, PartialEq, Eq, Debug)] pub struct GlyphMetric { pub name: Cow<'static, str>, pub advance: i32, pub bbox: [i32; 4], pub anchors: Cow<'static, [GlyphAnchor]>, } impl GlyphMetric { /// Constructs a metrics entry with no named anchors. pub const fn new(name: &'static str, advance: i32, bbox: [i32; 4]) -> Self { GlyphMetric { name: Cow::Borrowed(name), advance, bbox, anchors: Cow::Borrowed(&[]), } } /// Constructs a metrics entry with named anchors. pub const fn anchored( name: &'static str, advance: i32, bbox: [i32; 4], anchors: &'static [GlyphAnchor], ) -> Self { GlyphMetric { name: Cow::Borrowed(name), advance, bbox, anchors: Cow::Borrowed(anchors), } } /// The bounding box in staff spaces (Chapter 7: `GlyphMetrics.bounding_box`). pub fn bounding_box(&self) -> BoundingBox { let g = |u: i32| u as f32 / 1024.0; let [l, b, r, t] = self.bbox; BoundingBox::new(g(l), g(b), g(r), g(t)) } } /// A representative in-tree slice of Bravura's SMuFL metrics /// (`(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`]. /// /// **No glyph carries anchors.** They live in the font's SMuFL metadata /// (`bravura_metadata.json`), not its glyf bounds, so the outline extraction never /// covered them and the two the table once held were written by hand — and were /// wrong under any reading (P13-I3). `extract_bravura_outlines.py --anchors` now /// emits them from the pinned metadata; the table regains them at the next /// regeneration, generated rather than remembered. Nothing consumes anchors today: /// a stem takes its attachment from the notehead's bounding box, whose edges are /// the anchor x's the font declares. pub const BRAVURA_METRICS: &[GlyphMetric] = &[ GlyphMetric::new("noteheadBlack", 1208, [0, -512, 1209, 512]), GlyphMetric::new("noteheadHalf", 1208, [0, -512, 1209, 512]), 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]), // Repeat signs: composite barline glyphs (heavy/thin lines plus dots), // staff-spanning like the barlines above; `repeatDots` is the bare dot // pair drawn beside a barline (its box sits in the middle two staff // spaces when anchored at the bottom line). GlyphMetric::new("repeatLeft", 1507, [0, 0, 1500, 4096]), GlyphMetric::new("repeatRight", 1503, [4, 0, 1504, 4096]), GlyphMetric::new("repeatRightLeft", 2490, [4, 0, 2491, 4096]), GlyphMetric::new("repeatDots", 410, [0, 1302, 410, 2745]), 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. pub fn metrics(name: &str) -> Option<&'static GlyphMetric> { BRAVURA_METRICS.iter().find(|m| m.name.as_ref() == name) } /// Whether every name in `names` has bundled metrics. pub fn all_available<'a>(names: impl IntoIterator) -> bool { names.into_iter().all(|n| metrics(n).is_some()) } /// Whether a glyph name draws a **measure-boundary barline**: the plain and /// final barlines plus the composite repeat signs (which replace a plain /// barline at a repeat boundary). The casting-off solver classifies /// measure-boundary columns with this instead of hard-coding the engraver's /// name choices. `repeatDots` is decoration drawn *beside* a barline, not a /// barline itself, so it is excluded. pub fn is_barline_glyph(name: &str) -> bool { name.starts_with("barline") || matches!(name, "repeatLeft" | "repeatRight" | "repeatRightLeft") } /// A glyph's rendering data (Chapter 7 §"Glyph Catalog Interface": /// `GlyphRenderData`). The full outline/bitmap vocabulary (`PathCommand`, /// `GlyphBitmap`) belongs to the out-of-core renderer; v0 carries an opaque /// marker so the interface is complete without bundling outlines. #[derive(Clone, PartialEq, Debug)] pub enum PathCommand { MoveTo(Point), LineTo(Point), CurveTo { control1: Point, control2: Point, to: Point, }, Close, } #[derive(Clone, PartialEq, Eq, Debug)] pub struct GlyphBitmap { pub width: u32, pub height: u32, pub rgba8: Vec, } #[derive(Clone, PartialEq, Debug, Default)] pub struct GlyphRenderData { pub outline: Vec, pub bitmap: Option, } /// The font-catalog query interface (Chapter 7 §"Glyph Catalog Interface": /// `GlyphCatalog`). `Send + Sync` per the spec, so a catalog can be shared /// across threads during parallel re-engraving. pub trait GlyphCatalog: Send + Sync { /// Resolve a glyph name to its metrics. fn metrics(&self, name: &str) -> Option<&GlyphMetric>; /// Resolve a glyph name to its rendering data, if any. fn render_data(&self, name: &str) -> Option; /// The SMuFL version this catalog supports. fn smufl_version(&self) -> SmuflVersion; /// This catalog's reproducibility identity over the given consulted names. fn identity(&self, consulted: &[&str]) -> GlyphCatalogIdentity; } /// The bundled in-tree Bravura catalog. **Metric-only**: it bundles no render /// data (outlines/bitmaps are a renderer concern), so /// [`BravuraCatalog::render_data`] honestly returns `None` for every glyph. pub struct BravuraCatalog; impl GlyphCatalog for BravuraCatalog { fn metrics(&self, name: &str) -> Option<&GlyphMetric> { metrics(name) } fn render_data(&self, _name: &str) -> Option { // No outlines or bitmaps are bundled; reporting `Some` would claim render // data that does not exist. None } fn smufl_version(&self) -> SmuflVersion { SmuflVersion::from_decimal(1, "4").expect("1.4 is a valid SMuFL version") } fn identity(&self, consulted: &[&str]) -> GlyphCatalogIdentity { GlyphCatalogIdentity { metrics_hash: metrics_hash_for(consulted.iter().copied()), ..GlyphCatalogIdentity::default() } } } /// The catalog metrics identity (Chapter 7 §7.3.2): a **domain-tagged** /// (`MUSCFNTM`) BLAKE3 hash over the canonical serialization of the consulted /// glyph metrics (advance, bounding box, and named anchors), rather than a raw /// hash of a descriptive string. /// /// Names are de-duplicated and put in canonical (sorted) order first, so two /// solves consulting the same metric set hash identically regardless of glyph /// ordering (Appendix D §"Ordered Iteration"). Panics if a name has no bundled /// metrics — every glyph delivered to a solve MUST name available metrics. pub fn metrics_hash_for<'a>(names: impl IntoIterator) -> [u8; 32] { let names: BTreeSet<&str> = names.into_iter().collect(); let entries: Vec<(&str, &GlyphMetric)> = names .into_iter() .map(|name| { ( name, metrics(name).expect("every consulted glyph must name bundled metrics"), ) }) .collect(); metrics_hash_of(&entries) } /// Hashes `(name, metrics)` pairs already in canonical name order — the body of /// [`metrics_hash_for`], factored out so a test can hash metrics that are not in /// the bundled table (the only way to prove a *field* participates in the hash is /// to vary it while holding every other field fixed). fn metrics_hash_of(entries: &[(&str, &GlyphMetric)]) -> [u8; 32] { let mut p = Preimage::new(DomainTag::FONT_METRICS); p.push_u64_le(entries.len() as u64); for (name, m) in entries { p.push_u64_le(name.len() as u64); p.push_bytes(name.as_bytes()); p.push_u64_le(m.advance as u64); for coord in m.bbox { p.push_u64_le(coord as u64); } // Anchors are a *map* keyed by name (Chapter 7 §"Glyph Catalog // Interface": `anchors: HashMap`), so hash them in // canonical name order (Appendix D §"Ordered Iteration over Sets and // Maps"). A duplicate anchor name is invalid map data and is **rejected** // (a panic), not silently order-collapsed — so the hash never depends on // anchor slice order. let mut anchors: BTreeMap<&str, (i32, i32)> = BTreeMap::new(); for a in m.anchors.iter() { assert!( anchors.insert(a.name.as_ref(), (a.x, a.y)).is_none(), "glyph {} has a duplicate anchor name {}", name, a.name ); } p.push_u64_le(anchors.len() as u64); for (anchor_name, (x, y)) in anchors { p.push_u64_le(anchor_name.len() as u64); p.push_bytes(anchor_name.as_bytes()); p.push_u64_le(x as u64); p.push_u64_le(y as u64); } } *p.finish().as_bytes() } /// The bundled Bravura catalog identity (the [`GlyphCatalogIdentity::default`]). pub fn bravura_catalog_identity() -> GlyphCatalogIdentity { GlyphCatalogIdentity::default() } #[cfg(test)] mod tests { use super::*; #[test] fn metrics_hash_is_domain_tagged_and_nonzero() { let h = bravura_catalog_identity().metrics_hash; assert_ne!(h, [0u8; 32]); assert_eq!(GlyphCatalogIdentity::default().metrics_hash, h); assert_eq!( GlyphCatalogIdentity::default().font_version, Some(BRAVURA_VERSION) ); } /// P13-S12 / Push 4b tranche 3b-ii: the live bug this tranche fixes. /// Layout-ir used to define its own `SmuflVersion { major: u16, minor: /// u16 }` storing the minor digit *literally*, so derived `Ord` sorted /// 1.3 and 1.4 **before** 1.12 — backwards versus SMuFL's real release /// order (1.12 → 1.18 → 1.20 → 1.3 → 1.4). Exercised through /// [`GlyphCatalogIdentity::smufl_version`], not the bare type, because /// that field is the layout-conformance identity the bug actually lived /// on. /// /// Non-vacuous: reverting `crate::SmuflVersion` to the deleted literal-minor /// shape (`{ major: u16, minor: u16 }` with derived `Ord`) and rebuilding /// each identity as `{ major: 1, minor: }` makes this /// test fail — `{1,3}` and `{1,4}` sort *before* `{1,12}`, breaking the /// third assertion in the loop below. Verified by hand against the /// pre-unification type before it was deleted. #[test] fn catalog_identity_smufl_version_orders_the_real_release_sequence() { let identity_with = |minor_digits: &str| GlyphCatalogIdentity { smufl_version: SmuflVersion::from_decimal(1, minor_digits) .expect("valid SMuFL version"), ..GlyphCatalogIdentity::default() }; let sequence = ["12", "18", "20", "3", "4"].map(identity_with); for pair in sequence.windows(2) { assert!( pair[0].smufl_version < pair[1].smufl_version, "{:?} did not sort before {:?} in SMuFL's real release order", pair[0].smufl_version, pair[1].smufl_version ); } } #[test] fn metrics_hash_is_order_and_duplicate_independent() { let a = metrics_hash_for(["gClef", "noteheadBlack", "accidentalSharp"]); let b = metrics_hash_for(["accidentalSharp", "gClef", "noteheadBlack", "gClef"]); assert_eq!(a, b); } /// A glyph's anchors are part of its catalog identity: two catalogs whose /// glyphs differ only in an anchor are different inputs, and a byte-equal /// conformance claim across them is not expected. /// /// This must be shown by varying the anchors while holding every other field /// fixed. The previous version of this test compared `noteheadBlack` (which /// carried anchors) with `noteheadWhole` (which did not) and asserted they /// hash differently — but their `advance` and `bbox` differ too, so it would /// have passed with the anchors ignored entirely. It proved nothing. #[test] fn anchors_participate_in_the_hash() { let bare = GlyphMetric::new("g", 1024, [0, 0, 1024, 1024]); let anchored = GlyphMetric { anchors: Cow::Owned(vec![GlyphAnchor { name: Cow::Owned("stemUpSE".to_owned()), x: 1208, y: 172, }]), ..bare.clone() }; let moved = GlyphMetric { anchors: Cow::Owned(vec![GlyphAnchor { name: Cow::Owned("stemUpSE".to_owned()), x: 1209, y: 172, }]), ..bare.clone() }; let renamed = GlyphMetric { anchors: Cow::Owned(vec![GlyphAnchor { name: Cow::Owned("stemDownNW".to_owned()), x: 1208, y: 172, }]), ..bare.clone() }; let hash = |m: &GlyphMetric| metrics_hash_of(&[("g", m)]); assert_ne!(hash(&bare), hash(&anchored), "presence changes the hash"); assert_ne!(hash(&anchored), hash(&moved), "a coordinate does too"); assert_ne!(hash(&anchored), hash(&renamed), "and so does a name"); } /// The bundled table carries no anchors. They were hand-written where every /// other number in it is machine-extracted from the SHA-pinned font, and they /// were wrong under any reading: they named `stemUpNW`/`stemDownSE` — corners /// a normal notehead's stems do not attach to, and a pair Bravura's /// `noteheadBlack` does not define — with an x of 1180, which reads as 1.18 /// staff spaces written in thousandths rather than the table's 1/1024 units /// (1.18 sp = 1208). Nothing consumed them. `extract_bravura_outlines.py` /// now emits anchors from `bravura_metadata.json`; the table regains them at /// the next regeneration, generated rather than remembered (P13-I3). #[test] fn the_bundled_table_carries_no_hand_written_anchors() { for metric in BRAVURA_METRICS { assert!( metric.anchors.is_empty(), "{} carries hand-written anchors", metric.name ); } } #[test] fn catalog_trait_resolves_and_identifies() { let cat = BravuraCatalog; assert_eq!(cat.metrics("gClef"), metrics("gClef")); assert!(cat.metrics("noSuchGlyph").is_none()); assert_eq!( cat.identity(&["gClef"]).metrics_hash, metrics_hash_for(["gClef"]) ); } #[test] fn runtime_catalog_can_use_owned_names_and_report_identity_through_dyn_trait() { struct RuntimeCatalog { metric: GlyphMetric, } impl GlyphCatalog for RuntimeCatalog { fn metrics(&self, name: &str) -> Option<&GlyphMetric> { (self.metric.name.as_ref() == name).then_some(&self.metric) } fn render_data(&self, _name: &str) -> Option { None } fn smufl_version(&self) -> SmuflVersion { SmuflVersion::from_decimal(1, "4").expect("1.4 is a valid SMuFL version") } fn identity(&self, _consulted: &[&str]) -> GlyphCatalogIdentity { GlyphCatalogIdentity { smufl_version: self.smufl_version(), font_id: FontId::owned("Runtime Font"), font_version: None, metrics_hash: [7; 32], } } } let catalog: Box = Box::new(RuntimeCatalog { metric: GlyphMetric { name: Cow::Owned("runtimeGlyph".to_owned()), advance: 1024, bbox: [0, 0, 1024, 1024], anchors: Cow::Owned(vec![GlyphAnchor { name: Cow::Owned("runtimeAnchor".to_owned()), x: 0, y: 0, }]), }, }); assert!(catalog.metrics("runtimeGlyph").is_some()); assert_eq!(catalog.identity(&["runtimeGlyph"]).metrics_hash, [7; 32]); } #[test] fn barline_classification_covers_the_repeat_signs_but_not_the_dots() { for name in [ "barlineSingle", "barlineFinal", "repeatLeft", "repeatRight", "repeatRightLeft", ] { assert!( is_barline_glyph(name), "{name} is a measure-boundary barline" ); } for name in ["repeatDots", "noteheadBlack", "timeSig2", "gClef"] { assert!(!is_barline_glyph(name), "{name} is not a barline"); } } #[test] fn every_bundled_name_is_unique_and_resolvable() { let mut seen = BTreeSet::new(); for m in BRAVURA_METRICS { assert!( seen.insert(m.name.as_ref()), "duplicate bundled glyph {}", m.name ); assert_eq!(metrics(m.name.as_ref()), Some(m)); } assert!(all_available( BRAVURA_METRICS.iter().map(|m| m.name.as_ref()) )); assert!(!all_available(["noSuchGlyph"])); } }