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 acorde_core::NoteAddr;
8pub use engine::compute_layout;
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 {
32            measures_per_row: 4,
33            concert_pitch: false,
34            first_row_measures: None,
35        }
36    }
37}
38
39/// A resolved span between two note addresses.
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub enum SpanMark {
42    Hairpin {
43        kind: HairpinKind,
44        start: NoteAddr,
45        end: NoteAddr,
46    },
47    Ottava {
48        kind: OttavaKind,
49        start: NoteAddr,
50        end: NoteAddr,
51    },
52    Pedal {
53        start: NoteAddr,
54        end: NoteAddr,
55    },
56    Slur {
57        start: NoteAddr,
58        end: NoteAddr,
59    },
60    TrillLine {
61        start: NoteAddr,
62        end: NoteAddr,
63    },
64}
65
66/// One horizontal row (system) of measures.
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct RowLayout {
69    /// Ordered list of physical measure indices that appear on this row.
70    pub measure_indices: Vec<usize>,
71}
72
73/// Concert-pitch key signature override for a specific staff of a transposing instrument.
74///
75/// Populated when `LayoutConfig::concert_pitch` is `true` and the staff has a non-zero
76/// `transpose_semitones`. Renderers use this to draw the correct key signature.
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct ConcertKeyOverride {
79    pub part_index: usize,
80    pub staff_index: usize,
81    /// Key signature in fifths (−7 … +7) adjusted to concert pitch.
82    pub fifths: i8,
83}
84
85/// A group of beamed notes within a single voice of a measure.
86///
87/// `note_indices` are 0-based positions within `score.parts[part].staves[staff]
88/// .measures[measure].voices[voice]`.
89///
90/// Consumers (e.g. VexFlow) use this to explicitly specify beam groupings rather than
91/// relying on automatic detection, which can produce incorrect results for complex rhythms.
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct BeamGroup {
94    pub part: usize,
95    pub staff: usize,
96    pub measure: usize,
97    pub voice: usize,
98    /// Ordered note indices within the voice that form this beam group.
99    pub note_indices: Vec<usize>,
100}
101
102/// A group of notes forming one tuplet bracket within a single voice of one measure.
103///
104/// `note_indices` are 0-based positions within the voice. `actual_notes` and `normal_notes`
105/// mirror [`TupletInfo`] for direct use in VexFlow tuplet rendering.
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct TupletGroup {
108    pub part: usize,
109    pub staff: usize,
110    pub measure: usize,
111    pub voice: usize,
112    /// Ordered note indices within the voice, in order.
113    pub note_indices: Vec<usize>,
114    /// Number of notes in the tuplet (e.g. 3 for a triplet).
115    pub actual_notes: u8,
116    /// Normal beat count displaced (e.g. 2 for a triplet fitting in 2 beats).
117    pub normal_notes: u8,
118}
119
120/// A courtesy (cautionary) accidental to display in parentheses.
121///
122/// Emitted when the same pitch (step + octave) was chromatically altered
123/// in the immediately preceding measure and the renderer needs to remind the
124/// performer that the alteration no longer applies.
125#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct CourtesyAccidental {
127    pub part: usize,
128    pub staff: usize,
129    pub measure: usize,
130    pub voice: usize,
131    pub note_index: usize,
132    /// Index within `note.pitches` (0 for single-pitch notes, ≥1 for chords).
133    pub pitch_index: usize,
134    /// Accidental to display: 0 = natural, 1 = sharp, -1 = flat, 2 = double-sharp, -2 = double-flat.
135    pub alter: i8,
136}
137
138/// A mandatory (non-courtesy) accidental that must be drawn beside a notehead.
139///
140/// Emitted the first time, within a measure, that a pitch (step + octave, scoped across
141/// all voices of a staff — accidentals do not carry across barlines) differs from the
142/// alteration established by the key signature or by an earlier note of the same
143/// step+octave earlier in the same measure. This is standard music engraving, not a
144/// rendering choice, so it is computed here rather than in a renderer.
145///
146/// When both an [`AccidentalMark`] and a [`CourtesyAccidental`] exist for the same
147/// `(part, staff, measure, voice, note_index, pitch_index)`, the mandatory mark takes
148/// precedence: renderers should draw it plain and suppress the courtesy parentheses.
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct AccidentalMark {
151    pub part: usize,
152    pub staff: usize,
153    pub measure: usize,
154    pub voice: usize,
155    pub note_index: usize,
156    /// Index within `note.pitches` (0 for single-pitch notes, ≥1 for chords).
157    pub pitch_index: usize,
158    /// Accidental to display: 0 = natural, 1 = sharp, -1 = flat, 2 = double-sharp, -2 = double-flat.
159    pub alter: i8,
160}
161
162/// The result of a layout pass.
163#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct LayoutResult {
165    /// Maps each visual column index to a physical measure index.
166    ///
167    /// Each entry `vis_slots[v]` is the physical measure index for visual column `v`.
168    /// Non-multi-rest measures each contribute exactly one entry. A measure with
169    /// `multi_rest_count = N` contributes `N` consecutive entries all equal to that
170    /// measure's physical index.
171    ///
172    /// Therefore `vis_slots.len()` equals the **total number of visual columns** —
173    /// which is ≥ the number of physical measures (equal when no multi-rests are
174    /// present, and greater when multi-rests expand visual space).
175    ///
176    /// Example: 3 physical measures where measure 0 has `multi_rest_count = Some(4)`
177    /// produces `vis_slots = [0, 0, 0, 0, 1, 2]` — six visual columns.
178    pub vis_slots: Vec<usize>,
179
180    /// Each row in display order; rows cover all parts simultaneously.
181    pub rows: Vec<RowLayout>,
182
183    /// Fully resolved span marks (hairpin / ottava / pedal start+end pairs).
184    pub spans: Vec<SpanMark>,
185
186    /// Per-staff concert-pitch key signature overrides.
187    /// Non-empty only when `LayoutConfig::concert_pitch` is `true` and at least one staff
188    /// has a non-zero `transpose_semitones`.
189    #[serde(default)]
190    pub concert_key_overrides: Vec<ConcertKeyOverride>,
191
192    /// Beam groups across all parts, staves, measures, and voices.
193    ///
194    /// Derived from `BeamState` flags on individual notes. Each group contains at least
195    /// two note indices. Groups are ordered by (part, staff, measure, voice).
196    #[serde(default)]
197    pub beam_groups: Vec<BeamGroup>,
198
199    /// Tuplet groups across all parts, staves, measures, and voices.
200    ///
201    /// Each group represents one tuplet bracket. Notes in a group share the same
202    /// `TupletInfo`. Groups are ordered by (part, staff, measure, voice).
203    #[serde(default)]
204    pub tuplet_groups: Vec<TupletGroup>,
205
206    /// Courtesy (cautionary) accidentals across all parts, staves, measures, and voices.
207    ///
208    /// A courtesy accidental is emitted when the same pitch (step + octave) was chromatically
209    /// altered in the immediately preceding measure, reminding the performer the alteration
210    /// no longer applies. Ordered by (part, staff, measure, voice, note_index, pitch_index).
211    #[serde(default)]
212    pub courtesy_accidentals: Vec<CourtesyAccidental>,
213
214    /// Mandatory (non-courtesy) accidentals across all parts, staves, measures, and voices.
215    ///
216    /// Emitted for the first chromatic alteration of a step+octave within a measure.
217    /// See [`AccidentalMark`] for the precedence rule against `courtesy_accidentals`.
218    #[serde(default)]
219    pub accidentals: Vec<AccidentalMark>,
220}