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