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