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