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