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