Skip to main content

acorde_layout/
lib.rs

1//! Logical, pixel-free score layout engine for [`acorde-core`](https://docs.rs/acorde-core)
2//! scores — computes row breaks, multi-rest collapsing, beam groups, tuplet groups, courtesy
3//! accidentals, and span (hairpin/ottava/pedal/slur) resolution for score renderers (e.g. VexFlow).
4
5mod engine;
6
7pub use engine::compute_layout;
8pub use acorde_core::NoteAddr;
9
10use acorde_core::{HairpinKind, OttavaKind};
11use serde::{Deserialize, Serialize};
12
13/// Configuration for a layout pass.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct LayoutConfig {
16    /// How many visual measure-columns fit on one row/system.
17    pub measures_per_row: usize,
18    /// When `true`, key signatures in [`LayoutResult::concert_key_overrides`] reflect
19    /// concert pitch for transposing instruments.
20    #[serde(default)]
21    pub concert_pitch: bool,
22    /// Override for the number of measures on the first system row only.
23    /// When `None`, falls back to [`measures_per_row`].
24    /// Useful when the first system is shorter due to clef/key/time signature headers.
25    #[serde(default)]
26    pub first_row_measures: Option<usize>,
27}
28
29impl Default for LayoutConfig {
30    fn default() -> Self {
31        Self { measures_per_row: 4, concert_pitch: false, first_row_measures: None }
32    }
33}
34
35/// A resolved span between two note addresses.
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub enum SpanMark {
38    Hairpin { kind: HairpinKind, start: NoteAddr, end: NoteAddr },
39    Ottava  { kind: OttavaKind,  start: NoteAddr, end: NoteAddr },
40    Pedal   {                    start: NoteAddr, end: NoteAddr },
41    Slur      {                    start: NoteAddr, end: NoteAddr },
42    TrillLine {                    start: NoteAddr, end: NoteAddr },
43}
44
45/// One horizontal row (system) of measures.
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct RowLayout {
48    /// Ordered list of physical measure indices that appear on this row.
49    pub measure_indices: Vec<usize>,
50}
51
52/// Concert-pitch key signature override for a specific staff of a transposing instrument.
53///
54/// Populated when `LayoutConfig::concert_pitch` is `true` and the staff has a non-zero
55/// `transpose_semitones`. Renderers use this to draw the correct key signature.
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct ConcertKeyOverride {
58    pub part_index: usize,
59    pub staff_index: usize,
60    /// Key signature in fifths (−7 … +7) adjusted to concert pitch.
61    pub fifths: i8,
62}
63
64/// A group of beamed notes within a single voice of a measure.
65///
66/// `note_indices` are 0-based positions within `score.parts[part].staves[staff]
67/// .measures[measure].voices[voice]`.
68///
69/// Consumers (e.g. VexFlow) use this to explicitly specify beam groupings rather than
70/// relying on automatic detection, which can produce incorrect results for complex rhythms.
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct BeamGroup {
73    pub part: usize,
74    pub staff: usize,
75    pub measure: usize,
76    pub voice: usize,
77    /// Ordered note indices within the voice that form this beam group.
78    pub note_indices: Vec<usize>,
79}
80
81/// A group of notes forming one tuplet bracket within a single voice of one measure.
82///
83/// `note_indices` are 0-based positions within the voice. `actual_notes` and `normal_notes`
84/// mirror [`TupletInfo`] for direct use in VexFlow tuplet rendering.
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct TupletGroup {
87    pub part: usize,
88    pub staff: usize,
89    pub measure: usize,
90    pub voice: usize,
91    /// Ordered note indices within the voice, in order.
92    pub note_indices: Vec<usize>,
93    /// Number of notes in the tuplet (e.g. 3 for a triplet).
94    pub actual_notes: u8,
95    /// Normal beat count displaced (e.g. 2 for a triplet fitting in 2 beats).
96    pub normal_notes: u8,
97}
98
99/// A courtesy (cautionary) accidental to display in parentheses.
100///
101/// Emitted when the same pitch (step + octave) was chromatically altered
102/// in the immediately preceding measure and the renderer needs to remind the
103/// performer that the alteration no longer applies.
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct CourtesyAccidental {
106    pub part: usize,
107    pub staff: usize,
108    pub measure: usize,
109    pub voice: usize,
110    pub note_index: usize,
111    /// Index within `note.pitches` (0 for single-pitch notes, ≥1 for chords).
112    pub pitch_index: usize,
113    /// Accidental to display: 0 = natural, 1 = sharp, -1 = flat, 2 = double-sharp, -2 = double-flat.
114    pub alter: i8,
115}
116
117/// The result of a layout pass.
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct LayoutResult {
120    /// Maps each visual column index to a physical measure index.
121    ///
122    /// Each entry `vis_slots[v]` is the physical measure index for visual column `v`.
123    /// Non-multi-rest measures each contribute exactly one entry. A measure with
124    /// `multi_rest_count = N` contributes `N` consecutive entries all equal to that
125    /// measure's physical index.
126    ///
127    /// Therefore `vis_slots.len()` equals the **total number of visual columns** —
128    /// which is ≥ the number of physical measures (equal when no multi-rests are
129    /// present, and greater when multi-rests expand visual space).
130    ///
131    /// Example: 3 physical measures where measure 0 has `multi_rest_count = Some(4)`
132    /// produces `vis_slots = [0, 0, 0, 0, 1, 2]` — six visual columns.
133    pub vis_slots: Vec<usize>,
134
135    /// Each row in display order; rows cover all parts simultaneously.
136    pub rows: Vec<RowLayout>,
137
138    /// Fully resolved span marks (hairpin / ottava / pedal start+end pairs).
139    pub spans: Vec<SpanMark>,
140
141    /// Per-staff concert-pitch key signature overrides.
142    /// Non-empty only when `LayoutConfig::concert_pitch` is `true` and at least one staff
143    /// has a non-zero `transpose_semitones`.
144    #[serde(default)]
145    pub concert_key_overrides: Vec<ConcertKeyOverride>,
146
147    /// Beam groups across all parts, staves, measures, and voices.
148    ///
149    /// Derived from `BeamState` flags on individual notes. Each group contains at least
150    /// two note indices. Groups are ordered by (part, staff, measure, voice).
151    #[serde(default)]
152    pub beam_groups: Vec<BeamGroup>,
153
154    /// Tuplet groups across all parts, staves, measures, and voices.
155    ///
156    /// Each group represents one tuplet bracket. Notes in a group share the same
157    /// `TupletInfo`. Groups are ordered by (part, staff, measure, voice).
158    #[serde(default)]
159    pub tuplet_groups: Vec<TupletGroup>,
160
161    /// Courtesy (cautionary) accidentals across all parts, staves, measures, and voices.
162    ///
163    /// A courtesy accidental is emitted when the same pitch (step + octave) was chromatically
164    /// altered in the immediately preceding measure, reminding the performer the alteration
165    /// no longer applies. Ordered by (part, staff, measure, voice, note_index, pitch_index).
166    #[serde(default)]
167    pub courtesy_accidentals: Vec<CourtesyAccidental>,
168}