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