Skip to main content

acorde_layout/
lib.rs

1mod engine;
2
3pub use engine::compute_layout;
4pub use acorde_core::NoteAddr;
5
6use acorde_core::{HairpinKind, OttavaKind};
7use serde::{Deserialize, Serialize};
8
9/// Configuration for a layout pass.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct LayoutConfig {
12    /// How many visual measure-columns fit on one row/system.
13    pub measures_per_row: usize,
14    /// When `true`, key signatures in [`LayoutResult::concert_key_overrides`] reflect
15    /// concert pitch for transposing instruments.
16    #[serde(default)]
17    pub concert_pitch: bool,
18    /// Override for the number of measures on the first system row only.
19    /// When `None`, falls back to [`measures_per_row`].
20    /// Useful when the first system is shorter due to clef/key/time signature headers.
21    #[serde(default)]
22    pub first_row_measures: Option<usize>,
23}
24
25impl Default for LayoutConfig {
26    fn default() -> Self {
27        Self { measures_per_row: 4, concert_pitch: false, first_row_measures: None }
28    }
29}
30
31/// A resolved span between two note addresses.
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub enum SpanMark {
34    Hairpin { kind: HairpinKind, start: NoteAddr, end: NoteAddr },
35    Ottava  { kind: OttavaKind,  start: NoteAddr, end: NoteAddr },
36    Pedal   {                    start: NoteAddr, end: NoteAddr },
37    Slur      {                    start: NoteAddr, end: NoteAddr },
38    TrillLine {                    start: NoteAddr, end: NoteAddr },
39}
40
41/// One horizontal row (system) of measures.
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct RowLayout {
44    /// Ordered list of physical measure indices that appear on this row.
45    pub measure_indices: Vec<usize>,
46}
47
48/// Concert-pitch key signature override for a specific staff of a transposing instrument.
49///
50/// Populated when `LayoutConfig::concert_pitch` is `true` and the staff has a non-zero
51/// `transpose_semitones`. Renderers use this to draw the correct key signature.
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct ConcertKeyOverride {
54    pub part_index: usize,
55    pub staff_index: usize,
56    /// Key signature in fifths (−7 … +7) adjusted to concert pitch.
57    pub fifths: i8,
58}
59
60/// A group of beamed notes within a single voice of a measure.
61///
62/// `note_indices` are 0-based positions within `score.parts[part].staves[staff]
63/// .measures[measure].voices[voice]`.
64///
65/// Consumers (e.g. VexFlow) use this to explicitly specify beam groupings rather than
66/// relying on automatic detection, which can produce incorrect results for complex rhythms.
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct BeamGroup {
69    pub part: usize,
70    pub staff: usize,
71    pub measure: usize,
72    pub voice: usize,
73    /// Ordered note indices within the voice that form this beam group.
74    pub note_indices: Vec<usize>,
75}
76
77/// A group of notes forming one tuplet bracket within a single voice of one measure.
78///
79/// `note_indices` are 0-based positions within the voice. `actual_notes` and `normal_notes`
80/// mirror [`TupletInfo`] for direct use in VexFlow tuplet rendering.
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct TupletGroup {
83    pub part: usize,
84    pub staff: usize,
85    pub measure: usize,
86    pub voice: usize,
87    /// Ordered note indices within the voice, in order.
88    pub note_indices: Vec<usize>,
89    /// Number of notes in the tuplet (e.g. 3 for a triplet).
90    pub actual_notes: u8,
91    /// Normal beat count displaced (e.g. 2 for a triplet fitting in 2 beats).
92    pub normal_notes: u8,
93}
94
95/// A courtesy (cautionary) accidental to display in parentheses.
96///
97/// Emitted when the same pitch (step + octave) was chromatically altered
98/// in the immediately preceding measure and the renderer needs to remind the
99/// performer that the alteration no longer applies.
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct CourtesyAccidental {
102    pub part: usize,
103    pub staff: usize,
104    pub measure: usize,
105    pub voice: usize,
106    pub note_index: usize,
107    /// Index within `note.pitches` (0 for single-pitch notes, ≥1 for chords).
108    pub pitch_index: usize,
109    /// Accidental to display: 0 = natural, 1 = sharp, -1 = flat, 2 = double-sharp, -2 = double-flat.
110    pub alter: i8,
111}
112
113/// The result of a layout pass.
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct LayoutResult {
116    /// Maps each visual column index to a physical measure index.
117    ///
118    /// Each entry `vis_slots[v]` is the physical measure index for visual column `v`.
119    /// Non-multi-rest measures each contribute exactly one entry. A measure with
120    /// `multi_rest_count = N` contributes `N` consecutive entries all equal to that
121    /// measure's physical index.
122    ///
123    /// Therefore `vis_slots.len()` equals the **total number of visual columns** —
124    /// which is ≥ the number of physical measures (equal when no multi-rests are
125    /// present, and greater when multi-rests expand visual space).
126    ///
127    /// Example: 3 physical measures where measure 0 has `multi_rest_count = Some(4)`
128    /// produces `vis_slots = [0, 0, 0, 0, 1, 2]` — six visual columns.
129    pub vis_slots: Vec<usize>,
130
131    /// Each row in display order; rows cover all parts simultaneously.
132    pub rows: Vec<RowLayout>,
133
134    /// Fully resolved span marks (hairpin / ottava / pedal start+end pairs).
135    pub spans: Vec<SpanMark>,
136
137    /// Per-staff concert-pitch key signature overrides.
138    /// Non-empty only when `LayoutConfig::concert_pitch` is `true` and at least one staff
139    /// has a non-zero `transpose_semitones`.
140    #[serde(default)]
141    pub concert_key_overrides: Vec<ConcertKeyOverride>,
142
143    /// Beam groups across all parts, staves, measures, and voices.
144    ///
145    /// Derived from `BeamState` flags on individual notes. Each group contains at least
146    /// two note indices. Groups are ordered by (part, staff, measure, voice).
147    #[serde(default)]
148    pub beam_groups: Vec<BeamGroup>,
149
150    /// Tuplet groups across all parts, staves, measures, and voices.
151    ///
152    /// Each group represents one tuplet bracket. Notes in a group share the same
153    /// `TupletInfo`. Groups are ordered by (part, staff, measure, voice).
154    #[serde(default)]
155    pub tuplet_groups: Vec<TupletGroup>,
156
157    /// Courtesy (cautionary) accidentals across all parts, staves, measures, and voices.
158    ///
159    /// A courtesy accidental is emitted when the same pitch (step + octave) was chromatically
160    /// altered in the immediately preceding measure, reminding the performer the alteration
161    /// no longer applies. Ordered by (part, staff, measure, voice, note_index, pitch_index).
162    #[serde(default)]
163    pub courtesy_accidentals: Vec<CourtesyAccidental>,
164}