epiphany/crates/epiphany-engrave/src/casting.rs

2576 lines
106 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.

//! The **casting-off pass** — Minimal-tier system breaking, vertical stacking,
//! and page assignment (Chapter 9 §"The Constraint-Solving Stage": the solver
//! "resolve\[s\] page and system breaks"; Chapter 7 §"ResolvedLayoutIR" defines
//! the page/system tree this pass populates).
//!
//! ## The algorithm (optimal break search)
//!
//! [`SolverTier::Minimal`](epiphany_layout_ir::SolverTier) requires the break
//! constraint family to be supported and every hard constraint satisfied (or an
//! honest `Unsatisfiable`); it makes **no optimality claim**. Casting-off uses a
//! deterministic **badness-minimizing break search** (`optimal_breaks`, a
//! KnuthPlass-style dynamic program) — an honest improvement on the earlier
//! greedy-first-fit-plus-widow-rebalance, not a formal optimality guarantee.
//!
//! 1. **System breaking.** Per region, partition the measures into systems to
//! minimize the total squared normalized underfill over ALL systems — which
//! evens them (no lopsided split) and fills them (no needless breaks), the
//! additive analog of the Quality Metric Catalog's break/imbalance
//! distribution cost; including the final system in the sum is what removes
//! the old separate widow rebalance. Breaks fall only at **measure
//! boundaries** — the barline columns (`to_constrained` draws each measure's
//! barline at its start column; the region-final barline closes the region
//! and is never a break candidate). A **hard** `SystemBreakAt`/`PageBreakAt`
//! is *always* honoured at its slot (and bounds the search's segments); a
//! **soft** one is honoured unless doing so would close a system with no
//! musical content (no notehead/rest column) — the documented exceptional
//! path, recorded as an [`EngravingDecision`] with
//! [`DecisionSource::IrOverride`] per the spec's override-resolution rule (an
//! unhonoured override is recorded, not silently dropped). A region with no
//! measures has no break candidates: it stays one (possibly overfull) system
//! unless breaks force otherwise. A single measure wider than the page yields
//! an overfull system — Minimal does not break mid-measure on its own.
//! 2. **Vertical stacking.** Each system's height is its real content extent
//! (glyph boxes plus stroke extents — the vertical spring solve that would
//! renegotiate band heights is deferred, so the constrained `y` geometry is
//! authoritative); consecutive systems are separated by the vertical-band
//! model's **inter-system gap** ([`VerticalBand::inter_system_gap`], the
//! preferred height — genuinely read from the band constructor so the two
//! cannot drift). Systems that no longer fit the page content height start
//! the next page.
//! 3. **Page assignment and the world frame.** Pages stack **vertically in one
//! world**: page *n*'s top edge sits [`INTER_PAGE_GAP`] staff spaces below
//! page *n1*'s bottom edge, page 1's top-left corner at the origin (world
//! is y-up, so pages grow downward in y). Every glyph and stroke position
//! is **baked** into this single world frame (each system is translated
//! rigidly: x back to the left margin, y to its stacked position), so the
//! flat glyph/stroke lists remain the renderer's and hit-tester's single
//! coordinate space — no per-page transform exists anywhere downstream.
//!
//! ## Region-spanning strokes
//!
//! A stroke confined to one system (a stem, a ledger, a barline-anchored mark)
//! translates rigidly with it. A stroke spanning several systems — in practice
//! the five staff lines, which `to_constrained` draws across the whole region —
//! is **split** at the system boundaries: the first segment keeps the original
//! stroke's exact provenance (so the round-trip's preservation contract holds),
//! and each later segment is engraver-**synthesized** from the same source
//! ([`SynthesisKind::Registered`] under [`SYSTEM_CONTINUATION_SYNTHESIS`], the
//! codebase's convention for a synthesis kind the normative vocabulary does not
//! name), keyed by [`continuation_instance_key`] so segments of different lines
//! can never collide.
//!
//! ## Default page geometry
//!
//! The spec names `Canvas.layout_defaults` ("paper size, margins") but does not
//! define its type, and the core graph deliberately does not carry it yet (the
//! graph home is staged to the data-model schema major — see `DECISIONS.md`),
//! so page geometry is an **engraver-side parameter** ([`PageGeometry`], a
//! constructor argument of [`crate::Engraver`]) with a documented default; see
//! [`PageGeometry::default`] for the arithmetic.
use std::collections::{BTreeMap, BTreeSet};
use epiphany_core::{StaffId, TypedObjectId};
use epiphany_layout_ir::{
continuation_instance_key, inter_staff_gap_id, is_barline_glyph, is_rigid_width_stroke,
synthesized_layout_id, BreakClass, BreakKind, ConstrainedLayoutIR, Curve, DecisionSource,
EngravingDecision, EngravingDecisionKind, EngravingOverrideId, GlyphObject, GlyphObjectId,
LayoutConstraint, LayoutObjectId, Margins, Point, PrimitiveIndices, Provenance, Rect,
ResolvedGlyph, ResolvedMeasure, ResolvedPage, ResolvedStaff, ResolvedSystem, Size2D,
SpringSlotId, StaffSpace, Stroke, SynthesisInstanceKey, SynthesisKind, SynthesisRegistryId,
VerticalBand, VerticalBandId, VerticalBandKind,
};
use crate::owning_glyph;
/// The registry id for the engraver's **system-continuation synthesis**: the
/// segment of a region-spanning stroke (a staff line) that casting-off places
/// in a system after the stroke's first. The normative [`SynthesisKind`] set
/// names no purely visual continuation rule, so — like the constrained stage's
/// staff-line/ledger/accidental syntheses — it is carried as a `Registered`
/// extension kind (Chapter 7 §"Behavior Under Unknown Extensions").
pub const SYSTEM_CONTINUATION_SYNTHESIS: SynthesisRegistryId =
SynthesisRegistryId(0x5359_5354_4D53_4547); // "SYSTMSEG"
/// The vertical gap between consecutive **pages** in the single world frame, in
/// staff spaces. Pages are separate physical sheets; this gap exists only in
/// the continuous scroll-like world the renderer and hit-tester share, so it is
/// a presentation constant, not engraving geometry.
pub const INTER_PAGE_GAP: f32 = 8.0;
/// Namespace bit for a synthesized *system* provenance instance key (a region's
/// second and later systems), disjoint from the page namespace below and — by
/// 128-bit-hash construction — from the slot-identity keys of break decisions.
const KEY_NS_SYSTEM: u128 = 1;
/// Namespace bit for a synthesized *page* provenance instance key.
const KEY_NS_PAGE: u128 = 2;
/// Page geometry the engraver casts off against: the page size and margins, in
/// staff spaces (Chapter 7 §7.2: IR coordinates are staff spaces). A parameter
/// of [`crate::Engraver`] because the score graph has no home for it yet — the
/// spec's `Canvas.layout_defaults` is named but never defined, and adding a
/// graph field is a data-model schema-major change (see `DECISIONS.md`).
#[derive(Copy, Clone, PartialEq, Debug)]
pub struct PageGeometry {
/// Full page size, in staff spaces.
pub size: Size2D,
/// Page margins, in staff spaces.
pub margins: Margins,
}
impl PageGeometry {
/// The horizontal content extent a system may fill: page width minus the
/// left and right margins. Non-positive geometry disables automatic
/// wrapping (treated as unbounded) rather than failing the solve.
pub fn content_width(&self) -> f32 {
self.size.width.0 - self.margins.left.0 - self.margins.right.0
}
/// The vertical content extent a page may fill: page height minus the top
/// and bottom margins. Non-positive geometry disables page overflow
/// (treated as unbounded) rather than failing the solve.
pub fn content_height(&self) -> f32 {
self.size.height.0 - self.margins.top.0 - self.margins.bottom.0
}
}
impl Default for PageGeometry {
/// **A4 portrait at an 8 mm staff height** (rastral ≈ size 1, a common
/// full-size instrumental-part raster), 15 mm margins. The arithmetic, with
/// 1 staff space = staff height / 4 = 2.0 mm:
///
/// * page: 210 mm × 297 mm → **105 × 148.5** staff spaces;
/// * margins: 15 mm each → **7.5** staff spaces;
/// * content area: 180 mm × 267 mm → **90 × 133.5** staff spaces.
///
/// 90 staff spaces of content width wraps the QUICKSTART's ten-measure
/// hand-off fixture (whose spaced width is ≈ 99 staff spaces) into two
/// systems — an honest multi-system default rather than one that only ever
/// produces the degenerate single line.
fn default() -> Self {
PageGeometry {
size: Size2D {
width: StaffSpace(105.0),
height: StaffSpace(148.5),
},
margins: Margins {
top: StaffSpace(7.5),
right: StaffSpace(7.5),
bottom: StaffSpace(7.5),
left: StaffSpace(7.5),
},
}
}
}
/// What the casting-off pass produced: the final world-frame geometry, the
/// populated page/system tree, the engraver's appended break decisions, and the
/// break structure the constraint evaluation consults.
pub(crate) struct CastLayout {
/// Final glyphs, in input order, positions baked into the world frame.
pub glyphs: Vec<ResolvedGlyph>,
/// Final strokes: the input strokes in order (each translated with its
/// system; a system-spanning stroke replaced by its first segment), then
/// the synthesized continuation segments.
pub strokes: Vec<Stroke>,
/// Final curves, in input order, each translated with its system. A curve
/// spanning a system break is split into per-system sub-curves by de
/// Casteljau subdivision (the first keeps the source's provenance, the rest
/// are synthesized continuations, like system-spanning strokes).
pub curves: Vec<Curve>,
/// The populated page tree (empty when the input declares no regions).
pub pages: Vec<ResolvedPage>,
/// Break decisions this pass made (chosen breaks in reading order, then
/// the skipped-soft `IrOverride` records in walk order).
pub decisions: Vec<EngravingDecision>,
/// Slots at which the final layout breaks: the first slot of every system.
pub system_start_slots: BTreeSet<SpringSlotId>,
/// Slots at which a page begins: the first slot of each page's first system.
pub page_start_slots: BTreeSet<SpringSlotId>,
/// The system each baked glyph landed in, parallel to `glyphs` — derived
/// once, inside the casting pass, from the slot→system assignment that
/// pass computes for its own use. **This is the glyph→system attribution,
/// full stop** (W1 pin 7): the raw slot map is deliberately *not*
/// published, so no consumer can grow a second copy of the rule that then
/// drifts. `None`: the glyph's slot was claimed by no region, so it
/// belongs to no per-system aggregate.
pub glyph_system: Vec<Option<usize>>,
/// The system each baked stroke landed in, parallel to `strokes` (including
/// the appended continuation segments). A stroke carries no spring slot, so
/// the slot map cannot answer for it; the casting pass records what it
/// already knew. `None`: claimed by no region.
pub stroke_system: Vec<Option<usize>>,
/// The system each baked curve landed in, parallel to `curves`.
pub curve_system: Vec<Option<usize>>,
/// The region each system slices, indexed by global system index (the
/// per-region grouping the casting-off quality metrics aggregate by).
pub region_of_system: Vec<usize>,
/// The primitives no system claims — `glyph_system`/`stroke_system`/
/// `curve_system` entries of `None`, gathered into the same shape
/// [`ResolvedSystem::primitives`] uses (W1 pin 3: unowned is a first-class
/// bucket, never coerced onto a system).
pub unowned: PrimitiveIndices,
}
/// One realized spring slot in spaced (pre-casting) coordinates, with the
/// classification the greedy walk needs.
struct SlotInfo {
id: SpringSlotId,
/// Reference x: the first member glyph's spaced baseline.
x: f32,
/// Leftmost content edge (member glyph boxes plus their rigid strokes).
lo: f32,
/// Rightmost content edge.
hi: f32,
/// Member glyph indices into the (parallel) input/spaced glyph vectors.
members: Vec<usize>,
/// The column carries a barline glyph — a measure boundary.
barline: bool,
/// The column carries the region-final barline (never a break candidate).
final_barline: bool,
/// The column carries musical content (a notehead or a rest).
note: bool,
/// The directly-manifested barline glyph of a measure *start* (glyph
/// index), for the per-system measure records. `None` at the final
/// barline: that measure's start is not marked by any column in this
/// projection, so its record is omitted rather than fabricated.
measure_barline: Option<usize>,
}
/// A break requirement a constraint declares at a slot.
#[derive(Copy, Clone)]
struct BreakReq {
page: bool,
hard: bool,
}
/// The boundary decision that opened a system (absent at a region's first).
#[derive(Copy, Clone)]
struct Boundary {
slot: SpringSlotId,
source: DecisionSource,
}
/// One cast-off system: which region it slices and which of that region's
/// slots it carries.
struct SystemPlan {
region: usize,
/// Region-local ordinal (0-based).
local: usize,
/// Indices into the region's ordered slot vector.
slots: Vec<usize>,
boundary: Option<Boundary>,
/// A page must start at this system (a page-break request sits here).
page_forced: bool,
/// Attribution for a forced page start (the page-break decision's source).
page_source: DecisionSource,
}
/// A stroke's casting fate: ride one system rigidly, or split at system
/// boundaries.
enum StrokeFate {
/// Translate the whole stroke with this system (`None`: not covered by any
/// region — left untransformed in the spaced frame, on no page).
Rigid(Option<usize>),
/// Per-system segments, ascending system order: `(system, from, to)` in
/// spaced coordinates.
Split(Vec<(usize, Point, Point)>),
}
/// A curve's casting fate: ride one system rigidly, or split at system
/// boundaries into per-system sub-cubics (de Casteljau).
enum CurveFate {
/// Translate the whole curve with this system (`None`: not covered by any
/// region).
Rigid(Option<usize>),
/// Per-system sub-cubics, ascending system order: `(system, control points)`
/// in spaced coordinates.
Split(Vec<(usize, [Point; 4])>),
}
/// A system's world-frame placement: a vertical shift `dy` plus a horizontal
/// affine map `world_x = a·x + b`.
///
/// A rigid (unjustified) system has `a = 1`, `b = dx` — a pure translation. A
/// **justified** system has `a > 1`: the horizontal slack (content width minus
/// natural ink width) is spread linearly across the line so its ink fills the
/// content width. The map is applied SLOT-RELATIVELY to glyphs — each slot's
/// members translate by the map evaluated at the slot's source, so intra-slot
/// offsets (a time signature after its barline, an accidental left of its
/// notehead) survive verbatim — directly to spanning-stroke and curve
/// endpoints, and via the owning slot for a rigid-width stroke (a stem or ledger
/// that must stay attached to its notehead, not stretch).
#[derive(Copy, Clone)]
struct Placement {
a: f32,
b: f32,
dy: f32,
/// The system's slot-source range `[x0, x1]`. The affine stretch acts only
/// WITHIN it; beyond it (a glyph's bearing overhang, a staff line drawn to
/// the ink edge) the map is rigid slope-1, so the mapped ink extremes agree
/// exactly with the per-slot deltas at the first/last slots.
x0: f32,
x1: f32,
}
impl Placement {
/// A pure translation (an unjustified system, or the identity fallback for
/// content no system claims). `a = 1`, so the clamp range is irrelevant.
fn rigid(dx: f32, dy: f32) -> Self {
Placement {
a: 1.0,
b: dx,
dy,
x0: 0.0,
x1: 0.0,
}
}
/// The world x of a spaced x: affine within the slot-source range, rigid
/// (slope 1) beyond it.
fn x(&self, x: f32) -> f32 {
let c = x.clamp(self.x0, self.x1);
self.a * c + self.b + (x - c)
}
/// The rigid delta every glyph in a slot whose source is `slot_x`
/// translates by — constant per slot, so intra-slot offsets are preserved.
/// Slot sources lie in `[x0, x1]`, so no clamp is needed.
fn slot_dx(&self, slot_x: f32) -> f32 {
(self.a - 1.0) * slot_x + self.b
}
/// The same placement sunk downward by `shift` — the inter-staff solve
/// pushes a staff's content down within its system (y-down is decreasing y).
fn sunk(&self, shift: f32) -> Self {
Placement {
dy: self.dy - shift,
..*self
}
}
}
/// The content extent of a system in spaced (pre-casting) coordinates.
#[derive(Copy, Clone)]
struct Extent {
min_x: f32,
min_y: f32,
max_x: f32,
max_y: f32,
any: bool,
}
impl Extent {
fn empty() -> Self {
Extent {
min_x: f32::INFINITY,
min_y: f32::INFINITY,
max_x: f32::NEG_INFINITY,
max_y: f32::NEG_INFINITY,
any: false,
}
}
/// Extend only the vertical extent (the inter-staff solve grows a system's
/// height by shifting staves apart, without touching its x-span).
fn add_y(&mut self, y0: f32, y1: f32) {
if y0.is_finite() && y1.is_finite() {
self.min_y = self.min_y.min(y0.min(y1));
self.max_y = self.max_y.max(y0.max(y1));
self.any = true;
}
}
/// Extend only the horizontal extent. Staff-attributed content contributes
/// its y through the inter-staff solve (SHIFTED), never here.
fn add_x(&mut self, x0: f32, x1: f32) {
if x0.is_finite() && x1.is_finite() {
self.min_x = self.min_x.min(x0.min(x1));
self.max_x = self.max_x.max(x0.max(x1));
self.any = true;
}
}
/// Normalized: a content-less system is a zero box at the origin.
fn normalized(self) -> Self {
if self.any {
self
} else {
Extent {
min_x: 0.0,
min_y: 0.0,
max_x: 0.0,
max_y: 0.0,
any: false,
}
}
}
}
/// The MUSCLOID target of an engraved break decision: synthesized from the
/// owning region's source under [`SynthesisKind::EngravedBreak`], keyed by the
/// breaking slot's identity (the slot id is itself content-derived from the
/// region and its column, so the key is the column's semantic identity, never a
/// layout-position ordinal).
fn break_target(region_source: TypedObjectId, slot: SpringSlotId) -> LayoutObjectId {
synthesized_layout_id(
&region_source,
SynthesisKind::EngravedBreak,
SynthesisInstanceKey(slot.0),
)
}
/// The decision source for a break honoured at `slot`: the user override that
/// asked for it when the projection recorded one, else `Automatic`.
fn origin_source(
origins: &BTreeMap<(u128, bool), EngravingOverrideId>,
slot: SpringSlotId,
page: bool,
) -> DecisionSource {
match origins.get(&(slot.0, page)) {
Some(id) => DecisionSource::UserOverride(*id),
None => DecisionSource::Automatic,
}
}
/// Casts the spaced layout off into systems and pages. Pure and deterministic:
/// a function of the input IR, the spaced geometry, and the page geometry.
pub(crate) fn cast_off(
input: &ConstrainedLayoutIR,
spaced_glyphs: &[ResolvedGlyph],
spaced_strokes: &[Stroke],
spaced_curves: &[Curve],
geometry: &PageGeometry,
) -> CastLayout {
// ---- Slot table (spaced coordinates) --------------------------------
let mut slots: BTreeMap<SpringSlotId, SlotInfo> = BTreeMap::new();
for (i, (glyph, spaced)) in input.glyphs.iter().zip(spaced_glyphs).enumerate() {
let name = glyph.glyph.as_str();
let x = spaced.position.x.0;
let lo = x + glyph.bounding_box.left.0;
let hi = x + glyph.bounding_box.right.0;
let entry = slots.entry(glyph.horizontal_slot).or_insert(SlotInfo {
id: glyph.horizontal_slot,
x,
lo,
hi,
members: Vec::new(),
barline: false,
final_barline: false,
note: false,
measure_barline: None,
});
entry.lo = entry.lo.min(lo);
entry.hi = entry.hi.max(hi);
entry.members.push(i);
// Barline classification by the engraver's own name vocabulary (which
// includes the composite repeat signs a repeat boundary morphs a
// measure barline into) — but only for a **directly-manifested measure
// barline**: the casting contract breaks systems at measure
// boundaries, so a repeat-synthesized standalone sign (a mid-measure
// boundary, a region edge without a final barline) must not become a
// phantom break candidate that could tear off a degenerate lone-sign
// trailing system or split a measure.
if is_barline_glyph(name)
&& glyph.provenance.synthesis.is_none()
&& matches!(glyph.provenance.source, TypedObjectId::Measure(_))
{
entry.barline = true;
if name == "barlineFinal" {
entry.final_barline = true;
} else if entry.measure_barline.is_none() {
entry.measure_barline = Some(i);
}
}
if name.starts_with("notehead") || name.starts_with("rest") {
entry.note = true;
}
}
// Fold each rigid stroke (a ledger line) into its owning slot's extent, so
// an overhanging ledger widens the measure it belongs to (mirrors the
// spacing pass's extent rule).
for (stroke, spaced) in input.strokes.iter().zip(spaced_strokes) {
if !is_rigid_width_stroke(stroke) {
continue;
}
if let Some(glyph) = owning_glyph(stroke, &input.glyphs) {
if let Some(entry) = slots.get_mut(&glyph.horizontal_slot) {
entry.lo = entry.lo.min(spaced.from.x.0.min(spaced.to.x.0));
entry.hi = entry.hi.max(spaced.from.x.0.max(spaced.to.x.0));
}
}
}
// ---- Region partition ------------------------------------------------
let mut region_of_glyph: BTreeMap<GlyphObjectId, usize> = BTreeMap::new();
for (r, region) in input.regions.iter().enumerate() {
for id in &region.glyphs {
region_of_glyph.entry(*id).or_insert(r);
}
}
let mut region_slots: Vec<Vec<SlotInfo>> =
(0..input.regions.len()).map(|_| Vec::new()).collect();
for (_, info) in slots {
let region = info
.members
.first()
.and_then(|&i| region_of_glyph.get(&input.glyphs[i].id()))
.copied();
// A slot no region claims (out-of-pipeline input) is left out: its
// glyphs stay in the spaced frame, on no page.
if let Some(r) = region {
region_slots[r].push(info);
}
}
for infos in &mut region_slots {
infos.sort_by(|a, b| a.x.total_cmp(&b.x).then_with(|| a.id.cmp(&b.id)));
}
// Each slot's spaced reference x, for the slot-relative justification delta.
let slot_source_x: BTreeMap<SpringSlotId, f32> = region_slots
.iter()
.flatten()
.map(|info| (info.id, info.x))
.collect();
// ---- Break requirements ----------------------------------------------
let mut reqs: BTreeMap<SpringSlotId, Vec<BreakReq>> = BTreeMap::new();
for constraint in &input.constraints {
let (slot, page, kind) = match constraint {
LayoutConstraint::SystemBreakAt { slot, kind } => (*slot, false, *kind),
LayoutConstraint::PageBreakAt { slot, kind } => (*slot, true, *kind),
_ => continue,
};
reqs.entry(slot).or_default().push(BreakReq {
page,
hard: kind == BreakKind::Hard,
});
}
let mut origins: BTreeMap<(u128, bool), EngravingOverrideId> = BTreeMap::new();
for origin in &input.break_origins {
origins
.entry((origin.slot.0, origin.class == BreakClass::Page))
.or_insert(origin.override_id);
}
// ---- System breaking (greedy first-fit per region) --------------------
let width_limit = {
let w = geometry.content_width();
if w > 0.0 {
w
} else {
f32::INFINITY
}
};
let mut systems: Vec<SystemPlan> = Vec::new();
let mut skipped: Vec<EngravingDecision> = Vec::new();
for (r, infos) in region_slots.iter().enumerate() {
let region_source = input.regions[r].provenance.source;
walk_region(
r,
infos,
&reqs,
&origins,
region_source,
width_limit,
&mut systems,
&mut skipped,
);
}
// (The old greedy pass needed a second widow-rebalance phase here; the
// optimal break search evens the final system directly — see
// `optimal_breaks`.)
// ---- Stroke fates ------------------------------------------------------
// Which system each slot landed in, and each region's slot span / per-system
// clip intervals (the interior cut points for system-spanning strokes).
let mut system_of_slot: BTreeMap<SpringSlotId, usize> = BTreeMap::new();
for (s, plan) in systems.iter().enumerate() {
for &i in &plan.slots {
system_of_slot.insert(region_slots[plan.region][i].id, s);
}
}
let region_spans: Vec<Option<(f32, f32)>> = region_slots
.iter()
.map(|infos| {
infos
.iter()
.map(|s| (s.lo, s.hi))
.reduce(|a, b| (a.0.min(b.0), a.1.max(b.1)))
})
.collect();
let mut region_systems: Vec<Vec<usize>> = vec![Vec::new(); input.regions.len()];
for (s, plan) in systems.iter().enumerate() {
region_systems[plan.region].push(s);
}
let mut clips: Vec<(f32, f32)> = vec![(f32::NEG_INFINITY, f32::INFINITY); systems.len()];
for (r, sys_of_region) in region_systems.iter().enumerate() {
let last = sys_of_region.len().saturating_sub(1);
for (local, &s) in sys_of_region.iter().enumerate() {
let lo = if local == 0 {
f32::NEG_INFINITY
} else {
systems[s]
.slots
.iter()
.map(|&i| region_slots[r][i].lo)
.fold(f32::INFINITY, f32::min)
};
let hi = if local == last {
f32::INFINITY
} else {
systems[s]
.slots
.iter()
.map(|&i| region_slots[r][i].hi)
.fold(f32::NEG_INFINITY, f32::max)
};
clips[s] = (lo, hi);
}
}
let fates: Vec<StrokeFate> = input
.strokes
.iter()
.zip(spaced_strokes)
.map(|(stroke, spaced)| {
stroke_fate(
stroke,
spaced,
input,
&system_of_slot,
&region_spans,
&region_systems,
&clips,
)
})
.collect();
// A curve rides one system whole when it fits within one, or splits into
// per-system sub-cubics (de Casteljau) when it spans a break — the same
// nearest-region / clip-overlap logic strokes use.
let curve_fates: Vec<CurveFate> = spaced_curves
.iter()
.map(|curve| curve_fate(curve, &region_spans, &region_systems, &clips))
.collect();
// ---- Inter-staff vertical solve + system extents -----------------------
// Attribute every primitive to its owning staff so the gaps BETWEEN a
// system's staves can be renegotiated: the constrained stage stacks staves
// at a fixed pitch, so tightly ledgered or slurred adjacent staves collide.
//
// Attribution is a BAND LOOKUP, not a geometric guess. Every primitive —
// glyph, stroke, curve — declares the vertical band it belongs to, and the
// projection that emitted it knew the answer: a stem's band is its note's, a
// slur's is its notes'. Content owned by no staff (a page-margin annotation,
// a repeat structure spanning several staves) names a non-`Staff` band and
// is attributed to `None` — it takes no staff shift.
//
// Inferring the owner from proximity instead is a trap this code fell into
// twice. A stem sits under its notehead but shares x columns with the staff
// above; a slur's endpoints are lifted clear of its own staff by design, so
// the nearest notehead is routinely on the ADJACENT staff. Neither is
// recoverable from geometry, and both silently tore primitives off their
// notes. See DECISIONS.md, "Why attribution is declared, not inferred".
let band_to_staff: BTreeMap<VerticalBandId, StaffId> = input
.vertical_bands
.iter()
.filter_map(|b| match b.kind {
VerticalBandKind::Staff(s) => Some((b.id, s)),
_ => None,
})
.collect();
let staff_of = |band: VerticalBandId| band_to_staff.get(&band).copied();
let glyph_staff_of: Vec<Option<StaffId>> = input
.glyphs
.iter()
.map(|g| staff_of(g.vertical_band))
.collect();
let stroke_staff_of: Vec<Option<StaffId>> = input
.strokes
.iter()
.map(|s| staff_of(s.vertical_band))
.collect();
let curve_staff_of: Vec<Option<StaffId>> = input
.curves
.iter()
.map(|c| staff_of(c.vertical_band))
.collect();
// Pass A: system extents (unshifted), and per (system, staff) content
// y-extents plus the staff-line reference y (for ordering).
let mut extents: Vec<Extent> = vec![Extent::empty(); systems.len()];
let mut staff_ext: BTreeMap<(usize, StaffId), (f32, f32)> = BTreeMap::new();
let mut staff_ref: BTreeMap<(usize, StaffId), f32> = BTreeMap::new();
let into_staff = |m: &mut BTreeMap<(usize, StaffId), (f32, f32)>,
s: usize,
staff: Option<StaffId>,
lo_y: f32,
hi_y: f32| {
if let Some(st) = staff {
m.entry((s, st))
.and_modify(|e| {
e.0 = e.0.min(lo_y);
e.1 = e.1.max(hi_y);
})
.or_insert((lo_y, hi_y));
}
};
for (s, plan) in systems.iter().enumerate() {
for &i in &plan.slots {
for &g in &region_slots[plan.region][i].members {
let glyph = &spaced_glyphs[g];
let (x, y) = (glyph.position.x.0, glyph.position.y.0);
let (lo_y, hi_y) = (
y + glyph.bounding_box.bottom.0,
y + glyph.bounding_box.top.0,
);
extents[s].add_x(
x + glyph.bounding_box.left.0,
x + glyph.bounding_box.right.0,
);
match glyph_staff_of[g] {
Some(_) => into_staff(&mut staff_ext, s, glyph_staff_of[g], lo_y, hi_y),
None => extents[s].add_y(lo_y, hi_y),
}
}
}
}
for (si, (fate, spaced)) in fates.iter().zip(spaced_strokes).enumerate() {
let half = (spaced.thickness.0 * 0.5).max(0.0);
let staff = stroke_staff_of[si];
let is_staff_line = matches!(spaced.provenance.source, TypedObjectId::Staff(_));
let segs: Vec<(usize, Point, Point)> = match fate {
StrokeFate::Rigid(Some(s)) => vec![(*s, spaced.from, spaced.to)],
StrokeFate::Rigid(None) => vec![],
StrokeFate::Split(segments) => segments.clone(),
};
for (s, from, to) in segs {
let (lo_y, hi_y) = (from.y.0.min(to.y.0) - half, from.y.0.max(to.y.0) + half);
extents[s].add_x(from.x.0 - half, to.x.0 + half);
match staff {
Some(_) => into_staff(&mut staff_ext, s, staff, lo_y, hi_y),
None => extents[s].add_y(lo_y, hi_y),
}
if is_staff_line {
if let Some(st) = staff {
staff_ref
.entry((s, st))
.and_modify(|r| *r = r.max(hi_y))
.or_insert(hi_y);
}
}
}
}
for (ci, (fate, curve)) in curve_fates.iter().zip(spaced_curves).enumerate() {
let half = (curve.thickness.0 * 0.5).max(0.0);
let staff = curve_staff_of[ci];
let segs: Vec<(usize, [Point; 4])> = match fate {
CurveFate::Rigid(Some(s)) => vec![(*s, curve.control_points())],
CurveFate::Rigid(None) => vec![],
CurveFate::Split(segments) => segments.clone(),
};
for (s, cp) in segs {
for p in cp {
extents[s].add_x(p.x.0 - half, p.x.0 + half);
match staff {
Some(_) => into_staff(&mut staff_ext, s, staff, p.y.0 - half, p.y.0 + half),
None => extents[s].add_y(p.y.0 - half, p.y.0 + half),
}
}
}
}
// Solve each system's inter-staff gaps: order the staves top-to-bottom by
// their reference y (staff line, else content mid), keep that order fixed,
// and shift each staff so its INK CLEARANCE to the one above realizes the
// gap band's declared height. `staff_shift[(system, staff)]` is the downward
// shift (subtracted from y); the top staff's is 0.
//
// The renegotiation is TWO-SIDED. A pair whose content collides is pushed
// apart; a pair the constrained stage left slack is pulled together. The
// fixed `SYSTEM_STAFF_PITCH` that stage stacks by is therefore an initial
// arrangement, not a floor: the band model is the height model, and the solve
// realizes it. (Expanding only was the earlier behaviour, and it was
// measurably wrong — `vertical_density_penalty` scored honest sprawl on every
// relaxed multi-staff system, because a gap wider than preferred is sprawl
// exactly as a narrower one is crowding.)
//
// The target is the gap band's `preferred_height`, held at or above its
// `min_height` — the hardest squeeze permitted. Validation already brackets
// preferred by min and max, so the clamp is belt-and-braces rather than a
// second policy. The band is the one the REGION DECLARED, not the
// constructor's default, so the solve and `vertical_density_penalty` — which
// scores the realized clearance against that same band — read one number.
//
// Gap `g` separates the region's staves `g-1` and `g` (see `to_constrained`).
// Every staff of a region carries content in every system of that region —
// its staff lines are per-staff strokes, split into each system — so the
// staves present here are the region's full staff order and the window index
// is the gap index. A band that somehow does not exist falls back to the
// constructor's default rather than silently skipping the pair.
let fallback = VerticalBand::inter_staff_gap(VerticalBandId(0));
let mut staff_shift: BTreeMap<(usize, StaffId), f32> = BTreeMap::new();
for (s, plan) in systems.iter().enumerate() {
let region_layout_id = input.regions[plan.region].provenance.stable_id;
let target_gap = |gap_index: usize| -> f32 {
let id = inter_staff_gap_id(region_layout_id, gap_index);
let band = input
.vertical_bands
.iter()
.find(|band| band.id == id)
.unwrap_or(&fallback);
band.preferred_height.0.max(band.min_height.0)
};
let mut staves: Vec<(StaffId, (f32, f32))> = staff_ext
.iter()
.filter(|((sys, _), _)| *sys == s)
.map(|((_, st), ext)| (*st, *ext))
.collect();
// Top first: larger reference y is higher on the page.
staves.sort_by(|a, b| {
let key = |st: StaffId, ext: (f32, f32)| {
staff_ref
.get(&(s, st))
.copied()
.unwrap_or((ext.0 + ext.1) * 0.5)
};
key(b.0, b.1).total_cmp(&key(a.0, a.1)).then(a.0.cmp(&b.0))
});
let mut shift = 0.0_f32;
for (g, w) in staves.windows(2).enumerate() {
let (upper, (upper_lo, _)) = w[0];
let (lower, (_, lower_hi)) = w[1];
staff_shift.insert((s, upper), shift);
// Both staves move, so solve the recurrence rather than guessing it.
// With `shift` the upper staff's cumulative shift, the realized
// clearance is `(upper_lo - shift_upper) - (lower_hi - shift_lower)`,
// and setting that equal to the target gives
//
// shift_lower = shift_upper + target - (upper_lo - lower_hi)
//
// — the UNSHIFTED gap. Subtracting `shift_upper` from the gap here
// and adding it back through `shift +=` would count it twice, which
// over-separated every pair below the first by exactly the shift
// above it (invisible on two staves, where that shift is 0). The
// correction is signed: positive opens a crowded pair, negative
// closes a slack one, and it accumulates down the stack.
let gap = upper_lo - lower_hi;
shift += target_gap(g + 1) - gap;
staff_shift.insert((s, lower), shift);
}
if staves.len() == 1 {
staff_shift.insert((s, staves[0].0), 0.0);
}
}
// Fold each staff's SHIFTED content y-extent into its system extent, so the
// stacking/justification below sees the taller, separated system.
for ((s, st), (lo, hi)) in &staff_ext {
let sh = staff_shift.get(&(*s, *st)).copied().unwrap_or(0.0);
extents[*s].add_y(lo - sh, hi - sh);
}
let extents: Vec<Extent> = extents.into_iter().map(Extent::normalized).collect();
// ---- Vertical stacking and page assignment ----------------------------
// The inter-system spacing comes from the vertical-band model's own
// constructor, so the casting-off gap and the band spring cannot drift.
let gap = VerticalBand::inter_system_gap(VerticalBandId(0))
.preferred_height
.0;
let content_height = geometry.content_height();
let bounded = content_height > 0.0;
let mut placements: Vec<Placement> = Vec::with_capacity(systems.len());
let mut page_systems: Vec<Vec<usize>> = Vec::new();
let mut cursor = 0.0_f32;
let mut page_floor = 0.0_f32;
for (s, plan) in systems.iter().enumerate() {
let ext = &extents[s];
let height = ext.max_y - ext.min_y;
// Every opened page immediately receives a system, so an overflow test
// against a non-empty page list never opens an empty page — a system
// taller than a whole page stays (overfull) on the page it opens.
let overflow = bounded && !page_systems.is_empty() && cursor - height < page_floor;
if page_systems.is_empty() || plan.page_forced || overflow {
let p = page_systems.len();
cursor = page_top_content(p, geometry);
page_floor = cursor - content_height.max(0.0);
page_systems.push(Vec::new());
}
let base_dx = geometry.margins.left.0 - ext.min_x;
let dy = cursor - ext.max_y;
placements.push(justify_system(
plan,
ext,
base_dx,
dy,
&region_slots,
&region_systems,
width_limit,
));
page_systems
.last_mut()
.expect("a page was opened above")
.push(s);
cursor -= height + gap;
}
// ---- Vertical justification -------------------------------------------
// Spread the systems of every NON-FINAL page so the last system's bottom
// reaches the content bottom, filling the page height — the vertical analog
// of per-system horizontal justification, distributing the slack evenly
// across the inter-system gaps. The last page stays ragged-bottom
// (top-aligned), as engraving convention wants; a page with a single system
// has no gap to grow, and an already-full (or overfull) page is left alone.
if bounded {
let last_page = page_systems.len().saturating_sub(1);
for (p, page) in page_systems.iter().enumerate() {
if p == last_page || page.len() < 2 {
continue;
}
let content_bottom = page_top_content(p, geometry) - content_height;
let last = *page.last().expect("a page carries at least one system");
let natural_bottom = placements[last].dy + extents[last].min_y;
let slack = natural_bottom - content_bottom;
if slack <= 0.0 {
continue;
}
// System i (0-based on the page) sinks by i/(n-1) of the slack, so
// the first stays at the content top and the last lands on the
// content bottom (y-down is decreasing y in this world frame).
let step = slack / (page.len() - 1) as f32;
for (i, &s) in page.iter().enumerate() {
placements[s].dy -= i as f32 * step;
}
}
}
// ---- Break structure and decisions -------------------------------------
let mut system_start_slots = BTreeSet::new();
for plan in &systems {
if let Some(&i) = plan.slots.first() {
system_start_slots.insert(region_slots[plan.region][i].id);
}
}
let mut page_start_slots = BTreeSet::new();
let mut decisions = Vec::new();
for (p, on_page) in page_systems.iter().enumerate() {
for (j, &s) in on_page.iter().enumerate() {
let plan = &systems[s];
let starts_page = j == 0;
if starts_page {
if let Some(&i) = plan.slots.first() {
page_start_slots.insert(region_slots[plan.region][i].id);
}
}
let region_source = input.regions[plan.region].provenance.source;
if let Some(boundary) = plan.boundary {
// A chosen intra-region break: a page decision when the system
// actually opens a page, a system decision otherwise.
decisions.push(EngravingDecision::with_source(
break_target(region_source, boundary.slot),
if starts_page {
EngravingDecisionKind::PageBreak
} else {
EngravingDecisionKind::SystemBreak
},
boundary.source,
));
} else if starts_page && p > 0 {
// A later page opening at a region's first system: the page
// start is itself an engraved decision (forced or overflow).
if let Some(&i) = plan.slots.first() {
decisions.push(EngravingDecision::with_source(
break_target(region_source, region_slots[plan.region][i].id),
EngravingDecisionKind::PageBreak,
plan.page_source,
));
}
}
}
}
decisions.extend(skipped);
// ---- Bake the world frame ----------------------------------------------
// A primitive's additional downward shift from the inter-staff solve.
let staff_dy = |s: usize, staff: Option<StaffId>| -> f32 {
staff
.and_then(|st| staff_shift.get(&(s, st)))
.copied()
.unwrap_or(0.0)
};
// Computed once, alongside the positioning it also drives (W1 pin 7): the
// quality-metric census consumes this published vector rather than
// re-deriving the same attribution from `system_of_slot` itself.
let (glyphs, glyph_system): (Vec<ResolvedGlyph>, Vec<Option<usize>>) = spaced_glyphs
.iter()
.zip(&input.glyphs)
.enumerate()
.map(|(gi, (spaced, glyph))| {
let system = system_of_slot.get(&glyph.horizontal_slot).copied();
let (dx, dy) = match system {
Some(s) => {
// Slot-relative: every member of a slot translates by the
// map at the slot's source, so intra-slot offsets survive.
let sx = slot_source_x
.get(&glyph.horizontal_slot)
.copied()
.unwrap_or(spaced.position.x.0);
(
placements[s].slot_dx(sx),
placements[s].dy - staff_dy(s, glyph_staff_of[gi]),
)
}
None => (0.0, 0.0),
};
let resolved = ResolvedGlyph {
position: Point::new(spaced.position.x.0 + dx, spaced.position.y.0 + dy),
..spaced.clone()
};
(resolved, system)
})
.unzip();
// Per-system staff-line marks, for the resolved staff records below.
let mut staff_marks: BTreeMap<(usize, StaffId), StaffAgg> = BTreeMap::new();
let mut strokes: Vec<Stroke> = Vec::with_capacity(spaced_strokes.len());
let mut continuations: Vec<Stroke> = Vec::new();
// The system each baked stroke landed in, parallel to `strokes` (a quality
// metric measures a system's realized per-staff content extents, and a
// stroke carries no spring slot to look one up with).
let mut stroke_system: Vec<Option<usize>> = Vec::with_capacity(spaced_strokes.len());
let mut continuation_system: Vec<Option<usize>> = Vec::new();
for (si, ((source, spaced), fate)) in input
.strokes
.iter()
.zip(spaced_strokes)
.zip(&fates)
.enumerate()
{
match fate {
StrokeFate::Rigid(sys) => {
let stroke = match sys {
Some(s) => place_stroke(
source,
spaced,
placements[*s].sunk(staff_dy(*s, stroke_staff_of[si])),
&slot_source_x,
&input.glyphs,
),
None => spaced.clone(),
};
if let (Some(s), TypedObjectId::Staff(staff)) = (sys, spaced.provenance.source) {
mark_staff(&mut staff_marks, *s, staff, &stroke);
}
strokes.push(stroke);
stroke_system.push(*sys);
}
StrokeFate::Split(segments) => {
for (k, (s, from, to)) in segments.iter().enumerate() {
// A split stroke spans systems — a staff line or volta
// bracket — so each segment stretches with its system.
let p = placements[*s].sunk(staff_dy(*s, stroke_staff_of[si]));
let provenance = if k == 0 {
// The first segment carries the original stroke's exact
// provenance: the object survives, re-shaped.
spaced.provenance.clone()
} else {
Provenance::synthesized(
spaced.provenance.source,
SynthesisKind::Registered(SYSTEM_CONTINUATION_SYNTHESIS),
continuation_instance_key(spaced.provenance.stable_id, k as u32),
spaced.provenance.dependencies.clone(),
)
};
let stroke = Stroke {
provenance,
from: Point::new(p.x(from.x.0), from.y.0 + p.dy),
to: Point::new(p.x(to.x.0), to.y.0 + p.dy),
thickness: spaced.thickness,
layer: spaced.layer,
style: spaced.style,
vertical_band: spaced.vertical_band,
};
if let TypedObjectId::Staff(staff) = spaced.provenance.source {
mark_staff(&mut staff_marks, *s, staff, &stroke);
}
if k == 0 {
strokes.push(stroke);
stroke_system.push(Some(*s));
} else {
continuations.push(stroke);
continuation_system.push(Some(*s));
}
}
}
}
}
strokes.extend(continuations);
stroke_system.extend(continuation_system);
// Curves: a curve that fits in one system is translated whole by that
// system's placement (or left in the spaced frame if no region claimed it).
// A curve that spans a system break is split into per-system sub-cubics: the
// first segment carries the slur's exact provenance (the object survives,
// re-shaped — the round-trip source surjection recovers it), later segments
// are synthesized continuations under `SYSTEM_CONTINUATION_SYNTHESIS`, as a
// split stroke's are.
let mut curves: Vec<Curve> = Vec::with_capacity(spaced_curves.len());
let mut curve_continuations: Vec<Curve> = Vec::new();
let mut curve_system: Vec<Option<usize>> = Vec::with_capacity(spaced_curves.len());
let mut curve_continuation_system: Vec<Option<usize>> = Vec::new();
for (ci, (curve, fate)) in spaced_curves.iter().zip(&curve_fates).enumerate() {
let curve_staff = curve_staff_of[ci];
// A slur has no intra-slot structure, so its control points map straight
// through the affine: the endpoints follow their anchor notes (which sit
// at slot sources) and the arc stretches horizontally with the span.
let shift =
|cp: [Point; 4], p: Placement| cp.map(|pt| Point::new(p.x(pt.x.0), pt.y.0 + p.dy));
match fate {
CurveFate::Rigid(system) => {
let p = system
.map(|s| placements[s].sunk(staff_dy(s, curve_staff)))
.unwrap_or(Placement::rigid(0.0, 0.0));
let [p0, p1, p2, p3] = shift(curve.control_points(), p);
curves.push(Curve {
p0,
p1,
p2,
p3,
..curve.clone()
});
curve_system.push(*system);
}
CurveFate::Split(segments) => {
for (k, (s, cp)) in segments.iter().enumerate() {
let [p0, p1, p2, p3] =
shift(*cp, placements[*s].sunk(staff_dy(*s, curve_staff)));
let provenance = if k == 0 {
curve.provenance.clone()
} else {
Provenance::synthesized(
curve.provenance.source,
SynthesisKind::Registered(SYSTEM_CONTINUATION_SYNTHESIS),
continuation_instance_key(curve.provenance.stable_id, k as u32),
curve.provenance.dependencies.clone(),
)
};
let segment = Curve {
provenance,
p0,
p1,
p2,
p3,
thickness: curve.thickness,
layer: curve.layer,
style: curve.style,
vertical_band: curve.vertical_band,
line: curve.line,
};
if k == 0 {
curves.push(segment);
curve_system.push(Some(*s));
} else {
curve_continuations.push(segment);
curve_continuation_system.push(Some(*s));
}
}
}
}
}
curves.extend(curve_continuations);
curve_system.extend(curve_continuation_system);
// ---- Per-system primitive ownership (W1) -------------------------------
// The partition already exists in `glyph_system`/`stroke_system`/
// `curve_system` above; this just stops discarding it. For each flat
// array, every index lands in exactly one system's list or in `unowned`
// (pin 4: a total, disjoint partition, tested in this module below).
let mut owned: Vec<PrimitiveIndices> = (0..systems.len())
.map(|_| PrimitiveIndices::default())
.collect();
let mut unowned = PrimitiveIndices::default();
for (i, system) in glyph_system.iter().enumerate() {
match system {
Some(s) => owned[*s].glyphs.push(i as u32),
None => unowned.glyphs.push(i as u32),
}
}
for (i, system) in stroke_system.iter().enumerate() {
match system {
Some(s) => owned[*s].strokes.push(i as u32),
None => unowned.strokes.push(i as u32),
}
}
for (i, system) in curve_system.iter().enumerate() {
match system {
Some(s) => owned[*s].curves.push(i as u32),
None => unowned.curves.push(i as u32),
}
}
// ---- The resolved page tree ---------------------------------------------
let resolved_systems: Vec<ResolvedSystem> = systems
.iter()
.enumerate()
.map(|(s, plan)| {
build_system(
s,
plan,
input,
&region_slots,
&extents,
&placements,
&staff_marks,
owned[s].clone(),
)
})
.collect();
let mut resolved_systems: Vec<Option<ResolvedSystem>> =
resolved_systems.into_iter().map(Some).collect();
let pages: Vec<ResolvedPage> = page_systems
.iter()
.enumerate()
.map(|(p, on_page)| {
let first_region = systems[on_page[0]].region;
let region_provenance = &input.regions[first_region].provenance;
let provenance = if p == 0 {
// Page 1 carries the first region's own provenance, as the
// degenerate single-page output always did.
input.regions[0].provenance.clone()
} else {
Provenance::synthesized(
region_provenance.source,
SynthesisKind::EngravedBreak,
SynthesisInstanceKey((KEY_NS_PAGE << 64) | (p as u128 + 1)),
region_provenance.dependencies.clone(),
)
};
ResolvedPage {
provenance,
number: p as u32 + 1,
size: geometry.size,
margins: geometry.margins,
systems: on_page
.iter()
.map(|&s| resolved_systems[s].take().expect("each system on one page"))
.collect(),
// Nothing in the Minimal pipeline is a page-level free object
// (region content is all system-bound); left empty rather than
// fabricated.
free_objects: Vec::new(),
}
})
.collect();
CastLayout {
glyphs,
strokes,
curves,
pages,
decisions,
system_start_slots,
page_start_slots,
glyph_system,
stroke_system,
curve_system,
region_of_system: systems.iter().map(|plan| plan.region).collect(),
unowned,
}
}
/// The world-frame y of page `p`'s content top: pages stack downward from the
/// origin, each a full page height plus [`INTER_PAGE_GAP`] below the previous.
fn page_top_content(p: usize, geometry: &PageGeometry) -> f32 {
-(p as f32) * (geometry.size.height.0 + INTER_PAGE_GAP) - geometry.margins.top.0
}
/// Optimal automatic system breaks for one region: a badness-minimizing
/// (KnuthPlass-style) partition of the region's measures into systems,
/// replacing greedy first-fit. Returns the slot ids at which an AUTOMATIC break
/// opens a system — the break REQUIREMENTS (hard / soft / page, which bound the
/// DP's segments) are honoured by [`walk_region`] itself, and never appear here.
///
/// **Objective.** Minimize the sum over ALL systems of the squared normalized
/// underfill `((width_limit w) / width_limit)²`. Squaring evens the systems
/// (a lopsided split costs more than a balanced one), and including the *final*
/// system in the sum is what subsumes the old tail-only widow rebalance — the
/// optimizer will not leave a narrow final stub if a more even partition is
/// cheaper. It is the additive, DP-tractable analog of the catalog's
/// break/imbalance distribution cost (`distribution_cost`, now retired): both
/// reward filled, even systems. A system may not exceed the content width unless
/// it is a **single unsplittable measure** (an overfull lone measure, which the
/// greedy pass also emitted). `Minimal` still makes no optimality *claim*; this
/// is a deterministic global heuristic, an honest improvement on first-fit.
///
/// **Determinism.** A pure function of the slot extents and requirements; the
/// DP minimizes the lexicographic `(cost, system_count)` (fewer systems breaks
/// ties, so ties favour fewer pages), and among equal `(cost, count)` the
/// earliest-considered predecessor (the largest final system) wins.
fn optimal_breaks(
slots: &[SlotInfo],
reqs: &BTreeMap<SpringSlotId, Vec<BreakReq>>,
width_limit: f32,
) -> BTreeSet<SpringSlotId> {
let mut automatic = BTreeSet::new();
if !width_limit.is_finite() || width_limit <= 0.0 || slots.is_empty() {
return automatic; // unbounded width: nothing wraps
}
let breakable = |slot: &SlotInfo| slot.barline && !slot.final_barline;
// Measure-boundary positions in slot-index space: region start, each
// breakable barline, region end. `forced[k]` marks a boundary carrying a
// break requirement (the DP may not span it). The region end is a boundary.
let mut pts: Vec<usize> = vec![0];
let mut forced: Vec<bool> = vec![false];
for (i, slot) in slots.iter().enumerate() {
if i > 0 && breakable(slot) {
pts.push(i);
forced.push(reqs.contains_key(&slot.id));
}
}
pts.push(slots.len());
forced.push(true);
let n = pts.len(); // n - 1 measures between the n boundaries
// A system spanning boundaries [a, b): its ink extent over slots
// `[pts[a] .. pts[b])`.
let width = |a: usize, b: usize| -> f32 {
let range = &slots[pts[a]..pts[b]];
let lo = range.iter().map(|s| s.lo).fold(f32::INFINITY, f32::min);
let hi = range.iter().map(|s| s.hi).fold(f32::NEG_INFINITY, f32::max);
(hi - lo).max(0.0)
};
// dp[b] = the min `(cost, system_count)` to partition measures [0, b).
let mut dp: Vec<(f64, usize)> = vec![(f64::INFINITY, usize::MAX); n];
let mut from: Vec<usize> = vec![0; n];
dp[0] = (0.0, 0);
for b in 1..n {
for a in 0..b {
// A system may not skip a forced break at an interior boundary.
if (a + 1..b).any(|k| forced[k]) {
continue;
}
let (prev_cost, prev_count) = dp[a];
if !prev_cost.is_finite() {
continue;
}
let w = width(a, b);
let bad = if w <= width_limit {
let u = f64::from((width_limit - w) / width_limit);
u * u
} else if b - a == 1 {
0.0 // a lone measure wider than the page: unavoidable, not charged
} else {
continue; // overfull and splittable: not a valid system
};
let cand = (prev_cost + bad, prev_count + 1);
if cand < dp[b] {
dp[b] = cand;
from[b] = a;
}
}
}
// Reconstruct the partition; its non-forced boundaries are the automatic
// breaks `walk_region` adds to its requirement-driven ones.
if dp[n - 1].0.is_finite() {
let mut b = n - 1;
while b > 0 {
let a = from[b];
if a > 0 && !forced[a] {
automatic.insert(slots[pts[a]].id);
}
b = a;
}
}
automatic
}
/// Walks one region's slots, opening a system at each break requirement and at
/// each optimal automatic break (`optimal_breaks`).
#[allow(clippy::too_many_arguments)]
fn walk_region(
region: usize,
slots: &[SlotInfo],
reqs: &BTreeMap<SpringSlotId, Vec<BreakReq>>,
origins: &BTreeMap<(u128, bool), EngravingOverrideId>,
region_source: TypedObjectId,
width_limit: f32,
systems: &mut Vec<SystemPlan>,
skipped: &mut Vec<EngravingDecision>,
) {
// The optimal automatic breaks (a global badness-minimizing partition,
// bounded by the break requirements); the walk opens a system at each.
let automatic = optimal_breaks(slots, reqs, width_limit);
// Overflow safety net. A lead-only (note-less) run can defer a *planned*
// break past its barline — the DP treats a requirement, or its own chosen
// automatic break, as a real system start, but the walk skips it when the
// closing system carries no musical content (the soft-break exception, and
// the `has_note` guard on the automatic break below). The DP optimizes each
// requirement-bounded segment independently and cannot foresee that skip, so
// without a net the following DP-filled system would absorb the furniture
// measures and overflow. `chunk_hi[i]` — the rightmost content edge of the
// measure beginning at slot `i` — lets the walk still break before a measure
// that would overflow the content width, exactly as first-fit did. In the
// common (content-full) case the DP's break fires first, so the net never
// triggers and the geometry is the optimizer's.
let breakable = |slot: &SlotInfo| slot.barline && !slot.final_barline;
let mut chunk_hi = vec![f32::NEG_INFINITY; slots.len()];
for i in (0..slots.len()).rev() {
let next = if i + 1 < slots.len() && !breakable(&slots[i + 1]) {
chunk_hi[i + 1]
} else {
f32::NEG_INFINITY
};
chunk_hi[i] = slots[i].hi.max(next);
}
let mut local = 0usize;
let mut current: Vec<usize> = Vec::new();
let mut has_note = false;
let mut current_lo = f32::INFINITY;
let mut open_boundary: Option<Boundary> = None;
let mut open_page_forced = false;
let mut open_page_source = DecisionSource::Automatic;
for (i, slot) in slots.iter().enumerate() {
let slot_reqs = reqs.get(&slot.id).map(Vec::as_slice).unwrap_or(&[]);
if current.is_empty() {
// The region's first slot is already at a system boundary, so a
// system break here is trivially honoured; a page break still
// forces this (first) system onto a fresh page.
for req in slot_reqs {
if req.page {
open_page_forced = true;
if open_page_source == DecisionSource::Automatic {
open_page_source = origin_source(origins, slot.id, true);
}
}
}
current.push(i);
has_note = slot.note;
current_lo = slot.lo;
continue;
}
let mut break_here = false;
let mut page_here = false;
let mut source = DecisionSource::Automatic;
for req in slot_reqs {
if !req.hard && !has_note {
// The documented exceptional path: honouring this *soft* break
// would close a system with no musical content (e.g. a bare
// clef/barline line). It is skipped, and the unhonoured
// override is recorded as an IR-stage-overridden decision
// (never silently dropped).
skipped.push(EngravingDecision::with_source(
break_target(region_source, slot.id),
if req.page {
EngravingDecisionKind::PageBreak
} else {
EngravingDecisionKind::SystemBreak
},
DecisionSource::IrOverride,
));
continue;
}
break_here = true;
page_here |= req.page;
if !matches!(source, DecisionSource::UserOverride(_)) {
source = origin_source(origins, slot.id, req.page);
}
}
// Optimal casting-off: open a system at a chosen automatic break — or,
// as the overflow net, before a measure that would overflow the content
// width — as long as the closing system carries musical content (a
// lead-only system is never torn off, matching the requirement rule).
if !break_here
&& has_note
&& (automatic.contains(&slot.id)
|| (breakable(slot) && chunk_hi[i] - current_lo > width_limit))
{
break_here = true;
}
if break_here {
systems.push(SystemPlan {
region,
local,
slots: std::mem::take(&mut current),
boundary: open_boundary.take(),
page_forced: open_page_forced,
page_source: open_page_source,
});
local += 1;
open_boundary = Some(Boundary {
slot: slot.id,
source,
});
open_page_forced = page_here;
open_page_source = if page_here {
source
} else {
DecisionSource::Automatic
};
current.push(i);
has_note = slot.note;
current_lo = slot.lo;
} else {
current.push(i);
has_note |= slot.note;
current_lo = current_lo.min(slot.lo);
}
}
// The region's last system — or, for a region with no slots at all, its
// single (empty) system, preserving one-system-per-region as the minimum.
systems.push(SystemPlan {
region,
local,
slots: current,
boundary: open_boundary,
page_forced: open_page_forced,
page_source: open_page_source,
});
}
/// Decides how a stroke rides the cast systems (see [`StrokeFate`]).
fn stroke_fate(
stroke: &Stroke,
spaced: &Stroke,
input: &ConstrainedLayoutIR,
system_of_slot: &BTreeMap<SpringSlotId, usize>,
region_spans: &[Option<(f32, f32)>],
region_systems: &[Vec<usize>],
clips: &[(f32, f32)],
) -> StrokeFate {
// A rigid-width stroke (a ledger line) rides its owning glyph's system, so
// it translates by exactly the same delta as its notehead.
if is_rigid_width_stroke(stroke) {
if let Some(glyph) = owning_glyph(stroke, &input.glyphs) {
return StrokeFate::Rigid(system_of_slot.get(&glyph.horizontal_slot).copied());
}
}
let lo = spaced.from.x.0.min(spaced.to.x.0);
let hi = spaced.from.x.0.max(spaced.to.x.0);
// The owning region: the one whose slot span is nearest (ties to the first).
let mut best: Option<(usize, f32)> = None;
for (r, span) in region_spans.iter().enumerate() {
let Some((rlo, rhi)) = span else { continue };
let distance = if hi < *rlo {
rlo - hi
} else if lo > *rhi {
lo - rhi
} else {
0.0
};
if best.is_none_or(|(_, d)| distance < d) {
best = Some((r, distance));
}
}
let Some((region, _)) = best else {
return StrokeFate::Rigid(None);
};
// The systems of that region the stroke's span overlaps.
let overlapped: Vec<usize> = region_systems[region]
.iter()
.copied()
.filter(|&s| lo <= clips[s].1 && hi >= clips[s].0)
.collect();
match overlapped.len() {
0 => {
// In the sliver between two systems' content: nearest system.
let nearest = region_systems[region]
.iter()
.copied()
.min_by(|&a, &b| {
let da = interval_distance(lo, hi, clips[a]);
let db = interval_distance(lo, hi, clips[b]);
da.total_cmp(&db).then(a.cmp(&b))
})
.expect("every region has at least one system");
StrokeFate::Rigid(Some(nearest))
}
1 => StrokeFate::Rigid(Some(overlapped[0])),
_ => {
// A system-spanning stroke (a staff line): one segment per system,
// cut at the systems' content edges, y interpolated along the
// stroke so a (hypothetical) sloped spanner splits consistently.
let (x0, y0) = (spaced.from.x.0, spaced.from.y.0);
let (x1, y1) = (spaced.to.x.0, spaced.to.y.0);
let point_at = |x: f32| -> Point {
if (x1 - x0).abs() < f32::EPSILON {
Point::new(x, y0)
} else {
let t = (x - x0) / (x1 - x0);
Point::new(x, y0 + t * (y1 - y0))
}
};
let segments = overlapped
.into_iter()
.map(|s| {
let a = lo.max(clips[s].0);
let b = hi.min(clips[s].1);
(s, point_at(a), point_at(b))
})
.collect();
StrokeFate::Split(segments)
}
}
}
/// A curve's casting fate. A curve overlapping ONE system rides it whole
/// (`Rigid(Some(s))`) — the nearest region's system whose clip interval
/// contains the curve's **start** control point, else that region's nearest
/// system; a curve no region claims is `Rigid(None)` (left in the spaced frame,
/// on no page). A curve spanning MULTIPLE systems is `Split` into per-system
/// sub-curves by de Casteljau subdivision at the parameters where its
/// x-monotonic path crosses each system's content-clip edges (a non-monotonic
/// curve — not produced by the engraver — cannot be honestly split and rides
/// its start system whole).
fn curve_fate(
curve: &Curve,
region_spans: &[Option<(f32, f32)>],
region_systems: &[Vec<usize>],
clips: &[(f32, f32)],
) -> CurveFate {
let cp = curve.control_points();
let xs = cp.map(|p| p.x.0);
let lo = xs.iter().copied().fold(f32::INFINITY, f32::min);
let hi = xs.iter().copied().fold(f32::NEG_INFINITY, f32::max);
// The owning region: the one whose slot span is nearest (ties to the first).
let mut best: Option<(usize, f32)> = None;
for (r, span) in region_spans.iter().enumerate() {
let Some((rlo, rhi)) = span else { continue };
let distance = interval_distance(lo, hi, (*rlo, *rhi));
if best.is_none_or(|(_, d)| distance < d) {
best = Some((r, distance));
}
}
let Some((region, _)) = best else {
return CurveFate::Rigid(None);
};
// The systems of that region the curve's x-span overlaps.
let overlapped: Vec<usize> = region_systems[region]
.iter()
.copied()
.filter(|&s| lo <= clips[s].1 && hi >= clips[s].0)
.collect();
// The start control point pins which single system the curve rides when it
// does not span a break.
let start_system = || {
region_systems[region].iter().copied().min_by(|&a, &b| {
let da = interval_distance(cp[0].x.0, cp[0].x.0, clips[a]);
let db = interval_distance(cp[0].x.0, cp[0].x.0, clips[b]);
da.total_cmp(&db).then(a.cmp(&b))
})
};
match overlapped.len() {
0 => CurveFate::Rigid(start_system()),
1 => CurveFate::Rigid(Some(overlapped[0])),
_ => {
// A curve spanning a system break is split into per-system
// sub-curves by de Casteljau subdivision at the parameters where it
// crosses each system's content clip edges. This needs an
// x-monotonic curve to invert `x -> t`; a slur is (its control
// points are x-ascending by construction). A non-monotonic curve
// (not produced by the engraver) cannot be honestly split, so it
// rides its start system whole.
if !is_x_monotonic(cp) {
return CurveFate::Rigid(start_system());
}
let segments = overlapped
.into_iter()
.map(|s| {
let (clo, chi) = clips[s];
let x0 = clo.max(cp[0].x.0);
let x1 = chi.min(cp[3].x.0);
let t0 = param_at_x(cp, x0);
let t1 = param_at_x(cp, x1);
(s, sub_cubic(cp, t0, t1))
})
.collect();
CurveFate::Split(segments)
}
}
}
/// Whether a cubic's control points ascend in x (so `x -> t` is invertible by
/// bisection), with a non-trivial x-span.
fn is_x_monotonic(cp: [Point; 4]) -> bool {
cp[0].x.0 <= cp[1].x.0
&& cp[1].x.0 <= cp[2].x.0
&& cp[2].x.0 <= cp[3].x.0
&& cp[3].x.0 > cp[0].x.0
}
/// The parameter `t` at which an x-monotonic cubic's x-coordinate equals `x`
/// (bisection; `x` is clamped to the curve's x-range by the caller).
fn param_at_x(cp: [Point; 4], x: f32) -> f32 {
let cubic_x = |t: f32| {
let u = 1.0 - t;
u * u * u * cp[0].x.0
+ 3.0 * u * u * t * cp[1].x.0
+ 3.0 * u * t * t * cp[2].x.0
+ t * t * t * cp[3].x.0
};
let (mut lo, mut hi) = (0.0_f32, 1.0_f32);
for _ in 0..40 {
let mid = 0.5 * (lo + hi);
if cubic_x(mid) < x {
lo = mid;
} else {
hi = mid;
}
}
0.5 * (lo + hi)
}
/// Linear interpolation between two points.
fn lerp_point(a: Point, b: Point, t: f32) -> Point {
Point::new(a.x.0 + (b.x.0 - a.x.0) * t, a.y.0 + (b.y.0 - a.y.0) * t)
}
/// de Casteljau split of a cubic at `t`: `(left [0, t], right [t, 1])`.
fn split_cubic(cp: [Point; 4], t: f32) -> ([Point; 4], [Point; 4]) {
let a = lerp_point(cp[0], cp[1], t);
let b = lerp_point(cp[1], cp[2], t);
let c = lerp_point(cp[2], cp[3], t);
let d = lerp_point(a, b, t);
let e = lerp_point(b, c, t);
let f = lerp_point(d, e, t);
([cp[0], a, d, f], [f, e, c, cp[3]])
}
/// The sub-cubic of `cp` over the parameter range `[t0, t1]` (two de Casteljau
/// splits: take `[0, t1]`, then within it the `[t0/t1, 1]` tail).
fn sub_cubic(cp: [Point; 4], t0: f32, t1: f32) -> [Point; 4] {
let (left, _) = split_cubic(cp, t1);
let tt = if t1 > f32::EPSILON {
(t0 / t1).clamp(0.0, 1.0)
} else {
0.0
};
let (_, right) = split_cubic(left, tt);
right
}
/// Distance from the span `[lo, hi]` to a clip interval (0 when they overlap).
fn interval_distance(lo: f32, hi: f32, clip: (f32, f32)) -> f32 {
if hi < clip.0 {
clip.0 - hi
} else if lo > clip.1 {
lo - clip.1
} else {
0.0
}
}
/// A stroke translated rigidly by `(dx, dy)`.
fn translated(stroke: &Stroke, dx: f32, dy: f32) -> Stroke {
Stroke {
provenance: stroke.provenance.clone(),
from: Point::new(stroke.from.x.0 + dx, stroke.from.y.0 + dy),
to: Point::new(stroke.to.x.0 + dx, stroke.to.y.0 + dy),
thickness: stroke.thickness,
layer: stroke.layer,
style: stroke.style,
vertical_band: stroke.vertical_band,
}
}
/// A system's placement: rigid (translated to the left margin) unless the
/// system JUSTIFIES — a non-final system of its region, narrower than the
/// content width, with a positive slot span — in which case the horizontal
/// slack is spread linearly so the system's ink fills the content width (its
/// leftmost ink at the left margin, its rightmost at the right margin). A
/// region's last system stays ragged-right, as engraving convention wants; a
/// system already at or over width is not compressed into overlap.
fn justify_system(
plan: &SystemPlan,
ext: &Extent,
base_dx: f32,
dy: f32,
region_slots: &[Vec<SlotInfo>],
region_systems: &[Vec<usize>],
width_limit: f32,
) -> Placement {
let is_last = plan.local + 1 >= region_systems[plan.region].len();
if is_last || !width_limit.is_finite() {
return Placement::rigid(base_dx, dy);
}
let slots = &region_slots[plan.region];
let (Some(&first), Some(&last)) = (plan.slots.first(), plan.slots.last()) else {
return Placement::rigid(base_dx, dy);
};
let x0 = slots[first].x;
let x1 = slots[last].x;
let span = x1 - x0;
let extra = width_limit - (ext.max_x - ext.min_x);
if span <= f32::EPSILON || extra <= f32::EPSILON {
return Placement::rigid(base_dx, dy);
}
// Within [x0, x1]: world_x(x) = x + base_dx + extra·(x x0)/span, i.e.
// a·x + b. Beyond it, `Placement::x` falls back to rigid slope 1.
Placement {
a: 1.0 + extra / span,
b: base_dx - extra * x0 / span,
dy,
x0,
x1,
}
}
/// Places a whole stroke under a system's justification. A per-event component
/// stroke (a stem or ledger) tracks its notehead: both endpoints translate by
/// the owning slot's delta, so it stays attached without stretching its offset.
/// A spanning stroke (a staff line, a volta bracket) stretches with the system:
/// each endpoint maps through the affine.
fn place_stroke(
source: &Stroke,
spaced: &Stroke,
p: Placement,
slot_source_x: &BTreeMap<SpringSlotId, f32>,
glyphs: &[GlyphObject],
) -> Stroke {
if let Some(dx) = crate::component_glyph(source, glyphs)
.and_then(|g| slot_source_x.get(&g.horizontal_slot))
.map(|&sx| p.slot_dx(sx))
{
return translated(spaced, dx, p.dy);
}
Stroke {
provenance: spaced.provenance.clone(),
from: Point::new(p.x(spaced.from.x.0), spaced.from.y.0 + p.dy),
to: Point::new(p.x(spaced.to.x.0), spaced.to.y.0 + p.dy),
thickness: spaced.thickness,
layer: spaced.layer,
style: spaced.style,
vertical_band: spaced.vertical_band,
}
}
/// Accumulated staff-line geometry within one system, for the resolved staff
/// record: the extent of the staff's line segments and the provenance of its
/// bottom line (the segment that anchors the staff in this system).
struct StaffAgg {
min_x: f32,
max_x: f32,
min_y: f32,
max_y: f32,
bottom: (f32, Provenance),
}
/// Folds a world-frame staff-line stroke into its `(system, staff)` aggregate.
fn mark_staff(
marks: &mut BTreeMap<(usize, StaffId), StaffAgg>,
system: usize,
staff: StaffId,
stroke: &Stroke,
) {
let half = (stroke.thickness.0 * 0.5).max(0.0);
let (lo_x, hi_x) = (
stroke.from.x.0.min(stroke.to.x.0),
stroke.from.x.0.max(stroke.to.x.0),
);
let (lo_y, hi_y) = (
stroke.from.y.0.min(stroke.to.y.0) - half,
stroke.from.y.0.max(stroke.to.y.0) + half,
);
marks
.entry((system, staff))
.and_modify(|agg| {
agg.min_x = agg.min_x.min(lo_x);
agg.max_x = agg.max_x.max(hi_x);
agg.min_y = agg.min_y.min(lo_y);
agg.max_y = agg.max_y.max(hi_y);
if lo_y < agg.bottom.0 {
agg.bottom = (lo_y, stroke.provenance.clone());
}
})
.or_insert_with(|| StaffAgg {
min_x: lo_x,
max_x: hi_x,
min_y: lo_y,
max_y: hi_y,
bottom: (lo_y, stroke.provenance.clone()),
});
}
/// Builds one populated [`ResolvedSystem`]: a real world-frame bounding box, a
/// staff record per staff whose lines reach this system (top staff first), and
/// a measure record per measure-start barline column the system carries. What
/// the pipeline does not know is left empty, never fabricated: a staff with no
/// engraved lines yields no staff record, and the final-barline measure (whose
/// start no column marks) yields no measure record.
#[allow(clippy::too_many_arguments)]
fn build_system(
system: usize,
plan: &SystemPlan,
input: &ConstrainedLayoutIR,
region_slots: &[Vec<SlotInfo>],
extents: &[Extent],
placements: &[Placement],
staff_marks: &BTreeMap<(usize, StaffId), StaffAgg>,
primitives: PrimitiveIndices,
) -> ResolvedSystem {
let region = &input.regions[plan.region];
let p = placements[system];
let ext = &extents[system];
let provenance = if plan.local == 0 {
region.provenance.clone()
} else {
// A region's second and later systems are engraver-created objects:
// synthesized from the region under `EngravedBreak`, keyed by the
// region-local system ordinal in its own key namespace.
Provenance::synthesized(
region.provenance.source,
SynthesisKind::EngravedBreak,
SynthesisInstanceKey((KEY_NS_SYSTEM << 64) | plan.local as u128),
region.provenance.dependencies.clone(),
)
};
let bounding_box = Rect {
// Justification stretches the horizontal extent: the box spans the
// system's world-frame ink, which for a justified system is the content
// width.
origin: Point::new(p.x(ext.min_x), ext.min_y + p.dy),
size: Size2D {
width: StaffSpace(p.x(ext.max_x) - p.x(ext.min_x)),
height: StaffSpace(ext.max_y - ext.min_y),
},
};
let mut staves: Vec<ResolvedStaff> = staff_marks
.range((system, StaffId::from_raw(0))..=(system, StaffId::from_raw(u128::MAX)))
.map(|(&(_, staff), agg)| ResolvedStaff {
provenance: agg.bottom.1.clone(),
staff,
bounding_box: Rect {
origin: Point::new(agg.min_x, agg.min_y),
size: Size2D {
width: StaffSpace(agg.max_x - agg.min_x),
height: StaffSpace(agg.max_y - agg.min_y),
},
},
})
.collect();
// Top staff first — the reading order of the system.
staves.sort_by(|a, b| {
let top_a = a.bounding_box.origin.y.0 + a.bounding_box.size.height.0;
let top_b = b.bounding_box.origin.y.0 + b.bounding_box.size.height.0;
top_b.total_cmp(&top_a)
});
// Measures: each measure-start barline column opens a span that runs to the
// next such column in this system, or to the system's content edge.
let slots = &region_slots[plan.region];
let marks: Vec<(usize, usize)> = plan
.slots
.iter()
.filter_map(|&i| slots[i].measure_barline.map(|g| (i, g)))
.collect();
let measures: Vec<ResolvedMeasure> = marks
.iter()
.enumerate()
.filter_map(|(k, &(i, g))| {
let glyph = &input.glyphs[g];
let TypedObjectId::Measure(measure) = glyph.provenance.source else {
return None;
};
let start = slots[i].lo;
let end = marks
.get(k + 1)
.map(|&(next, _)| slots[next].lo)
.unwrap_or(ext.max_x);
Some(ResolvedMeasure {
provenance: glyph.provenance.clone(),
measure,
bounding_box: Rect {
origin: Point::new(p.x(start), ext.min_y + p.dy),
size: Size2D {
width: StaffSpace(p.x(end) - p.x(start)),
height: StaffSpace(ext.max_y - ext.min_y),
},
},
})
})
.collect();
ResolvedSystem {
provenance,
bounding_box,
staves,
measures,
primitives,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_geometry_matches_the_documented_arithmetic() {
// A4 at an 8 mm staff: 1 staff space = 2 mm.
let geometry = PageGeometry::default();
assert_eq!(geometry.size.width.0, 210.0 / 2.0);
assert_eq!(geometry.size.height.0, 297.0 / 2.0);
for margin in [
geometry.margins.top,
geometry.margins.right,
geometry.margins.bottom,
geometry.margins.left,
] {
assert_eq!(margin.0, 15.0 / 2.0);
}
assert_eq!(geometry.content_width(), 90.0);
assert_eq!(geometry.content_height(), 133.5);
}
#[test]
fn pages_stack_downward_with_the_inter_page_gap() {
let geometry = PageGeometry::default();
assert_eq!(page_top_content(0, &geometry), -7.5);
assert_eq!(
page_top_content(1, &geometry),
-(148.5 + INTER_PAGE_GAP) - 7.5
);
}
/// A uniform test measure: one break-candidate barline slot per measure,
/// spanning `[i·10, i·10 + 9]` (each measure ~9 wide, step 10).
fn measure_slot(i: usize) -> SlotInfo {
SlotInfo {
id: SpringSlotId(i as u128 + 1),
x: i as f32 * 10.0,
lo: i as f32 * 10.0,
hi: i as f32 * 10.0 + 9.0,
members: Vec::new(),
barline: true,
final_barline: false,
note: true,
measure_barline: None,
}
}
#[test]
fn optimal_breaks_balances_systems_and_avoids_a_final_widow() {
// Six uniform measures; the content width fits four (4 measures span 39,
// 5 span 49). Greedy first-fit packs [4, 2] — a short final system;
// the optimal search balances to [3, 3] (lower total squared underfill),
// subsuming the old widow rebalance. One automatic break, before the
// fourth measure.
let slots: Vec<SlotInfo> = (0..6).map(measure_slot).collect();
let breaks = optimal_breaks(&slots, &BTreeMap::new(), 42.0);
assert_eq!(
breaks.len(),
1,
"one automatic break → two systems: {breaks:?}"
);
assert!(
breaks.contains(&slots[3].id),
"the break is before the 4th measure (a 3/3 split): {breaks:?}"
);
}
#[test]
fn optimal_breaks_never_spans_a_forced_break() {
// A break requirement at the 2nd measure partitions the DP: the first
// segment is a lone measure [0,1); the optimizer works only within
// [1,6). So measure 0 stands alone even though it would pack with more,
// and no automatic break coincides with the forced one.
let slots: Vec<SlotInfo> = (0..6).map(measure_slot).collect();
let mut reqs: BTreeMap<SpringSlotId, Vec<BreakReq>> = BTreeMap::new();
reqs.insert(
slots[1].id,
vec![BreakReq {
page: false,
hard: true,
}],
);
let breaks = optimal_breaks(&slots, &reqs, 42.0);
assert!(
!breaks.contains(&slots[1].id),
"the forced break is walk_region's, never reported here: {breaks:?}"
);
// The remaining measures [1..6) (5 of them, width 49 > 42) split
// optimally within their segment — every reported break is inside it.
for id in &breaks {
assert!(
slots[2..].iter().any(|s| s.id == *id),
"an automatic break stays inside the post-requirement segment: {id:?}"
);
}
}
#[test]
fn optimal_breaks_is_deterministic_and_empty_when_unbounded() {
let slots: Vec<SlotInfo> = (0..6).map(measure_slot).collect();
let a = optimal_breaks(&slots, &BTreeMap::new(), 42.0);
let b = optimal_breaks(&slots, &BTreeMap::new(), 42.0);
assert_eq!(a, b, "a pure function of the inputs");
assert!(
optimal_breaks(&slots, &BTreeMap::new(), f32::INFINITY).is_empty(),
"an unbounded width wraps nothing"
);
}
#[test]
fn a_content_less_measure_before_a_soft_break_never_overflows() {
// Review Finding 1: a note-less leading measure (M0, clef/key/time only)
// whose barline carries a SOFT break. `walk_region` skips the break (the
// closing system has no content) and the DP, which treated that barline
// as a forced segment boundary, cannot foresee the skip. Without the
// overflow net the optimizer-filled measures after it would absorb M0
// into a MULTI-measure overfull system; the net breaks before the measure
// that would overflow instead. Verify no non-final system is both
// multi-measure and wider than the content width.
use epiphany_core::{RegionId, ReplicaId};
let mk = |i: usize, lo: f32, hi: f32, note: bool| SlotInfo {
id: SpringSlotId(i as u128 + 1),
x: lo,
lo,
hi,
members: Vec::new(),
barline: true,
final_barline: false,
note,
measure_barline: None,
};
// A wide note-less M0; then three narrow measures and two wide ones, so
// the optimizer groups [M1,M2,M3,M4] (its first system, ~39 ≤ 42) and
// [M5] — which, with M0 prepended by the skipped break, would span
// M0..M4 ≈ 60 ≫ 42 without the net.
let slots = vec![
mk(0, 0.0, 20.0, false),
mk(1, 21.0, 26.0, true),
mk(2, 27.0, 32.0, true),
mk(3, 33.0, 38.0, true),
mk(4, 39.0, 60.0, true),
mk(5, 61.0, 82.0, true),
];
let mut reqs: BTreeMap<SpringSlotId, Vec<BreakReq>> = BTreeMap::new();
reqs.insert(
slots[1].id,
vec![BreakReq {
page: false,
hard: false,
}],
); // SOFT
let width_limit = 42.0;
let mut systems = Vec::new();
let mut skipped = Vec::new();
walk_region(
0,
&slots,
&reqs,
&BTreeMap::new(),
TypedObjectId::Region(RegionId::new(ReplicaId(1), 1)),
width_limit,
&mut systems,
&mut skipped,
);
for (s, plan) in systems.iter().enumerate() {
let lo = plan
.slots
.iter()
.map(|&k| slots[k].lo)
.fold(f32::INFINITY, f32::min);
let hi = plan
.slots
.iter()
.map(|&k| slots[k].hi)
.fold(f32::NEG_INFINITY, f32::max);
assert!(
hi - lo <= width_limit + 1e-3 || plan.slots.len() <= 1,
"system {s} spans {} measures at width {} > {width_limit}",
plan.slots.len(),
hi - lo
);
}
assert!(!skipped.is_empty(), "the skipped soft break is recorded");
}
#[test]
fn repeat_signs_keep_measure_records_honest_and_raise_their_system() {
use crate::Engraver;
use epiphany_layout_ir::{to_constrained, to_logical, ConstraintSolver, SolverConfig};
// The repeat fixture draws morphed repeat barlines, a standalone sign,
// the final-barline dot pair, and volta brackets. None of that may
// mint a phantom measure record (a standalone sign and the dot pair
// are repeat-synthesized, not measure barlines) or lose one (a morphed
// barline still marks its measure): both fixtures cast off to the same
// nine records — one per measure-*start* barline column; the final
// measure's barline closes the region and yields none, by convention.
let solve = |score| {
Engraver::default().solve(
&to_constrained(&to_logical(&score)),
&SolverConfig::default(),
)
};
let plain = solve(epiphany_testkit::fixtures::ten_measure_single_staff(
0x000A_11CE,
));
let repeats = solve(epiphany_testkit::fixtures::ten_measure_with_repeats(
0x000A_11CE,
));
let measure_count = |report: &crate::SolveReport| -> usize {
report
.layout
.pages
.iter()
.flat_map(|page| &page.systems)
.map(|system| system.measures.len())
.sum()
};
assert_eq!(measure_count(&plain), 9);
assert_eq!(measure_count(&repeats), 9);
// The volta brackets sit above the staff, so the system carrying them
// is taller than any repeat-free system.
let max_height = |report: &crate::SolveReport| -> f32 {
report
.layout
.pages
.iter()
.flat_map(|page| &page.systems)
.map(|system| system.bounding_box.size.height.0)
.fold(0.0, f32::max)
};
assert!(max_height(&repeats) > max_height(&plain));
}
#[test]
fn the_widow_rebalance_evens_the_final_system() {
use crate::Engraver;
use epiphany_layout_ir::{to_constrained, to_logical, ConstraintSolver, SolverConfig};
// The ten-measure fixture wraps into two systems under the default A4
// geometry. Greedy first-fit alone leaves a two-measure stub final
// system; the widow rebalance evens the split so the final system
// carries a substantial share of the measures — while the system
// *count* is unchanged. (Justification now stretches every non-final
// system to the full content width, so the rebalance's effect shows in
// the MEASURE distribution, not the baked widths — the non-final system
// fills the width regardless.)
let input = to_constrained(&to_logical(
&epiphany_testkit::fixtures::ten_measure_single_staff(0x000A_11CE),
));
let report = Engraver::default().solve(&input, &SolverConfig::default());
let page = &report.layout.pages[0];
assert_eq!(page.systems.len(), 2, "the fixture wraps into two systems");
let first = page.systems[0].measures.len();
let last = page.systems[1].measures.len();
assert!(
last * 2 >= first,
"the rebalanced final system carries a substantial share of the \
measures, not a stub: {last} vs {first}"
);
}
#[test]
fn sub_cubic_reproduces_the_original_curve_on_its_sub_range() {
// de Casteljau correctness: the sub-cubic over [t0, t1], evaluated at
// its own parameter u in [0, 1], equals the original evaluated at
// t0 + u·(t1 - t0). A slur-shaped x-ascending cubic.
let cp = [
Point::new(0.0, 0.0),
Point::new(2.0, 3.0),
Point::new(6.0, 3.0),
Point::new(8.0, 0.0),
];
let eval = |p: [Point; 4], t: f32| -> Point {
let u = 1.0 - t;
Point::new(
u * u * u * p[0].x.0
+ 3.0 * u * u * t * p[1].x.0
+ 3.0 * u * t * t * p[2].x.0
+ t * t * t * p[3].x.0,
u * u * u * p[0].y.0
+ 3.0 * u * u * t * p[1].y.0
+ 3.0 * u * t * t * p[2].y.0
+ t * t * t * p[3].y.0,
)
};
let (t0, t1) = (0.3_f32, 0.75_f32);
let sub = sub_cubic(cp, t0, t1);
for i in 0..=10 {
let u = i as f32 / 10.0;
let on_sub = eval(sub, u);
let on_orig = eval(cp, t0 + u * (t1 - t0));
assert!(
(on_sub.x.0 - on_orig.x.0).abs() < 1e-4 && (on_sub.y.0 - on_orig.y.0).abs() < 1e-4,
"sub-cubic diverges from the original at u={u}: {on_sub:?} vs {on_orig:?}"
);
}
// And `param_at_x` inverts the x-monotonic curve: the point at the found
// parameter has the requested x.
assert!(is_x_monotonic(cp));
let t = param_at_x(cp, 5.0);
assert!((eval(cp, t).x.0 - 5.0).abs() < 1e-3);
}
#[test]
fn a_slur_spanning_a_system_break_splits_into_per_system_sub_curves() {
use crate::Engraver;
use epiphany_core::{Slur, SlurId, SlurKind, SpanStyle, TypedObjectId};
use epiphany_layout_ir::{
to_constrained, to_logical, ConstraintSolver, SolverConfig, SynthesisKind,
};
// A slur over the whole ten-measure score — its endpoints cast into
// different systems (the fixture wraps into two), so the curve spans the
// break.
let mut score = epiphany_testkit::fixtures::ten_measure_single_staff(0x000A_11CE);
let events: Vec<_> = score.canvas.regions[0].staff_instances()[0].voices[0]
.events
.clone();
let slur_id: SlurId = score.identity.mint();
score.cross_cutting.slurs.push(Slur {
id: slur_id,
start_event: events[0],
end_event: events[events.len() - 1],
kind: SlurKind::Legato,
curvature_override: None,
style: SpanStyle::default(),
});
let report = Engraver::default().solve(
&to_constrained(&to_logical(&score)),
&SolverConfig::default(),
);
assert_eq!(report.layout.pages[0].systems.len(), 2, "two systems");
let slur_curves: Vec<_> = report
.layout
.curves
.iter()
.filter(|c| c.provenance.source == TypedObjectId::Slur(slur_id))
.collect();
// The slur split into ≥2 sub-cubics (one per spanned system).
assert!(
slur_curves.len() >= 2,
"a break-spanning slur splits, got {} segment(s)",
slur_curves.len()
);
// Exactly one segment carries the slur's exact provenance (the surjection
// recovers the source once); the rest are synthesized continuations.
let originals = slur_curves
.iter()
.filter(|c| c.provenance.synthesis.is_none())
.count();
assert_eq!(
originals, 1,
"one segment keeps the slur's exact provenance"
);
assert!(slur_curves
.iter()
.filter(|c| c.provenance.synthesis.is_some())
.all(|c| matches!(c.provenance.synthesis, Some(SynthesisKind::Registered(_)))));
// The segments sit in different systems, which casting stacks
// vertically (each system is translated down and restarts x at the left
// margin), so a real split separates them in Y — one curve overhanging
// into the next system would keep a single y-band.
let y_centroids: Vec<f32> = slur_curves
.iter()
.map(|c| (c.p0.y.0 + c.p1.y.0 + c.p2.y.0 + c.p3.y.0) / 4.0)
.collect();
let (lo, hi) = (
y_centroids.iter().copied().fold(f32::INFINITY, f32::min),
y_centroids
.iter()
.copied()
.fold(f32::NEG_INFINITY, f32::max),
);
assert!(
hi - lo > 1.0,
"the segments span distinct system y-bands (a real split), spread {}",
hi - lo
);
}
/// (m1) For each of the layout's three flat arrays, every system's owned
/// index list plus the layout's `unowned` bucket covers `0..len` exactly
/// once — pin 4's total, disjoint partition. The load-bearing invariant,
/// checked directly rather than assumed from construction.
fn assert_total_disjoint_partition(layout: &epiphany_layout_ir::ResolvedLayoutIR) {
let check = |label: &str, len: usize, owned: Vec<&Vec<u32>>, unowned: &[u32]| {
let mut seen = vec![0u8; len];
for &i in owned.iter().flat_map(|v| v.iter()).chain(unowned.iter()) {
let idx = i as usize;
assert!(idx < len, "{label}: index {i} out of range (len {len})");
seen[idx] += 1;
}
for (i, &count) in seen.iter().enumerate() {
assert_eq!(
count, 1,
"{label}: index {i} covered {count} times (want exactly 1)"
);
}
};
let glyph_lists: Vec<&Vec<u32>> = layout.systems().map(|s| &s.primitives.glyphs).collect();
check(
"glyphs",
layout.glyphs.len(),
glyph_lists,
&layout.unowned.glyphs,
);
let stroke_lists: Vec<&Vec<u32>> =
layout.systems().map(|s| &s.primitives.strokes).collect();
check(
"strokes",
layout.strokes.len(),
stroke_lists,
&layout.unowned.strokes,
);
let curve_lists: Vec<&Vec<u32>> = layout.systems().map(|s| &s.primitives.curves).collect();
check(
"curves",
layout.curves.len(),
curve_lists,
&layout.unowned.curves,
);
}
#[test]
fn primitive_ownership_partitions_every_flat_array_totally_and_disjointly() {
use crate::Engraver;
use epiphany_layout_ir::{to_constrained, to_logical, ConstraintSolver, SolverConfig};
// The wrapping ten-measure fixture: real glyphs and strokes, no curves.
let wrapping = Engraver::default().solve(
&to_constrained(&to_logical(
&epiphany_testkit::fixtures::ten_measure_single_staff(0x000A_11CE),
)),
&SolverConfig::default(),
);
assert_eq!(
wrapping.layout.pages[0].systems.len(),
2,
"the fixture wraps into two systems"
);
assert!(!wrapping.layout.glyphs.is_empty());
assert!(!wrapping.layout.strokes.is_empty());
assert_total_disjoint_partition(&wrapping.layout);
// The slurred fixture: also exercises curves, including a
// system-spanning split (G4's own construction).
let slurred = Engraver::default().solve(
&to_constrained(&to_logical(
&epiphany_testkit::fixtures::ten_measure_with_slurs(0),
)),
&SolverConfig::default(),
);
let slurred_systems: usize = slurred.layout.pages.iter().map(|p| p.systems.len()).sum();
assert!(
slurred_systems > 1,
"casting-off wraps the slurred fixture too"
);
assert!(
!slurred.layout.curves.is_empty(),
"the slur produces real curves"
);
assert_total_disjoint_partition(&slurred.layout);
}
#[test]
fn attribution_correctness_matches_the_real_per_system_counts() {
// (m3) The *actual* per-system glyph/stroke counts of the two-system
// fixture — real numbers, not `> 0` — value-asserted directly against
// what casting-off computed.
use crate::Engraver;
use epiphany_layout_ir::{to_constrained, to_logical, ConstraintSolver, SolverConfig};
let report = Engraver::default().solve(
&to_constrained(&to_logical(
&epiphany_testkit::fixtures::ten_measure_single_staff(0x000A_11CE),
)),
&SolverConfig::default(),
);
let systems: Vec<_> = report.layout.systems().collect();
assert_eq!(systems.len(), 2, "two systems");
let glyph_counts: Vec<usize> = systems.iter().map(|s| s.primitives.glyphs.len()).collect();
let stroke_counts: Vec<usize> =
systems.iter().map(|s| s.primitives.strokes.len()).collect();
assert_eq!(
glyph_counts,
vec![26, 25],
"the six/four widow-rebalanced measure split's real per-system glyph counts"
);
assert_eq!(
stroke_counts,
vec![51, 45],
"the six/four widow-rebalanced measure split's real per-system stroke counts"
);
assert_eq!(
glyph_counts[0] + glyph_counts[1],
report.layout.glyphs.len()
);
assert_eq!(
stroke_counts[0] + stroke_counts[1],
report.layout.strokes.len()
);
assert!(
report.layout.unowned.glyphs.is_empty(),
"the whole score is inside the one region this fixture declares"
);
}
#[test]
fn continuation_segments_are_owned_by_the_system_they_split_into() {
// (m5) A slur crossing a system break: its synthesized continuation
// segment is owned by the system it was split INTO, not the source
// segment's system.
use crate::Engraver;
use epiphany_core::{Slur, SlurId, SlurKind, SpanStyle, TypedObjectId};
use epiphany_layout_ir::{to_constrained, to_logical, ConstraintSolver, SolverConfig};
let mut score = epiphany_testkit::fixtures::ten_measure_single_staff(0x000A_11CE);
let events: Vec<_> = score.canvas.regions[0].staff_instances()[0].voices[0]
.events
.clone();
let slur_id: SlurId = score.identity.mint();
score.cross_cutting.slurs.push(Slur {
id: slur_id,
start_event: events[0],
end_event: events[events.len() - 1],
kind: SlurKind::Legato,
curvature_override: None,
style: SpanStyle::default(),
});
let report = Engraver::default().solve(
&to_constrained(&to_logical(&score)),
&SolverConfig::default(),
);
assert_eq!(report.layout.pages[0].systems.len(), 2, "two systems");
let original_index = report
.layout
.curves
.iter()
.position(|c| {
c.provenance.source == TypedObjectId::Slur(slur_id)
&& c.provenance.synthesis.is_none()
})
.expect("one segment keeps the slur's exact provenance");
let continuation_index = report
.layout
.curves
.iter()
.position(|c| {
c.provenance.source == TypedObjectId::Slur(slur_id)
&& c.provenance.synthesis.is_some()
})
.expect("the break-spanning slur splits and synthesizes a continuation");
let owner_of = |index: usize| -> Option<usize> {
report
.layout
.systems()
.position(|s| s.primitives.curves.contains(&(index as u32)))
};
let owner_first = owner_of(original_index).expect("the original segment is owned");
let owner_continuation = owner_of(continuation_index).expect("the continuation is owned");
assert_eq!(
owner_first, 0,
"the original segment starts in the first system"
);
assert_eq!(
owner_continuation, 1,
"the continuation is owned by the system it was split INTO, not the source's"
);
}
}