epiphany/crates/epiphany-testkit/src/fixtures.rs

994 lines
37 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Hand-built score fixtures the testkit needs but Agent B's generators do not
//! provide in the exact shape an acceptance criterion names.
//!
//! In particular the layout hand-off case (QUICKSTART, Agent E) is *"a 10-measure
//! single-staff score"*, which [`ten_measure_single_staff`] builds — Agent B's
//! [`valid_score`](epiphany_core::generators::valid_score) is 12 staves with no
//! measures, and [`valid_score_rich`](epiphany_core::generators::valid_score_rich)
//! is three staves. The fixture is invariant-clean and carries measures plus a
//! tie, spanner, marker, and chord symbol so the layout projection exercises the
//! cross-cutting objects.
use epiphany_core::{
AcousticPitch, AcousticRealization, AnchorOffset, Canvas, ChordSymbol, CmnNominal,
CrossCuttingRegistry, CurvatureOverride, CurveDirection, Event, EventArena, EventDuration,
EventPosition, IdentifiedPitch, IdentityContext, LineStyle, Marker, Measure, MeasurePosition,
MetricTimeModel, MusicalDuration, MusicalPosition, Pitch, PitchSpaceId, PitchSpacePosition,
RationalTime, RegionContent, RegionEdge, RegionTimeModel, RepeatKind, RepeatStructure,
ScalePosition, Score, Slur, SlurKind, SpaceUnit, SpanStyle, Spanner, Staff, StaffBasedContent,
StaffExtent, StaffInstance, StaffLineConfiguration, StemConfiguration, Tie, TieClass,
TimeAnchor, TimeExtent, TuningReference, Voice, Volta, WallClockTime,
};
use epiphany_core::{
ChordSymbolId, EventId, InstrumentId, MarkerId, MeasureId, PitchId, RegionId,
RepeatStructureId, ReplicaId, SlurId, SpannerId, StaffId, StaffInstanceId, TieId, VoiceId,
};
use epiphany_determinism::CanonicalF64;
use epiphany_determinism::fuzz::SplitMix64;
/// A C4 pitched quarter-note at musical position `index/4`. All notes are C4 so
/// the tie between adjacent events is enharmonically valid.
fn quarter(eid: EventId, voice: VoiceId, pid: PitchId, index: i64) -> Event {
Event::Pitched(epiphany_core::PitchedEvent {
id: eid,
voice,
position: EventPosition::Musical(MusicalPosition(RationalTime::new(index, 4).unwrap())),
duration: EventDuration::Musical(MusicalDuration(RationalTime::new(1, 4).unwrap())),
pitches: vec![IdentifiedPitch {
id: pid,
pitch: Pitch {
scale_position: ScalePosition {
space: PitchSpaceId::new("cmn-12"),
position: PitchSpacePosition::Cmn {
nominal: CmnNominal::C,
alteration: 0,
octave: 4,
},
},
acoustic: AcousticPitch {
tuning: TuningReference::Inherit,
realization: AcousticRealization::Implicit,
},
},
}],
articulations: vec![],
dynamic: None,
ornaments: vec![],
stem: StemConfiguration,
grace: None,
})
}
/// A C pitched quarter-note at `octave`, at musical position `index/4`.
fn c_at(eid: EventId, voice: VoiceId, pid: PitchId, index: i64, octave: i8) -> Event {
Event::Pitched(epiphany_core::PitchedEvent {
id: eid,
voice,
position: EventPosition::Musical(MusicalPosition(RationalTime::new(index, 4).unwrap())),
duration: EventDuration::Musical(MusicalDuration(RationalTime::new(1, 4).unwrap())),
pitches: vec![IdentifiedPitch {
id: pid,
pitch: Pitch {
scale_position: ScalePosition {
space: PitchSpaceId::new("cmn-12"),
position: PitchSpacePosition::Cmn {
nominal: CmnNominal::C,
alteration: 0,
octave,
},
},
acoustic: AcousticPitch {
tuning: TuningReference::Inherit,
realization: AcousticRealization::Implicit,
},
},
}],
articulations: vec![],
dynamic: None,
ornaments: vec![],
stem: StemConfiguration,
grace: None,
})
}
/// A one-measure, TWO-staff (treble/treble) metric score whose staves carry
/// vertically extreme content: the top staff dips to low ledger notes (down to
/// C2) while the bottom staff climbs to high ledger notes (up to C6) under a
/// slur that arcs further above them. At the engraver's default *fixed* staff
/// pitch the two staves' content nearly collides — the inter-staff pressure case
/// the vertical spring solve must separate. Invariant-clean.
pub fn two_staff_close_content(seed: u64) -> Score {
let mut rng = SplitMix64::new(seed ^ 0x0002_57AF_F123);
let replica =
ReplicaId::from_entropy(rng.next_u64().to_le_bytes()).unwrap_or(ReplicaId(0x2ADF));
let mut idc = IdentityContext::new(replica);
let top_staff: StaffId = idc.mint();
let bottom_staff: StaffId = idc.mint();
let instrument: InstrumentId = idc.mint();
let region_id: RegionId = idc.mint();
let top_instance: StaffInstanceId = idc.mint();
let bottom_instance: StaffInstanceId = idc.mint();
let top_voice: VoiceId = idc.mint();
let bottom_voice: VoiceId = idc.mint();
let mut arena = EventArena::new();
// Top staff descends into low ledgers; bottom staff climbs into high ones.
let mut top_v = Voice::user(top_voice);
let mut bot_v = Voice::user(bottom_voice);
let mut bot_events: Vec<EventId> = Vec::new();
for (i, oct) in [4i8, 3, 2, 3].into_iter().enumerate() {
let (eid, pid): (EventId, PitchId) = (idc.mint(), idc.mint());
arena
.insert(c_at(eid, top_voice, pid, i as i64, oct))
.unwrap();
top_v.events.push(eid);
}
for (i, oct) in [4i8, 5, 6, 5].into_iter().enumerate() {
let (eid, pid): (EventId, PitchId) = (idc.mint(), idc.mint());
arena
.insert(c_at(eid, bottom_voice, pid, i as i64, oct))
.unwrap();
bot_v.events.push(eid);
bot_events.push(eid);
}
let measure = |id: MeasureId| Measure {
id,
start: TimeAnchor::Region {
id: region_id,
edge: RegionEdge::Start,
offset: AnchorOffset::Zero,
},
time_signature: None,
explicit_number: Some(1),
number_visibility: Default::default(),
};
let mut top_inst = StaffInstance::new(top_instance, top_staff);
top_inst.voices.push(top_v);
top_inst.measures.push(measure(idc.mint()));
let mut bot_inst = StaffInstance::new(bottom_instance, bottom_staff);
bot_inst.voices.push(bot_v);
bot_inst.measures.push(measure(idc.mint()));
// A slur over the bottom staff's high notes, arcing further up — extra
// upward extent that sharpens the collision with the top staff.
let mut cross_cutting = CrossCuttingRegistry::default();
cross_cutting.slurs.push(Slur {
id: idc.mint::<SlurId>(),
start_event: bot_events[1],
end_event: bot_events[3],
kind: SlurKind::Legato,
curvature_override: None,
style: SpanStyle::default(),
});
let region = epiphany_core::Region {
id: region_id,
time_model: RegionTimeModel::Metric(MetricTimeModel::default()),
content: RegionContent::StaffBased(StaffBasedContent {
staff_instances: vec![top_inst, bot_inst],
..Default::default()
}),
time_extent: TimeExtent {
start: TimeAnchor::WallClock {
time: WallClockTime(0),
},
end: TimeAnchor::WallClock {
time: WallClockTime(10_000_000),
},
},
staff_extent: StaffExtent {
staves: vec![top_staff, bottom_staff],
},
local_tempo_map: None,
permits_spanning_slurs: false,
};
let staff = |id: StaffId, name: &str| Staff {
id,
name: String::from(name),
abbreviation: None,
instrument,
default_staff_lines: StaffLineConfiguration::default(),
group: None,
default_clef: epiphany_core::Clef::treble(),
};
let mut score = Score::empty(idc.clone());
score.identity = idc;
score.instruments = vec![epiphany_core::Instrument::new(
instrument,
String::from("Keyboard"),
)];
score.staves = vec![staff(top_staff, "Right"), staff(bottom_staff, "Left")];
score.events = arena;
score.cross_cutting = cross_cutting;
score.canvas = Canvas {
regions: vec![region],
..Default::default()
};
score
}
/// A one-measure, THREE-staff metric score with **asymmetric** inter-staff
/// pressure, built to exercise the vertical solve's cumulative shift cascade.
///
/// The upper pair collides hard (staff 1 plunges to C1 while staff 2 towers to
/// C7); the lower pair collides gently (staff 2's C3 against staff 3's C6 and
/// the slur arcing over it). So the first correction is several times the
/// second — which is the point. A solve that computed each pair's correction
/// *independently* would shift staff 3 by only the small amount while staff 2
/// took the large one, pulling staff 3 back **through** staff 2 and closing
/// their staff-line gap to well under the fixed pitch. Only a solve whose shift
/// accumulates down the stack keeps every pair separated. Invariant-clean.
pub fn three_staff_close_content(seed: u64) -> Score {
let mut rng = SplitMix64::new(seed ^ 0x0003_57AF_F123);
let replica =
ReplicaId::from_entropy(rng.next_u64().to_le_bytes()).unwrap_or(ReplicaId(0x3ADF));
let mut idc = IdentityContext::new(replica);
let staves: Vec<StaffId> = (0..3).map(|_| idc.mint()).collect();
let instrument: InstrumentId = idc.mint();
let region_id: RegionId = idc.mint();
let measure = |id: MeasureId| Measure {
id,
start: TimeAnchor::Region {
id: region_id,
edge: RegionEdge::Start,
offset: AnchorOffset::Zero,
},
time_signature: None,
explicit_number: Some(1),
number_visibility: Default::default(),
};
// Staff 1 dips to low ledgers; staff 2 spans C7 down to C3 (so it presses
// upward on staff 1 and downward on staff 3); staff 3 climbs to C6.
let octaves: [[i8; 4]; 3] = [[4, 3, 2, 1], [7, 3, 7, 3], [6, 5, 6, 5]];
let mut arena = EventArena::new();
let mut instances: Vec<StaffInstance> = Vec::new();
let mut low_events: Vec<EventId> = Vec::new();
for (staff_index, staff_id) in staves.iter().enumerate() {
let instance_id: StaffInstanceId = idc.mint();
let voice_id: VoiceId = idc.mint();
let mut voice = Voice::user(voice_id);
for (i, oct) in octaves[staff_index].into_iter().enumerate() {
let (eid, pid): (EventId, PitchId) = (idc.mint(), idc.mint());
arena
.insert(c_at(eid, voice_id, pid, i as i64, oct))
.unwrap();
voice.events.push(eid);
if staff_index == 2 {
low_events.push(eid);
}
}
let mut instance = StaffInstance::new(instance_id, *staff_id);
instance.voices.push(voice);
instance.measures.push(measure(idc.mint()));
instances.push(instance);
}
// A slur over the bottom staff's high notes, arcing further up — the extra
// upward extent that turns the lower pair's near-miss into real pressure.
let mut cross_cutting = CrossCuttingRegistry::default();
cross_cutting.slurs.push(Slur {
id: idc.mint::<SlurId>(),
start_event: low_events[0],
end_event: low_events[2],
kind: SlurKind::Legato,
curvature_override: None,
style: SpanStyle::default(),
});
let region = epiphany_core::Region {
id: region_id,
time_model: RegionTimeModel::Metric(MetricTimeModel::default()),
content: RegionContent::StaffBased(StaffBasedContent {
staff_instances: instances,
..Default::default()
}),
time_extent: TimeExtent {
start: TimeAnchor::WallClock {
time: WallClockTime(0),
},
end: TimeAnchor::WallClock {
time: WallClockTime(10_000_000),
},
},
staff_extent: StaffExtent {
staves: staves.clone(),
},
local_tempo_map: None,
permits_spanning_slurs: false,
};
let staff = |id: StaffId, name: &str| Staff {
id,
name: String::from(name),
abbreviation: None,
instrument,
default_staff_lines: StaffLineConfiguration::default(),
group: None,
default_clef: epiphany_core::Clef::treble(),
};
let mut score = Score::empty(idc.clone());
score.identity = idc;
score.instruments = vec![epiphany_core::Instrument::new(
instrument,
String::from("Organ"),
)];
score.staves = vec![
staff(staves[0], "Upper"),
staff(staves[1], "Middle"),
staff(staves[2], "Lower"),
];
score.events = arena;
score.cross_cutting = cross_cutting;
score.canvas = Canvas {
regions: vec![region],
..Default::default()
};
score
}
/// A TWELVE-measure, TWO-staff metric score that wraps into more than one
/// system and carries its inter-staff pressure in the FIRST measure only: the
/// top staff dips to C2 and the bottom staff climbs to C6 there, while every
/// later measure is plain C4s on both staves.
///
/// So the two systems of the one region want genuinely different staff gaps —
/// the first must open to clear the colliding ledgers, the second is already
/// slack. The inter-staff solve sizes each system's gaps from that system's own
/// content, so a quality metric that measured only the first system realizing a
/// gap band would average the second one away. Invariant-clean.
pub fn two_staff_wrapping_pressure(seed: u64) -> Score {
let mut rng = SplitMix64::new(seed ^ 0x0004_57AF_F123);
let replica =
ReplicaId::from_entropy(rng.next_u64().to_le_bytes()).unwrap_or(ReplicaId(0x4ADF));
let mut idc = IdentityContext::new(replica);
let top_staff: StaffId = idc.mint();
let bottom_staff: StaffId = idc.mint();
let instrument: InstrumentId = idc.mint();
let region_id: RegionId = idc.mint();
const MEASURES: i64 = 12;
const BEATS: i64 = 4;
// Measure 0 collides (top dives, bottom climbs); the rest sit inside the
// staff, where the fixed staff pitch already leaves the gap slack.
let octave = |staff_index: usize, index: i64| -> i8 {
if index >= BEATS {
return 4;
}
if staff_index == 0 {
[4i8, 3, 2, 3][index as usize]
} else {
[4i8, 5, 6, 5][index as usize]
}
};
let mut arena = EventArena::new();
let mut instances: Vec<StaffInstance> = Vec::new();
for (staff_index, staff_id) in [top_staff, bottom_staff].into_iter().enumerate() {
let instance_id: StaffInstanceId = idc.mint();
let voice_id: VoiceId = idc.mint();
let mut voice = Voice::user(voice_id);
for index in 0..(MEASURES * BEATS) {
let (eid, pid): (EventId, PitchId) = (idc.mint(), idc.mint());
arena
.insert(c_at(eid, voice_id, pid, index, octave(staff_index, index)))
.unwrap();
voice.events.push(eid);
}
let mut instance = StaffInstance::new(instance_id, staff_id);
instance.voices.push(voice);
for m in 0..MEASURES {
instance.measures.push(Measure {
id: idc.mint(),
start: TimeAnchor::Region {
id: region_id,
edge: RegionEdge::Start,
offset: AnchorOffset::Musical(MusicalDuration(
RationalTime::new(m, 1).unwrap(),
)),
},
time_signature: None,
explicit_number: Some((m + 1) as u32),
number_visibility: Default::default(),
});
}
instances.push(instance);
}
let region = epiphany_core::Region {
id: region_id,
time_model: RegionTimeModel::Metric(MetricTimeModel::default()),
content: RegionContent::StaffBased(StaffBasedContent {
staff_instances: instances,
..Default::default()
}),
time_extent: TimeExtent {
start: TimeAnchor::WallClock {
time: WallClockTime(0),
},
end: TimeAnchor::WallClock {
time: WallClockTime(120_000_000),
},
},
staff_extent: StaffExtent {
staves: vec![top_staff, bottom_staff],
},
local_tempo_map: None,
permits_spanning_slurs: false,
};
let staff = |id: StaffId, name: &str| Staff {
id,
name: String::from(name),
abbreviation: None,
instrument,
default_staff_lines: StaffLineConfiguration::default(),
group: None,
default_clef: epiphany_core::Clef::treble(),
};
let mut score = Score::empty(idc.clone());
score.identity = idc;
score.instruments = vec![epiphany_core::Instrument::new(
instrument,
String::from("Keyboard"),
)];
score.staves = vec![staff(top_staff, "Right"), staff(bottom_staff, "Left")];
score.events = arena;
score.canvas = Canvas {
regions: vec![region],
..Default::default()
};
score
}
/// A two-staff score whose LOWER staff engraves **no glyphs at all**: it is a
/// percussion-clef placeholder — a staff instance with a `ClefChange` to
/// `ClefShape::Percussion`, which has no bundled SMuFL glyph (it engraves to a
/// traced anchor stroke), and no voices or measures. Its vertical band therefore
/// owns five staff-line strokes plus that anchor, and **zero** members.
///
/// The upper staff carries twelve plain measures of C4, so it wraps and its gap
/// to the placeholder is slack everywhere. Together: a valid score on which any
/// consumer that identifies a region's staff bands by their glyph `members`
/// silently loses the lower staff. Invariant-clean.
pub fn percussion_placeholder_staff(seed: u64) -> Score {
let mut rng = SplitMix64::new(seed ^ 0x0005_57AF_F123);
let replica =
ReplicaId::from_entropy(rng.next_u64().to_le_bytes()).unwrap_or(ReplicaId(0x5ADF));
let mut idc = IdentityContext::new(replica);
let top_staff: StaffId = idc.mint();
let drum_staff: StaffId = idc.mint();
let instrument: InstrumentId = idc.mint();
let region_id: RegionId = idc.mint();
const MEASURES: i64 = 12;
const BEATS: i64 = 4;
let mut arena = EventArena::new();
let top_instance: StaffInstanceId = idc.mint();
let top_voice: VoiceId = idc.mint();
let mut voice = Voice::user(top_voice);
for index in 0..(MEASURES * BEATS) {
let (eid, pid): (EventId, PitchId) = (idc.mint(), idc.mint());
arena.insert(quarter(eid, top_voice, pid, index)).unwrap();
voice.events.push(eid);
}
let mut top = StaffInstance::new(top_instance, top_staff);
top.voices.push(voice);
for m in 0..MEASURES {
top.measures.push(Measure {
id: idc.mint(),
start: TimeAnchor::Region {
id: region_id,
edge: RegionEdge::Start,
offset: AnchorOffset::Musical(MusicalDuration(RationalTime::new(m, 1).unwrap())),
},
time_signature: None,
explicit_number: Some((m + 1) as u32),
number_visibility: Default::default(),
});
}
// The placeholder: a percussion clef, no voices, no measures.
let drum_instance: StaffInstanceId = idc.mint();
let mut drums = StaffInstance::new(drum_instance, drum_staff);
drums.clef_sequence.push(epiphany_core::ClefChange {
anchor: TimeAnchor::WallClock {
time: WallClockTime(0),
},
clef: epiphany_core::Clef {
shape: epiphany_core::ClefShape::Percussion,
line: 3,
octave_shift: 0,
},
});
let region = epiphany_core::Region {
id: region_id,
time_model: RegionTimeModel::Metric(MetricTimeModel::default()),
content: RegionContent::StaffBased(StaffBasedContent {
staff_instances: vec![top, drums],
..Default::default()
}),
time_extent: TimeExtent {
start: TimeAnchor::WallClock {
time: WallClockTime(0),
},
end: TimeAnchor::WallClock {
time: WallClockTime(120_000_000),
},
},
staff_extent: StaffExtent {
staves: vec![top_staff, drum_staff],
},
local_tempo_map: None,
permits_spanning_slurs: false,
};
let staff = |id: StaffId, name: &str, clef: epiphany_core::Clef| Staff {
id,
name: String::from(name),
abbreviation: None,
instrument,
default_staff_lines: StaffLineConfiguration::default(),
group: None,
default_clef: clef,
};
let mut score = Score::empty(idc.clone());
score.identity = idc;
score.instruments = vec![epiphany_core::Instrument::new(
instrument,
String::from("Ensemble"),
)];
score.staves = vec![
staff(top_staff, "Melody", epiphany_core::Clef::treble()),
staff(
drum_staff,
"Drums",
epiphany_core::Clef {
shape: epiphany_core::ClefShape::Percussion,
line: 3,
octave_shift: 0,
},
),
];
score.events = arena;
score.canvas = Canvas {
regions: vec![region],
..Default::default()
};
score
}
/// A 10-measure, single-staff, single-voice metric score with 40 quarter notes
/// (four per measure), plus a tie, a spanner, a marker, and a chord symbol. The
/// QUICKSTART layout hand-off case. Invariant-clean (the returned graph passes
/// [`check_invariants`](epiphany_core::check_invariants)).
pub fn ten_measure_single_staff(seed: u64) -> Score {
let mut rng = SplitMix64::new(seed ^ 0x0010_3EA5_5AFF);
let replica =
ReplicaId::from_entropy(rng.next_u64().to_le_bytes()).unwrap_or(ReplicaId(0x10AF));
let mut idc = IdentityContext::new(replica);
let staff_id: StaffId = idc.mint();
let instrument: InstrumentId = idc.mint();
let region_id: RegionId = idc.mint();
let instance_id: StaffInstanceId = idc.mint();
let voice_id: VoiceId = idc.mint();
const MEASURES: i64 = 10;
const BEATS_PER_MEASURE: i64 = 4;
let mut arena = EventArena::new();
let mut voice = Voice::user(voice_id);
let mut events: Vec<EventId> = Vec::new();
for index in 0..(MEASURES * BEATS_PER_MEASURE) {
let eid: EventId = idc.mint();
let pid: PitchId = idc.mint();
arena.insert(quarter(eid, voice_id, pid, index)).unwrap();
voice.events.push(eid);
events.push(eid);
}
let mut instance = StaffInstance::new(instance_id, staff_id);
instance.voices.push(voice);
for m in 0..MEASURES {
let mid: MeasureId = idc.mint();
instance.measures.push(Measure {
id: mid,
// Region-anchored at a whole-note musical offset (one measure of 4/4).
start: TimeAnchor::Region {
id: region_id,
edge: RegionEdge::Start,
offset: AnchorOffset::Musical(MusicalDuration(RationalTime::new(m, 1).unwrap())),
},
time_signature: None,
explicit_number: Some((m + 1) as u32),
number_visibility: Default::default(),
});
}
// Cross-cutting: a tie between the first two (C4) events, a spanner across
// the staff, a marker, and a chord symbol — all region-anchored validly.
let mut cross_cutting = CrossCuttingRegistry::default();
cross_cutting.ties.push(Tie {
id: idc.mint::<TieId>(),
start_event: events[0],
end_event: events[1],
pitch_pairing: None,
class: TieClass::Standard,
style: Default::default(),
});
cross_cutting.spanners.push(Spanner {
id: idc.mint::<SpannerId>(),
start: TimeAnchor::Region {
id: region_id,
edge: RegionEdge::Start,
offset: AnchorOffset::Musical(MusicalDuration::zero()),
},
end: TimeAnchor::WallClock {
time: WallClockTime(10),
},
staves: vec![staff_id],
kind: Default::default(),
style: Default::default(),
});
cross_cutting.markers.push(Marker {
id: idc.mint::<MarkerId>(),
anchor: TimeAnchor::Region {
id: region_id,
edge: RegionEdge::Start,
offset: AnchorOffset::Zero,
},
});
cross_cutting.chord_symbols.push(ChordSymbol {
id: idc.mint::<ChordSymbolId>(),
anchor: TimeAnchor::Region {
id: region_id,
edge: RegionEdge::Start,
offset: AnchorOffset::Zero,
},
});
// (A slur id is minted lazily elsewhere; keep the registry otherwise empty.)
let _ = SlurId::new(replica, 0);
let region = epiphany_core::Region {
id: region_id,
time_model: RegionTimeModel::Metric(MetricTimeModel::default()),
content: RegionContent::StaffBased(StaffBasedContent {
staff_instances: vec![instance],
..Default::default()
}),
time_extent: TimeExtent {
start: TimeAnchor::WallClock {
time: WallClockTime(0),
},
end: TimeAnchor::WallClock {
time: WallClockTime(10_000_000),
},
},
staff_extent: StaffExtent {
staves: vec![staff_id],
},
local_tempo_map: None,
permits_spanning_slurs: false,
};
let mut score = Score::empty(idc.clone());
score.identity = idc;
score.instruments = vec![epiphany_core::Instrument::new(
instrument,
String::from("Flute"),
)];
score.staves = vec![Staff {
id: staff_id,
name: String::from("Flute"),
abbreviation: Some(String::from("Fl.")),
instrument,
default_staff_lines: StaffLineConfiguration::default(),
group: None,
default_clef: epiphany_core::Clef::treble(),
}];
score.events = arena;
score.cross_cutting = cross_cutting;
score.canvas = Canvas {
regions: vec![region],
..Default::default()
};
score
}
/// [`ten_measure_single_staff`] plus three repeat structures — the
/// repeat-rendering acceptance fixture (schema-major-2 E1). Measure-anchored so
/// every boundary coincides with a barline column, it exercises each visual
/// form at once: a simple repeat over measures 24 whose end meets the volta
/// repeat's start (the combined `repeatRightLeft` sign), a `Volta`-kind repeat
/// over measures 57 with a first ending over measure 7 and a "2 3" ending
/// over measure 8 (brackets + ending numerals), and a simple repeat of the
/// last measure closing on the region end (the dot pair beside the final
/// barline). Invariant-clean.
pub fn ten_measure_with_repeats(seed: u64) -> Score {
let mut score = ten_measure_single_staff(seed);
let measures: Vec<MeasureId> = score.canvas.regions[0].staff_instances()[0]
.measures
.iter()
.map(|measure| measure.id)
.collect();
let at_start = |index: usize| TimeAnchor::Measure {
id: measures[index],
position: MeasurePosition::Start,
offset: AnchorOffset::Zero,
};
score.cross_cutting.repeats.push(RepeatStructure {
id: score.identity.mint::<RepeatStructureId>(),
start: at_start(1),
end: at_start(4),
kind: RepeatKind::SimpleRepeat { count: 2 },
voltas: Vec::new(),
});
score.cross_cutting.repeats.push(RepeatStructure {
id: score.identity.mint::<RepeatStructureId>(),
start: at_start(4),
end: at_start(7),
kind: RepeatKind::Volta,
voltas: vec![
Volta {
endings: vec![1],
start: at_start(6),
end: at_start(7),
},
Volta {
endings: vec![2, 3],
start: at_start(7),
end: at_start(8),
},
],
});
score.cross_cutting.repeats.push(RepeatStructure {
id: score.identity.mint::<RepeatStructureId>(),
start: at_start(9),
end: TimeAnchor::Measure {
id: measures[9],
position: MeasurePosition::End,
offset: AnchorOffset::Zero,
},
kind: RepeatKind::SimpleRepeat { count: 2 },
voltas: Vec::new(),
});
score
}
/// [`ten_measure_single_staff`] plus three slurs — the slur-rendering
/// acceptance fixture (schema-major-2 E2). All endpoints are events on the one
/// staff, so each resolves to a note column: a default (Legato, auto direction
/// = above) slur over the first four notes, an authored below slur with an
/// explicit apex height over the middle four, and an editorial slur over a
/// later pair. Invariant-clean.
pub fn ten_measure_with_slurs(seed: u64) -> Score {
let mut score = ten_measure_single_staff(seed);
let events: Vec<EventId> = score.canvas.regions[0].staff_instances()[0].voices[0]
.events
.clone();
score.cross_cutting.slurs.push(Slur {
id: score.identity.mint::<SlurId>(),
start_event: events[0],
end_event: events[3],
kind: SlurKind::Legato,
curvature_override: None,
style: SpanStyle::default(),
});
score.cross_cutting.slurs.push(Slur {
id: score.identity.mint::<SlurId>(),
start_event: events[5],
end_event: events[8],
kind: SlurKind::Phrase,
curvature_override: Some(CurvatureOverride {
direction: Some(CurveDirection::Below),
height: Some(SpaceUnit(CanonicalF64::new(2.0).expect("2.0 is finite"))),
}),
style: SpanStyle::default(),
});
score.cross_cutting.slurs.push(Slur {
id: score.identity.mint::<SlurId>(),
start_event: events[10],
end_event: events[12],
kind: SlurKind::Editorial,
curvature_override: None,
// An authored dashed line — rendered faithfully as a dashed cubic
// (`stroke-dasharray`), so the fixture exercises the line-pattern path.
style: SpanStyle {
line: LineStyle::Dashed,
thickness: None,
},
});
score
}
#[cfg(test)]
mod tests {
use super::*;
use epiphany_core::check_invariants;
#[test]
fn fixture_is_invariant_clean_and_shaped() {
let s = ten_measure_single_staff(1);
let v = check_invariants(&s);
assert!(v.is_empty(), "ten-measure fixture has violations: {v:?}");
assert_eq!(s.staves.len(), 1, "single staff");
assert_eq!(s.canvas.regions[0].staff_instances()[0].measures.len(), 10);
assert_eq!(s.events.len(), 40);
assert_eq!(s.cross_cutting.ties.len(), 1);
assert_eq!(s.cross_cutting.spanners.len(), 1);
assert_eq!(s.cross_cutting.markers.len(), 1);
assert_eq!(s.cross_cutting.chord_symbols.len(), 1);
}
#[test]
fn two_staff_close_content_is_invariant_clean_and_two_staves() {
let s = two_staff_close_content(1);
let v = check_invariants(&s);
assert!(v.is_empty(), "two-staff fixture has violations: {v:?}");
assert_eq!(s.staves.len(), 2, "two staves");
assert_eq!(
s.canvas.regions[0].staff_instances().len(),
2,
"two staff instances in the region"
);
assert_eq!(s.events.len(), 8, "four notes per staff");
assert_eq!(s.cross_cutting.slurs.len(), 1, "a slur over the high notes");
}
#[test]
fn percussion_placeholder_staff_is_invariant_clean_and_glyphless_below() {
use epiphany_layout_ir::{to_constrained, to_logical, VerticalBandKind};
let s = percussion_placeholder_staff(1);
let v = check_invariants(&s);
assert!(v.is_empty(), "percussion fixture has violations: {v:?}");
let c = to_constrained(&to_logical(&s));
let glyphless: Vec<_> = c
.vertical_bands
.iter()
.filter(|b| matches!(b.kind, VerticalBandKind::Staff(_)))
.filter(|b| !c.glyphs.iter().any(|g| g.vertical_band == b.id))
.collect();
assert_eq!(
glyphless.len(),
1,
"exactly one staff band owns no glyph (the percussion placeholder)"
);
assert!(
glyphless[0].members.is_empty(),
"and therefore no band members either"
);
assert!(
c.strokes
.iter()
.filter(|st| st.vertical_band == glyphless[0].id)
.count()
>= 5,
"but it does own its staff lines"
);
}
#[test]
fn two_staff_wrapping_pressure_is_invariant_clean_and_front_loaded() {
let s = two_staff_wrapping_pressure(1);
let v = check_invariants(&s);
assert!(
v.is_empty(),
"wrapping two-staff fixture has violations: {v:?}"
);
assert_eq!(s.staves.len(), 2, "two staves");
assert_eq!(s.events.len(), 96, "12 measures x 4 beats x 2 staves");
assert_eq!(
s.canvas.regions[0].staff_instances().len(),
2,
"two staff instances in ONE region -- so both staves share every system"
);
}
#[test]
fn three_staff_close_content_is_invariant_clean_and_asymmetric() {
let s = three_staff_close_content(1);
let v = check_invariants(&s);
assert!(v.is_empty(), "three-staff fixture has violations: {v:?}");
assert_eq!(s.staves.len(), 3, "three staves");
assert_eq!(
s.canvas.regions[0].staff_instances().len(),
3,
"three staff instances in ONE region — so all three land in one system"
);
assert_eq!(s.events.len(), 12, "four notes per staff");
assert_eq!(s.cross_cutting.slurs.len(), 1, "a slur over the low staff");
}
#[test]
fn repeat_fixture_is_invariant_clean_and_carries_three_repeats() {
let s = ten_measure_with_repeats(1);
let v = check_invariants(&s);
assert!(v.is_empty(), "repeat fixture has violations: {v:?}");
assert_eq!(s.cross_cutting.repeats.len(), 3);
// One volta structure with two brackets; the rest simple repeats.
assert_eq!(
s.cross_cutting
.repeats
.iter()
.map(|rp| rp.voltas.len())
.sum::<usize>(),
2
);
}
#[test]
fn slur_fixture_is_invariant_clean_and_carries_three_slurs() {
let s = ten_measure_with_slurs(1);
let v = check_invariants(&s);
assert!(v.is_empty(), "slur fixture has violations: {v:?}");
assert_eq!(s.cross_cutting.slurs.len(), 3);
// One slur authors a below-direction curvature override; the rest use
// the engraver's default (above).
assert_eq!(
s.cross_cutting
.slurs
.iter()
.filter(|slur| slur.curvature_override.is_some())
.count(),
1
);
}
/// A measure that references a time signature lists it (and its start
/// anchor's target) among its invalidation dependencies, so a time-signature
/// display change with an unchanged id invalidates the measure and its
/// synthesized time-signature glyphs.
#[test]
fn a_measure_depends_on_its_time_signature_and_start_anchor() {
use epiphany_core::{TimeSignatureId, TypedObjectId};
use epiphany_layout_ir::to_logical;
let mut score = ten_measure_single_staff(1);
let time_signature: TimeSignatureId = score.identity.mint();
let (measure_id, region_id) = {
let region = &mut score.canvas.regions[0];
let region_id = region.id;
let instance = region
.content
.staff_instances_mut()
.expect("the fixture is staff-based")
.first_mut()
.expect("a staff instance");
instance.measures[0].time_signature = Some(time_signature);
(instance.measures[0].id, region_id)
};
let logical = to_logical(&score);
let measure = logical
.regions
.iter()
.flat_map(|r| r.objects.iter())
.find(|o| o.provenance().source == TypedObjectId::Measure(measure_id))
.expect("measure 0 is projected");
let deps = &measure.provenance().dependencies;
assert!(
deps.contains(&TypedObjectId::TimeSignature(time_signature)),
"a measure depends on the time signature it displays"
);
// Each fixture measure is region-anchored, so the region is a dep too.
assert!(
deps.contains(&TypedObjectId::Region(region_id)),
"a measure depends on its start anchor's target"
);
}
}