Skip to main content

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