diff --git a/crates/epiphany-engrave/src/lib.rs b/crates/epiphany-engrave/src/lib.rs index c88d6e0..3add97b 100644 --- a/crates/epiphany-engrave/src/lib.rs +++ b/crates/epiphany-engrave/src/lib.rs @@ -45,12 +45,30 @@ use std::collections::BTreeMap; use epiphany_layout_ir::{ all_available, Axis, BravuraCatalog, BreakKind, ConstrainedLayoutIR, ConstraintId, - ConstraintSolver, GlyphCatalog, GlyphObjectId, InvalidationSet, LayoutConstraint, Margins, - Point, QualityMetricVector, Rect, ResolvedGlyph, ResolvedLayoutIR, ResolvedPage, + ConstraintSolver, GlyphCatalog, GlyphObject, GlyphObjectId, InvalidationSet, LayoutConstraint, + Margins, Point, QualityMetricVector, Rect, ResolvedGlyph, ResolvedLayoutIR, ResolvedPage, ResolvedSystem, Size2D, SolveReport, SolveStatus, SolverBudgetUsed, SolverConfig, SolverState, SolverTier, SolverVersion, SolverWarning, SolverWarningKind, Stroke, }; +/// The glyph a fixed-width stroke (a ledger line) belongs to: the same-source glyph +/// whose baseline falls within the stroke's horizontal span (its accidentals sit +/// outside the span, to the left). The stroke is anchored to this glyph's column so +/// it translates with it — found by source, never inferred from the stroke's own +/// midpoint, which for a wide head can fall nearer a neighbouring column. +pub(crate) fn owning_glyph<'a>( + stroke: &Stroke, + glyphs: &'a [GlyphObject], +) -> Option<&'a GlyphObject> { + let lo = stroke.from.x.0.min(stroke.to.x.0); + let hi = stroke.from.x.0.max(stroke.to.x.0); + glyphs.iter().find(|g| { + g.provenance.source == stroke.provenance.source + && g.baseline.x.0 >= lo + && g.baseline.x.0 <= hi + }) +} + /// The Epiphany engraving solver (Chapter 9). A `Minimal`-tier solver: it spaces /// glyphs horizontally and satisfies the IR's declared hard constraints. See the /// crate docs for what each tier claims and what remains deferred. @@ -248,18 +266,39 @@ impl HorizontalRemap { .collect() } - /// Re-maps both endpoints of each stroke, so it tracks the glyphs it spans. + /// Re-maps each stroke's endpoints so it tracks the glyphs it spans. A + /// system-spanning stroke (staff line, barline, …) maps both endpoints, so it + /// stretches with the spacing; a fixed-width stroke (a ledger line on one + /// notehead) is translated rigidly by its owning column's delta + /// ([`epiphany_layout_ir::is_rigid_width_stroke`]) — preserving both its length + /// and its offset from its glyph, which maps by that same delta at its column. fn strokes(&self, input: &ConstrainedLayoutIR) -> Vec { input .strokes .iter() - .map(|s| Stroke { - provenance: s.provenance.clone(), - from: Point::new(self.map(s.from.x.0), s.from.y.0), - to: Point::new(self.map(s.to.x.0), s.to.y.0), - thickness: s.thickness, - layer: s.layer, - style: s.style, + .map(|s| { + let (from_x, to_x) = if epiphany_layout_ir::is_rigid_width_stroke(s) { + // Translate rigidly by the *owning glyph's* column delta — found by + // source, not the stroke's midpoint, which for a wide head could + // pick a neighbouring column and reintroduce drift. The glyph + // baseline is a control point, so `map(baseline) − baseline` is its + // exact column delta; applying it keeps the stroke's offset from the + // glyph and its length. + let delta = owning_glyph(s, &input.glyphs) + .map(|g| self.map(g.baseline.x.0) - g.baseline.x.0) + .unwrap_or(0.0); + (s.from.x.0 + delta, s.to.x.0 + delta) + } else { + (self.map(s.from.x.0), self.map(s.to.x.0)) + }; + Stroke { + provenance: s.provenance.clone(), + from: Point::new(from_x, s.from.y.0), + to: Point::new(to_x, s.to.y.0), + thickness: s.thickness, + layer: s.layer, + style: s.style, + } }) .collect() } @@ -395,7 +434,7 @@ impl ConstraintSolver for Engraver { mod tests { use super::*; use epiphany_core::generators::valid_score_rich; - use epiphany_layout_ir::{to_constrained, to_logical, StubSolver}; + use epiphany_layout_ir::{is_rigid_width_stroke, to_constrained, to_logical, StubSolver}; fn fixture() -> ConstrainedLayoutIR { to_constrained(&to_logical(&valid_score_rich(11))) @@ -569,6 +608,270 @@ mod tests { ); } + #[test] + fn ledger_lines_keep_their_width_through_the_spacing_pass() { + // A corpus score with off-staff notes yields ledger strokes; the horizontal + // spacing pass must translate them (preserving length), not re-map both + // endpoints — which would scale a fixed-width mark with the local spacing. + let mut checked = 0; + for seed in 0..16 { + let input = to_constrained(&to_logical(&valid_score_rich(seed))); + let widths: std::collections::HashMap = input + .strokes + .iter() + .filter(|s| is_rigid_width_stroke(s)) + .map(|s| (s.provenance.stable_id.0, s.to.x.0 - s.from.x.0)) + .collect(); + if widths.is_empty() { + continue; + } + let report = Engraver.solve(&input, &SolverConfig::default()); + for s in report + .layout + .strokes + .iter() + .filter(|s| is_rigid_width_stroke(s)) + { + if let Some(&w_in) = widths.get(&s.provenance.stable_id.0) { + let w_out = s.to.x.0 - s.from.x.0; + assert!( + (w_out - w_in).abs() < 1e-4, + "ledger width changed through spacing: {w_in} -> {w_out}" + ); + checked += 1; + } + } + } + assert!(checked > 0, "no ledger strokes exercised across 16 seeds"); + } + + /// Two whole notes (wide heads) a step above the staff, in adjacent time + /// columns — the case where a ledger's own midpoint can fall nearer the next + /// column than its notehead's. + fn two_off_staff_whole_notes() -> ConstrainedLayoutIR { + use epiphany_core::{ + CmnNominal, EventId, MusicalDuration, MusicalPosition, NotatedComponent, NoteValue, + PitchId, PitchSpelling, RationalTime, RegionId, StaffId, StaffInstanceId, + TypedObjectId, + }; + use epiphany_layout_ir::{ + LayoutContent, LayoutObject, LayoutRegion, LocalCoordinateSystem, LogicalLayoutIR, + MetricTimeAxis, NoteContent, NotePitch, PlacedComponent, Provenance, ScoreVersion, + StaffContent, TimeAxisModel, TimePoint, VerticalExtent, + }; + let region = RegionId::from_raw(1); + let staff = StaffId::from_raw(10); + let manifested = |src, content| { + LayoutObject::from_projection_with_content( + Provenance::manifested(src, region, vec![]), + Some(staff), + content, + ) + }; + let whole = || { + vec![PlacedComponent { + offset: MusicalDuration::zero(), + component: NotatedComponent { + base_value: NoteValue::Whole, + dots: 0, + tuplet: None, + tied_to_next: false, + }, + tuplet: None, + }] + }; + let note = |eid: u128, pid: u128, pos: MusicalPosition| { + let pitch = PitchId::from_raw(pid); + [ + manifested( + TypedObjectId::Event(EventId::from_raw(eid)), + LayoutContent::Note(NoteContent { + position: TimePoint::Musical(pos), + components: whole(), + // C6 is a step above the treble staff, so each head earns + // ledger lines. + pitches: vec![NotePitch { + pitch, + spelling: Some(PitchSpelling::cmn(CmnNominal::C, 6)), + }], + }), + ), + manifested(TypedObjectId::Pitch(pitch), LayoutContent::Structural), + ] + }; + let mut objects = vec![manifested( + TypedObjectId::StaffInstance(StaffInstanceId::from_raw(1)), + LayoutContent::Staff(StaffContent { + clefs: vec![], + keys: vec![], + }), + )]; + objects.extend(note(1, 101, MusicalPosition::origin())); + objects.extend(note( + 2, + 102, + MusicalPosition(RationalTime::new(1, 1).unwrap()), + )); + let logical = LogicalLayoutIR { + source: ScoreVersion::default(), + regions: vec![LayoutRegion { + provenance: Provenance::projected(TypedObjectId::Region(region), vec![]), + coordinate_system: LocalCoordinateSystem::default(), + time_axis: TimeAxisModel::Metric(MetricTimeAxis::default()), + vertical_extent: VerticalExtent { + staves: vec![staff], + }, + objects, + }], + engraving_decisions: vec![], + overrides: vec![], + cross_region: vec![], + }; + to_constrained(&logical) + } + + #[test] + fn an_off_staff_whole_note_ledger_does_not_drift() { + let input = two_off_staff_whole_notes(); + assert!( + input + .glyphs + .iter() + .any(|g| g.glyph.as_str() == "noteheadWhole"), + "the fixture engraves whole notes" + ); + let ledger_count = input + .strokes + .iter() + .filter(|s| is_rigid_width_stroke(s)) + .count(); + assert!( + ledger_count >= 2, + "off-staff whole notes earn ledger strokes" + ); + + let report = Engraver.solve(&input, &SolverConfig::default()); + // The wide whole-note columns re-space (their deltas differ), so a midpoint- + // anchored ledger would translate by a neighbouring column's delta and drift. + // The owning-glyph anchor keeps every ledger at its notehead's offset. + for s_in in input.strokes.iter().filter(|s| is_rigid_width_stroke(s)) { + let g_in = owning_glyph(s_in, &input.glyphs).expect("owning notehead"); + let s_out = report + .layout + .strokes + .iter() + .find(|s| s.provenance.stable_id == s_in.provenance.stable_id) + .expect("stroke survives"); + let g_out = report + .layout + .glyphs + .iter() + .find(|g| g.provenance.stable_id == g_in.provenance.stable_id) + .expect("glyph survives"); + let offset_in = s_in.from.x.0 - g_in.baseline.x.0; + let offset_out = s_out.from.x.0 - g_out.position.x.0; + assert!( + (offset_out - offset_in).abs() < 1e-4, + "whole-note ledger drifted {offset_in} -> {offset_out}" + ); + } + } + + #[test] + fn ledger_offsets_from_the_notehead_survive_the_engraver() { + // The column-delta translation keeps a ledger at exactly the same offset from + // its notehead through the spacing pass; interpolating its midpoint (the bug + // this replaced) would shift it under a non-unit local slope. + let mut checked = 0; + for seed in 0..16 { + let input = to_constrained(&to_logical(&valid_score_rich(seed))); + let report = Engraver.solve(&input, &SolverConfig::default()); + for s_in in input.strokes.iter().filter(|s| is_rigid_width_stroke(s)) { + let lo = s_in.from.x.0.min(s_in.to.x.0); + let hi = s_in.from.x.0.max(s_in.to.x.0); + let Some(g_in) = input.glyphs.iter().find(|g| { + g.provenance.source == s_in.provenance.source + && g.baseline.x.0 >= lo + && g.baseline.x.0 <= hi + }) else { + continue; + }; + let Some(s_out) = report + .layout + .strokes + .iter() + .find(|s| s.provenance.stable_id == s_in.provenance.stable_id) + else { + continue; + }; + let Some(g_out) = report + .layout + .glyphs + .iter() + .find(|g| g.provenance.stable_id == g_in.provenance.stable_id) + else { + continue; + }; + let offset_in = s_in.from.x.0 - g_in.baseline.x.0; + let offset_out = s_out.from.x.0 - g_out.position.x.0; + assert!( + (offset_out - offset_in).abs() < 1e-4, + "seed {seed}: ledger offset drifted {offset_in} -> {offset_out}" + ); + checked += 1; + } + } + assert!(checked > 0, "no ledger/notehead pairs exercised"); + } + + #[test] + fn adjacent_ledger_lines_are_spaced_not_overlapping() { + use std::collections::HashMap; + // The spacing pass reserves room for ledger overhang, so two off-staff notes + // that share a ledger height (same step) and sit in neighbouring columns get + // ledger strokes that do not overlap. + let mut ledgers = 0; + let mut pairs = 0; + for seed in 0..32 { + let report = Engraver.solve( + &to_constrained(&to_logical(&valid_score_rich(seed))), + &SolverConfig::default(), + ); + let mut by_height: HashMap> = HashMap::new(); + for s in report + .layout + .strokes + .iter() + .filter(|s| is_rigid_width_stroke(s)) + { + ledgers += 1; + let y = (s.from.y.0 * 1024.0).round() as i64; + let (lo, hi) = (s.from.x.0.min(s.to.x.0), s.from.x.0.max(s.to.x.0)); + by_height + .entry(y) + .or_default() + .push((lo, hi, s.provenance.source)); + } + for group in by_height.values_mut() { + group.sort_by(|a, b| a.0.total_cmp(&b.0)); + for w in group.windows(2) { + // Distinct notes' ledgers at the same height must not overlap. + if w[0].2 != w[1].2 { + pairs += 1; + assert!( + w[0].1 <= w[1].0 + 1e-3, + "seed {seed}: ledger lines overlap ({:?} vs {:?})", + w[0], + w[1] + ); + } + } + } + } + assert!(ledgers > 0, "the corpus exercised no ledger lines"); + assert!(pairs > 0, "no adjacent same-height ledger pairs to check"); + } + #[test] fn solves_the_stub_pipeline_and_preserves_provenance() { let input = fixture(); diff --git a/crates/epiphany-engrave/src/spacing.rs b/crates/epiphany-engrave/src/spacing.rs index 66449af..561219d 100644 --- a/crates/epiphany-engrave/src/spacing.rs +++ b/crates/epiphany-engrave/src/spacing.rs @@ -20,7 +20,9 @@ use std::collections::BTreeMap; -use epiphany_layout_ir::{ConstrainedLayoutIR, SpringSlotId}; +use epiphany_layout_ir::{is_rigid_width_stroke, ConstrainedLayoutIR, SpringSlotId}; + +use crate::owning_glyph; /// Inter-slot gap (staff spaces) reserved between one slot's right content and /// the next slot's left content. @@ -71,6 +73,25 @@ pub(crate) fn control_points(input: &ConstrainedLayoutIR) -> Vec<(f32, f32)> { }); } + // Fold each ledger line (a fixed-width stroke) into its notehead's slot extent, + // so a ledger that overhangs the notehead reserves room — otherwise adjacent + // off-staff notes' ledgers can overlap even though glyph spacing is collision- + // aware. The owning notehead is the same-source glyph whose baseline lies within + // the stroke's span (its accidentals sit outside it, to the left). + for stroke in &input.strokes { + if !is_rigid_width_stroke(stroke) { + continue; + } + if let Some(glyph) = owning_glyph(stroke, &input.glyphs) { + let lo = stroke.from.x.0.min(stroke.to.x.0); + let hi = stroke.from.x.0.max(stroke.to.x.0); + if let Some(extent) = by_slot.get_mut(&glyph.horizontal_slot) { + extent.min_left = extent.min_left.min(lo); + extent.max_right = extent.max_right.max(hi); + } + } + } + let mut slots: Vec = by_slot.into_values().collect(); slots.sort_by(|a, b| { a.source diff --git a/crates/epiphany-layout-ir/src/constrained.rs b/crates/epiphany-layout-ir/src/constrained.rs index 6b2ead3..9ab0648 100644 --- a/crates/epiphany-layout-ir/src/constrained.rs +++ b/crates/epiphany-layout-ir/src/constrained.rs @@ -464,6 +464,8 @@ const TIME_DIGIT_X: f32 = 0.8; // x advance per time-signature digit /// five lines share its source, so four of them must be synthesized to earn /// distinct stable ids; this is the kind they declare. const STAFF_LINE_SYNTHESIS: SynthesisRegistryId = SynthesisRegistryId(0x5354_4146_465F_4C4E); // "STAFFLN" +const LEDGER_LINE_SYNTHESIS: SynthesisRegistryId = SynthesisRegistryId(0x4C45_4447_4552_4C4E); // "LEDGERLN" +const LEDGER_LINE_EXTENSION: f32 = 0.3; // a ledger line reaches this far past the notehead, each side /// The registry id for **notated-component synthesis**: a note/rest notated as a /// tied decomposition (e.g. a quarter tied to an eighth across a barline) draws @@ -654,6 +656,7 @@ pub fn try_to_constrained( name, key: key.clone(), y, + step, comp, accidentals, }); @@ -984,6 +987,31 @@ pub fn try_to_constrained( staff, info.slot, ); + // Ledger lines: short strokes continuing the staff to a + // notehead above or below it, one per whole step between + // the staff and the note, reaching `LEDGER_LINE_EXTENSION` + // past each side of *this notehead's* drawn box — so a + // wider head (a whole note) gets a wider ledger. Synthesized + // from the pitch; render-svg draws strokes under glyphs at a + // layer, so the notehead sits over them. + let head_box = metrics(head.name).map(|m| m.bounding_box()); + let head_left = head_box.map_or(0.0, |b| b.left.0); + let head_right = head_box.map_or(NOTEHEAD_STEM_X, |b| b.right.0); + for ledger_step in ledger_steps(head.step) { + let y = step_to_y(yo, ledger_step); + let ledger_provenance = Provenance::synthesized( + provenance.source, + SynthesisKind::Registered(LEDGER_LINE_SYNTHESIS), + ledger_line_key(head.comp, ledger_step), + provenance.dependencies.clone(), + ); + emit.stroke(line_stroke( + ledger_provenance, + Point::new(info.x + head_left - LEDGER_LINE_EXTENSION, y), + Point::new(info.x + head_right + LEDGER_LINE_EXTENSION, y), + STAFF_LINE_THICKNESS, + )); + } // The spelling's accidental stack: synthesized glyphs // left of the notehead (innermost nearest it), at its // staff position, sharing the notehead's column slot. @@ -1188,6 +1216,8 @@ struct Head { name: &'static str, key: ColumnKey, y: f32, + /// The note's diatonic staff position, for ledger-line emission. + step: StaffStep, comp: usize, /// The spelling's accidental stack (innermost — nearest the notehead — first), /// each drawn left of the notehead. Present only on the first component (a tie @@ -1532,6 +1562,49 @@ fn step_to_y(y_origin: f32, step: StaffStep) -> f32 { y_origin + step as f32 * 0.5 } +/// The staff steps at which a note at `step` needs ledger lines: the even steps +/// strictly outside the five-line staff (whose lines are the even steps `0..=8`), +/// from the staff out to the note. Empty when the note is on or within the staff, +/// or one space just outside it (an odd step at `±1` from the nearest line). At +/// most one of the two loops runs, since a step cannot be both above and below. +fn ledger_steps(step: StaffStep) -> Vec { + let mut steps = Vec::new(); + let mut above = 10; + while above <= step { + steps.push(above); + above += 2; + } + let mut below = -2; + while below >= step { + steps.push(below); + below -= 2; + } + steps +} + +/// A distinct synthesis key for a ledger line on component `comp` at diatonic +/// `step`. The component occupies the high 64 bits and the signed step the low 64, +/// so the two fields never overlap — including a step below `-128`, whose +/// two's-complement low bits would otherwise reach into the component field and let +/// two components of a very low pitch mint colliding stable ids. +fn ledger_line_key(comp: usize, step: StaffStep) -> SynthesisInstanceKey { + SynthesisInstanceKey(((comp as u128) << 64) | (step as i64 as u64 as u128)) +} + +/// Whether a stroke must keep a **fixed width** when a solver resolves horizontal +/// spacing: its length is a glyph-relative constant, not a span across the columns +/// the spacing pass stretches. A solver should translate such a stroke (preserving +/// its length) rather than re-map both endpoints, which would scale it. Ledger lines +/// are the case today — a fixed-width mark centered on one notehead, unlike a staff +/// line or barline that genuinely spans the system. Public so the constraint solver +/// can honor it without hard-coding the ledger synthesis identity. +pub fn is_rigid_width_stroke(stroke: &Stroke) -> bool { + matches!( + stroke.provenance.synthesis, + Some(SynthesisKind::Registered(k)) if k == LEDGER_LINE_SYNTHESIS + ) +} + /// The staff step of a clef's reference line — a neutral fallback position for a /// pitch whose spelling does not resolve to a CMN nominal. fn reference_step(clef: &Clef) -> StaffStep { @@ -1627,6 +1700,93 @@ mod tests { use epiphany_core::generators::valid_score_rich; use std::collections::BTreeSet; + #[test] + fn ledger_steps_cover_only_lines_outside_the_staff() { + // Within the five-line staff (steps 0..=8) and one space just outside: none. + for step in [-1, 0, 4, 8, 9] { + assert!(ledger_steps(step).is_empty(), "no ledger at step {step}"); + } + // First ledger above (step 10) and below (-2): exactly one line, on the note. + assert_eq!(ledger_steps(10), vec![10]); + assert_eq!(ledger_steps(-2), vec![-2]); + // A note in the space above the first ledger still needs just that line. + assert_eq!(ledger_steps(11), vec![10]); + // Two lines above / below, in order from the staff outward. + assert_eq!(ledger_steps(12), vec![10, 12]); + assert_eq!(ledger_steps(-4), vec![-2, -4]); + } + + #[test] + fn ledger_line_keys_are_distinct_across_components_and_low_steps() { + // The high/low 64-bit split keeps the component and step fields disjoint — + // including a step below -128, whose two's-complement low bits are large and, + // under a naive `(comp << small) | (step + bias)`, would reach into the + // component field and collide with another component's key. + let mut seen = std::collections::HashSet::new(); + for comp in 0..3usize { + for step in [-300, -200, -130, -128, -2, 10, 200] { + assert!( + seen.insert(ledger_line_key(comp, step).0), + "ledger key collision at comp={comp} step={step}" + ); + } + } + } + + #[test] + fn a_ledger_line_spans_its_notehead() { + // The ledger reaches past the notehead on both sides, for any notehead width + // — a whole note's head is wider than a black head, and the ledger must use + // the real bounding box, not a fixed notehead width. + let mut checked = 0; + for seed in 0..32 { + let c = to_constrained(&to_logical(&valid_score_rich(seed))); + for s in c.strokes.iter().filter(|s| is_rigid_width_stroke(s)) { + let lo = s.from.x.0.min(s.to.x.0); + let hi = s.from.x.0.max(s.to.x.0); + // The owning notehead: same source, baseline within the stroke span. + if let Some(g) = c.glyphs.iter().find(|g| { + g.provenance.source == s.provenance.source + && g.baseline.x.0 >= lo + && g.baseline.x.0 <= hi + }) { + let head_left = g.baseline.x.0 + g.bounding_box.left.0; + let head_right = g.baseline.x.0 + g.bounding_box.right.0; + assert!( + lo <= head_left + 1e-4 && hi >= head_right - 1e-4, + "seed {seed}: ledger [{lo}, {hi}] does not span notehead [{head_left}, {head_right}]" + ); + checked += 1; + } + } + } + assert!(checked > 0, "no ledger/notehead pairs to check"); + } + + #[test] + fn a_note_far_above_the_staff_gets_ledger_strokes() { + // A constrained layout of the rich corpus has noteheads across the range; at + // least one sits far enough off the staff to earn a ledger line, emitted as a + // synthesized stroke sourced from its pitch. + let mut any_ledger = false; + for seed in 0..16 { + let c = to_constrained(&to_logical(&valid_score_rich(seed))); + if c.strokes.iter().any(|s| { + matches!( + s.provenance.synthesis, + Some(SynthesisKind::Registered(k)) if k == LEDGER_LINE_SYNTHESIS + ) + }) { + any_ledger = true; + break; + } + } + assert!( + any_ledger, + "no ledger-line strokes across 16 rich-corpus seeds" + ); + } + #[test] fn out_of_range_finite_stroke_thickness_is_rejected() { let mut c = to_constrained(&to_logical(&valid_score_rich(11))); diff --git a/crates/epiphany-layout-ir/src/lib.rs b/crates/epiphany-layout-ir/src/lib.rs index 46eb0f8..612a763 100644 --- a/crates/epiphany-layout-ir/src/lib.rs +++ b/crates/epiphany-layout-ir/src/lib.rs @@ -90,8 +90,8 @@ pub use cache::{ ResolvedSystemCache, SystemId, }; pub use constrained::{ - to_constrained, try_to_constrained, Axis, BreakKind, ConstrainedLayoutIR, - ConstrainedLayoutRegion, ConstrainedValidationError, ConstraintParameters, + is_rigid_width_stroke, to_constrained, try_to_constrained, Axis, BreakKind, + ConstrainedLayoutIR, ConstrainedLayoutRegion, ConstrainedValidationError, ConstraintParameters, ConstraintRegistryId, GlyphObject, GlyphObjectId, GlyphStyle, LayoutConstraint, LayoutTransformError, SpringSlot, Stroke, }; diff --git a/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.engrave.snapshot.txt b/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.engrave.snapshot.txt index 44b9459..6d1fdab 100644 --- a/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.engrave.snapshot.txt +++ b/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.engrave.snapshot.txt @@ -2,12 +2,12 @@ fixture=ten_measure_single_staff solver=engrave glyph_count=51 path_count=51 fallback_rect_count=0 -stroke_count=51 -provenance_count=102 +stroke_count=91 +provenance_count=142 layer_count=1 hard_constraint_count=0 xml_well_formed=true -view_box=[-3.059857 -3.632 82.456436 11.024] +view_box=[-3.059857 -3.632 102.98304 11.024] class_counts: barline=10 clef=1 diff --git a/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.engrave.svg b/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.engrave.svg index a7a82ac..e41a03b 100644 --- a/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.engrave.svg +++ b/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.engrave.svg @@ -1,110 +1,150 @@ - + - - - - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - + + + + + + + + + diff --git a/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.stub.snapshot.txt b/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.stub.snapshot.txt index a70b3c4..363ec67 100644 --- a/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.stub.snapshot.txt +++ b/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.stub.snapshot.txt @@ -2,8 +2,8 @@ fixture=ten_measure_single_staff solver=stub glyph_count=51 path_count=51 fallback_rect_count=0 -stroke_count=51 -provenance_count=102 +stroke_count=91 +provenance_count=142 layer_count=1 hard_constraint_count=0 xml_well_formed=true diff --git a/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.stub.svg b/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.stub.svg index 5713f7d..c11f5c3 100644 --- a/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.stub.svg +++ b/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.stub.svg @@ -11,45 +11,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crates/epiphany-render-svg/tests/golden/valid_score_rich.engrave.snapshot.txt b/crates/epiphany-render-svg/tests/golden/valid_score_rich.engrave.snapshot.txt index dd06d51..caa35f9 100644 --- a/crates/epiphany-render-svg/tests/golden/valid_score_rich.engrave.snapshot.txt +++ b/crates/epiphany-render-svg/tests/golden/valid_score_rich.engrave.snapshot.txt @@ -2,12 +2,12 @@ fixture=valid_score_rich solver=engrave glyph_count=11 path_count=11 fallback_rect_count=0 -stroke_count=33 -provenance_count=44 +stroke_count=38 +provenance_count=49 layer_count=1 hard_constraint_count=0 xml_well_formed=true -view_box=[-3.059857 -3.632 27.953568 11.024] +view_box=[-3.1598568 -3.632 31.588375 11.024] class_counts: barline=1 clef=3 diff --git a/crates/epiphany-render-svg/tests/golden/valid_score_rich.engrave.svg b/crates/epiphany-render-svg/tests/golden/valid_score_rich.engrave.svg index ba6e080..7c71015 100644 --- a/crates/epiphany-render-svg/tests/golden/valid_score_rich.engrave.svg +++ b/crates/epiphany-render-svg/tests/golden/valid_score_rich.engrave.svg @@ -1,52 +1,57 @@ - + - + - - - - - + + + + + - - - + + + + + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + diff --git a/crates/epiphany-render-svg/tests/golden/valid_score_rich.stub.snapshot.txt b/crates/epiphany-render-svg/tests/golden/valid_score_rich.stub.snapshot.txt index 263543e..6597dcb 100644 --- a/crates/epiphany-render-svg/tests/golden/valid_score_rich.stub.snapshot.txt +++ b/crates/epiphany-render-svg/tests/golden/valid_score_rich.stub.snapshot.txt @@ -2,8 +2,8 @@ fixture=valid_score_rich solver=stub glyph_count=11 path_count=11 fallback_rect_count=0 -stroke_count=33 -provenance_count=44 +stroke_count=38 +provenance_count=49 layer_count=1 hard_constraint_count=0 xml_well_formed=true diff --git a/crates/epiphany-render-svg/tests/golden/valid_score_rich.stub.svg b/crates/epiphany-render-svg/tests/golden/valid_score_rich.stub.svg index a69b0f7..be002e1 100644 --- a/crates/epiphany-render-svg/tests/golden/valid_score_rich.stub.svg +++ b/crates/epiphany-render-svg/tests/golden/valid_score_rich.stub.svg @@ -11,8 +11,11 @@ + + + @@ -26,6 +29,7 @@ + @@ -35,6 +39,7 @@ +