1627 lines
58 KiB
Rust
1627 lines
58 KiB
Rust
//! The layout round-trip harness (v0 acceptance criterion 6 — Chapter 7's IR
|
||
//! contract):
|
||
//!
|
||
//! > A score graph → LogicalLayoutIR → stub-solved ResolvedLayoutIR → RenderIR
|
||
//! > interface call completes without panic and without losing provenance
|
||
//! > back-references.
|
||
//!
|
||
//! **Agent E (`epiphany-layout-ir`) has landed.** This module was formerly a
|
||
//! faithful in-tree *stub* of the layout IR; per the QUICKSTART re-point note it
|
||
//! now drives the **real** crate, re-exporting its IR types behind the same
|
||
//! [`round_trip`] signature the acceptance suite already calls. The
|
||
//! provenance-preservation contract the harness asserts is implemented and
|
||
//! tested inside `epiphany-layout-ir` itself; the testkit keeps deterministic
|
||
//! generators for E's public types (Agent F's "generators for every public type
|
||
//! in A through E" mandate) and exercises the real `round_trip` on the testkit's
|
||
//! hand-off fixtures.
|
||
//!
|
||
//! The module name is retained so the harness entry point stays
|
||
//! `layout_stub::round_trip`.
|
||
|
||
// Re-export the real layout IR: the four stages, `TimeAxisModel`, the provenance
|
||
// back-references, the glyph-catalog identity, the engraving-decision and
|
||
// vertical-band models, the edit-barrier types, and the stub solver.
|
||
pub use epiphany_layout_ir::*;
|
||
|
||
use crate::rng::Rng;
|
||
|
||
// --- Deterministic generators for Agent E's public types (Agent F mandate) ---
|
||
|
||
/// A generated layout-object identifier value.
|
||
pub fn gen_layout_object_id(rng: &mut Rng) -> LayoutObjectId {
|
||
LayoutObjectId(((rng.next_u64() as u128) << 64) | rng.next_u64() as u128)
|
||
}
|
||
|
||
/// A synthesis kind (every variant, including the registered form).
|
||
pub fn gen_synthesis_kind(rng: &mut Rng) -> SynthesisKind {
|
||
match rng.below(7) {
|
||
0 => SynthesisKind::CancellationAccidental,
|
||
1 => SynthesisKind::KeySignatureNatural,
|
||
2 => SynthesisKind::GeneratedRest,
|
||
3 => SynthesisKind::EngravedBreak,
|
||
4 => SynthesisKind::MultimeasureRest,
|
||
5 => SynthesisKind::Cautionary,
|
||
_ => SynthesisKind::Registered(SynthesisRegistryId(
|
||
((rng.next_u64() as u128) << 64) | rng.next_u64() as u128,
|
||
)),
|
||
}
|
||
}
|
||
|
||
/// A time-axis model (every variant of the tagged enum).
|
||
pub fn gen_time_axis_model(rng: &mut Rng) -> TimeAxisModel {
|
||
match rng.below(4) {
|
||
0 => TimeAxisModel::Metric(MetricTimeAxis::default()),
|
||
1 => TimeAxisModel::Proportional(ProportionalTimeAxis {
|
||
duration_ns: rng.range(0, 1 << 40) as i64,
|
||
space_per_second: gen_staff_space(rng),
|
||
placements: vec![],
|
||
}),
|
||
2 => TimeAxisModel::Aleatoric(AleatoricTimeAxis::default()),
|
||
_ => TimeAxisModel::Registered(
|
||
TimeAxisRegistryId(((rng.next_u64() as u128) << 64) | rng.next_u64() as u128),
|
||
SerializedRegisteredAxis(vec![rng.next_u64() as u8]),
|
||
),
|
||
}
|
||
}
|
||
|
||
/// A provenance record with a random source and a few dependency
|
||
/// back-references, **internally consistent**: a synthesized provenance carries
|
||
/// `Some(kind)` *and* the matching `synthesized_layout_id`; a projected one
|
||
/// carries `None` *and* `stable_layout_id`. (It never sets a synthesis kind while
|
||
/// keeping the plain source-only id.)
|
||
pub fn gen_provenance(rng: &mut Rng) -> Provenance {
|
||
let source = crate::generators::typed_object_id(rng);
|
||
let mut dependencies = Vec::new();
|
||
for _ in 0..rng.range_usize(0, 3) {
|
||
dependencies.push(crate::generators::typed_object_id(rng));
|
||
}
|
||
if rng.boolean() {
|
||
let kind = gen_synthesis_kind(rng);
|
||
Provenance::synthesized(
|
||
source,
|
||
kind,
|
||
SynthesisInstanceKey(((rng.next_u64() as u128) << 64) | rng.next_u64() as u128),
|
||
dependencies,
|
||
)
|
||
} else {
|
||
Provenance::projected(source, dependencies)
|
||
}
|
||
}
|
||
|
||
/// A bundled SMuFL glyph name.
|
||
fn gen_glyph_name(rng: &mut Rng) -> &'static str {
|
||
BRAVURA_METRICS[rng.below(BRAVURA_METRICS.len() as u64) as usize]
|
||
.name
|
||
.as_ref()
|
||
}
|
||
|
||
/// A solver status (every variant).
|
||
pub fn gen_solve_status(rng: &mut Rng) -> SolveStatus {
|
||
*rng.choose(&[
|
||
SolveStatus::Solved,
|
||
SolveStatus::SolvedWithWarnings,
|
||
SolveStatus::PartialBudgetExhausted,
|
||
SolveStatus::Unsatisfiable,
|
||
SolveStatus::InternalError,
|
||
])
|
||
}
|
||
|
||
/// A glyph-catalog identity (the bundled Bravura identity).
|
||
pub fn gen_glyph_catalog_identity(_rng: &mut Rng) -> GlyphCatalogIdentity {
|
||
GlyphCatalogIdentity::default()
|
||
}
|
||
|
||
/// A 2-D staff-space point.
|
||
pub fn gen_point(rng: &mut Rng) -> Point {
|
||
// Spread points over a few hundred staff spaces at sub-grid resolution.
|
||
let coord = |r: &mut Rng| (r.range(0, 400_000) as f32) / 1000.0;
|
||
Point::new(coord(rng), coord(rng))
|
||
}
|
||
|
||
/// A logical engraving-content payload — every [`LayoutContent`] variant, so the
|
||
/// generator/fuzz surface exercises the enriched payloads, not just structural.
|
||
pub fn gen_layout_content(rng: &mut Rng) -> LayoutContent {
|
||
fn time(rng: &mut Rng) -> TimePoint {
|
||
if rng.boolean() {
|
||
TimePoint::Musical(epiphany_core::MusicalPosition(
|
||
epiphany_core::RationalTime::new(rng.range(0, 16) as i64, 4).expect("nonzero"),
|
||
))
|
||
} else {
|
||
TimePoint::WallClock(epiphany_core::WallClockTime(rng.range(0, 10_000) as i64))
|
||
}
|
||
}
|
||
fn duration(numerator: i64, denominator: i64) -> epiphany_core::MusicalDuration {
|
||
epiphany_core::MusicalDuration(
|
||
epiphany_core::RationalTime::new(numerator, denominator).expect("nonzero"),
|
||
)
|
||
}
|
||
fn component(rng: &mut Rng, offset: epiphany_core::MusicalDuration) -> PlacedComponent {
|
||
PlacedComponent {
|
||
offset,
|
||
component: epiphany_core::NotatedComponent {
|
||
base_value: *rng.choose(&[
|
||
epiphany_core::NoteValue::Whole,
|
||
epiphany_core::NoteValue::Half,
|
||
epiphany_core::NoteValue::Quarter,
|
||
epiphany_core::NoteValue::Eighth,
|
||
epiphany_core::NoteValue::Sixteenth,
|
||
]),
|
||
dots: rng.range(0, 4) as u8,
|
||
tuplet: None,
|
||
tied_to_next: rng.boolean(),
|
||
},
|
||
tuplet: None,
|
||
}
|
||
}
|
||
fn components(rng: &mut Rng) -> Vec<PlacedComponent> {
|
||
let mut out = vec![component(rng, epiphany_core::MusicalDuration::zero())];
|
||
if rng.boolean() {
|
||
out.push(component(rng, duration(1, 4)));
|
||
}
|
||
out
|
||
}
|
||
fn clefs(rng: &mut Rng) -> Vec<PlacedClef> {
|
||
if rng.boolean() {
|
||
vec![
|
||
PlacedClef {
|
||
time: time(rng),
|
||
clef: epiphany_core::Clef::treble(),
|
||
},
|
||
PlacedClef {
|
||
time: time(rng),
|
||
clef: epiphany_core::Clef::bass(),
|
||
},
|
||
]
|
||
} else {
|
||
Vec::new()
|
||
}
|
||
}
|
||
fn keys(rng: &mut Rng) -> Vec<PlacedKeySignature> {
|
||
if rng.boolean() {
|
||
vec![PlacedKeySignature {
|
||
time: time(rng),
|
||
key: epiphany_core::KeySignature::new(rng.range(0, 14) as i8 - 7)
|
||
.expect("generator stays in -7..=7"),
|
||
}]
|
||
} else {
|
||
Vec::new()
|
||
}
|
||
}
|
||
fn spelling(rng: &mut Rng) -> Option<epiphany_core::PitchSpelling> {
|
||
rng.boolean().then(|| {
|
||
let nominal = *rng.choose(&[
|
||
epiphany_core::CmnNominal::C,
|
||
epiphany_core::CmnNominal::D,
|
||
epiphany_core::CmnNominal::E,
|
||
epiphany_core::CmnNominal::F,
|
||
epiphany_core::CmnNominal::G,
|
||
epiphany_core::CmnNominal::A,
|
||
epiphany_core::CmnNominal::B,
|
||
]);
|
||
epiphany_core::PitchSpelling::cmn(nominal, rng.range(2, 7) as i8)
|
||
})
|
||
}
|
||
fn barline(rng: &mut Rng) -> BarlineKind {
|
||
match rng.below(3) {
|
||
0 => BarlineKind::Interior,
|
||
1 => BarlineKind::RegionEnd,
|
||
_ => BarlineKind::Final,
|
||
}
|
||
}
|
||
fn placement(rng: &mut Rng) -> RepeatPlacement {
|
||
match rng.below(3) {
|
||
0 => RepeatPlacement::At(time(rng)),
|
||
1 => RepeatPlacement::RegionEnd,
|
||
_ => RepeatPlacement::Unresolved,
|
||
}
|
||
}
|
||
match rng.below(6) {
|
||
0 => LayoutContent::Structural,
|
||
1 => LayoutContent::Staff(StaffContent {
|
||
default_clef: epiphany_core::Clef::default(),
|
||
clefs: clefs(rng),
|
||
keys: keys(rng),
|
||
}),
|
||
2 => LayoutContent::Note(NoteContent {
|
||
position: time(rng),
|
||
components: components(rng),
|
||
pitches: vec![NotePitch {
|
||
pitch: epiphany_core::PitchId::new(epiphany_core::ReplicaId(7), rng.next_u64()),
|
||
spelling: spelling(rng),
|
||
}],
|
||
}),
|
||
3 => LayoutContent::Rest(RestContent {
|
||
position: time(rng),
|
||
components: components(rng),
|
||
staff_position: rng
|
||
.boolean()
|
||
.then(|| epiphany_core::StaffPosition(rng.range(0, 9) as i16)),
|
||
}),
|
||
4 => LayoutContent::Measure(MeasureContent {
|
||
start: time(rng),
|
||
barline: barline(rng),
|
||
time_signature: rng.boolean().then(|| TimeSignatureContent {
|
||
numerator: rng.range(1, 13) as u16,
|
||
denominator: *rng.choose(&[2, 4, 8, 16]),
|
||
}),
|
||
}),
|
||
_ => LayoutContent::Repeat(RepeatContent {
|
||
barlines: rng.boolean(),
|
||
start: placement(rng),
|
||
end: placement(rng),
|
||
voltas: (0..rng.below(3))
|
||
.map(|index| VoltaContent {
|
||
endings: vec![index as u32 + 1],
|
||
start: placement(rng),
|
||
end: placement(rng),
|
||
})
|
||
.collect(),
|
||
}),
|
||
}
|
||
}
|
||
|
||
/// A logical layout object (provenance + an optional owning staff + content).
|
||
pub fn gen_layout_object(rng: &mut Rng) -> LayoutObject {
|
||
LayoutObject::from_projection_with_content(
|
||
gen_provenance(rng),
|
||
rng.boolean().then(|| crate::generators::staff_id(rng)),
|
||
gen_layout_content(rng),
|
||
)
|
||
}
|
||
|
||
/// A constrained glyph object (provenance + glyph + baseline anchor + band).
|
||
pub fn gen_glyph_object(rng: &mut Rng) -> GlyphObject {
|
||
let glyph_name = gen_glyph_name(rng);
|
||
GlyphObject {
|
||
provenance: gen_provenance(rng),
|
||
glyph: GlyphReference::borrowed(glyph_name),
|
||
horizontal_slot: gen_spring_slot_id(rng),
|
||
baseline: gen_point(rng),
|
||
vertical_band: gen_vertical_band_id(rng),
|
||
bounding_box: metrics(glyph_name)
|
||
.expect("generator chooses a bundled glyph")
|
||
.bounding_box(),
|
||
anchor: gen_point(rng),
|
||
layer: rng.range(0, 8) as i32,
|
||
style: GlyphStyle {
|
||
rgba: rng.next_u64() as u32,
|
||
},
|
||
}
|
||
}
|
||
|
||
/// A resolved glyph (provenance + glyph + definitive position).
|
||
pub fn gen_resolved_glyph(rng: &mut Rng) -> ResolvedGlyph {
|
||
ResolvedGlyph {
|
||
provenance: gen_provenance(rng),
|
||
glyph: GlyphReference::borrowed(gen_glyph_name(rng)),
|
||
position: gen_point(rng),
|
||
transform: None,
|
||
bounding_box: gen_glyph_bounding_box(rng),
|
||
style: GlyphStyle {
|
||
rgba: rng.next_u64() as u32,
|
||
},
|
||
layer: rng.range(0, 8) as i32,
|
||
}
|
||
}
|
||
|
||
/// A logical layout region (provenance + time axis + a few objects).
|
||
pub fn gen_layout_region(rng: &mut Rng) -> LayoutRegion {
|
||
let n = rng.range_usize(0, 4);
|
||
let region = crate::generators::region_id(rng);
|
||
let objects: Vec<LayoutObject> = (0..n).map(|_| gen_layout_object(rng)).collect();
|
||
let staves = objects.iter().filter_map(LayoutObject::staff).collect();
|
||
LayoutRegion {
|
||
provenance: Provenance::projected(epiphany_core::TypedObjectId::Region(region), vec![]),
|
||
coordinate_system: LocalCoordinateSystem::default(),
|
||
time_axis: gen_time_axis_model(rng),
|
||
vertical_extent: VerticalExtent { staves },
|
||
objects,
|
||
}
|
||
}
|
||
|
||
/// A logical layout IR (a few regions and engraving decisions).
|
||
pub fn gen_logical_layout_ir(rng: &mut Rng) -> LogicalLayoutIR {
|
||
let n = rng.range_usize(0, 3);
|
||
let d = rng.range_usize(0, 3);
|
||
LogicalLayoutIR {
|
||
source: ScoreVersion::default(),
|
||
regions: (0..n).map(|_| gen_layout_region(rng)).collect(),
|
||
engraving_decisions: (0..d).map(|_| gen_engraving_decision(rng)).collect(),
|
||
overrides: vec![],
|
||
cross_region: vec![],
|
||
}
|
||
}
|
||
|
||
/// A non-glyph line primitive (a staff line / stem / barline), so the
|
||
/// generator/fuzz surface exercises strokes alongside glyphs.
|
||
pub fn gen_stroke(rng: &mut Rng, band: VerticalBandId) -> Stroke {
|
||
Stroke {
|
||
provenance: gen_provenance(rng),
|
||
from: gen_point(rng),
|
||
to: gen_point(rng),
|
||
thickness: gen_staff_space(rng),
|
||
layer: rng.range(0, 8) as i32 - 4,
|
||
style: GlyphStyle {
|
||
rgba: rng.next_u64() as u32,
|
||
},
|
||
// A stroke names a band that exists; validation rejects a dangling one.
|
||
vertical_band: band,
|
||
}
|
||
}
|
||
|
||
/// A cubic-bézier curve primitive with generated provenance and geometry.
|
||
pub fn gen_curve(rng: &mut Rng, band: VerticalBandId) -> Curve {
|
||
Curve {
|
||
provenance: gen_provenance(rng),
|
||
p0: gen_point(rng),
|
||
p1: gen_point(rng),
|
||
p2: gen_point(rng),
|
||
p3: gen_point(rng),
|
||
thickness: gen_staff_space(rng),
|
||
layer: rng.range(0, 8) as i32 - 4,
|
||
style: GlyphStyle {
|
||
rgba: rng.next_u64() as u32,
|
||
},
|
||
line: *rng.choose(&[
|
||
epiphany_core::LineStyle::Solid,
|
||
epiphany_core::LineStyle::Dashed,
|
||
epiphany_core::LineStyle::Dotted,
|
||
]),
|
||
// A curve names a band that exists; validation rejects a dangling one.
|
||
vertical_band: band,
|
||
}
|
||
}
|
||
|
||
/// A constrained layout IR whose catalog hash covers exactly its glyph metrics
|
||
/// (so the real stub solver accepts it as well-formed), with an **internally
|
||
/// consistent** vertical band: every glyph names the band, and the band's
|
||
/// members are exactly those glyphs.
|
||
pub fn gen_constrained_layout_ir(rng: &mut Rng) -> ConstrainedLayoutIR {
|
||
let n = rng.range_usize(0, 8);
|
||
let mut glyphs: Vec<GlyphObject> = (0..n).map(|_| gen_glyph_object(rng)).collect();
|
||
// One band owning exactly these glyphs; point every glyph at it.
|
||
let band = VerticalBand::staff(
|
||
crate::generators::staff_id(rng),
|
||
glyphs.iter().map(|g| g.id()).collect(),
|
||
);
|
||
for g in &mut glyphs {
|
||
g.vertical_band = band.id;
|
||
}
|
||
let horizontal_slots: Vec<SpringSlot> = glyphs
|
||
.iter_mut()
|
||
.enumerate()
|
||
.map(|(index, glyph)| {
|
||
let id = SpringSlotId(glyph.id().0);
|
||
glyph.horizontal_slot = id;
|
||
SpringSlot {
|
||
id,
|
||
time: TimePoint::WallClock(epiphany_core::WallClockTime(index as i64)),
|
||
min_width: StaffSpace(1.0),
|
||
preferred_width: StaffSpace(1.5),
|
||
max_width: None,
|
||
stretch_factor: 1.0,
|
||
compress_factor: 1.0,
|
||
members: vec![glyph.id()],
|
||
}
|
||
})
|
||
.collect();
|
||
let metrics_hash = metrics_hash_for(glyphs.iter().map(|g| g.glyph.as_str()));
|
||
let decisions = rng.range_usize(0, 3);
|
||
ConstrainedLayoutIR {
|
||
source: ScoreVersion::default(),
|
||
regions: vec![],
|
||
horizontal_slots,
|
||
glyphs,
|
||
strokes: (0..rng.range_usize(0, 3))
|
||
.map(|_| gen_stroke(rng, band.id))
|
||
.collect(),
|
||
curves: (0..rng.range_usize(0, 3))
|
||
.map(|_| gen_curve(rng, band.id))
|
||
.collect(),
|
||
vertical_bands: vec![band],
|
||
constraints: vec![],
|
||
break_origins: vec![],
|
||
engraving_decisions: (0..decisions)
|
||
.map(|_| gen_engraving_decision(rng))
|
||
.collect(),
|
||
diagnostics: Vec::new(),
|
||
catalog: GlyphCatalogIdentity {
|
||
metrics_hash,
|
||
..GlyphCatalogIdentity::default()
|
||
},
|
||
}
|
||
}
|
||
|
||
/// A resolved layout IR produced by the real stub-solver interface.
|
||
pub fn gen_resolved_layout_ir(rng: &mut Rng) -> ResolvedLayoutIR {
|
||
StubSolver
|
||
.solve(&gen_constrained_layout_ir(rng), &SolverConfig::default())
|
||
.layout
|
||
}
|
||
|
||
pub fn gen_resolved_staff(rng: &mut Rng) -> ResolvedStaff {
|
||
let staff = crate::generators::staff_id(rng);
|
||
ResolvedStaff {
|
||
provenance: Provenance::projected(epiphany_core::TypedObjectId::Staff(staff), vec![]),
|
||
staff,
|
||
bounding_box: gen_rect(rng),
|
||
}
|
||
}
|
||
|
||
pub fn gen_resolved_measure(rng: &mut Rng) -> ResolvedMeasure {
|
||
let measure = crate::generators::measure_id(rng);
|
||
ResolvedMeasure {
|
||
provenance: Provenance::projected(epiphany_core::TypedObjectId::Measure(measure), vec![]),
|
||
measure,
|
||
bounding_box: gen_rect(rng),
|
||
}
|
||
}
|
||
|
||
pub fn gen_resolved_system(rng: &mut Rng) -> ResolvedSystem {
|
||
ResolvedSystem {
|
||
provenance: gen_provenance(rng),
|
||
bounding_box: gen_rect(rng),
|
||
staves: (0..rng.range_usize(0, 2))
|
||
.map(|_| gen_resolved_staff(rng))
|
||
.collect(),
|
||
measures: (0..rng.range_usize(0, 2))
|
||
.map(|_| gen_resolved_measure(rng))
|
||
.collect(),
|
||
// Not tied to any real flat array here (this generator builds an
|
||
// isolated `ResolvedSystem`, not a whole `ResolvedLayoutIR`), so a
|
||
// random index list would be equally fake; left empty rather than
|
||
// fabricated.
|
||
primitives: Default::default(),
|
||
}
|
||
}
|
||
|
||
pub fn gen_resolved_page(rng: &mut Rng) -> ResolvedPage {
|
||
ResolvedPage {
|
||
provenance: gen_provenance(rng),
|
||
number: rng.range(1, 500) as u32,
|
||
size: gen_size2d(rng),
|
||
margins: gen_margins(rng),
|
||
systems: (0..rng.range_usize(0, 2))
|
||
.map(|_| gen_resolved_system(rng))
|
||
.collect(),
|
||
free_objects: (0..rng.range_usize(0, 2))
|
||
.map(|_| gen_glyph_object_id(rng))
|
||
.collect(),
|
||
}
|
||
}
|
||
|
||
/// A render primitive with generated provenance, glyph, and geometry.
|
||
pub fn gen_render_primitive(rng: &mut Rng) -> RenderPrimitive {
|
||
RenderPrimitive {
|
||
provenance: gen_provenance(rng),
|
||
glyph: GlyphReference::borrowed(gen_glyph_name(rng)),
|
||
position: gen_point(rng),
|
||
transform: None,
|
||
bounding_box: gen_glyph_bounding_box(rng),
|
||
style: GlyphStyle {
|
||
rgba: rng.next_u64() as u32,
|
||
},
|
||
layer: rng.range(0, 8) as i32,
|
||
}
|
||
}
|
||
|
||
/// A RenderIR generated through the resolved-to-render projection.
|
||
pub fn gen_render_ir(rng: &mut Rng) -> RenderIR {
|
||
to_render(&gen_resolved_layout_ir(rng))
|
||
}
|
||
|
||
/// A complete solver report generated through the solver interface.
|
||
pub fn gen_solve_report(rng: &mut Rng) -> SolveReport {
|
||
StubSolver.solve(&gen_constrained_layout_ir(rng), &SolverConfig::default())
|
||
}
|
||
|
||
/// A self-consistent round-trip report value.
|
||
pub fn gen_round_trip_report(rng: &mut Rng) -> RoundTripReport {
|
||
let render = gen_render_ir(rng);
|
||
let recovered_sources = render
|
||
.primitives
|
||
.iter()
|
||
.map(|primitive| primitive.provenance.source)
|
||
.chain(render.strokes.iter().map(|stroke| stroke.provenance.source))
|
||
.chain(render.curves.iter().map(|curve| curve.provenance.source))
|
||
.collect();
|
||
let total = render.primitives.len() + render.strokes.len() + render.curves.len();
|
||
RoundTripReport {
|
||
status: SolveStatus::Solved,
|
||
logical_objects: total,
|
||
glyphs: render.primitives.len(),
|
||
render_strokes: render.strokes.len(),
|
||
render_curves: render.curves.len(),
|
||
render_primitives: total,
|
||
recovered_sources,
|
||
}
|
||
}
|
||
|
||
// --- Generators for the rest of E's public surface (Agent F mandate) ---
|
||
|
||
/// A staff-space measurement.
|
||
pub fn gen_staff_space(rng: &mut Rng) -> StaffSpace {
|
||
StaffSpace((rng.range(0, 400_000) as f32) / 1000.0)
|
||
}
|
||
|
||
/// A 2-D staff-space size.
|
||
pub fn gen_size2d(rng: &mut Rng) -> Size2D {
|
||
Size2D {
|
||
width: gen_staff_space(rng),
|
||
height: gen_staff_space(rng),
|
||
}
|
||
}
|
||
|
||
/// A staff-space bounding box.
|
||
pub fn gen_bounding_box(rng: &mut Rng) -> BoundingBox {
|
||
let c = |r: &mut Rng| (r.range(0, 8000) as f32) / 1000.0;
|
||
BoundingBox::new(c(rng), c(rng), c(rng), c(rng))
|
||
}
|
||
|
||
pub fn gen_rect(rng: &mut Rng) -> Rect {
|
||
Rect {
|
||
origin: gen_point(rng),
|
||
size: gen_size2d(rng),
|
||
}
|
||
}
|
||
|
||
pub fn gen_transform_2d(rng: &mut Rng) -> Transform2D {
|
||
let mut transform = Transform2D::default();
|
||
transform.matrix[0][2] = gen_staff_space(rng).0;
|
||
transform.matrix[1][2] = gen_staff_space(rng).0;
|
||
transform
|
||
}
|
||
|
||
pub fn gen_margins(rng: &mut Rng) -> Margins {
|
||
Margins {
|
||
top: gen_staff_space(rng),
|
||
right: gen_staff_space(rng),
|
||
bottom: gen_staff_space(rng),
|
||
left: gen_staff_space(rng),
|
||
}
|
||
}
|
||
|
||
pub fn gen_score_version(rng: &mut Rng) -> ScoreVersion {
|
||
let mut bytes = [0; 32];
|
||
for chunk in bytes.chunks_exact_mut(8) {
|
||
chunk.copy_from_slice(&rng.next_u64().to_le_bytes());
|
||
}
|
||
ScoreVersion(bytes)
|
||
}
|
||
|
||
pub fn gen_local_coordinate_system(rng: &mut Rng) -> LocalCoordinateSystem {
|
||
LocalCoordinateSystem {
|
||
transform: gen_transform_2d(rng),
|
||
}
|
||
}
|
||
|
||
pub fn gen_vertical_extent(rng: &mut Rng) -> VerticalExtent {
|
||
VerticalExtent {
|
||
staves: (0..rng.range_usize(0, 3))
|
||
.map(|_| crate::generators::staff_id(rng))
|
||
.collect(),
|
||
}
|
||
}
|
||
|
||
pub fn gen_cross_region_object(rng: &mut Rng) -> CrossRegionObject {
|
||
CrossRegionObject {
|
||
provenance: gen_provenance(rng),
|
||
regions: (0..rng.range_usize(1, 3))
|
||
.map(|_| crate::generators::region_id(rng))
|
||
.collect(),
|
||
staff: rng.boolean().then(|| crate::generators::staff_id(rng)),
|
||
}
|
||
}
|
||
|
||
pub fn gen_dependency_index(rng: &mut Rng) -> DependencyIndex {
|
||
let mut index = DependencyIndex::default();
|
||
for _ in 0..rng.range_usize(0, 4) {
|
||
index.insert(&gen_provenance(rng));
|
||
}
|
||
index
|
||
}
|
||
|
||
pub fn gen_layout_cache(rng: &mut Rng) -> LayoutCache {
|
||
LayoutCache {
|
||
dependencies: gen_dependency_index(rng),
|
||
logical: Default::default(),
|
||
constrained: Default::default(),
|
||
resolved: Default::default(),
|
||
fine_cache: FineLayoutCache::default(),
|
||
}
|
||
}
|
||
|
||
pub fn gen_system_id(rng: &mut Rng) -> SystemId {
|
||
SystemId(((rng.next_u64() as u128) << 64) | rng.next_u64() as u128)
|
||
}
|
||
|
||
pub fn gen_logical_region_cache(rng: &mut Rng) -> LogicalRegionCache {
|
||
LogicalRegionCache {
|
||
objects: (0..rng.range_usize(0, 3))
|
||
.map(|_| gen_layout_object_id(rng))
|
||
.collect(),
|
||
}
|
||
}
|
||
|
||
pub fn gen_constrained_region_cache(rng: &mut Rng) -> ConstrainedRegionCache {
|
||
ConstrainedRegionCache {
|
||
objects: (0..rng.range_usize(0, 3))
|
||
.map(|_| gen_layout_object_id(rng))
|
||
.collect(),
|
||
}
|
||
}
|
||
|
||
pub fn gen_resolved_system_cache(rng: &mut Rng) -> ResolvedSystemCache {
|
||
ResolvedSystemCache {
|
||
objects: (0..rng.range_usize(0, 3))
|
||
.map(|_| gen_layout_object_id(rng))
|
||
.collect(),
|
||
}
|
||
}
|
||
|
||
pub fn gen_fine_layout_cache(rng: &mut Rng) -> FineLayoutCache {
|
||
FineLayoutCache {
|
||
objects: (0..rng.range_usize(0, 3))
|
||
.map(|_| gen_layout_object_id(rng))
|
||
.collect(),
|
||
}
|
||
}
|
||
|
||
/// A glyph-object identifier value.
|
||
pub fn gen_glyph_object_id(rng: &mut Rng) -> GlyphObjectId {
|
||
GlyphObjectId(((rng.next_u64() as u128) << 64) | rng.next_u64() as u128)
|
||
}
|
||
|
||
pub fn gen_synthesis_instance_key(rng: &mut Rng) -> SynthesisInstanceKey {
|
||
SynthesisInstanceKey(((rng.next_u64() as u128) << 64) | rng.next_u64() as u128)
|
||
}
|
||
|
||
pub fn gen_glyph_style(rng: &mut Rng) -> GlyphStyle {
|
||
GlyphStyle {
|
||
rgba: rng.next_u64() as u32,
|
||
}
|
||
}
|
||
|
||
pub fn gen_time_point(rng: &mut Rng) -> TimePoint {
|
||
if rng.boolean() {
|
||
TimePoint::Musical(epiphany_core::MusicalPosition(
|
||
epiphany_core::RationalTime::new(rng.range(0, 16) as i64, 4)
|
||
.expect("positive denominator"),
|
||
))
|
||
} else {
|
||
TimePoint::WallClock(epiphany_core::WallClockTime(rng.next_u64() as i64))
|
||
}
|
||
}
|
||
|
||
pub fn gen_time_range(rng: &mut Rng) -> TimeRange {
|
||
if rng.boolean() {
|
||
let start = epiphany_core::MusicalPosition(
|
||
epiphany_core::RationalTime::new(rng.range(0, 8) as i64, 4)
|
||
.expect("positive denominator"),
|
||
);
|
||
TimeRange::Musical {
|
||
start: start.clone(),
|
||
end: start
|
||
+ epiphany_core::MusicalDuration(
|
||
epiphany_core::RationalTime::new(1, 4).expect("positive denominator"),
|
||
),
|
||
}
|
||
} else {
|
||
let start = epiphany_core::WallClockTime(rng.range(0, 10_000) as i64);
|
||
TimeRange::WallClock {
|
||
start,
|
||
end: epiphany_core::WallClockTime(start.0 + 1_000),
|
||
}
|
||
}
|
||
}
|
||
|
||
pub fn gen_spring_slot(rng: &mut Rng) -> SpringSlot {
|
||
SpringSlot {
|
||
id: gen_spring_slot_id(rng),
|
||
time: gen_time_point(rng),
|
||
min_width: StaffSpace(1.0),
|
||
preferred_width: StaffSpace(2.0),
|
||
max_width: rng.boolean().then_some(StaffSpace(3.0)),
|
||
stretch_factor: 1.0,
|
||
compress_factor: 1.0,
|
||
members: (0..rng.range_usize(0, 3))
|
||
.map(|_| gen_glyph_object_id(rng))
|
||
.collect(),
|
||
}
|
||
}
|
||
|
||
pub fn gen_constrained_layout_region(rng: &mut Rng) -> ConstrainedLayoutRegion {
|
||
let region = crate::generators::region_id(rng);
|
||
ConstrainedLayoutRegion {
|
||
provenance: Provenance::projected(epiphany_core::TypedObjectId::Region(region), vec![]),
|
||
glyphs: (0..rng.range_usize(0, 3))
|
||
.map(|_| gen_glyph_object_id(rng))
|
||
.collect(),
|
||
time_axis: gen_time_axis_model(rng),
|
||
}
|
||
}
|
||
|
||
pub fn gen_layout_constraint(rng: &mut Rng) -> LayoutConstraint {
|
||
match rng.below(6) {
|
||
0 => LayoutConstraint::NoCollision {
|
||
a: gen_glyph_object_id(rng),
|
||
b: gen_glyph_object_id(rng),
|
||
},
|
||
1 => LayoutConstraint::Align {
|
||
a: gen_glyph_object_id(rng),
|
||
b: gen_glyph_object_id(rng),
|
||
axis: if rng.boolean() {
|
||
Axis::Horizontal
|
||
} else {
|
||
Axis::Vertical
|
||
},
|
||
},
|
||
2 => LayoutConstraint::PositionWithin {
|
||
glyph: gen_glyph_object_id(rng),
|
||
region: gen_rect(rng),
|
||
},
|
||
3 => LayoutConstraint::SystemBreakAt {
|
||
slot: gen_spring_slot_id(rng),
|
||
kind: if rng.boolean() {
|
||
BreakKind::Hard
|
||
} else {
|
||
BreakKind::Soft
|
||
},
|
||
},
|
||
4 => LayoutConstraint::PageBreakAt {
|
||
slot: gen_spring_slot_id(rng),
|
||
kind: if rng.boolean() {
|
||
BreakKind::Hard
|
||
} else {
|
||
BreakKind::Soft
|
||
},
|
||
},
|
||
_ => LayoutConstraint::Registered(
|
||
ConstraintRegistryId(rng.next_u64() as u128),
|
||
ConstraintParameters(vec![rng.next_u64() as u8]),
|
||
),
|
||
}
|
||
}
|
||
|
||
/// A constraint strength (both variants).
|
||
pub fn gen_constraint_strength(rng: &mut Rng) -> ConstraintStrength {
|
||
if rng.boolean() {
|
||
ConstraintStrength::Required
|
||
} else {
|
||
ConstraintStrength::Preferred {
|
||
weight: (rng.range(1, 1_000) as f64) / 100.0,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// A bundled glyph metric (drawn from the in-tree Bravura table).
|
||
pub fn gen_glyph_metric(rng: &mut Rng) -> GlyphMetric {
|
||
BRAVURA_METRICS[rng.below(BRAVURA_METRICS.len() as u64) as usize].clone()
|
||
}
|
||
|
||
/// A SMuFL version. `minor_centi` is fraction-normalized (P13-S12): a small
|
||
/// integer sampled directly (as the previous generator did) would denote
|
||
/// 1.00-1.05 as hundredths, versions SMuFL never released. Instead sample the
|
||
/// minor digit string from real SMuFL minor releases and normalize it through
|
||
/// [`SmuflVersion::from_decimal`], the same constructor production code uses,
|
||
/// so the generator can never emit a value the type's own invariant forbids.
|
||
pub fn gen_smufl_version(rng: &mut Rng) -> SmuflVersion {
|
||
const MINOR_RELEASES: [&str; 5] = ["12", "18", "20", "3", "4"];
|
||
let major = rng.range(1, 2) as u16;
|
||
let minor_digits = MINOR_RELEASES[rng.below(MINOR_RELEASES.len() as u64) as usize];
|
||
SmuflVersion::from_decimal(major, minor_digits).expect("a real SMuFL minor release")
|
||
}
|
||
|
||
/// A font identifier (the bundled font).
|
||
pub fn gen_font_id(rng: &mut Rng) -> FontId {
|
||
if rng.boolean() {
|
||
FontId::BRAVURA
|
||
} else {
|
||
FontId::owned(format!("runtime-font-{}", rng.next_u64()))
|
||
}
|
||
}
|
||
|
||
/// A decision source (every variant).
|
||
pub fn gen_decision_source(rng: &mut Rng) -> DecisionSource {
|
||
match rng.below(3) {
|
||
0 => DecisionSource::Automatic,
|
||
1 => DecisionSource::UserOverride(EngravingOverrideId(rng.next_u64() as u128)),
|
||
_ => DecisionSource::IrOverride,
|
||
}
|
||
}
|
||
|
||
/// An engraving-decision kind (a representative subset).
|
||
pub fn gen_engraving_decision_kind(rng: &mut Rng) -> EngravingDecisionKind {
|
||
match rng.below(5) {
|
||
0 => EngravingDecisionKind::StemDirection(if rng.boolean() {
|
||
StemDirection::Up
|
||
} else {
|
||
StemDirection::Down
|
||
}),
|
||
1 => EngravingDecisionKind::LedgerLineCount(rng.range(0, 5) as u8),
|
||
2 => EngravingDecisionKind::SystemBreak,
|
||
3 => EngravingDecisionKind::PageBreak,
|
||
_ => EngravingDecisionKind::Registered(EngravingDecisionRegistryId(rng.next_u64() as u128)),
|
||
}
|
||
}
|
||
|
||
/// An engraving-decision record with a content-derived id.
|
||
pub fn gen_engraving_decision(rng: &mut Rng) -> EngravingDecision {
|
||
EngravingDecision::with_source(
|
||
gen_layout_object_id(rng),
|
||
gen_engraving_decision_kind(rng),
|
||
gen_decision_source(rng),
|
||
)
|
||
}
|
||
|
||
pub fn gen_override_target(rng: &mut Rng) -> OverrideTarget {
|
||
if rng.boolean() {
|
||
OverrideTarget::ScoreGraph(crate::generators::typed_object_id(rng))
|
||
} else {
|
||
OverrideTarget::IrSynthesized(gen_layout_object_id(rng))
|
||
}
|
||
}
|
||
|
||
/// A break anchor for a generated break override (a representative subset: a
|
||
/// wall-clock instant or an event anchor without offset).
|
||
pub fn gen_break_anchor(rng: &mut Rng) -> epiphany_core::TimeAnchor {
|
||
if rng.boolean() {
|
||
epiphany_core::TimeAnchor::WallClock {
|
||
time: epiphany_core::WallClockTime(rng.range(0, 10_000) as i64),
|
||
}
|
||
} else {
|
||
epiphany_core::TimeAnchor::Event {
|
||
id: epiphany_core::EventId::from_raw(rng.next_u64() as u128),
|
||
offset: epiphany_core::AnchorOffset::Zero,
|
||
}
|
||
}
|
||
}
|
||
|
||
pub fn gen_override_kind(rng: &mut Rng) -> OverrideKind {
|
||
match rng.below(9) {
|
||
0 => OverrideKind::StemDirection(if rng.boolean() {
|
||
epiphany_core::StemDirection::Up
|
||
} else {
|
||
epiphany_core::StemDirection::Down
|
||
}),
|
||
1 => OverrideKind::AccidentalParenthesized(rng.boolean()),
|
||
2 => OverrideKind::AccidentalVisible(rng.boolean()),
|
||
3 => OverrideKind::SystemBreak {
|
||
anchor: gen_break_anchor(rng),
|
||
},
|
||
4 => OverrideKind::PageBreak {
|
||
anchor: gen_break_anchor(rng),
|
||
},
|
||
5 => OverrideKind::HiddenObject,
|
||
6 => OverrideKind::CustomPosition(gen_point(rng)),
|
||
7 => OverrideKind::LedgerLineSuppression,
|
||
_ => OverrideKind::Registered(rng.next_u64() as u128),
|
||
}
|
||
}
|
||
|
||
pub fn gen_engraving_override(rng: &mut Rng) -> EngravingOverride {
|
||
EngravingOverride {
|
||
id: EngravingOverrideId(rng.next_u64() as u128),
|
||
target: gen_override_target(rng),
|
||
kind: gen_override_kind(rng),
|
||
priority: if rng.boolean() {
|
||
OverridePriority::Hard
|
||
} else {
|
||
OverridePriority::Soft
|
||
},
|
||
origin: match rng.below(4) {
|
||
0 => OverrideOrigin::User {
|
||
author: AuthorId(rng.next_u64() as u128),
|
||
timestamp: Timestamp(rng.next_u64() as i64),
|
||
},
|
||
1 => OverrideOrigin::Import {
|
||
format: ForeignFormatId(rng.next_u64() as u128),
|
||
},
|
||
2 => OverrideOrigin::Plugin {
|
||
plugin: PluginId(rng.next_u64() as u128),
|
||
},
|
||
_ => OverrideOrigin::Internal,
|
||
},
|
||
}
|
||
}
|
||
|
||
/// A vertical-band kind (every variant).
|
||
pub fn gen_vertical_band_kind(rng: &mut Rng) -> VerticalBandKind {
|
||
match rng.below(4) {
|
||
0 => VerticalBandKind::Staff(crate::generators::staff_id(rng)),
|
||
1 => VerticalBandKind::InterStaffGap,
|
||
2 => VerticalBandKind::InterSystemGap,
|
||
_ => VerticalBandKind::MarginBand,
|
||
}
|
||
}
|
||
|
||
/// A vertical band (every constructor / kind exercised).
|
||
pub fn gen_vertical_band(rng: &mut Rng) -> VerticalBand {
|
||
let members: Vec<GlyphObjectId> = (0..rng.range_usize(0, 3))
|
||
.map(|_| gen_glyph_object_id(rng))
|
||
.collect();
|
||
match rng.below(4) {
|
||
0 => VerticalBand::staff(crate::generators::staff_id(rng), members),
|
||
1 => VerticalBand::margin(LayoutObjectId(rng.next_u64() as u128), members),
|
||
2 => VerticalBand::inter_staff_gap(gen_vertical_band_id(rng)),
|
||
_ => VerticalBand::inter_system_gap(gen_vertical_band_id(rng)),
|
||
}
|
||
}
|
||
|
||
/// An operation-kind tag (every variant Agent C's type provides, including the
|
||
/// registered form).
|
||
///
|
||
/// Genesis tranche G2a (row 29): **derived**, not hand-maintained. The prior
|
||
/// hand-written `match` had already gone stale twice — it stopped at 23 until
|
||
/// the Phase-3 tranche and again omitted the repeat pair — because nothing
|
||
/// forces a hand-written arm list to move when the vocabulary grows, even
|
||
/// though this generator's own doc promises *every* variant. Built-ins are
|
||
/// drawn uniformly from `OperationKindTag::PAYLOAD_FREE`
|
||
/// (`operation_kind_tag_vocabulary!`'s single source of truth, `payload.rs`) —
|
||
/// `Registered` is excluded from `PAYLOAD_FREE` by design (it carries a
|
||
/// payload, `payload.rs:469`) and is appended here explicitly. Every future
|
||
/// built-in append then follows structurally, with no edit to this function;
|
||
/// see `OperationKind::discriminant()`'s in-crate precedent
|
||
/// (`payload.rs:2052`, `all_tags`).
|
||
pub fn gen_operation_kind_tag(rng: &mut Rng) -> OperationKindTag {
|
||
// +1 slot for `Registered`, drawn last.
|
||
let index = rng.below(OperationKindTag::PAYLOAD_FREE.len() as u64 + 1) as usize;
|
||
match OperationKindTag::PAYLOAD_FREE.get(index) {
|
||
Some(tag) => *tag,
|
||
None => OperationKindTag::Registered(epiphany_ops::OperationKindRegistryId(
|
||
rng.next_u64() as u128
|
||
)),
|
||
}
|
||
}
|
||
|
||
/// An object kind (the kind of a generated score-graph object).
|
||
pub fn gen_object_kind(rng: &mut Rng) -> ObjectKind {
|
||
ObjectKind::of(&crate::generators::typed_object_id(rng))
|
||
}
|
||
|
||
/// A pitch-space id drawn from the built-in catalog.
|
||
fn gen_pitch_space_id(rng: &mut Rng) -> epiphany_core::PitchSpaceId {
|
||
let names = ["cmn-12", "edo-31", "ji-5limit"];
|
||
epiphany_core::PitchSpaceId::new(names[rng.below(names.len() as u64) as usize])
|
||
}
|
||
|
||
/// A barrier scope (every variant).
|
||
pub fn gen_barrier_scope(rng: &mut Rng) -> BarrierScope {
|
||
match rng.below(8) {
|
||
0 => BarrierScope::WholeScore,
|
||
1 => BarrierScope::Region(crate::generators::region_id(rng)),
|
||
2 => BarrierScope::StaffInstance(crate::generators::staff_instance_id(rng)),
|
||
3 => BarrierScope::AnalysisLayer(crate::generators::analysis_layer_id(rng)),
|
||
4 => BarrierScope::ObjectSet(
|
||
(0..rng.range_usize(0, 3))
|
||
.map(|_| crate::generators::typed_object_id(rng))
|
||
.collect(),
|
||
),
|
||
5 => BarrierScope::PitchSpace(gen_pitch_space_id(rng)),
|
||
6 => BarrierScope::TuningContext,
|
||
_ => BarrierScope::Registered(BarrierScopeRegistryId(rng.next_u64() as u128)),
|
||
}
|
||
}
|
||
|
||
/// A barrier condition (bounded recursion depth; every variant).
|
||
pub fn gen_barrier_condition(rng: &mut Rng) -> BarrierCondition {
|
||
match rng.below(7) {
|
||
0 => BarrierCondition::Always,
|
||
1 => BarrierCondition::ObjectExists(crate::generators::typed_object_id(rng)),
|
||
2 => BarrierCondition::ObjectHasExtensionData {
|
||
object: crate::generators::typed_object_id(rng),
|
||
extension: ExtensionRef(rng.next_u64() as u128),
|
||
},
|
||
3 => BarrierCondition::All(vec![BarrierCondition::Always]),
|
||
4 => BarrierCondition::Any(vec![
|
||
BarrierCondition::Always,
|
||
BarrierCondition::ObjectExists(crate::generators::typed_object_id(rng)),
|
||
]),
|
||
5 => BarrierCondition::Not(Box::new(BarrierCondition::Always)),
|
||
_ => BarrierCondition::Registered(BarrierConditionRegistryId(rng.next_u64() as u128)),
|
||
}
|
||
}
|
||
|
||
/// An edit barrier keyed on operation-kind tags.
|
||
pub fn gen_edit_barrier(rng: &mut Rng) -> EditBarrier {
|
||
EditBarrier {
|
||
scope: gen_barrier_scope(rng),
|
||
affected_object_kinds: (0..rng.range_usize(0, 3))
|
||
.map(|_| gen_object_kind(rng))
|
||
.collect(),
|
||
prohibited_operation_kinds: (0..rng.range_usize(0, 3))
|
||
.map(|_| gen_operation_kind_tag(rng))
|
||
.collect(),
|
||
condition: gen_barrier_condition(rng),
|
||
}
|
||
}
|
||
|
||
/// An edit context (an object's structural location, every field exercised).
|
||
pub fn gen_edit_context(rng: &mut Rng) -> EditContext {
|
||
EditContext {
|
||
region: rng.boolean().then(|| crate::generators::region_id(rng)),
|
||
staff_instance: rng
|
||
.boolean()
|
||
.then(|| crate::generators::staff_instance_id(rng)),
|
||
analysis_layer: rng
|
||
.boolean()
|
||
.then(|| crate::generators::analysis_layer_id(rng)),
|
||
pitch_space: rng.boolean().then(|| gen_pitch_space_id(rng)),
|
||
}
|
||
}
|
||
|
||
/// A solver tier (every variant).
|
||
pub fn gen_solver_tier(rng: &mut Rng) -> SolverTier {
|
||
*rng.choose(&[
|
||
SolverTier::Stub,
|
||
SolverTier::Minimal,
|
||
SolverTier::Standard,
|
||
SolverTier::Advanced,
|
||
])
|
||
}
|
||
|
||
/// A solver version.
|
||
pub fn gen_solver_version(rng: &mut Rng) -> SolverVersion {
|
||
SolverVersion(rng.next_u64() as u32)
|
||
}
|
||
|
||
/// A deterministic solver budget.
|
||
pub fn gen_solver_budget(rng: &mut Rng) -> SolverBudget {
|
||
SolverBudget {
|
||
max_iterations: rng.next_u64(),
|
||
max_nodes: rng.next_u64(),
|
||
max_constraint_evaluations: rng.next_u64(),
|
||
advisory_wall_time_ms: rng.boolean().then(|| rng.range(0, 1000)),
|
||
}
|
||
}
|
||
|
||
/// A consumed-budget record.
|
||
pub fn gen_solver_budget_used(rng: &mut Rng) -> SolverBudgetUsed {
|
||
SolverBudgetUsed {
|
||
iterations: rng.next_u64(),
|
||
nodes: rng.next_u64(),
|
||
constraint_evaluations: rng.next_u64(),
|
||
wall_time_ms: rng.next_u64(),
|
||
}
|
||
}
|
||
|
||
/// A solver state.
|
||
pub fn gen_solver_state(rng: &mut Rng) -> SolverState {
|
||
SolverState {
|
||
solver_version: rng.boolean().then(|| gen_solver_version(rng)),
|
||
resolved_glyphs: rng.range_usize(0, 32),
|
||
}
|
||
}
|
||
|
||
/// A solver profile (every variant).
|
||
pub fn gen_solver_profile(rng: &mut Rng) -> SolverProfile {
|
||
*rng.choose(&[
|
||
SolverProfile::Draft,
|
||
SolverProfile::Standard,
|
||
SolverProfile::Publication,
|
||
])
|
||
}
|
||
|
||
/// Tie-breaking weights.
|
||
pub fn gen_tie_breaking_weights(rng: &mut Rng) -> TieBreakingWeights {
|
||
let w = |r: &mut Rng| (r.range(0, 4000) as f64) / 1000.0;
|
||
TieBreakingWeights {
|
||
collision: w(rng),
|
||
spacing: w(rng),
|
||
slur_shape: w(rng),
|
||
beam_slope: w(rng),
|
||
vertical_density: w(rng),
|
||
system_break: w(rng),
|
||
page_fill: w(rng),
|
||
casting_off: w(rng),
|
||
symbol_density: w(rng),
|
||
}
|
||
}
|
||
|
||
/// A solver configuration (profile + budget + tie-breaking).
|
||
pub fn gen_solver_config(rng: &mut Rng) -> SolverConfig {
|
||
SolverConfig {
|
||
profile: gen_solver_profile(rng),
|
||
budget: gen_solver_budget(rng),
|
||
tie_breaking: gen_tie_breaking_weights(rng),
|
||
}
|
||
}
|
||
|
||
/// A normalized quality metric in `[0, 1]`.
|
||
pub fn gen_normalized_metric(rng: &mut Rng) -> NormalizedMetric {
|
||
NormalizedMetric::new((rng.range(0, 1000) as f64) / 1000.0)
|
||
}
|
||
|
||
/// A constraint id.
|
||
pub fn gen_constraint_id(rng: &mut Rng) -> ConstraintId {
|
||
ConstraintId(rng.next_u64() as u128)
|
||
}
|
||
|
||
/// A spring-slot id.
|
||
pub fn gen_spring_slot_id(rng: &mut Rng) -> SpringSlotId {
|
||
SpringSlotId(rng.next_u64() as u128)
|
||
}
|
||
|
||
/// A quality-metric kind (every variant).
|
||
pub fn gen_quality_metric_kind(rng: &mut Rng) -> QualityMetricKind {
|
||
*rng.choose(&[
|
||
QualityMetricKind::Collision,
|
||
QualityMetricKind::Spacing,
|
||
QualityMetricKind::SlurShape,
|
||
QualityMetricKind::BeamSlope,
|
||
QualityMetricKind::VerticalDensity,
|
||
QualityMetricKind::SystemBreak,
|
||
QualityMetricKind::PageFill,
|
||
QualityMetricKind::CastingOff,
|
||
QualityMetricKind::SymbolDensity,
|
||
])
|
||
}
|
||
|
||
/// An extension-warning id.
|
||
pub fn gen_extension_warning_id(rng: &mut Rng) -> ExtensionWarningId {
|
||
ExtensionWarningId(rng.next_u64() as u128)
|
||
}
|
||
|
||
/// A solver warning (every kind).
|
||
pub fn gen_solver_warning(rng: &mut Rng) -> SolverWarning {
|
||
let kind = match rng.below(4) {
|
||
0 => SolverWarningKind::LargeSoftConstraintViolation {
|
||
constraint: gen_constraint_id(rng),
|
||
magnitude: (rng.range(0, 10_000) as f64) / 1000.0,
|
||
},
|
||
1 => SolverWarningKind::UnusualLayoutDecision("stub".to_string()),
|
||
2 => SolverWarningKind::QualityFloorApproached {
|
||
metric: gen_quality_metric_kind(rng),
|
||
},
|
||
_ => SolverWarningKind::ExtensionWarning(gen_extension_warning_id(rng)),
|
||
};
|
||
SolverWarning {
|
||
kind,
|
||
affected_objects: (0..rng.range_usize(0, 2))
|
||
.map(|_| crate::generators::typed_object_id(rng))
|
||
.collect(),
|
||
message: "generated".to_string(),
|
||
}
|
||
}
|
||
|
||
/// An extension-metric id.
|
||
pub fn gen_extension_metric_id(rng: &mut Rng) -> ExtensionMetricId {
|
||
ExtensionMetricId(rng.next_u64() as u128)
|
||
}
|
||
|
||
/// An extension-contributed quality metric.
|
||
pub fn gen_extension_metric(rng: &mut Rng) -> ExtensionMetric {
|
||
ExtensionMetric {
|
||
metric_id: gen_extension_metric_id(rng),
|
||
value: gen_normalized_metric(rng),
|
||
}
|
||
}
|
||
|
||
/// A (non-default) quality metric vector.
|
||
pub fn gen_quality_metric_vector(rng: &mut Rng) -> QualityMetricVector {
|
||
QualityMetricVector {
|
||
collision_penalty: gen_normalized_metric(rng),
|
||
spacing_distortion: gen_normalized_metric(rng),
|
||
slur_shape_penalty: gen_normalized_metric(rng),
|
||
beam_slope_penalty: gen_normalized_metric(rng),
|
||
vertical_density_penalty: gen_normalized_metric(rng),
|
||
system_break_penalty: gen_normalized_metric(rng),
|
||
page_fill_efficiency: gen_normalized_metric(rng),
|
||
casting_off_quality: gen_normalized_metric(rng),
|
||
symbol_density_uniformity: gen_normalized_metric(rng),
|
||
extension_metrics: (0..rng.range_usize(0, 2))
|
||
.map(|_| gen_extension_metric(rng))
|
||
.collect(),
|
||
}
|
||
}
|
||
|
||
/// A render target (every variant).
|
||
pub fn gen_render_target(rng: &mut Rng) -> RenderTarget {
|
||
*rng.choose(&[
|
||
RenderTarget::Pdf,
|
||
RenderTarget::Svg,
|
||
RenderTarget::Screen,
|
||
RenderTarget::Print,
|
||
])
|
||
}
|
||
|
||
pub fn gen_color_configuration(rng: &mut Rng) -> ColorConfiguration {
|
||
ColorConfiguration {
|
||
color_space: *rng.choose(&[
|
||
ColorSpace::Srgb,
|
||
ColorSpace::DisplayP3,
|
||
ColorSpace::Cmyk,
|
||
ColorSpace::Grayscale,
|
||
]),
|
||
embed_profile: rng.boolean(),
|
||
}
|
||
}
|
||
|
||
pub fn gen_rasterization_configuration(rng: &mut Rng) -> RasterizationConfiguration {
|
||
RasterizationConfiguration {
|
||
antialias: rng.boolean(),
|
||
dpi: rng.range(72, 1201) as u32,
|
||
}
|
||
}
|
||
|
||
/// A render configuration.
|
||
pub fn gen_render_configuration(rng: &mut Rng) -> RenderConfiguration {
|
||
RenderConfiguration {
|
||
target: gen_render_target(rng),
|
||
color: gen_color_configuration(rng),
|
||
rasterization: gen_rasterization_configuration(rng),
|
||
}
|
||
}
|
||
|
||
/// An invalidation scope (every variant).
|
||
pub fn gen_invalidation_scope(rng: &mut Rng) -> InvalidationScope {
|
||
*rng.choose(&[
|
||
InvalidationScope::ObjectLocal,
|
||
InvalidationScope::MeasureLocal,
|
||
InvalidationScope::SystemLocal,
|
||
InvalidationScope::PageLocal,
|
||
InvalidationScope::RegionLocal,
|
||
InvalidationScope::WholeScore,
|
||
])
|
||
}
|
||
|
||
/// An invalidation set over generated slots, bands, constraints, and glyphs.
|
||
pub fn gen_invalidation_set(rng: &mut Rng) -> InvalidationSet {
|
||
let n = |r: &mut Rng| r.range_usize(0, 3);
|
||
InvalidationSet {
|
||
scope: gen_invalidation_scope(rng),
|
||
slots: (0..n(rng)).map(|_| gen_spring_slot_id(rng)).collect(),
|
||
bands: (0..n(rng)).map(|_| gen_vertical_band_id(rng)).collect(),
|
||
constraints: (0..n(rng)).map(|_| gen_constraint_id(rng)).collect(),
|
||
glyphs: (0..n(rng)).map(|_| gen_glyph_object_id(rng)).collect(),
|
||
}
|
||
}
|
||
|
||
// --- Remaining public-type generators (Agent F mandate completeness) ---
|
||
|
||
/// A render-scale context.
|
||
pub fn gen_scale_context(rng: &mut Rng) -> ScaleContext {
|
||
ScaleContext {
|
||
points_per_staff_space: 4.0 + (rng.range(0, 6000) as f32) / 1000.0,
|
||
}
|
||
}
|
||
|
||
/// A glyph anchor.
|
||
pub fn gen_glyph_anchor(rng: &mut Rng) -> GlyphAnchor {
|
||
let names = ["stemUpNW", "stemDownSE", "cutOutNE"];
|
||
GlyphAnchor {
|
||
name: std::borrow::Cow::Borrowed(names[rng.below(names.len() as u64) as usize]),
|
||
x: rng.range(0, 2048) as i32,
|
||
y: rng.range(0, 2048) as i32,
|
||
}
|
||
}
|
||
|
||
/// A vertical-band id.
|
||
pub fn gen_vertical_band_id(rng: &mut Rng) -> VerticalBandId {
|
||
VerticalBandId(((rng.next_u64() as u128) << 64) | rng.next_u64() as u128)
|
||
}
|
||
|
||
/// A time-axis kind (every variant).
|
||
pub fn gen_time_axis_kind(rng: &mut Rng) -> TimeAxisKind {
|
||
match rng.below(4) {
|
||
0 => TimeAxisKind::Metric,
|
||
1 => TimeAxisKind::Proportional,
|
||
2 => TimeAxisKind::Aleatoric,
|
||
_ => TimeAxisKind::Registered(TimeAxisRegistryId(rng.next_u64() as u128)),
|
||
}
|
||
}
|
||
|
||
/// An engraving-decision id (content-derived from a generated decision).
|
||
pub fn gen_engraving_decision_id(rng: &mut Rng) -> EngravingDecisionId {
|
||
gen_engraving_decision(rng).id
|
||
}
|
||
|
||
/// A Sem-ver font version.
|
||
pub fn gen_sem_ver(rng: &mut Rng) -> SemVer {
|
||
SemVer::new(
|
||
rng.range(0, 4) as u32,
|
||
rng.range(0, 400) as u32,
|
||
rng.range(0, 9) as u32,
|
||
)
|
||
}
|
||
|
||
/// Glyph render data availability.
|
||
pub fn gen_glyph_render_data(rng: &mut Rng) -> GlyphRenderData {
|
||
GlyphRenderData {
|
||
outline: if rng.boolean() {
|
||
vec![gen_path_command(rng), PathCommand::Close]
|
||
} else {
|
||
vec![]
|
||
},
|
||
bitmap: rng.boolean().then(|| gen_glyph_bitmap(rng)),
|
||
}
|
||
}
|
||
|
||
pub fn gen_path_command(rng: &mut Rng) -> PathCommand {
|
||
match rng.below(4) {
|
||
0 => PathCommand::MoveTo(gen_point(rng)),
|
||
1 => PathCommand::LineTo(gen_point(rng)),
|
||
2 => PathCommand::CurveTo {
|
||
control1: gen_point(rng),
|
||
control2: gen_point(rng),
|
||
to: gen_point(rng),
|
||
},
|
||
_ => PathCommand::Close,
|
||
}
|
||
}
|
||
|
||
pub fn gen_glyph_bitmap(rng: &mut Rng) -> GlyphBitmap {
|
||
GlyphBitmap {
|
||
width: 1,
|
||
height: 1,
|
||
rgba8: vec![rng.next_u64() as u8; 4],
|
||
}
|
||
}
|
||
|
||
/// A staff-space bounding box drawn from a bundled glyph.
|
||
pub fn gen_glyph_bounding_box(rng: &mut Rng) -> BoundingBox {
|
||
gen_glyph_metric(rng).bounding_box()
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use crate::fixtures;
|
||
use epiphany_core::TypedObjectId;
|
||
use epiphany_determinism::CanonicalEncode;
|
||
|
||
/// (s10, row 29) `gen_operation_kind_tag`'s draw must cover exactly
|
||
/// `PAYLOAD_FREE` ∪ `{Registered}` — **not** merely the appended
|
||
/// discriminants 30..=34, which would pass even if the `Registered`
|
||
/// append (structurally different from every built-in: it is the one
|
||
/// variant `PAYLOAD_FREE` excludes by design, `payload.rs:469`) were
|
||
/// deleted from the generator entirely.
|
||
#[test]
|
||
fn gen_operation_kind_tag_covers_payload_free_and_registered() {
|
||
let mut rng = Rng::new(0x6E_57_A9);
|
||
let mut saw_registered = false;
|
||
let mut seen_builtins = std::collections::BTreeSet::new();
|
||
for _ in 0..5000 {
|
||
match gen_operation_kind_tag(&mut rng) {
|
||
OperationKindTag::Registered(_) => saw_registered = true,
|
||
other => {
|
||
seen_builtins.insert(other);
|
||
}
|
||
}
|
||
}
|
||
assert!(saw_registered, "Registered never drawn in 5000 samples");
|
||
let expected: std::collections::BTreeSet<_> =
|
||
OperationKindTag::PAYLOAD_FREE.iter().copied().collect();
|
||
assert_eq!(
|
||
seen_builtins, expected,
|
||
"the draw's built-in coverage must be exactly PAYLOAD_FREE"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn edit_barrier_blob_codec_round_trips_generated_barriers() {
|
||
// Property gate for the provisional manifest-blob byte form
|
||
// (`ExtensionDeclaration.edit_barriers` / `.affected_object_kinds`):
|
||
// any generated barrier set encodes to a blob that decodes and
|
||
// re-encodes byte-identically, and each decoded barrier's own canonical
|
||
// bytes round-trip through the single-barrier decoder.
|
||
let mut rng = Rng::new(0xBA22_1E2C_0DEC);
|
||
for _ in 0..256 {
|
||
let barriers: Vec<EditBarrier> = (0..rng.range_usize(0, 4))
|
||
.map(|_| gen_edit_barrier(&mut rng))
|
||
.collect();
|
||
let blob = encode_edit_barriers(&barriers);
|
||
let decoded = decode_edit_barriers(&blob).expect("a canonical blob decodes");
|
||
assert_eq!(
|
||
encode_edit_barriers(&decoded),
|
||
blob,
|
||
"decode → re-encode is byte-identical"
|
||
);
|
||
for barrier in &decoded {
|
||
let bytes = barrier.to_canonical_bytes();
|
||
assert_eq!(
|
||
EditBarrier::decode_canonical_bytes(&bytes).as_ref(),
|
||
Ok(barrier)
|
||
);
|
||
}
|
||
|
||
let kinds: Vec<ObjectKind> = (0..rng.range_usize(0, 6))
|
||
.map(|_| gen_object_kind(&mut rng))
|
||
.collect();
|
||
let kind_blob = encode_affected_object_kinds(&kinds);
|
||
let decoded_kinds =
|
||
decode_affected_object_kinds(&kind_blob).expect("a canonical blob decodes");
|
||
assert_eq!(encode_affected_object_kinds(&decoded_kinds), kind_blob);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn ir_generators_are_deterministic_and_well_formed() {
|
||
let mut a = Rng::new(13);
|
||
let mut b = Rng::new(13);
|
||
// Deterministic from the seed.
|
||
assert_eq!(gen_logical_layout_ir(&mut a), gen_logical_layout_ir(&mut b));
|
||
let generated = gen_logical_layout_ir(&mut Rng::new(14));
|
||
let constrained = try_to_constrained(&generated)
|
||
.expect("generated logical IR must be structurally transformable");
|
||
// The spacing stage emits real constraints, which the stub does not
|
||
// evaluate: the solve is renderable either way, and satisfaction is
|
||
// claimed exactly when the problem is constraint-free.
|
||
let report = StubSolver.solve(&constrained, &SolverConfig::default());
|
||
assert!(report.status.is_renderable());
|
||
assert_eq!(
|
||
report.satisfied_hard_constraints,
|
||
constrained.constraints.is_empty()
|
||
);
|
||
// Provenance ids are consistent with their synthesis: a projected one
|
||
// (no synthesis) carries the source-only id; a synthesized one does not.
|
||
// Every generated constrained IR is accepted by the real stub solver.
|
||
let mut rng = Rng::new(99);
|
||
for _ in 0..64 {
|
||
let p = gen_provenance(&mut rng);
|
||
match p.synthesis {
|
||
None => assert_eq!(p.stable_id, stable_layout_id(&p.source)),
|
||
Some(_) => assert_ne!(p.stable_id, stable_layout_id(&p.source)),
|
||
}
|
||
let ci = gen_constrained_layout_ir(&mut rng);
|
||
assert_eq!(
|
||
StubSolver.solve(&ci, &SolverConfig::default()).status,
|
||
SolveStatus::Solved
|
||
);
|
||
let _ = gen_glyph_object(&mut rng);
|
||
let _ = gen_resolved_glyph(&mut rng);
|
||
let _ = gen_time_axis_model(&mut rng);
|
||
let _ = gen_synthesis_kind(&mut rng);
|
||
let _ = gen_solve_status(&mut rng);
|
||
let _ = gen_glyph_catalog_identity(&mut rng);
|
||
let _ = gen_layout_object_id(&mut rng);
|
||
let _ = gen_resolved_layout_ir(&mut rng);
|
||
let _ = gen_render_primitive(&mut rng);
|
||
let _ = gen_render_ir(&mut rng);
|
||
let _ = gen_solve_report(&mut rng);
|
||
let _ = gen_round_trip_report(&mut rng);
|
||
// The rest of E's public surface.
|
||
let _ = gen_staff_space(&mut rng);
|
||
let _ = gen_size2d(&mut rng);
|
||
let _ = gen_bounding_box(&mut rng);
|
||
let _ = gen_glyph_object_id(&mut rng);
|
||
let _ = gen_glyph_metric(&mut rng);
|
||
let _ = gen_smufl_version(&mut rng);
|
||
let _ = gen_font_id(&mut rng);
|
||
let _ = gen_engraving_decision(&mut rng);
|
||
let _ = gen_vertical_band(&mut rng);
|
||
let _ = gen_vertical_band_kind(&mut rng);
|
||
let barrier = gen_edit_barrier(&mut rng);
|
||
// The barrier canonically encodes (Appendix D), and prohibition is
|
||
// well-defined against a generated edit context.
|
||
let _ = barrier.to_canonical_bytes();
|
||
let _ = barrier.prohibits_edit(
|
||
gen_operation_kind_tag(&mut rng),
|
||
&crate::generators::typed_object_id(&mut rng),
|
||
&gen_edit_context(&mut rng),
|
||
&AlwaysLiveOracle,
|
||
);
|
||
let _ = gen_solver_config(&mut rng);
|
||
let _ = gen_solver_tier(&mut rng);
|
||
let _ = gen_solver_version(&mut rng);
|
||
let _ = gen_invalidation_set(&mut rng);
|
||
// The remaining public surface.
|
||
let _ = gen_solver_budget_used(&mut rng);
|
||
let _ = gen_solver_state(&mut rng);
|
||
let _ = gen_solver_profile(&mut rng);
|
||
let _ = gen_tie_breaking_weights(&mut rng);
|
||
let _ = gen_normalized_metric(&mut rng);
|
||
let _ = gen_constraint_id(&mut rng);
|
||
let _ = gen_spring_slot_id(&mut rng);
|
||
let _ = gen_solver_warning(&mut rng);
|
||
let _ = gen_scale_context(&mut rng);
|
||
let _ = gen_glyph_anchor(&mut rng);
|
||
let _ = gen_vertical_band_id(&mut rng);
|
||
let _ = gen_time_axis_kind(&mut rng);
|
||
let _ = gen_engraving_decision_id(&mut rng);
|
||
let _ = gen_sem_ver(&mut rng);
|
||
let _ = gen_glyph_render_data(&mut rng);
|
||
let _ = gen_glyph_bounding_box(&mut rng);
|
||
let _ = gen_decision_source(&mut rng);
|
||
let _ = gen_engraving_decision_kind(&mut rng);
|
||
let _ = gen_object_kind(&mut rng);
|
||
let _ = gen_barrier_scope(&mut rng);
|
||
let _ = gen_barrier_condition(&mut rng);
|
||
let _ = gen_invalidation_scope(&mut rng);
|
||
let _ = gen_size2d(&mut rng);
|
||
let _ = gen_layout_object(&mut rng);
|
||
let _ = gen_layout_region(&mut rng);
|
||
let _ = gen_font_id(&mut rng);
|
||
let _ = gen_smufl_version(&mut rng);
|
||
let _ = gen_quality_metric_kind(&mut rng);
|
||
let _ = gen_extension_warning_id(&mut rng);
|
||
let _ = gen_extension_metric(&mut rng);
|
||
let _ = gen_extension_metric_id(&mut rng);
|
||
let _ = gen_quality_metric_vector(&mut rng);
|
||
let _ = gen_render_target(&mut rng);
|
||
let _ = gen_render_configuration(&mut rng);
|
||
let _ = gen_glyph_render_data(&mut rng);
|
||
let _ = gen_vertical_band(&mut rng);
|
||
let _ = gen_rect(&mut rng);
|
||
let _ = gen_transform_2d(&mut rng);
|
||
let _ = gen_margins(&mut rng);
|
||
let _ = gen_score_version(&mut rng);
|
||
let _ = gen_local_coordinate_system(&mut rng);
|
||
let _ = gen_vertical_extent(&mut rng);
|
||
let _ = gen_cross_region_object(&mut rng);
|
||
let _ = gen_synthesis_instance_key(&mut rng);
|
||
let _ = gen_glyph_style(&mut rng);
|
||
let _ = gen_time_point(&mut rng);
|
||
let _ = gen_time_range(&mut rng);
|
||
let _ = gen_spring_slot(&mut rng);
|
||
let _ = gen_constrained_layout_region(&mut rng);
|
||
let _ = gen_layout_constraint(&mut rng);
|
||
let _ = gen_constraint_strength(&mut rng);
|
||
let _ = gen_engraving_override(&mut rng);
|
||
let _ = gen_resolved_staff(&mut rng);
|
||
let _ = gen_resolved_measure(&mut rng);
|
||
let _ = gen_resolved_system(&mut rng);
|
||
let _ = gen_resolved_page(&mut rng);
|
||
let _ = gen_dependency_index(&mut rng);
|
||
let _ = gen_layout_cache(&mut rng);
|
||
let _ = gen_system_id(&mut rng);
|
||
let _ = gen_logical_region_cache(&mut rng);
|
||
let _ = gen_constrained_region_cache(&mut rng);
|
||
let _ = gen_resolved_system_cache(&mut rng);
|
||
let _ = gen_fine_layout_cache(&mut rng);
|
||
let _ = gen_color_configuration(&mut rng);
|
||
let _ = gen_rasterization_configuration(&mut rng);
|
||
let _ = gen_path_command(&mut rng);
|
||
let _ = gen_glyph_bitmap(&mut rng);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn solver_rejects_a_catalog_hash_that_misses_its_glyphs() {
|
||
let mut rng = Rng::new(8);
|
||
let mut input = gen_constrained_layout_ir(&mut rng);
|
||
// Ensure there is at least one glyph to consult.
|
||
if input.glyphs.is_empty() {
|
||
input = gen_constrained_layout_ir(&mut Rng::new(3));
|
||
}
|
||
assert_eq!(
|
||
StubSolver.solve(&input, &SolverConfig::default()).status,
|
||
SolveStatus::Solved
|
||
);
|
||
input.catalog.metrics_hash[0] ^= 1;
|
||
assert_eq!(
|
||
StubSolver.solve(&input, &SolverConfig::default()).status,
|
||
SolveStatus::InternalError,
|
||
"solver must reject a catalog hash that does not cover consulted metrics"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn ten_measure_single_staff_round_trips() {
|
||
// The QUICKSTART's headline case for Agent E's hand-off: a real
|
||
// 10-measure single-staff score, driven through the real crate.
|
||
let score = fixtures::ten_measure_single_staff(0xA11CE);
|
||
let report = round_trip(&score);
|
||
assert!(report.glyphs > 0);
|
||
assert_eq!(
|
||
report.render_primitives,
|
||
report.glyphs + report.render_strokes
|
||
);
|
||
|
||
let measures = report
|
||
.recovered_sources
|
||
.iter()
|
||
.filter(|s| matches!(s, TypedObjectId::Measure(_)))
|
||
.count();
|
||
assert_eq!(measures, 10, "all ten measures must be laid out");
|
||
assert!(report
|
||
.recovered_sources
|
||
.iter()
|
||
.any(|s| matches!(s, TypedObjectId::Tie(_))));
|
||
assert!(report
|
||
.recovered_sources
|
||
.iter()
|
||
.any(|s| matches!(s, TypedObjectId::ChordSymbol(_))));
|
||
}
|
||
}
|