Inter-staff slice 2/2: renegotiate the gaps between a system's staves

The last vertical-spring piece: the gaps BETWEEN a system's staves are
renegotiated so tightly ledgered or slurred adjacent staves — which the
constrained stage stacks at a fixed pitch — separate. ENGRAVER_VERSION 10 -> 11
(a multi-staff score whose staves press together shifts them apart; a
single-staff score, with no inter-staff pair, is byte-identical).

Attribution (vertical_band + owning-glyph, per the chosen approach): a glyph via
its vertical_band (VerticalBandKind::Staff -> StaffId); a stem/ledger via its
notehead (component_glyph); a staff line via its Staff source; a slur via the
notehead nearest its start. Spacing is horizontal-only, so a primitive's y is
unchanged from the source frame the attribution reads.

The solve: per system, per staff, collect the real content y-extent (glyphs,
strokes, curves — ledgers and slurs included); order staves top-to-bottom by
their staff-line reference y (order fixed); shift each staff down by the
cumulative amount needed to bring its gap to the one above up to the band
model's preferred inter-staff gap. staff_shift[(system, staff)] is a per-staff
dy the bake applies (Placement::sunk) atop the per-system dy, so glyphs,
strokes, curves, the staff/measure/system records, content bounds, hit-test, and
quality metrics all read the same shifted geometry. The shifts grow each
system's extent, which the vertical stacking and justification then consume.

Regression inter_staff_solve_separates_colliding_staves (the two-staff fixture's
staff-line gap opens past the fixed pitch; a single-staff score keeps one staff
per system) + the two_staff_close_content render golden updates (slice 1 tight,
slice 2 separated). Only that engrave golden churned; single-staff goldens
byte-stable. 947 tests, clippy 0, docs -D warnings, conformance 8/8.

This completes the Standard-tier layout story end to end.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Levi Neuwirth 2026-07-08 23:04:25 -04:00
parent f103641013
commit 3978225e06
5 changed files with 318 additions and 88 deletions

View File

