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