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