@ -29,8 +29,12 @@ JUSTIFICATION** (see "Per-system justification (Push 3)" below): every non-final
system stretches to fill the content width; and **vertical justification** (same system stretches to fill the content width; and **vertical justification** (same
section): the systems of a non-final page spread to fill the page height; and section): the systems of a non-final page spread to fill the page height; and
**optimal break search** (see "Optimal break search (Push 3)" below) replaces **optimal break search** (see "Optimal break search (Push 3)" below) replaces
greedy first-fit + widow rebalance. Still deferred: the inter-staff soft-spring greedy first-fit + widow rebalance; and the **inter-staff vertical solve** (see
solve *within* a system (band-height renegotiation). "Inter-staff vertical solve (Push 3)" below) renegotiates the gaps between a
system's staves. This completes the Standard-tier layout story end to end;
per-system justification (horizontal), vertical justification (inter-system),
optimal breaks, and the inter-staff solve (intra-system vertical) now all
compose.
### Honest tier ### Honest tier
@ -694,3 +698,45 @@ predecessor wins. Tests: `optimal_breaks_balances_systems_and_avoids_a_final_
widow`, `optimal_breaks_never_spans_a_forced_break`, widow`, `optimal_breaks_never_spans_a_forced_break`,
`optimal_breaks_is_deterministic_and_empty_when_unbounded`; the widow test now `optimal_breaks_is_deterministic_and_empty_when_unbounded`; the widow test now
checks the balanced measure distribution the DP produces. checks the balanced measure distribution the DP produces.
## Inter-staff vertical solve (Push 3, 2026-07-08)
The last vertical-spring piece: the gaps BETWEEN a system's staves are
renegotiated, so tightly ledgered or slurred adjacent staves — which the
constrained stage stacks at a fixed pitch — separate. `ENGRAVER_VERSION` 10 →
11 (a multi-staff score whose staves press together shifts them apart; a
single-staff score, with no inter-staff pair, is byte-identical).
**Attribution (`vertical_band` + owning-glyph).** Every resolved primitive is
attributed to its owning staff: a glyph via its `vertical_band`
(`VerticalBandKind::Staff` → `StaffId`); a stem or ledger via its notehead
(`component_glyph` → that glyph's band); a staff line via its `Staff`
provenance source; a slur via the notehead nearest its start endpoint. Spacing
is horizontal-only, so a primitive's y is unchanged from the source frame the
attribution reads, and the resolved and source indices line up. (The geometric
`round(-y / pitch)` alternative was rejected: an extreme ledgered note on the
top or bottom staff rounds to a non-existent neighbour.)
**The solve.** Per system, per staff, the real content y-extent is collected
(glyphs, strokes, curves — ledgers and slurs included, not just noteheads). The
staves are ordered top-to-bottom by their staff-line reference y and that order
is kept fixed; each staff is then shifted DOWN by the cumulative amount needed
to bring its gap to the one above up to the band model's preferred inter-staff
gap (`VerticalBand::inter_staff_gap`). `staff_shift[(system, staff)]` — the top
staff's is 0 — is a per-staff `dy` the bake applies (via `Placement::sunk`) on
top of the per-system `dy`, so glyphs, strokes, curves, the staff/measure/system
records, content bounds, hit-test regions, and the quality metrics all read the
same shifted geometry (they all consume the baked output). The shifts grow each
system's extent, which the vertical stacking and justification then consume — so
the inter-staff (intra-system) and inter-system passes compose.
**Scope.** Enforces the preferred gap (which subsumes non-overlap). Only `dy`
per staff changes; staff order is fixed; a single-staff system has no pair and
is untouched. Locked by `inter_staff_solve_separates_colliding_staves` (the
two-staff pressure fixture's staff-line gap opens past the fixed pitch while a
single-staff score keeps one staff per system) and the `two_staff_close_content`
render golden (the visible before/after: slice 1 tight, slice 2 separated).
**Deferred:** compressing an OVER-wide fixed gap toward preferred (the solve
only expands, never pulls staves together — the fixed pitch is generous by
default, so this is rarely wanted); per-staff spring *stretch* to fill spare
system height (the inter-system justification carries the fill for now).

View File

@ -78,10 +78,10 @@ use epiphany_layout_ir::{
EngravingDecisionKind, EngravingOverrideId, GlyphObject, GlyphObjectId, LayoutConstraint, EngravingDecisionKind, EngravingOverrideId, GlyphObject, GlyphObjectId, LayoutConstraint,
LayoutObjectId, Margins, Point, Provenance, Rect, ResolvedGlyph, ResolvedMeasure, ResolvedPage, LayoutObjectId, Margins, Point, Provenance, Rect, ResolvedGlyph, ResolvedMeasure, ResolvedPage,
ResolvedStaff, ResolvedSystem, Size2D, SpringSlotId, StaffSpace, Stroke, SynthesisInstanceKey, ResolvedStaff, ResolvedSystem, Size2D, SpringSlotId, StaffSpace, Stroke, SynthesisInstanceKey,
SynthesisKind, SynthesisRegistryId, VerticalBand, VerticalBandId, SynthesisKind, SynthesisRegistryId, VerticalBand, VerticalBandId, VerticalBandKind,
}; };
use crate::owning_glyph; use crate::{component_glyph, owning_glyph};
/// The registry id for the engraver's **system-continuation synthesis**: the /// The registry id for the engraver's **system-continuation synthesis**: the
/// segment of a region-spanning stroke (a staff line) that casting-off places /// segment of a region-spanning stroke (a staff line) that casting-off places
@ -322,6 +322,14 @@ impl Placement {
fn slot_dx(&self, slot_x: f32) -> f32 { fn slot_dx(&self, slot_x: f32) -> f32 {
(self.a - 1.0) * slot_x + self.b (self.a - 1.0) * slot_x + self.b
} }
/// The same placement sunk downward by `shift` — the inter-staff solve
/// pushes a staff's content down within its system (y-down is decreasing y).
fn sunk(&self, shift: f32) -> Self {
Placement {
dy: self.dy - shift,
..*self
}
}
} }
/// The content extent of a system in spaced (pre-casting) coordinates. /// The content extent of a system in spaced (pre-casting) coordinates.
@ -355,6 +363,15 @@ impl Extent {
} }
} }
/// Extend only the vertical extent (the inter-staff solve grows a system's
/// height by shifting staves apart, without touching its x-span).
fn add_y(&mut self, y0: f32, y1: f32) {
if y0.is_finite() && y1.is_finite() {
self.min_y = self.min_y.min(y0.min(y1));
self.max_y = self.max_y.max(y0.max(y1));
}
}
/// Normalized: a content-less system is a zero box at the origin. /// Normalized: a content-less system is a zero box at the origin.
fn normalized(self) -> Self { fn normalized(self) -> Self {
if self.any { if self.any {
@ -616,70 +633,174 @@ pub(crate) fn cast_off(
.map(|curve| curve_fate(curve, &region_spans, &region_systems, &clips)) .map(|curve| curve_fate(curve, &region_spans, &region_systems, &clips))
.collect(); .collect();
// ---- System extents ---------------------------------------------------- // ---- Inter-staff vertical solve + system extents -----------------------
// Attribute every primitive to its owning staff so the gaps BETWEEN a
// system's staves can be renegotiated: the constrained stage stacks staves
// at a fixed pitch, so tightly ledgered or slurred adjacent staves collide.
// Glyphs name their staff via their vertical band; a stem/ledger tracks its
// notehead; a staff line names its staff; a slur takes the staff of the
// note nearest its start. Spacing is horizontal-only, so a primitive's y is
// unchanged from the source frame the attribution is computed in.
let band_to_staff: BTreeMap<VerticalBandId, StaffId> = input
.vertical_bands
.iter()
.filter_map(|b| match b.kind {
VerticalBandKind::Staff(s) => Some((b.id, s)),
_ => None,
})
.collect();
let glyph_band_staff = |g: &GlyphObject| band_to_staff.get(&g.vertical_band).copied();
let glyph_staff_of: Vec<Option<StaffId>> = input.glyphs.iter().map(glyph_band_staff).collect();
let stroke_staff_of: Vec<Option<StaffId>> = input
.strokes
.iter()
.map(|s| match s.provenance.source {
TypedObjectId::Staff(st) => Some(st),
_ => component_glyph(s, &input.glyphs).and_then(glyph_band_staff),
})
.collect();
let curve_staff_of: Vec<Option<StaffId>> = input
.curves
.iter()
.map(|c| {
let (px, py) = (c.p0.x.0, c.p0.y.0);
input
.glyphs
.iter()
.filter(|g| g.glyph.as_str().starts_with("notehead"))
.min_by(|a, b| {
let d = |g: &GlyphObject| {
(g.baseline.x.0 - px).powi(2) + (g.baseline.y.0 - py).powi(2)
};
d(a).total_cmp(&d(b))
})
.and_then(glyph_band_staff)
})
.collect();
// Pass A: system extents (unshifted), and per (system, staff) content
// y-extents plus the staff-line reference y (for ordering).
let mut extents: Vec<Extent> = vec![Extent::empty(); systems.len()]; let mut extents: Vec<Extent> = vec![Extent::empty(); systems.len()];
let mut staff_ext: BTreeMap<(usize, StaffId), (f32, f32)> = BTreeMap::new();
let mut staff_ref: BTreeMap<(usize, StaffId), f32> = BTreeMap::new();
let into_staff = |m: &mut BTreeMap<(usize, StaffId), (f32, f32)>,
s: usize,
staff: Option<StaffId>,
lo_y: f32,
hi_y: f32| {
if let Some(st) = staff {
m.entry((s, st))
.and_modify(|e| {
e.0 = e.0.min(lo_y);
e.1 = e.1.max(hi_y);
})
.or_insert((lo_y, hi_y));
}
};
for (s, plan) in systems.iter().enumerate() { for (s, plan) in systems.iter().enumerate() {
for &i in &plan.slots { for &i in &plan.slots {
for &g in &region_slots[plan.region][i].members { for &g in &region_slots[plan.region][i].members {
let glyph = &spaced_glyphs[g]; let glyph = &spaced_glyphs[g];
let (x, y) = (glyph.position.x.0, glyph.position.y.0); let (x, y) = (glyph.position.x.0, glyph.position.y.0);
extents[s].add( let (lo_y, hi_y) = (
x + glyph.bounding_box.left.0,
y + glyph.bounding_box.bottom.0, y + glyph.bounding_box.bottom.0,
x + glyph.bounding_box.right.0,
y + glyph.bounding_box.top.0, y + glyph.bounding_box.top.0,
); );
}
}
}
for (fate, spaced) in fates.iter().zip(spaced_strokes) {
let half = (spaced.thickness.0 * 0.5).max(0.0);
match fate {
StrokeFate::Rigid(Some(s)) => extents[*s].add(
spaced.from.x.0 - half,
spaced.from.y.0.min(spaced.to.y.0) - half,
spaced.to.x.0 + half,
spaced.from.y.0.max(spaced.to.y.0) + half,
),
StrokeFate::Rigid(None) => {}
StrokeFate::Split(segments) => {
for (s, from, to) in segments {
extents[*s].add(
from.x.0 - half,
from.y.0.min(to.y.0) - half,
to.x.0 + half,
from.y.0.max(to.y.0) + half,
);
}
}
}
}
// A curve's control-point hull (± half-thickness) grows its system's
// extent, so a slur above the staff raises the system height (page overflow
// accounts for it) exactly as the volta bracket strokes do. A split curve
// grows each system by its own sub-cubic's hull.
for (fate, curve) in curve_fates.iter().zip(spaced_curves) {
let half = (curve.thickness.0 * 0.5).max(0.0);
let mut grow = |s: usize, cp: &[Point; 4]| {
for point in cp {
extents[s].add( extents[s].add(
point.x.0 - half, x + glyph.bounding_box.left.0,
point.y.0 - half, lo_y,
point.x.0 + half, x + glyph.bounding_box.right.0,
point.y.0 + half, hi_y,
); );
into_staff(&mut staff_ext, s, glyph_staff_of[g], lo_y, hi_y);
} }
}
}
for (si, (fate, spaced)) in fates.iter().zip(spaced_strokes).enumerate() {
let half = (spaced.thickness.0 * 0.5).max(0.0);
let staff = stroke_staff_of[si];
let is_staff_line = matches!(spaced.provenance.source, TypedObjectId::Staff(_));
let segs: Vec<(usize, Point, Point)> = match fate {
StrokeFate::Rigid(Some(s)) => vec![(*s, spaced.from, spaced.to)],
StrokeFate::Rigid(None) => vec![],
StrokeFate::Split(segments) => segments.clone(),
}; };
match fate { for (s, from, to) in segs {
CurveFate::Rigid(Some(s)) => grow(*s, &curve.control_points()), let (lo_y, hi_y) = (from.y.0.min(to.y.0) - half, from.y.0.max(to.y.0) + half);
CurveFate::Rigid(None) => {} extents[s].add(from.x.0 - half, lo_y, to.x.0 + half, hi_y);
CurveFate::Split(segments) => { into_staff(&mut staff_ext, s, staff, lo_y, hi_y);
for (s, cp) in segments { if is_staff_line {
grow(*s, cp); if let Some(st) = staff {
staff_ref
.entry((s, st))
.and_modify(|r| *r = r.max(hi_y))
.or_insert(hi_y);
} }
} }
} }
} }
for (ci, (fate, curve)) in curve_fates.iter().zip(spaced_curves).enumerate() {
let half = (curve.thickness.0 * 0.5).max(0.0);
let staff = curve_staff_of[ci];
let segs: Vec<(usize, [Point; 4])> = match fate {
CurveFate::Rigid(Some(s)) => vec![(*s, curve.control_points())],
CurveFate::Rigid(None) => vec![],
CurveFate::Split(segments) => segments.clone(),
};
for (s, cp) in segs {
for p in cp {
extents[s].add(p.x.0 - half, p.y.0 - half, p.x.0 + half, p.y.0 + half);
into_staff(&mut staff_ext, s, staff, p.y.0 - half, p.y.0 + half);
}
}
}
// Solve each system's inter-staff gaps: order the staves top-to-bottom by
// their reference y (staff line, else content mid), keep that order fixed,
// and shift each staff down until its gap to the one above meets the band
// model's preferred inter-staff gap. `staff_shift[(system, staff)]` is the
// downward shift (subtracted from y); the top staff's is 0.
let preferred_gap = VerticalBand::inter_staff_gap(VerticalBandId(0))
.preferred_height
.0;
let mut staff_shift: BTreeMap<(usize, StaffId), f32> = BTreeMap::new();
for s in 0..systems.len() {
let mut staves: Vec<(StaffId, (f32, f32))> = staff_ext
.iter()
.filter(|((sys, _), _)| *sys == s)
.map(|((_, st), ext)| (*st, *ext))
.collect();
// Top first: larger reference y is higher on the page.
staves.sort_by(|a, b| {
let key = |st: StaffId, ext: (f32, f32)| {
staff_ref
.get(&(s, st))
.copied()
.unwrap_or((ext.0 + ext.1) * 0.5)
};
key(b.0, b.1).total_cmp(&key(a.0, a.1)).then(a.0.cmp(&b.0))
});
let mut shift = 0.0_f32;
for w in staves.windows(2) {
let (upper, (upper_lo, _)) = w[0];
let (lower, (_, lower_hi)) = w[1];
staff_shift.insert((s, upper), shift);
// Realized gap after the upper's shift: (upper_lo - shift) lower_hi.
let gap = (upper_lo - shift) - lower_hi;
shift += (preferred_gap - gap).max(0.0);
staff_shift.insert((s, lower), shift);
}
if staves.len() == 1 {
staff_shift.insert((s, staves[0].0), 0.0);
}
}
// Fold each staff's SHIFTED content y-extent into its system extent, so the
// stacking/justification below sees the taller, separated system.
for ((s, st), (lo, hi)) in &staff_ext {
let sh = staff_shift.get(&(*s, *st)).copied().unwrap_or(0.0);
extents[*s].add_y(lo - sh, hi - sh);
}
let extents: Vec<Extent> = extents.into_iter().map(Extent::normalized).collect(); let extents: Vec<Extent> = extents.into_iter().map(Extent::normalized).collect();
// ---- Vertical stacking and page assignment ---------------------------- // ---- Vertical stacking and page assignment ----------------------------
@ -802,10 +923,18 @@ pub(crate) fn cast_off(
decisions.extend(skipped); decisions.extend(skipped);
// ---- Bake the world frame ---------------------------------------------- // ---- Bake the world frame ----------------------------------------------
// A primitive's additional downward shift from the inter-staff solve.
let staff_dy = |s: usize, staff: Option<StaffId>| -> f32 {
staff
.and_then(|st| staff_shift.get(&(s, st)))
.copied()
.unwrap_or(0.0)
};
let glyphs: Vec<ResolvedGlyph> = spaced_glyphs let glyphs: Vec<ResolvedGlyph> = spaced_glyphs
.iter() .iter()
.zip(&input.glyphs) .zip(&input.glyphs)
.map(|(spaced, glyph)| { .enumerate()
.map(|(gi, (spaced, glyph))| {
let (dx, dy) = match system_of_slot.get(&glyph.horizontal_slot) { let (dx, dy) = match system_of_slot.get(&glyph.horizontal_slot) {
Some(&s) => { Some(&s) => {
// Slot-relative: every member of a slot translates by the // Slot-relative: every member of a slot translates by the
@ -814,7 +943,10 @@ pub(crate) fn cast_off(
.get(&glyph.horizontal_slot) .get(&glyph.horizontal_slot)
.copied() .copied()
.unwrap_or(spaced.position.x.0); .unwrap_or(spaced.position.x.0);
(placements[s].slot_dx(sx), placements[s].dy) (
placements[s].slot_dx(sx),
placements[s].dy - staff_dy(s, glyph_staff_of[gi]),
)
} }
None => (0.0, 0.0), None => (0.0, 0.0),
}; };
@ -829,14 +961,20 @@ pub(crate) fn cast_off(
let mut staff_marks: BTreeMap<(usize, StaffId), StaffAgg> = BTreeMap::new(); let mut staff_marks: BTreeMap<(usize, StaffId), StaffAgg> = BTreeMap::new();
let mut strokes: Vec<Stroke> = Vec::with_capacity(spaced_strokes.len()); let mut strokes: Vec<Stroke> = Vec::with_capacity(spaced_strokes.len());
let mut continuations: Vec<Stroke> = Vec::new(); let mut continuations: Vec<Stroke> = Vec::new();
for ((source, spaced), fate) in input.strokes.iter().zip(spaced_strokes).zip(&fates) { for (si, ((source, spaced), fate)) in input
.strokes
.iter()
.zip(spaced_strokes)
.zip(&fates)
.enumerate()
{
match fate { match fate {
StrokeFate::Rigid(sys) => { StrokeFate::Rigid(sys) => {
let stroke = match sys { let stroke = match sys {
Some(s) => place_stroke( Some(s) => place_stroke(
source, source,
spaced, spaced,
placements[*s], placements[*s].sunk(staff_dy(*s, stroke_staff_of[si])),
&slot_source_x, &slot_source_x,
&input.glyphs, &input.glyphs,
), ),
@ -851,7 +989,7 @@ pub(crate) fn cast_off(
for (k, (s, from, to)) in segments.iter().enumerate() { for (k, (s, from, to)) in segments.iter().enumerate() {
// A split stroke spans systems — a staff line or volta // A split stroke spans systems — a staff line or volta
// bracket — so each segment stretches with its system. // bracket — so each segment stretches with its system.
let p = placements[*s]; let p = placements[*s].sunk(staff_dy(*s, stroke_staff_of[si]));
let provenance = if k == 0 { let provenance = if k == 0 {
// The first segment carries the original stroke's exact // The first segment carries the original stroke's exact
// provenance: the object survives, re-shaped. // provenance: the object survives, re-shaped.
@ -895,7 +1033,8 @@ pub(crate) fn cast_off(
// split stroke's are. // split stroke's are.
let mut curves: Vec<Curve> = Vec::with_capacity(spaced_curves.len()); let mut curves: Vec<Curve> = Vec::with_capacity(spaced_curves.len());
let mut curve_continuations: Vec<Curve> = Vec::new(); let mut curve_continuations: Vec<Curve> = Vec::new();
for (curve, fate) in spaced_curves.iter().zip(&curve_fates) { for (ci, (curve, fate)) in spaced_curves.iter().zip(&curve_fates).enumerate() {
let curve_staff = curve_staff_of[ci];
// A slur has no intra-slot structure, so its control points map straight // A slur has no intra-slot structure, so its control points map straight
// through the affine: the endpoints follow their anchor notes (which sit // through the affine: the endpoints follow their anchor notes (which sit
// at slot sources) and the arc stretches horizontally with the span. // at slot sources) and the arc stretches horizontally with the span.
@ -904,7 +1043,7 @@ pub(crate) fn cast_off(
match fate { match fate {
CurveFate::Rigid(system) => { CurveFate::Rigid(system) => {
let p = system let p = system
.map(|s| placements[s]) .map(|s| placements[s].sunk(staff_dy(s, curve_staff)))
.unwrap_or(Placement::rigid(0.0, 0.0)); .unwrap_or(Placement::rigid(0.0, 0.0));
let [p0, p1, p2, p3] = shift(curve.control_points(), p); let [p0, p1, p2, p3] = shift(curve.control_points(), p);
curves.push(Curve { curves.push(Curve {
@ -917,7 +1056,8 @@ pub(crate) fn cast_off(
} }
CurveFate::Split(segments) => { CurveFate::Split(segments) => {
for (k, (s, cp)) in segments.iter().enumerate() { for (k, (s, cp)) in segments.iter().enumerate() {
let [p0, p1, p2, p3] = shift(*cp, placements[*s]); let [p0, p1, p2, p3] =
shift(*cp, placements[*s].sunk(staff_dy(*s, curve_staff)));
let provenance = if k == 0 { let provenance = if k == 0 {
curve.provenance.clone() curve.provenance.clone()
} else { } else {

View File

@ -200,8 +200,12 @@ pub struct Engraver {
/// score's measures partition into more balanced systems — e.g. a 5/4 split /// score's measures partition into more balanced systems — e.g. a 5/4 split
/// where greedy left a fuller-then-stub one — so its system breaks, and every /// where greedy left a fuller-then-stub one — so its system breaks, and every
/// geometry that flows from them, differ; a score that does not wrap is /// geometry that flows from them, differ; a score that does not wrap is
/// unchanged). /// unchanged), and to `11` when the **inter-staff vertical solve** landed (the
pub const ENGRAVER_VERSION: SolverVersion = SolverVersion(10); /// gaps between a system's staves are renegotiated so tightly ledgered or
/// slurred adjacent staves separate to the band model's preferred gap; a
/// multi-staff score whose staves press together shifts them apart, while a
/// single-staff score — with no inter-staff pair — is unchanged).
pub const ENGRAVER_VERSION: SolverVersion = SolverVersion(11);
impl Engraver { impl Engraver {
/// An engraver casting off against the given page geometry. /// An engraver casting off against the given page geometry.
@ -2229,6 +2233,46 @@ mod tests {
} }
} }
#[test]
fn inter_staff_solve_separates_colliding_staves() {
use epiphany_layout_ir::{to_constrained, to_logical};
// The two-staff fixture's staves collide under the fixed staff pitch:
// low ledger notes on the top staff, high ledger notes plus a slur on
// the bottom. The inter-staff solve pushes them apart until the content
// clears the band model's preferred gap.
let report = Engraver::default().solve(
&to_constrained(&to_logical(
&epiphany_testkit::fixtures::two_staff_close_content(1),
)),
&SolverConfig::default(),
);
let sys = &report.layout.pages[0].systems[0];
assert_eq!(sys.staves.len(), 2, "two staves in the system");
// Staff records are top-first; the gap between the two staves' staff-line
// boxes now exceeds the fixed inter-staff pitch (12 staff spaces), where
// under fixed stacking it is ~8 — the solve opened room for the colliding
// ledgers and slur (their content clears with margin to spare).
let (top, bottom) = (&sys.staves[0].bounding_box, &sys.staves[1].bounding_box);
let gap = top.origin.y.0 - (bottom.origin.y.0 + bottom.size.height.0);
assert!(
gap > 12.0,
"the colliding staves separate: staff-line gap {gap}"
);
// A no-pressure single-staff score has no inter-staff pair, so it is
// untouched — its goldens stay byte-stable (see the render-svg suite).
let single =
Engraver::default().solve(&ten_measure_constrained(), &SolverConfig::default());
assert!(
single
.layout
.pages
.iter()
.flat_map(|p| &p.systems)
.all(|s| s.staves.len() <= 1),
"single-staff systems have no inter-staff pair to renegotiate"
);
}
#[test] #[test]
fn vertical_justification_fills_non_final_pages() { fn vertical_justification_fills_non_final_pages() {
use epiphany_layout_ir::{Margins, Size2D, StaffSpace}; use epiphany_layout_ir::{Margins, Size2D, StaffSpace};

View File

@ -8,7 +8,7 @@ provenance_count=54
layer_count=1 layer_count=1
hard_constraint_count=18 hard_constraint_count=18
xml_well_formed=true xml_well_formed=true
view_box=[5.5 -28.524578 17.379084 23.024] view_box=[5.5 -41.584576 17.379084 36.084]
class_counts: class_counts:
barline=2 barline=2
clef=2 clef=2

View File

@ -1,28 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="173.7908" height="230.24" viewBox="0 0 17.3791 23.024"> <svg xmlns="http://www.w3.org/2000/svg" width="173.7908" height="360.84" viewBox="0 0 17.3791 36.084">
<!-- epiphany-render-svg: glyphs are genuine Bravura SMuFL outlines inlined as paths; geometry is the resolved layout verbatim, no engraving performed here; every glyph, stroke, and curve carries a data-prov trace to its score-graph source --> <!-- epiphany-render-svg: glyphs are genuine Bravura SMuFL outlines inlined as paths; geometry is the resolved layout verbatim, no engraving performed here; every glyph, stroke, and curve carries a data-prov trace to its score-graph source -->
<g transform="translate(-5.5 -5.5006) scale(1 -1)"> <g transform="translate(-5.5 -5.5006) scale(1 -1)">
<g data-layer="0"> <g data-layer="0">
<line x1="8.6599" y1="-12.8926" x2="8.6599" y2="-12.8926" stroke="#000000" stroke-width="0" data-prov="5ff1785b2e5e554c42cff8d78dd57953" data-source-kind="6" data-kind="stroke"/> <line x1="8.6599" y1="-25.9526" x2="8.6599" y2="-25.9526" stroke="#000000" stroke-width="0" data-prov="5ff1785b2e5e554c42cff8d78dd57953" data-source-kind="6" data-kind="stroke"/>
<line x1="7.565" y1="-12.8926" x2="20.2543" y2="-12.8926" stroke="#000000" stroke-width="0.13" data-prov="28cbe5009fab9990097b67e48c426c2b" data-source-kind="3" data-kind="stroke"/> <line x1="7.565" y1="-12.8926" x2="20.2543" y2="-12.8926" stroke="#000000" stroke-width="0.13" data-prov="28cbe5009fab9990097b67e48c426c2b" data-source-kind="3" data-kind="stroke"/>
<line x1="7.565" y1="-11.8926" x2="20.2543" y2="-11.8926" stroke="#000000" stroke-width="0.13" data-prov="2dcbda48c9ae66371e9b618b4e0ff496" data-source-kind="3" data-kind="stroke"/> <line x1="7.565" y1="-11.8926" x2="20.2543" y2="-11.8926" stroke="#000000" stroke-width="0.13" data-prov="2dcbda48c9ae66371e9b618b4e0ff496" data-source-kind="3" data-kind="stroke"/>
<line x1="7.565" y1="-10.8926" x2="20.2543" y2="-10.8926" stroke="#000000" stroke-width="0.13" data-prov="5f47dfd6d0910fe3fa68504e3dc8b4cb" data-source-kind="3" data-kind="stroke"/> <line x1="7.565" y1="-10.8926" x2="20.2543" y2="-10.8926" stroke="#000000" stroke-width="0.13" data-prov="5f47dfd6d0910fe3fa68504e3dc8b4cb" data-source-kind="3" data-kind="stroke"/>
<line x1="7.565" y1="-9.8926" x2="20.2543" y2="-9.8926" stroke="#000000" stroke-width="0.13" data-prov="2f1bc986dc62f2e24256b98e76338b40" data-source-kind="3" data-kind="stroke"/> <line x1="7.565" y1="-9.8926" x2="20.2543" y2="-9.8926" stroke="#000000" stroke-width="0.13" data-prov="2f1bc986dc62f2e24256b98e76338b40" data-source-kind="3" data-kind="stroke"/>
<line x1="7.565" y1="-8.8926" x2="20.2543" y2="-8.8926" stroke="#000000" stroke-width="0.13" data-prov="967277083788b3e3f9928206e546eb2d" data-source-kind="3" data-kind="stroke"/> <line x1="7.565" y1="-8.8926" x2="20.2543" y2="-8.8926" stroke="#000000" stroke-width="0.13" data-prov="967277083788b3e3f9928206e546eb2d" data-source-kind="3" data-kind="stroke"/>
<line x1="7.565" y1="-24.8926" x2="20.2543" y2="-24.8926" stroke="#000000" stroke-width="0.13" data-prov="c6afc2060e4ee437d03c460c5e51edb0" data-source-kind="3" data-kind="stroke"/> <line x1="7.565" y1="-37.9526" x2="20.2543" y2="-37.9526" stroke="#000000" stroke-width="0.13" data-prov="c6afc2060e4ee437d03c460c5e51edb0" data-source-kind="3" data-kind="stroke"/>
<line x1="7.565" y1="-23.8926" x2="20.2543" y2="-23.8926" stroke="#000000" stroke-width="0.13" data-prov="fc03fb2cca3c3f5d6276b60b1ff83b72" data-source-kind="3" data-kind="stroke"/> <line x1="7.565" y1="-36.9526" x2="20.2543" y2="-36.9526" stroke="#000000" stroke-width="0.13" data-prov="fc03fb2cca3c3f5d6276b60b1ff83b72" data-source-kind="3" data-kind="stroke"/>
<line x1="7.565" y1="-22.8926" x2="20.2543" y2="-22.8926" stroke="#000000" stroke-width="0.13" data-prov="4ac3502112246f2950bf9ba00346e839" data-source-kind="3" data-kind="stroke"/> <line x1="7.565" y1="-35.9526" x2="20.2543" y2="-35.9526" stroke="#000000" stroke-width="0.13" data-prov="4ac3502112246f2950bf9ba00346e839" data-source-kind="3" data-kind="stroke"/>
<line x1="7.565" y1="-21.8926" x2="20.2543" y2="-21.8926" stroke="#000000" stroke-width="0.13" data-prov="dbcbf5fb791e00972d5a014eb0897966" data-source-kind="3" data-kind="stroke"/> <line x1="7.565" y1="-34.9526" x2="20.2543" y2="-34.9526" stroke="#000000" stroke-width="0.13" data-prov="dbcbf5fb791e00972d5a014eb0897966" data-source-kind="3" data-kind="stroke"/>
<line x1="7.565" y1="-20.8926" x2="20.2543" y2="-20.8926" stroke="#000000" stroke-width="0.13" data-prov="0c940e546e084c0e0c76dc5b5d4cc29f" data-source-kind="3" data-kind="stroke"/> <line x1="7.565" y1="-33.9526" x2="20.2543" y2="-33.9526" stroke="#000000" stroke-width="0.13" data-prov="0c940e546e084c0e0c76dc5b5d4cc29f" data-source-kind="3" data-kind="stroke"/>
<line x1="8.6599" y1="-12.8926" x2="8.6599" y2="-12.8926" stroke="#000000" stroke-width="0" data-prov="dec48b26ccf8d32aaef3b0d78c728ac1" data-source-kind="2" data-kind="stroke"/> <line x1="8.6599" y1="-25.9526" x2="8.6599" y2="-25.9526" stroke="#000000" stroke-width="0" data-prov="dec48b26ccf8d32aaef3b0d78c728ac1" data-source-kind="2" data-kind="stroke"/>
<line x1="13.0944" y1="-13.8926" x2="13.0944" y2="-10.3926" stroke="#000000" stroke-width="0.12" data-prov="2605e711923a6304409ca2b5627aed7e" data-source-kind="0" data-kind="stroke"/> <line x1="13.0944" y1="-26.9526" x2="13.0944" y2="-23.4526" stroke="#000000" stroke-width="0.12" data-prov="2605e711923a6304409ca2b5627aed7e" data-source-kind="0" data-kind="stroke"/>
<line x1="11.6444" y1="-13.8926" x2="13.4251" y2="-13.8926" stroke="#000000" stroke-width="0.13" data-prov="9f71ea414173b35b44c2a526ba3572b7" data-source-kind="1" data-kind="stroke"/> <line x1="11.6444" y1="-13.8926" x2="13.4251" y2="-13.8926" stroke="#000000" stroke-width="0.13" data-prov="9f71ea414173b35b44c2a526ba3572b7" data-source-kind="1" data-kind="stroke"/>
<line x1="15.1751" y1="-17.3926" x2="15.1751" y2="-13.8926" stroke="#000000" stroke-width="0.12" data-prov="b2df28c1e46d5eb33759d335032e7d07" data-source-kind="0" data-kind="stroke"/> <line x1="15.1751" y1="-30.4526" x2="15.1751" y2="-26.9526" stroke="#000000" stroke-width="0.12" data-prov="b2df28c1e46d5eb33759d335032e7d07" data-source-kind="0" data-kind="stroke"/>
<line x1="13.7251" y1="-13.8926" x2="15.5058" y2="-13.8926" stroke="#000000" stroke-width="0.13" data-prov="609bda392c2721347d787ed7b85bab5a" data-source-kind="1" data-kind="stroke"/> <line x1="13.7251" y1="-13.8926" x2="15.5058" y2="-13.8926" stroke="#000000" stroke-width="0.13" data-prov="609bda392c2721347d787ed7b85bab5a" data-source-kind="1" data-kind="stroke"/>
<line x1="13.7251" y1="-14.8926" x2="15.5058" y2="-14.8926" stroke="#000000" stroke-width="0.13" data-prov="6112bc164d0d03853f4fc44f55dcb57a" data-source-kind="1" data-kind="stroke"/> <line x1="13.7251" y1="-14.8926" x2="15.5058" y2="-14.8926" stroke="#000000" stroke-width="0.13" data-prov="6112bc164d0d03853f4fc44f55dcb57a" data-source-kind="1" data-kind="stroke"/>
<line x1="13.7251" y1="-15.8926" x2="15.5058" y2="-15.8926" stroke="#000000" stroke-width="0.13" data-prov="bef6e57fb87c56c2916a05fdb334f49c" data-source-kind="1" data-kind="stroke"/> <line x1="13.7251" y1="-15.8926" x2="15.5058" y2="-15.8926" stroke="#000000" stroke-width="0.13" data-prov="bef6e57fb87c56c2916a05fdb334f49c" data-source-kind="1" data-kind="stroke"/>
<line x1="13.7251" y1="-16.8926" x2="15.5058" y2="-16.8926" stroke="#000000" stroke-width="0.13" data-prov="15e0d12c71c4e3aa49c9145482409090" data-source-kind="1" data-kind="stroke"/> <line x1="13.7251" y1="-16.8926" x2="15.5058" y2="-16.8926" stroke="#000000" stroke-width="0.13" data-prov="15e0d12c71c4e3aa49c9145482409090" data-source-kind="1" data-kind="stroke"/>
<line x1="17.2558" y1="-20.8926" x2="17.2558" y2="-17.3926" stroke="#000000" stroke-width="0.12" data-prov="b625587ff114949abad6d0b181fbfd20" data-source-kind="0" data-kind="stroke"/> <line x1="17.2558" y1="-33.9526" x2="17.2558" y2="-30.4526" stroke="#000000" stroke-width="0.12" data-prov="b625587ff114949abad6d0b181fbfd20" data-source-kind="0" data-kind="stroke"/>
<line x1="15.8058" y1="-13.8926" x2="17.5864" y2="-13.8926" stroke="#000000" stroke-width="0.13" data-prov="99a975983299cf3260278173c3867b94" data-source-kind="1" data-kind="stroke"/> <line x1="15.8058" y1="-13.8926" x2="17.5864" y2="-13.8926" stroke="#000000" stroke-width="0.13" data-prov="99a975983299cf3260278173c3867b94" data-source-kind="1" data-kind="stroke"/>
<line x1="15.8058" y1="-14.8926" x2="17.5864" y2="-14.8926" stroke="#000000" stroke-width="0.13" data-prov="a53017a9a527ee7597897591ec0e0974" data-source-kind="1" data-kind="stroke"/> <line x1="15.8058" y1="-14.8926" x2="17.5864" y2="-14.8926" stroke="#000000" stroke-width="0.13" data-prov="a53017a9a527ee7597897591ec0e0974" data-source-kind="1" data-kind="stroke"/>
<line x1="15.8058" y1="-15.8926" x2="17.5864" y2="-15.8926" stroke="#000000" stroke-width="0.13" data-prov="250e0863d6d90879e1d890ec74b581c6" data-source-kind="1" data-kind="stroke"/> <line x1="15.8058" y1="-15.8926" x2="17.5864" y2="-15.8926" stroke="#000000" stroke-width="0.13" data-prov="250e0863d6d90879e1d890ec74b581c6" data-source-kind="1" data-kind="stroke"/>
@ -31,32 +31,32 @@
<line x1="15.8058" y1="-18.8926" x2="17.5864" y2="-18.8926" stroke="#000000" stroke-width="0.13" data-prov="74ef3fb044547118b3a45fa7ab21f91f" data-source-kind="1" data-kind="stroke"/> <line x1="15.8058" y1="-18.8926" x2="17.5864" y2="-18.8926" stroke="#000000" stroke-width="0.13" data-prov="74ef3fb044547118b3a45fa7ab21f91f" data-source-kind="1" data-kind="stroke"/>
<line x1="15.8058" y1="-19.8926" x2="17.5864" y2="-19.8926" stroke="#000000" stroke-width="0.13" data-prov="a3f1031ad8b73109c6c7fdebb5a82f88" data-source-kind="1" data-kind="stroke"/> <line x1="15.8058" y1="-19.8926" x2="17.5864" y2="-19.8926" stroke="#000000" stroke-width="0.13" data-prov="a3f1031ad8b73109c6c7fdebb5a82f88" data-source-kind="1" data-kind="stroke"/>
<line x1="15.8058" y1="-20.8926" x2="17.5864" y2="-20.8926" stroke="#000000" stroke-width="0.13" data-prov="14bc5e293e7e48821e34201207cedf7a" data-source-kind="1" data-kind="stroke"/> <line x1="15.8058" y1="-20.8926" x2="17.5864" y2="-20.8926" stroke="#000000" stroke-width="0.13" data-prov="14bc5e293e7e48821e34201207cedf7a" data-source-kind="1" data-kind="stroke"/>
<line x1="19.3364" y1="-17.3926" x2="19.3364" y2="-13.8926" stroke="#000000" stroke-width="0.12" data-prov="7cc4fe5d82ef7386da10c5cef6508856" data-source-kind="0" data-kind="stroke"/> <line x1="19.3364" y1="-30.4526" x2="19.3364" y2="-26.9526" stroke="#000000" stroke-width="0.12" data-prov="7cc4fe5d82ef7386da10c5cef6508856" data-source-kind="0" data-kind="stroke"/>
<line x1="17.8864" y1="-13.8926" x2="19.6671" y2="-13.8926" stroke="#000000" stroke-width="0.13" data-prov="516a866745c72968322bf22e817109f3" data-source-kind="1" data-kind="stroke"/> <line x1="17.8864" y1="-13.8926" x2="19.6671" y2="-13.8926" stroke="#000000" stroke-width="0.13" data-prov="516a866745c72968322bf22e817109f3" data-source-kind="1" data-kind="stroke"/>
<line x1="17.8864" y1="-14.8926" x2="19.6671" y2="-14.8926" stroke="#000000" stroke-width="0.13" data-prov="faa92f6a10e55a97e9b2f7e359f09038" data-source-kind="1" data-kind="stroke"/> <line x1="17.8864" y1="-14.8926" x2="19.6671" y2="-14.8926" stroke="#000000" stroke-width="0.13" data-prov="faa92f6a10e55a97e9b2f7e359f09038" data-source-kind="1" data-kind="stroke"/>
<line x1="17.8864" y1="-15.8926" x2="19.6671" y2="-15.8926" stroke="#000000" stroke-width="0.13" data-prov="c7f132db91c48ecc8015b7e748f00819" data-source-kind="1" data-kind="stroke"/> <line x1="17.8864" y1="-15.8926" x2="19.6671" y2="-15.8926" stroke="#000000" stroke-width="0.13" data-prov="c7f132db91c48ecc8015b7e748f00819" data-source-kind="1" data-kind="stroke"/>
<line x1="17.8864" y1="-16.8926" x2="19.6671" y2="-16.8926" stroke="#000000" stroke-width="0.13" data-prov="867f712bbd9c4c178425fd0f53e01fa2" data-source-kind="1" data-kind="stroke"/> <line x1="17.8864" y1="-16.8926" x2="19.6671" y2="-16.8926" stroke="#000000" stroke-width="0.13" data-prov="867f712bbd9c4c178425fd0f53e01fa2" data-source-kind="1" data-kind="stroke"/>
<line x1="8.6599" y1="-24.8926" x2="8.6599" y2="-24.8926" stroke="#000000" stroke-width="0" data-prov="781ef3842ebea010cbefab514c733440" data-source-kind="2" data-kind="stroke"/> <line x1="8.6599" y1="-37.9526" x2="8.6599" y2="-37.9526" stroke="#000000" stroke-width="0" data-prov="781ef3842ebea010cbefab514c733440" data-source-kind="2" data-kind="stroke"/>
<line x1="13.0944" y1="-25.8926" x2="13.0944" y2="-22.3926" stroke="#000000" stroke-width="0.12" data-prov="8fedf07e00164403db044570899f2550" data-source-kind="0" data-kind="stroke"/> <line x1="13.0944" y1="-38.9526" x2="13.0944" y2="-35.4526" stroke="#000000" stroke-width="0.12" data-prov="8fedf07e00164403db044570899f2550" data-source-kind="0" data-kind="stroke"/>
<line x1="11.6444" y1="-25.8926" x2="13.4251" y2="-25.8926" stroke="#000000" stroke-width="0.13" data-prov="32b5060f271a3aa21b504bfc3160af6d" data-source-kind="1" data-kind="stroke"/> <line x1="11.6444" y1="-38.9526" x2="13.4251" y2="-38.9526" stroke="#000000" stroke-width="0.13" data-prov="32b5060f271a3aa21b504bfc3160af6d" data-source-kind="1" data-kind="stroke"/>
<line x1="15.1751" y1="-22.3926" x2="15.1751" y2="-18.8926" stroke="#000000" stroke-width="0.12" data-prov="19a0fc032dbfc94ac8b2801dfdeaa658" data-source-kind="0" data-kind="stroke"/> <line x1="15.1751" y1="-35.4526" x2="15.1751" y2="-31.9526" stroke="#000000" stroke-width="0.12" data-prov="19a0fc032dbfc94ac8b2801dfdeaa658" data-source-kind="0" data-kind="stroke"/>
<line x1="17.2558" y1="-18.8926" x2="17.2558" y2="-15.3926" stroke="#000000" stroke-width="0.12" data-prov="e4e5f1717e87111231d31ba493aaac57" data-source-kind="0" data-kind="stroke"/> <line x1="17.2558" y1="-31.9526" x2="17.2558" y2="-28.4526" stroke="#000000" stroke-width="0.12" data-prov="e4e5f1717e87111231d31ba493aaac57" data-source-kind="0" data-kind="stroke"/>
<line x1="15.8058" y1="-19.8926" x2="17.5864" y2="-19.8926" stroke="#000000" stroke-width="0.13" data-prov="1caed515110c4415278d31cc474bf065" data-source-kind="1" data-kind="stroke"/> <line x1="15.8058" y1="-32.9526" x2="17.5864" y2="-32.9526" stroke="#000000" stroke-width="0.13" data-prov="1caed515110c4415278d31cc474bf065" data-source-kind="1" data-kind="stroke"/>
<line x1="15.8058" y1="-18.8926" x2="17.5864" y2="-18.8926" stroke="#000000" stroke-width="0.13" data-prov="1189a8cf5e7e1526a44f3cf89e0b160e" data-source-kind="1" data-kind="stroke"/> <line x1="15.8058" y1="-31.9526" x2="17.5864" y2="-31.9526" stroke="#000000" stroke-width="0.13" data-prov="1189a8cf5e7e1526a44f3cf89e0b160e" data-source-kind="1" data-kind="stroke"/>
<line x1="19.3364" y1="-22.3926" x2="19.3364" y2="-18.8926" stroke="#000000" stroke-width="0.12" data-prov="f230acdf26cd79ad3ba5cd02002fa1f9" data-source-kind="0" data-kind="stroke"/> <line x1="19.3364" y1="-35.4526" x2="19.3364" y2="-31.9526" stroke="#000000" stroke-width="0.12" data-prov="f230acdf26cd79ad3ba5cd02002fa1f9" data-source-kind="0" data-kind="stroke"/>
<path d="M 14.8053 -20.1926 C 15.6723 -19.1259 16.5392 -19.1259 17.4062 -20.1926" fill="none" stroke="#000000" stroke-width="0.12" data-prov="0165993515166bea8f7cada46e27c86f" data-source-kind="11" data-kind="curve"/> <path d="M 14.8053 -20.1926 C 15.6723 -19.1259 16.5392 -19.1259 17.4062 -20.1926" fill="none" stroke="#000000" stroke-width="0.12" data-prov="0165993515166bea8f7cada46e27c86f" data-source-kind="11" data-kind="curve"/>
<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(8.6599 -11.8926)" fill="#000000" data-prov="e81b0f9cc6ccb1d0209d51b1d9797db5" data-source-kind="4" data-glyph="gClef" data-class="clef"/> <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(8.6599 -11.8926)" fill="#000000" data-prov="e81b0f9cc6ccb1d0209d51b1d9797db5" data-source-kind="4" data-glyph="gClef" data-class="clef"/>
<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(11.9444 -13.8926)" fill="#000000" data-prov="9b3ac956eb2deb3e538d9b1942c6ae97" data-source-kind="1" data-glyph="noteheadBlack" 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(11.9444 -13.8926)" fill="#000000" data-prov="9b3ac956eb2deb3e538d9b1942c6ae97" data-source-kind="1" data-glyph="noteheadBlack" 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.0251 -17.3926)" fill="#000000" data-prov="751a5a693a87d6c1a15a8dfc3eda8102" data-source-kind="1" data-glyph="noteheadBlack" 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.0251 -17.3926)" fill="#000000" data-prov="751a5a693a87d6c1a15a8dfc3eda8102" data-source-kind="1" data-glyph="noteheadBlack" 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.1058 -20.8926)" fill="#000000" data-prov="130e9eddac04d8c2fb7373aae2c3b458" data-source-kind="1" data-glyph="noteheadBlack" 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.1058 -20.8926)" fill="#000000" data-prov="130e9eddac04d8c2fb7373aae2c3b458" data-source-kind="1" data-glyph="noteheadBlack" 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.1864 -17.3926)" fill="#000000" data-prov="e6f9d102cb9512c808d4bff5a3560b34" data-source-kind="1" data-glyph="noteheadBlack" 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.1864 -17.3926)" fill="#000000" data-prov="e6f9d102cb9512c808d4bff5a3560b34" data-source-kind="1" data-glyph="noteheadBlack" 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(8.6599 -23.8926)" fill="#000000" data-prov="380c7e6f256e62d482135a6342d18380" data-source-kind="4" data-glyph="gClef" data-class="clef"/> <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(8.6599 -36.9526)" fill="#000000" data-prov="380c7e6f256e62d482135a6342d18380" data-source-kind="4" data-glyph="gClef" data-class="clef"/>
<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(11.9444 -25.8926)" fill="#000000" data-prov="38b6271edbb50b409f119eff31d0faa9" data-source-kind="1" data-glyph="noteheadBlack" 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(11.9444 -38.9526)" fill="#000000" data-prov="38b6271edbb50b409f119eff31d0faa9" data-source-kind="1" data-glyph="noteheadBlack" 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.0251 -22.3926)" fill="#000000" data-prov="b2c29b264d8593df8589608aee4d8465" data-source-kind="1" data-glyph="noteheadBlack" 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.0251 -35.4526)" fill="#000000" data-prov="b2c29b264d8593df8589608aee4d8465" data-source-kind="1" data-glyph="noteheadBlack" 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.1058 -18.8926)" fill="#000000" data-prov="a6892ec5562df412ce4bdf3bc118e5e9" data-source-kind="1" data-glyph="noteheadBlack" 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.1058 -31.9526)" fill="#000000" data-prov="a6892ec5562df412ce4bdf3bc118e5e9" data-source-kind="1" data-glyph="noteheadBlack" 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.1864 -22.3926)" fill="#000000" data-prov="d8123de74f57fff5bdc35478e8ec8e9e" data-source-kind="1" data-glyph="noteheadBlack" 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.1864 -35.4526)" fill="#000000" data-prov="d8123de74f57fff5bdc35478e8ec8e9e" data-source-kind="1" data-glyph="noteheadBlack" data-class="notehead"/>
<path d="M0.144 0V4H0V0ZM0.912 4H0.412V0H0.912Z" transform="translate(19.9671 -12.8926)" fill="#000000" data-prov="ed49137c7e716c68d78a85b89b50500a" data-source-kind="9" data-glyph="barlineFinal" data-class="barline"/> <path d="M0.144 0V4H0V0ZM0.912 4H0.412V0H0.912Z" transform="translate(19.9671 -12.8926)" fill="#000000" data-prov="ed49137c7e716c68d78a85b89b50500a" data-source-kind="9" data-glyph="barlineFinal" data-class="barline"/>
<path d="M0.144 0V4H0V0ZM0.912 4H0.412V0H0.912Z" transform="translate(19.9671 -24.8926)" fill="#000000" data-prov="7171ecca2c439b3c03c959bdad56cf08" data-source-kind="9" data-glyph="barlineFinal" data-class="barline"/> <path d="M0.144 0V4H0V0ZM0.912 4H0.412V0H0.912Z" transform="translate(19.9671 -37.9526)" fill="#000000" data-prov="7171ecca2c439b3c03c959bdad56cf08" data-source-kind="9" data-glyph="barlineFinal" data-class="barline"/>
</g> </g>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB