204 lines
8.3 KiB
Rust
204 lines
8.3 KiB
Rust
//! The vertical-band model (Chapter 7 §"Vertical Bands").
|
|
//!
|
|
//! "Vertical layout uses the same spring model. A *vertical band* is a
|
|
//! horizontal slice of the canvas (typically a staff or an inter-staff gap)
|
|
//! with its own spring parameters." The solver resolves vertical positions by
|
|
//! treating bands as springs, exactly as it treats horizontal time slots
|
|
//! (Chapter 7 §"Spring-based spacing").
|
|
//!
|
|
//! All dimensions are staff-space `f32` per the Chapter 7 §7.2 IR-coordinate
|
|
//! rule: the elastic spring parameters (`stretch_factor`, `compress_factor`) and
|
|
//! the band heights ([`StaffSpace`]) alike are layout-engine inputs, quantized
|
|
//! only if and when a band extent is serialized into canonical output.
|
|
|
|
use epiphany_core::{StaffId, TypedObjectId};
|
|
use epiphany_determinism::{DomainTag, Preimage};
|
|
|
|
use crate::constrained::GlyphObjectId;
|
|
use crate::provenance::{stable_layout_id, LayoutObjectId};
|
|
use crate::spatial::StaffSpace;
|
|
|
|
/// A stable identifier for a vertical band (Chapter 7: `VerticalBandId`).
|
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
|
|
pub struct VerticalBandId(pub u128);
|
|
|
|
/// Derives an inter-staff gap id in its own type-prefixed preimage namespace.
|
|
/// It cannot alias the region's margin id by arithmetic construction.
|
|
pub fn inter_staff_gap_id(region: LayoutObjectId, gap_index: usize) -> VerticalBandId {
|
|
let mut preimage = Preimage::new(DomainTag::CONFLICT);
|
|
preimage.push_bytes(b"vertical-band/inter-staff-gap");
|
|
preimage.push_u64_le((region.0 >> 64) as u64);
|
|
preimage.push_u64_le(region.0 as u64);
|
|
preimage.push_u64_le(gap_index as u64);
|
|
VerticalBandId(preimage.finish_trunc128())
|
|
}
|
|
|
|
/// What a vertical band represents (Chapter 7: `VerticalBandKind`).
|
|
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
|
pub enum VerticalBandKind {
|
|
/// A staff's own band.
|
|
Staff(StaffId),
|
|
/// The gap between two staves of a system.
|
|
InterStaffGap,
|
|
/// The gap between two systems on a page.
|
|
InterSystemGap,
|
|
/// A page-margin band.
|
|
MarginBand,
|
|
}
|
|
|
|
/// A vertical band: a horizontal slice of the canvas with its own spring
|
|
/// parameters (Chapter 7 §"Vertical Bands"). The constraint solver consumes
|
|
/// bands uniformly, just like horizontal spring slots.
|
|
#[derive(Clone, PartialEq, Debug)]
|
|
pub struct VerticalBand {
|
|
pub id: VerticalBandId,
|
|
pub kind: VerticalBandKind,
|
|
/// The smallest the band may compress to, in staff spaces.
|
|
pub min_height: StaffSpace,
|
|
/// The band's natural height under unconstrained spacing, in staff spaces.
|
|
pub preferred_height: StaffSpace,
|
|
/// The largest the band may stretch to, if bounded, in staff spaces.
|
|
pub max_height: Option<StaffSpace>,
|
|
/// How readily the band stretches when more height is available.
|
|
pub stretch_factor: f32,
|
|
/// How readily the band compresses when less height is available.
|
|
pub compress_factor: f32,
|
|
/// The glyphs belonging to this band.
|
|
pub members: Vec<GlyphObjectId>,
|
|
}
|
|
|
|
impl VerticalBand {
|
|
/// A default staff band, with the band id derived from the staff source
|
|
/// alone. Use this for a staff with a single manifestation; for a staff
|
|
/// manifested in a specific region, use [`VerticalBand::staff_manifestation`]
|
|
/// so two manifestations get two distinct band ids.
|
|
pub fn staff(staff: StaffId, members: Vec<GlyphObjectId>) -> Self {
|
|
Self::with_id(
|
|
VerticalBandId(stable_layout_id(&TypedObjectId::Staff(staff)).0),
|
|
staff,
|
|
members,
|
|
)
|
|
}
|
|
|
|
/// A staff band for a specific manifestation, identified by the staff
|
|
/// layout object's stable id (`(staff, region)` — see
|
|
/// [`crate::Provenance::manifested`]). Two manifestations of one staff thus
|
|
/// get two distinct band ids, mirroring the two distinct staff layout
|
|
/// objects.
|
|
pub fn staff_manifestation(
|
|
manifestation: LayoutObjectId,
|
|
staff: StaffId,
|
|
members: Vec<GlyphObjectId>,
|
|
) -> Self {
|
|
Self::with_id(VerticalBandId(manifestation.0), staff, members)
|
|
}
|
|
|
|
/// A margin band (for region content with no staff, e.g. a free-graphic
|
|
/// region), identified by `id` (typically the region's layout id).
|
|
pub fn margin(id: LayoutObjectId, members: Vec<GlyphObjectId>) -> Self {
|
|
let four_spaces = StaffSpace(4.0);
|
|
VerticalBand {
|
|
id: VerticalBandId(id.0),
|
|
kind: VerticalBandKind::MarginBand,
|
|
min_height: four_spaces,
|
|
preferred_height: four_spaces,
|
|
max_height: None,
|
|
stretch_factor: 1.0,
|
|
compress_factor: 1.0,
|
|
members,
|
|
}
|
|
}
|
|
|
|
/// An inter-staff gap band: the empty (member-less) spacing region between
|
|
/// two staves of a system (Chapter 7 §"Vertical Bands": `InterStaffGap`). Its
|
|
/// height is a spring the solver resolves; it carries no glyphs.
|
|
///
|
|
/// **Its height is an INK CLEARANCE**, not a distance between staff lines:
|
|
/// the vertical separation between the two staves' outermost *content* —
|
|
/// ledger lines, stems, slurs, everything. That is the unit the Quality
|
|
/// Metric Catalog's `vertical_density_penalty` measures against
|
|
/// (`req:qmc:vertical`, "the adjacent content extents the band separates"),
|
|
/// so the solve and the metric read one number.
|
|
///
|
|
/// `preferred` is a staff height plus a space: two staves whose ink is that
|
|
/// far apart read as separate systems of lines without wasting the page.
|
|
/// Plain ledgered content then lands at a staff *pitch* of about 10.6 staff
|
|
/// spaces — near conventional two-staff spacing — while ledgered or slurred
|
|
/// content pushes the staves further apart on its own. `min` is the hardest
|
|
/// squeeze a compressing solve may apply.
|
|
///
|
|
/// (The earlier `preferred = 2.0` was a placeholder reconciled with nothing:
|
|
/// it is neither the 8.0 staff-box gap the constrained stage's fixed
|
|
/// `SYSTEM_STAFF_PITCH` produces, nor the ~6.4 ink clearance that stacking
|
|
/// leaves for plain content. Realizing it would have crushed a relaxed
|
|
/// system to a pitch of ~7.6.)
|
|
pub fn inter_staff_gap(id: VerticalBandId) -> Self {
|
|
VerticalBand {
|
|
id,
|
|
kind: VerticalBandKind::InterStaffGap,
|
|
min_height: StaffSpace(2.0),
|
|
preferred_height: StaffSpace(5.0),
|
|
max_height: None,
|
|
stretch_factor: 1.0,
|
|
compress_factor: 1.0,
|
|
members: Vec::new(),
|
|
}
|
|
}
|
|
|
|
/// An inter-system gap band used during page casting-off.
|
|
pub fn inter_system_gap(id: VerticalBandId) -> Self {
|
|
VerticalBand {
|
|
id,
|
|
kind: VerticalBandKind::InterSystemGap,
|
|
min_height: StaffSpace(2.0),
|
|
preferred_height: StaffSpace(4.0),
|
|
max_height: None,
|
|
stretch_factor: 1.0,
|
|
compress_factor: 1.0,
|
|
members: Vec::new(),
|
|
}
|
|
}
|
|
|
|
fn with_id(id: VerticalBandId, staff: StaffId, members: Vec<GlyphObjectId>) -> Self {
|
|
// 4 staff spaces between the outer lines of a 5-line staff.
|
|
let four_spaces = StaffSpace(4.0);
|
|
VerticalBand {
|
|
id,
|
|
kind: VerticalBandKind::Staff(staff),
|
|
min_height: four_spaces,
|
|
preferred_height: four_spaces,
|
|
max_height: None,
|
|
stretch_factor: 1.0,
|
|
compress_factor: 1.0,
|
|
members,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn staff_band_is_stable_and_carries_members() {
|
|
let staff = StaffId::from_raw(0x42);
|
|
let members = vec![GlyphObjectId(1), GlyphObjectId(2)];
|
|
let a = VerticalBand::staff(staff, members.clone());
|
|
let b = VerticalBand::staff(staff, members);
|
|
assert_eq!(a.id, b.id);
|
|
assert_eq!(a.kind, VerticalBandKind::Staff(staff));
|
|
assert_eq!(a.members.len(), 2);
|
|
// A different staff → a different band id.
|
|
let other = VerticalBand::staff(StaffId::from_raw(0x43), vec![]);
|
|
assert_ne!(a.id, other.id);
|
|
}
|
|
|
|
#[test]
|
|
fn inter_staff_gap_ids_are_stable_and_separate_from_the_region() {
|
|
let region = LayoutObjectId(42);
|
|
assert_eq!(inter_staff_gap_id(region, 1), inter_staff_gap_id(region, 1));
|
|
assert_ne!(inter_staff_gap_id(region, 1), inter_staff_gap_id(region, 2));
|
|
assert_ne!(inter_staff_gap_id(region, 1), VerticalBandId(region.0));
|
|
}
|
|
}
|