//! 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 //! Knuth–Plass-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 *n−1*'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, /// 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, /// 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, /// The populated page tree (empty when the input declares no regions). pub pages: Vec, /// Break decisions this pass made (chosen breaks in reading order, then /// the skipped-soft `IrOverride` records in walk order). pub decisions: Vec, /// Slots at which the final layout breaks: the first slot of every system. pub system_start_slots: BTreeSet, /// Slots at which a page begins: the first slot of each page's first system. pub page_start_slots: BTreeSet, /// 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>, /// 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>, /// The system each baked curve landed in, parallel to `curves`. pub curve_system: Vec>, /// 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, /// 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, /// 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, } /// 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, boundary: Option, /// 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), /// 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), /// 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( ®ion_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 = 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 = BTreeMap::new(); for (r, region) in input.regions.iter().enumerate() { for id in ®ion.glyphs { region_of_glyph.entry(*id).or_insert(r); } } let mut region_slots: Vec> = (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 = region_slots .iter() .flatten() .map(|info| (info.id, info.x)) .collect(); // ---- Break requirements ---------------------------------------------- let mut reqs: BTreeMap> = 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 = Vec::new(); let mut skipped: Vec = 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 = 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> = 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![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 = input .strokes .iter() .zip(spaced_strokes) .map(|(stroke, spaced)| { stroke_fate( stroke, spaced, input, &system_of_slot, ®ion_spans, ®ion_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 = spaced_curves .iter() .map(|curve| curve_fate(curve, ®ion_spans, ®ion_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 = 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> = input .glyphs .iter() .map(|g| staff_of(g.vertical_band)) .collect(); let stroke_staff_of: Vec> = input .strokes .iter() .map(|s| staff_of(s.vertical_band)) .collect(); let curve_staff_of: Vec> = 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 = 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, 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 ®ion_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 = 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 = Vec::with_capacity(systems.len()); let mut page_systems: Vec> = 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, ®ion_slots, ®ion_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| -> 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, Vec>) = 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 = Vec::with_capacity(spaced_strokes.len()); let mut continuations: Vec = 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> = Vec::with_capacity(spaced_strokes.len()); let mut continuation_system: Vec> = 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 = Vec::with_capacity(spaced_curves.len()); let mut curve_continuations: Vec = Vec::new(); let mut curve_system: Vec> = Vec::with_capacity(spaced_curves.len()); let mut curve_continuation_system: Vec> = 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 = (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 = systems .iter() .enumerate() .map(|(s, plan)| { build_system( s, plan, input, ®ion_slots, &extents, &placements, &staff_marks, owned[s].clone(), ) }) .collect(); let mut resolved_systems: Vec> = resolved_systems.into_iter().map(Some).collect(); let pages: Vec = 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 /// (Knuth–Plass-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>, width_limit: f32, ) -> BTreeSet { 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 = vec![0]; let mut forced: Vec = 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 = 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>, origins: &BTreeMap<(u128, bool), EngravingOverrideId>, region_source: TypedObjectId, width_limit: f32, systems: &mut Vec, skipped: &mut Vec, ) { // 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 = Vec::new(); let mut has_note = false; let mut current_lo = f32::INFINITY; let mut open_boundary: Option = 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, region_spans: &[Option<(f32, f32)>], region_systems: &[Vec], 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 = 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], 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 = 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], region_systems: &[Vec], 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 = ®ion_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, 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], 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 = 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 = ®ion_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 = 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 = (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 = (0..6).map(measure_slot).collect(); let mut reqs: BTreeMap> = 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 = (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> = 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 = 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>, 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> = layout.systems().map(|s| &s.primitives.glyphs).collect(); check( "glyphs", layout.glyphs.len(), glyph_lists, &layout.unowned.glyphs, ); let stroke_lists: Vec<&Vec> = layout.systems().map(|s| &s.primitives.strokes).collect(); check( "strokes", layout.strokes.len(), stroke_lists, &layout.unowned.strokes, ); let curve_lists: Vec<&Vec> = 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 = systems.iter().map(|s| s.primitives.glyphs.len()).collect(); let stroke_counts: Vec = 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 { 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" ); } }