Phase 2 (Agent I): visible-slice scaffold — Bravura SVG renderer vs stub
Lands the renderer-against-stub slice of Agent I's visible engraving work (spec/PHASE2_QUICKSTART.md). Two new crates; prerequisites (G Pass 11, H spelling/decomposition) are in place. Real engraving + Minimal-tier solver follow next phase. epiphany-render-svg (the deliverable this phase): - Renders a ResolvedLayoutIR to well-formed SVG 1.1, drawing each glyph as a GENUINE Bravura SMuFL outline <path>. Outlines are extracted reproducibly from the official OFL Bravura.otf by a committed generator (tools/extract_bravura_outlines.py, OFL.txt); the font is not vendored, only the generated Rust (src/outlines_generated.rs). Staff-space/y-up coords with one global y-flip wrapper; viewBox in staff spaces, px scale on the root. - Non-overreach: every element traces to a ResolvedGlyph (data-prov) or a declared wrapper; a glyph lacking an outline is surfaced as a diagnostic and drawn as a fallback rect, never silently dropped. - Hand-rolled xml::check_well_formed (no XML dep); acceptance tests cross-check with system xmllint when present. - examples/render_fixture.rs demo (fixture name -> SVG stdout, --solver=stub|real). - Golden-locked machine acceptance snapshot + full-SVG golden for ten_measure_single_staff and valid_score_rich; deterministic output. epiphany-engrave (honest scaffold): - Engraver: a deterministic horizontal-spacing pass (first axis of the planned two-pass spring layout). Reports SolverTier::Stub — NOT Minimal — until it evaluates the declared hard constraints, guarded by a regression test. The demo's --solver=real exercises it end to end. Honesty notes (recorded as Pass-12 candidates P12-I1..I3 in spec/PASS12_BATCH.md and the crates' DECISIONS.md): the v0 to_logical/to_constrained pipeline is a structural placeholder (arbitrary glyph per object, y=0), so stub output is not yet recognizable notation and the QUICKSTART human visual gate is a next-phase gate; MUSCLOID layout-id derivation stays unwired; bundled BRAVURA_METRICS are approximations that disagree with the real outlines. Gates: cargo fmt + clippy -D warnings clean; cargo test --workspace 504 passed, 0 failed, 0 ignored (criterion 6 layout round-trip still green). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
732660988d
commit
ac7c076e73
|
|
@ -93,6 +93,14 @@ dependencies = [
|
|||
"blake3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "epiphany-engrave"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"epiphany-core",
|
||||
"epiphany-layout-ir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "epiphany-layout-ir"
|
||||
version = "0.0.0"
|
||||
|
|
@ -111,6 +119,16 @@ dependencies = [
|
|||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "epiphany-render-svg"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"epiphany-core",
|
||||
"epiphany-engrave",
|
||||
"epiphany-layout-ir",
|
||||
"epiphany-testkit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "epiphany-testkit"
|
||||
version = "0.0.0"
|
||||
|
|
|
|||
14
Cargo.toml
14
Cargo.toml
|
|
@ -6,6 +6,8 @@ members = [
|
|||
"crates/epiphany-bundle",
|
||||
"crates/epiphany-ops",
|
||||
"crates/epiphany-layout-ir",
|
||||
"crates/epiphany-engrave",
|
||||
"crates/epiphany-render-svg",
|
||||
"crates/epiphany-testkit",
|
||||
]
|
||||
|
||||
|
|
@ -45,6 +47,18 @@ epiphany-ops = { path = "crates/epiphany-ops" }
|
|||
# (v0 acceptance criterion 6), so its path is declared here alongside the other
|
||||
# intra-workspace crates.
|
||||
epiphany-layout-ir = { path = "crates/epiphany-layout-ir" }
|
||||
# Agent I's Phase-2 visible-slice crates: epiphany-engrave is the real
|
||||
# constraint solver (Chapter 9 Minimal tier — in progress) consuming the
|
||||
# ConstrainedLayoutIR; epiphany-render-svg is the SVG renderer behind the
|
||||
# Chapter 7 RenderIR interface. The renderer is developed against the stub
|
||||
# solver first (QUICKSTART, Agent I), so it depends on the engrave crate only
|
||||
# for the demo binary's `--solver=real` path.
|
||||
epiphany-engrave = { path = "crates/epiphany-engrave" }
|
||||
epiphany-render-svg = { path = "crates/epiphany-render-svg" }
|
||||
# Agent F's testkit. Declared here so Agent I's render crate can use its score
|
||||
# fixtures (`ten_measure_single_staff`) as a dev-dependency for the demo binary
|
||||
# and acceptance tests.
|
||||
epiphany-testkit = { path = "crates/epiphany-testkit" }
|
||||
# Event-arena storage backend (QUICKSTART decision 2): slotmap gives
|
||||
# generation-checked stale-handle detection, matching the spec's
|
||||
# identifier-stability requirement (Chapter 5).
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "epiphany-engrave"
|
||||
version = "0.0.0"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
description = "Agent I's Epiphany engraving solver (spec Chapter 9): turns a ConstrainedLayoutIR into a ResolvedLayoutIR with real geometry. Phase-2 scaffold — a deterministic horizontal-spacing pass (the first axis of the planned two-pass spring layout) that honestly reports SolverTier::Stub until it evaluates the declared hard constraints and earns the Minimal tier."
|
||||
|
||||
[dependencies]
|
||||
# The solver consumes/produces the Chapter 7 IR stages and implements the
|
||||
# Chapter 9 `ConstraintSolver` interface — all defined in epiphany-layout-ir,
|
||||
# which re-exports the epiphany-core ids/types the solver references.
|
||||
epiphany-layout-ir.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
# Tests drive the solver from real score fixtures via epiphany-core's generators
|
||||
# (and a couple of leaf id/time types).
|
||||
epiphany-core.workspace = true
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
# epiphany-engrave — decisions and Pass 12 candidates
|
||||
|
||||
This file records (a) the implementation decisions the Phase-2 QUICKSTART asked
|
||||
Agent I to make once and document, and (b) ambiguities discovered while building
|
||||
the crate, batched as **Pass 12 candidates** (`spec/PASS12_BATCH.md`) rather than
|
||||
improvised in code.
|
||||
|
||||
## Scope and phase status
|
||||
|
||||
`epiphany-engrave` is the production-side **constraint solver** (Chapter 9): it
|
||||
turns a `ConstrainedLayoutIR` into a `ResolvedLayoutIR` with real geometry. It is
|
||||
a separate crate from `epiphany-layout-ir` deliberately — `layout-ir` is the
|
||||
*interface* layer (the graph↔renderer contract); the actual constraint-solving is
|
||||
on the product side of the spec's core/product boundary, so replacing the
|
||||
`StubSolver` *inside* `layout-ir` would blur it (`spec/PHASE2_QUICKSTART.md`,
|
||||
crate topology).
|
||||
|
||||
**This phase ships the renderer-against-stub slice** (QUICKSTART, Agent I,
|
||||
"Development pattern": build the renderer against the stub solver first, then
|
||||
grow the real solver). So this crate is an **honest scaffold**: `Engraver` runs a
|
||||
genuine deterministic *horizontal spacing pass* — the first axis of the planned
|
||||
two-pass spring layout — placing each spring slot left-to-right by its preferred
|
||||
width, rather than echoing the stub's input columns. It does **not yet** run the
|
||||
vertical pass, the soft-spring stretch/compress solve, or evaluate the IR's
|
||||
declared hard constraints.
|
||||
|
||||
### Honest tier
|
||||
|
||||
By the same rule `layout-ir`'s `StubSolver` follows, a solver that does not
|
||||
evaluate the declared hard constraints and computes no quality metrics MUST
|
||||
report `SolverTier::Stub`, never `Minimal` (Chapter 9 §"Conformance Tiers").
|
||||
`Engraver::tier()` therefore reports `Stub` today; it is promoted to `Minimal` in
|
||||
the same change that lands real hard-constraint satisfaction. A regression test
|
||||
(`reports_the_honest_stub_tier_until_it_earns_minimal`) guards this so the tier
|
||||
cannot be silently inflated. The quality-metric vector stays the conservative
|
||||
all-worst placeholder (`QualityMetricVector::unmeasured`) until the Quality Metric
|
||||
Catalog lands (Phase 3, explicitly out of Agent I's scope).
|
||||
|
||||
## Implementation decisions (QUICKSTART "Decisions you'll need to make")
|
||||
|
||||
1. **Spelling algorithm** — N/A (Agent H, `epiphany-core`).
|
||||
2. **Solver architecture for engraving — two-pass spring layout (horizontal then
|
||||
vertical), constraint graph derived from the existing `ConstrainedLayoutIR`.**
|
||||
This is the QUICKSTART's recommendation: it matches the IR's spring-slot /
|
||||
vertical-band shape, and the spec's deterministic-output requirement makes a
|
||||
global optimization solver expensive to validate (hard to make bit-reproducible)
|
||||
and a rule-based fallback brittle. The horizontal pass implemented here
|
||||
(`spacing::slot_positions`) is that architecture's first axis. Global
|
||||
optimization and rule-based fallback are **rejected**.
|
||||
3. **Renderer SVG dialect** — N/A here (see `epiphany-render-svg/DECISIONS.md`).
|
||||
4. **Catalog versioning / 5. Binary Format versioning** — N/A (Agents K, J).
|
||||
|
||||
### Local decisions
|
||||
|
||||
- **`#![forbid(unsafe_code)]`; sync only; MSRV = workspace 1.77.** Same as every
|
||||
implementation crate.
|
||||
- **Solver version `1` (`ENGRAVER_VERSION`), distinct from the stub's `0`.**
|
||||
Chapter 9: within a fixed version, identical input produces identical output.
|
||||
The horizontal pass is a pure function of the slot sequence and preferred
|
||||
widths, so this holds; a determinism test asserts byte-identical
|
||||
`canonical_bytes()` across solves.
|
||||
- **Well-formedness gate mirrors the stub.** An invalid structure, an unknown
|
||||
glyph, a forged catalog identity, or an explicit hard constraint this scaffold
|
||||
cannot yet evaluate yields `SolveStatus::InternalError` (diagnostic-only),
|
||||
never a panic and never a false `Solved`. When constraints are present the
|
||||
scaffold additionally attaches a `SolverWarning` naming the limitation, rather
|
||||
than silently ignoring them.
|
||||
- **Horizontal spacing preserves provenance, glyph identity, bounds, style, and
|
||||
layer; it changes only `position`.** It assigns each glyph the `x` of its
|
||||
spring slot and keeps its baseline `y` (the vertical pass is future work).
|
||||
|
||||
## Pass 12 candidates
|
||||
|
||||
See `spec/PASS12_BATCH.md` (rows P12-I1, P12-I2, P12-I3). In brief:
|
||||
|
||||
- **P12-I1** — the v0 `to_logical`/`to_constrained` pipeline is a *structural
|
||||
placeholder* (each layout object → one arbitrary glyph by `discriminant % N`,
|
||||
laid out at `y = 0`), not real notation. Chapter 7 says the *logical* stage has
|
||||
"engraving decisions made"; the spec should clarify which engraving decisions
|
||||
(glyph-by-duration selection, pitch→staff-position, clef/key/meter/barline
|
||||
realization, stems/beams) are core-IR construction versus solver work, so the
|
||||
real-notation engraving has a defined home before it is built next phase.
|
||||
- **P12-I3** — `layout-ir`'s bundled `BRAVURA_METRICS` are *approximations* and
|
||||
disagree with the genuine Bravura outlines the renderer now bundles (e.g.
|
||||
`timeSig4` vertical registration). Real spacing needs exact metrics; the metrics
|
||||
table should be regenerated from the font or reconciled with the outline source.
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
# epiphany-engrave
|
||||
|
||||
Agent I's **engraving constraint solver** (spec Chapter 9): turns a
|
||||
`ConstrainedLayoutIR` into a `ResolvedLayoutIR` with real geometry. It is the
|
||||
production-side replacement for `epiphany-layout-ir`'s interface-only
|
||||
`StubSolver`; the two live in separate crates so the spec's core/product boundary
|
||||
stays sharp.
|
||||
|
||||
## Status: honest scaffold (renderer-against-stub phase)
|
||||
|
||||
Per the QUICKSTART development pattern, Agent I builds the SVG renderer
|
||||
([`epiphany-render-svg`](../epiphany-render-svg)) against the **stub solver**
|
||||
first, then grows this crate into the real two-pass spring solver. This commit is
|
||||
the first increment:
|
||||
|
||||
- `Engraver` runs a deterministic **horizontal spacing pass** (the first axis of
|
||||
the planned two-pass spring layout): each spring slot is placed left-to-right by
|
||||
its preferred width instead of being echoed verbatim.
|
||||
- It honestly reports `SolverTier::Stub` — it does not yet evaluate the IR's
|
||||
declared hard constraints or compute quality metrics, so it has not earned
|
||||
`Minimal`. It is promoted to `Minimal` in the change that lands real constraint
|
||||
satisfaction.
|
||||
|
||||
The vertical spring pass, soft-constraint solve, hard-constraint evaluation, and
|
||||
the quality-metric vector are the next-phase / Phase-3 work. See `DECISIONS.md`.
|
||||
|
||||
```rust
|
||||
use epiphany_engrave::Engraver;
|
||||
use epiphany_layout_ir::{ConstraintSolver, SolverConfig};
|
||||
|
||||
let report = Engraver.solve(&constrained_ir, &SolverConfig::default());
|
||||
assert!(report.satisfied_hard_constraints); // for constraint-free stub-pipeline input
|
||||
let resolved = report.layout; // hand to epiphany-render-svg
|
||||
```
|
||||
|
|
@ -0,0 +1,309 @@
|
|||
#![forbid(unsafe_code)]
|
||||
//! # epiphany-engrave
|
||||
//!
|
||||
//! Agent I's **engraving constraint solver** (spec **Chapter 9**, "Constraint-
|
||||
//! Solver Interface"): it turns a [`ConstrainedLayoutIR`] into a
|
||||
//! [`ResolvedLayoutIR`] with real geometry. It is the production-side replacement
|
||||
//! for `epiphany-layout-ir`'s interface-only [`StubSolver`] — the QUICKSTART puts
|
||||
//! the *interface* (`layout-ir`) and the *algorithm* (`engrave`) in separate
|
||||
//! crates so the core/product boundary stays sharp (`spec/PHASE2_QUICKSTART.md`,
|
||||
//! crate topology).
|
||||
//!
|
||||
//! ## Phase status — honest scaffold, not yet `Minimal`
|
||||
//!
|
||||
//! Per the QUICKSTART's recommended development pattern, Agent I develops the
|
||||
//! renderer ([`epiphany-render-svg`]) against the **stub solver** first, then
|
||||
//! grows this crate into the real two-pass spring solver. This is the first
|
||||
//! increment: [`Engraver`] runs a genuine deterministic **horizontal spacing
|
||||
//! pass** (see [`spacing`]) — the first axis of the planned two-pass spring
|
||||
//! layout — placing each spring slot left-to-right by its preferred width
|
||||
//! instead of returning the input columns verbatim.
|
||||
//!
|
||||
//! It does **not yet** run the vertical spring pass, the soft-constraint
|
||||
//! stretch/compress solve, or evaluate the IR's declared hard constraints. By the
|
||||
//! same honesty rule `layout-ir`'s [`StubSolver`] follows, a solver that does not
|
||||
//! evaluate the declared constraints and computes no quality metrics MUST report
|
||||
//! [`SolverTier::Stub`], never [`SolverTier::Minimal`] (Chapter 9 §"Conformance
|
||||
//! Tiers"). So [`Engraver::tier`] reports `Stub` today; it is promoted to
|
||||
//! `Minimal` in the same change that lands real constraint satisfaction. The
|
||||
//! quality-metric vector is the conservative all-worst placeholder
|
||||
//! ([`QualityMetricVector::unmeasured`]) until the Quality Metric Catalog lands
|
||||
//! (Phase 3).
|
||||
//!
|
||||
//! ## Architecture decision (see `DECISIONS.md`)
|
||||
//!
|
||||
//! The solver is a **two-pass spring layout** (horizontal then vertical), with
|
||||
//! the constraint graph derived from the existing [`ConstrainedLayoutIR`]
|
||||
//! (QUICKSTART decision 2). A global optimization solver is rejected: the spec's
|
||||
//! deterministic-output requirement makes it expensive to validate.
|
||||
//!
|
||||
//! [`epiphany-render-svg`]: ../epiphany_render_svg/index.html
|
||||
|
||||
mod spacing;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use epiphany_layout_ir::{
|
||||
all_available, BravuraCatalog, ConstrainedLayoutIR, ConstraintSolver, GlyphCatalog,
|
||||
InvalidationSet, Margins, QualityMetricVector, Rect, ResolvedGlyph, ResolvedLayoutIR,
|
||||
ResolvedPage, ResolvedSystem, Size2D, SolveReport, SolveStatus, SolverBudgetUsed, SolverConfig,
|
||||
SolverState, SolverTier, SolverVersion, SolverWarning, SolverWarningKind,
|
||||
};
|
||||
|
||||
/// The Epiphany engraving solver (Chapter 9). See the crate docs for the phase
|
||||
/// status: this is the horizontal-spacing scaffold of the planned two-pass
|
||||
/// spring layout, reporting [`SolverTier::Stub`] until it earns `Minimal`.
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
pub struct Engraver;
|
||||
|
||||
/// The implementation version of this solver (Chapter 9: within a fixed version,
|
||||
/// identical input produces identical output). Distinct from the stub's `0`.
|
||||
pub const ENGRAVER_VERSION: SolverVersion = SolverVersion(1);
|
||||
|
||||
impl Engraver {
|
||||
/// Resolves geometry: a deterministic horizontal spacing pass over the spring
|
||||
/// slots, copying each glyph to its slot's `x` with its baseline `y`
|
||||
/// preserved. Well-formedness is gated exactly as the stub's is — an unknown
|
||||
/// glyph, a forged catalog identity, malformed structure, or an explicit
|
||||
/// constraint this scaffold cannot yet evaluate yields
|
||||
/// [`SolveStatus::InternalError`] (a diagnostic-only layout), never a panic.
|
||||
fn resolve(&self, input: &ConstrainedLayoutIR) -> SolveReport {
|
||||
let structural_valid = input.validate().is_ok();
|
||||
|
||||
// Short-circuit before catalog construction so an unknown glyph yields a
|
||||
// diagnostic, not a panic in the metrics hash (mirrors the stub).
|
||||
let names: Vec<&str> = input.glyphs.iter().map(|g| g.glyph.as_str()).collect();
|
||||
let metrics_available = all_available(names.iter().copied());
|
||||
let catalog_valid = metrics_available && input.catalog == BravuraCatalog.identity(&names);
|
||||
|
||||
// This scaffold does not yet evaluate explicit hard constraints, so — like
|
||||
// the stub — it must not claim them satisfied. An input carrying explicit
|
||||
// constraints is reported as not-yet-solvable rather than falsely Solved.
|
||||
let well_formed = structural_valid && catalog_valid && input.constraints.is_empty();
|
||||
|
||||
let glyphs: Vec<ResolvedGlyph> = if structural_valid {
|
||||
let positions = spacing::slot_positions(input);
|
||||
place_glyphs(input, &positions)
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let resolved_glyphs = glyphs.len();
|
||||
|
||||
let mut warnings = Vec::new();
|
||||
if structural_valid && catalog_valid && !input.constraints.is_empty() {
|
||||
warnings.push(SolverWarning {
|
||||
kind: SolverWarningKind::UnusualLayoutDecision(
|
||||
"explicit hard-constraint evaluation is not implemented in the \
|
||||
horizontal-spacing scaffold; it lands with the Minimal tier"
|
||||
.to_owned(),
|
||||
),
|
||||
affected_objects: Vec::new(),
|
||||
message: "engrave scaffold cannot yet evaluate declared constraints".to_owned(),
|
||||
});
|
||||
}
|
||||
|
||||
let pages = input
|
||||
.regions
|
||||
.first()
|
||||
.map(|first| ResolvedPage {
|
||||
provenance: first.provenance.clone(),
|
||||
number: 1,
|
||||
size: Size2D::default(),
|
||||
margins: Margins::default(),
|
||||
systems: input
|
||||
.regions
|
||||
.iter()
|
||||
.map(|region| ResolvedSystem {
|
||||
provenance: region.provenance.clone(),
|
||||
bounding_box: Rect::default(),
|
||||
staves: Vec::new(),
|
||||
measures: Vec::new(),
|
||||
})
|
||||
.collect(),
|
||||
free_objects: Vec::new(),
|
||||
})
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
SolveReport {
|
||||
status: if well_formed {
|
||||
SolveStatus::Solved
|
||||
} else {
|
||||
SolveStatus::InternalError
|
||||
},
|
||||
satisfied_hard_constraints: well_formed,
|
||||
layout: ResolvedLayoutIR {
|
||||
source: input.source,
|
||||
pages,
|
||||
glyphs,
|
||||
engraving_decisions: input.engraving_decisions.clone(),
|
||||
catalog: input.catalog.clone(),
|
||||
},
|
||||
unsatisfied_constraints: Vec::new(),
|
||||
warnings,
|
||||
// No normalized quality metrics computed yet (Stub tier, Phase 3 work).
|
||||
metric_vector: QualityMetricVector::unmeasured(),
|
||||
budget_used: SolverBudgetUsed {
|
||||
// The horizontal pass touches each slot once; report that honestly.
|
||||
iterations: input.horizontal_slots.len() as u64,
|
||||
nodes: resolved_glyphs as u64,
|
||||
constraint_evaluations: 0,
|
||||
wall_time_ms: 0,
|
||||
},
|
||||
state: SolverState {
|
||||
solver_version: Some(self.version()),
|
||||
resolved_glyphs,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Copies each glyph to the `x` of its horizontal slot (its baseline `y`
|
||||
/// preserved), preserving provenance, glyph identity, bounds, style, and layer.
|
||||
/// A glyph whose slot has no computed position keeps its baseline `x`.
|
||||
fn place_glyphs(
|
||||
input: &ConstrainedLayoutIR,
|
||||
positions: &BTreeMap<epiphany_layout_ir::SpringSlotId, f32>,
|
||||
) -> Vec<ResolvedGlyph> {
|
||||
input
|
||||
.glyphs
|
||||
.iter()
|
||||
.map(|g| {
|
||||
let x = positions
|
||||
.get(&g.horizontal_slot)
|
||||
.copied()
|
||||
.unwrap_or(g.baseline.x.0);
|
||||
ResolvedGlyph {
|
||||
provenance: g.provenance.clone(),
|
||||
glyph: g.glyph.clone(),
|
||||
position: epiphany_layout_ir::Point::new(x, g.baseline.y.0),
|
||||
transform: None,
|
||||
bounding_box: g.bounding_box,
|
||||
style: g.style,
|
||||
layer: g.layer,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl ConstraintSolver for Engraver {
|
||||
fn tier(&self) -> SolverTier {
|
||||
// Honest: a solver that does not evaluate the declared hard constraints
|
||||
// and computes no quality metrics is below Minimal (Chapter 9). Promoted
|
||||
// to `Minimal` in the change that lands real constraint satisfaction.
|
||||
SolverTier::Stub
|
||||
}
|
||||
|
||||
fn version(&self) -> SolverVersion {
|
||||
ENGRAVER_VERSION
|
||||
}
|
||||
|
||||
fn solve(&self, input: &ConstrainedLayoutIR, _config: &SolverConfig) -> SolveReport {
|
||||
self.resolve(input)
|
||||
}
|
||||
|
||||
fn solve_incremental(
|
||||
&self,
|
||||
input: &ConstrainedLayoutIR,
|
||||
_prior: &SolverState,
|
||||
_invalidations: &InvalidationSet,
|
||||
_config: &SolverConfig,
|
||||
) -> SolveReport {
|
||||
// The scaffold recomputes spacing from scratch, which is trivially
|
||||
// observationally equivalent to a scoped incremental solve (Chapter 9
|
||||
// §"Observational Equivalence"). Real incremental scoping is Minimal-tier
|
||||
// work.
|
||||
self.resolve(input)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use epiphany_core::generators::valid_score_rich;
|
||||
use epiphany_layout_ir::{to_constrained, to_logical, StubSolver};
|
||||
|
||||
fn fixture() -> ConstrainedLayoutIR {
|
||||
to_constrained(&to_logical(&valid_score_rich(11)))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reports_the_honest_stub_tier_until_it_earns_minimal() {
|
||||
// The crate exists to *become* the Minimal solver, but until it evaluates
|
||||
// the declared constraints it reports Stub — never a tier it has not
|
||||
// earned. This guards the honesty invariant.
|
||||
assert_eq!(Engraver.tier(), SolverTier::Stub);
|
||||
assert!(Engraver.tier() < SolverTier::Minimal);
|
||||
assert_eq!(Engraver.version(), ENGRAVER_VERSION);
|
||||
assert_ne!(Engraver.version(), StubSolver.version());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn solves_the_stub_pipeline_and_preserves_provenance() {
|
||||
let input = fixture();
|
||||
let report = Engraver.solve(&input, &SolverConfig::default());
|
||||
assert_eq!(report.status, SolveStatus::Solved);
|
||||
assert!(report.satisfied_hard_constraints);
|
||||
assert_eq!(report.layout.glyphs.len(), input.glyphs.len());
|
||||
// Every input glyph's provenance survives, one-for-one.
|
||||
for (resolved, original) in report.layout.glyphs.iter().zip(&input.glyphs) {
|
||||
assert_eq!(resolved.provenance, original.provenance);
|
||||
assert_eq!(resolved.glyph, original.glyph);
|
||||
}
|
||||
// The metric vector is the honest all-worst placeholder (no metrics yet).
|
||||
assert_eq!(report.metric_vector, QualityMetricVector::unmeasured());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn horizontal_spacing_differs_from_the_verbatim_stub() {
|
||||
// The whole point of the scaffold: it re-spaces horizontally rather than
|
||||
// echoing the input columns. With multiple glyphs the positions differ
|
||||
// from the stub's verbatim baselines.
|
||||
let input = fixture();
|
||||
assert!(input.glyphs.len() >= 2);
|
||||
let engraved = Engraver.solve(&input, &SolverConfig::default()).layout;
|
||||
let stub = StubSolver.solve(&input, &SolverConfig::default()).layout;
|
||||
assert_ne!(
|
||||
engraved
|
||||
.glyphs
|
||||
.iter()
|
||||
.map(|g| g.position.x.0)
|
||||
.collect::<Vec<_>>(),
|
||||
stub.glyphs
|
||||
.iter()
|
||||
.map(|g| g.position.x.0)
|
||||
.collect::<Vec<_>>(),
|
||||
"the engrave pass must re-space, not echo the stub's columns"
|
||||
);
|
||||
// ...but it preserves the same glyph set and order.
|
||||
assert_eq!(engraved.glyphs.len(), stub.glyphs.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn solve_is_deterministic_and_quantizable() {
|
||||
let input = fixture();
|
||||
let a = Engraver.solve(&input, &SolverConfig::default()).layout;
|
||||
let b = Engraver.solve(&input, &SolverConfig::default()).layout;
|
||||
// Byte-identical canonical output across solves (Chapter 9 determinism).
|
||||
assert_eq!(a.canonical_bytes(), b.canonical_bytes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn incremental_is_observationally_equivalent_to_full() {
|
||||
let input = fixture();
|
||||
let full = Engraver.solve(&input, &SolverConfig::default());
|
||||
let inc = Engraver.solve_incremental(
|
||||
&input,
|
||||
&full.state,
|
||||
&InvalidationSet {
|
||||
scope: epiphany_layout_ir::InvalidationScope::WholeScore,
|
||||
slots: vec![],
|
||||
bands: vec![],
|
||||
constraints: vec![],
|
||||
glyphs: vec![],
|
||||
},
|
||||
&SolverConfig::default(),
|
||||
);
|
||||
assert_eq!(full.layout, inc.layout);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
//! The horizontal spacing pass — the first axis of the planned two-pass spring
|
||||
//! layout (`epiphany-engrave`'s DECISIONS.md, decision 1).
|
||||
//!
|
||||
//! A `ConstrainedLayoutIR` carries one horizontal spring slot per musical time
|
||||
//! column, each with a `preferred_width` in staff spaces. This pass walks the
|
||||
//! slots in their emitted left-to-right order and assigns each an absolute `x`
|
||||
//! by accumulating preferred widths, producing even, non-overlapping horizontal
|
||||
//! spacing (the stub solver, by contrast, returns the raw input columns
|
||||
//! verbatim). The vertical pass, the soft-spring stretch/compress solve, and
|
||||
//! constraint evaluation are deferred to the `Minimal`-tier work (next phase).
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use epiphany_layout_ir::{ConstrainedLayoutIR, SpringSlotId, StaffSpace};
|
||||
|
||||
/// The absolute `x` (in staff spaces) assigned to each spring slot, accumulated
|
||||
/// left-to-right over the slots' emitted order. Deterministic: a pure function
|
||||
/// of the slot sequence and their preferred widths.
|
||||
pub(crate) fn slot_positions(input: &ConstrainedLayoutIR) -> BTreeMap<SpringSlotId, f32> {
|
||||
let mut positions = BTreeMap::new();
|
||||
let mut cursor = 0.0_f32;
|
||||
for slot in &input.horizontal_slots {
|
||||
positions.insert(slot.id, cursor);
|
||||
// Advance by the slot's preferred width; a non-finite or negative width
|
||||
// would have been rejected by `ConstrainedLayoutIR::validate`, but clamp
|
||||
// defensively so the cursor stays finite and monotonic regardless.
|
||||
let StaffSpace(width) = slot.preferred_width;
|
||||
cursor += if width.is_finite() && width >= 0.0 {
|
||||
width
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
}
|
||||
positions
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use epiphany_core::generators::valid_score_rich;
|
||||
use epiphany_core::WallClockTime;
|
||||
use epiphany_layout_ir::{
|
||||
to_constrained, to_logical, GlyphCatalogIdentity, SpringSlot, TimePoint,
|
||||
};
|
||||
|
||||
fn slot(id: u128, preferred: f32) -> SpringSlot {
|
||||
SpringSlot {
|
||||
id: SpringSlotId(id),
|
||||
time: TimePoint::WallClock(WallClockTime(id as i64)),
|
||||
min_width: StaffSpace(preferred),
|
||||
preferred_width: StaffSpace(preferred),
|
||||
max_width: None,
|
||||
stretch_factor: 1.0,
|
||||
compress_factor: 1.0,
|
||||
members: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn ir_with_slots(slots: Vec<SpringSlot>) -> ConstrainedLayoutIR {
|
||||
ConstrainedLayoutIR {
|
||||
source: Default::default(),
|
||||
regions: vec![],
|
||||
horizontal_slots: slots,
|
||||
glyphs: vec![],
|
||||
vertical_bands: vec![],
|
||||
constraints: vec![],
|
||||
engraving_decisions: vec![],
|
||||
catalog: GlyphCatalogIdentity::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slots_are_placed_left_to_right_by_accumulated_width() {
|
||||
let c = to_constrained(&to_logical(&valid_score_rich(7)));
|
||||
let positions = slot_positions(&c);
|
||||
// Every slot got a position equal to the running cursor, non-decreasing
|
||||
// in emitted order (preferred widths are non-negative).
|
||||
assert_eq!(positions.len(), c.horizontal_slots.len());
|
||||
let mut cursor = 0.0_f32;
|
||||
for slot in &c.horizontal_slots {
|
||||
let x = positions[&slot.id];
|
||||
assert!(x.is_finite());
|
||||
assert!(
|
||||
(x - cursor).abs() < 1e-3,
|
||||
"slot x must equal the running cursor"
|
||||
);
|
||||
cursor += slot.preferred_width.0;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spacing_uses_preferred_widths_not_input_columns() {
|
||||
// Slots that each prefer 2.0 staff spaces lay out at 0, 2, 4 — a pure
|
||||
// function of widths, independent of any input baseline column.
|
||||
let input = ir_with_slots(vec![slot(1, 2.0), slot(2, 2.0), slot(3, 2.0)]);
|
||||
let p = slot_positions(&input);
|
||||
assert_eq!(p[&SpringSlotId(1)], 0.0);
|
||||
assert_eq!(p[&SpringSlotId(2)], 2.0);
|
||||
assert_eq!(p[&SpringSlotId(3)], 4.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spacing_is_deterministic() {
|
||||
let input = ir_with_slots(vec![slot(10, 1.5), slot(20, 1.5)]);
|
||||
assert_eq!(slot_positions(&input), slot_positions(&input));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
[package]
|
||||
name = "epiphany-render-svg"
|
||||
version = "0.0.0"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
description = "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 as a genuine Bravura SMuFL outline <path>. A renderer, not an engraver — it makes SVG-encoding choices only, never engraving-semantic ones."
|
||||
|
||||
[dependencies]
|
||||
# The renderer consumes the Chapter 7 ResolvedLayoutIR / RenderIR and the glyph
|
||||
# catalog, all defined in epiphany-layout-ir.
|
||||
epiphany-layout-ir.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
# The demo binary's `--solver=real` path drives Agent I's engrave solver; only
|
||||
# the example/tests need it, so it is a dev-dependency (the renderer itself is
|
||||
# solver-agnostic — it consumes a ResolvedLayoutIR from any solver).
|
||||
epiphany-engrave.workspace = true
|
||||
# Fixtures and the score-graph types the demo binary and acceptance tests build
|
||||
# their input from.
|
||||
epiphany-testkit.workspace = true
|
||||
epiphany-core.workspace = true
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
# epiphany-render-svg — decisions and Pass 12 candidates
|
||||
|
||||
This file records (a) the Phase-2 QUICKSTART decisions Agent I made for the
|
||||
renderer, and (b) ambiguities batched as **Pass 12 candidates**
|
||||
(`spec/PASS12_BATCH.md`) rather than improvised in code.
|
||||
|
||||
## Scope and phase status
|
||||
|
||||
`epiphany-render-svg` is one renderer behind the Chapter 7 `RenderIR` interface:
|
||||
it turns a `ResolvedLayoutIR` into well-formed **SVG 1.1**, drawing each glyph as
|
||||
a genuine Bravura SMuFL outline `<path>`. Per the QUICKSTART development pattern
|
||||
it is built and **golden-locked against the stub solver's output first**, before
|
||||
the real engraving solver and the score→real-notation engraving pass land. The
|
||||
stub returns the constrained IR's geometry verbatim — a structural projection,
|
||||
not yet real notation (each layout object becomes one arbitrary glyph in a row) —
|
||||
so this phase proves the renderer is *correct and faithful* (genuine outlines,
|
||||
provenance preserved, output XML-valid and deterministic), independent of
|
||||
engraving quality. The renderer already consumes any solver's `ResolvedLayoutIR`,
|
||||
so when the real `epiphany-engrave` solver lands, the visible result improves with
|
||||
no renderer change (the demo binary's `--solver=stub|real` flag exercises both).
|
||||
|
||||
## The non-overreach rule (Chapter 7 / QUICKSTART, Agent I)
|
||||
|
||||
The renderer makes **SVG-encoding choices only** and **no engraving-semantic**
|
||||
choices. Every emitted element traces to a `ResolvedGlyph` (and thus a score-graph
|
||||
source, via `data-prov`/`data-source-kind`) or to a declared renderer wrapper
|
||||
(the `<svg>` root, the metadata comment, the y-flip `<g>`, a per-layer `<g>`). A
|
||||
glyph with no bundled outline is **surfaced as a diagnostic** and drawn as a
|
||||
visible bounding-box fallback `<rect>` — never silently dropped and never
|
||||
invented. The acceptance harness asserts one drawn element per glyph and one
|
||||
provenance trace per drawn element.
|
||||
|
||||
## Implementation decisions (QUICKSTART "Decisions you'll need to make")
|
||||
|
||||
1. **Spelling / 2. Solver architecture** — N/A here (Agent H; `epiphany-engrave`).
|
||||
3. **Renderer SVG dialect — SVG 1.1 + inline presentation attributes.** Maximum
|
||||
portability for what is effectively a viewer/print artifact; no CSS, no SVG 2
|
||||
features. (CSS styling can come later if needed.) Output validates under the
|
||||
system `xmllint` (libxml2) as well as the in-crate well-formedness checker.
|
||||
4. **Catalog / 5. Binary Format versioning** — N/A (Agents K, J).
|
||||
|
||||
### Local decisions
|
||||
|
||||
- **Glyph rendering — inline genuine Bravura outline `<path>`s
|
||||
(`GlyphMode::PathOutline`), the default and only mode this phase.** The
|
||||
QUICKSTART's recommendation: path outlines make the SVG self-contained (it
|
||||
renders in any browser, image tool, or print pipeline with no font installed),
|
||||
at the cost of file size. An embedded-`@font-face` mode is a documented future
|
||||
option, intentionally **not stubbed** so the interface does not lie about a
|
||||
capability that is absent.
|
||||
- **Outline source — the official OFL `Bravura.otf`, extracted reproducibly.**
|
||||
`tools/extract_bravura_outlines.py` fetches the font + SMuFL `glyphnames.json`
|
||||
and emits `src/outlines_generated.rs`. The font is **not vendored**; only the
|
||||
generated Rust is committed (`tools/OFL.txt` carries the SIL Open Font License
|
||||
1.1 under which the outlines are redistributed). Exactly the glyph set the v0
|
||||
pipeline can name (`layout-ir`'s `BRAVURA_METRICS`) is bundled; a test asserts
|
||||
every pipeline glyph has an outline (so the table cannot silently fall behind).
|
||||
- **Coordinate system — staff spaces, y-up, one global flip.** Outlines are
|
||||
extracted in staff-space units (SMuFL em = 4 staff spaces, Bravura
|
||||
unitsPerEm = 1000 ⇒ 1 staff space = 250 font units), **y-up** (musical
|
||||
convention). The whole document is wrapped in one
|
||||
`translate(-min_x, max_y) scale(1, -1)` group, so every glyph is placed with a
|
||||
plain `translate(x, y)` and the `viewBox` is `0 0 W H` in staff spaces;
|
||||
`width`/`height` carry the px display scale. The renderer never bakes the flip
|
||||
into per-glyph data.
|
||||
- **Determinism — fixed 4-decimal number formatting, `-0` normalised to `0`.**
|
||||
Outline `d` data is fixed bundled text; every computed coordinate is formatted
|
||||
identically, so identical input yields byte-identical SVG. The acceptance
|
||||
harness golden-locks the full SVG and a machine snapshot (object/glyph/path/
|
||||
provenance/layer/per-class/hard-constraint counts + XML validity).
|
||||
- **No external XML dependency.** The workspace is deliberately dependency-light;
|
||||
`xml::check_well_formed` is a hand-rolled validator for the subset the renderer
|
||||
emits, and the acceptance test additionally cross-checks with `xmllint` when
|
||||
present so the "XML-validates" claim rests on a real parser too.
|
||||
|
||||
## Pass 12 candidates
|
||||
|
||||
See `spec/PASS12_BATCH.md` (rows P12-I1, P12-I2, P12-I3). Most relevant here:
|
||||
|
||||
- **P12-I1** — the constrained IR is a structural placeholder, so the rendered
|
||||
stub output is *not yet recognizable notation*. The QUICKSTART's human-review
|
||||
visual-acceptance gate ("the SVG visually parses as standard music notation")
|
||||
is therefore a **next-phase** gate, met once real engraving lands; this phase's
|
||||
gate is renderer correctness/faithfulness. Recorded so the visual gate is not
|
||||
mistaken for already-met.
|
||||
- **P12-I2** — stable layout-object id derivation (`MUSCLOID`, Pass-11 item 2.6,
|
||||
deferred to Agent I) is still unwired: the determinism crate exposes no
|
||||
`MUSCLOID` tag and is frozen. The renderer traces provenance by the existing
|
||||
provisional `stable_id`; wiring the ratified derivation is Track A work.
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
# 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 as a **genuine Bravura SMuFL outline** `<path>`. It is the visible end of
|
||||
the v0 `Score → layout IR` pipeline.
|
||||
|
||||
## Status: renderer against the stub solver
|
||||
|
||||
This phase builds and golden-locks the renderer against the **stub solver's**
|
||||
output (the QUICKSTART development pattern), before the real engraving solver and
|
||||
the score→real-notation engraving pass land. The stub returns the IR geometry
|
||||
verbatim — a structural projection (each object becomes one arbitrary glyph in a
|
||||
row), not yet recognizable notation — so what is proven here is **renderer
|
||||
correctness and faithfulness**: real outlines, provenance preserved, output
|
||||
XML-valid and deterministic. The renderer consumes any solver's output, so the
|
||||
picture improves with no renderer change once `epiphany-engrave` lands.
|
||||
|
||||
## Demo
|
||||
|
||||
```sh
|
||||
# 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
|
||||
```
|
||||
|
||||
Fixtures: `ten_measure_single_staff`, `valid_score_rich`, `valid_score`. Stats and
|
||||
diagnostics go to stderr; the SVG goes to stdout.
|
||||
|
||||
## Library
|
||||
|
||||
```rust
|
||||
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) — nothing that changes
|
||||
engraving.
|
||||
|
||||
## Bundled outlines
|
||||
|
||||
The glyph outlines in `src/outlines_generated.rs` are extracted from the official
|
||||
OFL `Bravura.otf` by `tools/extract_bravura_outlines.py`. 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 (`tools/OFL.txt`); the
|
||||
extracted outlines are redistributed under the same license. To regenerate:
|
||||
|
||||
```sh
|
||||
cd crates/epiphany-render-svg/tools
|
||||
python3 -m venv .venv && . .venv/bin/activate && pip install fonttools
|
||||
python3 extract_bravura_outlines.py > ../src/outlines_generated.rs
|
||||
```
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
//! Demo harness (NOT an application): render a fixture score to SVG on stdout.
|
||||
//!
|
||||
//! This is the "show the work" tool from the QUICKSTART (Agent I, "Demo binary
|
||||
//! discipline"): it drives the full v0 pipeline
|
||||
//! `Score → to_logical → to_constrained → solver → render` for a named fixture
|
||||
//! and writes the SVG to stdout (stats + diagnostics to stderr). It is invaluable
|
||||
//! for visual regression review and triage; it is not a viewer or a GUI host.
|
||||
//!
|
||||
//! Usage:
|
||||
//!
|
||||
//! ```text
|
||||
//! cargo run -p epiphany-render-svg --example render_fixture -- \
|
||||
//! ten_measure_single_staff [--solver=stub|real] [--seed=N] [--no-provenance] \
|
||||
//! > out.svg
|
||||
//! ```
|
||||
//!
|
||||
//! The `--solver` flag selects the interface-only stub (`stub`, the default this
|
||||
//! phase) or Agent I's engrave solver (`real`); keeping the renderer working
|
||||
//! against both is how a renderer-vs-solver bug is bisected with a one-flag
|
||||
//! change (QUICKSTART, Agent I, "Development pattern").
|
||||
|
||||
use std::process::ExitCode;
|
||||
|
||||
use epiphany_engrave::Engraver;
|
||||
use epiphany_layout_ir::{to_constrained, to_logical, ConstraintSolver, SolverConfig, StubSolver};
|
||||
use epiphany_render_svg::{render, RenderOptions};
|
||||
|
||||
const FIXTURES: &str = "ten_measure_single_staff, valid_score_rich, valid_score";
|
||||
const SOLVERS: &str = "stub, real";
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let mut fixture: Option<String> = None;
|
||||
let mut solver = String::from("stub");
|
||||
let mut seed: u64 = 0x000A_11CE;
|
||||
let mut emit_provenance = true;
|
||||
|
||||
for arg in std::env::args().skip(1) {
|
||||
if let Some(v) = arg.strip_prefix("--solver=") {
|
||||
solver = v.to_owned();
|
||||
} else if let Some(v) = arg.strip_prefix("--seed=") {
|
||||
match v.parse() {
|
||||
Ok(n) => seed = n,
|
||||
Err(_) => return fail(&format!("invalid --seed value: {v}")),
|
||||
}
|
||||
} else if arg == "--no-provenance" {
|
||||
emit_provenance = false;
|
||||
} else if arg == "--help" || arg == "-h" {
|
||||
usage();
|
||||
return ExitCode::SUCCESS;
|
||||
} else if arg.starts_with("--") {
|
||||
return fail(&format!("unknown flag: {arg}"));
|
||||
} else if fixture.is_none() {
|
||||
fixture = Some(arg);
|
||||
} else {
|
||||
return fail(&format!("unexpected argument: {arg}"));
|
||||
}
|
||||
}
|
||||
|
||||
let Some(fixture) = fixture else {
|
||||
usage();
|
||||
return fail("a fixture name is required");
|
||||
};
|
||||
|
||||
let score = match fixture.as_str() {
|
||||
"ten_measure_single_staff" => epiphany_testkit::fixtures::ten_measure_single_staff(seed),
|
||||
"valid_score_rich" => epiphany_core::generators::valid_score_rich(seed),
|
||||
"valid_score" => epiphany_core::generators::valid_score(seed),
|
||||
other => return fail(&format!("unknown fixture {other:?}; known: {FIXTURES}")),
|
||||
};
|
||||
|
||||
let constrained = to_constrained(&to_logical(&score));
|
||||
let config = SolverConfig::default();
|
||||
let (report, tier) = match solver.as_str() {
|
||||
"stub" => (StubSolver.solve(&constrained, &config), StubSolver.tier()),
|
||||
"real" => (Engraver.solve(&constrained, &config), Engraver.tier()),
|
||||
other => return fail(&format!("unknown solver {other:?}; known: {SOLVERS}")),
|
||||
};
|
||||
|
||||
let out = render(
|
||||
&report.layout,
|
||||
&RenderOptions {
|
||||
emit_provenance,
|
||||
..RenderOptions::default()
|
||||
},
|
||||
);
|
||||
|
||||
eprintln!(
|
||||
"fixture={fixture} seed={seed:#x} solver={solver} tier={tier:?} status={:?}",
|
||||
report.status
|
||||
);
|
||||
eprintln!(
|
||||
"glyphs={} paths={} fallback_rects={} provenance={} layers={} hard_constraints={} well_formed={}",
|
||||
out.stats.glyph_count,
|
||||
out.stats.path_count,
|
||||
out.stats.fallback_rect_count,
|
||||
out.stats.provenance_count,
|
||||
out.stats.layer_count,
|
||||
constrained.constraints.len(),
|
||||
out.is_well_formed(),
|
||||
);
|
||||
for d in &out.diagnostics {
|
||||
eprintln!("diagnostic: {} (glyph: {:?})", d.message, d.glyph);
|
||||
}
|
||||
|
||||
print!("{}", out.svg);
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
|
||||
fn usage() {
|
||||
eprintln!(
|
||||
"usage: render_fixture <fixture> [--solver=stub|real] [--seed=N] [--no-provenance]\n\
|
||||
fixtures: {FIXTURES}\n\
|
||||
solvers: {SOLVERS} (default: stub)"
|
||||
);
|
||||
}
|
||||
|
||||
fn fail(message: &str) -> ExitCode {
|
||||
eprintln!("error: {message}");
|
||||
ExitCode::from(2)
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
#![forbid(unsafe_code)]
|
||||
//! # epiphany-render-svg
|
||||
//!
|
||||
//! Agent I's **SVG renderer** behind the Epiphany `RenderIR` interface (spec
|
||||
//! **Chapter 7** §"RenderIR"): it turns a
|
||||
//! [`ResolvedLayoutIR`](epiphany_layout_ir::ResolvedLayoutIR) into well-formed
|
||||
//! **SVG 1.1**, drawing each glyph as a **genuine Bravura SMuFL outline**
|
||||
//! `<path>`. It is the visible end of the v0 `Score → layout IR` pipeline: from a
|
||||
//! resolved layout, produce an image a musician would recognise.
|
||||
//!
|
||||
//! ## Scope of this phase (renderer-against-stub)
|
||||
//!
|
||||
//! Per the QUICKSTART development pattern (`spec/PHASE2_QUICKSTART.md`, Agent I),
|
||||
//! the renderer is built and golden-locked against the **stub solver's** output
|
||||
//! first, before the real engraving solver lands. The stub returns the
|
||||
//! constrained IR's geometry verbatim — a structural projection, not yet real
|
||||
//! notation — so this phase proves the renderer is *correct and faithful* (every
|
||||
//! glyph drawn from its real Bravura outline, provenance preserved, output
|
||||
//! XML-valid and deterministic), independently of engraving quality. The real
|
||||
//! [`epiphany_engrave`](../epiphany_engrave/index.html) solver and the
|
||||
//! score→real-notation engraving pass are the next phase; the renderer already
|
||||
//! consumes any solver's `ResolvedLayoutIR`.
|
||||
//!
|
||||
//! ## What it draws, and the non-overreach rule
|
||||
//!
|
||||
//! The bundled outlines are extracted from the official OFL `Bravura.otf` (see
|
||||
//! `tools/extract_bravura_outlines.py` and `tools/OFL.txt`) in staff-space,
|
||||
//! y-up coordinates. The renderer makes SVG-encoding choices only and never
|
||||
//! engraving-semantic ones; see [`svg`] for the coordinate system, the
|
||||
//! provenance-tracing contract, and the diagnostic-not-paper-over rule.
|
||||
//!
|
||||
//! ## Font availability
|
||||
//!
|
||||
//! The default and only mode this phase is [`GlyphMode::PathOutline`] — inline
|
||||
//! outlines, so the SVG is self-contained and needs no font installed
|
||||
//! (QUICKSTART, Agent I, recommendation). An embedded-`@font-face` mode is a
|
||||
//! future option; it is intentionally not implemented yet rather than stubbed
|
||||
//! dishonestly.
|
||||
|
||||
mod outline;
|
||||
mod outlines_generated;
|
||||
mod svg;
|
||||
pub mod xml;
|
||||
|
||||
pub use outline::{bundled_glyph_count, smufl_codepoint};
|
||||
pub use svg::{
|
||||
render, Diagnostic, GlyphClass, GlyphMode, RenderOptions, RenderOutput, RenderStats,
|
||||
};
|
||||
pub use xml::{check_well_formed, XmlError};
|
||||
|
||||
// Re-exported so callers can name the renderer's input without also importing
|
||||
// epiphany-layout-ir directly.
|
||||
pub use epiphany_layout_ir::{ResolvedLayoutIR, ScaleContext};
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
//! Lookup over the bundled Bravura outlines ([`crate::outlines_generated`]).
|
||||
|
||||
use crate::outlines_generated::{BravuraOutline, BRAVURA_OUTLINES};
|
||||
|
||||
/// The genuine Bravura outline for a SMuFL glyph name, if bundled. The table is
|
||||
/// sorted by name, so this is a binary search.
|
||||
pub(crate) fn outline(name: &str) -> Option<&'static BravuraOutline> {
|
||||
BRAVURA_OUTLINES
|
||||
.binary_search_by(|o| o.name.cmp(name))
|
||||
.ok()
|
||||
.map(|i| &BRAVURA_OUTLINES[i])
|
||||
}
|
||||
|
||||
/// How many glyph outlines are bundled.
|
||||
pub fn bundled_glyph_count() -> usize {
|
||||
BRAVURA_OUTLINES.len()
|
||||
}
|
||||
|
||||
/// The SMuFL codepoint of a bundled glyph name, if bundled. Useful for a future
|
||||
/// embedded-font rendering mode (which references glyphs by codepoint) and for
|
||||
/// debugging glyph identity.
|
||||
pub fn smufl_codepoint(name: &str) -> Option<u32> {
|
||||
outline(name).map(|o| o.codepoint)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn the_table_is_sorted_and_searchable() {
|
||||
// Binary search depends on the generator emitting names in order.
|
||||
assert!(BRAVURA_OUTLINES.windows(2).all(|w| w[0].name < w[1].name));
|
||||
assert!(outline("noteheadBlack").is_some());
|
||||
assert!(outline("gClef").is_some());
|
||||
assert!(outline("noSuchGlyph").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_pipeline_glyph_has_a_bundled_outline() {
|
||||
// Non-vacuity: every glyph the v0 layout pipeline can name (the
|
||||
// layout-ir BRAVURA_METRICS set) is drawable. If the metrics table grows
|
||||
// a glyph, the generator must be re-run — this test fails until it is.
|
||||
for m in epiphany_layout_ir::BRAVURA_METRICS {
|
||||
assert!(
|
||||
outline(m.name.as_ref()).is_some(),
|
||||
"no bundled outline for pipeline glyph {}",
|
||||
m.name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outlines_have_finite_bounds_and_nonempty_paths() {
|
||||
for o in BRAVURA_OUTLINES {
|
||||
assert!(o.bbox.iter().all(|v| v.is_finite()));
|
||||
assert!(o.bbox[0] <= o.bbox[2] && o.bbox[1] <= o.bbox[3]);
|
||||
assert!(!o.path.is_empty());
|
||||
assert!(o.path.starts_with('M'), "{} path must start with M", o.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
//! GENERATED by `tools/extract_bravura_outlines.py` — do not edit by hand.
|
||||
//!
|
||||
//! Real Bravura SMuFL glyph outlines, extracted from the official OFL
|
||||
//! `Bravura.otf` (unitsPerEm = 1000; 1 staff space = 250 font units, since the
|
||||
//! SMuFL em is 4 staff spaces). Path `d` data is in **staff-space** units, **y-up**
|
||||
//! (musical convention, positive y = higher pitch), relative to each glyph's
|
||||
//! origin, rounded to 4 decimals. The renderer applies a single y-flip wrapper.
|
||||
//!
|
||||
//! Bravura is (c) Steinberg Media Technologies GmbH under the SIL Open Font
|
||||
//! License 1.1; these extracted outlines are redistributed under the same license
|
||||
//! (see `tools/OFL.txt`).
|
||||
|
||||
/// One bundled glyph outline: SMuFL name, codepoint, the SVG path `d` in
|
||||
/// staff-space / y-up coordinates, and the outline's tight bounding box
|
||||
/// `[left, bottom, right, top]` in staff spaces.
|
||||
pub(crate) struct BravuraOutline {
|
||||
pub name: &'static str,
|
||||
pub codepoint: u32,
|
||||
pub path: &'static str,
|
||||
pub bbox: [f32; 4],
|
||||
}
|
||||
|
||||
/// Every glyph the v0 layout pipeline can name (the `BRAVURA_METRICS` set), with
|
||||
/// its genuine Bravura outline. Sorted by name for deterministic binary search.
|
||||
pub(crate) const BRAVURA_OUTLINES: &[BravuraOutline] = &[
|
||||
BravuraOutline {
|
||||
name: "accidentalDoubleSharp",
|
||||
codepoint: 0xE263,
|
||||
path: "M0.76 -0.128C0.716 -0.104 0.572 -0.024 0.572 0.004C0.572 0.06 0.704 0.12 0.76 0.14C0.764 0.14 0.768 0.14 0.772 0.14L0.796 0.136C0.844 0.136 0.92 0.144 0.96 0.176C0.98 0.196 0.988 0.256 0.988 0.32C0.988 0.484 0.976 0.508 0.812 0.508C0.74 0.508 0.668 0.5 0.648 0.48C0.632 0.468 0.628 0.396 0.628 0.324C0.608 0.28 0.544 0.136 0.496 0.136C0.44 0.136 0.376 0.268 0.36 0.324C0.36 0.392 0.348 0.464 0.328 0.48C0.312 0.5 0.248 0.508 0.184 0.508C0.116 0.508 0.044 0.5 0.02 0.48C0.008 0.472 0 0.404 0 0.332C0 0.26 0.008 0.188 0.02 0.176C0.048 0.152 0.12 0.136 0.188 0.136C0.204 0.136 0.216 0.14 0.228 0.14C0.272 0.116 0.416 0.036 0.416 0.008C0.416 -0.044 0.288 -0.108 0.232 -0.128H0.192C0.12 -0.128 0.044 -0.136 0.02 -0.156C0.008 -0.168 0 -0.236 0 -0.308C0 -0.38 0.008 -0.452 0.02 -0.46C0.048 -0.484 0.112 -0.5 0.18 -0.5C0.24 -0.5 0.304 -0.488 0.328 -0.46C0.352 -0.44 0.36 -0.372 0.36 -0.308C0.36 -0.304 0.364 -0.296 0.368 -0.292C0.392 -0.236 0.44 -0.124 0.492 -0.124C0.544 -0.124 0.612 -0.252 0.628 -0.308C0.628 -0.38 0.632 -0.448 0.648 -0.46C0.676 -0.484 0.74 -0.5 0.808 -0.5C0.96 -0.5 0.988 -0.46 0.988 -0.316C0.988 -0.248 0.976 -0.176 0.96 -0.156C0.94 -0.136 0.868 -0.128 0.8 -0.128Z",
|
||||
bbox: [0.0, -0.5, 0.988, 0.508],
|
||||
},
|
||||
BravuraOutline {
|
||||
name: "accidentalFlat",
|
||||
codepoint: 0xE260,
|
||||
path: "M0.048 -0.68C0.06 -0.696 0.072 -0.7 0.084 -0.7C0.096 -0.7 0.108 -0.692 0.108 -0.692C0.228 -0.624 0.324 -0.516 0.424 -0.448C0.78 -0.2 0.904 0.044 0.904 0.228C0.904 0.456 0.728 0.6 0.544 0.612C0.476 0.612 0.38 0.58 0.324 0.544C0.3 0.524 0.256 0.488 0.236 0.488C0.228 0.488 0.224 0.488 0.216 0.492C0.188 0.504 0.172 0.532 0.172 0.56C0.176 0.648 0.2 1.608 0.2 1.688C0.2 1.732 0.164 1.756 0.124 1.756C0.068 1.756 0.004 1.716 0 1.644C0 1.644 0.016 -0.64 0.048 -0.68ZM0.188 -0.324C0.188 -0.324 0.176 -0.084 0.176 0.076C0.176 0.14 0.18 0.188 0.184 0.204C0.212 0.284 0.372 0.4 0.464 0.4C0.58 0.4 0.628 0.268 0.628 0.168C0.628 -0.048 0.444 -0.264 0.272 -0.372C0.256 -0.38 0.244 -0.384 0.232 -0.384C0.196 -0.384 0.188 -0.344 0.188 -0.324Z",
|
||||
bbox: [0.0, -0.7, 0.904, 1.756],
|
||||
},
|
||||
BravuraOutline {
|
||||
name: "accidentalNatural",
|
||||
codepoint: 0xE261,
|
||||
path: "M0.564 0.724C0.556 0.724 0.552 0.72 0.548 0.72C0.548 0.72 0.292 0.628 0.188 0.628C0.164 0.628 0.148 0.632 0.148 0.648V1.316C0.148 1.344 0.124 1.364 0.1 1.364H0.048C0.02 1.364 0 1.344 0 1.316V-0.744C0 -0.768 0.012 -0.78 0.036 -0.78L0.044 -0.776C0.048 -0.776 0.056 -0.776 0.06 -0.772C0.116 -0.748 0.34 -0.652 0.456 -0.652C0.496 -0.652 0.524 -0.664 0.524 -0.696V-1.292C0.524 -1.32 0.544 -1.34 0.572 -1.34H0.624C0.648 -1.34 0.672 -1.32 0.672 -1.292V0.716C0.672 0.736 0.656 0.748 0.64 0.748C0.636 0.748 0.628 0.748 0.624 0.744ZM0.148 0.156C0.148 0.212 0.392 0.316 0.488 0.316C0.512 0.316 0.524 0.312 0.524 0.296V-0.116C0.524 -0.188 0.296 -0.28 0.196 -0.28C0.168 -0.28 0.148 -0.272 0.148 -0.256Z",
|
||||
bbox: [0.0, -1.34, 0.672, 1.364],
|
||||
},
|
||||
BravuraOutline {
|
||||
name: "accidentalSharp",
|
||||
codepoint: 0xE262,
|
||||
path: "M0.948 0.472C0.976 0.484 0.996 0.516 0.996 0.54V0.824C0.996 0.844 0.984 0.856 0.968 0.856C0.96 0.856 0.956 0.856 0.948 0.852C0.948 0.852 0.868 0.82 0.848 0.816C0.82 0.816 0.792 0.836 0.792 0.868V1.356C0.792 1.38 0.768 1.4 0.736 1.4C0.696 1.4 0.672 1.38 0.672 1.356V0.836C0.668 0.796 0.656 0.744 0.62 0.72C0.572 0.692 0.436 0.636 0.368 0.62C0.332 0.62 0.32 0.668 0.32 0.7V1.18C0.32 1.204 0.292 1.224 0.264 1.224C0.224 1.224 0.2 1.204 0.2 1.18V0.64C0.2 0.584 0.176 0.544 0.152 0.532C0.128 0.52 0.048 0.488 0.048 0.488C0.02 0.48 0 0.448 0 0.424V0.14C0 0.116 0.012 0.104 0.036 0.104L0.044 0.108C0.048 0.108 0.108 0.132 0.14 0.148L0.144 0.152C0.176 0.152 0.2 0.112 0.2 0.08V-0.316C0.2 -0.36 0.18 -0.396 0.156 -0.408C0.132 -0.416 0.048 -0.452 0.048 -0.452C0.02 -0.46 0 -0.492 0 -0.516V-0.8C0 -0.824 0.012 -0.836 0.036 -0.836L0.044 -0.832C0.048 -0.832 0.104 -0.808 0.14 -0.796C0.144 -0.792 0.148 -0.792 0.152 -0.792C0.18 -0.792 0.2 -0.836 0.2 -0.856V-1.348C0.2 -1.372 0.224 -1.392 0.252 -1.392C0.292 -1.392 0.32 -1.372 0.32 -1.348V-0.792C0.32 -0.74 0.34 -0.712 0.36 -0.704L0.604 -0.604C0.604 -0.604 0.608 -0.604 0.608 -0.604L0.616 -0.6C0.652 -0.6 0.672 -0.648 0.672 -0.672V-1.172C0.672 -1.196 0.696 -1.216 0.724 -1.216C0.768 -1.216 0.792 -1.196 0.792 -1.172V-0.604C0.792 -0.572 0.808 -0.524 0.836 -0.512C0.864 -0.5 0.948 -0.468 0.948 -0.468C0.976 -0.456 0.996 -0.424 0.996 -0.4V-0.116C0.996 -0.096 0.984 -0.084 0.968 -0.084C0.96 -0.084 0.956 -0.084 0.948 -0.088L0.844 -0.128C0.82 -0.128 0.792 -0.104 0.792 -0.056V0.316C0.792 0.344 0.812 0.42 0.844 0.432ZM0.672 -0.18C0.648 -0.26 0.46 -0.34 0.368 -0.34C0.344 -0.34 0.324 -0.332 0.32 -0.32C0.312 -0.304 0.308 -0.216 0.308 -0.12C0.308 0.004 0.312 0.144 0.32 0.176C0.328 0.244 0.512 0.328 0.612 0.328C0.64 0.328 0.664 0.32 0.672 0.304C0.68 0.284 0.688 0.184 0.688 0.076C0.688 -0.032 0.68 -0.144 0.672 -0.18Z",
|
||||
bbox: [0.0, -1.392, 0.996, 1.4],
|
||||
},
|
||||
BravuraOutline {
|
||||
name: "augmentationDot",
|
||||
codepoint: 0xE1E7,
|
||||
path: "M0.4 0C0.4 0.112 0.312 0.2 0.2 0.2C0.088 0.2 0 0.112 0 0C0 -0.112 0.088 -0.2 0.2 -0.2C0.312 -0.2 0.4 -0.112 0.4 0Z",
|
||||
bbox: [0.0, -0.2, 0.4, 0.2],
|
||||
},
|
||||
BravuraOutline {
|
||||
name: "barlineFinal",
|
||||
codepoint: 0xE032,
|
||||
path: "M0.144 0V4H0V0ZM0.912 4H0.412V0H0.912Z",
|
||||
bbox: [0.0, 0.0, 0.912, 4.0],
|
||||
},
|
||||
BravuraOutline {
|
||||
name: "barlineSingle",
|
||||
codepoint: 0xE030,
|
||||
path: "M0.144 0V4H0V0Z",
|
||||
bbox: [0.0, 0.0, 0.144, 4.0],
|
||||
},
|
||||
BravuraOutline {
|
||||
name: "cClef",
|
||||
codepoint: 0xE05C,
|
||||
path: "M0.92 1.928C0.92 1.984 0.892 2.012 0.836 2.012H0.832C0.776 2.012 0.748 1.984 0.748 1.928V-1.928C0.748 -1.984 0.776 -2.012 0.832 -2.012H0.836C0.892 -2.012 0.92 -1.984 0.92 -1.928V-0.176C0.92 -0.144 0.94 -0.148 0.956 -0.152C1.06 -0.18 1.228 -0.284 1.312 -0.736C1.324 -0.8 1.348 -0.836 1.388 -0.836C1.432 -0.836 1.452 -0.796 1.472 -0.728C1.524 -0.552 1.616 -0.356 1.9 -0.356C2.16 -0.356 2.232 -0.612 2.232 -1.136C2.232 -1.66 2.14 -1.896 1.808 -1.896C1.752 -1.896 1.468 -1.872 1.468 -1.788C1.468 -1.768 1.532 -1.744 1.576 -1.728C1.656 -1.7 1.736 -1.62 1.736 -1.468C1.736 -1.292 1.62 -1.192 1.464 -1.192C1.292 -1.192 1.156 -1.308 1.156 -1.52C1.156 -1.772 1.376 -2.024 1.852 -2.024C2.508 -2.024 2.796 -1.564 2.796 -1.148C2.796 -0.596 2.492 -0.212 1.96 -0.212C1.844 -0.212 1.768 -0.232 1.716 -0.248C1.676 -0.26 1.636 -0.268 1.6 -0.244C1.544 -0.208 1.456 -0.08 1.456 0C1.456 0.08 1.544 0.208 1.6 0.244C1.636 0.268 1.676 0.26 1.716 0.248C1.768 0.232 1.844 0.212 1.96 0.212C2.492 0.212 2.796 0.596 2.796 1.148C2.796 1.564 2.508 2.024 1.852 2.024C1.376 2.024 1.156 1.772 1.156 1.52C1.156 1.308 1.292 1.192 1.464 1.192C1.62 1.192 1.736 1.292 1.736 1.468C1.736 1.62 1.656 1.7 1.576 1.728C1.532 1.744 1.468 1.768 1.468 1.788C1.468 1.872 1.752 1.896 1.808 1.896C2.14 1.896 2.232 1.66 2.232 1.136C2.232 0.612 2.16 0.356 1.9 0.356C1.616 0.356 1.524 0.552 1.472 0.728C1.452 0.796 1.432 0.836 1.388 0.836C1.348 0.836 1.324 0.8 1.312 0.736C1.228 0.284 1.06 0.18 0.956 0.152C0.94 0.148 0.92 0.144 0.92 0.176ZM0.084 2.012C0.028 2.012 0 1.984 0 1.928V-1.928C0 -1.984 0.028 -2.012 0.084 -2.012H0.428C0.484 -2.012 0.512 -1.984 0.512 -1.928V1.928C0.512 1.984 0.484 2.012 0.428 2.012Z",
|
||||
bbox: [0.0, -2.024, 2.796, 2.024],
|
||||
},
|
||||
BravuraOutline {
|
||||
name: "dynamicForte",
|
||||
codepoint: 0xE522,
|
||||
path: "M0.064 1.056C0.02 1.056 0 1.036 0 0.992C0 0.952 0.02 0.932 0.06 0.932H0.292C0.316 0.932 0.324 0.932 0.324 0.916C0.324 0.908 0.32 0.896 0.316 0.876L0.064 0C-0.048 -0.392 -0.12 -0.52 -0.28 -0.52C-0.332 -0.52 -0.352 -0.504 -0.352 -0.484C-0.352 -0.452 -0.316 -0.472 -0.256 -0.432C-0.208 -0.4 -0.176 -0.348 -0.176 -0.284C-0.176 -0.18 -0.248 -0.12 -0.356 -0.12C-0.476 -0.12 -0.564 -0.216 -0.564 -0.34C-0.564 -0.516 -0.432 -0.608 -0.264 -0.608C0.04 -0.608 0.228 -0.404 0.448 0.048C0.564 0.292 0.652 0.54 0.748 0.88L0.752 0.884C0.752 0.896 0.784 0.932 0.804 0.932H1.064C1.108 0.932 1.128 0.952 1.128 0.996C1.128 1.036 1.108 1.056 1.068 1.056H0.828C0.8 1.056 0.788 1.056 0.788 1.076C0.788 1.088 0.792 1.1 0.796 1.124C0.876 1.472 0.964 1.684 1.164 1.684C1.196 1.684 1.228 1.676 1.228 1.652C1.228 1.628 1.208 1.632 1.164 1.612C1.112 1.588 1.08 1.532 1.08 1.46C1.08 1.348 1.16 1.292 1.26 1.292C1.364 1.292 1.456 1.36 1.456 1.508C1.456 1.656 1.364 1.776 1.112 1.776C0.724 1.776 0.508 1.5 0.376 1.116C0.356 1.056 0.352 1.056 0.296 1.056Z",
|
||||
bbox: [-0.564, -0.608, 1.456, 1.776],
|
||||
},
|
||||
BravuraOutline {
|
||||
name: "dynamicPiano",
|
||||
codepoint: 0xE520,
|
||||
path: "M1.096 1.096C0.972 1.096 0.884 1.056 0.812 0.992C0.756 0.944 0.744 0.912 0.728 0.912C0.708 0.912 0.72 0.94 0.684 1.008C0.656 1.056 0.596 1.092 0.492 1.092C0.256 1.092 0.128 0.924 0.004 0.696C-0.016 0.66 -0.024 0.64 -0.024 0.62C-0.024 0.592 -0.004 0.576 0.02 0.576C0.048 0.576 0.064 0.6 0.084 0.636C0.2 0.836 0.28 0.94 0.352 0.94C0.384 0.94 0.396 0.92 0.396 0.892C0.396 0.86 0.384 0.82 0.372 0.792L-0.12 -0.428C-0.132 -0.46 -0.14 -0.468 -0.18 -0.468H-0.304C-0.34 -0.468 -0.356 -0.484 -0.356 -0.52C-0.356 -0.552 -0.34 -0.568 -0.308 -0.568H0.464C0.5 -0.568 0.516 -0.552 0.516 -0.516C0.516 -0.484 0.5 -0.468 0.468 -0.468H0.308C0.284 -0.468 0.272 -0.468 0.272 -0.456C0.272 -0.452 0.276 -0.44 0.28 -0.428L0.46 0.02C0.468 0.04 0.476 0.068 0.496 0.068C0.516 0.068 0.528 0.028 0.592 -0.004C0.648 -0.032 0.7 -0.04 0.768 -0.04C1.152 -0.04 1.464 0.36 1.464 0.74C1.464 0.972 1.32 1.096 1.096 1.096ZM0.988 0.948C1.056 0.948 1.08 0.888 1.08 0.8C1.08 0.604 0.868 0.096 0.676 0.096C0.608 0.096 0.576 0.14 0.576 0.224C0.576 0.308 0.608 0.388 0.652 0.5L0.732 0.696C0.788 0.832 0.892 0.948 0.988 0.948Z",
|
||||
bbox: [-0.356, -0.568, 1.464, 1.096],
|
||||
},
|
||||
BravuraOutline {
|
||||
name: "fClef",
|
||||
codepoint: 0xE062,
|
||||
path: "M1.008 1.048C0.312 1.048 0 0.54 0 0.156C0 -0.164 0.168 -0.44 0.492 -0.44C0.744 -0.44 0.916 -0.264 0.916 -0.016C0.916 0.24 0.728 0.4 0.532 0.4C0.424 0.4 0.384 0.372 0.332 0.372C0.28 0.372 0.268 0.404 0.268 0.444C0.268 0.604 0.508 0.896 0.916 0.896C1.34 0.896 1.524 0.48 1.524 -0.148C1.524 -1.264 0.972 -1.888 0.04 -2.42C0.004 -2.44 -0.02 -2.46 -0.02 -2.492C-0.02 -2.516 -0.004 -2.54 0.032 -2.54C0.052 -2.54 0.076 -2.532 0.1 -2.52C1.084 -2.04 2.124 -1.328 2.124 -0.112C2.124 0.584 1.7 1.048 1.008 1.048ZM2.516 0.72C2.392 0.72 2.296 0.624 2.296 0.5C2.296 0.376 2.392 0.28 2.516 0.28C2.64 0.28 2.736 0.376 2.736 0.5C2.736 0.624 2.64 0.72 2.516 0.72ZM2.52 -0.284C2.396 -0.284 2.304 -0.376 2.304 -0.5C2.304 -0.624 2.396 -0.716 2.52 -0.716C2.644 -0.716 2.736 -0.624 2.736 -0.5C2.736 -0.376 2.644 -0.284 2.52 -0.284Z",
|
||||
bbox: [-0.02, -2.54, 2.736, 1.048],
|
||||
},
|
||||
BravuraOutline {
|
||||
name: "flag8thDown",
|
||||
codepoint: 0xE241,
|
||||
path: "M0.96 3.04C1.016 2.872 1.044 2.672 1.044 2.492C1.044 2.256 0.944 1.92 0.884 1.784C0.736 1.448 0.536 1.124 0 0.944V0.004C0 -0.036 0.032 -0.056 0.064 -0.056C0.1 -0.056 0.152 -0.032 0.16 0.032C0.228 0.412 0.524 0.76 0.728 1.076C0.98 1.472 1.224 1.948 1.224 2.448C1.224 2.76 1.144 3.044 1.112 3.172C1.1 3.216 1.076 3.232 1.048 3.232C0.988 3.232 0.92 3.156 0.96 3.04Z",
|
||||
bbox: [0.0, -0.056, 1.224, 3.232],
|
||||
},
|
||||
BravuraOutline {
|
||||
name: "flag8thUp",
|
||||
codepoint: 0xE240,
|
||||
path: "M0.952 -3.16C0.952 -3.16 1.056 -2.78 1.056 -2.468C1.056 -1.968 0.848 -1.496 0.596 -1.096C0.392 -0.78 0.224 -0.436 0.16 -0.052C0.148 0.012 0.116 0.036 0.076 0.036C0.032 0.036 0 0.024 0 -0.024V-0.98C0.264 -1.028 0.644 -1.572 0.788 -1.912C0.848 -2.048 0.884 -2.276 0.884 -2.512C0.884 -2.692 0.856 -2.88 0.788 -3.06C0.78 -3.084 0.776 -3.104 0.776 -3.12C0.776 -3.184 0.816 -3.22 0.84 -3.236C0.864 -3.252 0.932 -3.228 0.952 -3.16Z",
|
||||
bbox: [0.0, -3.2408, 1.056, 0.036],
|
||||
},
|
||||
BravuraOutline {
|
||||
name: "gClef",
|
||||
codepoint: 0xE050,
|
||||
path: "M1.504 1.66C1.496 1.708 1.504 1.712 1.528 1.736C1.96 2.14 2.288 2.648 2.288 3.26C2.288 3.608 2.192 3.952 2.028 4.192C1.968 4.28 1.864 4.392 1.82 4.392C1.764 4.392 1.64 4.288 1.56 4.2C1.264 3.872 1.168 3.372 1.168 2.956C1.168 2.724 1.196 2.464 1.224 2.3C1.232 2.252 1.236 2.244 1.188 2.204C0.612 1.728 0 1.156 0 0.348C0 -0.348 0.476 -1.008 1.456 -1.008C1.548 -1.008 1.652 -1 1.732 -0.984C1.776 -0.976 1.784 -0.972 1.792 -1.02C1.84 -1.288 1.9 -1.636 1.9 -1.824C1.9 -2.416 1.5 -2.488 1.264 -2.488C1.048 -2.488 0.944 -2.424 0.944 -2.372C0.944 -2.344 0.98 -2.332 1.072 -2.304C1.196 -2.268 1.34 -2.16 1.34 -1.928C1.34 -1.708 1.2 -1.52 0.956 -1.52C0.688 -1.52 0.528 -1.732 0.528 -1.98C0.528 -2.24 0.684 -2.632 1.288 -2.632C1.556 -2.632 2.076 -2.512 2.076 -1.832C2.076 -1.604 2.004 -1.224 1.96 -0.976C1.952 -0.928 1.956 -0.932 2.012 -0.908C2.416 -0.748 2.684 -0.408 2.684 0.044C2.684 0.556 2.308 1.008 1.72 1.008C1.616 1.008 1.616 1.008 1.604 1.08ZM1.88 3.772C2.012 3.772 2.12 3.664 2.12 3.444C2.12 3 1.74 2.64 1.424 2.364C1.396 2.34 1.38 2.344 1.372 2.396C1.356 2.5 1.348 2.636 1.348 2.764C1.348 3.388 1.636 3.772 1.88 3.772ZM1.444 1.048C1.456 0.972 1.456 0.976 1.384 0.952C1.032 0.832 0.804 0.516 0.804 0.176C0.804 -0.184 0.992 -0.44 1.264 -0.532C1.296 -0.544 1.344 -0.556 1.372 -0.556C1.404 -0.556 1.42 -0.536 1.42 -0.512C1.42 -0.484 1.388 -0.472 1.36 -0.46C1.192 -0.388 1.072 -0.216 1.072 -0.032C1.072 0.196 1.228 0.368 1.472 0.436C1.536 0.452 1.544 0.448 1.552 0.404L1.752 -0.788C1.76 -0.832 1.756 -0.832 1.696 -0.844C1.632 -0.856 1.552 -0.864 1.472 -0.864C0.772 -0.864 0.32 -0.476 0.32 0.08C0.32 0.316 0.36 0.632 0.692 1.008C0.932 1.276 1.116 1.424 1.304 1.576C1.344 1.608 1.352 1.604 1.36 1.56ZM1.72 0.412C1.712 0.46 1.716 0.472 1.764 0.468C2.088 0.44 2.356 0.168 2.356 -0.184C2.356 -0.436 2.204 -0.64 1.98 -0.752C1.932 -0.776 1.924 -0.776 1.916 -0.728Z",
|
||||
bbox: [0.0, -2.632, 2.684, 4.392],
|
||||
},
|
||||
BravuraOutline {
|
||||
name: "noteheadBlack",
|
||||
codepoint: 0xE0A4,
|
||||
path: "M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z",
|
||||
bbox: [0.0, -0.5, 1.18, 0.5],
|
||||
},
|
||||
BravuraOutline {
|
||||
name: "noteheadDoubleWhole",
|
||||
codepoint: 0xE0A0,
|
||||
path: "M0.036 0.62C0.012 0.62 0 0.604 0 0.588V-0.592C0 -0.608 0.012 -0.62 0.036 -0.62H0.088C0.108 -0.62 0.124 -0.608 0.124 -0.592V0.588C0.124 0.604 0.108 0.62 0.088 0.62ZM0.268 0.62C0.244 0.62 0.228 0.604 0.228 0.588V-0.592C0.228 -0.608 0.244 -0.62 0.268 -0.62H0.316C0.336 -0.62 0.356 -0.608 0.356 -0.592V0.588C0.356 0.604 0.336 0.62 0.316 0.62ZM2.076 0.62C2.056 0.62 2.036 0.604 2.036 0.588V-0.592C2.036 -0.608 2.056 -0.62 2.076 -0.62H2.128C2.148 -0.62 2.168 -0.608 2.168 -0.592V0.588C2.168 0.604 2.148 0.62 2.128 0.62ZM2.304 0.62C2.288 0.62 2.264 0.604 2.264 0.588V-0.592C2.264 -0.608 2.288 -0.62 2.304 -0.62H2.356C2.38 -0.62 2.396 -0.608 2.396 -0.592V0.588C2.396 0.604 2.38 0.62 2.356 0.62ZM1.216 0.5C0.688 0.5 0.36 0.28 0.36 0.004C0.36 -0.256 0.584 -0.5 1.176 -0.5C1.824 -0.5 2.036 -0.272 2.036 0.004C2.036 0.288 1.588 0.5 1.216 0.5ZM1.304 -0.404C1.136 -0.404 1.024 -0.312 0.92 -0.196C0.848 -0.104 0.788 0.032 0.788 0.16C0.788 0.364 0.924 0.408 1.112 0.408C1.384 0.408 1.604 0.116 1.604 -0.124C1.604 -0.32 1.48 -0.404 1.304 -0.404Z",
|
||||
bbox: [0.0, -0.62, 2.396, 0.62],
|
||||
},
|
||||
BravuraOutline {
|
||||
name: "noteheadHalf",
|
||||
codepoint: 0xE0A3,
|
||||
path: "M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z",
|
||||
bbox: [0.0, -0.5, 1.18, 0.5],
|
||||
},
|
||||
BravuraOutline {
|
||||
name: "noteheadWhole",
|
||||
codepoint: 0xE0A2,
|
||||
path: "M0.864 0.5C0.332 0.5 0 0.28 0 0.008C0 -0.26 0.228 -0.5 0.824 -0.5C1.48 -0.5 1.688 -0.272 1.688 0.008C1.688 0.292 1.236 0.5 0.864 0.5ZM0.444 0.252C0.488 0.392 0.636 0.412 0.76 0.412C1.036 0.412 1.256 0.116 1.256 -0.124C1.256 -0.248 1.204 -0.36 1.072 -0.392C1.032 -0.404 0.988 -0.408 0.948 -0.408C0.804 -0.408 0.656 -0.312 0.572 -0.2C0.492 -0.108 0.432 0.028 0.432 0.156C0.432 0.188 0.436 0.22 0.444 0.252Z",
|
||||
bbox: [0.0, -0.5, 1.688, 0.5],
|
||||
},
|
||||
BravuraOutline {
|
||||
name: "rest8th",
|
||||
codepoint: 0xE4E6,
|
||||
path: "M0.536 0.428C0.536 0.576 0.416 0.696 0.268 0.696C0.12 0.696 0 0.576 0 0.428C0 0.344 0.048 0.272 0.108 0.224C0.172 0.18 0.248 0.156 0.324 0.156C0.38 0.156 0.436 0.168 0.48 0.184C0.536 0.2 0.572 0.216 0.624 0.244C0.632 0.248 0.64 0.248 0.644 0.248C0.66 0.248 0.664 0.232 0.664 0.212C0.664 0.2 0.664 0.184 0.66 0.168C0.648 0.108 0.36 -0.688 0.288 -0.952C0.288 -1 0.38 -1.004 0.404 -1.004C0.448 -1.004 0.504 -0.996 0.544 -0.964C0.556 -0.956 0.948 0.448 0.948 0.448C0.964 0.52 0.984 0.584 0.988 0.604C0.988 0.644 0.948 0.664 0.94 0.668C0.932 0.668 0.92 0.668 0.896 0.652C0.868 0.628 0.668 0.388 0.536 0.388Z",
|
||||
bbox: [0.0, -1.004, 0.988, 0.696],
|
||||
},
|
||||
BravuraOutline {
|
||||
name: "restHalf",
|
||||
codepoint: 0xE4E4,
|
||||
path: "M1.128 0.096V0.464C1.128 0.524 1.08 0.568 1.024 0.568H0.104C0.044 0.568 0 0.524 0 0.464V0.096C0 0.04 0.044 -0.008 0.104 -0.008H1.024C1.08 -0.008 1.128 0.04 1.128 0.096Z",
|
||||
bbox: [0.0, -0.008, 1.128, 0.568],
|
||||
},
|
||||
BravuraOutline {
|
||||
name: "restQuarter",
|
||||
codepoint: 0xE4E5,
|
||||
path: "M0.312 -0.152C0.376 -0.232 0.432 -0.308 0.484 -0.392C0.492 -0.408 0.508 -0.44 0.508 -0.448C0.508 -0.452 0.508 -0.46 0.504 -0.464C0.496 -0.48 0.48 -0.484 0.46 -0.484C0.444 -0.484 0.412 -0.476 0.396 -0.472C0.392 -0.472 0.384 -0.472 0.38 -0.472L0.348 -0.46C0.14 -0.46 0.004 -0.648 0.004 -0.844C0.004 -1.044 0.176 -1.24 0.468 -1.464C0.5 -1.488 0.54 -1.5 0.572 -1.5C0.6 -1.5 0.628 -1.492 0.632 -1.476C0.636 -1.464 0.64 -1.456 0.64 -1.448C0.64 -1.412 0.608 -1.38 0.576 -1.352C0.524 -1.352 0.48 -1.244 0.472 -1.208C0.46 -1.176 0.456 -1.14 0.456 -1.104C0.456 -0.94 0.54 -0.812 0.708 -0.812C0.824 -0.812 0.956 -0.856 1.024 -0.88L1.028 -0.884C1.044 -0.888 1.052 -0.888 1.06 -0.888C1.072 -0.888 1.08 -0.884 1.08 -0.872C1.08 -0.824 0.976 -0.692 0.932 -0.644C0.78 -0.46 0.656 -0.312 0.656 -0.088C0.656 -0.084 0.656 -0.08 0.656 -0.076L0.66 -0.048C0.66 -0.044 0.66 -0.04 0.66 -0.036C0.676 0.196 0.82 0.388 0.924 0.552C0.936 0.572 0.94 0.592 0.94 0.612C0.94 0.652 0.924 0.688 0.924 0.688C0.924 0.688 0.332 1.392 0.264 1.46C0.244 1.48 0.216 1.492 0.192 1.492C0.152 1.492 0.112 1.464 0.112 1.408C0.112 1.388 0.116 1.368 0.128 1.344C0.144 1.3 0.372 1.096 0.372 0.808C0.372 0.66 0.312 0.488 0.132 0.3C0.092 0.26 0.076 0.216 0.076 0.184C0.076 0.128 0.116 0.088 0.116 0.088Z",
|
||||
bbox: [0.004, -1.5, 1.08, 1.492],
|
||||
},
|
||||
BravuraOutline {
|
||||
name: "restWhole",
|
||||
codepoint: 0xE4E3,
|
||||
path: "M1.128 -0.436V-0.068C1.128 -0.008 1.08 0.036 1.024 0.036H0.104C0.044 0.036 0 -0.008 0 -0.068V-0.436C0 -0.492 0.044 -0.54 0.104 -0.54H1.024C1.08 -0.54 1.128 -0.492 1.128 -0.436Z",
|
||||
bbox: [0.0, -0.54, 1.128, 0.036],
|
||||
},
|
||||
BravuraOutline {
|
||||
name: "timeSig4",
|
||||
codepoint: 0xE084,
|
||||
path: "M1.448 -0.296V0.56C1.448 0.592 1.444 0.628 1.4 0.628C1.364 0.628 1.344 0.62 1.32 0.592L0.94 0.132C0.924 0.112 0.904 0.088 0.904 0.04V-0.296H0.364C0.684 -0.024 1.324 0.884 1.336 0.932L1.34 0.944C1.34 0.98 1.312 1.004 1.28 1.004C1.244 1.004 1.08 0.996 1.008 0.996C0.936 0.996 0.756 1.004 0.724 1.004C0.688 1.004 0.632 0.992 0.632 0.928C0.632 0.432 0.24 -0.124 0.12 -0.292L0.096 -0.324C0.096 -0.324 0.096 -0.328 0.096 -0.328L0.092 -0.332C0.084 -0.352 0.08 -0.368 0.08 -0.38C0.08 -0.42 0.112 -0.448 0.16 -0.448H0.904V-0.7C0.904 -0.808 0.816 -0.84 0.744 -0.84C0.68 -0.84 0.652 -0.876 0.652 -0.916C0.652 -0.956 0.668 -1 0.728 -1H1.58C1.62 -1 1.66 -0.972 1.66 -0.916C1.66 -0.86 1.612 -0.836 1.572 -0.836C1.532 -0.836 1.448 -0.812 1.448 -0.684V-0.448H1.74C1.78 -0.448 1.8 -0.42 1.8 -0.372C1.8 -0.324 1.784 -0.296 1.74 -0.296Z",
|
||||
bbox: [0.08, -1.0, 1.8, 1.004],
|
||||
},
|
||||
BravuraOutline {
|
||||
name: "timeSigCommon",
|
||||
codepoint: 0xE08A,
|
||||
path: "M0.932 1.004C0.364 1.004 0.02 0.5 0.02 -0.04C0.02 -0.3 0.116 -0.528 0.28 -0.728C0.432 -0.912 0.652 -0.996 0.892 -0.996C1.592 -0.996 1.696 -0.36 1.696 -0.244C1.696 -0.156 1.652 -0.156 1.636 -0.156C1.604 -0.156 1.576 -0.16 1.576 -0.228C1.576 -0.744 1.136 -0.864 1.008 -0.864C0.684 -0.864 0.512 -0.548 0.512 0.112C0.512 0.772 0.816 0.908 1.032 0.908C1.24 0.908 1.324 0.812 1.324 0.772C1.324 0.736 1.312 0.684 1.252 0.684C1.092 0.684 1 0.512 1 0.388C1 0.16 1.196 0.088 1.308 0.088C1.308 0.088 1.612 0.1 1.612 0.432C1.612 0.956 1.008 1.004 0.932 1.004Z",
|
||||
bbox: [0.02, -0.996, 1.696, 1.004],
|
||||
},
|
||||
];
|
||||
|
|
@ -0,0 +1,508 @@
|
|||
//! The SVG renderer: a [`ResolvedLayoutIR`] to well-formed SVG 1.1.
|
||||
//!
|
||||
//! ## What this renderer is, and is not
|
||||
//!
|
||||
//! It is a **renderer**, not an engraver. Per the QUICKSTART non-overreach rule
|
||||
//! (`spec/PHASE2_QUICKSTART.md`, Agent I), every emitted SVG element traces to a
|
||||
//! [`ResolvedGlyph`] (and thus to its score-graph source) or to a declared
|
||||
//! renderer wrapper (the `<svg>` root, a metadata comment, the y-flip group, a
|
||||
//! per-layer `<g>`). The renderer makes *SVG-encoding* choices only — grouping,
|
||||
//! `<path>` vs `<rect>` fallback, the viewBox, the coordinate flip, colour
|
||||
//! representation. It makes **no engraving-semantic** choices (stem direction,
|
||||
//! spacing, beam slope, accidental placement, glyph selection): those are the
|
||||
//! solver's and the IR's. If a glyph it is asked to draw has no bundled outline,
|
||||
//! it surfaces a [`Diagnostic`] and falls back to a visible bounding-box rect —
|
||||
//! it never invents geometry.
|
||||
//!
|
||||
//! ## Coordinate system
|
||||
//!
|
||||
//! The IR is in **staff spaces, y-up** (musical convention: larger `y` = higher
|
||||
//! pitch). SVG is y-down. The renderer keeps every coordinate in staff-space,
|
||||
//! y-up world units and applies a single y-flip on the content group
|
||||
//! (`translate(-min_x, max_y) scale(1, -1)`), so a world point `(x, y)` maps to
|
||||
//! screen `(x - min_x, max_y - y)`. The `viewBox` is `0 0 W H` in staff spaces;
|
||||
//! `width`/`height` carry the display scale in px
|
||||
//! ([`RenderOptions::px_per_staff_space`]). Glyph outlines are bundled in the
|
||||
//! same staff-space/y-up frame, so each is placed with a plain
|
||||
//! `translate(x, y)`.
|
||||
//!
|
||||
//! ## Determinism
|
||||
//!
|
||||
//! Outline `d` data is fixed bundled text; every computed number is formatted to
|
||||
//! at most 4 decimals with `-0` normalised to `0`, so identical input yields
|
||||
//! byte-identical SVG (the acceptance snapshot golden-locks this).
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt::Write as _;
|
||||
|
||||
use epiphany_layout_ir::{BoundingBox, Provenance, ResolvedGlyph, ResolvedLayoutIR};
|
||||
|
||||
use crate::outline::outline;
|
||||
use crate::xml::{check_well_formed, escape_attr};
|
||||
|
||||
/// How glyphs are drawn.
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)]
|
||||
pub enum GlyphMode {
|
||||
/// Inline genuine Bravura outline `<path>`s (default). Self-contained: the
|
||||
/// SVG renders in any viewer with no font dependency (QUICKSTART, Agent I:
|
||||
/// "inline path outlines for golden fixtures and the demonstrable
|
||||
/// deliverable").
|
||||
#[default]
|
||||
PathOutline,
|
||||
}
|
||||
|
||||
/// Renderer configuration. SVG-encoding choices only — nothing here changes
|
||||
/// engraving.
|
||||
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||
pub struct RenderOptions {
|
||||
/// Display scale: points (px) per staff space, applied as the `<svg>`
|
||||
/// width/height while the viewBox stays in staff spaces.
|
||||
pub px_per_staff_space: f32,
|
||||
/// Blank margin around the content, in staff spaces.
|
||||
pub margin: f32,
|
||||
/// How glyphs are drawn.
|
||||
pub glyph_mode: GlyphMode,
|
||||
/// Emit `data-*` provenance attributes tracing each element to its source.
|
||||
pub emit_provenance: bool,
|
||||
}
|
||||
|
||||
impl Default for RenderOptions {
|
||||
fn default() -> Self {
|
||||
RenderOptions {
|
||||
px_per_staff_space: 10.0,
|
||||
margin: 2.0,
|
||||
glyph_mode: GlyphMode::PathOutline,
|
||||
emit_provenance: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A coarse glyph category, used for the machine acceptance snapshot's
|
||||
/// bounding-box-class counts.
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
|
||||
pub enum GlyphClass {
|
||||
Notehead,
|
||||
Clef,
|
||||
Accidental,
|
||||
Rest,
|
||||
Flag,
|
||||
TimeSignature,
|
||||
Barline,
|
||||
Dynamic,
|
||||
AugmentationDot,
|
||||
Other,
|
||||
}
|
||||
|
||||
impl GlyphClass {
|
||||
/// Classifies a SMuFL glyph name by its category prefix.
|
||||
pub fn of(name: &str) -> GlyphClass {
|
||||
if name.starts_with("notehead") {
|
||||
GlyphClass::Notehead
|
||||
} else if name.ends_with("Clef") {
|
||||
GlyphClass::Clef
|
||||
} else if name.starts_with("accidental") {
|
||||
GlyphClass::Accidental
|
||||
} else if name.starts_with("rest") {
|
||||
GlyphClass::Rest
|
||||
} else if name.starts_with("flag") {
|
||||
GlyphClass::Flag
|
||||
} else if name.starts_with("timeSig") {
|
||||
GlyphClass::TimeSignature
|
||||
} else if name.starts_with("barline") {
|
||||
GlyphClass::Barline
|
||||
} else if name.starts_with("dynamic") {
|
||||
GlyphClass::Dynamic
|
||||
} else if name == "augmentationDot" {
|
||||
GlyphClass::AugmentationDot
|
||||
} else {
|
||||
GlyphClass::Other
|
||||
}
|
||||
}
|
||||
|
||||
/// A short, stable token for the SVG `data-class` attribute and snapshots.
|
||||
pub fn token(self) -> &'static str {
|
||||
match self {
|
||||
GlyphClass::Notehead => "notehead",
|
||||
GlyphClass::Clef => "clef",
|
||||
GlyphClass::Accidental => "accidental",
|
||||
GlyphClass::Rest => "rest",
|
||||
GlyphClass::Flag => "flag",
|
||||
GlyphClass::TimeSignature => "timeSig",
|
||||
GlyphClass::Barline => "barline",
|
||||
GlyphClass::Dynamic => "dynamic",
|
||||
GlyphClass::AugmentationDot => "augmentationDot",
|
||||
GlyphClass::Other => "other",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A non-fatal note the renderer surfaces instead of papering over a problem
|
||||
/// (QUICKSTART, Agent I: "surface it via a diagnostic, don't paper over it").
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct Diagnostic {
|
||||
pub message: String,
|
||||
pub glyph: Option<String>,
|
||||
}
|
||||
|
||||
/// Machine-readable facts about a render, for the golden-locked acceptance
|
||||
/// snapshot (object/glyph/path/provenance/class counts + bounds).
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub struct RenderStats {
|
||||
/// Glyphs in the resolved layout (the renderer's input objects).
|
||||
pub glyph_count: usize,
|
||||
/// `<path>` elements emitted (glyphs drawn from a bundled outline).
|
||||
pub path_count: usize,
|
||||
/// Fallback `<rect>` elements emitted (glyphs with no bundled outline).
|
||||
pub fallback_rect_count: usize,
|
||||
/// Elements carrying a `data-prov` trace back to a score-graph source.
|
||||
pub provenance_count: usize,
|
||||
/// Distinct layers, each rendered as one `<g>` group.
|
||||
pub layer_count: usize,
|
||||
/// Per-class glyph counts.
|
||||
pub class_counts: BTreeMap<GlyphClass, usize>,
|
||||
/// The content viewBox `[min_x, min_y, width, height]`, in staff spaces.
|
||||
pub view_box: [f32; 4],
|
||||
}
|
||||
|
||||
/// The result of a render: the SVG text, machine stats, and any diagnostics.
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub struct RenderOutput {
|
||||
pub svg: String,
|
||||
pub stats: RenderStats,
|
||||
pub diagnostics: Vec<Diagnostic>,
|
||||
}
|
||||
|
||||
impl RenderOutput {
|
||||
/// Whether the emitted SVG passes the in-crate well-formedness check.
|
||||
pub fn is_well_formed(&self) -> bool {
|
||||
check_well_formed(&self.svg).is_ok()
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders a resolved layout to SVG. Pure and deterministic.
|
||||
pub fn render(resolved: &ResolvedLayoutIR, options: &RenderOptions) -> RenderOutput {
|
||||
let mut diagnostics = Vec::new();
|
||||
let mut class_counts: BTreeMap<GlyphClass, usize> = BTreeMap::new();
|
||||
for g in &resolved.glyphs {
|
||||
*class_counts
|
||||
.entry(GlyphClass::of(g.glyph.as_str()))
|
||||
.or_insert(0) += 1;
|
||||
}
|
||||
|
||||
// World bounds over each glyph's drawn extent (its outline bbox, or the IR
|
||||
// bounding box for a glyph with no bundled outline).
|
||||
let bounds = content_bounds(resolved, options.margin);
|
||||
|
||||
let (min_x, min_y, width, height) = match bounds {
|
||||
Some(b) => b,
|
||||
// Empty layout: a minimal, valid, honest empty canvas.
|
||||
None => {
|
||||
let svg = empty_svg();
|
||||
let well_formed = check_well_formed(&svg).is_ok();
|
||||
debug_assert!(well_formed);
|
||||
return RenderOutput {
|
||||
stats: RenderStats {
|
||||
glyph_count: 0,
|
||||
path_count: 0,
|
||||
fallback_rect_count: 0,
|
||||
provenance_count: 0,
|
||||
layer_count: 0,
|
||||
class_counts,
|
||||
view_box: [0.0, 0.0, 1.0, 1.0],
|
||||
},
|
||||
svg,
|
||||
diagnostics,
|
||||
};
|
||||
}
|
||||
};
|
||||
let max_y = min_y + height;
|
||||
|
||||
// Group glyph indices by layer (ascending), preserving input order within.
|
||||
let mut layers: BTreeMap<i32, Vec<usize>> = BTreeMap::new();
|
||||
for (i, g) in resolved.glyphs.iter().enumerate() {
|
||||
layers.entry(g.layer).or_default().push(i);
|
||||
}
|
||||
|
||||
let mut path_count = 0;
|
||||
let mut fallback_rect_count = 0;
|
||||
let mut provenance_count = 0;
|
||||
|
||||
let mut s = String::new();
|
||||
s.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
|
||||
let _ = writeln!(
|
||||
s,
|
||||
"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{}\" height=\"{}\" viewBox=\"0 0 {} {}\">",
|
||||
num(width * options.px_per_staff_space),
|
||||
num(height * options.px_per_staff_space),
|
||||
num(width),
|
||||
num(height),
|
||||
);
|
||||
// Declared metadata wrapper (a comment — honest about what this is).
|
||||
s.push_str(
|
||||
" <!-- epiphany-render-svg: glyphs are genuine Bravura SMuFL outlines; \
|
||||
geometry is the resolved layout verbatim, no engraving performed here -->\n",
|
||||
);
|
||||
// The single y-flip wrapper: staff-space/y-up world -> SVG y-down.
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <g transform=\"translate({} {}) scale(1 -1)\">",
|
||||
num(-min_x),
|
||||
num(max_y),
|
||||
);
|
||||
|
||||
for (layer, indices) in &layers {
|
||||
let _ = writeln!(s, " <g data-layer=\"{}\">", layer);
|
||||
for &i in indices {
|
||||
let g = &resolved.glyphs[i];
|
||||
let name = g.glyph.as_str();
|
||||
let (x, y) = (g.position.x.0, g.position.y.0);
|
||||
let (fill, opacity) = colour(g.style.rgba);
|
||||
let prov = if options.emit_provenance {
|
||||
provenance_count += 1;
|
||||
provenance_attrs(&g.provenance, name, GlyphClass::of(name))
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
match outline(name) {
|
||||
Some(o) => {
|
||||
path_count += 1;
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <path d=\"{}\" transform=\"translate({} {})\" fill=\"{}\"{}{}/>",
|
||||
o.path,
|
||||
num(x),
|
||||
num(y),
|
||||
fill,
|
||||
opacity,
|
||||
prov,
|
||||
);
|
||||
}
|
||||
None => {
|
||||
// No outline: surface it and draw the IR bounding box so the
|
||||
// missing glyph is visible, not silently absent.
|
||||
fallback_rect_count += 1;
|
||||
diagnostics.push(Diagnostic {
|
||||
message: "no bundled Bravura outline; drew bounding-box fallback"
|
||||
.to_owned(),
|
||||
glyph: Some(name.to_owned()),
|
||||
});
|
||||
let bb = g.bounding_box;
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <rect x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\" \
|
||||
fill=\"none\" stroke=\"#cc0000\" stroke-width=\"0.05\"{}/>",
|
||||
num(x + bb.left.0),
|
||||
num(y + bb.bottom.0),
|
||||
num((bb.right.0 - bb.left.0).max(0.0)),
|
||||
num((bb.top.0 - bb.bottom.0).max(0.0)),
|
||||
prov,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
s.push_str(" </g>\n");
|
||||
}
|
||||
|
||||
s.push_str(" </g>\n");
|
||||
s.push_str("</svg>\n");
|
||||
|
||||
RenderOutput {
|
||||
stats: RenderStats {
|
||||
glyph_count: resolved.glyphs.len(),
|
||||
path_count,
|
||||
fallback_rect_count,
|
||||
provenance_count,
|
||||
layer_count: layers.len(),
|
||||
class_counts,
|
||||
view_box: [num_f(min_x), num_f(min_y), num_f(width), num_f(height)],
|
||||
},
|
||||
svg: s,
|
||||
diagnostics,
|
||||
}
|
||||
}
|
||||
|
||||
/// The content bounds `(min_x, min_y, width, height)` in staff spaces, padded by
|
||||
/// `margin`, or `None` if there is nothing to draw.
|
||||
fn content_bounds(resolved: &ResolvedLayoutIR, margin: f32) -> Option<(f32, f32, f32, f32)> {
|
||||
let mut min_x = f32::INFINITY;
|
||||
let mut min_y = f32::INFINITY;
|
||||
let mut max_x = f32::NEG_INFINITY;
|
||||
let mut max_y = f32::NEG_INFINITY;
|
||||
let mut any = false;
|
||||
for g in &resolved.glyphs {
|
||||
let bb = drawn_bbox(g);
|
||||
let (x, y) = (g.position.x.0, g.position.y.0);
|
||||
for (px, py) in [
|
||||
(x + bb.left.0, y + bb.bottom.0),
|
||||
(x + bb.right.0, y + bb.top.0),
|
||||
] {
|
||||
if px.is_finite() && py.is_finite() {
|
||||
any = true;
|
||||
min_x = min_x.min(px);
|
||||
min_y = min_y.min(py);
|
||||
max_x = max_x.max(px);
|
||||
max_y = max_y.max(py);
|
||||
}
|
||||
}
|
||||
}
|
||||
if !any {
|
||||
return None;
|
||||
}
|
||||
min_x -= margin;
|
||||
min_y -= margin;
|
||||
max_x += margin;
|
||||
max_y += margin;
|
||||
// Guard against a degenerate zero-area box (e.g. a single zero-size glyph).
|
||||
let width = (max_x - min_x).max(f32::EPSILON);
|
||||
let height = (max_y - min_y).max(f32::EPSILON);
|
||||
Some((num_f(min_x), num_f(min_y), num_f(width), num_f(height)))
|
||||
}
|
||||
|
||||
/// The bounding box the renderer uses for a glyph: its real outline extent when
|
||||
/// bundled (what is actually drawn), else its IR bounding box.
|
||||
fn drawn_bbox(g: &ResolvedGlyph) -> BoundingBox {
|
||||
match outline(g.glyph.as_str()) {
|
||||
Some(o) => BoundingBox::new(o.bbox[0], o.bbox[1], o.bbox[2], o.bbox[3]),
|
||||
None => g.bounding_box,
|
||||
}
|
||||
}
|
||||
|
||||
/// `data-*` provenance attributes tracing an element to its score-graph source.
|
||||
fn provenance_attrs(p: &Provenance, glyph: &str, class: GlyphClass) -> String {
|
||||
format!(
|
||||
" data-prov=\"{:032x}\" data-source-kind=\"{}\" data-glyph=\"{}\" data-class=\"{}\"",
|
||||
p.stable_id.0,
|
||||
p.source.discriminant(),
|
||||
escape_attr(glyph),
|
||||
class.token(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Splits an `0xRRGGBBAA` colour into an SVG `fill` value and an optional
|
||||
/// `fill-opacity` attribute (empty when fully opaque).
|
||||
fn colour(rgba: u32) -> (String, String) {
|
||||
let r = (rgba >> 24) & 0xff;
|
||||
let g = (rgba >> 16) & 0xff;
|
||||
let b = (rgba >> 8) & 0xff;
|
||||
let a = rgba & 0xff;
|
||||
let fill = format!("#{r:02x}{g:02x}{b:02x}");
|
||||
let opacity = if a == 0xff {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" fill-opacity=\"{}\"", num(a as f32 / 255.0))
|
||||
};
|
||||
(fill, opacity)
|
||||
}
|
||||
|
||||
/// A minimal, valid empty SVG for a layout with nothing to draw.
|
||||
fn empty_svg() -> String {
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
|
||||
<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1\" height=\"1\" viewBox=\"0 0 1 1\">\n\
|
||||
\x20\x20<!-- epiphany-render-svg: empty resolved layout (no glyphs) -->\n\
|
||||
</svg>\n"
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
/// Formats a coordinate to at most 4 decimals, trimming trailing zeros and
|
||||
/// normalising `-0` to `0` for deterministic, compact output.
|
||||
fn num(v: f32) -> String {
|
||||
let v = num_f(v);
|
||||
let mut s = format!("{v:.4}");
|
||||
if s.contains('.') {
|
||||
while s.ends_with('0') {
|
||||
s.pop();
|
||||
}
|
||||
if s.ends_with('.') {
|
||||
s.pop();
|
||||
}
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
/// Normalises a coordinate value: `-0.0` and tiny `±0` round to `0.0`; other
|
||||
/// values pass through. Keeps the formatted form and the stored stats agreeing.
|
||||
fn num_f(v: f32) -> f32 {
|
||||
if v == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
v
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use epiphany_core::generators::valid_score_rich;
|
||||
use epiphany_layout_ir::{
|
||||
to_constrained, to_logical, ConstraintSolver, SolverConfig, StubSolver,
|
||||
};
|
||||
|
||||
fn stub_layout(seed: u64) -> ResolvedLayoutIR {
|
||||
let constrained = to_constrained(&to_logical(&valid_score_rich(seed)));
|
||||
StubSolver
|
||||
.solve(&constrained, &SolverConfig::default())
|
||||
.layout
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renders_well_formed_svg_with_one_path_per_glyph() {
|
||||
let layout = stub_layout(11);
|
||||
let out = render(&layout, &RenderOptions::default());
|
||||
assert!(out.is_well_formed(), "emitted SVG must be well-formed");
|
||||
assert_eq!(out.stats.glyph_count, layout.glyphs.len());
|
||||
// The stub pipeline only names bundled glyphs, so every glyph is a path.
|
||||
assert_eq!(out.stats.path_count, layout.glyphs.len());
|
||||
assert_eq!(out.stats.fallback_rect_count, 0);
|
||||
assert!(out.diagnostics.is_empty());
|
||||
assert_eq!(out.stats.provenance_count, layout.glyphs.len());
|
||||
assert!(out.svg.contains("<svg"));
|
||||
assert!(out.svg.contains("data-prov="));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_is_deterministic() {
|
||||
let layout = stub_layout(3);
|
||||
assert_eq!(
|
||||
render(&layout, &RenderOptions::default()).svg,
|
||||
render(&layout, &RenderOptions::default()).svg
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_layout_renders_a_valid_empty_canvas() {
|
||||
let layout = ResolvedLayoutIR {
|
||||
source: Default::default(),
|
||||
pages: vec![],
|
||||
glyphs: vec![],
|
||||
engraving_decisions: vec![],
|
||||
catalog: Default::default(),
|
||||
};
|
||||
let out = render(&layout, &RenderOptions::default());
|
||||
assert!(out.is_well_formed());
|
||||
assert_eq!(out.stats.glyph_count, 0);
|
||||
assert_eq!(out.stats.path_count, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provenance_can_be_suppressed() {
|
||||
let layout = stub_layout(5);
|
||||
let out = render(
|
||||
&layout,
|
||||
&RenderOptions {
|
||||
emit_provenance: false,
|
||||
..RenderOptions::default()
|
||||
},
|
||||
);
|
||||
assert_eq!(out.stats.provenance_count, 0);
|
||||
assert!(!out.svg.contains("data-prov="));
|
||||
assert!(out.is_well_formed());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classes_partition_the_glyphs() {
|
||||
let layout = stub_layout(7);
|
||||
let out = render(&layout, &RenderOptions::default());
|
||||
let total: usize = out.stats.class_counts.values().sum();
|
||||
assert_eq!(total, layout.glyphs.len());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,324 @@
|
|||
//! XML escaping and a from-scratch well-formedness validator for the SVG subset
|
||||
//! the renderer emits.
|
||||
//!
|
||||
//! Epiphany vendors no XML library (the workspace is deliberately dependency-
|
||||
//! light — every codec is hand-rolled). The renderer fully controls its output,
|
||||
//! so this module validates exactly the XML constructs it produces: a single
|
||||
//! root element, balanced and correctly nested tags, double-quoted attributes,
|
||||
//! and `&`/`<` only as escaped entities. It is a *well-formedness* checker (not a
|
||||
//! DTD/schema validator); the acceptance tests additionally cross-check output
|
||||
//! with the system `xmllint` when it is available, so the claim "the SVG
|
||||
//! XML-validates" rests on a real parser too, not only on this checker.
|
||||
|
||||
/// Escapes text content: `&`, `<`, `>` (the last for defensiveness against the
|
||||
/// `]]>` sequence). Quotes are legal in text and left as-is.
|
||||
pub fn escape_text(s: &str) -> String {
|
||||
let mut out = String::with_capacity(s.len());
|
||||
for c in s.chars() {
|
||||
match c {
|
||||
'&' => out.push_str("&"),
|
||||
'<' => out.push_str("<"),
|
||||
'>' => out.push_str(">"),
|
||||
_ => out.push(c),
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Escapes a double-quoted attribute value: `&`, `<`, `>`, and `"`.
|
||||
pub fn escape_attr(s: &str) -> String {
|
||||
let mut out = String::with_capacity(s.len());
|
||||
for c in s.chars() {
|
||||
match c {
|
||||
'&' => out.push_str("&"),
|
||||
'<' => out.push_str("<"),
|
||||
'>' => out.push_str(">"),
|
||||
'"' => out.push_str("""),
|
||||
_ => out.push(c),
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// A well-formedness defect, with a human-readable reason and the byte offset.
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct XmlError {
|
||||
pub reason: String,
|
||||
pub offset: usize,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for XmlError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "malformed XML at byte {}: {}", self.offset, self.reason)
|
||||
}
|
||||
}
|
||||
|
||||
/// Validates that `xml` is well-formed within the subset the renderer emits:
|
||||
/// optional leading `<?xml …?>`, comments, exactly one root element, balanced
|
||||
/// and properly nested tags, quoted attributes, and valid entity references.
|
||||
pub fn check_well_formed(xml: &str) -> Result<(), XmlError> {
|
||||
let b = xml.as_bytes();
|
||||
let mut i = 0;
|
||||
let mut stack: Vec<&str> = Vec::new();
|
||||
// True once the first (root) element has been opened. With an empty stack
|
||||
// it means the root has closed: any further element is a second root, and
|
||||
// any non-whitespace text is stray content.
|
||||
let mut seen_any_element = false;
|
||||
|
||||
let err = |offset: usize, reason: &str| {
|
||||
Err(XmlError {
|
||||
reason: reason.to_owned(),
|
||||
offset,
|
||||
})
|
||||
};
|
||||
|
||||
while i < b.len() {
|
||||
if b[i] == b'<' {
|
||||
// Tag of some kind.
|
||||
if xml[i..].starts_with("<?xml") {
|
||||
if i != 0 {
|
||||
return err(i, "XML declaration must be the first content");
|
||||
}
|
||||
let Some(end) = xml[i..].find("?>") else {
|
||||
return err(i, "unterminated XML declaration");
|
||||
};
|
||||
i += end + 2;
|
||||
continue;
|
||||
}
|
||||
if xml[i..].starts_with("<!--") {
|
||||
let body_start = i + 4;
|
||||
let Some(rel) = xml[body_start..].find("-->") else {
|
||||
return err(i, "unterminated comment");
|
||||
};
|
||||
if xml[body_start..body_start + rel].contains("--") {
|
||||
return err(i, "'--' is not allowed inside a comment");
|
||||
}
|
||||
i = body_start + rel + 3;
|
||||
continue;
|
||||
}
|
||||
if xml[i..].starts_with("<!") || xml[i..].starts_with("<?") {
|
||||
return err(i, "unsupported declaration/processing-instruction");
|
||||
}
|
||||
if xml[i..].starts_with("</") {
|
||||
// Close tag.
|
||||
let name_start = i + 2;
|
||||
let Some(rel) = xml[name_start..].find('>') else {
|
||||
return err(i, "unterminated close tag");
|
||||
};
|
||||
let name = xml[name_start..name_start + rel].trim_end();
|
||||
if !is_name(name) {
|
||||
return err(i, "invalid element name in close tag");
|
||||
}
|
||||
match stack.pop() {
|
||||
Some(open) if open == name => {}
|
||||
Some(open) => {
|
||||
return err(
|
||||
i,
|
||||
&format!("close tag </{name}> does not match open <{open}>"),
|
||||
)
|
||||
}
|
||||
None => return err(i, &format!("close tag </{name}> with no open element")),
|
||||
}
|
||||
i = name_start + rel + 1;
|
||||
continue;
|
||||
}
|
||||
// Open or self-closing tag: parse name then attributes.
|
||||
let (name, mut j) = read_name(xml, i + 1).ok_or_else(|| XmlError {
|
||||
reason: "invalid element name".to_owned(),
|
||||
offset: i,
|
||||
})?;
|
||||
if stack.is_empty() && seen_any_element {
|
||||
return err(i, "more than one root element");
|
||||
}
|
||||
seen_any_element = true;
|
||||
// Attributes.
|
||||
loop {
|
||||
j = skip_ws(b, j);
|
||||
if j >= b.len() {
|
||||
return err(i, "unterminated start tag");
|
||||
}
|
||||
if b[j] == b'>' {
|
||||
stack.push(name);
|
||||
j += 1;
|
||||
break;
|
||||
}
|
||||
if b[j] == b'/' {
|
||||
if j + 1 < b.len() && b[j + 1] == b'>' {
|
||||
// Self-closing: opens and closes in place; one element.
|
||||
j += 2;
|
||||
break;
|
||||
}
|
||||
return err(j, "'/' not followed by '>'");
|
||||
}
|
||||
// An attribute: name (=) "value".
|
||||
let (attr, after_name) = read_name(xml, j).ok_or_else(|| XmlError {
|
||||
reason: "invalid attribute name".to_owned(),
|
||||
offset: j,
|
||||
})?;
|
||||
let _ = attr;
|
||||
let k = skip_ws(b, after_name);
|
||||
if k >= b.len() || b[k] != b'=' {
|
||||
return err(k.min(b.len()), "attribute missing '='");
|
||||
}
|
||||
let k = skip_ws(b, k + 1);
|
||||
if k >= b.len() || (b[k] != b'"' && b[k] != b'\'') {
|
||||
return err(k.min(b.len()), "attribute value must be quoted");
|
||||
}
|
||||
let quote = b[k];
|
||||
let val_start = k + 1;
|
||||
let mut m = val_start;
|
||||
while m < b.len() && b[m] != quote {
|
||||
if b[m] == b'<' {
|
||||
return err(m, "'<' not allowed in attribute value");
|
||||
}
|
||||
if b[m] == b'&' {
|
||||
m = check_entity(xml, m)?;
|
||||
continue;
|
||||
}
|
||||
m += 1;
|
||||
}
|
||||
if m >= b.len() {
|
||||
return err(val_start, "unterminated attribute value");
|
||||
}
|
||||
j = m + 1; // past the closing quote
|
||||
}
|
||||
i = j;
|
||||
} else {
|
||||
// Text content. Outside the root (before it opens or after it
|
||||
// closes) only whitespace is permitted; inside, entities must be
|
||||
// valid and a raw '<' would already have been taken as a tag.
|
||||
if stack.is_empty() {
|
||||
if !b[i].is_ascii_whitespace() {
|
||||
return err(
|
||||
i,
|
||||
if seen_any_element {
|
||||
"text after the root element"
|
||||
} else {
|
||||
"text before the root element"
|
||||
},
|
||||
);
|
||||
}
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if b[i] == b'&' {
|
||||
i = check_entity(xml, i)?;
|
||||
continue;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if !stack.is_empty() {
|
||||
return Err(XmlError {
|
||||
reason: format!("unclosed element <{}>", stack.last().unwrap()),
|
||||
offset: xml.len(),
|
||||
});
|
||||
}
|
||||
if !seen_any_element {
|
||||
return Err(XmlError {
|
||||
reason: "no root element".to_owned(),
|
||||
offset: 0,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validates an entity reference starting at `start` (`b[start] == '&'`),
|
||||
/// returning the offset just past its terminating `;`.
|
||||
fn check_entity(xml: &str, start: usize) -> Result<usize, XmlError> {
|
||||
let rest = &xml[start..];
|
||||
let Some(semi) = rest.find(';') else {
|
||||
return Err(XmlError {
|
||||
reason: "entity reference missing ';'".to_owned(),
|
||||
offset: start,
|
||||
});
|
||||
};
|
||||
let body = &rest[1..semi];
|
||||
let ok = matches!(body, "amp" | "lt" | "gt" | "quot" | "apos")
|
||||
|| (body.starts_with("#x")
|
||||
&& body.len() > 2
|
||||
&& body[2..].bytes().all(|c| c.is_ascii_hexdigit()))
|
||||
|| (body.starts_with('#')
|
||||
&& body.len() > 1
|
||||
&& !body.starts_with("#x")
|
||||
&& body[1..].bytes().all(|c| c.is_ascii_digit()));
|
||||
if !ok {
|
||||
return Err(XmlError {
|
||||
reason: format!("invalid entity reference &{body};"),
|
||||
offset: start,
|
||||
});
|
||||
}
|
||||
Ok(start + semi + 1)
|
||||
}
|
||||
|
||||
fn skip_ws(b: &[u8], mut i: usize) -> usize {
|
||||
while i < b.len() && b[i].is_ascii_whitespace() {
|
||||
i += 1;
|
||||
}
|
||||
i
|
||||
}
|
||||
|
||||
/// Reads an XML name starting at `start`, returning `(name, offset_past_name)`.
|
||||
fn read_name(xml: &str, start: usize) -> Option<(&str, usize)> {
|
||||
let b = xml.as_bytes();
|
||||
if start >= b.len() || !is_name_start(b[start]) {
|
||||
return None;
|
||||
}
|
||||
let mut i = start + 1;
|
||||
while i < b.len() && is_name_char(b[i]) {
|
||||
i += 1;
|
||||
}
|
||||
Some((&xml[start..i], i))
|
||||
}
|
||||
|
||||
fn is_name(s: &str) -> bool {
|
||||
let b = s.as_bytes();
|
||||
!b.is_empty() && is_name_start(b[0]) && b[1..].iter().all(|&c| is_name_char(c))
|
||||
}
|
||||
|
||||
fn is_name_start(c: u8) -> bool {
|
||||
c.is_ascii_alphabetic() || c == b'_' || c == b':'
|
||||
}
|
||||
|
||||
fn is_name_char(c: u8) -> bool {
|
||||
is_name_start(c) || c.is_ascii_digit() || c == b'-' || c == b'.'
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn accepts_well_formed_documents() {
|
||||
check_well_formed(r#"<svg xmlns="x"><g><path d="M0 0Z"/></g></svg>"#).unwrap();
|
||||
check_well_formed("<?xml version=\"1.0\"?>\n<a><!-- c --><b x='1'/></a>\n").unwrap();
|
||||
check_well_formed(r#"<a t="&<0/"/>"#).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_mismatched_and_unclosed_tags() {
|
||||
assert!(check_well_formed("<a><b></a></b>").is_err());
|
||||
assert!(check_well_formed("<a><b></b>").is_err());
|
||||
assert!(check_well_formed("<a>").is_err());
|
||||
assert!(check_well_formed("</a>").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_two_roots_and_stray_markup() {
|
||||
assert!(check_well_formed("<a/><b/>").is_err());
|
||||
assert!(check_well_formed("<a>&bogus;</a>").is_err());
|
||||
assert!(check_well_formed(r#"<a x=1/>"#).is_err());
|
||||
assert!(check_well_formed(r#"<a x="1<2"/>"#).is_err());
|
||||
assert!(check_well_formed("text<a/>").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escaping_is_correct_and_round_trips_through_the_checker() {
|
||||
let t = escape_text("a & b < c > d");
|
||||
assert_eq!(t, "a & b < c > d");
|
||||
let a = escape_attr(r#"x"&<y"#);
|
||||
assert_eq!(a, "x"&<y");
|
||||
check_well_formed(&format!("<n title=\"{a}\">{t}</n>")).unwrap();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,251 @@
|
|||
//! Agent I's acceptance harness (the merge gate for `epiphany-render-svg`,
|
||||
//! `spec/PHASE2_QUICKSTART.md` §"Acceptance criteria per agent" → I).
|
||||
//!
|
||||
//! For each named fixture, drive the full v0 pipeline through the **stub solver**
|
||||
//! (this phase's deliverable is the renderer-against-stub) and assert:
|
||||
//!
|
||||
//! * the SVG is well-formed (the in-crate checker *and*, when available, the
|
||||
//! system `xmllint` — a real external XML parser);
|
||||
//! * every resolved glyph is drawn (one `<path>` per glyph; no silent drops) and
|
||||
//! traces back to its score-graph source via `data-prov`;
|
||||
//! * a **golden-locked machine acceptance snapshot** (object / glyph / path /
|
||||
//! provenance / layer / per-class / hard-constraint counts + XML validity)
|
||||
//! matches a committed file — changes require an explicit golden update;
|
||||
//! * the full SVG bytes match a committed golden — the renderer is deterministic.
|
||||
//!
|
||||
//! Regenerate goldens deliberately with `UPDATE_GOLDEN=1 cargo test -p
|
||||
//! epiphany-render-svg --test acceptance`.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use epiphany_core::Score;
|
||||
use epiphany_layout_ir::{
|
||||
to_constrained, to_logical, ConstrainedLayoutIR, ConstraintSolver, SolverConfig, StubSolver,
|
||||
};
|
||||
use epiphany_render_svg::{check_well_formed, render, GlyphClass, RenderOptions, RenderOutput};
|
||||
|
||||
/// The fixtures Agent I's acceptance names, each with a fixed seed so the score
|
||||
/// — and therefore the layout and SVG — is deterministic.
|
||||
fn fixtures() -> Vec<(&'static str, Score)> {
|
||||
vec![
|
||||
(
|
||||
"ten_measure_single_staff",
|
||||
epiphany_testkit::fixtures::ten_measure_single_staff(0x000A_11CE),
|
||||
),
|
||||
(
|
||||
"valid_score_rich",
|
||||
epiphany_core::generators::valid_score_rich(0x5EED),
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
fn pipeline(score: &Score) -> (ConstrainedLayoutIR, RenderOutput) {
|
||||
let constrained = to_constrained(&to_logical(score));
|
||||
let layout = StubSolver
|
||||
.solve(&constrained, &SolverConfig::default())
|
||||
.layout;
|
||||
let out = render(&layout, &RenderOptions::default());
|
||||
(constrained, out)
|
||||
}
|
||||
|
||||
/// A deterministic, human-diffable serialization of the machine acceptance
|
||||
/// snapshot.
|
||||
fn snapshot_text(fixture: &str, constrained: &ConstrainedLayoutIR, out: &RenderOutput) -> String {
|
||||
let mut s = String::new();
|
||||
s.push_str(&format!("fixture={fixture} solver=stub\n"));
|
||||
s.push_str(&format!("glyph_count={}\n", out.stats.glyph_count));
|
||||
s.push_str(&format!("path_count={}\n", out.stats.path_count));
|
||||
s.push_str(&format!(
|
||||
"fallback_rect_count={}\n",
|
||||
out.stats.fallback_rect_count
|
||||
));
|
||||
s.push_str(&format!(
|
||||
"provenance_count={}\n",
|
||||
out.stats.provenance_count
|
||||
));
|
||||
s.push_str(&format!("layer_count={}\n", out.stats.layer_count));
|
||||
s.push_str(&format!(
|
||||
"hard_constraint_count={}\n",
|
||||
constrained.constraints.len()
|
||||
));
|
||||
s.push_str(&format!(
|
||||
"xml_well_formed={}\n",
|
||||
check_well_formed(&out.svg).is_ok()
|
||||
));
|
||||
let vb = out.stats.view_box;
|
||||
s.push_str(&format!(
|
||||
"view_box=[{} {} {} {}]\n",
|
||||
vb[0], vb[1], vb[2], vb[3]
|
||||
));
|
||||
let labelled: BTreeMap<&str, usize> = out
|
||||
.stats
|
||||
.class_counts
|
||||
.iter()
|
||||
.map(|(c, n)| (c.token(), *n))
|
||||
.collect();
|
||||
s.push_str("class_counts:\n");
|
||||
for (token, n) in labelled {
|
||||
s.push_str(&format!(" {token}={n}\n"));
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
fn golden_path(name: &str) -> PathBuf {
|
||||
let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
p.push("tests");
|
||||
p.push("golden");
|
||||
p.push(name);
|
||||
p
|
||||
}
|
||||
|
||||
/// Compares `actual` to the committed golden at `tests/golden/<name>`, or writes
|
||||
/// it when `UPDATE_GOLDEN` is set.
|
||||
fn assert_golden(name: &str, actual: &str) {
|
||||
let path = golden_path(name);
|
||||
if std::env::var_os("UPDATE_GOLDEN").is_some() {
|
||||
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
std::fs::write(&path, actual).unwrap();
|
||||
return;
|
||||
}
|
||||
let expected = std::fs::read_to_string(&path).unwrap_or_else(|e| {
|
||||
panic!(
|
||||
"missing golden {} ({e}); regenerate with UPDATE_GOLDEN=1",
|
||||
path.display()
|
||||
)
|
||||
});
|
||||
assert_eq!(
|
||||
actual,
|
||||
expected,
|
||||
"golden mismatch for {}; if intended, rerun with UPDATE_GOLDEN=1",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixtures_render_to_golden_locked_svg_and_snapshot() {
|
||||
for (fixture, score) in fixtures() {
|
||||
let (constrained, out) = pipeline(&score);
|
||||
|
||||
// Structural invariants (would fail if the renderer stubbed/dropped work).
|
||||
assert!(out.stats.glyph_count > 0, "{fixture}: nothing was laid out");
|
||||
assert_eq!(
|
||||
out.stats.path_count + out.stats.fallback_rect_count,
|
||||
out.stats.glyph_count,
|
||||
"{fixture}: every glyph must be drawn (path or fallback), none dropped"
|
||||
);
|
||||
assert_eq!(
|
||||
out.stats.provenance_count, out.stats.glyph_count,
|
||||
"{fixture}: every drawn glyph must carry a provenance trace"
|
||||
);
|
||||
let class_sum: usize = out.stats.class_counts.values().sum();
|
||||
assert_eq!(
|
||||
class_sum, out.stats.glyph_count,
|
||||
"{fixture}: classes partition glyphs"
|
||||
);
|
||||
|
||||
// The in-crate well-formedness gate.
|
||||
check_well_formed(&out.svg)
|
||||
.unwrap_or_else(|e| panic!("{fixture}: emitted SVG is not well-formed: {e}"));
|
||||
|
||||
// Provenance survival: every laid-out glyph's stable id appears as a
|
||||
// data-prov trace in the SVG text.
|
||||
for g in &out_glyph_ids(&score) {
|
||||
assert!(
|
||||
out.svg.contains(&format!("data-prov=\"{g:032x}\"")),
|
||||
"{fixture}: provenance {g:032x} did not survive into the SVG"
|
||||
);
|
||||
}
|
||||
|
||||
// Golden locks: the machine snapshot and the full SVG bytes.
|
||||
assert_golden(
|
||||
&format!("{fixture}.stub.snapshot.txt"),
|
||||
&snapshot_text(fixture, &constrained, &out),
|
||||
);
|
||||
assert_golden(&format!("{fixture}.stub.svg"), &out.svg);
|
||||
}
|
||||
}
|
||||
|
||||
/// The stable layout-object ids the pipeline lays out for a score (used to check
|
||||
/// provenance survival into the SVG).
|
||||
fn out_glyph_ids(score: &Score) -> Vec<u128> {
|
||||
let layout = StubSolver
|
||||
.solve(
|
||||
&to_constrained(&to_logical(score)),
|
||||
&SolverConfig::default(),
|
||||
)
|
||||
.layout;
|
||||
layout
|
||||
.glyphs
|
||||
.iter()
|
||||
.map(|g| g.provenance.stable_id.0)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_renderer_draws_real_bravura_curves_not_placeholders() {
|
||||
// Non-vacuity: the SVG must contain genuine outline data (cubic bezier 'C'
|
||||
// commands from the Bravura CFF outlines), not just rectangles or moves.
|
||||
let (_, out) = pipeline(&epiphany_testkit::fixtures::ten_measure_single_staff(
|
||||
0x000A_11CE,
|
||||
));
|
||||
let curve_paths = out
|
||||
.svg
|
||||
.lines()
|
||||
.filter(|l| l.contains("<path") && l.contains('C'))
|
||||
.count();
|
||||
assert!(
|
||||
curve_paths > 0,
|
||||
"expected genuine Bravura bezier outlines, found none"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classes_are_non_trivial() {
|
||||
// The stub pipeline maps several object kinds to several glyph classes; the
|
||||
// snapshot should not collapse to a single class (which would suggest the
|
||||
// classifier or the pipeline went vacuous).
|
||||
let (_, out) = pipeline(&epiphany_core::generators::valid_score_rich(0x5EED));
|
||||
let nonzero = out.stats.class_counts.values().filter(|&&n| n > 0).count();
|
||||
assert!(
|
||||
nonzero >= 2,
|
||||
"expected several glyph classes, got {nonzero}"
|
||||
);
|
||||
// Sanity: the classifier knows the standard categories.
|
||||
assert_eq!(GlyphClass::of("gClef"), GlyphClass::Clef);
|
||||
assert_eq!(GlyphClass::of("noteheadBlack"), GlyphClass::Notehead);
|
||||
}
|
||||
|
||||
/// Best-effort cross-check with the system `xmllint` (a real XML parser). When
|
||||
/// `xmllint` is absent the in-crate checker remains the gate; when present, the
|
||||
/// "XML-validates" claim rests on an external parser too.
|
||||
#[test]
|
||||
fn svg_validates_under_xmllint_when_available() {
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
|
||||
let (_, out) = pipeline(&epiphany_testkit::fixtures::ten_measure_single_staff(
|
||||
0x000A_11CE,
|
||||
));
|
||||
|
||||
let mut tmp = std::env::temp_dir();
|
||||
tmp.push("epiphany_render_svg_xmllint_check.svg");
|
||||
let mut f = match std::fs::File::create(&tmp) {
|
||||
Ok(f) => f,
|
||||
Err(_) => return, // can't write a temp file; skip
|
||||
};
|
||||
f.write_all(out.svg.as_bytes()).unwrap();
|
||||
drop(f);
|
||||
|
||||
match Command::new("xmllint").arg("--noout").arg(&tmp).status() {
|
||||
Ok(status) => assert!(
|
||||
status.success(),
|
||||
"xmllint rejected the emitted SVG as malformed"
|
||||
),
|
||||
Err(_) => {
|
||||
// xmllint not installed; the in-crate checker already gated this run.
|
||||
eprintln!("note: xmllint not available; relied on the in-crate checker");
|
||||
}
|
||||
}
|
||||
let _ = std::fs::remove_file(&tmp);
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
fixture=ten_measure_single_staff solver=stub
|
||||
glyph_count=98
|
||||
path_count=98
|
||||
fallback_rect_count=0
|
||||
provenance_count=98
|
||||
layer_count=1
|
||||
hard_constraint_count=0
|
||||
xml_well_formed=true
|
||||
view_box=[-2 -5.2408 102.18 11.6328]
|
||||
class_counts:
|
||||
accidental=10
|
||||
clef=2
|
||||
flag=1
|
||||
notehead=83
|
||||
rest=2
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1021.8" height="116.328" viewBox="0 0 102.18 11.6328">
|
||||
<!-- epiphany-render-svg: glyphs are genuine Bravura SMuFL outlines; geometry is the resolved layout verbatim, no engraving performed here -->
|
||||
<g transform="translate(2 6.392) scale(1 -1)">
|
||||
<g data-layer="0">
|
||||
<path d="M0.92 1.928C0.92 1.984 0.892 2.012 0.836 2.012H0.832C0.776 2.012 0.748 1.984 0.748 1.928V-1.928C0.748 -1.984 0.776 -2.012 0.832 -2.012H0.836C0.892 -2.012 0.92 -1.984 0.92 -1.928V-0.176C0.92 -0.144 0.94 -0.148 0.956 -0.152C1.06 -0.18 1.228 -0.284 1.312 -0.736C1.324 -0.8 1.348 -0.836 1.388 -0.836C1.432 -0.836 1.452 -0.796 1.472 -0.728C1.524 -0.552 1.616 -0.356 1.9 -0.356C2.16 -0.356 2.232 -0.612 2.232 -1.136C2.232 -1.66 2.14 -1.896 1.808 -1.896C1.752 -1.896 1.468 -1.872 1.468 -1.788C1.468 -1.768 1.532 -1.744 1.576 -1.728C1.656 -1.7 1.736 -1.62 1.736 -1.468C1.736 -1.292 1.62 -1.192 1.464 -1.192C1.292 -1.192 1.156 -1.308 1.156 -1.52C1.156 -1.772 1.376 -2.024 1.852 -2.024C2.508 -2.024 2.796 -1.564 2.796 -1.148C2.796 -0.596 2.492 -0.212 1.96 -0.212C1.844 -0.212 1.768 -0.232 1.716 -0.248C1.676 -0.26 1.636 -0.268 1.6 -0.244C1.544 -0.208 1.456 -0.08 1.456 0C1.456 0.08 1.544 0.208 1.6 0.244C1.636 0.268 1.676 0.26 1.716 0.248C1.768 0.232 1.844 0.212 1.96 0.212C2.492 0.212 2.796 0.596 2.796 1.148C2.796 1.564 2.508 2.024 1.852 2.024C1.376 2.024 1.156 1.772 1.156 1.52C1.156 1.308 1.292 1.192 1.464 1.192C1.62 1.192 1.736 1.292 1.736 1.468C1.736 1.62 1.656 1.7 1.576 1.728C1.532 1.744 1.468 1.768 1.468 1.788C1.468 1.872 1.752 1.896 1.808 1.896C2.14 1.896 2.232 1.66 2.232 1.136C2.232 0.612 2.16 0.356 1.9 0.356C1.616 0.356 1.524 0.552 1.472 0.728C1.452 0.796 1.432 0.836 1.388 0.836C1.348 0.836 1.324 0.8 1.312 0.736C1.228 0.284 1.06 0.18 0.956 0.152C0.94 0.148 0.92 0.144 0.92 0.176ZM0.084 2.012C0.028 2.012 0 1.984 0 1.928V-1.928C0 -1.984 0.028 -2.012 0.084 -2.012H0.428C0.484 -2.012 0.512 -1.984 0.512 -1.928V1.928C0.512 1.984 0.484 2.012 0.428 2.012Z" transform="translate(0 0)" fill="#000000" data-prov="2ef8d2dba955aee38147023b72bdabb7" data-source-kind="6" data-glyph="cClef" data-class="clef"/>
|
||||
<path d="M0.036 0.62C0.012 0.62 0 0.604 0 0.588V-0.592C0 -0.608 0.012 -0.62 0.036 -0.62H0.088C0.108 -0.62 0.124 -0.608 0.124 -0.592V0.588C0.124 0.604 0.108 0.62 0.088 0.62ZM0.268 0.62C0.244 0.62 0.228 0.604 0.228 0.588V-0.592C0.228 -0.608 0.244 -0.62 0.268 -0.62H0.316C0.336 -0.62 0.356 -0.608 0.356 -0.592V0.588C0.356 0.604 0.336 0.62 0.316 0.62ZM2.076 0.62C2.056 0.62 2.036 0.604 2.036 0.588V-0.592C2.036 -0.608 2.056 -0.62 2.076 -0.62H2.128C2.148 -0.62 2.168 -0.608 2.168 -0.592V0.588C2.168 0.604 2.148 0.62 2.128 0.62ZM2.304 0.62C2.288 0.62 2.264 0.604 2.264 0.588V-0.592C2.264 -0.608 2.288 -0.62 2.304 -0.62H2.356C2.38 -0.62 2.396 -0.608 2.396 -0.592V0.588C2.396 0.604 2.38 0.62 2.356 0.62ZM1.216 0.5C0.688 0.5 0.36 0.28 0.36 0.004C0.36 -0.256 0.584 -0.5 1.176 -0.5C1.824 -0.5 2.036 -0.272 2.036 0.004C2.036 0.288 1.588 0.5 1.216 0.5ZM1.304 -0.404C1.136 -0.404 1.024 -0.312 0.92 -0.196C0.848 -0.104 0.788 0.032 0.788 0.16C0.788 0.364 0.924 0.408 1.112 0.408C1.384 0.408 1.604 0.116 1.604 -0.124C1.604 -0.32 1.48 -0.404 1.304 -0.404Z" transform="translate(1 0)" fill="#000000" data-prov="a2b28d85f329dbc90f5b6c27789b129b" data-source-kind="3" data-glyph="noteheadDoubleWhole" data-class="notehead"/>
|
||||
<path d="M1.504 1.66C1.496 1.708 1.504 1.712 1.528 1.736C1.96 2.14 2.288 2.648 2.288 3.26C2.288 3.608 2.192 3.952 2.028 4.192C1.968 4.28 1.864 4.392 1.82 4.392C1.764 4.392 1.64 4.288 1.56 4.2C1.264 3.872 1.168 3.372 1.168 2.956C1.168 2.724 1.196 2.464 1.224 2.3C1.232 2.252 1.236 2.244 1.188 2.204C0.612 1.728 0 1.156 0 0.348C0 -0.348 0.476 -1.008 1.456 -1.008C1.548 -1.008 1.652 -1 1.732 -0.984C1.776 -0.976 1.784 -0.972 1.792 -1.02C1.84 -1.288 1.9 -1.636 1.9 -1.824C1.9 -2.416 1.5 -2.488 1.264 -2.488C1.048 -2.488 0.944 -2.424 0.944 -2.372C0.944 -2.344 0.98 -2.332 1.072 -2.304C1.196 -2.268 1.34 -2.16 1.34 -1.928C1.34 -1.708 1.2 -1.52 0.956 -1.52C0.688 -1.52 0.528 -1.732 0.528 -1.98C0.528 -2.24 0.684 -2.632 1.288 -2.632C1.556 -2.632 2.076 -2.512 2.076 -1.832C2.076 -1.604 2.004 -1.224 1.96 -0.976C1.952 -0.928 1.956 -0.932 2.012 -0.908C2.416 -0.748 2.684 -0.408 2.684 0.044C2.684 0.556 2.308 1.008 1.72 1.008C1.616 1.008 1.616 1.008 1.604 1.08ZM1.88 3.772C2.012 3.772 2.12 3.664 2.12 3.444C2.12 3 1.74 2.64 1.424 2.364C1.396 2.34 1.38 2.344 1.372 2.396C1.356 2.5 1.348 2.636 1.348 2.764C1.348 3.388 1.636 3.772 1.88 3.772ZM1.444 1.048C1.456 0.972 1.456 0.976 1.384 0.952C1.032 0.832 0.804 0.516 0.804 0.176C0.804 -0.184 0.992 -0.44 1.264 -0.532C1.296 -0.544 1.344 -0.556 1.372 -0.556C1.404 -0.556 1.42 -0.536 1.42 -0.512C1.42 -0.484 1.388 -0.472 1.36 -0.46C1.192 -0.388 1.072 -0.216 1.072 -0.032C1.072 0.196 1.228 0.368 1.472 0.436C1.536 0.452 1.544 0.448 1.552 0.404L1.752 -0.788C1.76 -0.832 1.756 -0.832 1.696 -0.844C1.632 -0.856 1.552 -0.864 1.472 -0.864C0.772 -0.864 0.32 -0.476 0.32 0.08C0.32 0.316 0.36 0.632 0.692 1.008C0.932 1.276 1.116 1.424 1.304 1.576C1.344 1.608 1.352 1.604 1.36 1.56ZM1.72 0.412C1.712 0.46 1.716 0.472 1.764 0.468C2.088 0.44 2.356 0.168 2.356 -0.184C2.356 -0.436 2.204 -0.64 1.98 -0.752C1.932 -0.776 1.924 -0.776 1.916 -0.728Z" transform="translate(2 0)" fill="#000000" data-prov="7979bb59896a39443f4602af4f2ee4fb" data-source-kind="4" data-glyph="gClef" data-class="clef"/>
|
||||
<path d="M0.864 0.5C0.332 0.5 0 0.28 0 0.008C0 -0.26 0.228 -0.5 0.824 -0.5C1.48 -0.5 1.688 -0.272 1.688 0.008C1.688 0.292 1.236 0.5 0.864 0.5ZM0.444 0.252C0.488 0.392 0.636 0.412 0.76 0.412C1.036 0.412 1.256 0.116 1.256 -0.124C1.256 -0.248 1.204 -0.36 1.072 -0.392C1.032 -0.404 0.988 -0.408 0.948 -0.408C0.804 -0.408 0.656 -0.312 0.572 -0.2C0.492 -0.108 0.432 0.028 0.432 0.156C0.432 0.188 0.436 0.22 0.444 0.252Z" transform="translate(3 0)" fill="#000000" data-prov="299bf2a93f24f45bbad8cd9368623750" data-source-kind="2" data-glyph="noteheadWhole" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(4 0)" fill="#000000" data-prov="f180ab2f8fefa5fd114a522262781511" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(5 0)" fill="#000000" data-prov="b94bca553a7a2dbe815bef6d8b770dc8" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(6 0)" fill="#000000" data-prov="00cff43b791d393f0132b2b1fefa97fb" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(7 0)" fill="#000000" data-prov="a3820f9b52ab61a5c70d077f37b1dfbe" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(8 0)" fill="#000000" data-prov="ce8b8ed3f423f2233ee0349407382e86" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(9 0)" fill="#000000" data-prov="893ce7ac8d0b518dd85d53aae07485a6" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(10 0)" fill="#000000" data-prov="3454bf92ac286b4fa89f0711eb8406ab" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(11 0)" fill="#000000" data-prov="e0a27906823095e45f5f8acb6e5784ab" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(12 0)" fill="#000000" data-prov="78705817c33095fd68585149efa39693" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(13 0)" fill="#000000" data-prov="111d36d0544df9583dc71243a02cbace" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(14 0)" fill="#000000" data-prov="e7010481978598b50f2d16247c9e536f" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(15 0)" fill="#000000" data-prov="5abc05432af9047dd37a867f5508d366" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(16 0)" fill="#000000" data-prov="9d2e5b21bd83575f4a739897b23903f8" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(17 0)" fill="#000000" data-prov="cb42d5aafb0af3c41ab468790c214695" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(18 0)" fill="#000000" data-prov="4ae7a4d1adce3e02f9617f49301b790e" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(19 0)" fill="#000000" data-prov="463525d1acbd9e2cfeb6bf72b8a3bb2a" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(20 0)" fill="#000000" data-prov="8d1e19dc98ef35c36a61bf40c10e078a" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(21 0)" fill="#000000" data-prov="a673ee8395ee4b4d9652eebb4ded695b" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(22 0)" fill="#000000" data-prov="722042f4225d40465081128a28b9ba6d" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(23 0)" fill="#000000" data-prov="963cd4bce2dec2fc217d92c9691cfa10" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(24 0)" fill="#000000" data-prov="7979e5ca0cc41e36adf997ca73e3f11f" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(25 0)" fill="#000000" data-prov="3bff036aa7f994b09edf4cdea0bbd625" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(26 0)" fill="#000000" data-prov="6ea8506bc1beb9e26dd4f0a962b47e5c" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(27 0)" fill="#000000" data-prov="d63ffa0c7268bf23fcfeac0a1afde7f4" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(28 0)" fill="#000000" data-prov="3381c124bd495652c03209c9137ad1d6" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(29 0)" fill="#000000" data-prov="184c7ae4b9e86ee27d0bd85ff4b3ec7f" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(30 0)" fill="#000000" data-prov="c6bd0131d5d8b7f7084ce352fe03b9ee" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(31 0)" fill="#000000" data-prov="fb85357d0a96b2eb7a677d6edc58977e" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(32 0)" fill="#000000" data-prov="d1444672c538461a35616863866dd529" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(33 0)" fill="#000000" data-prov="c5bb48e08cef8b001fa7953f02590464" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(34 0)" fill="#000000" data-prov="36be7aa63cfa919f189daf8907b33414" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(35 0)" fill="#000000" data-prov="4f5cb48ae83cce69bc9907b49f2c55d8" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(36 0)" fill="#000000" data-prov="a9abf87e1178566e992ad27a605625db" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(37 0)" fill="#000000" data-prov="0cb38d85c0821f1a91f93d13cba6edc8" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(38 0)" fill="#000000" data-prov="663911388f594e9faf492ca1bef4424c" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(39 0)" fill="#000000" data-prov="4a111579a942ea99be4094d257b9229c" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(40 0)" fill="#000000" data-prov="e13d5950ccece94e0e78721af7a9b7ff" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(41 0)" fill="#000000" data-prov="edfdfb91c37ae76bfa51efd66c0f310a" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(42 0)" fill="#000000" data-prov="2d4b55ae05bb8a68647b9f53172d15f3" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(43 0)" fill="#000000" data-prov="6361d185baa8ed77f350287e10ed2176" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(44 0)" fill="#000000" data-prov="fabd84e3712a5226d704896a4711856d" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(45 0)" fill="#000000" data-prov="c2caa10ac0cf05f631b91d334ef24493" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(46 0)" fill="#000000" data-prov="254b610b6e96d8977e752e1e329c242c" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(47 0)" fill="#000000" data-prov="c16110f407e5c8ac507641d79d932fe6" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(48 0)" fill="#000000" data-prov="45aaeb27e4587d964fe054d9da91a55d" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(49 0)" fill="#000000" data-prov="cf10c843c33809009e9333526dd0879f" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(50 0)" fill="#000000" data-prov="9db9470ac3ce354bf774ef612c676c1f" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(51 0)" fill="#000000" data-prov="2aecf8993db2e9bf833e391d8824cac9" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(52 0)" fill="#000000" data-prov="b934ef5e3771b425d5e646d1ec905ba7" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(53 0)" fill="#000000" data-prov="b523a1a70a99069c4258e9a7f62744b8" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(54 0)" fill="#000000" data-prov="9ee165a1b754cf15a94af8ce1a2ed74a" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(55 0)" fill="#000000" data-prov="e970cb6a1cac78405c15ceaf6627f370" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(56 0)" fill="#000000" data-prov="e7c3cefbeaf0c7b182f61938f50b84b7" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(57 0)" fill="#000000" data-prov="e3eb76921ae215aa98331a9d1ad26804" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(58 0)" fill="#000000" data-prov="43269b2ba973f7d04f787e51fa30d2fe" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(59 0)" fill="#000000" data-prov="0fba3a760b42655a82fb25596d29fd02" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(60 0)" fill="#000000" data-prov="dbb189030d3fb922021f944ac8aa8c6d" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(61 0)" fill="#000000" data-prov="66fa0c6a9dfef2806751936f0360c782" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(62 0)" fill="#000000" data-prov="65ff34e6e9f8e9b1bbf795ffe3eaa2e6" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(63 0)" fill="#000000" data-prov="b546aac49da2b4624d759aed4527fe1d" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(64 0)" fill="#000000" data-prov="b00773cbb8db6a4218f1f956ebce9e25" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(65 0)" fill="#000000" data-prov="0275a564c27b5031f71cce54b0a8f830" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(66 0)" fill="#000000" data-prov="2d8c211fa0abbeb102ecafd986ec9f32" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(67 0)" fill="#000000" data-prov="a5b5cfea1a313032df813df1dcb06bc8" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(68 0)" fill="#000000" data-prov="b28b480792b411f3928a16c150a2a231" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(69 0)" fill="#000000" data-prov="6c3f374430ab9118c68bf368ccf35357" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(70 0)" fill="#000000" data-prov="67a80c74cc078028d23262082a6467af" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(71 0)" fill="#000000" data-prov="e95f370ea65c0f5c664b994bce3040ad" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(72 0)" fill="#000000" data-prov="c4f5b4a9375f3dcc6dd566865df0c92f" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(73 0)" fill="#000000" data-prov="c27abce6ad5e01170068a491b9424cbb" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(74 0)" fill="#000000" data-prov="ba2739c661b9acb3d32494f0f1adae98" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(75 0)" fill="#000000" data-prov="682d690f334af9f63b1a71a7d6cb10d8" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(76 0)" fill="#000000" data-prov="baea3329fdb0f32274fe3b9985ffe572" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(77 0)" fill="#000000" data-prov="b6f35f534dabfbb2ff75515cdd1713c5" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(78 0)" fill="#000000" data-prov="4dbd0d2bd320be9b9f333e3f8f4dad92" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(79 0)" fill="#000000" data-prov="5bc064cf9ed41d380fddb186477aeb78" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(80 0)" fill="#000000" data-prov="21e0c4e7089e613815b7a5d6bf2c103f" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(81 0)" fill="#000000" data-prov="8e06e0b817d3359c9e251afed6d58cee" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(82 0)" fill="#000000" data-prov="d9c5b62c4340d6bd9038a1a8ff081b3a" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(83 0)" fill="#000000" data-prov="86405f19f3e47252c21d628ae33e94c2" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.564 0.724C0.556 0.724 0.552 0.72 0.548 0.72C0.548 0.72 0.292 0.628 0.188 0.628C0.164 0.628 0.148 0.632 0.148 0.648V1.316C0.148 1.344 0.124 1.364 0.1 1.364H0.048C0.02 1.364 0 1.344 0 1.316V-0.744C0 -0.768 0.012 -0.78 0.036 -0.78L0.044 -0.776C0.048 -0.776 0.056 -0.776 0.06 -0.772C0.116 -0.748 0.34 -0.652 0.456 -0.652C0.496 -0.652 0.524 -0.664 0.524 -0.696V-1.292C0.524 -1.32 0.544 -1.34 0.572 -1.34H0.624C0.648 -1.34 0.672 -1.32 0.672 -1.292V0.716C0.672 0.736 0.656 0.748 0.64 0.748C0.636 0.748 0.628 0.748 0.624 0.744ZM0.148 0.156C0.148 0.212 0.392 0.316 0.488 0.316C0.512 0.316 0.524 0.312 0.524 0.296V-0.116C0.524 -0.188 0.296 -0.28 0.196 -0.28C0.168 -0.28 0.148 -0.272 0.148 -0.256Z" transform="translate(84 0)" fill="#000000" data-prov="913e510f5e3d36b6ab5a21a0bd531816" data-source-kind="9" data-glyph="accidentalNatural" data-class="accidental"/>
|
||||
<path d="M0.564 0.724C0.556 0.724 0.552 0.72 0.548 0.72C0.548 0.72 0.292 0.628 0.188 0.628C0.164 0.628 0.148 0.632 0.148 0.648V1.316C0.148 1.344 0.124 1.364 0.1 1.364H0.048C0.02 1.364 0 1.344 0 1.316V-0.744C0 -0.768 0.012 -0.78 0.036 -0.78L0.044 -0.776C0.048 -0.776 0.056 -0.776 0.06 -0.772C0.116 -0.748 0.34 -0.652 0.456 -0.652C0.496 -0.652 0.524 -0.664 0.524 -0.696V-1.292C0.524 -1.32 0.544 -1.34 0.572 -1.34H0.624C0.648 -1.34 0.672 -1.32 0.672 -1.292V0.716C0.672 0.736 0.656 0.748 0.64 0.748C0.636 0.748 0.628 0.748 0.624 0.744ZM0.148 0.156C0.148 0.212 0.392 0.316 0.488 0.316C0.512 0.316 0.524 0.312 0.524 0.296V-0.116C0.524 -0.188 0.296 -0.28 0.196 -0.28C0.168 -0.28 0.148 -0.272 0.148 -0.256Z" transform="translate(85 0)" fill="#000000" data-prov="e8f4fdf6d757a83af167a7d7b0d268b9" data-source-kind="9" data-glyph="accidentalNatural" data-class="accidental"/>
|
||||
<path d="M0.564 0.724C0.556 0.724 0.552 0.72 0.548 0.72C0.548 0.72 0.292 0.628 0.188 0.628C0.164 0.628 0.148 0.632 0.148 0.648V1.316C0.148 1.344 0.124 1.364 0.1 1.364H0.048C0.02 1.364 0 1.344 0 1.316V-0.744C0 -0.768 0.012 -0.78 0.036 -0.78L0.044 -0.776C0.048 -0.776 0.056 -0.776 0.06 -0.772C0.116 -0.748 0.34 -0.652 0.456 -0.652C0.496 -0.652 0.524 -0.664 0.524 -0.696V-1.292C0.524 -1.32 0.544 -1.34 0.572 -1.34H0.624C0.648 -1.34 0.672 -1.32 0.672 -1.292V0.716C0.672 0.736 0.656 0.748 0.64 0.748C0.636 0.748 0.628 0.748 0.624 0.744ZM0.148 0.156C0.148 0.212 0.392 0.316 0.488 0.316C0.512 0.316 0.524 0.312 0.524 0.296V-0.116C0.524 -0.188 0.296 -0.28 0.196 -0.28C0.168 -0.28 0.148 -0.272 0.148 -0.256Z" transform="translate(86 0)" fill="#000000" data-prov="aa6e5be72b5517b089a04363f42a1d1c" data-source-kind="9" data-glyph="accidentalNatural" data-class="accidental"/>
|
||||
<path d="M0.564 0.724C0.556 0.724 0.552 0.72 0.548 0.72C0.548 0.72 0.292 0.628 0.188 0.628C0.164 0.628 0.148 0.632 0.148 0.648V1.316C0.148 1.344 0.124 1.364 0.1 1.364H0.048C0.02 1.364 0 1.344 0 1.316V-0.744C0 -0.768 0.012 -0.78 0.036 -0.78L0.044 -0.776C0.048 -0.776 0.056 -0.776 0.06 -0.772C0.116 -0.748 0.34 -0.652 0.456 -0.652C0.496 -0.652 0.524 -0.664 0.524 -0.696V-1.292C0.524 -1.32 0.544 -1.34 0.572 -1.34H0.624C0.648 -1.34 0.672 -1.32 0.672 -1.292V0.716C0.672 0.736 0.656 0.748 0.64 0.748C0.636 0.748 0.628 0.748 0.624 0.744ZM0.148 0.156C0.148 0.212 0.392 0.316 0.488 0.316C0.512 0.316 0.524 0.312 0.524 0.296V-0.116C0.524 -0.188 0.296 -0.28 0.196 -0.28C0.168 -0.28 0.148 -0.272 0.148 -0.256Z" transform="translate(87 0)" fill="#000000" data-prov="ee1eea474c5d0d997d284a2610206922" data-source-kind="9" data-glyph="accidentalNatural" data-class="accidental"/>
|
||||
<path d="M0.564 0.724C0.556 0.724 0.552 0.72 0.548 0.72C0.548 0.72 0.292 0.628 0.188 0.628C0.164 0.628 0.148 0.632 0.148 0.648V1.316C0.148 1.344 0.124 1.364 0.1 1.364H0.048C0.02 1.364 0 1.344 0 1.316V-0.744C0 -0.768 0.012 -0.78 0.036 -0.78L0.044 -0.776C0.048 -0.776 0.056 -0.776 0.06 -0.772C0.116 -0.748 0.34 -0.652 0.456 -0.652C0.496 -0.652 0.524 -0.664 0.524 -0.696V-1.292C0.524 -1.32 0.544 -1.34 0.572 -1.34H0.624C0.648 -1.34 0.672 -1.32 0.672 -1.292V0.716C0.672 0.736 0.656 0.748 0.64 0.748C0.636 0.748 0.628 0.748 0.624 0.744ZM0.148 0.156C0.148 0.212 0.392 0.316 0.488 0.316C0.512 0.316 0.524 0.312 0.524 0.296V-0.116C0.524 -0.188 0.296 -0.28 0.196 -0.28C0.168 -0.28 0.148 -0.272 0.148 -0.256Z" transform="translate(88 0)" fill="#000000" data-prov="a353d60fd103f7a61e55a2b8aa95114e" data-source-kind="9" data-glyph="accidentalNatural" data-class="accidental"/>
|
||||
<path d="M0.564 0.724C0.556 0.724 0.552 0.72 0.548 0.72C0.548 0.72 0.292 0.628 0.188 0.628C0.164 0.628 0.148 0.632 0.148 0.648V1.316C0.148 1.344 0.124 1.364 0.1 1.364H0.048C0.02 1.364 0 1.344 0 1.316V-0.744C0 -0.768 0.012 -0.78 0.036 -0.78L0.044 -0.776C0.048 -0.776 0.056 -0.776 0.06 -0.772C0.116 -0.748 0.34 -0.652 0.456 -0.652C0.496 -0.652 0.524 -0.664 0.524 -0.696V-1.292C0.524 -1.32 0.544 -1.34 0.572 -1.34H0.624C0.648 -1.34 0.672 -1.32 0.672 -1.292V0.716C0.672 0.736 0.656 0.748 0.64 0.748C0.636 0.748 0.628 0.748 0.624 0.744ZM0.148 0.156C0.148 0.212 0.392 0.316 0.488 0.316C0.512 0.316 0.524 0.312 0.524 0.296V-0.116C0.524 -0.188 0.296 -0.28 0.196 -0.28C0.168 -0.28 0.148 -0.272 0.148 -0.256Z" transform="translate(89 0)" fill="#000000" data-prov="55d172d9d863190758f311e8b5c69c9b" data-source-kind="9" data-glyph="accidentalNatural" data-class="accidental"/>
|
||||
<path d="M0.564 0.724C0.556 0.724 0.552 0.72 0.548 0.72C0.548 0.72 0.292 0.628 0.188 0.628C0.164 0.628 0.148 0.632 0.148 0.648V1.316C0.148 1.344 0.124 1.364 0.1 1.364H0.048C0.02 1.364 0 1.344 0 1.316V-0.744C0 -0.768 0.012 -0.78 0.036 -0.78L0.044 -0.776C0.048 -0.776 0.056 -0.776 0.06 -0.772C0.116 -0.748 0.34 -0.652 0.456 -0.652C0.496 -0.652 0.524 -0.664 0.524 -0.696V-1.292C0.524 -1.32 0.544 -1.34 0.572 -1.34H0.624C0.648 -1.34 0.672 -1.32 0.672 -1.292V0.716C0.672 0.736 0.656 0.748 0.64 0.748C0.636 0.748 0.628 0.748 0.624 0.744ZM0.148 0.156C0.148 0.212 0.392 0.316 0.488 0.316C0.512 0.316 0.524 0.312 0.524 0.296V-0.116C0.524 -0.188 0.296 -0.28 0.196 -0.28C0.168 -0.28 0.148 -0.272 0.148 -0.256Z" transform="translate(90 0)" fill="#000000" data-prov="7552f781fca3b72788a57822d143ee69" data-source-kind="9" data-glyph="accidentalNatural" data-class="accidental"/>
|
||||
<path d="M0.564 0.724C0.556 0.724 0.552 0.72 0.548 0.72C0.548 0.72 0.292 0.628 0.188 0.628C0.164 0.628 0.148 0.632 0.148 0.648V1.316C0.148 1.344 0.124 1.364 0.1 1.364H0.048C0.02 1.364 0 1.344 0 1.316V-0.744C0 -0.768 0.012 -0.78 0.036 -0.78L0.044 -0.776C0.048 -0.776 0.056 -0.776 0.06 -0.772C0.116 -0.748 0.34 -0.652 0.456 -0.652C0.496 -0.652 0.524 -0.664 0.524 -0.696V-1.292C0.524 -1.32 0.544 -1.34 0.572 -1.34H0.624C0.648 -1.34 0.672 -1.32 0.672 -1.292V0.716C0.672 0.736 0.656 0.748 0.64 0.748C0.636 0.748 0.628 0.748 0.624 0.744ZM0.148 0.156C0.148 0.212 0.392 0.316 0.488 0.316C0.512 0.316 0.524 0.312 0.524 0.296V-0.116C0.524 -0.188 0.296 -0.28 0.196 -0.28C0.168 -0.28 0.148 -0.272 0.148 -0.256Z" transform="translate(91 0)" fill="#000000" data-prov="4e04c5515de885c96a377123738e0397" data-source-kind="9" data-glyph="accidentalNatural" data-class="accidental"/>
|
||||
<path d="M0.564 0.724C0.556 0.724 0.552 0.72 0.548 0.72C0.548 0.72 0.292 0.628 0.188 0.628C0.164 0.628 0.148 0.632 0.148 0.648V1.316C0.148 1.344 0.124 1.364 0.1 1.364H0.048C0.02 1.364 0 1.344 0 1.316V-0.744C0 -0.768 0.012 -0.78 0.036 -0.78L0.044 -0.776C0.048 -0.776 0.056 -0.776 0.06 -0.772C0.116 -0.748 0.34 -0.652 0.456 -0.652C0.496 -0.652 0.524 -0.664 0.524 -0.696V-1.292C0.524 -1.32 0.544 -1.34 0.572 -1.34H0.624C0.648 -1.34 0.672 -1.32 0.672 -1.292V0.716C0.672 0.736 0.656 0.748 0.64 0.748C0.636 0.748 0.628 0.748 0.624 0.744ZM0.148 0.156C0.148 0.212 0.392 0.316 0.488 0.316C0.512 0.316 0.524 0.312 0.524 0.296V-0.116C0.524 -0.188 0.296 -0.28 0.196 -0.28C0.168 -0.28 0.148 -0.272 0.148 -0.256Z" transform="translate(92 0)" fill="#000000" data-prov="65d223b9dca9579949a3a134b8d06182" data-source-kind="9" data-glyph="accidentalNatural" data-class="accidental"/>
|
||||
<path d="M0.564 0.724C0.556 0.724 0.552 0.72 0.548 0.72C0.548 0.72 0.292 0.628 0.188 0.628C0.164 0.628 0.148 0.632 0.148 0.648V1.316C0.148 1.344 0.124 1.364 0.1 1.364H0.048C0.02 1.364 0 1.344 0 1.316V-0.744C0 -0.768 0.012 -0.78 0.036 -0.78L0.044 -0.776C0.048 -0.776 0.056 -0.776 0.06 -0.772C0.116 -0.748 0.34 -0.652 0.456 -0.652C0.496 -0.652 0.524 -0.664 0.524 -0.696V-1.292C0.524 -1.32 0.544 -1.34 0.572 -1.34H0.624C0.648 -1.34 0.672 -1.32 0.672 -1.292V0.716C0.672 0.736 0.656 0.748 0.64 0.748C0.636 0.748 0.628 0.748 0.624 0.744ZM0.148 0.156C0.148 0.212 0.392 0.316 0.488 0.316C0.512 0.316 0.524 0.312 0.524 0.296V-0.116C0.524 -0.188 0.296 -0.28 0.196 -0.28C0.168 -0.28 0.148 -0.272 0.148 -0.256Z" transform="translate(93 0)" fill="#000000" data-prov="8f49273e29ce144556b73f0e914cf377" data-source-kind="9" data-glyph="accidentalNatural" data-class="accidental"/>
|
||||
<path d="M1.128 0.096V0.464C1.128 0.524 1.08 0.568 1.024 0.568H0.104C0.044 0.568 0 0.524 0 0.464V0.096C0 0.04 0.044 -0.008 0.104 -0.008H1.024C1.08 -0.008 1.128 0.04 1.128 0.096Z" transform="translate(94 0)" fill="#000000" data-prov="ad675e7f174a140139e2959e3ebdc8ad" data-source-kind="12" data-glyph="restHalf" data-class="rest"/>
|
||||
<path d="M0.536 0.428C0.536 0.576 0.416 0.696 0.268 0.696C0.12 0.696 0 0.576 0 0.428C0 0.344 0.048 0.272 0.108 0.224C0.172 0.18 0.248 0.156 0.324 0.156C0.38 0.156 0.436 0.168 0.48 0.184C0.536 0.2 0.572 0.216 0.624 0.244C0.632 0.248 0.64 0.248 0.644 0.248C0.66 0.248 0.664 0.232 0.664 0.212C0.664 0.2 0.664 0.184 0.66 0.168C0.648 0.108 0.36 -0.688 0.288 -0.952C0.288 -1 0.38 -1.004 0.404 -1.004C0.448 -1.004 0.504 -0.996 0.544 -0.964C0.556 -0.956 0.948 0.448 0.948 0.448C0.964 0.52 0.984 0.584 0.988 0.604C0.988 0.644 0.948 0.664 0.94 0.668C0.932 0.668 0.92 0.668 0.896 0.652C0.868 0.628 0.668 0.388 0.536 0.388Z" transform="translate(95 0)" fill="#000000" data-prov="14cf3b9c808d0467e40ee355ef4329a8" data-source-kind="14" data-glyph="rest8th" data-class="rest"/>
|
||||
<path d="M0.952 -3.16C0.952 -3.16 1.056 -2.78 1.056 -2.468C1.056 -1.968 0.848 -1.496 0.596 -1.096C0.392 -0.78 0.224 -0.436 0.16 -0.052C0.148 0.012 0.116 0.036 0.076 0.036C0.032 0.036 0 0.024 0 -0.024V-0.98C0.264 -1.028 0.644 -1.572 0.788 -1.912C0.848 -2.048 0.884 -2.276 0.884 -2.512C0.884 -2.692 0.856 -2.88 0.788 -3.06C0.78 -3.084 0.776 -3.104 0.776 -3.12C0.776 -3.184 0.816 -3.22 0.84 -3.236C0.864 -3.252 0.932 -3.228 0.952 -3.16Z" transform="translate(96 0)" fill="#000000" data-prov="82ca9c2a2208e28338167ec9151748d2" data-source-kind="15" data-glyph="flag8thUp" data-class="flag"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(97 0)" fill="#000000" data-prov="33b51b69dbfc551be0448fbb257675b2" data-source-kind="25" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 50 KiB |
|
|
@ -0,0 +1,16 @@
|
|||
fixture=valid_score_rich solver=stub
|
||||
glyph_count=32
|
||||
path_count=32
|
||||
fallback_rect_count=0
|
||||
provenance_count=32
|
||||
layer_count=1
|
||||
hard_constraint_count=0
|
||||
xml_well_formed=true
|
||||
view_box=[-2 -5.2408 36.18 11.6328]
|
||||
class_counts:
|
||||
accidental=1
|
||||
clef=6
|
||||
dynamic=1
|
||||
flag=1
|
||||
notehead=21
|
||||
rest=2
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="361.8" height="116.328" viewBox="0 0 36.18 11.6328">
|
||||
<!-- epiphany-render-svg: glyphs are genuine Bravura SMuFL outlines; geometry is the resolved layout verbatim, no engraving performed here -->
|
||||
<g transform="translate(2 6.392) scale(1 -1)">
|
||||
<g data-layer="0">
|
||||
<path d="M0.92 1.928C0.92 1.984 0.892 2.012 0.836 2.012H0.832C0.776 2.012 0.748 1.984 0.748 1.928V-1.928C0.748 -1.984 0.776 -2.012 0.832 -2.012H0.836C0.892 -2.012 0.92 -1.984 0.92 -1.928V-0.176C0.92 -0.144 0.94 -0.148 0.956 -0.152C1.06 -0.18 1.228 -0.284 1.312 -0.736C1.324 -0.8 1.348 -0.836 1.388 -0.836C1.432 -0.836 1.452 -0.796 1.472 -0.728C1.524 -0.552 1.616 -0.356 1.9 -0.356C2.16 -0.356 2.232 -0.612 2.232 -1.136C2.232 -1.66 2.14 -1.896 1.808 -1.896C1.752 -1.896 1.468 -1.872 1.468 -1.788C1.468 -1.768 1.532 -1.744 1.576 -1.728C1.656 -1.7 1.736 -1.62 1.736 -1.468C1.736 -1.292 1.62 -1.192 1.464 -1.192C1.292 -1.192 1.156 -1.308 1.156 -1.52C1.156 -1.772 1.376 -2.024 1.852 -2.024C2.508 -2.024 2.796 -1.564 2.796 -1.148C2.796 -0.596 2.492 -0.212 1.96 -0.212C1.844 -0.212 1.768 -0.232 1.716 -0.248C1.676 -0.26 1.636 -0.268 1.6 -0.244C1.544 -0.208 1.456 -0.08 1.456 0C1.456 0.08 1.544 0.208 1.6 0.244C1.636 0.268 1.676 0.26 1.716 0.248C1.768 0.232 1.844 0.212 1.96 0.212C2.492 0.212 2.796 0.596 2.796 1.148C2.796 1.564 2.508 2.024 1.852 2.024C1.376 2.024 1.156 1.772 1.156 1.52C1.156 1.308 1.292 1.192 1.464 1.192C1.62 1.192 1.736 1.292 1.736 1.468C1.736 1.62 1.656 1.7 1.576 1.728C1.532 1.744 1.468 1.768 1.468 1.788C1.468 1.872 1.752 1.896 1.808 1.896C2.14 1.896 2.232 1.66 2.232 1.136C2.232 0.612 2.16 0.356 1.9 0.356C1.616 0.356 1.524 0.552 1.472 0.728C1.452 0.796 1.432 0.836 1.388 0.836C1.348 0.836 1.324 0.8 1.312 0.736C1.228 0.284 1.06 0.18 0.956 0.152C0.94 0.148 0.92 0.144 0.92 0.176ZM0.084 2.012C0.028 2.012 0 1.984 0 1.928V-1.928C0 -1.984 0.028 -2.012 0.084 -2.012H0.428C0.484 -2.012 0.512 -1.984 0.512 -1.928V1.928C0.512 1.984 0.484 2.012 0.428 2.012Z" transform="translate(0 0)" fill="#000000" data-prov="bd56ad529a36a7bbf2b4c343886b55e5" data-source-kind="6" data-glyph="cClef" data-class="clef"/>
|
||||
<path d="M0.036 0.62C0.012 0.62 0 0.604 0 0.588V-0.592C0 -0.608 0.012 -0.62 0.036 -0.62H0.088C0.108 -0.62 0.124 -0.608 0.124 -0.592V0.588C0.124 0.604 0.108 0.62 0.088 0.62ZM0.268 0.62C0.244 0.62 0.228 0.604 0.228 0.588V-0.592C0.228 -0.608 0.244 -0.62 0.268 -0.62H0.316C0.336 -0.62 0.356 -0.608 0.356 -0.592V0.588C0.356 0.604 0.336 0.62 0.316 0.62ZM2.076 0.62C2.056 0.62 2.036 0.604 2.036 0.588V-0.592C2.036 -0.608 2.056 -0.62 2.076 -0.62H2.128C2.148 -0.62 2.168 -0.608 2.168 -0.592V0.588C2.168 0.604 2.148 0.62 2.128 0.62ZM2.304 0.62C2.288 0.62 2.264 0.604 2.264 0.588V-0.592C2.264 -0.608 2.288 -0.62 2.304 -0.62H2.356C2.38 -0.62 2.396 -0.608 2.396 -0.592V0.588C2.396 0.604 2.38 0.62 2.356 0.62ZM1.216 0.5C0.688 0.5 0.36 0.28 0.36 0.004C0.36 -0.256 0.584 -0.5 1.176 -0.5C1.824 -0.5 2.036 -0.272 2.036 0.004C2.036 0.288 1.588 0.5 1.216 0.5ZM1.304 -0.404C1.136 -0.404 1.024 -0.312 0.92 -0.196C0.848 -0.104 0.788 0.032 0.788 0.16C0.788 0.364 0.924 0.408 1.112 0.408C1.384 0.408 1.604 0.116 1.604 -0.124C1.604 -0.32 1.48 -0.404 1.304 -0.404Z" transform="translate(1 0)" fill="#000000" data-prov="f6b4640bbb87f4c52d2e8bb00667c1c4" data-source-kind="3" data-glyph="noteheadDoubleWhole" data-class="notehead"/>
|
||||
<path d="M1.504 1.66C1.496 1.708 1.504 1.712 1.528 1.736C1.96 2.14 2.288 2.648 2.288 3.26C2.288 3.608 2.192 3.952 2.028 4.192C1.968 4.28 1.864 4.392 1.82 4.392C1.764 4.392 1.64 4.288 1.56 4.2C1.264 3.872 1.168 3.372 1.168 2.956C1.168 2.724 1.196 2.464 1.224 2.3C1.232 2.252 1.236 2.244 1.188 2.204C0.612 1.728 0 1.156 0 0.348C0 -0.348 0.476 -1.008 1.456 -1.008C1.548 -1.008 1.652 -1 1.732 -0.984C1.776 -0.976 1.784 -0.972 1.792 -1.02C1.84 -1.288 1.9 -1.636 1.9 -1.824C1.9 -2.416 1.5 -2.488 1.264 -2.488C1.048 -2.488 0.944 -2.424 0.944 -2.372C0.944 -2.344 0.98 -2.332 1.072 -2.304C1.196 -2.268 1.34 -2.16 1.34 -1.928C1.34 -1.708 1.2 -1.52 0.956 -1.52C0.688 -1.52 0.528 -1.732 0.528 -1.98C0.528 -2.24 0.684 -2.632 1.288 -2.632C1.556 -2.632 2.076 -2.512 2.076 -1.832C2.076 -1.604 2.004 -1.224 1.96 -0.976C1.952 -0.928 1.956 -0.932 2.012 -0.908C2.416 -0.748 2.684 -0.408 2.684 0.044C2.684 0.556 2.308 1.008 1.72 1.008C1.616 1.008 1.616 1.008 1.604 1.08ZM1.88 3.772C2.012 3.772 2.12 3.664 2.12 3.444C2.12 3 1.74 2.64 1.424 2.364C1.396 2.34 1.38 2.344 1.372 2.396C1.356 2.5 1.348 2.636 1.348 2.764C1.348 3.388 1.636 3.772 1.88 3.772ZM1.444 1.048C1.456 0.972 1.456 0.976 1.384 0.952C1.032 0.832 0.804 0.516 0.804 0.176C0.804 -0.184 0.992 -0.44 1.264 -0.532C1.296 -0.544 1.344 -0.556 1.372 -0.556C1.404 -0.556 1.42 -0.536 1.42 -0.512C1.42 -0.484 1.388 -0.472 1.36 -0.46C1.192 -0.388 1.072 -0.216 1.072 -0.032C1.072 0.196 1.228 0.368 1.472 0.436C1.536 0.452 1.544 0.448 1.552 0.404L1.752 -0.788C1.76 -0.832 1.756 -0.832 1.696 -0.844C1.632 -0.856 1.552 -0.864 1.472 -0.864C0.772 -0.864 0.32 -0.476 0.32 0.08C0.32 0.316 0.36 0.632 0.692 1.008C0.932 1.276 1.116 1.424 1.304 1.576C1.344 1.608 1.352 1.604 1.36 1.56ZM1.72 0.412C1.712 0.46 1.716 0.472 1.764 0.468C2.088 0.44 2.356 0.168 2.356 -0.184C2.356 -0.436 2.204 -0.64 1.98 -0.752C1.932 -0.776 1.924 -0.776 1.916 -0.728Z" transform="translate(2 0)" fill="#000000" data-prov="31186a3ef9c061378bfd5da443b88aa8" data-source-kind="4" data-glyph="gClef" data-class="clef"/>
|
||||
<path d="M0.864 0.5C0.332 0.5 0 0.28 0 0.008C0 -0.26 0.228 -0.5 0.824 -0.5C1.48 -0.5 1.688 -0.272 1.688 0.008C1.688 0.292 1.236 0.5 0.864 0.5ZM0.444 0.252C0.488 0.392 0.636 0.412 0.76 0.412C1.036 0.412 1.256 0.116 1.256 -0.124C1.256 -0.248 1.204 -0.36 1.072 -0.392C1.032 -0.404 0.988 -0.408 0.948 -0.408C0.804 -0.408 0.656 -0.312 0.572 -0.2C0.492 -0.108 0.432 0.028 0.432 0.156C0.432 0.188 0.436 0.22 0.444 0.252Z" transform="translate(3 0)" fill="#000000" data-prov="c9e2f7b92061893043380eabfe8fbcd2" data-source-kind="2" data-glyph="noteheadWhole" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(4 0)" fill="#000000" data-prov="fb2b9c85e73e553a05ccc47d55af4811" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(5 0)" fill="#000000" data-prov="1c92743d8b3afa333dde4fc1bbe503ba" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(6 0)" fill="#000000" data-prov="109fe59f980a1b9bfa00a76e41b78dba" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(7 0)" fill="#000000" data-prov="56a954e9547cffd75e82fdad63afb684" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(8 0)" fill="#000000" data-prov="d4ef3689df957acec175a1396cebd27c" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(9 0)" fill="#000000" data-prov="290c3f76bff74030da13f651df94fd4b" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.564 0.724C0.556 0.724 0.552 0.72 0.548 0.72C0.548 0.72 0.292 0.628 0.188 0.628C0.164 0.628 0.148 0.632 0.148 0.648V1.316C0.148 1.344 0.124 1.364 0.1 1.364H0.048C0.02 1.364 0 1.344 0 1.316V-0.744C0 -0.768 0.012 -0.78 0.036 -0.78L0.044 -0.776C0.048 -0.776 0.056 -0.776 0.06 -0.772C0.116 -0.748 0.34 -0.652 0.456 -0.652C0.496 -0.652 0.524 -0.664 0.524 -0.696V-1.292C0.524 -1.32 0.544 -1.34 0.572 -1.34H0.624C0.648 -1.34 0.672 -1.32 0.672 -1.292V0.716C0.672 0.736 0.656 0.748 0.64 0.748C0.636 0.748 0.628 0.748 0.624 0.744ZM0.148 0.156C0.148 0.212 0.392 0.316 0.488 0.316C0.512 0.316 0.524 0.312 0.524 0.296V-0.116C0.524 -0.188 0.296 -0.28 0.196 -0.28C0.168 -0.28 0.148 -0.272 0.148 -0.256Z" transform="translate(10 0)" fill="#000000" data-prov="3111f6a4a6bc3100a78fa6f04b2e2f86" data-source-kind="9" data-glyph="accidentalNatural" data-class="accidental"/>
|
||||
<path d="M1.128 0.096V0.464C1.128 0.524 1.08 0.568 1.024 0.568H0.104C0.044 0.568 0 0.524 0 0.464V0.096C0 0.04 0.044 -0.008 0.104 -0.008H1.024C1.08 -0.008 1.128 0.04 1.128 0.096Z" transform="translate(11 0)" fill="#000000" data-prov="a647c465b9e490a52c23029a2c906cda" data-source-kind="12" data-glyph="restHalf" data-class="rest"/>
|
||||
<path d="M0.064 1.056C0.02 1.056 0 1.036 0 0.992C0 0.952 0.02 0.932 0.06 0.932H0.292C0.316 0.932 0.324 0.932 0.324 0.916C0.324 0.908 0.32 0.896 0.316 0.876L0.064 0C-0.048 -0.392 -0.12 -0.52 -0.28 -0.52C-0.332 -0.52 -0.352 -0.504 -0.352 -0.484C-0.352 -0.452 -0.316 -0.472 -0.256 -0.432C-0.208 -0.4 -0.176 -0.348 -0.176 -0.284C-0.176 -0.18 -0.248 -0.12 -0.356 -0.12C-0.476 -0.12 -0.564 -0.216 -0.564 -0.34C-0.564 -0.516 -0.432 -0.608 -0.264 -0.608C0.04 -0.608 0.228 -0.404 0.448 0.048C0.564 0.292 0.652 0.54 0.748 0.88L0.752 0.884C0.752 0.896 0.784 0.932 0.804 0.932H1.064C1.108 0.932 1.128 0.952 1.128 0.996C1.128 1.036 1.108 1.056 1.068 1.056H0.828C0.8 1.056 0.788 1.056 0.788 1.076C0.788 1.088 0.792 1.1 0.796 1.124C0.876 1.472 0.964 1.684 1.164 1.684C1.196 1.684 1.228 1.676 1.228 1.652C1.228 1.628 1.208 1.632 1.164 1.612C1.112 1.588 1.08 1.532 1.08 1.46C1.08 1.348 1.16 1.292 1.26 1.292C1.364 1.292 1.456 1.36 1.456 1.508C1.456 1.656 1.364 1.776 1.112 1.776C0.724 1.776 0.508 1.5 0.376 1.116C0.356 1.056 0.352 1.056 0.296 1.056Z" transform="translate(12 0)" fill="#000000" data-prov="8ed9d2ef46e84abf412a1b82e125d3a6" data-source-kind="22" data-glyph="dynamicForte" data-class="dynamic"/>
|
||||
<path d="M0.536 0.428C0.536 0.576 0.416 0.696 0.268 0.696C0.12 0.696 0 0.576 0 0.428C0 0.344 0.048 0.272 0.108 0.224C0.172 0.18 0.248 0.156 0.324 0.156C0.38 0.156 0.436 0.168 0.48 0.184C0.536 0.2 0.572 0.216 0.624 0.244C0.632 0.248 0.64 0.248 0.644 0.248C0.66 0.248 0.664 0.232 0.664 0.212C0.664 0.2 0.664 0.184 0.66 0.168C0.648 0.108 0.36 -0.688 0.288 -0.952C0.288 -1 0.38 -1.004 0.404 -1.004C0.448 -1.004 0.504 -0.996 0.544 -0.964C0.556 -0.956 0.948 0.448 0.948 0.448C0.964 0.52 0.984 0.584 0.988 0.604C0.988 0.644 0.948 0.664 0.94 0.668C0.932 0.668 0.92 0.668 0.896 0.652C0.868 0.628 0.668 0.388 0.536 0.388Z" transform="translate(13 0)" fill="#000000" data-prov="7b42d513243366bc1409bbb4cf418fac" data-source-kind="14" data-glyph="rest8th" data-class="rest"/>
|
||||
<path d="M0.952 -3.16C0.952 -3.16 1.056 -2.78 1.056 -2.468C1.056 -1.968 0.848 -1.496 0.596 -1.096C0.392 -0.78 0.224 -0.436 0.16 -0.052C0.148 0.012 0.116 0.036 0.076 0.036C0.032 0.036 0 0.024 0 -0.024V-0.98C0.264 -1.028 0.644 -1.572 0.788 -1.912C0.848 -2.048 0.884 -2.276 0.884 -2.512C0.884 -2.692 0.856 -2.88 0.788 -3.06C0.78 -3.084 0.776 -3.104 0.776 -3.12C0.776 -3.184 0.816 -3.22 0.84 -3.236C0.864 -3.252 0.932 -3.228 0.952 -3.16Z" transform="translate(14 0)" fill="#000000" data-prov="c8ad1fa9386570a1f3e49ccbf43b17f8" data-source-kind="15" data-glyph="flag8thUp" data-class="flag"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(15 0)" fill="#000000" data-prov="9f8b66aae47123317fae42a521a8ea1a" data-source-kind="25" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.92 1.928C0.92 1.984 0.892 2.012 0.836 2.012H0.832C0.776 2.012 0.748 1.984 0.748 1.928V-1.928C0.748 -1.984 0.776 -2.012 0.832 -2.012H0.836C0.892 -2.012 0.92 -1.984 0.92 -1.928V-0.176C0.92 -0.144 0.94 -0.148 0.956 -0.152C1.06 -0.18 1.228 -0.284 1.312 -0.736C1.324 -0.8 1.348 -0.836 1.388 -0.836C1.432 -0.836 1.452 -0.796 1.472 -0.728C1.524 -0.552 1.616 -0.356 1.9 -0.356C2.16 -0.356 2.232 -0.612 2.232 -1.136C2.232 -1.66 2.14 -1.896 1.808 -1.896C1.752 -1.896 1.468 -1.872 1.468 -1.788C1.468 -1.768 1.532 -1.744 1.576 -1.728C1.656 -1.7 1.736 -1.62 1.736 -1.468C1.736 -1.292 1.62 -1.192 1.464 -1.192C1.292 -1.192 1.156 -1.308 1.156 -1.52C1.156 -1.772 1.376 -2.024 1.852 -2.024C2.508 -2.024 2.796 -1.564 2.796 -1.148C2.796 -0.596 2.492 -0.212 1.96 -0.212C1.844 -0.212 1.768 -0.232 1.716 -0.248C1.676 -0.26 1.636 -0.268 1.6 -0.244C1.544 -0.208 1.456 -0.08 1.456 0C1.456 0.08 1.544 0.208 1.6 0.244C1.636 0.268 1.676 0.26 1.716 0.248C1.768 0.232 1.844 0.212 1.96 0.212C2.492 0.212 2.796 0.596 2.796 1.148C2.796 1.564 2.508 2.024 1.852 2.024C1.376 2.024 1.156 1.772 1.156 1.52C1.156 1.308 1.292 1.192 1.464 1.192C1.62 1.192 1.736 1.292 1.736 1.468C1.736 1.62 1.656 1.7 1.576 1.728C1.532 1.744 1.468 1.768 1.468 1.788C1.468 1.872 1.752 1.896 1.808 1.896C2.14 1.896 2.232 1.66 2.232 1.136C2.232 0.612 2.16 0.356 1.9 0.356C1.616 0.356 1.524 0.552 1.472 0.728C1.452 0.796 1.432 0.836 1.388 0.836C1.348 0.836 1.324 0.8 1.312 0.736C1.228 0.284 1.06 0.18 0.956 0.152C0.94 0.148 0.92 0.144 0.92 0.176ZM0.084 2.012C0.028 2.012 0 1.984 0 1.928V-1.928C0 -1.984 0.028 -2.012 0.084 -2.012H0.428C0.484 -2.012 0.512 -1.984 0.512 -1.928V1.928C0.512 1.984 0.484 2.012 0.428 2.012Z" transform="translate(16 0)" fill="#000000" data-prov="311476ddcf5ddef3b76e9d807237a21a" data-source-kind="6" data-glyph="cClef" data-class="clef"/>
|
||||
<path d="M0.036 0.62C0.012 0.62 0 0.604 0 0.588V-0.592C0 -0.608 0.012 -0.62 0.036 -0.62H0.088C0.108 -0.62 0.124 -0.608 0.124 -0.592V0.588C0.124 0.604 0.108 0.62 0.088 0.62ZM0.268 0.62C0.244 0.62 0.228 0.604 0.228 0.588V-0.592C0.228 -0.608 0.244 -0.62 0.268 -0.62H0.316C0.336 -0.62 0.356 -0.608 0.356 -0.592V0.588C0.356 0.604 0.336 0.62 0.316 0.62ZM2.076 0.62C2.056 0.62 2.036 0.604 2.036 0.588V-0.592C2.036 -0.608 2.056 -0.62 2.076 -0.62H2.128C2.148 -0.62 2.168 -0.608 2.168 -0.592V0.588C2.168 0.604 2.148 0.62 2.128 0.62ZM2.304 0.62C2.288 0.62 2.264 0.604 2.264 0.588V-0.592C2.264 -0.608 2.288 -0.62 2.304 -0.62H2.356C2.38 -0.62 2.396 -0.608 2.396 -0.592V0.588C2.396 0.604 2.38 0.62 2.356 0.62ZM1.216 0.5C0.688 0.5 0.36 0.28 0.36 0.004C0.36 -0.256 0.584 -0.5 1.176 -0.5C1.824 -0.5 2.036 -0.272 2.036 0.004C2.036 0.288 1.588 0.5 1.216 0.5ZM1.304 -0.404C1.136 -0.404 1.024 -0.312 0.92 -0.196C0.848 -0.104 0.788 0.032 0.788 0.16C0.788 0.364 0.924 0.408 1.112 0.408C1.384 0.408 1.604 0.116 1.604 -0.124C1.604 -0.32 1.48 -0.404 1.304 -0.404Z" transform="translate(17 0)" fill="#000000" data-prov="ead033c623ec6fc4eab9fd3839b7caa9" data-source-kind="3" data-glyph="noteheadDoubleWhole" data-class="notehead"/>
|
||||
<path d="M1.504 1.66C1.496 1.708 1.504 1.712 1.528 1.736C1.96 2.14 2.288 2.648 2.288 3.26C2.288 3.608 2.192 3.952 2.028 4.192C1.968 4.28 1.864 4.392 1.82 4.392C1.764 4.392 1.64 4.288 1.56 4.2C1.264 3.872 1.168 3.372 1.168 2.956C1.168 2.724 1.196 2.464 1.224 2.3C1.232 2.252 1.236 2.244 1.188 2.204C0.612 1.728 0 1.156 0 0.348C0 -0.348 0.476 -1.008 1.456 -1.008C1.548 -1.008 1.652 -1 1.732 -0.984C1.776 -0.976 1.784 -0.972 1.792 -1.02C1.84 -1.288 1.9 -1.636 1.9 -1.824C1.9 -2.416 1.5 -2.488 1.264 -2.488C1.048 -2.488 0.944 -2.424 0.944 -2.372C0.944 -2.344 0.98 -2.332 1.072 -2.304C1.196 -2.268 1.34 -2.16 1.34 -1.928C1.34 -1.708 1.2 -1.52 0.956 -1.52C0.688 -1.52 0.528 -1.732 0.528 -1.98C0.528 -2.24 0.684 -2.632 1.288 -2.632C1.556 -2.632 2.076 -2.512 2.076 -1.832C2.076 -1.604 2.004 -1.224 1.96 -0.976C1.952 -0.928 1.956 -0.932 2.012 -0.908C2.416 -0.748 2.684 -0.408 2.684 0.044C2.684 0.556 2.308 1.008 1.72 1.008C1.616 1.008 1.616 1.008 1.604 1.08ZM1.88 3.772C2.012 3.772 2.12 3.664 2.12 3.444C2.12 3 1.74 2.64 1.424 2.364C1.396 2.34 1.38 2.344 1.372 2.396C1.356 2.5 1.348 2.636 1.348 2.764C1.348 3.388 1.636 3.772 1.88 3.772ZM1.444 1.048C1.456 0.972 1.456 0.976 1.384 0.952C1.032 0.832 0.804 0.516 0.804 0.176C0.804 -0.184 0.992 -0.44 1.264 -0.532C1.296 -0.544 1.344 -0.556 1.372 -0.556C1.404 -0.556 1.42 -0.536 1.42 -0.512C1.42 -0.484 1.388 -0.472 1.36 -0.46C1.192 -0.388 1.072 -0.216 1.072 -0.032C1.072 0.196 1.228 0.368 1.472 0.436C1.536 0.452 1.544 0.448 1.552 0.404L1.752 -0.788C1.76 -0.832 1.756 -0.832 1.696 -0.844C1.632 -0.856 1.552 -0.864 1.472 -0.864C0.772 -0.864 0.32 -0.476 0.32 0.08C0.32 0.316 0.36 0.632 0.692 1.008C0.932 1.276 1.116 1.424 1.304 1.576C1.344 1.608 1.352 1.604 1.36 1.56ZM1.72 0.412C1.712 0.46 1.716 0.472 1.764 0.468C2.088 0.44 2.356 0.168 2.356 -0.184C2.356 -0.436 2.204 -0.64 1.98 -0.752C1.932 -0.776 1.924 -0.776 1.916 -0.728Z" transform="translate(18 0)" fill="#000000" data-prov="5a3ada73e6c7dab552b6c44a6aa8f9b6" data-source-kind="4" data-glyph="gClef" data-class="clef"/>
|
||||
<path d="M0.864 0.5C0.332 0.5 0 0.28 0 0.008C0 -0.26 0.228 -0.5 0.824 -0.5C1.48 -0.5 1.688 -0.272 1.688 0.008C1.688 0.292 1.236 0.5 0.864 0.5ZM0.444 0.252C0.488 0.392 0.636 0.412 0.76 0.412C1.036 0.412 1.256 0.116 1.256 -0.124C1.256 -0.248 1.204 -0.36 1.072 -0.392C1.032 -0.404 0.988 -0.408 0.948 -0.408C0.804 -0.408 0.656 -0.312 0.572 -0.2C0.492 -0.108 0.432 0.028 0.432 0.156C0.432 0.188 0.436 0.22 0.444 0.252Z" transform="translate(19 0)" fill="#000000" data-prov="c36e7a12ef652f2f8e8cf771489cdcdc" data-source-kind="2" data-glyph="noteheadWhole" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(20 0)" fill="#000000" data-prov="e01815eeb89bc5fcd5157c32311ccf10" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(21 0)" fill="#000000" data-prov="23cf4d4c63aa6a23024a756ec4aabc6d" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(22 0)" fill="#000000" data-prov="119c53a4e3bf288ded2bd28178d6fcbe" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(23 0)" fill="#000000" data-prov="866d5e0219f737a3aa929bcff3d98833" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.92 1.928C0.92 1.984 0.892 2.012 0.836 2.012H0.832C0.776 2.012 0.748 1.984 0.748 1.928V-1.928C0.748 -1.984 0.776 -2.012 0.832 -2.012H0.836C0.892 -2.012 0.92 -1.984 0.92 -1.928V-0.176C0.92 -0.144 0.94 -0.148 0.956 -0.152C1.06 -0.18 1.228 -0.284 1.312 -0.736C1.324 -0.8 1.348 -0.836 1.388 -0.836C1.432 -0.836 1.452 -0.796 1.472 -0.728C1.524 -0.552 1.616 -0.356 1.9 -0.356C2.16 -0.356 2.232 -0.612 2.232 -1.136C2.232 -1.66 2.14 -1.896 1.808 -1.896C1.752 -1.896 1.468 -1.872 1.468 -1.788C1.468 -1.768 1.532 -1.744 1.576 -1.728C1.656 -1.7 1.736 -1.62 1.736 -1.468C1.736 -1.292 1.62 -1.192 1.464 -1.192C1.292 -1.192 1.156 -1.308 1.156 -1.52C1.156 -1.772 1.376 -2.024 1.852 -2.024C2.508 -2.024 2.796 -1.564 2.796 -1.148C2.796 -0.596 2.492 -0.212 1.96 -0.212C1.844 -0.212 1.768 -0.232 1.716 -0.248C1.676 -0.26 1.636 -0.268 1.6 -0.244C1.544 -0.208 1.456 -0.08 1.456 0C1.456 0.08 1.544 0.208 1.6 0.244C1.636 0.268 1.676 0.26 1.716 0.248C1.768 0.232 1.844 0.212 1.96 0.212C2.492 0.212 2.796 0.596 2.796 1.148C2.796 1.564 2.508 2.024 1.852 2.024C1.376 2.024 1.156 1.772 1.156 1.52C1.156 1.308 1.292 1.192 1.464 1.192C1.62 1.192 1.736 1.292 1.736 1.468C1.736 1.62 1.656 1.7 1.576 1.728C1.532 1.744 1.468 1.768 1.468 1.788C1.468 1.872 1.752 1.896 1.808 1.896C2.14 1.896 2.232 1.66 2.232 1.136C2.232 0.612 2.16 0.356 1.9 0.356C1.616 0.356 1.524 0.552 1.472 0.728C1.452 0.796 1.432 0.836 1.388 0.836C1.348 0.836 1.324 0.8 1.312 0.736C1.228 0.284 1.06 0.18 0.956 0.152C0.94 0.148 0.92 0.144 0.92 0.176ZM0.084 2.012C0.028 2.012 0 1.984 0 1.928V-1.928C0 -1.984 0.028 -2.012 0.084 -2.012H0.428C0.484 -2.012 0.512 -1.984 0.512 -1.928V1.928C0.512 1.984 0.484 2.012 0.428 2.012Z" transform="translate(24 0)" fill="#000000" data-prov="973e800421dc74ca8f5d1b5be644a433" data-source-kind="6" data-glyph="cClef" data-class="clef"/>
|
||||
<path d="M0.036 0.62C0.012 0.62 0 0.604 0 0.588V-0.592C0 -0.608 0.012 -0.62 0.036 -0.62H0.088C0.108 -0.62 0.124 -0.608 0.124 -0.592V0.588C0.124 0.604 0.108 0.62 0.088 0.62ZM0.268 0.62C0.244 0.62 0.228 0.604 0.228 0.588V-0.592C0.228 -0.608 0.244 -0.62 0.268 -0.62H0.316C0.336 -0.62 0.356 -0.608 0.356 -0.592V0.588C0.356 0.604 0.336 0.62 0.316 0.62ZM2.076 0.62C2.056 0.62 2.036 0.604 2.036 0.588V-0.592C2.036 -0.608 2.056 -0.62 2.076 -0.62H2.128C2.148 -0.62 2.168 -0.608 2.168 -0.592V0.588C2.168 0.604 2.148 0.62 2.128 0.62ZM2.304 0.62C2.288 0.62 2.264 0.604 2.264 0.588V-0.592C2.264 -0.608 2.288 -0.62 2.304 -0.62H2.356C2.38 -0.62 2.396 -0.608 2.396 -0.592V0.588C2.396 0.604 2.38 0.62 2.356 0.62ZM1.216 0.5C0.688 0.5 0.36 0.28 0.36 0.004C0.36 -0.256 0.584 -0.5 1.176 -0.5C1.824 -0.5 2.036 -0.272 2.036 0.004C2.036 0.288 1.588 0.5 1.216 0.5ZM1.304 -0.404C1.136 -0.404 1.024 -0.312 0.92 -0.196C0.848 -0.104 0.788 0.032 0.788 0.16C0.788 0.364 0.924 0.408 1.112 0.408C1.384 0.408 1.604 0.116 1.604 -0.124C1.604 -0.32 1.48 -0.404 1.304 -0.404Z" transform="translate(25 0)" fill="#000000" data-prov="5909bb98de8ef36fa41f0493dd500023" data-source-kind="3" data-glyph="noteheadDoubleWhole" data-class="notehead"/>
|
||||
<path d="M1.504 1.66C1.496 1.708 1.504 1.712 1.528 1.736C1.96 2.14 2.288 2.648 2.288 3.26C2.288 3.608 2.192 3.952 2.028 4.192C1.968 4.28 1.864 4.392 1.82 4.392C1.764 4.392 1.64 4.288 1.56 4.2C1.264 3.872 1.168 3.372 1.168 2.956C1.168 2.724 1.196 2.464 1.224 2.3C1.232 2.252 1.236 2.244 1.188 2.204C0.612 1.728 0 1.156 0 0.348C0 -0.348 0.476 -1.008 1.456 -1.008C1.548 -1.008 1.652 -1 1.732 -0.984C1.776 -0.976 1.784 -0.972 1.792 -1.02C1.84 -1.288 1.9 -1.636 1.9 -1.824C1.9 -2.416 1.5 -2.488 1.264 -2.488C1.048 -2.488 0.944 -2.424 0.944 -2.372C0.944 -2.344 0.98 -2.332 1.072 -2.304C1.196 -2.268 1.34 -2.16 1.34 -1.928C1.34 -1.708 1.2 -1.52 0.956 -1.52C0.688 -1.52 0.528 -1.732 0.528 -1.98C0.528 -2.24 0.684 -2.632 1.288 -2.632C1.556 -2.632 2.076 -2.512 2.076 -1.832C2.076 -1.604 2.004 -1.224 1.96 -0.976C1.952 -0.928 1.956 -0.932 2.012 -0.908C2.416 -0.748 2.684 -0.408 2.684 0.044C2.684 0.556 2.308 1.008 1.72 1.008C1.616 1.008 1.616 1.008 1.604 1.08ZM1.88 3.772C2.012 3.772 2.12 3.664 2.12 3.444C2.12 3 1.74 2.64 1.424 2.364C1.396 2.34 1.38 2.344 1.372 2.396C1.356 2.5 1.348 2.636 1.348 2.764C1.348 3.388 1.636 3.772 1.88 3.772ZM1.444 1.048C1.456 0.972 1.456 0.976 1.384 0.952C1.032 0.832 0.804 0.516 0.804 0.176C0.804 -0.184 0.992 -0.44 1.264 -0.532C1.296 -0.544 1.344 -0.556 1.372 -0.556C1.404 -0.556 1.42 -0.536 1.42 -0.512C1.42 -0.484 1.388 -0.472 1.36 -0.46C1.192 -0.388 1.072 -0.216 1.072 -0.032C1.072 0.196 1.228 0.368 1.472 0.436C1.536 0.452 1.544 0.448 1.552 0.404L1.752 -0.788C1.76 -0.832 1.756 -0.832 1.696 -0.844C1.632 -0.856 1.552 -0.864 1.472 -0.864C0.772 -0.864 0.32 -0.476 0.32 0.08C0.32 0.316 0.36 0.632 0.692 1.008C0.932 1.276 1.116 1.424 1.304 1.576C1.344 1.608 1.352 1.604 1.36 1.56ZM1.72 0.412C1.712 0.46 1.716 0.472 1.764 0.468C2.088 0.44 2.356 0.168 2.356 -0.184C2.356 -0.436 2.204 -0.64 1.98 -0.752C1.932 -0.776 1.924 -0.776 1.916 -0.728Z" transform="translate(26 0)" fill="#000000" data-prov="c46ea45155710bbe02c7dc32cfe32bc2" data-source-kind="4" data-glyph="gClef" data-class="clef"/>
|
||||
<path d="M0.864 0.5C0.332 0.5 0 0.28 0 0.008C0 -0.26 0.228 -0.5 0.824 -0.5C1.48 -0.5 1.688 -0.272 1.688 0.008C1.688 0.292 1.236 0.5 0.864 0.5ZM0.444 0.252C0.488 0.392 0.636 0.412 0.76 0.412C1.036 0.412 1.256 0.116 1.256 -0.124C1.256 -0.248 1.204 -0.36 1.072 -0.392C1.032 -0.404 0.988 -0.408 0.948 -0.408C0.804 -0.408 0.656 -0.312 0.572 -0.2C0.492 -0.108 0.432 0.028 0.432 0.156C0.432 0.188 0.436 0.22 0.444 0.252Z" transform="translate(27 0)" fill="#000000" data-prov="3550b79c14ba8069d76656ff5347dc71" data-source-kind="2" data-glyph="noteheadWhole" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(28 0)" fill="#000000" data-prov="2a1a8f5701737dac37ac7249833d1b7c" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(29 0)" fill="#000000" data-prov="7d6b27caefc21b2a04761b299840e44d" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C0.744 -0.5 1.18 -0.172 1.18 0.168C1.18 0.372 1.02 0.5 0.792 0.5C0.352 0.5 0 0.176 0 -0.168C0 -0.376 0.172 -0.5 0.388 -0.5Z" transform="translate(30 0)" fill="#000000" data-prov="ec08ad9ddf4f78c806a81632fe2ae1e4" data-source-kind="0" data-glyph="noteheadBlack" data-class="notehead"/>
|
||||
<path d="M0.388 -0.5C1.048 -0.5 1.18 0.036 1.18 0.168C1.18 0.372 1.016 0.5 0.784 0.5C0.188 0.5 0 0.04 0 -0.168C0 -0.38 0.168 -0.5 0.388 -0.5ZM0.3 -0.348C0.216 -0.348 0.168 -0.304 0.14 -0.256C0.128 -0.232 0.116 -0.204 0.116 -0.176C0.116 0.02 0.696 0.336 0.884 0.336C0.96 0.336 1.004 0.3 1.032 0.252C1.044 0.228 1.056 0.204 1.056 0.176C1.056 0.004 0.492 -0.348 0.3 -0.348Z" transform="translate(31 0)" fill="#000000" data-prov="8311e890285ee9cffb3e19c98277caa4" data-source-kind="1" data-glyph="noteheadHalf" data-class="notehead"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 27 KiB |
|
|
@ -0,0 +1,94 @@
|
|||
Copyright © 2015, Steinberg Media Technologies GmbH (http://www.steinberg.net/),
|
||||
with Reserved Font Name "Bravura".
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Extract genuine Bravura SMuFL glyph outlines into a Rust table.
|
||||
|
||||
Reproducible generator for `epiphany-render-svg`'s bundled outline data. It
|
||||
fetches the official OFL `Bravura.otf` and the SMuFL `glyphnames.json`, then
|
||||
emits `src/outlines_generated.rs` with each glyph's outline as an SVG path in
|
||||
**staff-space**, **y-up** coordinates (the renderer's coordinate system).
|
||||
|
||||
Usage:
|
||||
python3 -m venv .venv && . .venv/bin/activate && pip install fonttools
|
||||
python3 extract_bravura_outlines.py > ../crates/epiphany-render-svg/src/outlines_generated.rs
|
||||
|
||||
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 json, re, sys, urllib.request
|
||||
|
||||
FONT_URL = "https://raw.githubusercontent.com/steinbergmedia/bravura/master/redist/otf/Bravura.otf"
|
||||
NAMES_URL = "https://raw.githubusercontent.com/w3c/smufl/gh-pages/metadata/glyphnames.json"
|
||||
|
||||
# Exactly the glyph set the v0 layout pipeline can name (layout-ir BRAVURA_METRICS).
|
||||
NAMES = ["noteheadBlack","noteheadHalf","noteheadWhole","noteheadDoubleWhole",
|
||||
"gClef","fClef","cClef","accidentalSharp","accidentalFlat","accidentalNatural",
|
||||
"accidentalDoubleSharp","restWhole","restHalf","restQuarter","rest8th",
|
||||
"flag8thUp","flag8thDown","augmentationDot","timeSig4","timeSigCommon",
|
||||
"barlineSingle","barlineFinal","dynamicForte","dynamicPiano"]
|
||||
|
||||
def load():
|
||||
from fontTools.ttLib import TTFont
|
||||
import io
|
||||
if "--local" in sys.argv:
|
||||
font = TTFont("Bravura.otf"); names = json.load(open("glyphnames.json"))
|
||||
else:
|
||||
font = TTFont(io.BytesIO(urllib.request.urlopen(FONT_URL).read()))
|
||||
names = json.loads(urllib.request.urlopen(NAMES_URL).read())
|
||||
return font, names
|
||||
|
||||
def round_d(d, nd=4):
|
||||
def r(m):
|
||||
v = round(float(m.group(0)), nd)
|
||||
s = f"{v:.{nd}f}".rstrip('0').rstrip('.')
|
||||
return "0" if s in ("", "-0") else s
|
||||
return re.sub(r'-?\d+\.?\d*(?:e-?\d+)?', r, d)
|
||||
|
||||
def main():
|
||||
from fontTools.pens.svgPathPen import SVGPathPen
|
||||
from fontTools.pens.transformPen import TransformPen
|
||||
from fontTools.pens.boundsPen import BoundsPen
|
||||
font, glyphnames = load()
|
||||
upm = font["head"].unitsPerEm
|
||||
sp = upm / 4.0 # font units per staff space (SMuFL em = 4 staff spaces)
|
||||
scale = 1.0 / sp
|
||||
gs = font.getGlyphSet()
|
||||
cmap = font.getBestCmap()
|
||||
rows = []
|
||||
for name in NAMES:
|
||||
cp = int(glyphnames[name]["codepoint"].replace("U+", ""), 16)
|
||||
g = cmap.get(cp)
|
||||
if g is None:
|
||||
print(f"// MISSING {name} U+{cp:04X}", file=sys.stderr); continue
|
||||
pen = SVGPathPen(gs)
|
||||
gs[g].draw(TransformPen(pen, (scale, 0, 0, scale, 0, 0)))
|
||||
d = round_d(pen.getCommands())
|
||||
bp = BoundsPen(gs); gs[g].draw(bp)
|
||||
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)))
|
||||
rows.sort()
|
||||
o = []
|
||||
o.append("//! GENERATED by `tools/extract_bravura_outlines.py` — do not edit by hand.")
|
||||
o.append("//!")
|
||||
o.append("//! Real Bravura SMuFL glyph outlines, extracted from the official OFL")
|
||||
o.append(f"//! `Bravura.otf` (unitsPerEm = {upm}; 1 staff space = {sp:g} font units, since the")
|
||||
o.append("//! SMuFL em is 4 staff spaces). Path `d` data is in **staff-space** units, **y-up**")
|
||||
o.append("//! (musical convention, positive y = higher pitch), relative to each glyph's")
|
||||
o.append("//! origin, rounded to 4 decimals. The renderer applies a single y-flip wrapper.")
|
||||
o.append("//!")
|
||||
o.append("//! Bravura is (c) Steinberg Media Technologies GmbH under the SIL Open Font")
|
||||
o.append("//! License 1.1; these extracted outlines are redistributed under the same license")
|
||||
o.append("//! (see `tools/OFL.txt`).")
|
||||
o.append("")
|
||||
o.append("/// One bundled glyph outline: SMuFL name, codepoint, the SVG path `d` in")
|
||||
o.append("/// staff-space / y-up coordinates, and the outline's tight bounding box")
|
||||
o.append("/// `[left, bottom, right, top]` in staff spaces.")
|
||||
o.append("pub(crate) struct BravuraOutline {")
|
||||
o.append(" pub name: &'static str,")
|
||||
o.append(" pub codepoint: u32,")
|
||||
o.append(" pub path: &'static str,")
|
||||
o.append(" pub bbox: [f32; 4],")
|
||||
o.append("}")
|
||||
o.append("")
|
||||
o.append("/// Every glyph the v0 layout pipeline can name (the `BRAVURA_METRICS` set), with")
|
||||
o.append("/// its genuine Bravura outline. Sorted by name for deterministic binary search.")
|
||||
o.append("pub(crate) const BRAVURA_OUTLINES: &[BravuraOutline] = &[")
|
||||
for name, cp, d, (l, b, r, t) in rows:
|
||||
o.append(" BravuraOutline {")
|
||||
o.append(f" name: {json.dumps(name)},")
|
||||
o.append(f" codepoint: 0x{cp:04X},")
|
||||
o.append(f" path: {json.dumps(d)},")
|
||||
o.append(f" bbox: [{l}, {b}, {r}, {t}],")
|
||||
o.append(" },")
|
||||
o.append("];")
|
||||
sys.stdout.write("\n".join(o) + "\n")
|
||||
print(f"// extracted {len(rows)}/{len(NAMES)} glyphs", file=sys.stderr)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -26,9 +26,12 @@ code instead is the failure mode this batch exists to prevent.
|
|||
| P12-H3 | `epiphany-core` H | Chromatic-run convention (ascending = sharps, descending = flats) is only a *tiebreak* in the centre-of-gravity rule, so an isolated chromatic run with no tonal context may pick the enharmonic the convention would not. Voice-leading refinement deferred. | G / Pass 12 (spelling) |
|
||||
| P12-H4 | `epiphany-core` H | Decomposition simplifications: single governing meter per region (multi/mid-region meter changes deferred); region origin assumed a barline (anacrusis deferred); compound-meter beat grouping beyond the dyadic default; tuplet nesting and cross-beat tuplet members; double+ augmentation dots (`MAX_DOTS = 1`). | G (decomposition scope) |
|
||||
| P12-H5 | `epiphany-core` H | Automatic spelling under aleatoric regions (the spec's open question). H spells pitches region-independently but performs no region-specific aleatoric spelling; defer if the algorithm does not generalise cleanly. | G / Pass 12 (open question) |
|
||||
| P12-I1 | `epiphany-layout-ir` / `engrave` / `render-svg` I | The v0 `to_logical`/`to_constrained` pipeline is a **structural placeholder**: each layout object becomes one *arbitrary* glyph (`BRAVURA_METRICS[discriminant % N]`) at `y = 0`, not real notation. Chapter 7 says the *logical* stage has "engraving decisions made"; the spec should clarify which engraving decisions (glyph-by-duration selection, pitch→staff-position, clef/key/meter/barline realization, stems/beams) are core-IR construction versus solver work, so the real-notation engraving has a defined home before it is built next phase. Consequence: the QUICKSTART human visual-acceptance gate ("the SVG visually parses as standard notation") is a **next-phase** gate, *not* met by stub output — this phase's gate is renderer correctness/faithfulness. | G / Pass 12 (Ch 7 engraving boundary) |
|
||||
| P12-I2 | `epiphany-render-svg` / `engrave` I | Stable layout-object id derivation (`MUSCLOID`, Pass-11 item 2.6, deferred to I) is still unwired: the frozen `epiphany-determinism` exposes no `MUSCLOID` tag, so provenance is traced via the provisional `stable_id`. Wiring the ratified derivation is Track A work (already noted in `layout-ir/DECISIONS.md`). | G (determinism tag) / Track A |
|
||||
| P12-I3 | `epiphany-layout-ir` I | The bundled `BRAVURA_METRICS` are *approximations* that disagree with the genuine Bravura outlines the renderer now extracts from the font (e.g. `timeSig4`: metrics bbox `[40,0,1240,2048]` vs real outline ≈ `[0.08,-1.0,1.8,1.004]` staff spaces). Real spacing needs exact metrics; regenerate the metrics table from the font or reconcile it with the outline source. | G / Pass 12 (glyph metrics) |
|
||||
|
||||
## Not yet open elsewhere
|
||||
|
||||
Track A's other agents (I) and Track B (K, J) have not yet contributed items.
|
||||
When they do, append rows; the batch is already open, so they join directly (no
|
||||
new threshold).
|
||||
Agent I (Track A) has contributed P12-I1..I3 above. Track B (K, J) has not yet
|
||||
contributed items. When they do, append rows; the batch is already open, so they
|
||||
join directly (no new threshold).
|
||||
|
|
|
|||
Loading…
Reference in New Issue